├── global.h ├── repkg.hook ├── repkg.sh ├── .gitignore ├── pacmanrunner.h ├── global.cpp ├── clicontroller.h ├── pkgresolver.h ├── main.cpp ├── completitions ├── zsh │ └── _repkg └── bash │ └── repkg ├── LICENSE ├── rulecontroller.h ├── repkg.pro ├── README.md ├── pacmanrunner.cpp ├── pkgresolver.cpp ├── clicontroller.cpp └── rulecontroller.cpp /global.h: -------------------------------------------------------------------------------- 1 | #ifndef GLOBAL_H 2 | #define GLOBAL_H 3 | 4 | #include 5 | 6 | namespace global 7 | { 8 | 9 | bool isRoot(); 10 | 11 | QDir userPath(); 12 | QDir rootPath(); 13 | QDir systemPath(); 14 | } 15 | 16 | #endif // GLOBAL_H 17 | -------------------------------------------------------------------------------- /repkg.hook: -------------------------------------------------------------------------------- 1 | [Trigger] 2 | Operation=Upgrade 3 | Type=Package 4 | Target=* 5 | 6 | [Action] 7 | Description=Check for packages that need to be rebuild 8 | Exec=/usr/share/libalpm/scripts/repkg.sh 9 | When=PostTransaction 10 | NeedsTargets 11 | -------------------------------------------------------------------------------- /repkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | /usr/bin/repkg update --stdin 5 | 6 | updatePkg=$(/usr/bin/repkg list) 7 | if [ -n "$updatePkg" ]; then 8 | echo -e "\e[36m>>>\e[0m \e[33mSome packages need to be rebuilt. See list below:\e[0m " 9 | echo -e "\e[36m>>>\e[0m $updatePkg" 10 | echo -e "\e[36m>>>\e[0m Run \e[32mrepkg\e[0m to automatically rebuild all packages that need one." 11 | fi 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | /.qmake.cache 16 | /.qmake.stash 17 | *.pro.user 18 | *.pro.user.* 19 | *.qbs.user 20 | *.qbs.user.* 21 | *.moc 22 | moc_*.cpp 23 | moc_*.h 24 | qrc_*.cpp 25 | ui_*.h 26 | Makefile* 27 | *build-* 28 | 29 | # QtCreator 30 | 31 | *.autosave 32 | 33 | # QtCtreator Qml 34 | *.qmlproject.user 35 | *.qmlproject.user.* 36 | 37 | # QtCtreator CMake 38 | CMakeLists.txt.user* 39 | 40 | -------------------------------------------------------------------------------- /pacmanrunner.h: -------------------------------------------------------------------------------- 1 | #ifndef PACMANRUNNER_H 2 | #define PACMANRUNNER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class PacmanRunner : public QObject 9 | { 10 | Q_OBJECT 11 | 12 | public: 13 | explicit PacmanRunner(QObject *parent = nullptr); 14 | 15 | std::tuple frontend() const; //(frontend, waved) 16 | QString frontendDescription() const; 17 | void setFrontend(const QStringList &cli, bool waved); 18 | void resetFrontend(); 19 | bool isWaved() const; 20 | 21 | int run(const QList &pkgs); 22 | 23 | QString readPackageVersion(const QString &pkg); 24 | QStringList readForeignPackages(); 25 | QStringList listDependencies(const QString &pkg); 26 | 27 | private: 28 | void initPacman(QProcess &proc, bool asPactree = false) const; 29 | }; 30 | 31 | #endif // PACMANRUNNER_H 32 | -------------------------------------------------------------------------------- /global.cpp: -------------------------------------------------------------------------------- 1 | #include "global.h" 2 | #include 3 | #include 4 | #include 5 | 6 | bool global::isRoot() 7 | { 8 | return ::geteuid() == 0; 9 | } 10 | 11 | QDir global::userPath() 12 | { 13 | QByteArray user; 14 | 15 | auto login = ::getlogin(); 16 | if(login) 17 | user = login; 18 | else { 19 | user = qgetenv("SUDO_USER"); 20 | if(user.isEmpty()) 21 | user = qgetenv("USER"); 22 | } 23 | 24 | QDir dir = QStringLiteral("/home/%1/.config/%2/rules") 25 | .arg(QString::fromUtf8(user), QCoreApplication::applicationName()); 26 | dir.mkpath(QStringLiteral(".")); 27 | if(dir.exists()) 28 | return dir; 29 | else 30 | return {}; 31 | } 32 | 33 | QDir global::rootPath() 34 | { 35 | QDir dir = QStringLiteral("/etc/%1/rules") 36 | .arg(QCoreApplication::applicationName()); 37 | if(isRoot()) 38 | dir.mkpath(QStringLiteral(".")); 39 | if(dir.exists()) 40 | return dir; 41 | else 42 | return {}; 43 | } 44 | 45 | QDir global::systemPath() 46 | { 47 | QDir dir{rootPath()}; 48 | if(dir.cd(QStringLiteral("system"))) 49 | return dir; 50 | else 51 | return {}; 52 | } 53 | -------------------------------------------------------------------------------- /clicontroller.h: -------------------------------------------------------------------------------- 1 | #ifndef CLICONTROLLER_H 2 | #define CLICONTROLLER_H 3 | 4 | #include "pkgresolver.h" 5 | #include "rulecontroller.h" 6 | #include "pacmanrunner.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | class CliController : public QObject 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | explicit CliController(QObject *parent = nullptr); 18 | 19 | void parseArguments(const QCoreApplication &app); 20 | static bool verbose(); 21 | 22 | private slots: 23 | void run(); 24 | 25 | private: 26 | void setup(); 27 | 28 | void rebuild(); 29 | void update(QStringList pkgs, bool fromStdin); 30 | void create(const QString &pkg, bool autoDepends, const QStringList &rules); 31 | void remove(const QStringList &pkgs); 32 | void list(bool detail); 33 | void listRules(bool listShort, bool userOnly); 34 | void clear(const QStringList &pkgs); 35 | void frontend(); 36 | void setFrontend(const QStringList &frontend, bool waved); 37 | void resetFrontend(); 38 | 39 | void testEmpty(const QStringList &args); 40 | 41 | QScopedPointer _parser; 42 | 43 | PacmanRunner *_runner; 44 | RuleController *_rules; 45 | PkgResolver *_resolver; 46 | 47 | static bool _verbose; 48 | }; 49 | 50 | #endif // CLICONTROLLER_H 51 | -------------------------------------------------------------------------------- /pkgresolver.h: -------------------------------------------------------------------------------- 1 | #ifndef PKGRESOLVER_H 2 | #define PKGRESOLVER_H 3 | 4 | #include "pacmanrunner.h" 5 | #include "rulecontroller.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | class PkgResolver : public QObject 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit PkgResolver(PacmanRunner *runner, RuleController *controller, QObject *parent = nullptr); 17 | 18 | QStringList listPkgs() const; 19 | QString listDetailPkgs() const; 20 | QList listPkgWaves() const; 21 | 22 | void updatePkgs(const QStringList &pkgs); 23 | void clear(const QStringList &pkgs); 24 | 25 | private: 26 | struct VersionTuple { 27 | int epoche = 0; 28 | QVersionNumber version; 29 | QString suffix; 30 | QVersionNumber revision; 31 | }; 32 | using PkgInfos = QMap>; //package -> triggered by 33 | 34 | QSettings *_settings; 35 | PacmanRunner *_runner; 36 | RuleController *_controller; 37 | 38 | PkgInfos readPkgs() const; 39 | void writePkgs(const PkgInfos &pkgInfos); 40 | 41 | QString readVersion(); 42 | 43 | bool checkVersionUpdate(const RuleController::RuleInfo &pkgInfo, const QString &target); 44 | static VersionTuple splitVersion(const QString &version, bool &ok); 45 | }; 46 | 47 | #endif // PKGRESOLVER_H 48 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "clicontroller.h" 4 | 5 | static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | QCoreApplication a(argc, argv); 10 | QCoreApplication::setApplicationName(QStringLiteral(TARGET)); 11 | QCoreApplication::setApplicationVersion(QStringLiteral(VERSION)); 12 | QCoreApplication::setOrganizationName(QStringLiteral(COMPANY)); 13 | QCoreApplication::setOrganizationDomain(QStringLiteral(BUNDLE)); 14 | 15 | CliController controller; 16 | controller.parseArguments(a); 17 | qInstallMessageHandler(messageHandler); 18 | 19 | return a.exec(); 20 | } 21 | 22 | static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) 23 | { 24 | auto message = qFormatLogMessage(type, context, msg); 25 | 26 | QT_WARNING_PUSH 27 | QT_WARNING_DISABLE_GCC("-Wimplicit-fallthrough") 28 | switch (type) { 29 | case QtDebugMsg: 30 | if(!CliController::verbose()) 31 | break; 32 | case QtWarningMsg: 33 | case QtCriticalMsg: 34 | case QtFatalMsg: 35 | std::cerr << message.toStdString() << std::endl; 36 | break; 37 | case QtInfoMsg: 38 | std::cout << message.toStdString() << std::endl; 39 | break; 40 | default: 41 | break; 42 | } 43 | QT_WARNING_POP 44 | 45 | if(type == QtFatalMsg) 46 | abort(); 47 | } 48 | -------------------------------------------------------------------------------- /completitions/zsh/_repkg: -------------------------------------------------------------------------------- 1 | #compdef repkg 2 | 3 | typeset -A opt_args 4 | setopt extendedglob 5 | 6 | local optargs cmdargs providers 7 | 8 | optargs=( 9 | '(-h --help)'{-h,--help}'[help]' 10 | {-v,--version}'[version]' 11 | '--verbose[show more output]' 12 | ) 13 | 14 | cmdargs=(':first command:(clear create frontend list rebuild remove rules update)') 15 | 16 | _arguments -C $cmdargs $optargs "*::arg:->args" 17 | 18 | cmdargs=() 19 | case $line[1] in 20 | clear) 21 | cmdargs=("*::packages:($(repkg list))") 22 | ;; 23 | create) 24 | cmdargs=( 25 | ":package:($(pacman -Qq))" 26 | "*:dependencies:($(pacman -Qq))" 27 | ) 28 | ;; 29 | frontend) 30 | optargs=( 31 | $optargs 32 | {-s,--set}'[change the frontend]:tool:_files' 33 | {-w,--waved}'[call in waved mode]' 34 | {-r,--reset}'[reset to default]' 35 | ) 36 | ;; 37 | list) 38 | optargs=( 39 | $optargs 40 | {-d,--detail}'[display a detailed table]' 41 | ) 42 | ;; 43 | remove) 44 | cmdargs=("*::packages:($(repkg rules --short --user))") 45 | ;; 46 | rules) 47 | optargs=( 48 | $optargs 49 | {-s,--short}'[display short list]' 50 | {-u,--user}'[only rules of current user]' 51 | ) 52 | ;; 53 | update) 54 | optargs=( 55 | $optargs 56 | '--stdin[read packages from stdin]' 57 | ) 58 | cmdargs=("*::packages:($(pacman -Qq))") 59 | ;; 60 | esac 61 | 62 | _arguments $optargs $cmdargs 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Felix Barz 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /rulecontroller.h: -------------------------------------------------------------------------------- 1 | #ifndef RULECONTROLLER_H 2 | #define RULECONTROLLER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "pacmanrunner.h" 13 | 14 | class RuleController : public QObject 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | enum class RuleScope { 20 | Any, 21 | Epoche, 22 | Version, 23 | Suffix, 24 | Revision 25 | }; 26 | Q_ENUM(RuleScope) 27 | 28 | struct RuleInfo { 29 | using RangeContent = std::pair>; 30 | using Range = std::optional; // (offset, limit) 31 | 32 | QString package; 33 | RuleScope scope = RuleScope::Any; 34 | Range range; 35 | std::optional count; 36 | }; 37 | 38 | struct RuleSource { 39 | bool extension = false; 40 | bool isRoot = false; 41 | QStringList targets; 42 | }; 43 | 44 | explicit RuleController(PacmanRunner *runner, QObject *parent = nullptr); 45 | 46 | void createRule(const QString &pkg, bool autoDepends, QStringList deps); 47 | void removeRule(const QString &pkg); 48 | 49 | QString listRules(bool pkgOnly, bool userOnly); 50 | 51 | QList findRules(const QString &pkg); 52 | 53 | private: 54 | PacmanRunner *_runner; 55 | QMap _ruleSources; 56 | QMultiHash _rules; 57 | 58 | void readRules(); 59 | QList readRuleDefinitions(const QFileInfo &fileInfo, RuleSource &srcBase); 60 | static void parseScope(RuleInfo &ruleInfo, const QStringRef &scopeStr); 61 | static void addRules(QList &target, const QList &newRules); 62 | }; 63 | 64 | #endif // RULECONTROLLER_H 65 | -------------------------------------------------------------------------------- /repkg.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | 3 | QT += core 4 | QT -= gui 5 | 6 | CONFIG += c++17 console warning_clean exceptions 7 | CONFIG -= app_bundle 8 | 9 | TARGET = repkg 10 | VERSION = 1.4.0 11 | 12 | RC_ICONS += ./icons/repkg.ico 13 | QMAKE_TARGET_COMPANY = "Skycoder42" 14 | QMAKE_TARGET_PRODUCT = $$TARGET 15 | QMAKE_TARGET_DESCRIPTION = $$TARGET 16 | QMAKE_TARGET_COPYRIGHT = "Felix Barz" 17 | QMAKE_TARGET_BUNDLE_PREFIX = de.skycoder42 18 | 19 | DEFINES += "TARGET=\\\"$$TARGET\\\"" 20 | DEFINES += "VERSION=\\\"$$VERSION\\\"" 21 | DEFINES += "COMPANY=\"\\\"$$QMAKE_TARGET_COMPANY\\\"\"" 22 | DEFINES += "BUNDLE=\"\\\"$$QMAKE_TARGET_BUNDLE_PREFIX\\\"\"" 23 | 24 | DEFINES += QT_DEPRECATED_WARNINGS QT_ASCII_CAST_WARNINGS 25 | 26 | HEADERS += \ 27 | clicontroller.h \ 28 | rulecontroller.h \ 29 | pkgresolver.h \ 30 | pacmanrunner.h \ 31 | global.h 32 | 33 | SOURCES += main.cpp \ 34 | clicontroller.cpp \ 35 | rulecontroller.cpp \ 36 | pkgresolver.cpp \ 37 | pacmanrunner.cpp \ 38 | global.cpp 39 | 40 | DISTFILES += \ 41 | README.md \ 42 | repkg.sh \ 43 | repkg.hook \ 44 | completitions/bash/repkg \ 45 | completitions/zsh/_repkg 46 | 47 | target.path = $$[QT_INSTALL_BINS] 48 | INSTALLS += target 49 | 50 | hook.path = /usr/share/libalpm/hooks 51 | hook.files += repkg.hook 52 | script.path = /usr/share/libalpm/scripts 53 | script.files += repkg.sh 54 | bashcomp.path = /usr/share/bash-completion/completions 55 | bashcomp.files += completitions/bash/repkg 56 | zshcomp.path = /usr/share/zsh/site-functions/ 57 | zshcomp.files = completitions/zsh/_repkg 58 | INSTALLS += hook script bashcomp zshcomp 59 | 60 | QDEP_DEPENDS += Skycoder42/QCliParser 61 | !load(qdep):error("Failed to load qdep feature! Run 'qdep.py prfgen --qmake $$QMAKE_QMAKE' to create it.") 62 | -------------------------------------------------------------------------------- /completitions/bash/repkg: -------------------------------------------------------------------------------- 1 | # file: repkg 2 | # repkg parameter-completion 3 | 4 | function _repkg_contains_element { 5 | local e match="$1" 6 | shift 7 | for e; do [[ "$e" == "$match" ]] && return 0; done 8 | return 1 9 | } 10 | 11 | function _repkg { 12 | local cur prev last bin arg optargs prefix 13 | 14 | cur=${COMP_WORDS[COMP_CWORD]} 15 | bin=${COMP_WORDS[0]} 16 | last=${COMP_WORDS[COMP_CWORD-1]} 17 | prev=("${COMP_WORDS[@]:1}") 18 | unset 'prev[${#prev[@]}-1]' 19 | COMPREPLY=() 20 | 21 | ## special last arg completitions for options 22 | case "$last" in 23 | -s|--set) 24 | COMPREPLY=($(compgen -o plusdirs -c -- "${COMP_WORDS[COMP_CWORD]}")) 25 | ;; 26 | *) ##default: normal completition 27 | optargs='-h --help -v --version --verbose' 28 | prefix='rebuild update create remove list rules clear frontend' 29 | for arg in "${prev[@]}"; do 30 | ## collect all opt args 31 | case "$arg" in 32 | list) 33 | optargs="$optargs -d --detail" 34 | ;; 35 | rules) 36 | optargs="$optargs -s --short -u --user" 37 | ;; 38 | frontend) 39 | optargs="$optargs -s --set --waved -r --reset" 40 | ;; 41 | esac 42 | 43 | ## find the prefix: check if prefix was in prev list 44 | if _repkg_contains_element $arg $prefix; then 45 | case "$arg" in 46 | update|create) 47 | prefix="$(pacman -Qq)" 48 | break # break the loop here 49 | ;; 50 | clear) 51 | prefix="$($bin list)" 52 | break # break the loop here 53 | ;; 54 | remove) 55 | prefix="$($bin rules --short --user)" 56 | break # break the loop here 57 | ;; 58 | *) 59 | prefix="" 60 | ;; 61 | esac 62 | fi 63 | done 64 | COMPREPLY=($(compgen -W "$prefix $optargs" -- $cur)) 65 | ;; 66 | esac 67 | 68 | 69 | return 0 70 | } 71 | 72 | complete -F _repkg -o filenames repkg 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # repkg 2 | A tool to manage rebuilding of AUR packages based on their dependencies. 3 | 4 | ## Features 5 | The general idea is: For an AUR package foo, that depends on bar, you always want to rebuild foo everytime bar is update. This tool makes this easily possible: 6 | 7 | - Create rules for packages with other packages that will trigger a rebuild 8 | - System rules: System-wide rules, to work for any user 9 | - can be created by users and applications. This way packagers can provide the required dependencies, without actually depending on this package (i.e. opt depend) 10 | - User rules: created by the user, can override system rules. Only work when sudoing as that user 11 | - Pacman hook to detect packages that need a rebuild after an update 12 | - works recursivly, i.e. if A dependes on B and B on C, C will trigger B and A 13 | - Automatic detection of successful rebuilds 14 | - Works with any pacman frontend that supports the default pacman syntax 15 | - yay, trizen, pacaur and yaourt work out of the box 16 | - other frontends can be set via a command 17 | - allows "waved" rebuilds for frontends that are unable to correctly order packages for the rebuild 18 | 19 | ## Installation 20 | The tool is provided as AUR package: [repkg](https://aur.archlinux.org/packages/repkg) 21 | 22 | This repository also has a sample repkg rule for itself. 23 | 24 | ## Usage 25 | ### Users 26 | As a user, you can create rules by calling: 27 | ``` 28 | repkg create [dependencies...] 29 | ``` 30 | `package` is the AUR package that should be rebuild, when one of the given `dependencies` is updated to a newer versions. You can also create rules directly by creating a rule file in `~/.config/repkg/rules`, with the package name beeing the filename (i.e. `package.rule`) and the content beeing the dependencies, space seperated. 31 | 32 | For system admins, when running this command as root, the rules are instead written to `/etc/repkg/rules`. For repkg prior to version `1.3.0` this will overwrite the rules created by installed packages. But since `1.3.0` packages should place their rules in `/etc/repkg/rules/system` to prevent such conflicts. 33 | 34 | When updating packages via pacman (or any frontend), rebuilds are automatically detected. You will see a message with all packages that need rebuilds at the end. You can also run 35 | ``` 36 | repkg list detail 37 | ``` 38 | to show all packages that need rebuilds. 39 | 40 | To actually rebuild them, simply run 41 | ``` 42 | repkg 43 | ``` 44 | This will start the frontend of your choice (e.g. yay, trizen, pacaur, yaourt, ...) and rebuild all required packages. 45 | 46 | ### Package Providers 47 | Simply add a rule file to your PKGBUILD, and install it to `/etc/repkg/rules/system` (or `/etc/repkg/rules` if you want to be compatible with versions of repkg before `1.3.0`). Assuming your package is name `my-pkg` and should be rebuild when `dep-a` or `dep-b` is updated, the file must be named `my-pkg.rule` and contain: 48 | ``` 49 | dep-a dep-b 50 | ``` 51 | 52 | Add repkg as (optional) dependency, and your good to go. 53 | 54 | #### Version Filtering 55 | If you want the package to only be updated if the change is significant enough (i.e. a major version update), you can do so by adding a version filter expression to the dependency. These special filters tell repkg to only compare parts of the version numbers, not the whole number. The general syntax for that is: 56 | ``` 57 | [=] 58 | ``` 59 | Leaving out the filter means that the package is always rebuilt if the version string changes in any way. Possible filter expressions are: 60 | 61 | - `0`: Only update if the epoche changes. E.g. from `1:2.3.5` to `2:1.0.0`. 62 | - `v`: Only update if the epoche or the version number itself changes. This *exlcudes* the package revision and possible suffixes. E.g. from `1.2.3` to `1.2.4` 63 | - `1..*` (any other positive number besides 0): Same as `v`, but limit the version segments to check. E.g. if specifying `1`, Only major version updates (`1.2.3` to `2.0.0`) trigger a rebuild. With `2` this also includes minor versions etc. 64 | - `s`: Only update if the epoche or the version number itself changes. This *includes* the package revision and possible suffixes. E.g. from `1.2.3-alpha` to `1.2.3` 65 | - `r`: Always update, even if only the package revision changes. E.g. `1.2.3-1` to `1.2.3-2` 66 | - `:[:]`: Do a normal string based comaprison, but only compare a substring of the version number, starting at `offset` and `length` characters long (both must be 0 or positive integers). E.g. `:2:4` on `1.2345.6` will reduce the string to `2345` before comparing. 67 | - `:[:]::`: Same as before, but instead of a string compare, use another filter. Can be any of the above except the two range limiters. E.g. using the filter `:1::v` on `v1.2.3` will reduce the string to `1.2.3` and then do a normal version compare. Without the previous removal of the `v`, a version-based compare would not work for this example. 68 | -------------------------------------------------------------------------------- /pacmanrunner.cpp: -------------------------------------------------------------------------------- 1 | #include "pacmanrunner.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | PacmanRunner::PacmanRunner(QObject *parent) : 13 | QObject(parent) 14 | {} 15 | 16 | std::tuple PacmanRunner::frontend() const 17 | { 18 | QSettings settings; 19 | if(settings.contains(QStringLiteral("frontend"))) { 20 | return std::make_tuple(settings.value(QStringLiteral("frontend")).toStringList(), 21 | settings.value(QStringLiteral("frontend/waved"), false).toBool()); 22 | } else { 23 | static const QList> defaultFn = { 24 | std::make_tuple(QStringList{QStringLiteral("yay"), QStringLiteral("--rebuild"), QStringLiteral("-S")}, false), 25 | std::make_tuple(QStringList{QStringLiteral("trizen"), QStringLiteral("-S")}, false), 26 | std::make_tuple(QStringList{QStringLiteral("pacaur"), QStringLiteral("--rebuild"), QStringLiteral("-S")}, false), 27 | std::make_tuple(QStringList{QStringLiteral("yaourt"), QStringLiteral("-S")}, true) 28 | }; 29 | for(const auto &fn : defaultFn) { 30 | if(!QStandardPaths::findExecutable(std::get<0>(fn).first()).isNull()) 31 | return fn; 32 | } 33 | return std::make_tuple(QStringList{QStringLiteral("pacman")}, true); 34 | } 35 | } 36 | 37 | QString PacmanRunner::frontendDescription() const 38 | { 39 | auto fn = frontend(); 40 | return QStringLiteral("%1 %2") 41 | .arg(std::get<0>(fn).join(QLatin1Char(' ')), 42 | std::get<1>(fn) ? QStringLiteral(" ...") : QStringLiteral("")); 43 | } 44 | 45 | void PacmanRunner::setFrontend(const QStringList &cli, bool waved) 46 | { 47 | QSettings settings; 48 | settings.setValue(QStringLiteral("frontend"), cli); 49 | settings.setValue(QStringLiteral("frontend/waved"), waved); 50 | qDebug() << "Updated pacman frontend to" << cli.first() 51 | << (waved ? "(waved)" : "(sorted)"); 52 | } 53 | 54 | void PacmanRunner::resetFrontend() 55 | { 56 | QSettings settings; 57 | settings.remove(QStringLiteral("frontend")); 58 | } 59 | 60 | int PacmanRunner::run(const QList &waves) 61 | { 62 | if(waves.isEmpty()) { 63 | qWarning() << "No packages need to be rebuilt"; 64 | return EXIT_SUCCESS; 65 | } 66 | 67 | //check if all packages are installed 68 | QProcess proc; 69 | initPacman(proc); 70 | proc.setStandardOutputFile(QProcess::nullDevice()); 71 | QStringList pacArgs {QStringLiteral("-Qi")}; 72 | for(const auto& pkgs : waves) 73 | pacArgs.append(pkgs); 74 | proc.setArguments(pacArgs); 75 | 76 | qDebug() << "Checking if all packages are still installed..."; 77 | proc.start(); 78 | proc.waitForFinished(-1); 79 | if(proc.exitCode() != EXIT_SUCCESS) 80 | throw QStringLiteral("Please remove repkg files of uninstalled packages and mark the unchanged via `repkg clear `"); 81 | 82 | // run the frontend to reinstall packages 83 | QStringList cliArgs; 84 | bool waved; 85 | std::tie(cliArgs, waved) = frontend(); 86 | auto bin = QStandardPaths::findExecutable(cliArgs.takeFirst()); 87 | if(bin.isNull()) 88 | throw QStringLiteral("Unable to find binary \"%1\" in PATH").arg(bin); 89 | if(waved) { 90 | for(const auto& pkgs : waves) { 91 | auto args = cliArgs; 92 | args.append(pkgs); 93 | auto res = QProcess::execute(bin, args); 94 | if(res != EXIT_SUCCESS) 95 | return res; 96 | } 97 | return EXIT_SUCCESS; 98 | } else { 99 | for(const auto& pkgs : waves) 100 | cliArgs.append(pkgs); 101 | 102 | // prepare arguments 103 | auto argSize = cliArgs.size() + 1; 104 | QByteArrayList rawArgs; 105 | rawArgs.reserve(argSize); 106 | QVector execArgs{argSize + 1, nullptr}; 107 | // bin 108 | rawArgs.append(bin.toUtf8()); 109 | execArgs[0] = rawArgs[0].data(); 110 | // args 111 | for(auto i = 1; i < argSize; i++) { 112 | rawArgs.append(cliArgs[i - 1].toUtf8()); 113 | execArgs[i] = rawArgs[i].data(); 114 | } 115 | 116 | ::execv(execArgs[0], execArgs.data()); 117 | //unexcepted error 118 | throw QStringLiteral("execv failed with error: %1").arg(qt_error_string(errno)); 119 | } 120 | } 121 | 122 | QString PacmanRunner::readPackageVersion(const QString &pkg) 123 | { 124 | //read the package version from pacman 125 | QProcess proc; 126 | initPacman(proc); 127 | proc.setArguments({QStringLiteral("-Q"), pkg}); 128 | 129 | qDebug() << "Querying package version of" << pkg << "..."; 130 | proc.start(); 131 | proc.waitForFinished(-1); 132 | if(proc.exitCode() != EXIT_SUCCESS) 133 | throw QStringLiteral("Failed to get current version of package %1 from pacman").arg(pkg); 134 | 135 | auto match = QRegularExpression { 136 | QStringLiteral(R"__(^(?:%1)\s*(.*)$)__") 137 | .arg(QRegularExpression::escape(pkg)) 138 | }.match(QString::fromUtf8(proc.readAllStandardOutput()).simplified()); 139 | if(!match.hasMatch()) 140 | throw QStringLiteral("Failed to get current version of package %1 from pacman").arg(pkg); 141 | return match.captured(1); 142 | } 143 | 144 | QStringList PacmanRunner::readForeignPackages() 145 | { 146 | QProcess proc; 147 | initPacman(proc); 148 | proc.setArguments({QStringLiteral("-Qqm")}); 149 | 150 | qDebug() << "Querying all foreign packages..."; 151 | proc.start(); 152 | proc.waitForFinished(-1); 153 | if(proc.exitCode() != EXIT_SUCCESS) 154 | throw QStringLiteral("Failed to get foreign packages from pacman"); 155 | 156 | return QString::fromUtf8(proc.readAllStandardOutput()).simplified().split(QLatin1Char(' ')); 157 | } 158 | 159 | QStringList PacmanRunner::listDependencies(const QString &pkg) 160 | { 161 | QProcess proc; 162 | initPacman(proc, true); 163 | proc.setArguments({ 164 | QStringLiteral("-u"), 165 | QStringLiteral("-d1"), 166 | pkg 167 | }); 168 | 169 | qDebug() << "Querying all dependencies of the" << pkg << "package..."; 170 | proc.start(); 171 | proc.waitForFinished(-1); 172 | if(proc.exitCode() != EXIT_SUCCESS) 173 | throw QStringLiteral("Failed to get dependencies of %1 from pactree").arg(pkg); 174 | 175 | // skip first element, is always the package itself 176 | return QString::fromUtf8(proc.readAllStandardOutput()).simplified().split(QLatin1Char(' ')).mid(1); 177 | } 178 | 179 | void PacmanRunner::initPacman(QProcess &proc, bool asPactree) const 180 | { 181 | auto pacman = asPactree ? 182 | QStandardPaths::findExecutable(QStringLiteral("pactree")) : 183 | QStandardPaths::findExecutable(QStringLiteral("pacman")); 184 | if(pacman.isNull()) 185 | throw QStringLiteral("Unable to find %1 binary in PATH").arg(asPactree ? QStringLiteral("vercmp") :QStringLiteral("pacman")); 186 | proc.setProgram(pacman); 187 | proc.setProcessChannelMode(QProcess::ForwardedErrorChannel); 188 | } 189 | -------------------------------------------------------------------------------- /pkgresolver.cpp: -------------------------------------------------------------------------------- 1 | #include "pkgresolver.h" 2 | #include "global.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | using namespace global; 10 | 11 | PkgResolver::PkgResolver(PacmanRunner *runner, RuleController *controller, QObject *parent) : 12 | QObject{parent}, 13 | _settings{new QSettings{ 14 | rootPath().absoluteFilePath(QStringLiteral("../state.conf")), 15 | QSettings::IniFormat, 16 | this 17 | }}, 18 | _runner{runner}, 19 | _controller{controller} 20 | {} 21 | 22 | QStringList PkgResolver::listPkgs() const 23 | { 24 | return readPkgs().keys(); 25 | } 26 | 27 | QString PkgResolver::listDetailPkgs() const 28 | { 29 | QStringList pkgs; 30 | pkgs.append(QStringLiteral("%1| Triggered by").arg(QStringLiteral(" Package Update"), -30)); 31 | pkgs.append(QStringLiteral("-").repeated(30) + QLatin1Char('|') + QStringLiteral("-").repeated(49)); 32 | 33 | auto pkgInfos = readPkgs(); 34 | for(auto it = pkgInfos.constBegin(); it != pkgInfos.constEnd(); it++) { 35 | auto lst = it.value().toList(); 36 | std::sort(lst.begin(), lst.end()); 37 | pkgs.append(QStringLiteral("%1| %2") 38 | .arg(it.key(), -30) 39 | .arg(lst.join(QStringLiteral(", ")))); 40 | } 41 | return pkgs.join(QLatin1Char('\n')); 42 | } 43 | 44 | QList PkgResolver::listPkgWaves() const 45 | { 46 | auto pkgs = readPkgs(); 47 | 48 | QList waves; 49 | while(!pkgs.isEmpty()) { 50 | //find all packages that dont have a trigger that needs to be rebuild as well 51 | const auto keys = QSet::fromList(pkgs.keys()); 52 | QStringList wave; 53 | for(auto it = pkgs.begin(); it != pkgs.end();) { 54 | if(keys.intersects(it.value())) 55 | it++; //has a trigger dep, postpone for later 56 | else { 57 | wave.append(it.key()); 58 | it = pkgs.erase(it); 59 | } 60 | } 61 | if(wave.isEmpty()) { 62 | throw QStringLiteral("Cyclic dependencies detected! Is within packages: %1") 63 | .arg(keys.toList().join(QLatin1Char(' '))); 64 | } 65 | waves.append(wave); 66 | qDebug() << "Calculated wave:" << wave.join(QLatin1Char(' ')); 67 | } 68 | 69 | return waves; 70 | } 71 | 72 | void PkgResolver::updatePkgs(const QStringList &pkgs) 73 | { 74 | if(!isRoot()) 75 | throw QStringLiteral("Must be run as root to update packages!"); 76 | 77 | QQueue pkgQueue; 78 | for(const auto& pkg : pkgs) 79 | pkgQueue.enqueue(pkg); 80 | 81 | auto pkgInfos = readPkgs(); 82 | QSet skipPkgs; 83 | 84 | while (!pkgQueue.isEmpty()) { 85 | //handle each package only once 86 | auto pkg = pkgQueue.dequeue(); 87 | if(skipPkgs.contains(pkg)) 88 | continue; 89 | 90 | //check if packages need updates 91 | auto matches = _controller->findRules(pkg); 92 | //add those to the "needs updates" list 93 | //and check if they themselves will trigger rebuilds by adding them to the queue 94 | for(const auto& match : matches) { 95 | if(checkVersionUpdate(match, pkg)) { 96 | pkgInfos[match.package].insert(pkg); 97 | pkgQueue.enqueue(match.package); 98 | qDebug() << "Rule triggered. Marked" 99 | << match.package 100 | << "for updates because of" 101 | << pkg; 102 | } else { 103 | qDebug() << "Rule skipped. Did not mark " 104 | << match.package 105 | << "for updates because version of" 106 | << pkg 107 | << "did not change significantly"; 108 | } 109 | } 110 | 111 | //each package only once -> skip next time 112 | skipPkgs.insert(pkg); 113 | } 114 | 115 | //remove all "original" packages from the rebuild list as they have just been built 116 | for(const auto& pkg : pkgs) 117 | pkgInfos.remove(pkg); 118 | 119 | //save the infos 120 | writePkgs(pkgInfos); 121 | } 122 | 123 | void PkgResolver::clear(const QStringList &pkgs) 124 | { 125 | if(!isRoot()) 126 | throw QStringLiteral("Must be run as root to clear packages!"); 127 | 128 | if(pkgs.isEmpty()) { 129 | _settings->remove(QStringLiteral("pkgstate")); 130 | qDebug() << "Cleared all pending package rebuilds"; 131 | } else { 132 | auto pkgInfos = readPkgs(); 133 | auto save = false; 134 | for(const auto& pkg : pkgs) 135 | save = pkgInfos.remove(pkg) || save; 136 | if(save) 137 | writePkgs(pkgInfos); 138 | qDebug() << "Cleared specified pending package rebuilds"; 139 | } 140 | } 141 | 142 | PkgResolver::PkgInfos PkgResolver::readPkgs() const 143 | { 144 | auto count = _settings->beginReadArray(QStringLiteral("pkgstate")); 145 | PkgInfos pkgs; 146 | for(auto i = 0; i < count; i++) { 147 | _settings->setArrayIndex(i); 148 | pkgs.insert(_settings->value(QStringLiteral("name")).toString(), 149 | QSet::fromList(_settings->value(QStringLiteral("reason")).toStringList())); 150 | } 151 | _settings->endArray(); 152 | return pkgs; 153 | } 154 | 155 | void PkgResolver::writePkgs(const PkgInfos &pkgInfos) 156 | { 157 | auto keys = pkgInfos.keys(); 158 | _settings->remove(QStringLiteral("pkgstate")); 159 | _settings->beginWriteArray(QStringLiteral("pkgstate"), pkgInfos.size()); 160 | for(auto i = 0; i < pkgInfos.size(); i++) { 161 | _settings->setArrayIndex(i); 162 | _settings->setValue(QStringLiteral("name"), keys[i]); 163 | _settings->setValue(QStringLiteral("reason"), static_cast(pkgInfos[keys[i]].toList())); 164 | } 165 | _settings->endArray(); 166 | } 167 | 168 | bool PkgResolver::checkVersionUpdate(const RuleController::RuleInfo &pkgInfo, const QString &target) 169 | { 170 | auto newVersion = _runner->readPackageVersion(target); 171 | 172 | _settings->beginGroup(QStringLiteral("versions")); 173 | _settings->beginGroup(pkgInfo.package); 174 | auto oldVersion = _settings->value(target).toString(); 175 | _settings->setValue(target, newVersion); 176 | _settings->endGroup(); 177 | _settings->endGroup(); 178 | 179 | if(oldVersion.isEmpty()) 180 | return true; 181 | 182 | // apply filter rule to determine if the version changed 183 | // first: filter both versions 184 | if(pkgInfo.range) { 185 | oldVersion = oldVersion.mid(pkgInfo.range->first, pkgInfo.range->second.value_or(-1)); 186 | newVersion = newVersion.mid(pkgInfo.range->first, pkgInfo.range->second.value_or(-1)); 187 | } 188 | // second: for any-compares, do so without further processing 189 | if(pkgInfo.scope == RuleController::RuleScope::Any) 190 | return oldVersion != newVersion; 191 | // third: split the version and compare based on scope 192 | auto ok = true; 193 | const auto oldVTuple = splitVersion(oldVersion, ok); 194 | const auto newVTuple = splitVersion(newVersion, ok); 195 | if(!ok) 196 | return oldVersion != newVersion; 197 | switch (pkgInfo.scope) { 198 | case RuleController::RuleScope::Revision: 199 | if(oldVTuple.revision != newVTuple.revision) 200 | return true; 201 | Q_FALLTHROUGH(); 202 | case RuleController::RuleScope::Suffix: 203 | if(oldVTuple.suffix != newVTuple.suffix) 204 | return true; 205 | Q_FALLTHROUGH(); 206 | case RuleController::RuleScope::Version: 207 | if(pkgInfo.count) { 208 | if(oldVTuple.version.segments().mid(0, *pkgInfo.count) != 209 | newVTuple.version.segments().mid(0, *pkgInfo.count)) 210 | return true; 211 | } else { 212 | if(oldVTuple.version != newVTuple.version) 213 | return true; 214 | } 215 | Q_FALLTHROUGH(); 216 | case RuleController::RuleScope::Epoche: 217 | return oldVTuple.epoche != newVTuple.epoche; 218 | default: 219 | Q_UNREACHABLE(); 220 | return false; 221 | } 222 | } 223 | 224 | PkgResolver::VersionTuple PkgResolver::splitVersion(const QString &version, bool &ok) 225 | { 226 | static const QRegularExpression regex{QStringLiteral(R"__(^(?:(\d+):)?(.*)-([\d\.]+)$)__")}; 227 | const auto match = regex.match(version); 228 | if(match.hasMatch()) { 229 | VersionTuple vTuple; 230 | if(match.capturedLength(1) > 0) 231 | vTuple.epoche = match.capturedRef(1).toInt(); 232 | int sIndex = 0; 233 | vTuple.version = QVersionNumber::fromString(match.capturedView(2), &sIndex); 234 | if(sIndex < match.capturedLength(2)) 235 | vTuple.suffix = match.capturedView(2).mid(sIndex).toString(); 236 | vTuple.revision = QVersionNumber::fromString(match.capturedView(3)); 237 | return vTuple; 238 | } else { 239 | qWarning() << "Failed to parse version string" 240 | << version 241 | << "- falling back to basic string comparison!"; 242 | ok = false; 243 | return {}; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /clicontroller.cpp: -------------------------------------------------------------------------------- 1 | #include "clicontroller.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | bool CliController::_verbose = false; 8 | 9 | CliController::CliController(QObject *parent) : 10 | QObject(parent), 11 | _parser{new QCliParser{}}, 12 | _runner{new PacmanRunner{this}}, 13 | _rules{new RuleController{_runner, this}}, 14 | _resolver{new PkgResolver{_runner, _rules, this}} 15 | {} 16 | 17 | void CliController::parseArguments(const QCoreApplication &app) 18 | { 19 | setup(); 20 | _parser->process(app, true); 21 | 22 | _verbose = _parser->isSet(QStringLiteral("verbose")); 23 | QMetaObject::invokeMethod(this, "run", Qt::QueuedConnection); 24 | } 25 | 26 | bool CliController::verbose() 27 | { 28 | return _verbose; 29 | } 30 | 31 | void CliController::run() 32 | { 33 | try { 34 | auto args = _parser->positionalArguments(); 35 | if(_parser->enterContext(QStringLiteral("rebuild"))) { 36 | testEmpty(args); 37 | rebuild(); 38 | } else if(_parser->enterContext(QStringLiteral("update"))) 39 | update(args, _parser->isSet(QStringLiteral("stdin"))); 40 | else if(_parser->enterContext(QStringLiteral("create"))) { 41 | if(args.isEmpty()) 42 | throw tr("You must specify a package to create a rule for"); 43 | create(args.takeFirst(), _parser->isSet(QStringLiteral("depends")), args); 44 | } else if(_parser->enterContext(QStringLiteral("remove"))) 45 | remove(args); 46 | else if(_parser->enterContext(QStringLiteral("list"))) { 47 | testEmpty(args); 48 | list(_parser->isSet(QStringLiteral("detail"))); 49 | } else if(_parser->enterContext(QStringLiteral("rules"))) { 50 | testEmpty(args); 51 | listRules(_parser->isSet(QStringLiteral("short")), 52 | _parser->isSet(QStringLiteral("user"))); 53 | } else if(_parser->enterContext(QStringLiteral("clear"))) 54 | clear(args); 55 | else if(_parser->enterContext(QStringLiteral("frontend"))) { 56 | testEmpty(args); 57 | if(_parser->isSet(QStringLiteral("set"))) { 58 | setFrontend(_parser->value(QStringLiteral("set")).split(QLatin1Char(' ')), 59 | _parser->isSet(QStringLiteral("waved"))); 60 | } else if(_parser->isSet(QStringLiteral("reset"))) 61 | resetFrontend(); 62 | else 63 | frontend(); 64 | } else 65 | throw QStringLiteral("Invalid arguments"); 66 | _parser->leaveContext(); 67 | } catch(QString &e) { 68 | qCritical().noquote() << e; 69 | qApp->exit(EXIT_FAILURE); 70 | } 71 | } 72 | 73 | void CliController::setup() 74 | { 75 | _parser->setApplicationDescription(QStringLiteral("A tool to manage rebuilding of AUR packages based on their dependencies")); 76 | _parser->addVersionOption(); 77 | _parser->addHelpOption(); 78 | 79 | _parser->addOption({ 80 | QStringLiteral("verbose"), 81 | QStringLiteral("Run in verbose mode to output more information.") 82 | }); 83 | 84 | _parser->addLeafNode(QStringLiteral("rebuild"), QStringLiteral("Build all packages that need a rebuild.")); 85 | _parser->setDefaultNode(QStringLiteral("rebuild")); 86 | 87 | auto updateNode = _parser->addLeafNode(QStringLiteral("update"), QStringLiteral("Mark packages as updated.")); 88 | updateNode->addPositionalArgument(QStringLiteral("packages"), 89 | QStringLiteral("The packages to be marked as updated."), 90 | QStringLiteral("[ ...]")); 91 | updateNode->addOption({ 92 | QStringLiteral("stdin"), 93 | QStringLiteral("Read the packages to be updated from stdin") 94 | }); 95 | 96 | auto createNode = _parser->addLeafNode(QStringLiteral("create"), QStringLiteral("Create a rule for a package and it's dependencies.")); 97 | createNode->addOption({ 98 | {QStringLiteral("d"), QStringLiteral("depends")}, 99 | QStringLiteral("Automatically all direct dependencies of the given package as dependency") 100 | }); 101 | createNode->addPositionalArgument(QStringLiteral("package"), QStringLiteral("The package to create a rule for.")); 102 | createNode->addPositionalArgument(QStringLiteral("dependencies"), 103 | QStringLiteral("The packages this one depends on and requires a rebuild for."), 104 | QStringLiteral("[ ...]")); 105 | 106 | auto removeNode = _parser->addLeafNode(QStringLiteral("remove"), QStringLiteral("Remove previously created package rules")); 107 | removeNode->addPositionalArgument(QStringLiteral("packages"), 108 | QStringLiteral("The packages to remove the rules for."), 109 | QStringLiteral("[ ...]")); 110 | 111 | auto listNode = _parser->addLeafNode(QStringLiteral("list"), QStringLiteral("List all packages that need to be rebuilt.")); 112 | listNode->addOption({ 113 | {QStringLiteral("d"), QStringLiteral("detail")}, 114 | QStringLiteral("Display a detailed table with all packages and the dependencies that triggered them.") 115 | }); 116 | 117 | auto rulesNode = _parser->addLeafNode(QStringLiteral("rules"), QStringLiteral("List all rules known to repkg")); 118 | rulesNode->addOption({ 119 | {QStringLiteral("s"), QStringLiteral("short")}, 120 | QStringLiteral("Only display the package names, not the path to the rule file.") 121 | }); 122 | rulesNode->addOption({ 123 | {QStringLiteral("u"), QStringLiteral("user")}, 124 | QStringLiteral("Only display rules the belong to the currently executing user.") 125 | }); 126 | 127 | 128 | auto clearNode = _parser->addLeafNode(QStringLiteral("clear"), 129 | QStringLiteral("Clear all packages that are marked to be rebuilt, or only the ones specified as parameters.")); 130 | clearNode->addPositionalArgument(QStringLiteral("packages"), 131 | QStringLiteral("The packages to be cleared. If no packages are specified, all packages are cleared."), 132 | QStringLiteral("[ ...]")); 133 | 134 | auto frontendNode = _parser->addLeafNode(QStringLiteral("frontend"), QStringLiteral("Display the current frontend or set a custom one.")); 135 | frontendNode->addOption({ 136 | {QStringLiteral("s"), QStringLiteral("set")}, 137 | QStringLiteral("Instead of displaying the tool, set a new as the one to be used by repkg."), 138 | QStringLiteral("tool") 139 | }); 140 | frontendNode->addOption({ 141 | QStringLiteral("waved"), 142 | QStringLiteral("Combine with '--set'. Instead of passing all packages at once, split the into waves to avoid multiple " 143 | "rebuilds for frontends that do not support orderd parameters.") 144 | }); 145 | frontendNode->addOption({ 146 | {QStringLiteral("r"), QStringLiteral("reset")}, 147 | QStringLiteral("Reset the frontend, so that repkg can automatically find the default to be used with correct parameters") 148 | }); 149 | } 150 | 151 | void CliController::rebuild() 152 | { 153 | qApp->exit(_runner->run(_resolver->listPkgWaves())); 154 | } 155 | 156 | void CliController::update(QStringList pkgs, bool fromStdin) 157 | { 158 | if(fromStdin) { 159 | if(!pkgs.isEmpty()) 160 | qWarning() << "Ignoring packages passed as arguments, reading from stdin"; 161 | QFile in; 162 | in.open(stdin, QIODevice::ReadOnly); 163 | pkgs = QString::fromUtf8(in.readAll().simplified()).split(QLatin1Char(' '), QString::SkipEmptyParts); 164 | } 165 | _resolver->updatePkgs(pkgs); 166 | qApp->quit(); 167 | } 168 | 169 | void CliController::create(const QString &pkg, bool autoDepends, const QStringList &rules) 170 | { 171 | _rules->createRule(pkg, autoDepends, rules); 172 | qApp->quit(); 173 | } 174 | 175 | void CliController::remove(const QStringList &pkgs) 176 | { 177 | for(const auto &pkg : pkgs) 178 | _rules->removeRule(pkg); 179 | qInfo() << "Remember to run `sudo repkg update ` to remove any already scheduled rebuilds"; 180 | qApp->quit(); 181 | } 182 | 183 | void CliController::list(bool detail) 184 | { 185 | if(detail) 186 | qInfo().noquote() << _resolver->listDetailPkgs(); 187 | else { 188 | auto list = _resolver->listPkgs(); 189 | if(!list.isEmpty()) 190 | qInfo().noquote() << list.join(QStringLiteral(" ")); 191 | } 192 | qApp->quit(); 193 | } 194 | 195 | void CliController::listRules(bool listShort, bool userOnly) 196 | { 197 | qInfo().noquote() << _rules->listRules(listShort, userOnly); 198 | qApp->quit(); 199 | } 200 | 201 | void CliController::clear(const QStringList &pkgs) 202 | { 203 | _resolver->clear(pkgs); 204 | qApp->quit(); 205 | } 206 | 207 | void CliController::frontend() 208 | { 209 | qInfo().noquote() << _runner->frontendDescription(); 210 | qApp->quit(); 211 | } 212 | 213 | void CliController::setFrontend(const QStringList &frontend, bool waved) 214 | { 215 | _runner->setFrontend(frontend, waved); 216 | qApp->quit(); 217 | } 218 | 219 | void CliController::resetFrontend() 220 | { 221 | _runner->resetFrontend(); 222 | qApp->quit(); 223 | } 224 | 225 | void CliController::testEmpty(const QStringList &args) 226 | { 227 | if(!args.isEmpty()) 228 | throw QStringLiteral("Unexpected arguments after command!"); 229 | } 230 | -------------------------------------------------------------------------------- /rulecontroller.cpp: -------------------------------------------------------------------------------- 1 | #include "rulecontroller.h" 2 | #include "global.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | using namespace global; 11 | 12 | RuleController::RuleController(PacmanRunner *runner, QObject *parent) : 13 | QObject{parent}, 14 | _runner{runner} 15 | {} 16 | 17 | void RuleController::createRule(const QString &pkg, bool autoDepends, QStringList deps) 18 | { 19 | QDir path; 20 | if(isRoot()) 21 | path = rootPath(); 22 | else 23 | path = userPath(); 24 | 25 | if(autoDepends) 26 | deps.append(_runner->listDependencies(pkg)); 27 | 28 | QFile ruleFile(path.absoluteFilePath(pkg + QStringLiteral(".rule"))); 29 | if(!ruleFile.open(QIODevice::WriteOnly | QIODevice::Text)) { 30 | throw QStringLiteral("Failed to create rule file for %1 with error: %2") 31 | .arg(pkg, ruleFile.errorString()); 32 | } 33 | 34 | ruleFile.write(deps.join(QStringLiteral(" ")).toUtf8()); 35 | ruleFile.close(); 36 | qDebug() << "Created rule for" << qUtf8Printable(pkg) << "as:" << ruleFile.fileName(); 37 | } 38 | 39 | void RuleController::removeRule(const QString &pkg) 40 | { 41 | QDir path; 42 | if(isRoot()) 43 | path = rootPath(); 44 | else 45 | path = userPath(); 46 | 47 | QFile ruleFile(path.absoluteFilePath(pkg + QStringLiteral(".rule"))); 48 | if(!ruleFile.exists()) 49 | qWarning() << "Rule for" << qUtf8Printable(pkg) << "does not exist"; 50 | else if(!ruleFile.remove()) 51 | throw QStringLiteral("Failed to remove rule file for %1").arg(pkg); 52 | } 53 | 54 | QString RuleController::listRules(bool pkgOnly, bool userOnly) 55 | { 56 | if(_rules.isEmpty()) 57 | readRules(); 58 | 59 | if(pkgOnly) { 60 | QStringList pkgs; 61 | pkgs.reserve(_ruleSources.size()); 62 | for(auto it = _ruleSources.constBegin(); it != _ruleSources.constEnd(); it++) { 63 | if(userOnly && (it->isRoot != isRoot())) 64 | continue; 65 | pkgs.append(it.key()); 66 | } 67 | return pkgs.join(QLatin1Char(' ')); 68 | } else { 69 | auto baselen = 9; 70 | for(auto it = _ruleSources.constBegin(); it != _ruleSources.constEnd(); it++) 71 | baselen = std::max(baselen, it.key().size() + 2); 72 | 73 | QStringList pkgs; 74 | pkgs.reserve(_ruleSources.size() + 2); 75 | pkgs.append(QStringLiteral("%1| Origin | Ext. | Triggers").arg(QStringLiteral(" Package"), -baselen)); 76 | pkgs.append(QStringLiteral("-").repeated(baselen) + QLatin1Char('|') + 77 | QStringLiteral("-").repeated(8) + QLatin1Char('|') + 78 | QStringLiteral("-").repeated(6) + QLatin1Char('|') + 79 | QStringLiteral("-").repeated(67 - baselen)); 80 | 81 | for(auto it = _ruleSources.constBegin(); it != _ruleSources.constEnd(); it++) { 82 | if(userOnly && (it->isRoot != isRoot())) 83 | continue; 84 | pkgs.append(QStringLiteral(" %1| %2| %3| %4") 85 | .arg(it.key(), -(baselen - 1)) 86 | .arg(it->isRoot ? QStringLiteral("System") : QStringLiteral("User"), -7) 87 | .arg(it->extension ? QStringLiteral("Yes") : QStringLiteral("No"), -5) 88 | .arg(it->targets.join(QLatin1Char(' ')))); 89 | } 90 | return pkgs.join(QLatin1Char('\n')); 91 | } 92 | } 93 | 94 | QList RuleController::findRules(const QString &pkg) 95 | { 96 | if(_rules.isEmpty()) 97 | readRules(); 98 | return _rules.values(pkg); 99 | } 100 | 101 | void RuleController::readRules() 102 | { 103 | QList> paths { 104 | {userPath(), false}, 105 | {rootPath(), true}, 106 | {systemPath(), true}, 107 | }; 108 | 109 | _ruleSources.clear(); 110 | _rules.clear(); 111 | QHash, bool>> ruleBase; // (rules, extension) 112 | QHash, bool>> wildcardRules; // (pattern, rules, extension) 113 | 114 | for(auto &path : paths) { 115 | auto &dir = path.first; 116 | dir.setFilter(QDir::Files | QDir::NoDotAndDotDot | QDir::Readable); 117 | dir.setNameFilters({QStringLiteral("*.rule")}); 118 | for(const auto &fileInfo : dir.entryInfoList()) { 119 | RuleSource ruleSrc; 120 | auto name = fileInfo.completeBaseName(); 121 | ruleSrc.isRoot = path.second; 122 | // check for extension rules 123 | if(name.startsWith(QLatin1Char('+'))) { 124 | name = name.mid(1); 125 | ruleSrc.extension = true; 126 | } 127 | 128 | // special handling for wildcard rules 129 | if(name.contains(QLatin1Char('*')) || 130 | name.contains(QLatin1Char('?')) || 131 | (name.contains(QLatin1Char('[')) && name.contains(QLatin1Char(']')))) { 132 | auto definitions = readRuleDefinitions(fileInfo, ruleSrc); 133 | if(wildcardRules.contains(name)) { 134 | auto &entry = wildcardRules[name]; 135 | if(std::get<2>(entry)) { 136 | addRules(std::get<1>(entry), definitions); 137 | std::get<2>(entry) = ruleSrc.extension; 138 | } 139 | } else { 140 | QRegularExpression ruleRegex { 141 | QRegularExpression::wildcardToRegularExpression(name), 142 | QRegularExpression::DontCaptureOption 143 | }; 144 | wildcardRules.insert(name, std::make_tuple(std::move(ruleRegex), std::move(definitions), ruleSrc.extension)); 145 | } 146 | } else { // normal rules are treated normall 147 | // skip already handeled rules 148 | if(ruleBase.contains(name)) 149 | continue; 150 | // read rule definitions and add to mapping and rule list 151 | ruleBase.insert(name, {readRuleDefinitions(fileInfo, ruleSrc), ruleSrc.extension}); 152 | } 153 | _ruleSources.insert(name, ruleSrc); 154 | } 155 | } 156 | 157 | // find ALL foreign packages and match them against the wildcards to add them if neccessary 158 | if(!wildcardRules.isEmpty()) { 159 | for(const auto &pkg : _runner->readForeignPackages()) { 160 | // skip already existing rules 161 | if(ruleBase.contains(pkg)) 162 | continue; 163 | // match againts wildcards 164 | for(const auto &wTpl : qAsConst(wildcardRules)) { 165 | if(std::get<0>(wTpl).match(pkg).hasMatch()) 166 | ruleBase.insert(pkg, {std::get<1>(wTpl), std::get<2>(wTpl)}); 167 | } 168 | } 169 | } 170 | 171 | for(auto it = ruleBase.begin(); it != ruleBase.end(); it++) { 172 | // add regex rules to extensible normal rules 173 | if(it->second) { 174 | for(const auto &wTpl : qAsConst(wildcardRules)) { 175 | if(std::get<0>(wTpl).match(it.key()).hasMatch()) 176 | addRules(it->first, std::get<1>(wTpl)); 177 | } 178 | } 179 | 180 | //invert rules for easier evaluation 181 | for(auto &rule : it->first) { 182 | auto name = it.key(); 183 | std::swap(name, rule.package); 184 | _rules.insert(name, rule); 185 | } 186 | } 187 | } 188 | 189 | QList RuleController::readRuleDefinitions(const QFileInfo &fileInfo, RuleSource &srcBase) 190 | { 191 | QFile file{fileInfo.absoluteFilePath()}; 192 | if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) { 193 | qWarning() << "Failed to read file" << file.fileName() 194 | << "with error" << file.errorString(); 195 | return {}; 196 | } 197 | 198 | auto str = QString::fromUtf8(file.readAll()); 199 | file.close(); 200 | static const QRegularExpression splitRegex{QStringLiteral("\\s+"), QRegularExpression::DontCaptureOption}; 201 | auto pkgs = str.split(splitRegex, QString::SkipEmptyParts); 202 | 203 | QList rules; 204 | for(auto pkgInfo : pkgs) { //MAJOR make const & again 205 | { 206 | //MAJOR compat workaround to filter out old regex syntax 207 | static const QRegularExpression pkgCompatRegex{QStringLiteral(R"__(^(.*?)(?:{{(.*)}})?$)__")}; 208 | const auto compatMatch = pkgCompatRegex.match(pkgInfo); 209 | if(compatMatch.hasMatch()) 210 | pkgInfo = compatMatch.captured(1); 211 | } 212 | 213 | static const QRegularExpression pkgInfoRegex{QStringLiteral(R"__(^(.+?)(?:=([\dvsr:]+))?$)__")}; 214 | auto match = pkgInfoRegex.match(pkgInfo); 215 | Q_ASSERT(match.hasMatch()); 216 | 217 | RuleInfo rule; 218 | rule.package = match.captured(1); 219 | if(match.capturedLength(2) > 0) 220 | parseScope(rule, match.capturedRef(2)); 221 | rules.append(rule); 222 | srcBase.targets.append(rule.package); 223 | } 224 | 225 | return rules; 226 | } 227 | 228 | void RuleController::parseScope(RuleInfo &ruleInfo, const QStringRef &scopeStr) 229 | { 230 | // first: extract the substring range if applicable 231 | QStringRef parseStr; 232 | static const QRegularExpression rangeRegex{QStringLiteral(R"__(^:(\d+)(?::(\d+))?(?:::(.*)$|$))__")}; 233 | const auto rangeMatch = rangeRegex.match(scopeStr); 234 | if(rangeMatch.hasMatch()) { 235 | ruleInfo.range = RuleInfo::RangeContent{}; 236 | ruleInfo.range->first = rangeMatch.capturedRef(1).toInt(); 237 | if(rangeMatch.capturedLength(2) > 0) 238 | ruleInfo.range->second = rangeMatch.capturedRef(2).toInt(); 239 | if(rangeMatch.capturedLength(3) > 0) 240 | parseStr = rangeMatch.capturedRef(3); 241 | } else 242 | parseStr = scopeStr; 243 | 244 | if(!parseStr.isNull()) { 245 | static const QRegularExpression scopeRegex{QStringLiteral(R"__(^(?:\d+|v|s|r)$)__")}; 246 | if(!scopeRegex.match(parseStr).hasMatch()) { 247 | //TODO print warning 248 | } else { 249 | if(parseStr == QStringLiteral("r")) 250 | ruleInfo.scope = RuleScope::Revision; 251 | else if(parseStr == QStringLiteral("s")) 252 | ruleInfo.scope = RuleScope::Suffix; 253 | else if(parseStr == QStringLiteral("v")) 254 | ruleInfo.scope = RuleScope::Version; 255 | else if(parseStr == QStringLiteral("0")) 256 | ruleInfo.scope = RuleScope::Epoche; 257 | else { 258 | ruleInfo.scope = RuleScope::Version; 259 | ruleInfo.count = parseStr.toInt(); 260 | } 261 | } 262 | } 263 | } 264 | 265 | void RuleController::addRules(QList &target, const QList &newRules) 266 | { 267 | const auto begin = target.begin(); 268 | const auto end = target.end(); 269 | for(const auto &rule : newRules) { 270 | auto fIndex = std::find_if(begin, end, [rule](const RuleInfo &tRule){ 271 | return rule.package == tRule.package; 272 | }); 273 | if(fIndex == target.end()) 274 | target.append(rule); 275 | } 276 | } 277 | --------------------------------------------------------------------------------