From 7c56c57a6155e37fa627890a0eb21b9d46e81dcd Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Sat, 10 Oct 2026 17:42:13 +0300 Subject: [PATCH 01/15] Add editorconfig for consistent formatting --- .editorconfig | 17 ++++++++++++++ .../{applyTranslator.hpp => language.hpp} | 0 .../Informatics/apt_list.txt | 21 ----------------- .../Informatics/pacman_list.txt | 23 ------------------- .../{applyTranslator.cpp => language.cpp} | 0 5 files changed, 17 insertions(+), 44 deletions(-) create mode 100644 .editorconfig rename include/utils/{applyTranslator.hpp => language.hpp} (100%) delete mode 100644 resources/lists/University of Western Macedonia/Informatics/apt_list.txt delete mode 100644 resources/lists/University of Western Macedonia/Informatics/pacman_list.txt rename src/utils/{applyTranslator.cpp => language.cpp} (100%) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bd9ac79 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{cpp,hpp,txt,cmake,json,qrc,ui,ts,yml}] +indent_style = space +indent_size = 4 + +[*.{yml,qrc,ui}] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/include/utils/applyTranslator.hpp b/include/utils/language.hpp similarity index 100% rename from include/utils/applyTranslator.hpp rename to include/utils/language.hpp diff --git a/resources/lists/University of Western Macedonia/Informatics/apt_list.txt b/resources/lists/University of Western Macedonia/Informatics/apt_list.txt deleted file mode 100644 index bf50fa4..0000000 --- a/resources/lists/University of Western Macedonia/Informatics/apt_list.txt +++ /dev/null @@ -1,21 +0,0 @@ -bash -wget -gnupg -arduino -bison -build-essential -dbeaver-ce -default-jdk -flex -gcc -ghdl -gnuplot -gtkwave -kicad -libarmadillo-dev -mariadb-server -nodejs -npm -octave -simulide -weka diff --git a/resources/lists/University of Western Macedonia/Informatics/pacman_list.txt b/resources/lists/University of Western Macedonia/Informatics/pacman_list.txt deleted file mode 100644 index 45c01f1..0000000 --- a/resources/lists/University of Western Macedonia/Informatics/pacman_list.txt +++ /dev/null @@ -1,23 +0,0 @@ -bash -wget -gnupg -arduino -bison -base-devel -dbeaver -jdk-openjdk -flex -freecad -gcc -ghdl -gnuplot -gtkwave -kicad -armadillo -mariadb -nodejs -npm -octave -simulide -weka -wireshark-qt diff --git a/src/utils/applyTranslator.cpp b/src/utils/language.cpp similarity index 100% rename from src/utils/applyTranslator.cpp rename to src/utils/language.cpp From 749a96151e9ef5600b4ed1fe547837e528c33d9c Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Sat, 10 Oct 2026 17:51:40 +0300 Subject: [PATCH 02/15] Remove stale binary name from gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8998933..f830c34 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ .vscode/ # Executable -UniBackpack *.deb # Qt files From 15a1c089913b9140d9e9f15909723a3c9db4e3e9 Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Sat, 10 Oct 2026 22:16:05 +0300 Subject: [PATCH 03/15] Split package manager logic into backends and a core library --- CMakeLists.txt | 62 ++- include/Downloader.hpp | 94 ++++- include/MainWindow.hpp | 116 ++++-- include/PackageManager.hpp | 137 +++++++ include/utils/language.hpp | 55 ++- include/utils/loadUniversities.hpp | 62 ++- include/utils/packageList.hpp | 55 +++ .../Informatics/packages.json | 25 ++ resources/resources.qrc | 4 - resources/universities.json | 18 +- src/Downloader.cpp | 376 ++++++------------ src/MainWindow.cpp | 341 +++++++++------- src/PackageManager.cpp | 160 ++++++++ src/main.cpp | 30 +- src/utils/language.cpp | 60 ++- src/utils/loadUniversities.cpp | 89 ++--- src/utils/packageList.cpp | 54 +++ translations/unibackpack_el.ts | 160 +++++--- translations/unibackpack_en.ts | 145 +++++-- ui/MainWindow.ui | 22 +- 20 files changed, 1416 insertions(+), 649 deletions(-) create mode 100644 include/PackageManager.hpp create mode 100644 include/utils/packageList.hpp create mode 100644 resources/lists/University of Western Macedonia/Informatics/packages.json create mode 100644 src/PackageManager.cpp create mode 100644 src/utils/packageList.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cc9b119..936926e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(unibackpack VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network LinguistTools) +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets LinguistTools) set(CMAKE_AUTOMOC ON) # Handles Signals/Slots set(CMAKE_AUTORCC ON) # Handles .qrc resource files @@ -12,40 +12,68 @@ set(CMAKE_AUTOUIC ON) # Handles .ui files set(CMAKE_AUTOUIC_SEARCH_PATHS ui) -qt_add_executable(unibackpack - # Source files - src/main.cpp +include(GNUInstallDirs) + +# Everything except the widgets, so the tests can link against it +qt_add_library(unibackpack_core STATIC src/Downloader.cpp - src/MainWindow.cpp - src/utils/applyTranslator.cpp + src/PackageManager.cpp + src/utils/language.cpp src/utils/loadUniversities.cpp + src/utils/packageList.cpp - # Header files - include/MainWindow.hpp include/Downloader.hpp - include/utils/applyTranslator.hpp + include/PackageManager.hpp + include/utils/language.hpp include/utils/loadUniversities.hpp + include/utils/packageList.hpp +) - # UI - ui/MainWindow.ui +target_include_directories(unibackpack_core PUBLIC include) +target_link_libraries(unibackpack_core PUBLIC Qt6::Core) - # Resources +qt_add_executable(unibackpack + src/main.cpp + src/MainWindow.cpp + include/MainWindow.hpp + ui/MainWindow.ui resources/resources.qrc ) -target_include_directories(unibackpack PRIVATE include) - target_link_libraries(unibackpack PRIVATE - Qt6::Core + unibackpack_core Qt6::Gui Qt6::Widgets - Qt6::Network ) +# Every package list under resources/lists/ is embedded as :/lists///packages.json +file(GLOB_RECURSE PACKAGE_LISTS CONFIGURE_DEPENDS + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + resources/lists/*.json) +qt_add_resources(unibackpack package_lists + PREFIX "/" + BASE resources + FILES ${PACKAGE_LISTS}) + +# Strings from the core library are translated too, so lupdate scans its sources as well +get_target_property(CORE_SOURCES unibackpack_core SOURCES) qt_add_translations(unibackpack TS_FILES translations/unibackpack_en.ts translations/unibackpack_el.ts + SOURCES + ${CORE_SOURCES} + src/main.cpp + src/MainWindow.cpp + include/MainWindow.hpp + ui/MainWindow.ui + INCLUDE_DIRECTORIES + include ) -install(TARGETS unibackpack DESTINATION bin) +install(TARGETS unibackpack RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES resources/unibackpack.desktop + DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) +install(FILES resources/icons/unibackpack.png + DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/512x512/apps) + diff --git a/include/Downloader.hpp b/include/Downloader.hpp index 2c3e7f7..f383041 100644 --- a/include/Downloader.hpp +++ b/include/Downloader.hpp @@ -1,36 +1,92 @@ // Author: Apostolos Chalis 2026 +/** + * @file Downloader.hpp + * @brief Checks and installs a department's package list. + */ #ifndef DOWNLOADER_HPP #define DOWNLOADER_HPP +#include "PackageManager.hpp" + +#include #include #include -#include #include +#include + +class QProcess; + +/** + * @brief Installs a department's package list with the given package manager. + * + * start() checks which listed packages are in the repositories and then installs + * them through pkexec. Everything after start() runs asynchronously, and + * finished() is emitted exactly once at the end, including on failure. + */ class Downloader : public QObject { - Q_OBJECT + Q_OBJECT + +public: + /** + * @brief Creates a downloader for one department. + * + * @param university University name, as used in the @c :/lists/ resource path. + * @param department Department name, as used in the @c :/lists/ resource path. + * @param manager Backend to check and install with. Must not be null. It keeps + * progress state, so only one Downloader may use it at a time. + * @param parent Optional QObject parent. + */ + Downloader(const QString &university, const QString &department, + std::shared_ptr manager, QObject *parent = nullptr); + + /** + * @brief Starts the check, then the install. Returns immediately. + * + * If the department has no packages for this package manager, finished() + * is emitted before this function returns. + */ + void start(); + +signals: + /** + * @brief Overall progress of the install, from 0 to 99. + */ + void progress_updated(int percentage); + + /** + * @brief Text for the detailed log: complete lines of package manager output or own messages. + */ + void log_message(const QString &message); + + /** + * @brief A short, user-facing description of the current step. + */ + void status_changed(const QString &message); - private: - QString name_of_university; - QString name_of_department; + /** + * @brief Emitted once when the run ends. + * + * @param success true if the package manager finished with exit code 0. + * @param error Translated reason for the failure; empty on success. + */ + void finished(bool success, const QString &error); - public: - explicit Downloader(const QString &university, const QString &department, QObject *parent = nullptr) - : QObject(parent), name_of_university(university), name_of_department(department) {} +private: + void check_packages(const QHash &software_by_package); + void install(const QStringList &packages); - QString check_package_manager(); - QStringList read_package_list(bool standard_package_manager, QString package_manager); - bool is_in_pacman_repo(const QString &package_name); - bool is_in_apt_repo(const QString &package_name); + void handle_install_output(const QByteArray &data); + void flush_install_output(); + void connect_start_failure(QProcess *process); - void download_via_pacman(const QStringList &list_to_be_downloaded); - void download_via_apt(const QStringList &list_to_be_downloaded); + void report(const QString &message); + void fail(const QString &message); - signals: - void progress_updated(int percentage); - void status_message(const QString &message); - void status_update(const QString &message); - void download_completed(bool success); + QString name_of_university; + QString name_of_department; + std::shared_ptr package_manager; + QString pending_output; ///< Install output after the last complete line }; #endif // DOWNLOADER_HPP diff --git a/include/MainWindow.hpp b/include/MainWindow.hpp index ab4bdb3..12357ac 100644 --- a/include/MainWindow.hpp +++ b/include/MainWindow.hpp @@ -1,48 +1,102 @@ // Author: Apostolos Chalis 2026 // Co-Author: Ioannis Michadasis +/** + * @file MainWindow.hpp + * @brief Main application window. + */ #ifndef MAINWINDOW_HPP #define MAINWINDOW_HPP +#include "utils/loadUniversities.hpp" + +#include #include -#include -#include -#include -#include -#include +#include +#include + +#include +#include -void applyTranslator(const QString &locale); +class Downloader; +class PackageManager; +class QModelIndex; +class QStandardItemModel; QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE +/** + * @brief The application window. + * + * Lists the universities, then the selected university's departments, and + * installs the chosen department's packages with a Downloader while showing + * progress and the package manager's output. + */ class MainWindow : public QMainWindow { - Q_OBJECT - - public: - MainWindow(QWidget *parent = nullptr); - ~MainWindow(); - void retranslate(); - - protected: - void changeEvent(QEvent *event) override; - - private slots: - void on_university_selection(const QModelIndex &index); - void toggle_output(); - - private: - Ui::MainWindow *ui; - - QHash departments_by_university; - - QStandardItemModel *university_model; - QStandardItemModel *department_model; - - bool showing_universities; - bool output_visible = false; - QString current_university; + Q_OBJECT + +public: + /** + * @brief Builds the UI, loads the universities from @c :/universities.json + * and detects the system's package manager. + */ + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + + /** + * @brief Re-applies translated texts that are not part of the .ui form. + * + * Covers the university and department names, the status line, the + * progress bar text and the details button. + */ + void retranslate(); + +protected: + /** + * @brief Retranslates the whole window on QEvent::LanguageChange. + */ + void changeEvent(QEvent *event) override; + + /** + * @brief Keeps the window open while an installation is running. + */ + void closeEvent(QCloseEvent *event) override; + +private slots: + void handle_item_clicked(const QModelIndex &index); + void toggle_output(); + +private: + enum class InstallState { Idle, Running, Succeeded, Failed }; + + void setup_language_selector(); + void show_universities(); + void show_departments(const QString &university_name); + void start_install(const QString &department); + bool confirm_install(); + void finish_install(bool success, const QString &error); + void set_status(std::function text); + const Utils::University *find_university(const QString &name) const; + + std::unique_ptr ui; + + QList universities; + QStandardItemModel *university_model = nullptr; + QStandardItemModel *department_model = nullptr; + + /// Detected once at startup; null if the system has no supported package manager. + std::shared_ptr package_manager; + QPointer downloader; + InstallState install_state = InstallState::Idle; + + /// Produces the status line in the current language; empty while no status is shown. + std::function status_text; + + bool showing_universities = true; + bool output_visible = false; + QString current_university; }; #endif // MAINWINDOW_HPP diff --git a/include/PackageManager.hpp b/include/PackageManager.hpp new file mode 100644 index 0000000..50fcc5a --- /dev/null +++ b/include/PackageManager.hpp @@ -0,0 +1,137 @@ +// Authors: Apostolos Chalis 2026 , Ioannis Michadasis 2026 +/** + * @file PackageManager.hpp + * @brief Backends for the system package managers UniBackpack can install with. + */ +#ifndef PACKAGE_MANAGER_HPP +#define PACKAGE_MANAGER_HPP + +#include +#include +#include +#include + +#include +#include + +/** + * @brief Everything that differs between the supported system package managers. + * + * Downloader drives the check/install flow and asks the backend for the + * commands to run and how to interpret their output. All output given to a + * backend comes from processes run with @c LC_ALL=C, so it is untranslated. + */ +class PackageManager { + Q_DECLARE_TR_FUNCTIONS(PackageManager) + +public: + virtual ~PackageManager() = default; + + /** + * @brief Returns a backend for the first supported package manager on PATH. + * + * pacman is preferred over apt when both are present. + * + * @return The backend, or nullptr if no supported package manager is installed. + */ + static std::unique_ptr detect(); + + /** + * @brief Command name of the package manager, e.g. "pacman". + * + * Also the key for this manager's package names in a department's package list. + */ + virtual QString name() const = 0; + + /** + * @brief Program that prints repository info for several packages in a single run. + */ + virtual QString query_program() const = 0; + + /** + * @brief Arguments for query_program() that look up all of @p packages. + */ + virtual QStringList query_arguments(const QStringList &packages) const = 0; + + /** + * @brief Extracts the available package names from the query output. + * + * @param query_output Standard output of query_program(). + * @return Names of the packages found in the repositories. + */ + virtual QSet parse_available(const QString &query_output) const = 0; + + /** + * @brief Command line, program first, that installs @p packages. It is run through pkexec. + */ + virtual QStringList install_command(const QStringList &packages) const = 0; + + /** + * @brief Extra @c NAME=value variables for the install command. Empty by default. + * + * pkexec clears the environment, so these are passed through @c env. + */ + virtual QStringList install_environment() const; + + /** + * @brief Resets progress-parsing state before an install of @p package_count packages. + */ + virtual void begin_install(int package_count); + + /** + * @brief Parses overall install progress from one line of install output. + * + * @param line A complete line of standard output, without the line break. + * @return Progress from 0 to 99, or std::nullopt if the line carries no progress + * information. 100 is left for the caller once the install has finished. + */ + virtual std::optional parse_progress(const QString &line) = 0; + + /** + * @brief Message to show the user before installing. Empty by default. + */ + virtual QString pre_install_warning() const; +}; + +/** + * @brief Backend for Arch-based systems (pacman). + * + * Downloads fill the first half of the progress bar and installs the second, + * both counted against the "Packages (N)" total that pacman prints. + */ +class PacmanBackend : public PackageManager { +public: + QString name() const override; + QString query_program() const override; + QStringList query_arguments(const QStringList &packages) const override; + QSet parse_available(const QString &query_output) const override; + QStringList install_command(const QStringList &packages) const override; + void begin_install(int package_count) override; + std::optional parse_progress(const QString &line) override; + QString pre_install_warning() const override; + +private: + int total = 0; + int downloaded = 0; + int installed = 0; +}; + +/** + * @brief Backend for Debian-based systems (apt-get). + * + * Progress comes from the machine-readable status lines that apt-get writes + * when run with @c APT::Status-Fd=1: downloads fill the first half of the bar + * and dpkg the second. + */ +class AptBackend : public PackageManager { +public: + QString name() const override; + QString query_program() const override; + QStringList query_arguments(const QStringList &packages) const override; + QSet parse_available(const QString &query_output) const override; + QStringList install_command(const QStringList &packages) const override; + QStringList install_environment() const override; + std::optional parse_progress(const QString &line) override; +}; + +#endif // PACKAGE_MANAGER_HPP diff --git a/include/utils/language.hpp b/include/utils/language.hpp index 5aeeced..70d7a89 100644 --- a/include/utils/language.hpp +++ b/include/utils/language.hpp @@ -1,33 +1,62 @@ /** - * @file applyTranslator.hpp - * @brief Runtime language switching for the application. + * @file language.hpp + * @brief The supported UI languages and runtime language switching. */ -#ifndef UTILS_TRANSLATOR_HPP -#define UTILS_TRANSLATOR_HPP +#ifndef UTILS_LANGUAGE_HPP +#define UTILS_LANGUAGE_HPP +#include #include namespace Utils { /** - * @brief Switches the application's UI language to @p locale. + * @brief A language the UI can be shown in. + */ + struct Language { + /// Locale code stored in QSettings, e.g. "el_GR". + QString code; + /// The language's name in that language, e.g. "Ελληνικά". + QString native_name; + }; + + /** + * @brief The languages offered in the language selector, English first. * - * Only Greek is currently translated. If @p locale starts with @c "el", - * the Greek translator is loaded from @c :/i18n/unibackpack_el.qm and installed. - * The file is loaded once and cached for later calls. - * Any other locale removes the Greek translator, so the original English strings are shown. + * Each one except English needs a @c translations/unibackpack_.ts file. + */ + QList supportedLanguages(); + + /** + * @brief Switches the application's UI language to @p locale. * - * If the @c .qm file is missing or empty, nothing is installed and the UI stays in English. + * Loads @c :/i18n/unibackpack_.qm for the best match of @p locale + * and installs it, replacing the translator installed by a previous call. + * If no matching or non-empty @c .qm file exists, no translator is installed, + * so the original English strings are shown. Qt's own translations, used for + * example by standard dialog buttons, are installed the same way when the + * system has them. * * Installing or removing a translator makes Qt send QEvent::LanguageChange * to the application, and widgets then refresh their texts in @c changeEvent(). * - * @param locale Locale code as stored in QSettings under "language", - * e.g. "en_US" or "el_GR". + * @param locale Locale code such as "en_US" or "el_GR". * * @pre A QCoreApplication instance must exist. * @note Not thread-safe; call it from the GUI thread only. */ void applyTranslator(const QString &locale); + + /** + * @brief The language the user picked, or the system locale if they never picked one. + * + * @return Locale code such as "el_GR", read from QSettings under "language". + */ + QString savedLanguage(); + + /** + * @brief Stores @p locale as the user's language for savedLanguage(). + */ + void saveLanguage(const QString &locale); } -#endif // UTILS_TRANSLATOR_HPP +#endif // UTILS_LANGUAGE_HPP diff --git a/include/utils/loadUniversities.hpp b/include/utils/loadUniversities.hpp index 51a3e88..6e0e4d9 100644 --- a/include/utils/loadUniversities.hpp +++ b/include/utils/loadUniversities.hpp @@ -1,13 +1,67 @@ +/** + * @file loadUniversities.hpp + * @brief Loads the university/department catalogue from a JSON resource. + */ #ifndef LOAD_UNIVERSITIES_HPP #define LOAD_UNIVERSITIES_HPP +#include +#include #include -#include +/// @brief Free helper functions shared across the UI. namespace Utils { - void loadUniversities(const QString &path, - QHash &departments_by_university, - QStandardItemModel *university_model); + /** + * @brief A named catalogue entry with optional translated display names. + */ + struct CatalogEntry { + /// English name. Also the directory name under @c :/lists/, so it never changes with the language. + QString name; + /// Display names keyed by language code, e.g. "el". + QHash translations; + + /** + * @brief Name to show for @p locale, falling back to the English name. + * + * @param locale Locale code such as "el_GR". Only the language part is used. + */ + QString display_name(const QString &locale) const; + }; + + /** + * @brief A university, its logo and its departments. + */ + struct University : CatalogEntry { + /// Resource path of the university logo. + QString icon; + /// Departments in display order. + QList departments; + }; + + /** + * @brief Parses the universities JSON. + * + * The file must contain a JSON array of objects of the form + * @code{.json} + * [ + * { + * "name": "Aristotle University of Thessaloniki", + * "translations": { "el": "Αριστοτέλειο Πανεπιστήμιο Θεσσαλονίκης" }, + * "icon": ":/icons/auth_logo.png", + * "departments": [ + * { "name": "Informatics", "translations": { "el": "Τμήμα Πληροφορικής" } } + * ] + * } + * ] + * @endcode + * @c translations is optional everywhere. Universities and departments with an + * empty or missing @c name are skipped. + * + * @param path Path to the JSON file, usually a Qt resource such as ":/universities.json". + * @return The universities in file order. Empty, with a logged warning, if the file + * cannot be opened or is not a valid JSON array. + */ + QList loadUniversities(const QString &path); } #endif // LOAD_UNIVERSITIES_HPP diff --git a/include/utils/packageList.hpp b/include/utils/packageList.hpp new file mode 100644 index 0000000..6f3a770 --- /dev/null +++ b/include/utils/packageList.hpp @@ -0,0 +1,55 @@ +/** + * @file packageList.hpp + * @brief Loads a department's package list from a JSON resource. + */ +#ifndef PACKAGE_LIST_HPP +#define PACKAGE_LIST_HPP + +#include +#include +#include + +namespace Utils { + /** + * @brief One piece of software and its package name in each package manager. + */ + struct PackageEntry { + /// Human-readable name, e.g. "Java JDK". + QString name; + /// Package names keyed by PackageManager::name(), e.g. "apt" -> "default-jdk". + QHash packages; + }; + + /** + * @brief Resource path of a department's package list. + * + * @return @c :/lists///packages.json + */ + QString packageListPath(const QString &university, const QString &department); + + /** + * @brief Parses a package list. + * + * The file must contain a JSON array of objects of the form + * @code{.json} + * [ + * { "name": "Java JDK", "apt": "default-jdk", "pacman": "jdk-openjdk" }, + * { "name": "FreeCAD", "pacman": "freecad" } + * ] + * @endcode + * Every key other than @c name is a package manager. Leave a manager out when + * the software is not packaged for it. Entries without a @c name are skipped. + * + * @param path Path to the JSON file, usually from packageListPath(). + * @return The entries in file order. Empty, with a logged warning, if the file + * cannot be opened or is not a valid JSON array. + */ + QList loadPackageList(const QString &path); + + /** + * @brief Whether any entry of @p entries has a package for @p package_manager. + */ + bool hasPackagesFor(const QList &entries, const QString &package_manager); +} + +#endif // PACKAGE_LIST_HPP diff --git a/resources/lists/University of Western Macedonia/Informatics/packages.json b/resources/lists/University of Western Macedonia/Informatics/packages.json new file mode 100644 index 0000000..0f44d61 --- /dev/null +++ b/resources/lists/University of Western Macedonia/Informatics/packages.json @@ -0,0 +1,25 @@ +[ + {"name": "Bash", "apt": "bash", "pacman": "bash"}, + {"name": "Wget", "apt": "wget", "pacman": "wget"}, + {"name": "GnuPG", "apt": "gnupg", "pacman": "gnupg"}, + {"name": "Arduino IDE", "apt": "arduino", "pacman": "arduino"}, + {"name": "Bison", "apt": "bison", "pacman": "bison"}, + {"name": "Build tools", "apt": "build-essential", "pacman": "base-devel"}, + {"name": "DBeaver", "apt": "dbeaver-ce", "pacman": "dbeaver"}, + {"name": "Java JDK", "apt": "default-jdk", "pacman": "jdk-openjdk"}, + {"name": "Flex", "apt": "flex", "pacman": "flex"}, + {"name": "FreeCAD", "pacman": "freecad"}, + {"name": "GCC", "apt": "gcc", "pacman": "gcc"}, + {"name": "GHDL", "apt": "ghdl", "pacman": "ghdl"}, + {"name": "gnuplot", "apt": "gnuplot", "pacman": "gnuplot"}, + {"name": "GTKWave", "apt": "gtkwave", "pacman": "gtkwave"}, + {"name": "KiCad", "apt": "kicad", "pacman": "kicad"}, + {"name": "Armadillo", "apt": "libarmadillo-dev", "pacman": "armadillo"}, + {"name": "MariaDB", "apt": "mariadb-server", "pacman": "mariadb"}, + {"name": "Node.js", "apt": "nodejs", "pacman": "nodejs"}, + {"name": "npm", "apt": "npm", "pacman": "npm"}, + {"name": "GNU Octave", "apt": "octave", "pacman": "octave"}, + {"name": "SimulIDE", "apt": "simulide", "pacman": "simulide"}, + {"name": "Weka", "apt": "weka", "pacman": "weka"}, + {"name": "Wireshark", "pacman": "wireshark-qt"} +] diff --git a/resources/resources.qrc b/resources/resources.qrc index 3a7c5f6..d289e99 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -11,9 +11,5 @@ universities.json - - lists/University of Western Macedonia/Informatics/pacman_list.txt - lists/University of Western Macedonia/Informatics/apt_list.txt - diff --git a/resources/universities.json b/resources/universities.json index 698b95a..2985455 100644 --- a/resources/universities.json +++ b/resources/universities.json @@ -1,17 +1,29 @@ [ { "name": "Aristotle University of Thessaloniki", + "translations": { "el": "Αριστοτέλειο Πανεπιστήμιο Θεσσαλονίκης" }, "icon": ":/icons/auth_logo.png", - "departments": ["Informatics", "Physics"] + "departments": [ + { "name": "Informatics", "translations": { "el": "Τμήμα Πληροφορικής" } }, + { "name": "Physics", "translations": { "el": "Τμήμα Φυσικής" } } + ] }, { "name": "University of Western Macedonia", + "translations": { "el": "Πανεπιστήμιο Δυτικής Μακεδονίας" }, "icon": ":/icons/uowm_logo.png", - "departments": ["Informatics", "Mechanical Engineering"] + "departments": [ + { "name": "Informatics", "translations": { "el": "Τμήμα Πληροφορικής" } }, + { "name": "Mechanical Engineering", "translations": { "el": "Τμήμα Μηχανολογίας" } } + ] }, { "name": "University of Macedonia", + "translations": { "el": "Πανεπιστήμιο Μακεδονίας" }, "icon": ":/icons/uom_logo.png", - "departments": ["Applied Informatics", "Economics"] + "departments": [ + { "name": "Applied Informatics", "translations": { "el": "Τμήμα Εφαρμοσμένης Πληροφορικής" } }, + { "name": "Economics", "translations": { "el": "Τμήμα Οικονομικών" } } + ] } ] diff --git a/src/Downloader.cpp b/src/Downloader.cpp index c37915d..fb15059 100644 --- a/src/Downloader.cpp +++ b/src/Downloader.cpp @@ -1,285 +1,173 @@ // Authors: Apostolos Chalis 2026 , Ioannis Michadasis 2026 #include "Downloader.hpp" -// test +#include "utils/packageList.hpp" + #include -#include -#include -#include -#include +#include #include -#include -#include -QString Downloader::check_package_manager() { - if (!QStandardPaths::findExecutable("pacman").isEmpty()) { - qDebug() << "Pacman found."; - return "pacman"; - } else if (!QStandardPaths::findExecutable("apt").isEmpty()) { - qDebug() << "Apt found."; - return "apt"; - } else { - qDebug() << "No supported package manager found."; - return "Unsupported"; +namespace { + QString describe_exit(int exit_code) { + // Exit codes reserved by pkexec, see pkexec(1) + switch (exit_code) { + case 126: + return Downloader::tr("Authorization was cancelled."); + case 127: + return Downloader::tr("You are not authorized to install packages."); + default: + return Downloader::tr("The package manager failed with exit code %1.").arg(exit_code); + } } } -bool Downloader::is_in_pacman_repo(const QString &package_name) { - QProcess process; - process.start("pacman", QStringList() << "-Si" << package_name); - process.waitForFinished(); - if (process.exitCode() == 0) { - QString msg = tr("Found: %1").arg(package_name); - emit status_message(msg); - emit status_update(msg); - QCoreApplication::processEvents(); - return true; - } else { - QString msg = tr("Not in repos: %1").arg(package_name); - emit status_message(msg); - emit status_update(msg); - QCoreApplication::processEvents(); - return false; +Downloader::Downloader(const QString &university, const QString &department, + std::shared_ptr manager, QObject *parent) + : QObject(parent), + name_of_university(university), + name_of_department(department), + package_manager(std::move(manager)) {} + +void Downloader::start() { + const QList entries = + Utils::loadPackageList(Utils::packageListPath(name_of_university, name_of_department)); + + // Package name -> software name, for the messages + QHash software_by_package; + for (const Utils::PackageEntry &entry : entries) { + const QString package = entry.packages.value(package_manager->name()); + if (package.isEmpty()) + report(tr("Not packaged for %1: %2").arg(package_manager->name(), entry.name)); + else + software_by_package.insert(package, entry.name); } -} -bool Downloader::is_in_apt_repo(const QString &package_name) { - QProcess process; - process.start("apt-cache", QStringList() << "show" << package_name); - process.waitForFinished(); - - QString stdout_output = process.readAllStandardOutput(); - - if (process.exitCode() == 0 && stdout_output.contains("Package:")) { - QString msg = tr("Found: %1").arg(package_name); - emit status_message(msg); - emit status_update(msg); - QCoreApplication::processEvents(); - return true; - } else { - QString msg = tr("Not in repos: %1").arg(package_name); - emit status_message(msg); - emit status_update(msg); - QCoreApplication::processEvents(); - return false; + if (software_by_package.isEmpty()) { + fail(tr("No %1 package list found for %2.").arg(package_manager->name(), name_of_department)); + return; } + check_packages(software_by_package); } -QStringList Downloader::read_package_list(bool standard_package_manager, QString package_manager) { - QStringList installable_with_standard_package_manager; - QStringList installable_with_non_standard_package_manager; +void Downloader::check_packages(const QHash &software_by_package) { + report(tr("Checking packages for %1...").arg(name_of_department)); - if (standard_package_manager && package_manager == "pacman") { - QString filepath_of_list = ":/lists/" + name_of_university + "/" + name_of_department + "/pacman_list.txt"; - QString msg = tr("Checking packages for %1...").arg(name_of_department); - emit status_message(msg); - emit status_update(msg); - QFile file(filepath_of_list); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qDebug() << "Critical Error: Could not open the file!" << file.errorString(); - return installable_with_standard_package_manager; - } - QTextStream in(&file); - while (!in.atEnd()) { - QString package = in.readLine().trimmed(); - if (!package.isEmpty() && is_in_pacman_repo(package)) { - QString addMsg = tr("Adding: %1").arg(package); - emit status_message(addMsg); - emit status_update(addMsg); - installable_with_standard_package_manager.append(package); - } - } - file.close(); - return installable_with_standard_package_manager; - } - - if (!standard_package_manager && package_manager == "pacman") { - QString filepath_of_list = ":/lists/" + name_of_university + "/" + name_of_department + "/pacman_list.txt"; - QString msg = tr("Checking packages for %1...").arg(name_of_department); - emit status_message(msg); - emit status_update(msg); - QFile file(filepath_of_list); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qDebug() << "Critical Error: Could not open the file!" << file.errorString(); - return installable_with_non_standard_package_manager; - } - QTextStream in(&file); - while (!in.atEnd()) { - QString package = in.readLine().trimmed(); - if (!package.isEmpty() && !is_in_pacman_repo(package)) { - QString addMsg = tr("Adding (non-standard): %1").arg(package); - emit status_message(addMsg); - emit status_update(addMsg); - installable_with_non_standard_package_manager.append(package); - } - } - file.close(); - return installable_with_non_standard_package_manager; - } + QStringList packages = software_by_package.keys(); + packages.sort(); - if (standard_package_manager && package_manager == "apt") { - QString filepath_of_list = ":/lists/" + name_of_university + "/" + name_of_department + "/apt_list.txt"; - QString msg = tr("Checking packages for %1...").arg(name_of_department); - emit status_message(msg); - emit status_update(msg); - QFile file(filepath_of_list); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qDebug() << "Critical Error: Could not open the file!" << file.errorString(); - return installable_with_standard_package_manager; - } - QTextStream in(&file); - while (!in.atEnd()) { - QString package = in.readLine().trimmed(); - if (!package.isEmpty() && is_in_apt_repo(package)) { - QString addMsg = tr("Adding: %1").arg(package); - emit status_message(addMsg); - emit status_update(addMsg); - installable_with_standard_package_manager.append(package); - } - } - file.close(); - return installable_with_standard_package_manager; - } + QProcess *query = new QProcess(this); - if (!standard_package_manager && package_manager == "apt") { - QString filepath_of_list = ":/lists/" + name_of_university + "/" + name_of_department + "/apt_list.txt"; - QString msg = tr("Checking packages for %1...").arg(name_of_department); -emit status_message(msg); -emit status_update(msg); - QFile file(filepath_of_list); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qDebug() << "Critical Error: Could not open the file!" << file.errorString(); - return installable_with_non_standard_package_manager; - } - QTextStream in(&file); - while (!in.atEnd()) { - QString package = in.readLine().trimmed(); - if (!package.isEmpty() && !is_in_apt_repo(package)) { - QString addMsg = tr("Adding (non-standard): %1").arg(package); - emit status_message(addMsg); - emit status_update(addMsg);; - installable_with_non_standard_package_manager.append(package); + // parse_available() matches untranslated field names + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("LC_ALL", "C"); + query->setProcessEnvironment(env); + + connect_start_failure(query); + connect(query, &QProcess::finished, this, [this, query, packages, software_by_package]() { + // The exit code is non-zero as soon as one package is missing, so only the output is used + const QSet available = + package_manager->parse_available(QString::fromUtf8(query->readAllStandardOutput())); + query->deleteLater(); + + QStringList installable; + for (const QString &package : packages) { + const QString label = QString("%1 (%2)").arg(software_by_package.value(package), package); + if (available.contains(package)) { + report(tr("Found: %1").arg(label)); + installable.append(package); + } else { + report(tr("Not in repos: %1").arg(label)); } } - file.close(); - return installable_with_non_standard_package_manager; - } + install(installable); + }); - qDebug() << "Critical Error: Unsupported package manager:" << package_manager; - return {}; + query->start(package_manager->query_program(), package_manager->query_arguments(packages)); } -void Downloader::download_via_pacman(const QStringList &list_to_be_downloaded) { - // there is a problem with pacman -Syu though, so the user should be alerted about that - QMessageBox::warning( - nullptr, - tr("Warning"), - tr("It is advised to update your system before proceeding.\nRun sudo pacman -Syu") - ); - - if (list_to_be_downloaded.isEmpty()) { - qDebug() << "Download list is empty\nNothing to do."; - emit download_completed(false); +void Downloader::install(const QStringList &packages) { + if (packages.isEmpty()) { + fail(tr("None of the packages are available in your repositories.")); return; } - qDebug() << "Starting to download package list via pacman"; - - int total = list_to_be_downloaded.size(); - int *downloaded = new int(0); - - QProcess *download_process = new QProcess(this); + report(tr("Installing %n package(s)...", nullptr, static_cast(packages.size()))); + package_manager->begin_install(static_cast(packages.size())); + pending_output.clear(); - connect(download_process, &QProcess::readyReadStandardOutput, this, [=]() { - QString output = download_process->readAllStandardOutput(); - emit status_message(output); - - // Download phase: "downloading pkgname..." - static QRegularExpression download_re(R"(downloading (.+)\.\.\.)"); - QRegularExpressionMatchIterator it = download_re.globalMatch(output); - int download_count = 0; - while (it.hasNext()) { - it.next(); - download_count++; - } - if (download_count > 0 && total > 0) { - *downloaded += download_count; - int percent = static_cast((*downloaded * 50.0) / total); - emit progress_updated(qMin(percent, 49)); - } + QProcess *process = new QProcess(this); - // Install phase: "(1/23) installing pkgname" - static QRegularExpression pacman_re(R"(\((\d+)/(\d+)\))"); - QRegularExpressionMatch match = pacman_re.match(output); - if (match.hasMatch()) { - int current = match.captured(1).toInt(); - int pkg_total = match.captured(2).toInt(); - if (pkg_total > 0) { - int percent = 50 + static_cast((current * 50.0) / pkg_total); - emit progress_updated(qMin(percent, 99)); - } - } + connect(process, &QProcess::readyReadStandardOutput, this, [this, process]() { + handle_install_output(process->readAllStandardOutput()); }); - - connect(download_process, &QProcess::readyReadStandardError, this, [=]() { - emit status_message(download_process->readAllStandardError()); + connect(process, &QProcess::readyReadStandardError, this, [this, process]() { + const QString errors = QString::fromUtf8(process->readAllStandardError()).trimmed(); + if (!errors.isEmpty()) + emit log_message(errors); }); - connect(download_process, QOverload::of(&QProcess::finished), - this, [=](int exitCode, QProcess::ExitStatus status) { - delete downloaded; - qDebug() << "Process finished with exit code:" << exitCode << "status:" << status; - if (exitCode == 0) { - qDebug() << "Package list downloaded via pacman"; - } else { - qDebug() << "Error downloading packages via pacman. Exit code:" << exitCode; - } - emit download_completed(exitCode == 0); - download_process->deleteLater(); + connect_start_failure(process); + connect(process, &QProcess::finished, this, [this, process](int exit_code, QProcess::ExitStatus status) { + qDebug() << "Process finished with exit code:" << exit_code << "status:" << status; + flush_install_output(); + process->deleteLater(); + + if (status != QProcess::NormalExit) + fail(tr("The package manager stopped unexpectedly.")); + else if (exit_code != 0) + fail(describe_exit(exit_code)); + else + emit finished(true, QString()); }); - QStringList command_structure; - command_structure << "pacman" << "-S" << "--noconfirm" << "--needed" << list_to_be_downloaded; - qDebug() << "Executing: pkexec" << command_structure.join(" "); - download_process->start("pkexec", command_structure); + // pkexec clears the environment, so variables have to go through env. + // LC_ALL=C keeps the output untranslated for parse_progress(). + const QStringList arguments = QStringList{"env", "LC_ALL=C"} + + package_manager->install_environment() + + package_manager->install_command(packages); + qDebug() << "Executing: pkexec" << arguments.join(' '); + process->start("pkexec", arguments); } -void Downloader::download_via_apt(const QStringList &list_to_be_downloaded) { - if (list_to_be_downloaded.isEmpty()) { - qDebug() << "Download list is empty\nNothing to do."; - emit download_completed(false); +void Downloader::handle_install_output(const QByteArray &data) { + // Chunks can end mid-line; keep the partial line until the rest arrives + pending_output += QString::fromUtf8(data); + const qsizetype end = pending_output.lastIndexOf('\n'); + if (end < 0) return; - } - qDebug() << "Starting to download package list via apt"; + const QString complete = pending_output.left(end); + pending_output.remove(0, end + 1); - QProcess *download_process = new QProcess(this); + emit log_message(complete); + for (const QString &line : complete.split('\n')) { + if (const std::optional percent = package_manager->parse_progress(line)) + emit progress_updated(*percent); + } +} - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - env.insert("DEBIAN_FRONTEND", "noninteractive"); - download_process->setProcessEnvironment(env); +void Downloader::flush_install_output() { + if (!pending_output.isEmpty()) + handle_install_output(QByteArrayLiteral("\n")); +} - connect(download_process, &QProcess::readyReadStandardOutput, this, [=]() { - emit status_message(download_process->readAllStandardOutput()); - }); - connect(download_process, &QProcess::readyReadStandardError, this, [=]() { - emit status_message(download_process->readAllStandardError()); - }); - connect(download_process, QOverload::of(&QProcess::finished), - this, [=](int exitCode, QProcess::ExitStatus status) { - qDebug() << "Process finished with exit code:" << exitCode << "status:" << status; - if (exitCode == 0) { - qDebug() << "Package list downloaded via apt"; - } else { - qDebug() << "Error downloading packages via apt. Exit code:" << exitCode; - } - emit download_completed(exitCode == 0); - download_process->deleteLater(); +void Downloader::connect_start_failure(QProcess *process) { + connect(process, &QProcess::errorOccurred, this, [this, process](QProcess::ProcessError error) { + // finished() is not emitted when the process never started, e.g. pkexec is missing + if (error != QProcess::FailedToStart) + return; + const QString message = tr("Could not run %1: %2").arg(process->program(), process->errorString()); + process->deleteLater(); + fail(message); }); +} + +void Downloader::report(const QString &message) { + emit log_message(message); + emit status_changed(message); +} - QStringList command_structure; - command_structure << "apt" << "install" << "-y" - << "-o" << "APT::Status-Fd=1" - << list_to_be_downloaded; - qDebug() << "Executing: pkexec" << command_structure.join(" "); - download_process->start("pkexec", command_structure); +void Downloader::fail(const QString &message) { + qWarning() << message; + emit log_message(message); + emit finished(false, message); } diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index e5aab63..1729120 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -3,204 +3,246 @@ #include "ui_MainWindow.h" #include "MainWindow.hpp" #include "Downloader.hpp" -#include "utils/applyTranslator.hpp" -#include "utils/loadUniversities.hpp" +#include "PackageManager.hpp" +#include "utils/language.hpp" +#include "utils/packageList.hpp" -#include -#include +#include #include -#include -#include #include -#include -#include -#include -#include -#include +#include +#include +#include +#include + +namespace { + const QString success_chunk_style = QStringLiteral("QProgressBar::chunk { background-color: #4CAF50; }"); + const QString failure_chunk_style = QStringLiteral("QProgressBar::chunk { background-color: #f44336; }"); +} MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent), ui(new Ui::MainWindow) { + : QMainWindow(parent), ui(std::make_unique()) { ui->setupUi(this); university_model = new QStandardItemModel(this); department_model = new QStandardItemModel(this); - Utils::loadUniversities(":/universities.json",departments_by_university ,university_model); + universities = Utils::loadUniversities(":/universities.json"); + for (const Utils::University &university : universities) { + QStandardItem *item = new QStandardItem(QIcon(university.icon), university.name); + item->setData(university.name, Qt::UserRole); + university_model->appendRow(item); + } ui->listView->setModel(university_model); - showing_universities = true; ui->progressBar->setVisible(false); ui->statusLabel->setVisible(false); ui->outputView->setVisible(false); ui->showMoreButton->setVisible(false); - connect(ui->listView, &QListView::clicked, this, &MainWindow::on_university_selection); + connect(ui->listView, &QListView::clicked, this, &MainWindow::handle_item_clicked); + connect(ui->backButton, &QPushButton::clicked, this, &MainWindow::show_universities); connect(ui->showMoreButton, &QPushButton::clicked, this, &MainWindow::toggle_output); - // Language selector in status bar - QComboBox *langCombo = new QComboBox(this); - langCombo->addItem("English", "en_US"); - langCombo->addItem("Ελληνικά", "el_GR"); - - QSettings settings; - QString currentLang = settings.value("language", "en_US").toString(); - int idx = langCombo->findData(currentLang); - if (idx >= 0) { - langCombo->blockSignals(true); - langCombo->setCurrentIndex(idx); - langCombo->blockSignals(false); - } - - statusBar()->addPermanentWidget(langCombo); + package_manager = PackageManager::detect(); - connect(langCombo, &QComboBox::currentIndexChanged, this, [=](int index) { - QString lang = langCombo->itemData(index).toString(); - QSettings s; - s.setValue("language", lang); - Utils::applyTranslator(lang); - QEvent ev(QEvent::LanguageChange); - QCoreApplication::sendEvent(this, &ev); - }); + if (universities.isEmpty()) + set_status([this]() { return tr("Could not load the list of universities."); }); + else if (!package_manager) + set_status([this]() { return tr("No supported package manager (pacman or apt) was found."); }); + setup_language_selector(); retranslate(); } MainWindow::~MainWindow() { - delete ui; + // A running Downloader is deleted with this window's children, after ui is gone + if (downloader) + downloader->disconnect(this); } +void MainWindow::setup_language_selector() { + QComboBox *language_combo = new QComboBox(this); + for (const Utils::Language &language : Utils::supportedLanguages()) + language_combo->addItem(language.native_name, language.code); + // Select the language main() applied, which falls back to the system locale + const QLocale::Language saved = QLocale(Utils::savedLanguage()).language(); + for (int i = 0; i < language_combo->count(); ++i) { + if (QLocale(language_combo->itemData(i).toString()).language() == saved) + language_combo->setCurrentIndex(i); + } + + statusBar()->addPermanentWidget(language_combo); + + connect(language_combo, &QComboBox::currentIndexChanged, this, [this, language_combo](int index) { + const QString language = language_combo->itemData(index).toString(); + Utils::saveLanguage(language); + // Swapping the translator sends LanguageChange, which retranslates the .ui texts. + // University names come from the JSON, so refresh them even if no translator changed. + Utils::applyTranslator(language); + retranslate(); + }); +} void MainWindow::toggle_output() { output_visible = !output_visible; ui->outputView->setVisible(output_visible); - ui->showMoreButton->setText(output_visible ? "Hide details ▲" : "Show details ▼"); + retranslate(); } -void MainWindow::on_university_selection(const QModelIndex &index) { - if (showing_universities) { - current_university = university_model->data(index, Qt::UserRole).toString(); +void MainWindow::handle_item_clicked(const QModelIndex &index) { + const QString key = index.data(Qt::UserRole).toString(); - const QStringList departments = departments_by_university.value(current_university); + if (showing_universities) + show_departments(key); + else + start_install(key); +} - department_model->clear(); +void MainWindow::show_universities() { + ui->listView->setModel(university_model); + ui->backButton->setVisible(false); + showing_universities = true; +} - QStandardItem *backItem = new QStandardItem(tr("Back to Universities")); - backItem->setData("__back__", Qt::UserRole); - department_model->appendRow(backItem); +void MainWindow::show_departments(const QString &university_name) { + const Utils::University *university = find_university(university_name); + if (!university) + return; + + current_university = university->name; + department_model->clear(); + + // Departments without packages for this system's package manager cannot be installed + for (const Utils::CatalogEntry &department : university->departments) { + QStandardItem *item = new QStandardItem; + item->setData(department.name, Qt::UserRole); + item->setEnabled(package_manager + && Utils::hasPackagesFor( + Utils::loadPackageList(Utils::packageListPath(university->name, department.name)), + package_manager->name())); + department_model->appendRow(item); + } - for (const QString &dept : departments) { - QStandardItem *item = new QStandardItem(tr(dept.toUtf8().constData())); - item->setData(dept, Qt::UserRole); - department_model->appendRow(item); - } + ui->listView->setModel(department_model); + ui->backButton->setVisible(true); + showing_universities = false; + retranslate(); +} - ui->listView->setModel(department_model); - showing_universities = false; +void MainWindow::start_install(const QString &department) { + if (!package_manager || !confirm_install()) + return; + + install_state = InstallState::Running; + ui->listView->setEnabled(false); + ui->backButton->setEnabled(false); + ui->outputView->clear(); + ui->progressBar->setMaximum(100); + ui->progressBar->setValue(0); + ui->progressBar->setStyleSheet(""); + ui->progressBar->setVisible(true); + ui->showMoreButton->setVisible(true); + set_status([this]() { return tr("Checking packages..."); }); + retranslate(); - } else { - QString selectedDept = department_model->data(index, Qt::UserRole).toString(); + downloader = new Downloader(current_university, department, package_manager, this); - if (selectedDept == "__back__") { - ui->listView->setModel(university_model); - showing_universities = true; - return; - } + connect(downloader, &Downloader::log_message, ui->outputView, &QPlainTextEdit::appendPlainText); + connect(downloader, &Downloader::status_changed, this, [this](const QString &message) { + // Already translated by the Downloader; stays in the language it was sent in + set_status([message]() { return message; }); + }); + connect(downloader, &Downloader::progress_updated, ui->progressBar, &QProgressBar::setValue); + connect(downloader, &Downloader::finished, this, &MainWindow::finish_install); - Downloader *downloader = new Downloader(current_university, selectedDept, this); - QString package_manager = downloader->check_package_manager(); - - if (package_manager != "Unsupported") { - ui->listView->setEnabled(false); - ui->outputView->clear(); - ui->progressBar->setMaximum(100); - ui->progressBar->setValue(0); - ui->progressBar->setFormat("%p%"); - ui->progressBar->setStyleSheet(""); - ui->progressBar->setVisible(true); - ui->statusLabel->setText("Checking packages..."); - ui->statusLabel->setVisible(true); - ui->showMoreButton->setVisible(true); - - connect(downloader, &Downloader::status_message, this, [=](const QString &msg) { - ui->outputView->append(msg); - for (const QString &line : msg.split('\n')) { - if (line.startsWith("dlstatus:") || line.startsWith("pmstatus:")) { - QStringList parts = line.split(':'); - if (parts.size() >= 3) { - bool ok; - double percent = parts[2].toDouble(&ok); - if (ok) { - ui->progressBar->setMaximum(100); - ui->progressBar->setValue(static_cast(percent)); - } - } - } - } - }); - - connect(downloader, &Downloader::status_update, this, [=](const QString &msg) { - ui->statusLabel->setText(msg.trimmed()); - }); - - connect(downloader, &Downloader::download_completed, this, [=](bool success) { - ui->listView->setEnabled(true); - ui->progressBar->setMaximum(100); - ui->progressBar->setValue(100); - if (success) { - ui->statusLabel->setText("✓ Finished!"); - ui->progressBar->setFormat("Done!"); - ui->progressBar->setStyleSheet("QProgressBar::chunk { background-color: #4CAF50; }"); - } else { - ui->statusLabel->setText("✗ Installation failed."); - ui->progressBar->setFormat("Failed"); - ui->progressBar->setStyleSheet("QProgressBar::chunk { background-color: #f44336; }"); - } - downloader->deleteLater(); - }); - - connect(downloader, &Downloader::progress_updated, this, [=](int percent) { - ui->progressBar->setMaximum(100); - ui->progressBar->setValue(percent); - }); - - QStringList packages_to_download = downloader->read_package_list(true, package_manager); - - if (package_manager == "pacman") { - downloader->download_via_pacman(packages_to_download); - } else if (package_manager == "apt") { - downloader->download_via_apt(packages_to_download); - } - } else { - ui->statusLabel->setText("Error: Unsupported Package Manager"); - ui->statusLabel->setVisible(true); - } + downloader->start(); +} + +bool MainWindow::confirm_install() { + const QString warning = package_manager->pre_install_warning(); + if (warning.isEmpty()) + return true; + + QMessageBox box(QMessageBox::Warning, tr("Warning"), warning, QMessageBox::NoButton, this); + QPushButton *continue_button = box.addButton(tr("Continue"), QMessageBox::AcceptRole); + box.addButton(tr("Cancel"), QMessageBox::RejectRole); + box.setDefaultButton(continue_button); + box.exec(); + return box.clickedButton() == continue_button; +} + +void MainWindow::finish_install(bool success, const QString &error) { + install_state = success ? InstallState::Succeeded : InstallState::Failed; + ui->listView->setEnabled(true); + ui->backButton->setEnabled(true); + ui->progressBar->setValue(100); + ui->progressBar->setStyleSheet(success ? success_chunk_style : failure_chunk_style); + + if (success) + set_status([this]() { return tr("✓ Finished!"); }); + else + set_status([this, error]() { return tr("✗ Installation failed: %1").arg(error); }); + retranslate(); + + if (downloader) + downloader->deleteLater(); +} + +void MainWindow::set_status(std::function text) { + status_text = std::move(text); + ui->statusLabel->setText(status_text().trimmed()); + ui->statusLabel->setVisible(true); +} + +const Utils::University *MainWindow::find_university(const QString &name) const { + for (const Utils::University &university : universities) { + if (university.name == name) + return &university; } + return nullptr; } void MainWindow::retranslate() { - for (int i = 0; i < university_model->rowCount(); ++i) { - QStandardItem *item = university_model->item(i); - QString key = item->data(Qt::UserRole).toString(); - QString translated = tr(key.toUtf8().constData()); - item->setText(translated.isEmpty() ? key : translated); + const QString language = Utils::savedLanguage(); + + for (int row = 0; row < university_model->rowCount(); ++row) { + QStandardItem *item = university_model->item(row); + if (const Utils::University *university = find_university(item->data(Qt::UserRole).toString())) + item->setText(university->display_name(language)); } - if (!showing_universities) { - for (int i = 0; i < department_model->rowCount(); ++i) { - QStandardItem *item = department_model->item(i); - QString key = item->data(Qt::UserRole).toString(); - if (key == "__back__") { - item->setText(tr("Back to Universities")); - } else { - QString translated = tr(key.toUtf8().constData()); - item->setText(translated.isEmpty() ? key : translated); + if (const Utils::University *university = find_university(current_university)) { + for (int row = 0; row < department_model->rowCount(); ++row) { + QStandardItem *item = department_model->item(row); + const QString key = item->data(Qt::UserRole).toString(); + for (const Utils::CatalogEntry &department : university->departments) { + if (department.name == key) + item->setText(department.display_name(language)); } + item->setToolTip(item->isEnabled() ? QString() + : tr("No package list is available for this department yet.")); } } + if (status_text) + ui->statusLabel->setText(status_text().trimmed()); + + switch (install_state) { + case InstallState::Succeeded: + ui->progressBar->setFormat(tr("Done!")); + break; + case InstallState::Failed: + ui->progressBar->setFormat(tr("Failed")); + break; + default: + ui->progressBar->setFormat("%p%"); + break; + } + ui->showMoreButton->setText(output_visible ? tr("Hide details ▲") : tr("Show details ▼")); } @@ -211,3 +253,14 @@ void MainWindow::changeEvent(QEvent *event) { } QMainWindow::changeEvent(event); } + +void MainWindow::closeEvent(QCloseEvent *event) { + // Interrupting apt or pacman can leave the package database broken + if (install_state == InstallState::Running) { + QMessageBox::information(this, tr("Installation in progress"), + tr("Please wait until the installation finishes.")); + event->ignore(); + return; + } + QMainWindow::closeEvent(event); +} diff --git a/src/PackageManager.cpp b/src/PackageManager.cpp new file mode 100644 index 0000000..060b00e --- /dev/null +++ b/src/PackageManager.cpp @@ -0,0 +1,160 @@ +// Authors: Apostolos Chalis 2026 , Ioannis Michadasis 2026 +#include "PackageManager.hpp" + +#include +#include +#include + +namespace { + QSet captured_names(const QRegularExpression &re, const QString &text) { + QSet names; + QRegularExpressionMatchIterator it = re.globalMatch(text); + while (it.hasNext()) + names.insert(it.next().captured(1)); + return names; + } + + /// Maps @p done of @p total onto [first, first + 49], so each phase gets half the bar. + int half_bar(int first, int done, int total) { + return first + qMin(static_cast((done * 50.0) / total), 49); + } +} + +std::unique_ptr PackageManager::detect() { + if (!QStandardPaths::findExecutable("pacman").isEmpty()) { + qDebug() << "Pacman found."; + return std::make_unique(); + } + if (!QStandardPaths::findExecutable("apt-get").isEmpty()) { + qDebug() << "Apt found."; + return std::make_unique(); + } + qDebug() << "No supported package manager found."; + return nullptr; +} + +QStringList PackageManager::install_environment() const { + return {}; +} + +void PackageManager::begin_install(int package_count) { + Q_UNUSED(package_count); +} + +QString PackageManager::pre_install_warning() const { + return {}; +} + +// ---- pacman ---- + +QString PacmanBackend::name() const { + return "pacman"; +} + +QString PacmanBackend::query_program() const { + return "pacman"; +} + +QStringList PacmanBackend::query_arguments(const QStringList &packages) const { + return QStringList{"-Si"} + packages; +} + +QSet PacmanBackend::parse_available(const QString &query_output) const { + // One "Name : pkgname" line per package found in the sync databases + static const QRegularExpression name_re(R"(^Name\s*:\s*(\S+))", QRegularExpression::MultilineOption); + return captured_names(name_re, query_output); +} + +QStringList PacmanBackend::install_command(const QStringList &packages) const { + return QStringList{"pacman", "-S", "--noconfirm", "--needed"} + packages; +} + +void PacmanBackend::begin_install(int package_count) { + // Replaced by the real total, dependencies included, once pacman prints it + total = package_count; + downloaded = 0; + installed = 0; +} + +std::optional PacmanBackend::parse_progress(const QString &line) { + // Without a terminal pacman prints plain lines instead of progress bars: + // "Packages (23) foo-1.0-1 bar-2.0-1 ..." + // " foo-1.0-1-x86_64 downloading..." (pacman >= 6; older: "downloading foo...") + // "installing foo..." (may be prefixed with "(3/23) ") + static const QRegularExpression total_re(R"(^Packages \((\d+)\))"); + static const QRegularExpression download_re(R"(\bdownloading\b)"); + static const QRegularExpression install_re(R"(^(?:\((\d+)/(\d+)\) )?(?:installing|upgrading|reinstalling) )"); + + if (const QRegularExpressionMatch match = total_re.match(line); match.hasMatch()) { + total = match.captured(1).toInt(); + return std::nullopt; + } + if (total <= 0) + return std::nullopt; + + if (download_re.match(line).hasMatch()) + return half_bar(0, ++downloaded, total); + + if (const QRegularExpressionMatch match = install_re.match(line); match.hasMatch()) { + if (!match.captured(1).isEmpty()) { + installed = match.captured(1).toInt(); + total = qMax(total, match.captured(2).toInt()); + } else { + ++installed; + } + return half_bar(50, installed, total); + } + return std::nullopt; +} + +QString PacmanBackend::pre_install_warning() const { + // pacman -S without a prior -Syu can leave the system partially upgraded + // Qualified so lupdate files it under the same context that tr() uses at runtime + return PackageManager::tr("It is advised to update your system before proceeding.\nRun sudo pacman -Syu"); +} + +// ---- apt ---- + +QString AptBackend::name() const { + return "apt"; +} + +QString AptBackend::query_program() const { + return "apt-cache"; +} + +QStringList AptBackend::query_arguments(const QStringList &packages) const { + return QStringList{"show", "--no-all-versions"} + packages; +} + +QSet AptBackend::parse_available(const QString &query_output) const { + // One "Package: pkgname" stanza per installable package; purely virtual ones print none + static const QRegularExpression package_re(R"(^Package:\s*(\S+))", QRegularExpression::MultilineOption); + return captured_names(package_re, query_output); +} + +QStringList AptBackend::install_command(const QStringList &packages) const { + // apt-get rather than apt: apt's output is not meant for scripts and warns about it + return QStringList{"apt-get", "install", "-y", "-o", "APT::Status-Fd=1"} + packages; +} + +QStringList AptBackend::install_environment() const { + return {"DEBIAN_FRONTEND=noninteractive"}; +} + +std::optional AptBackend::parse_progress(const QString &line) { + // "dlstatus:::" while downloading, "pmstatus:::" in dpkg + const bool downloading = line.startsWith("dlstatus:"); + if (!downloading && !line.startsWith("pmstatus:")) + return std::nullopt; + + const QStringList parts = line.split(':'); + if (parts.size() < 3) + return std::nullopt; + + bool ok; + const double percent = parts[2].toDouble(&ok); + if (!ok) + return std::nullopt; + return half_bar(downloading ? 0 : 50, static_cast(percent), 100); +} diff --git a/src/main.cpp b/src/main.cpp index a41ae36..ae6485a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,28 +1,22 @@ // Author: Apostolos Chalis 2026 #include "MainWindow.hpp" -#include "utils/applyTranslator.hpp" +#include "utils/language.hpp" #include -#include -#include -#include +#include -int main(int argc, char *argv[]){ - qputenv("QT_QPA_PLATFORMTHEME", ""); - QApplication uni_backpack_app(argc, argv); - uni_backpack_app.setOrganizationName("UniBackpack"); +int main(int argc, char *argv[]) { + qputenv("QT_QPA_PLATFORMTHEME", ""); + QApplication uni_backpack_app(argc, argv); + uni_backpack_app.setOrganizationName("UniBackpack"); uni_backpack_app.setApplicationName("UniBackpack"); - uni_backpack_app.setWindowIcon(QIcon(":/icons/unibackpack.png")); - uni_backpack_app.setDesktopFileName("UniBackpack"); + uni_backpack_app.setWindowIcon(QIcon(":/icons/unibackpack.png")); + uni_backpack_app.setDesktopFileName("unibackpack"); - // Load saved language, default to system locale, fall back to English - QSettings settings; - QString lang = settings.value("language", QLocale::system().name()).toString(); - Utils::applyTranslator(lang); + Utils::applyTranslator(Utils::savedLanguage()); + MainWindow main_window; + main_window.show(); - MainWindow main_window; - main_window.show(); - - return uni_backpack_app.exec(); + return uni_backpack_app.exec(); } diff --git a/src/utils/language.cpp b/src/utils/language.cpp index a6796b7..7f3888b 100644 --- a/src/utils/language.cpp +++ b/src/utils/language.cpp @@ -1,24 +1,54 @@ -#include "utils/applyTranslator.hpp" +#include "utils/language.hpp" + +#include +#include +#include +#include #include -#include "QCoreApplication" namespace Utils { - void applyTranslator(const QString &locale) { - static QTranslator elTranslator; - static bool installed = false; - - if (installed) { - QCoreApplication::removeTranslator(&elTranslator); - installed = false; - } + namespace { + const QString language_key = QStringLiteral("language"); - if (locale.startsWith("el")) { - if (elTranslator.isEmpty()) // if no translation is found, the original English text is shown - elTranslator.load(":/i18n/unibackpack_el.qm"); - if (!elTranslator.isEmpty()) { - QCoreApplication::installTranslator(&elTranslator); + // Replaces whatever @p translator held with the best match for @p locale + void reinstall(QTranslator &translator, bool &installed, const QLocale &locale, + const QString &file_name, const QString &directory) { + if (installed) { + QCoreApplication::removeTranslator(&translator); + installed = false; + } + if (translator.load(locale, file_name, "_", directory) && !translator.isEmpty()) { + QCoreApplication::installTranslator(&translator); installed = true; } } } + + QList supportedLanguages() { + return { + {"en_US", "English"}, + {"el_GR", "Ελληνικά"}, + }; + } + + void applyTranslator(const QString &locale) { + static QTranslator app_translator; + static bool app_installed = false; + static QTranslator qt_translator; + static bool qt_installed = false; + + const QLocale qlocale(locale); + // Tries unibackpack_el_GR.qm, then unibackpack_el.qm, and so on + reinstall(app_translator, app_installed, qlocale, "unibackpack", ":/i18n"); + reinstall(qt_translator, qt_installed, qlocale, "qtbase", + QLibraryInfo::path(QLibraryInfo::TranslationsPath)); + } + + QString savedLanguage() { + return QSettings().value(language_key, QLocale::system().name()).toString(); + } + + void saveLanguage(const QString &locale) { + QSettings().setValue(language_key, locale); + } } diff --git a/src/utils/loadUniversities.cpp b/src/utils/loadUniversities.cpp index 6749184..5c4c74c 100644 --- a/src/utils/loadUniversities.cpp +++ b/src/utils/loadUniversities.cpp @@ -1,74 +1,61 @@ -/** - * @file loadUniversities.hpp - * @brief Loads the university/department catalogue from a JSON resource. - */ - #include "utils/loadUniversities.hpp" -#include -#include +#include #include #include -#include +#include #include -#include +#include +#include -/// @brief Free helper functions shared across the UI. namespace Utils { - /** - * @brief Parses the universities JSON and fills the lookup table and list model. - * - * The file must contain a JSON array of objects of the form - * @code{.json} - * [ - * { - * "name": "Aristotle University of Thessaloniki", - * "icon": ":/icons/auth_logo.png", - * "departments": ["Informatics"] - * } - * ] - * @endcode - * Entries with an empty or missing @c name are skipped. - * For each university, one QStandardItem is appended to @p university_model. - * Its display text and Qt::UserRole data are both set to the name, and its icon comes from @c icon. - * - * @param path Path to the JSON file, usually a Qt resource such as ":/universities.json". - * @param departments_by_university Output map from university name to its department names. - * Existing entries with the same key are overwritten. - * @param university_model Model to append university rows to. Must not be null. - * Existing rows are not cleared. - * - * @note If the file cannot be opened or is not a valid JSON array, a warning is logged - * and neither output is modified. - */ - void loadUniversities(const QString &path, QHash &departments_by_university, QStandardItemModel *university_model) { + namespace { + CatalogEntry read_entry(const QJsonObject &object) { + CatalogEntry entry; + entry.name = object["name"].toString(); + const QJsonObject translations = object["translations"].toObject(); + for (auto it = translations.begin(); it != translations.end(); ++it) + entry.translations.insert(it.key(), it.value().toString()); + return entry; + } + } + + QString CatalogEntry::display_name(const QString &locale) const { + const QString language = QLocale(locale).name().section('_', 0, 0); + return translations.value(language, name); + } + + QList loadUniversities(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Could not open" << path; - return; + return {}; } QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); if (error.error != QJsonParseError::NoError || !doc.isArray()) { qWarning() << "Invalid universities JSON:" << error.errorString(); - return; + return {}; } + QList universities; for (const QJsonValue &value : doc.array()) { - QJsonObject uni = value.toObject(); - QString key = uni["name"].toString(); - if (key.isEmpty()) - continue; + const QJsonObject object = value.toObject(); - QStringList departments; - for (const QJsonValue &dept : uni["departments"].toArray()) - departments << dept.toString(); - departments_by_university.insert(key, departments); + University university; + static_cast(university) = read_entry(object); + if (university.name.isEmpty()) + continue; + university.icon = object["icon"].toString(); - QStandardItem *item = new QStandardItem(QIcon(uni["icon"].toString()), key); - item->setData(key, Qt::UserRole); - university_model->appendRow(item); + for (const QJsonValue &department : object["departments"].toArray()) { + CatalogEntry entry = read_entry(department.toObject()); + if (!entry.name.isEmpty()) + university.departments.append(entry); + } + universities.append(university); } + return universities; } } diff --git a/src/utils/packageList.cpp b/src/utils/packageList.cpp new file mode 100644 index 0000000..e5341c4 --- /dev/null +++ b/src/utils/packageList.cpp @@ -0,0 +1,54 @@ +#include "utils/packageList.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +namespace Utils { + QString packageListPath(const QString &university, const QString &department) { + return ":/lists/" + university + "/" + department + "/packages.json"; + } + + QList loadPackageList(const QString &path) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Could not open package list" << path << file.errorString(); + return {}; + } + + QJsonParseError error; + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); + if (error.error != QJsonParseError::NoError || !doc.isArray()) { + qWarning() << "Invalid package list" << path << error.errorString(); + return {}; + } + + QList entries; + for (const QJsonValue &value : doc.array()) { + const QJsonObject object = value.toObject(); + + PackageEntry entry; + entry.name = object["name"].toString(); + if (entry.name.isEmpty()) + continue; + for (auto it = object.begin(); it != object.end(); ++it) { + const QString package = it.value().toString().trimmed(); + if (it.key() != "name" && !package.isEmpty()) + entry.packages.insert(it.key(), package); + } + entries.append(entry); + } + return entries; + } + + bool hasPackagesFor(const QList &entries, const QString &package_manager) { + return std::any_of(entries.begin(), entries.end(), [&package_manager](const PackageEntry &entry) { + return entry.packages.contains(package_manager); + }); + } +} diff --git a/translations/unibackpack_el.ts b/translations/unibackpack_el.ts index 4e36d0c..2110545 100644 --- a/translations/unibackpack_el.ts +++ b/translations/unibackpack_el.ts @@ -4,48 +4,67 @@ Downloader - - + + No %1 package list found for %2. + Δεν βρέθηκε λίστα πακέτων %1 για %2. + + + + Checking packages for %1... + Έλεγχος πακέτων για %1... + + + Found: %1 Βρέθηκε: %1 - - + Not in repos: %1 Δεν βρέθηκε στα αποθετήρια: %1 - - - - - Checking packages for %1... - Έλεγχος πακέτων για %1... + + None of the packages are available in your repositories. + Κανένα από τα πακέτα δεν είναι διαθέσιμο στα αποθετήριά σας. + + + + Installing %n package(s)... + + Εγκατάσταση %n πακέτου... + Εγκατάσταση %n πακέτων... + - - - Adding: %1 - Προσθήκη: %1 + + The package manager stopped unexpectedly. + Ο διαχειριστής πακέτων τερματίστηκε απροσδόκητα. - - - Adding (non-standard): %1 - Προσθήκη (μη τυπικό): %1 + + Authorization was cancelled. + Η εξουσιοδότηση ακυρώθηκε. - - Warning - Προειδοποίηση + + You are not authorized to install packages. + Δεν έχετε δικαίωμα εγκατάστασης πακέτων. - - It is advised to update your system before proceeding. -Run sudo pacman -Syu - Συνιστάται να ενημερώσετε το σύστημά σας πριν συνεχίσετε. -Εκτελέστε sudo pacman -Syu + + The package manager failed with exit code %1. + Ο διαχειριστής πακέτων απέτυχε με κωδικό εξόδου %1. + + + + Could not run %1: %2 + Αδυναμία εκτέλεσης του %1: %2 + + + + Not packaged for %1: %2 + Δεν υπάρχει πακέτο %1 για: %2 @@ -56,52 +75,95 @@ Run sudo pacman -Syu UniBackpack - - + + Show details ▼ Εμφάνιση λεπτομερειών ▼ - + + Hide details ▲ + Απόκρυψη λεπτομερειών ▲ + + + Back to Universities Επιστροφή στην επιλογή Πανεπιστημίων - - Hide details ▲ - Απόκρυψη λεπτομερειών ▲ + + No supported package manager (pacman or apt) was found. + Δεν βρέθηκε υποστηριζόμενος διαχειριστής πακέτων (pacman ή apt). + + + + No package list is available for this department yet. + Δεν υπάρχει ακόμη λίστα πακέτων για αυτό το τμήμα. + + + + Warning + Προειδοποίηση + + + + Checking packages... + Έλεγχος πακέτων... - Aristotle University of Thessaloniki - Αριστοτέλειο Πανεπιστήμιο Θεσσαλονίκης + + ✓ Finished! + ✓ Ολοκληρώθηκε! - University of Western Macedonia - Πανεπιστήμιο Δυτικής Μακεδονίας + + Done! + Έτοιμο! - University of Macedonia - Πανεπιστήμιο Μακεδονίας + + ✗ Installation failed: %1 + ✗ Η εγκατάσταση απέτυχε: %1 - Informatics - Τμήμα Πληροφορικής + + Failed + Αποτυχία - Physics - Τμήμα Φυσικής + + Could not load the list of universities. + Δεν ήταν δυνατή η φόρτωση της λίστας πανεπιστημίων. - Mechanical Engineering - Τμήμα Μηχανολογίας + + Continue + Συνέχεια - Applied Informatics - Τμήμα Εφαρμοσμένης Πληροφορικής + + Cancel + Ακύρωση - Economics - Τμήμα Οικονομικών + + Installation in progress + Εγκατάσταση σε εξέλιξη + + + + Please wait until the installation finishes. + Περιμένετε να ολοκληρωθεί η εγκατάσταση. + + + + PackageManager + + + It is advised to update your system before proceeding. +Run sudo pacman -Syu + Συνιστάται να ενημερώσετε το σύστημά σας πριν συνεχίσετε. +Εκτελέστε sudo pacman -Syu - \ No newline at end of file + diff --git a/translations/unibackpack_en.ts b/translations/unibackpack_en.ts index 9211ae1..8344769 100644 --- a/translations/unibackpack_en.ts +++ b/translations/unibackpack_en.ts @@ -4,46 +4,66 @@ Downloader - - + + No %1 package list found for %2. + + + + + Checking packages for %1... + + + + Found: %1 - - + Not in repos: %1 - - - - - Checking packages for %1... + + None of the packages are available in your repositories. + + + Installing %n package(s)... + + Installing %n package... + Installing %n packages... + + - - - Adding: %1 + + The package manager stopped unexpectedly. - - - Adding (non-standard): %1 + + Authorization was cancelled. - - Warning + + You are not authorized to install packages. - - It is advised to update your system before proceeding. -Run sudo pacman -Syu + + The package manager failed with exit code %1. + + + + + Could not run %1: %2 + + + + + Not packaged for %1: %2 @@ -55,20 +75,93 @@ Run sudo pacman -Syu - - + + Show details ▼ - - + + Hide details ▲ + + + + Back to Universities - - Hide details ▲ + + No supported package manager (pacman or apt) was found. + + + + + No package list is available for this department yet. + + + + + Warning + + + + + Checking packages... + + + + + ✓ Finished! + + + + + Done! + + + + + ✗ Installation failed: %1 + + + + + Failed + + + + + Could not load the list of universities. + + + + + Continue + + + + + Cancel + + + + + Installation in progress + + + + + Please wait until the installation finishes. + + + + + PackageManager + + + It is advised to update your system before proceeding. +Run sudo pacman -Syu diff --git a/ui/MainWindow.ui b/ui/MainWindow.ui index d8189fc..cad56fa 100644 --- a/ui/MainWindow.ui +++ b/ui/MainWindow.ui @@ -15,6 +15,16 @@ + + + + false + + + Back to Universities + + + @@ -52,7 +62,7 @@ - + false @@ -66,16 +76,6 @@ - - - - 0 - 0 - 800 - 29 - - - From b9b73bdf42158a232bee0ff39a0f89c439896306 Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Sat, 10 Oct 2026 23:38:52 +0300 Subject: [PATCH 04/15] Add unit tests for package manager backends and bundled data --- CMakeLists.txt | 13 +++ tests/tst_package_manager.cpp | 176 ++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 tests/tst_package_manager.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 936926e..1365b01 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,8 @@ project(unibackpack VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +option(BUILD_TESTING "Build the unit tests" ON) + find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets LinguistTools) set(CMAKE_AUTOMOC ON) # Handles Signals/Slots @@ -77,3 +79,14 @@ install(FILES resources/unibackpack.desktop install(FILES resources/icons/unibackpack.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/512x512/apps) +if(BUILD_TESTING) + enable_testing() + find_package(Qt6 REQUIRED COMPONENTS Test) + + qt_add_executable(tst_package_manager tests/tst_package_manager.cpp) + target_link_libraries(tst_package_manager PRIVATE unibackpack_core Qt6::Test) + # The tests also check the bundled data files, read from the source tree + target_compile_definitions(tst_package_manager PRIVATE + UNIBACKPACK_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") + add_test(NAME tst_package_manager COMMAND tst_package_manager) +endif() diff --git a/tests/tst_package_manager.cpp b/tests/tst_package_manager.cpp new file mode 100644 index 0000000..27a3a84 --- /dev/null +++ b/tests/tst_package_manager.cpp @@ -0,0 +1,176 @@ +#include "PackageManager.hpp" +#include "utils/loadUniversities.hpp" +#include "utils/packageList.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace { + QStringList sorted(const QSet &set) { + QStringList list(set.begin(), set.end()); + list.sort(); + return list; + } + + // -1 for "no progress", so results print readably when a comparison fails + int progress(PackageManager &manager, const QString &line) { + return manager.parse_progress(line).value_or(-1); + } + + QString write_temporary(QTemporaryFile &file, const QByteArray &contents) { + if (!file.open()) + return {}; + file.write(contents); + file.close(); + return file.fileName(); + } +} + +class TestPackageManager : public QObject { + Q_OBJECT + +private slots: + void pacmanParsesAvailablePackages(); + void aptParsesAvailablePackages(); + void pacmanReportsProgress(); + void aptReportsProgress(); + void displayNameFallsBackToEnglish(); + void loadsPackageList(); + void bundledDataIsConsistent(); +}; + +void TestPackageManager::pacmanParsesAvailablePackages() { + // pacman -Si bash nodejs not-a-package, run with LC_ALL=C (the error goes to stderr) + const QString output = + "Repository : core\n" + "Name : bash\n" + "Version : 5.2.037-1\n" + "Description : The GNU Bourne Again shell\n" + "\n" + "Repository : extra\n" + "Name : nodejs\n" + "Version : 23.5.0-1\n"; + + PacmanBackend pacman; + QCOMPARE(sorted(pacman.parse_available(output)), (QStringList{"bash", "nodejs"})); +} + +void TestPackageManager::aptParsesAvailablePackages() { + // apt-cache show --no-all-versions bash gcc, run with LC_ALL=C + const QString output = + "Package: bash\n" + "Version: 5.2.21-2ubuntu4\n" + "Pre-Depends: libc6 (>= 2.38)\n" + "Description: GNU Bourne Again SHell\n" + "\n" + "Package: gcc\n" + "Version: 4:13.2.0-7ubuntu1\n" + "Package-Type: deb\n"; + + AptBackend apt; + QCOMPARE(sorted(apt.parse_available(output)), (QStringList{"bash", "gcc"})); +} + +void TestPackageManager::pacmanReportsProgress() { + PacmanBackend pacman; + pacman.begin_install(2); + + // The real total, with dependencies, replaces the requested count + QCOMPARE(progress(pacman, "Packages (4) a-1-1 b-1-1 c-1-1 d-1-1"), -1); + + // Downloads fill 0-49 + QCOMPARE(progress(pacman, " a-1-1-x86_64 downloading..."), 12); + QCOMPARE(progress(pacman, " b-1-1-x86_64 downloading..."), 25); + + // Installs fill 50-99, counted or taken from "(n/total)" + QCOMPARE(progress(pacman, "installing a..."), 62); + QCOMPARE(progress(pacman, "(4/4) installing d..."), 99); + QCOMPARE(progress(pacman, "(1/4) checking keys in keyring"), -1); + QCOMPARE(progress(pacman, ":: Processing package changes..."), -1); +} + +void TestPackageManager::aptReportsProgress() { + AptBackend apt; + apt.begin_install(3); + + // Downloads fill 0-49 and dpkg 50-99, so the bar never moves backwards + QCOMPARE(progress(apt, "dlstatus:1:20.0:Retrieving file 1 of 5"), 10); + QCOMPARE(progress(apt, "pmstatus:bash:50.0:Installing bash"), 75); + QCOMPARE(progress(apt, "pmstatus:bash:100:Installed bash"), 99); + QCOMPARE(progress(apt, "Reading package lists..."), -1); + QCOMPARE(progress(apt, "pmstatus:broken"), -1); +} + +void TestPackageManager::displayNameFallsBackToEnglish() { + Utils::CatalogEntry entry; + entry.name = "Informatics"; + entry.translations.insert("el", "Τμήμα Πληροφορικής"); + + QCOMPARE(entry.display_name("el_GR"), QString("Τμήμα Πληροφορικής")); + QCOMPARE(entry.display_name("en_US"), QString("Informatics")); + QCOMPARE(entry.display_name("de_DE"), QString("Informatics")); +} + +void TestPackageManager::loadsPackageList() { + QTemporaryFile file; + const QString path = write_temporary(file, R"([ + { "name": "Java JDK", "apt": "default-jdk", "pacman": "jdk-openjdk" }, + { "name": "FreeCAD", "pacman": "freecad", "apt": " " }, + { "apt": "nameless" } + ])"); + QVERIFY(!path.isEmpty()); + + const QList entries = Utils::loadPackageList(path); + QCOMPARE(entries.size(), 2); + QCOMPARE(entries[0].name, QString("Java JDK")); + QCOMPARE(entries[0].packages.value("apt"), QString("default-jdk")); + QCOMPARE(entries[0].packages.value("pacman"), QString("jdk-openjdk")); + QVERIFY(!entries[1].packages.contains("apt")); + + QVERIFY(Utils::hasPackagesFor(entries, "pacman")); + QVERIFY(!Utils::hasPackagesFor(entries, "dnf")); + QTest::ignoreMessage(QtWarningMsg, QRegularExpression("^Could not open package list")); + QVERIFY(Utils::loadPackageList("/does/not/exist.json").isEmpty()); +} + +void TestPackageManager::bundledDataIsConsistent() { + const QDir resources(QStringLiteral(UNIBACKPACK_SOURCE_DIR "/resources")); + + const QList universities = + Utils::loadUniversities(resources.filePath("universities.json")); + QVERIFY2(!universities.isEmpty(), "universities.json is missing or invalid"); + + QSet catalogued_lists; + for (const Utils::University &university : universities) { + QVERIFY2(QFile::exists(resources.filePath(university.icon.mid(2))), + qPrintable("Missing icon " + university.icon)); + + for (const Utils::CatalogEntry &department : university.departments) { + const QString path = resources.filePath("lists/" + university.name + "/" + department.name + "/packages.json"); + catalogued_lists.insert(QDir::cleanPath(path)); + if (!QFile::exists(path)) + continue; // Shown greyed out in the app + + const QList entries = Utils::loadPackageList(path); + QVERIFY2(!entries.isEmpty(), qPrintable(path + " is empty or invalid")); + for (const Utils::PackageEntry &entry : entries) + QVERIFY2(!entry.packages.isEmpty(), qPrintable(entry.name + " in " + path + " has no packages")); + } + } + + // A list whose directory names don't match the catalogue is never shown + QDirIterator it(resources.filePath("lists"), {"packages.json"}, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + const QString path = QDir::cleanPath(it.next()); + QVERIFY2(catalogued_lists.contains(path), + qPrintable(path + " does not match a university and department in universities.json")); + } +} + +QTEST_GUILESS_MAIN(TestPackageManager) +#include "tst_package_manager.moc" From 14a5dd52c3ca6fddb777c0cfae16f74f53f828b8 Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Sun, 11 Oct 2026 11:04:27 +0300 Subject: [PATCH 05/15] Build and run tests in CI --- .github/workflows/build.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8006a15..0000a1a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Static Analysis +name: Build, Test and Static Analysis on: push: @@ -8,10 +8,10 @@ on: branches: [ "main" ] jobs: - validate-and-package: + build-test-analyze: runs-on: ubuntu-24.04 permissions: - contents: write + contents: read steps: - name: Checkout Source Code @@ -24,11 +24,17 @@ jobs: sudo apt-get update sudo apt-get install -y \ build-essential cmake \ - qt6-base-dev qt6-tools-dev qt6-tools-dev-tools \ + qt6-base-dev qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \ libqt6widgets6 \ - cppcheck \ - dpkg-dev + cppcheck + - name: Build + run: | + cmake -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build --parallel + + - name: Run Tests + run: ctest --test-dir build --output-on-failure - name: Run cppcheck Static Analysis run: | @@ -36,4 +42,3 @@ jobs: --suppress=shadowVariable \ --error-exitcode=1 \ src/ - From 2e4dfc517640eb2f90a3ce88601e8bc555cd9990 Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Sun, 11 Oct 2026 11:27:09 +0300 Subject: [PATCH 06/15] Install desktop file and icon through CMake, run tests in PKGBUILD --- PKGBUILD | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 4130602..46890fd 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -29,17 +29,10 @@ build() { cmake --build build } +check() { + ctest --test-dir build --output-on-failure +} + package() { DESTDIR="$pkgdir" cmake --install build - - install -Dm644 "$srcdir/$_pkgname/resources/unibackpack.desktop" \ - "$pkgdir/usr/share/applications/unibackpack.desktop" - - if [ -f "$srcdir/$_pkgname/resources/icons/unibackpack.png" ]; then - install -Dm644 "$srcdir/$_pkgname/resources/icons/unibackpack.png" \ - "$pkgdir/usr/share/pixmaps/unibackpack.png" - else - sed -i 's/^Icon=.*/Icon=system-software-install/' \ - "$pkgdir/usr/share/applications/unibackpack.desktop" - fi } From 51254eb8beeedd907cadd6f943b299fb4c06dffb Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Sun, 11 Oct 2026 12:09:31 +0300 Subject: [PATCH 07/15] Update README with package lists, building and tests --- README.md | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f6924ac..ed35811 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ UniBackpack is a lightweight C++ Qt application designed to automate the install Before building UniBackpack, ensure that the following dependencies are installed on your system: * A C++17 compatible compiler (`gcc` or `clang`) -* CMake (version 3.10 or higher) +* CMake (version 3.16 or higher) * Qt6 Development Libraries * Polkit (`pkexec`) for granting installation privileges @@ -21,7 +21,7 @@ sudo pacman -S base-devel cmake qt6-base qt6-tools polkit **For Debian / Ubuntu / Linux Mint:** ```bash -sudo apt install build-essential cmake qt6-base-dev qt6-tools-dev qt6-tools-dev-tools policykit-1 +sudo apt install build-essential cmake qt6-base-dev qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools pkexec ``` ## Project Structure @@ -29,8 +29,43 @@ sudo apt install build-essential cmake qt6-base-dev qt6-tools-dev qt6-tools-dev- * `src/` - C++ source files (`.cpp`) * `include/` - C++ header files (`.hpp`) * `ui/` - Qt Designer forms (`.ui`) -* `resources/lists/` - Configuration files containing the software lists tailored for each academic department. -* `resources/icons/` - Icons for each academic department. +* `resources/lists///packages.json` - The software list for each academic department, with each tool's package name for every supported package manager. Every list in this directory is embedded automatically at build time. +* `resources/universities.json` - The universities and departments shown in the app, with their translated names. +* `resources/icons/` - Icons for each university. +* `translations/` - Qt Linguist translation files for the user interface. +* `tests/` - Unit tests for the package manager backends and the data files. + +## Building + +```bash +cmake -B build +cmake --build build +ctest --test-dir build --output-on-failure +./build/unibackpack +``` + +To install system-wide, including the desktop entry and icon: + +```bash +sudo cmake --install build +``` + +## Adding a Department + +1. Add the department to its university in `resources/universities.json`, with an `"el"` translation of its name. +2. Create `resources/lists///packages.json`, using the exact English names from `universities.json` for the directories: + + ```json + [ + { "name": "Java JDK", "apt": "default-jdk", "pacman": "jdk-openjdk" }, + { "name": "FreeCAD", "pacman": "freecad" } + ] + ``` + + Leave out a package manager when the software isn't packaged for it. +3. Run the tests: they check that every list parses and matches a department. + +Departments without packages for the user's package manager are shown greyed out. ## Contributors From 73bebac03a9f78609118c3eccfd44464bf8df0e1 Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Sun, 11 Oct 2026 13:35:48 +0300 Subject: [PATCH 08/15] Build and test on Arch Linux with pacman in CI --- .github/workflows/build.yml | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0000a1a..9c45da5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,11 +7,13 @@ on: pull_request: branches: [ "main" ] +permissions: + contents: read + jobs: - build-test-analyze: + ubuntu-apt: + name: Ubuntu (apt) runs-on: ubuntu-24.04 - permissions: - contents: read steps: - name: Checkout Source Code @@ -42,3 +44,30 @@ jobs: --suppress=shadowVariable \ --error-exitcode=1 \ src/ + + arch-pacman: + name: Arch Linux (pacman) + runs-on: ubuntu-24.04 + container: archlinux:latest + + steps: + - name: Install Build Dependencies + run: pacman -Syu --noconfirm --needed base-devel cmake qt6-base qt6-tools + + - name: Checkout Source Code + uses: actions/checkout@v7 + + - name: Build + run: | + cmake -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build --parallel + + - name: Run Tests + run: ctest --test-dir build --output-on-failure + + - name: Check Install Layout + run: | + DESTDIR="$PWD/pkg" cmake --install build --prefix /usr + test -x pkg/usr/bin/unibackpack + test -f pkg/usr/share/applications/unibackpack.desktop + test -f pkg/usr/share/icons/hicolor/512x512/apps/unibackpack.png From f790c6e437d64628cfb3931d51350e8d68a0cf60 Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Sun, 11 Oct 2026 14:47:12 +0300 Subject: [PATCH 09/15] Use std::find_if in find_university to satisfy cppcheck --- src/MainWindow.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 1729120..91e0b97 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -15,6 +15,8 @@ #include #include +#include + namespace { const QString success_chunk_style = QStringLiteral("QProgressBar::chunk { background-color: #4CAF50; }"); const QString failure_chunk_style = QStringLiteral("QProgressBar::chunk { background-color: #f44336; }"); @@ -199,11 +201,9 @@ void MainWindow::set_status(std::function text) { } const Utils::University *MainWindow::find_university(const QString &name) const { - for (const Utils::University &university : universities) { - if (university.name == name) - return &university; - } - return nullptr; + const auto it = std::find_if(universities.cbegin(), universities.cend(), + [&name](const Utils::University &university) { return university.name == name; }); + return it == universities.cend() ? nullptr : &*it; } void MainWindow::retranslate() { From 4c5fb1cacd8136d6fc32733a369e9dc68d8916f4 Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Sun, 11 Oct 2026 15:20:47 +0300 Subject: [PATCH 10/15] Restructure README to follow the UniOS docs guidelines --- README.md | 171 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 137 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index ed35811..70a9739 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,59 @@ # UniBackpack -UniBackpack is a lightweight C++ Qt application designed to automate the installation of essential software and development tools for university students. By selecting the target University and Department, the application automatically detects the underlying Linux distribution's package manager (`apt` or `pacman`) and installs the required toolchain. +UniBackpack is a lightweight C++ Qt application that installs the software and development tools a university student needs for their studies. You pick your university and department, and UniBackpack detects your Linux distribution's package manager (`pacman` or `apt`) and installs that department's toolchain in one go. -## Prerequisites +It ships with UniOS, and runs on any Arch- or Debian-based distribution. -Before building UniBackpack, ensure that the following dependencies are installed on your system: +## How it works -* A C++17 compatible compiler (`gcc` or `clang`) -* CMake (version 3.16 or higher) -* Qt6 Development Libraries -* Polkit (`pkexec`) for granting installation privileges +1. The app lists the universities, then the departments of the one you pick. Departments that have no package list for your package manager yet are greyed out. +2. When you pick a department, UniBackpack checks which of its packages exist in your repositories and reports the ones that don't. +3. It installs the available ones through `pkexec`, so you are asked for your password once, and shows the progress and the package manager's output. -### Installing Dependencies +The interface is available in English and Greek, selectable from the status bar. -**For Arch Linux / Manjaro / EndeavourOS:** +## Installation + +### As a user + +**On UniOS**, UniBackpack is preinstalled. If you removed it, install it again from `unios-repo`: ```bash -sudo pacman -S base-devel cmake qt6-base qt6-tools polkit +sudo pacman -S unibackpack ``` -**For Debian / Ubuntu / Linux Mint:** +**On other Arch-based distributions**, build the package from the `PKGBUILD` in this repository: + ```bash -sudo apt install build-essential cmake qt6-base-dev qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools pkexec +git clone https://github.com/open-source-uom/UniBackpack.git +cd UniBackpack +makepkg -si ``` -## Project Structure +**On Debian-based distributions**, build and install it from source as described below. -* `src/` - C++ source files (`.cpp`) -* `include/` - C++ header files (`.hpp`) -* `ui/` - Qt Designer forms (`.ui`) -* `resources/lists///packages.json` - The software list for each academic department, with each tool's package name for every supported package manager. Every list in this directory is embedded automatically at build time. -* `resources/universities.json` - The universities and departments shown in the app, with their translated names. -* `resources/icons/` - Icons for each university. -* `translations/` - Qt Linguist translation files for the user interface. -* `tests/` - Unit tests for the package manager backends and the data files. +### As a developer -## Building +Requirements: + +* A C++17 compiler (`gcc` or `clang`) +* CMake 3.16 or newer +* Qt 6 (Core, Gui, Widgets, LinguistTools and Test) +* Polkit (`pkexec`), to run the installs as root + +Install them on **Arch Linux / UniOS / Manjaro / EndeavourOS**: + +```bash +sudo pacman -S base-devel cmake qt6-base qt6-tools polkit +``` + +or on **Debian / Ubuntu / Linux Mint**: + +```bash +sudo apt install build-essential cmake qt6-base-dev qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools pkexec +``` + +Then build, run the tests and start the app: ```bash cmake -B build @@ -44,34 +62,119 @@ ctest --test-dir build --output-on-failure ./build/unibackpack ``` -To install system-wide, including the desktop entry and icon: +To install it system-wide, including the desktop entry and icon: ```bash sudo cmake --install build ``` +Every push and pull request is built and tested on both Ubuntu (apt) and Arch Linux (pacman) by GitHub Actions, which also runs `cppcheck`. API documentation can be generated with `doxygen` from the repository root; it is written to `docs/`. + +## Project Structure + +``` +UniBackpack/ +├── CMakeLists.txt build, install and test configuration +├── PKGBUILD Arch package recipe +├── Doxyfile API documentation settings +├── include/ headers (.hpp) +│ ├── Downloader.hpp runs the repository check and the install +│ ├── MainWindow.hpp the application window +│ ├── PackageManager.hpp pacman and apt backends +│ └── utils/ +│ ├── language.hpp supported languages and switching +│ ├── loadUniversities.hpp reads universities.json +│ └── packageList.hpp reads a department's packages.json +├── src/ sources (.cpp), same layout as include/ +├── ui/ +│ └── MainWindow.ui Qt Designer form +├── resources/ +│ ├── universities.json universities and departments, with Greek names +│ ├── lists/ +│ │ └── //packages.json +│ ├── icons/ university logos and the app icon +│ ├── resources.qrc icons and universities.json +│ └── unibackpack.desktop desktop entry +├── translations/ Qt Linguist files (unibackpack_el.ts, unibackpack_en.ts) +├── tests/ +│ └── tst_package_manager.cpp +└── .github/workflows/build.yml +``` + +All non-UI code is built as the `unibackpack_core` library, so the tests can link against it. The package lists are embedded into the binary automatically at build time; there is no need to list them in `resources.qrc`. + +### Data files + +`resources/universities.json` lists the universities and their departments. The English `name` is also the directory name under `resources/lists/`, and `translations` holds the names shown in other languages: + +```json +[ + { + "name": "University of Western Macedonia", + "translations": { "el": "Πανεπιστήμιο Δυτικής Μακεδονίας" }, + "icon": ":/icons/uowm_logo.png", + "departments": [ + { "name": "Informatics", "translations": { "el": "Τμήμα Πληροφορικής" } } + ] + } +] +``` + +`resources/lists///packages.json` lists the software for one department, with its package name for each package manager. Leave a package manager out when the software isn't packaged for it: + +```json +[ + { "name": "Java JDK", "apt": "default-jdk", "pacman": "jdk-openjdk" }, + { "name": "MariaDB", "apt": "mariadb-server", "pacman": "mariadb" }, + { "name": "FreeCAD", "pacman": "freecad" } +] +``` + +### Files the app writes + +UniBackpack only stores the language you picked, through `QSettings`, in `~/.config/UniBackpack/UniBackpack.conf`: + +```ini +[General] +language=el_GR +``` + +Without it, the app follows the system language. + ## Adding a Department 1. Add the department to its university in `resources/universities.json`, with an `"el"` translation of its name. -2. Create `resources/lists///packages.json`, using the exact English names from `universities.json` for the directories: +2. Create `resources/lists///packages.json`, using the exact English names from `universities.json` for the directories. +3. Run the tests. They check that every list parses, that every entry has at least one package, and that every list matches a department in `universities.json`. + +## Adding a Translation + +1. Add the language to `Utils::supportedLanguages()` in `src/utils/language.cpp`. +2. Add `translations/unibackpack_.ts` to `qt_add_translations` in `CMakeLists.txt`. +3. Build the `update_translations` target to fill it with the source strings, translate them with Qt Linguist, and add a `translations` entry for that language to every university and department in `universities.json`. - ```json - [ - { "name": "Java JDK", "apt": "default-jdk", "pacman": "jdk-openjdk" }, - { "name": "FreeCAD", "pacman": "freecad" } - ] - ``` +## Roadmap - Leave out a package manager when the software isn't packaged for it. -3. Run the tests: they check that every list parses and matches a department. +Help is welcome on: -Departments without packages for the user's package manager are shown greyed out. +* Package lists for the departments that don't have one yet. Only UoWM Informatics has one so far. +* `apt` package names for FreeCAD and Wireshark in the UoWM Informatics list. +* A backend for `dnf` (Fedora). +## Recent Changes + +* The install logic moved into separate `pacman` and `apt` backends behind a `PackageManager` interface, with unit tests for their output parsing. +* The two per-department text lists (`pacman_list.txt`, `apt_list.txt`) were merged into one `packages.json`. +* University and department names are translated in `universities.json` instead of the `.ts` files. +* The repository check is now a single non-blocking call, and failures always report a reason instead of leaving the window disabled. +* CI builds and tests on both Ubuntu and Arch Linux. ## Contributors +Maintained by Apostolos Chalis and Ioannis Michadasis , with contributions from: + ---- \ No newline at end of file +UniBackpack is part of the [UniOS project](https://unios-project.eu). See the [UniOS docs](https://docs.unios-project.eu) for the general development workflow. From 18191d32bd830f731cf7a81e4e28ead71a172b87 Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Mon, 12 Oct 2026 01:59:22 +0300 Subject: [PATCH 11/15] Refresh apt lists before installing and never wait on stdin --- include/PackageManager.hpp | 3 ++- src/Downloader.cpp | 2 ++ src/PackageManager.cpp | 11 +++++++++-- tests/tst_package_manager.cpp | 14 ++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/include/PackageManager.hpp b/include/PackageManager.hpp index 50fcc5a..7162f7e 100644 --- a/include/PackageManager.hpp +++ b/include/PackageManager.hpp @@ -119,7 +119,8 @@ class PacmanBackend : public PackageManager { /** * @brief Backend for Debian-based systems (apt-get). * - * Progress comes from the machine-readable status lines that apt-get writes + * Refreshes the package lists before installing. Progress comes from the + * machine-readable status lines that apt-get writes * when run with @c APT::Status-Fd=1: downloads fill the first half of the bar * and dpkg the second. */ diff --git a/src/Downloader.cpp b/src/Downloader.cpp index fb15059..e8b5747 100644 --- a/src/Downloader.cpp +++ b/src/Downloader.cpp @@ -95,6 +95,8 @@ void Downloader::install(const QStringList &packages) { pending_output.clear(); QProcess *process = new QProcess(this); + // Nobody can answer a prompt, so a read must hit EOF instead of waiting forever + process->setStandardInputFile(QProcess::nullDevice()); connect(process, &QProcess::readyReadStandardOutput, this, [this, process]() { handle_install_output(process->readAllStandardOutput()); diff --git a/src/PackageManager.cpp b/src/PackageManager.cpp index 060b00e..858ba52 100644 --- a/src/PackageManager.cpp +++ b/src/PackageManager.cpp @@ -134,8 +134,15 @@ QSet AptBackend::parse_available(const QString &query_output) const { } QStringList AptBackend::install_command(const QStringList &packages) const { - // apt-get rather than apt: apt's output is not meant for scripts and warns about it - return QStringList{"apt-get", "install", "-y", "-o", "APT::Status-Fd=1"} + packages; + // Stale package lists can 404 on download, so refresh them first (this upgrades nothing), + // in the same shell so the user authorizes once. Packages reach the script as "$@". + // apt-get rather than apt: apt's output is not meant for scripts and warns about it. + // Only the install gets Status-Fd, so the update does not move the progress bar. + // The Dpkg options keep a locally modified config file on upgrade instead of asking. + static const QString script = + "apt-get update && exec apt-get install -y -o APT::Status-Fd=1" + " -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold \"$@\""; + return QStringList{"sh", "-c", script, "sh"} + packages; } QStringList AptBackend::install_environment() const { diff --git a/tests/tst_package_manager.cpp b/tests/tst_package_manager.cpp index 27a3a84..08e7ee7 100644 --- a/tests/tst_package_manager.cpp +++ b/tests/tst_package_manager.cpp @@ -39,6 +39,7 @@ private slots: void aptParsesAvailablePackages(); void pacmanReportsProgress(); void aptReportsProgress(); + void aptUpdatesBeforeInstalling(); void displayNameFallsBackToEnglish(); void loadsPackageList(); void bundledDataIsConsistent(); @@ -106,6 +107,19 @@ void TestPackageManager::aptReportsProgress() { QCOMPARE(progress(apt, "pmstatus:broken"), -1); } +void TestPackageManager::aptUpdatesBeforeInstalling() { + AptBackend apt; + const QStringList command = apt.install_command({"gcc", "$(reboot)"}); + + // One shell runs update, then install; package names stay separate arguments ("$@") + QCOMPARE(command.size(), 6); + QCOMPARE(command.mid(0, 2), (QStringList{"sh", "-c"})); + QVERIFY(command[2].startsWith("apt-get update && exec apt-get install ")); + QVERIFY(command[2].contains("--force-confold")); + QVERIFY(!command[2].contains("gcc")); + QCOMPARE(command.mid(3), (QStringList{"sh", "gcc", "$(reboot)"})); +} + void TestPackageManager::displayNameFallsBackToEnglish() { Utils::CatalogEntry entry; entry.name = "Informatics"; From 2d9d0d3e98ec714ef877d5e8f3648a1b3223da52 Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Mon, 12 Oct 2026 01:59:22 +0300 Subject: [PATCH 12/15] Enable compiler warnings and treat them as errors in CI --- .github/workflows/build.yml | 4 ++-- CMakeLists.txt | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c45da5..6d72734 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: - name: Build run: | - cmake -B build -DCMAKE_BUILD_TYPE=Release + cmake -B build -DCMAKE_BUILD_TYPE=Release -DUNIBACKPACK_WERROR=ON cmake --build build --parallel - name: Run Tests @@ -59,7 +59,7 @@ jobs: - name: Build run: | - cmake -B build -DCMAKE_BUILD_TYPE=Release + cmake -B build -DCMAKE_BUILD_TYPE=Release -DUNIBACKPACK_WERROR=ON cmake --build build --parallel - name: Run Tests diff --git a/CMakeLists.txt b/CMakeLists.txt index 1365b01..a779856 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,15 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) option(BUILD_TESTING "Build the unit tests" ON) +option(UNIBACKPACK_WERROR "Treat compiler warnings as errors" OFF) + +# Applied to every target defined below; Qt's headers are system includes and stay quiet +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + add_compile_options(-Wall -Wextra -Wpedantic) + if(UNIBACKPACK_WERROR) + add_compile_options(-Werror) + endif() +endif() find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets LinguistTools) From 9b33e589e2c00fa5b1f147d35ff4ad4a8e6b1318 Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Mon, 12 Oct 2026 02:19:11 +0300 Subject: [PATCH 13/15] Add a Cancel button for installs that have not reached the package manager --- include/Downloader.hpp | 20 +++++++++++ include/MainWindow.hpp | 2 +- src/Downloader.cpp | 34 +++++++++++++++++- src/MainWindow.cpp | 14 ++++++-- translations/unibackpack_el.ts | 66 ++++++++++++++++++++++------------ translations/unibackpack_en.ts | 66 ++++++++++++++++++++++------------ ui/MainWindow.ui | 10 ++++++ 7 files changed, 164 insertions(+), 48 deletions(-) diff --git a/include/Downloader.hpp b/include/Downloader.hpp index f383041..4cb742a 100644 --- a/include/Downloader.hpp +++ b/include/Downloader.hpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -48,7 +49,22 @@ class Downloader : public QObject { */ void start(); + /** + * @brief Stops the run if it is still safe to, and finishes it as cancelled. + * + * Possible while checking the packages and while pkexec waits for + * authorization. Once the package manager runs as root it can no longer + * be stopped, and stopping it could leave the package database broken, + * so the install then continues and cancellable_changed(false) is emitted. + */ + void cancel(); + signals: + /** + * @brief Whether cancel() can still stop the run. It starts out true. + */ + void cancellable_changed(bool cancellable); + /** * @brief Overall progress of the install, from 0 to 99. */ @@ -76,6 +92,7 @@ class Downloader : public QObject { void check_packages(const QHash &software_by_package); void install(const QStringList &packages); + void mark_package_manager_running(); void handle_install_output(const QByteArray &data); void flush_install_output(); void connect_start_failure(QProcess *process); @@ -87,6 +104,9 @@ class Downloader : public QObject { QString name_of_department; std::shared_ptr package_manager; QString pending_output; ///< Install output after the last complete line + QPointer current_process; ///< The query or install process, while it runs + bool cancel_requested = false; + bool package_manager_running = false; ///< The install printed output, so it runs as root }; #endif // DOWNLOADER_HPP diff --git a/include/MainWindow.hpp b/include/MainWindow.hpp index 12357ac..c5a5efb 100644 --- a/include/MainWindow.hpp +++ b/include/MainWindow.hpp @@ -60,7 +60,7 @@ class MainWindow : public QMainWindow void changeEvent(QEvent *event) override; /** - * @brief Keeps the window open while an installation is running. + * @brief Keeps the window open once the package manager is installing. */ void closeEvent(QCloseEvent *event) override; diff --git a/src/Downloader.cpp b/src/Downloader.cpp index e8b5747..b03bd72 100644 --- a/src/Downloader.cpp +++ b/src/Downloader.cpp @@ -55,6 +55,7 @@ void Downloader::check_packages(const QHash &software_by_packa packages.sort(); QProcess *query = new QProcess(this); + current_process = query; // parse_available() matches untranslated field names QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); @@ -63,6 +64,12 @@ void Downloader::check_packages(const QHash &software_by_packa connect_start_failure(query); connect(query, &QProcess::finished, this, [this, query, packages, software_by_package]() { + if (cancel_requested) { + query->deleteLater(); + fail(tr("The installation was cancelled.")); + return; + } + // The exit code is non-zero as soon as one package is missing, so only the output is used const QSet available = package_manager->parse_available(QString::fromUtf8(query->readAllStandardOutput())); @@ -95,13 +102,16 @@ void Downloader::install(const QStringList &packages) { pending_output.clear(); QProcess *process = new QProcess(this); + current_process = process; // Nobody can answer a prompt, so a read must hit EOF instead of waiting forever process->setStandardInputFile(QProcess::nullDevice()); connect(process, &QProcess::readyReadStandardOutput, this, [this, process]() { + mark_package_manager_running(); handle_install_output(process->readAllStandardOutput()); }); connect(process, &QProcess::readyReadStandardError, this, [this, process]() { + mark_package_manager_running(); const QString errors = QString::fromUtf8(process->readAllStandardError()).trimmed(); if (!errors.isEmpty()) emit log_message(errors); @@ -113,7 +123,9 @@ void Downloader::install(const QStringList &packages) { flush_install_output(); process->deleteLater(); - if (status != QProcess::NormalExit) + if (status != QProcess::NormalExit && cancel_requested) + fail(tr("The installation was cancelled.")); + else if (status != QProcess::NormalExit) fail(tr("The package manager stopped unexpectedly.")); else if (exit_code != 0) fail(describe_exit(exit_code)); @@ -130,6 +142,26 @@ void Downloader::install(const QStringList &packages) { process->start("pkexec", arguments); } +void Downloader::cancel() { + if (!current_process || cancel_requested || package_manager_running) + return; + cancel_requested = true; + report(tr("Cancelling...")); + // Before authorization pkexec still runs as the user, so this reaches it and polkit + // drops the pending request. Once pkexec has become root the signal is refused, + // the install carries on, and mark_package_manager_running() tells the user so. + current_process->kill(); +} + +void Downloader::mark_package_manager_running() { + if (package_manager_running) + return; + package_manager_running = true; + emit cancellable_changed(false); + if (cancel_requested) + report(tr("The installation had already started and can no longer be cancelled.")); +} + void Downloader::handle_install_output(const QByteArray &data) { // Chunks can end mid-line; keep the partial line until the rest arrives pending_output += QString::fromUtf8(data); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 91e0b97..f87b346 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -47,6 +47,10 @@ MainWindow::MainWindow(QWidget *parent) connect(ui->listView, &QListView::clicked, this, &MainWindow::handle_item_clicked); connect(ui->backButton, &QPushButton::clicked, this, &MainWindow::show_universities); connect(ui->showMoreButton, &QPushButton::clicked, this, &MainWindow::toggle_output); + connect(ui->cancelButton, &QPushButton::clicked, this, [this]() { + if (downloader) + downloader->cancel(); + }); package_manager = PackageManager::detect(); @@ -148,6 +152,8 @@ void MainWindow::start_install(const QString &department) { ui->progressBar->setStyleSheet(""); ui->progressBar->setVisible(true); ui->showMoreButton->setVisible(true); + ui->cancelButton->setEnabled(true); + ui->cancelButton->setVisible(true); set_status([this]() { return tr("Checking packages..."); }); retranslate(); @@ -159,6 +165,7 @@ void MainWindow::start_install(const QString &department) { set_status([message]() { return message; }); }); connect(downloader, &Downloader::progress_updated, ui->progressBar, &QProgressBar::setValue); + connect(downloader, &Downloader::cancellable_changed, ui->cancelButton, &QPushButton::setEnabled); connect(downloader, &Downloader::finished, this, &MainWindow::finish_install); downloader->start(); @@ -181,6 +188,7 @@ void MainWindow::finish_install(bool success, const QString &error) { install_state = success ? InstallState::Succeeded : InstallState::Failed; ui->listView->setEnabled(true); ui->backButton->setEnabled(true); + ui->cancelButton->setVisible(false); ui->progressBar->setValue(100); ui->progressBar->setStyleSheet(success ? success_chunk_style : failure_chunk_style); @@ -244,6 +252,7 @@ void MainWindow::retranslate() { } ui->showMoreButton->setText(output_visible ? tr("Hide details ▲") : tr("Show details ▼")); + ui->cancelButton->setToolTip(tr("Once the package manager has started, stopping it could break your system.")); } void MainWindow::changeEvent(QEvent *event) { @@ -255,8 +264,9 @@ void MainWindow::changeEvent(QEvent *event) { } void MainWindow::closeEvent(QCloseEvent *event) { - // Interrupting apt or pacman can leave the package database broken - if (install_state == InstallState::Running) { + // Interrupting apt or pacman can leave the package database broken. Before they + // start, closing is a cancel: the processes are killed along with the Downloader. + if (install_state == InstallState::Running && !ui->cancelButton->isEnabled()) { QMessageBox::information(this, tr("Installation in progress"), tr("Please wait until the installation finishes.")); event->ignore(); diff --git a/translations/unibackpack_el.ts b/translations/unibackpack_el.ts index 2110545..2b87488 100644 --- a/translations/unibackpack_el.ts +++ b/translations/unibackpack_el.ts @@ -14,22 +14,28 @@ Έλεγχος πακέτων για %1... - + + + The installation was cancelled. + Η εγκατάσταση ακυρώθηκε. + + + Found: %1 Βρέθηκε: %1 - + Not in repos: %1 Δεν βρέθηκε στα αποθετήρια: %1 - + None of the packages are available in your repositories. Κανένα από τα πακέτα δεν είναι διαθέσιμο στα αποθετήριά σας. - + Installing %n package(s)... Εγκατάσταση %n πακέτου... @@ -37,7 +43,7 @@ - + The package manager stopped unexpectedly. Ο διαχειριστής πακέτων τερματίστηκε απροσδόκητα. @@ -57,7 +63,17 @@ Ο διαχειριστής πακέτων απέτυχε με κωδικό εξόδου %1. - + + Cancelling... + Ακύρωση... + + + + The installation had already started and can no longer be cancelled. + Η εγκατάσταση είχε ήδη ξεκινήσει και δεν μπορεί πλέον να ακυρωθεί. + + + Could not run %1: %2 Αδυναμία εκτέλεσης του %1: %2 @@ -75,13 +91,13 @@ UniBackpack - - + + Show details ▼ Εμφάνιση λεπτομερειών ▼ - + Hide details ▲ Απόκρυψη λεπτομερειών ▲ @@ -91,67 +107,73 @@ Επιστροφή στην επιλογή Πανεπιστημίων - + No supported package manager (pacman or apt) was found. Δεν βρέθηκε υποστηριζόμενος διαχειριστής πακέτων (pacman ή apt). - + No package list is available for this department yet. Δεν υπάρχει ακόμη λίστα πακέτων για αυτό το τμήμα. - + Warning Προειδοποίηση - + Checking packages... Έλεγχος πακέτων... - + ✓ Finished! ✓ Ολοκληρώθηκε! - + Done! Έτοιμο! - + + Once the package manager has started, stopping it could break your system. + Αφού ξεκινήσει ο διαχειριστής πακέτων, η διακοπή του μπορεί να χαλάσει το σύστημά σας. + + + ✗ Installation failed: %1 ✗ Η εγκατάσταση απέτυχε: %1 - + Failed Αποτυχία - + Could not load the list of universities. Δεν ήταν δυνατή η φόρτωση της λίστας πανεπιστημίων. - + Continue Συνέχεια - + + Cancel Ακύρωση - + Installation in progress Εγκατάσταση σε εξέλιξη - + Please wait until the installation finishes. Περιμένετε να ολοκληρωθεί η εγκατάσταση. diff --git a/translations/unibackpack_en.ts b/translations/unibackpack_en.ts index 8344769..bfa8758 100644 --- a/translations/unibackpack_en.ts +++ b/translations/unibackpack_en.ts @@ -14,22 +14,28 @@ - + + + The installation was cancelled. + + + + Found: %1 - + Not in repos: %1 - + None of the packages are available in your repositories. - + Installing %n package(s)... Installing %n package... @@ -37,7 +43,7 @@ - + The package manager stopped unexpectedly. @@ -57,7 +63,17 @@ - + + Cancelling... + + + + + The installation had already started and can no longer be cancelled. + + + + Could not run %1: %2 @@ -75,13 +91,13 @@ - - + + Show details ▼ - + Hide details ▲ @@ -91,67 +107,73 @@ - + No supported package manager (pacman or apt) was found. - + No package list is available for this department yet. - + Warning - + Checking packages... - + ✓ Finished! - + Done! - + + Once the package manager has started, stopping it could break your system. + + + + ✗ Installation failed: %1 - + Failed - + Could not load the list of universities. - + Continue - + + Cancel - + Installation in progress - + Please wait until the installation finishes. diff --git a/ui/MainWindow.ui b/ui/MainWindow.ui index cad56fa..a0ce2a0 100644 --- a/ui/MainWindow.ui +++ b/ui/MainWindow.ui @@ -51,6 +51,16 @@ + + + + false + + + Cancel + + + From 77676f452f23d39e3f5c58850cfbfd8c1720eb1b Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Mon, 12 Oct 2026 02:19:11 +0300 Subject: [PATCH 14/15] Run Doxygen in CI and cppcheck on headers and tests --- .github/workflows/build.yml | 31 ++++++++++++++++++++++++++++++- include/utils/language.hpp | 4 ++-- include/utils/packageList.hpp | 2 +- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d72734..ca2f362 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,9 +41,11 @@ jobs: - name: Run cppcheck Static Analysis run: | cppcheck --enable=warning,style,performance,portability \ + --library=qt \ --suppress=shadowVariable \ --error-exitcode=1 \ - src/ + -I include \ + src/ include/ tests/ arch-pacman: name: Arch Linux (pacman) @@ -71,3 +73,30 @@ jobs: test -x pkg/usr/bin/unibackpack test -f pkg/usr/share/applications/unibackpack.desktop test -f pkg/usr/share/icons/hicolor/512x512/apps/unibackpack.png + + docs: + name: Documentation (Doxygen) + runs-on: ubuntu-24.04 + # Ubuntu's Doxygen is older than the Doxyfile and warns about its tags + container: archlinux:latest + + steps: + - name: Install Doxygen + run: pacman -Syu --noconfirm --needed doxygen + + - name: Checkout Source Code + uses: actions/checkout@v7 + + # Fails on any warning, except for parameters and return values without their own + # @param/@return: most functions are documented in one line that names them with @p + - name: Generate Documentation + run: | + ( cat Doxyfile + echo "WARN_AS_ERROR = FAIL_ON_WARNINGS" + echo "WARN_NO_PARAMDOC = NO" ) | doxygen - + + - name: Upload Documentation + uses: actions/upload-artifact@v7 + with: + name: docs-html + path: docs/html diff --git a/include/utils/language.hpp b/include/utils/language.hpp index 70d7a89..bdb58b4 100644 --- a/include/utils/language.hpp +++ b/include/utils/language.hpp @@ -22,14 +22,14 @@ namespace Utils { /** * @brief The languages offered in the language selector, English first. * - * Each one except English needs a @c translations/unibackpack_.ts file. + * Each one except English needs a translations/unibackpack_\.ts file. */ QList supportedLanguages(); /** * @brief Switches the application's UI language to @p locale. * - * Loads @c :/i18n/unibackpack_.qm for the best match of @p locale + * Loads :/i18n/unibackpack_\.qm for the best match of @p locale * and installs it, replacing the translator installed by a previous call. * If no matching or non-empty @c .qm file exists, no translator is installed, * so the original English strings are shown. Qt's own translations, used for diff --git a/include/utils/packageList.hpp b/include/utils/packageList.hpp index 6f3a770..6f01b32 100644 --- a/include/utils/packageList.hpp +++ b/include/utils/packageList.hpp @@ -23,7 +23,7 @@ namespace Utils { /** * @brief Resource path of a department's package list. * - * @return @c :/lists///packages.json + * @return :/lists/\/\/packages.json */ QString packageListPath(const QString &university, const QString &department); From d00bc151d5eb5cb7bcfa26bfc0e91861b6f454cf Mon Sep 17 00:00:00 2001 From: Ioannis Michadasis Date: Mon, 12 Oct 2026 02:43:30 +0300 Subject: [PATCH 15/15] Describe cancelling, apt list refresh and the CI checks in README --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 70a9739..e4c2c18 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ It ships with UniOS, and runs on any Arch- or Debian-based distribution. 1. The app lists the universities, then the departments of the one you pick. Departments that have no package list for your package manager yet are greyed out. 2. When you pick a department, UniBackpack checks which of its packages exist in your repositories and reports the ones that don't. -3. It installs the available ones through `pkexec`, so you are asked for your password once, and shows the progress and the package manager's output. +3. It installs the available ones through `pkexec`, so you are asked for your password once, and shows the progress and the package manager's output. With `apt` it refreshes the package lists first. With `pacman` it asks you to update your system yourself beforehand, since Arch doesn't support partial upgrades. +4. Until the package manager starts, you can cancel the run or close the window. After that, the window stays open until the install finishes, because interrupting `apt` or `pacman` can break the package database. The interface is available in English and Greek, selectable from the status bar. @@ -68,7 +69,7 @@ To install it system-wide, including the desktop entry and icon: sudo cmake --install build ``` -Every push and pull request is built and tested on both Ubuntu (apt) and Arch Linux (pacman) by GitHub Actions, which also runs `cppcheck`. API documentation can be generated with `doxygen` from the repository root; it is written to `docs/`. +Every push and pull request is built and tested on both Ubuntu (apt) and Arch Linux (pacman) by GitHub Actions, with compiler warnings treated as errors. To build the same way locally, add `-DUNIBACKPACK_WERROR=ON` to the first `cmake` command. The workflow also runs `cppcheck` and generates the API documentation with `doxygen`, failing on any warning; each run offers the result for download as the `docs-html` artifact. To generate it yourself, run `doxygen` from the repository root; it is written to `docs/`. ## Project Structure @@ -160,6 +161,8 @@ Help is welcome on: * Package lists for the departments that don't have one yet. Only UoWM Informatics has one so far. * `apt` package names for FreeCAD and Wireshark in the UoWM Informatics list. * A backend for `dnf` (Fedora). +* A small installer helper with its own polkit policy, so the password prompt names it instead of `/usr/bin/env`. +* A `.deb` package for Debian-based distributions. ## Recent Changes @@ -167,7 +170,9 @@ Help is welcome on: * The two per-department text lists (`pacman_list.txt`, `apt_list.txt`) were merged into one `packages.json`. * University and department names are translated in `universities.json` instead of the `.ts` files. * The repository check is now a single non-blocking call, and failures always report a reason instead of leaving the window disabled. -* CI builds and tests on both Ubuntu and Arch Linux. +* With `apt`, the package lists are refreshed before installing, and an install can no longer hang on a configuration file prompt. +* A Cancel button stops a run until the package manager starts. +* CI builds and tests on both Ubuntu and Arch Linux, with compiler warnings as errors, and checks the API documentation with Doxygen. ## Contributors