├── .clang-format ├── .github └── workflows │ └── arch.x86_64.yml ├── .gitignore ├── .ycm_extra_conf.py ├── LICENSE ├── README.md ├── build-aux ├── ci-test ├── source-format └── test-san ├── extra ├── bash_completion └── zsh_completion ├── man └── auracle.1.pod ├── meson.build ├── meson_options.txt ├── src ├── aur │ ├── client.cc │ ├── client.hh │ ├── package.hh │ ├── request.cc │ ├── request.hh │ ├── request_test.cc │ ├── response.cc │ ├── response.hh │ └── response_test.cc ├── auracle │ ├── auracle.cc │ ├── auracle.hh │ ├── dependency.cc │ ├── dependency.hh │ ├── dependency_kind.cc │ ├── dependency_kind.hh │ ├── dependency_kind_test.cc │ ├── dependency_test.cc │ ├── format.cc │ ├── format.hh │ ├── format_test.cc │ ├── package_cache.cc │ ├── package_cache.hh │ ├── package_cache_test.cc │ ├── pacman.cc │ ├── pacman.hh │ ├── search_fragment.cc │ ├── search_fragment.hh │ ├── search_fragment_test.cc │ ├── sort.cc │ ├── sort.hh │ ├── sort_test.cc │ ├── terminal.cc │ └── terminal.hh ├── auracle_main.cc └── test │ └── gtest_main.cc └── tests ├── __init__.py ├── auracle_test.py ├── fakeaur ├── __init__.py ├── db │ ├── info │ │ ├── auracle-git │ │ ├── camlidl │ │ ├── curl-c-ares │ │ ├── curl-git │ │ ├── curl-http3-ngtcp2 │ │ ├── curl-quiche-git │ │ ├── gapi-ocaml │ │ ├── google-drive-ocamlfuse │ │ ├── mingw-w64-environment │ │ ├── nlohmann-json │ │ ├── ocaml-base │ │ ├── ocaml-configurator │ │ ├── ocaml-cryptokit │ │ ├── ocaml-curl │ │ ├── ocaml-extlib │ │ ├── ocaml-ounit │ │ ├── ocaml-pcre │ │ ├── ocaml-sexplib0 │ │ ├── ocaml-sqlite3 │ │ ├── ocaml-stdio │ │ ├── ocaml-xmlm │ │ ├── ocaml-zarith │ │ ├── ocamlfuse │ │ ├── ocamlnet │ │ ├── pacman-fancy-progress-git │ │ ├── pacman-git │ │ ├── pacman-pb │ │ ├── pkgfile-git │ │ ├── python-booleanoperations │ │ ├── python-defcon │ │ ├── python-fontmath │ │ ├── python-fontparts │ │ ├── python-fontpens │ │ ├── python-pyclipper │ │ └── yaourt │ ├── refreshdb │ └── search │ │ ├── maintainer|falconindy │ │ ├── name-desc|aura │ │ ├── name-desc|aurac │ │ ├── name-desc|auracle │ │ ├── name-desc|auracle-git │ │ ├── name-desc|git │ │ ├── name-desc|le-git │ │ ├── name-desc|systemd │ │ ├── provides|curl │ │ └── provides|pacman ├── git └── server.py ├── fakepacman ├── local │ ├── ALPM_DB_VERSION │ ├── auracle-git-r20.fb982ca-1 │ │ └── desc │ ├── ocaml-4.07.0-1 │ │ ├── desc │ │ └── mtree │ └── pkgfile-git-18.5.gf31f10b-1 │ │ └── desc └── sync │ ├── community.db │ ├── community.db.tar.gz │ ├── extra.db │ └── extra.db.tar.gz ├── test_buildorder.py ├── test_clone.py ├── test_custom_format.py ├── test_info.py ├── test_outdated.py ├── test_raw_query.py ├── test_regex_search.py ├── test_resolve.py ├── test_search.py ├── test_show.py ├── test_sort.py └── test_update.py /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | BasedOnStyle: Google 3 | -------------------------------------------------------------------------------- /.github/workflows/arch.x86_64.yml: -------------------------------------------------------------------------------- 1 | name: arch.x86_64 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | 8 | container: 9 | image: archlinux 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - run: build-aux/ci-test plain 14 | 15 | # ASAN tests don't work with abseil when it's linked as a shared lib. 16 | # test_asan: 17 | # runs-on: ubuntu-latest 18 | 19 | # container: 20 | # image: archlinux 21 | 22 | # steps: 23 | # - uses: actions/checkout@v4 24 | # - run: build-aux/ci-test asan 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | subprojects/*/ 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /.ycm_extra_conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Google Inc. 2 | # 3 | # This file is part of ycmd. 4 | # 5 | # ycmd is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # ycmd is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with ycmd. If not, see . 17 | 18 | import os 19 | import ycm_core 20 | 21 | # These are the compilation flags that will be used in case there's no 22 | # compilation database set (by default, one is not set). 23 | # CHANGE THIS LIST OF FLAGS. YES, THIS IS THE DROID YOU HAVE BEEN LOOKING FOR. 24 | flags = [ 25 | '-Wall', 26 | '-Wextra', 27 | '-Werror', 28 | '-fexceptions', 29 | '-DNDEBUG', 30 | # THIS IS IMPORTANT! Without a "-std=" flag, clang won't know which 31 | # language to use when compiling headers. So it will guess. Badly. So C++ 32 | # headers will be compiled as C headers. You don't want that so ALWAYS specify 33 | # a "-std=". 34 | # For a C project, you would set this to something like 'c99' instead of 35 | # 'c++11'. 36 | '-std=c++17', 37 | # ...and the same thing goes for the magic -x option which specifies the 38 | # language that the files to be compiled are written in. This is mostly 39 | # relevant for c++ headers. 40 | # For a C project, you would set this to 'c' instead of 'c++'. 41 | '-x', 42 | 'c++', 43 | '-isystem', 44 | '/usr/include', 45 | '-isystem', 46 | '/usr/local/include', 47 | ] 48 | 49 | 50 | # Set this to the absolute path to the folder (NOT the file!) containing the 51 | # compile_commands.json file to use that instead of 'flags'. See here for 52 | # more details: http://clang.llvm.org/docs/JSONCompilationDatabase.html 53 | # 54 | # Most projects will NOT need to set this to anything; you can just change the 55 | # 'flags' list of compilation flags. 56 | compilation_database_folder = 'build' 57 | 58 | if os.path.exists( compilation_database_folder ): 59 | database = ycm_core.CompilationDatabase( compilation_database_folder ) 60 | else: 61 | database = None 62 | 63 | SOURCE_EXTENSIONS = [ '.cpp', '.cxx', '.cc', '.c', '.m', '.mm' ] 64 | 65 | def DirectoryOfThisScript(): 66 | return os.path.dirname( os.path.abspath( __file__ ) ) 67 | 68 | 69 | def MakeRelativePathsInFlagsAbsolute( flags, working_directory ): 70 | if not working_directory: 71 | return list( flags ) 72 | new_flags = [] 73 | make_next_absolute = False 74 | path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ] 75 | for flag in flags: 76 | new_flag = flag 77 | 78 | if make_next_absolute: 79 | make_next_absolute = False 80 | if not flag.startswith( '/' ): 81 | new_flag = os.path.join( working_directory, flag ) 82 | 83 | for path_flag in path_flags: 84 | if flag == path_flag: 85 | make_next_absolute = True 86 | break 87 | 88 | if flag.startswith( path_flag ): 89 | path = flag[ len( path_flag ): ] 90 | new_flag = path_flag + os.path.join( working_directory, path ) 91 | break 92 | 93 | if new_flag: 94 | new_flags.append( new_flag ) 95 | return new_flags 96 | 97 | 98 | def IsHeaderFile( filename ): 99 | extension = os.path.splitext( filename )[ 1 ] 100 | return extension in [ '.h', '.hxx', '.hpp', '.hh' ] 101 | 102 | 103 | def GetCompilationInfoForFile( filename ): 104 | # The compilation_commands.json file generated by CMake does not have entries 105 | # for header files. So we do our best by asking the db for flags for a 106 | # corresponding source file, if any. If one exists, the flags for that file 107 | # should be good enough. 108 | if IsHeaderFile( filename ): 109 | basename = os.path.splitext( filename )[ 0 ] 110 | for extension in SOURCE_EXTENSIONS: 111 | replacement_file = basename + extension 112 | if os.path.exists( replacement_file ): 113 | compilation_info = database.GetCompilationInfoForFile( 114 | replacement_file ) 115 | if compilation_info.compiler_flags_: 116 | return compilation_info 117 | return None 118 | return database.GetCompilationInfoForFile( filename ) 119 | 120 | 121 | # This is the entry point; this function is called by ycmd to produce flags for 122 | # a file. 123 | def FlagsForFile( filename, **kwargs ): 124 | if database: 125 | # Bear in mind that compilation_info.compiler_flags_ does NOT return a 126 | # python list, but a "list-like" StringVec object 127 | compilation_info = GetCompilationInfoForFile( filename ) 128 | if not compilation_info: 129 | return None 130 | 131 | final_flags = MakeRelativePathsInFlagsAbsolute( 132 | compilation_info.compiler_flags_, 133 | compilation_info.compiler_working_dir_ ) 134 | else: 135 | relative_to = DirectoryOfThisScript() 136 | final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to ) 137 | 138 | return { 'flags': final_flags } 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2020 Dave Reisner 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub Actions status | falconindy/auracle](https://github.com/falconindy/auracle/workflows/arch.x86_64/badge.svg) 2 | 3 | ## What is Auracle? 4 | 5 | Auracle is a command line tool used to interact with Arch Linux's User 6 | Repository, commonly referred to as the [AUR](https://aur.archlinux.org). 7 | 8 | ### Features 9 | 10 | Auracle has a number of actions it can perform: 11 | 12 | * `search`: find packages in the AUR by regular expression. 13 | * `info`: return detailed information about packages. 14 | * `show`: show the contents of a source file for a package (e.g. the PKGBUILD) 15 | * `resolve`: find packages which provide dependencies. 16 | * `raw{info,search}`: similar to info and search, but output raw json responses 17 | rather than formatting them. 18 | * `clone`: clone the git repository for packages. 19 | * `buildorder`: show the order and origin of packages that need to be built for 20 | a given set of AUR packages. 21 | * `outdated`: attempt to find updates for installed AUR packages. 22 | * `update`: clone out of date foreign packages 23 | 24 | ### Non-goals 25 | 26 | Auracle does not currently, and will probably never: 27 | 28 | * Build packages for you. 29 | * Look at upstream VCS repos to find updates. 30 | 31 | ### Building and Testing 32 | 33 | Building auracle requires: 34 | 35 | * A C++23 compiler 36 | * meson 37 | * libsystemd 38 | * libalpm 39 | * libcurl 40 | 41 | Testing additionally depends on: 42 | 43 | * gmock 44 | * gtest 45 | * python3 46 | 47 | You're probably building this from the AUR, though, so just go use the 48 | [PKGBUILD](https://aur.archlinux.org/packages/auracle-git). 49 | 50 | If you're hacking on auracle, you can do this manually: 51 | 52 | ```sh 53 | $ meson setup build 54 | $ meson compile -C build 55 | ``` 56 | 57 | And running the tests is simply a matter of: 58 | 59 | ```sh 60 | $ meson test -C build 61 | ``` 62 | -------------------------------------------------------------------------------- /build-aux/ci-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash -le 2 | 3 | debug() { 4 | echo "::debug::$*" 5 | } 6 | 7 | error() { 8 | echo "::error::$*" 9 | } 10 | 11 | fatal() { 12 | error "$*" 13 | exit 1 14 | } 15 | 16 | group() { 17 | echo "::group::$*" 18 | } 19 | 20 | endgroup() { 21 | echo "::endgroup::" 22 | } 23 | 24 | debug "Running with buildmode=$buildmode" 25 | 26 | group 'setup' 27 | 28 | buildmode=$1 29 | setup_flags=() 30 | builddir=build-$buildmode 31 | test_install=0 32 | 33 | case $buildmode in 34 | plain) 35 | test_install=1 36 | ;; 37 | asan) 38 | setup_flags=('-Db_sanitize=address') 39 | ;; 40 | *) 41 | fatal "unknown build mode $buildmode" 42 | ;; 43 | esac 44 | 45 | pacman -Syu --noconfirm abseil-cpp base-devel fmt git glaze gmock gtest meson perl python 46 | 47 | # Needed to ensure PATH is properly set for perl, etc. 48 | source /etc/profile 49 | 50 | endgroup 51 | 52 | # build 53 | group 'build' 54 | meson setup "$builddir" \ 55 | --prefix=/usr \ 56 | --buildtype=debugoptimized \ 57 | "${setup_flags[@]}" 58 | meson compile -C "$builddir" 59 | endgroup 60 | 61 | # test 62 | group 'test' 63 | meson test -C "$builddir" 64 | endgroup 65 | 66 | # install 67 | if (( test_install )); then 68 | group 'install' 69 | meson install -C "$builddir" 70 | endgroup 71 | fi 72 | -------------------------------------------------------------------------------- /build-aux/source-format: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [[ $MESON_SOURCE_ROOT ]]; then 6 | cd "$MESON_SOURCE_ROOT" 7 | fi 8 | 9 | git ls-files -z -- '*.cc' '*.hh' | xargs -0 clang-format -i 10 | git ls-files -z -- 'tests/*.py' | xargs -0 yapf -i 11 | -------------------------------------------------------------------------------- /build-aux/test-san: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | sanitizer=${1:-address} 6 | 7 | if [[ -d build ]]; then 8 | verb=configure 9 | else 10 | verb=setup 11 | fi 12 | 13 | meson "$verb" build -Db_sanitize="$sanitizer" 14 | meson test -C build 15 | -------------------------------------------------------------------------------- /extra/bash_completion: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | __contains_word () { 4 | local w word=$1; shift 5 | 6 | for w in "$@"; do 7 | [[ $w = "$word" ]] && return 0 8 | done 9 | 10 | return 1 11 | } 12 | 13 | _auracle() { 14 | local cur prev words cword split 15 | _init_completion -s || return 16 | 17 | local i verb comps 18 | local -A OPTS=( 19 | [STANDALONE]='--help -h --version --quiet -q --recurse -r --literal' 20 | [ARG]='-C --chdir --searchby --color --sort --rsort --show-file -F --format --resolve-deps' 21 | ) 22 | 23 | if __contains_word "$prev" ${OPTS[ARG]}; then 24 | case $prev in 25 | '--color') 26 | comps='always never auto' 27 | ;; 28 | '--searchby') 29 | comps='name name-desc maintainer depends makedepends optdepends 30 | checkdepends submitter provides conflicts replaces 31 | keywords groups maintainers' 32 | ;; 33 | '--sort'|'--rsort') 34 | comps="name votes popularity firstsubmitted lastmodified" 35 | ;; 36 | '-C'|'--chdir') 37 | comps=$(compgen -A directory -- "$cur" ) 38 | compopt -o filenames 39 | ;; 40 | '--resolve-deps') 41 | local c=({^,!,+,}{check,make,}depends) 42 | comps="${c[*]}" 43 | ;; 44 | esac 45 | 46 | COMPREPLY=($(compgen -W '$comps' -- "$cur")) 47 | return 0 48 | fi 49 | 50 | if [[ "$cur" = -* ]]; then 51 | COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") ) 52 | return 0 53 | fi 54 | 55 | local -A VERBS=( 56 | [AUR_PACKAGES]='buildorder clone show info rawinfo' 57 | [LOCAL_PACKAGES]='outdated update' 58 | [NONE]='search rawsearch resolve' 59 | ) 60 | 61 | for ((i=0; i < COMP_CWORD; i++)); do 62 | if __contains_word "${COMP_WORDS[i]}" ${VERBS[*]} && 63 | ! __contains_word "${COMP_WORDS[i-1]}" ${OPTS[ARG]}; then 64 | verb=${COMP_WORDS[i]} 65 | break 66 | fi 67 | done 68 | 69 | 70 | if [[ -z $verb ]]; then 71 | comps=${VERBS[*]} 72 | elif __contains_word "$verb" ${VERBS[AUR_PACKAGES]}; then 73 | if (( ${#cur} >= 2 )); then 74 | comps=$(auracle search --quiet "$cur" 2>/dev/null) 75 | fi 76 | elif __contains_word "$verb" ${VERBS[LOCAL_PACKAGES]}; then 77 | comps=$(pacman -Qmq) 78 | fi 79 | 80 | COMPREPLY=($(compgen -W '$comps' -- "$cur")) 81 | return 0 82 | } 83 | 84 | complete -F _auracle auracle 85 | -------------------------------------------------------------------------------- /extra/zsh_completion: -------------------------------------------------------------------------------- 1 | #compdef auracle 2 | 3 | local curcontext=$curcontext state line 4 | 5 | _arguments -S \ 6 | {--help,-h}'[Show help]' \ 7 | '--version[Show software version]' \ 8 | {--quiet,-q}'[Output less, when possible]' \ 9 | {--recurse,-r}'[Recurse through dependencies on download]' \ 10 | '--literal[Disallow regex in searches]' \ 11 | '--searchby=[Change search-by dimension]: :(name name-desc maintainer depends makedepends optdepends checkdepends submitter provides conflicts replaces keywords groups comaintainers)' \ 12 | '--color=[Control colored output]: :(auto never always)' \ 13 | {--chdir=,-C+}'[Change directory before downloading]:directory:_files -/' \ 14 | {--format=,-F+}'[Specify custom output for search and info]' \ 15 | '(--rsort)--sort=[Sort results in ascending order]: :(name popularity votes firstsubmitted lastmodified)' \ 16 | '(--sort)--rsort=[Sort results in descending order]: :(name popularity votes firstsubmitted lastmodified)' \ 17 | '--resolve-deps=[Control depends resolution]: :(depends checkdepends makedepends)' \ 18 | "--show-file=[File to dump with 'show' command]" \ 19 | '(-): :->command' \ 20 | '*:: :->option-or-argument' 21 | 22 | case $state in 23 | 24 | (command) 25 | local -a commands 26 | commands=( 27 | 'buildorder:Show build order' 28 | 'clone:Clone or update git repos for packages' 29 | 'info:Show detailed information' 30 | 'rawinfo:Dump unformatted JSON for info query' 31 | 'rawsearch:Dump unformatted JSON for search query' 32 | 'resolve:Resolve dependencies' 33 | 'search:Search for packages' 34 | 'show:Dump package source file' 35 | 'outdated:Check for updates for foreign packages' 36 | 'update:Clone out of date foreign packages') 37 | _describe -t commands command commands 38 | ;; 39 | 40 | (option-or-argument) 41 | curcontext=${curcontext%:*}-$line[1]: 42 | local prefix=$PREFIX 43 | local -a packages 44 | case $line[1] in 45 | (buildorder|clone|info|rawinfo|show) 46 | [[ $compstate[quote] = [\'\"] ]] && prefix=$compstate[quote]$PREFIX$compstate[quote] 47 | packages=(${(f)"$(auracle search --quiet "${(Q)prefix}" 2> /dev/null)"}) 48 | _describe -t packages package packages 49 | ;; 50 | (outdated|update) 51 | packages=(${(f)"$(pacman -Qmq 2> /dev/null)"}) 52 | _describe -t packages package packages 53 | ;; 54 | esac 55 | ;; 56 | 57 | esac 58 | 59 | return 0 60 | -------------------------------------------------------------------------------- /man/auracle.1.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | auracle - an AUR client with a focus on flexibility 4 | 5 | =head1 SYNOPSIS 6 | 7 | I [OPTIONS...] COMMAND [ARGS...] 8 | 9 | =head1 DESCRIPTION 10 | 11 | auracle is a tool for querying information stored by the Arch User Repository 12 | (AUR). Invoking auracle consists of supplying an operation, any applicable 13 | options, and usually one or more arguments. 14 | 15 | =head1 OPTIONS 16 | 17 | =over 4 18 | 19 | =item B<--color=> 20 | 21 | Control colored output. Argument must be one of I, I, or 22 | I. When set to I, color will be enabled if stdout is connected 23 | to a terminal. 24 | 25 | This option defaults to I. 26 | 27 | =item B<-h>, B<--help> 28 | 29 | Print a short help text and exit. 30 | 31 | =item B<--literal> 32 | 33 | When used with the B command, interpret all search terms as literal, 34 | rather than as regular expressions. 35 | 36 | =item B<--quiet> 37 | 38 | When used with the B and B commands, output will be limited to 39 | package names only. 40 | 41 | =item B<-r>, B<--recurse> 42 | 43 | When used with the B command, recursively follow and clone 44 | dependencies of each given argument. 45 | 46 | =item B<--resolve-deps=>I 47 | 48 | When performing recursive operations, control the kinds of dependencies that 49 | auracle will consider. By default, this is all dependency kinds -- I, 50 | I, and I. This flag takes a comma-delimited string 51 | value with one to many of these dependency kinds. 52 | 53 | Additionally, some special syntax exists to make this more composable. A 54 | leading I or I<^> character will cause dependencies to be removed from the 55 | set, whereas a leading I<+> will cause dependencies to be added to the set. 56 | These special indicators are only understood when they appear as the first 57 | character in the string and never after a comma. 58 | 59 | Specifying this flag multiple times is additive, for example: 60 | 61 | auracle buildorder --resolve-deps=depends --resolve-deps=+makedepends 62 | 63 | Is the same as I or I<^checkdepends>. 64 | 65 | =item B<--searchby=>I 66 | 67 | When used with the B and B commands, change the search 68 | dimension. I must be one of I, I, I, 69 | I, I, I, I, I, 70 | I, I, I, I, I, or 71 | I. 72 | 73 | This option defaults to I. 74 | 75 | =item B<--sort=>I, B<--rsort=>I 76 | 77 | For search and info queries, sorts the results in ascending and descending 78 | order, respectively. I must be one of: B, B, B, 79 | B, or B. 80 | 81 | This option defaults to sorting by B in ascending order. 82 | 83 | =item B<--show-file=>I 84 | 85 | Name of the file to fetch with the B command. 86 | 87 | This option defaults to B. 88 | 89 | =item B<-C >I, B<--chdir=>I 90 | 91 | Change directory to I before performing any actions. Only useful with the 92 | B command. 93 | 94 | =back 95 | 96 | =head1 COMMANDS 97 | 98 | =over 4 99 | 100 | =item B I... 101 | 102 | Pass one to many arguments to print a build order for the given packages. The 103 | resulting output will be the total ordering to build all packages. Each line is 104 | at least two columns delimited by a single whitespace. The first column contains an 105 | identifier and the second column names the dependency. Possible identifiers 106 | are: 107 | 108 | =over 4 109 | 110 | B The dependency was found in the AUR and needs to be built and 111 | installed. The output will contain an additional column of the pkgbase the 112 | package is provided by. 113 | 114 | B The dependency was found in a binary repo and can be installed via pacman. 115 | 116 | B The dependency was unable to be found anywhere. This might 117 | indicate a broken dependency chain. The output will contain additional columns 118 | denoting the dependency path that was walked (in reverse) to get to the missing 119 | dependency. 120 | 121 | =back 122 | 123 | Additionally, both of B and B can be prefixed with B to 124 | indicate that the dependency is already installed. B may be prefixed with 125 | B to indicate that the package was explicitly specified on the 126 | commandline. 127 | 128 | =item B I... 129 | 130 | Pass one to many arguments to get clone git repositories. Use the 131 | B<--recurse> flag to get dependencies of packages. If the git repository 132 | already exists, the repository will instead be updated. A best effort attempt 133 | will be made to preserve existing local changes to packages. Auracle will 134 | instruct git to autostash staged changes and will then rebase new incoming 135 | changes before unstashing. This is intended to handle the case of makepkg 136 | updating the I attribute of PKGBUILDs but may not handle more complex 137 | changes made by a local user. 138 | 139 | =item B I... 140 | 141 | Pass one to many arguments to perform an info query. 142 | 143 | =item B I... 144 | 145 | Dump the raw JSON response from the AUR for an info request. 146 | 147 | =item B I... 148 | 149 | Dump the raw JSON response from the AUR for search requests formed from the 150 | given terms. 151 | 152 | =item B I... 153 | 154 | Pass one to many arguments to perform a search query. Results will be the 155 | intersection of all terms. Terms may be a regular expression as described by 156 | the POSIX extended regex specification. 157 | 158 | B: the AUR does not actually support searching by regular expressions 159 | and support in auracle is implemented on a best-effort basis. 160 | 161 | =item B I... 162 | 163 | Pass one to many dependency string arguments to perform a search query for 164 | packages that can satisfy the input dependencies. Dependency syntax follows 165 | that of pacman and makepkg, for example: I or I=21>. 166 | 167 | =item B I... 168 | 169 | Pass one to many arguments to print source files for the given packages. The 170 | file fetched is controlled by the B<--show-file> flag. 171 | 172 | =item B [I...] 173 | 174 | Pass one to many arguments to check for newer versions existing in the AUR. 175 | Each argument is assumed to be a package installed locally using B(8). 176 | If no arguments are given, pacman is queried for all foreign packages as an 177 | input to this operation. 178 | 179 | =item B [I...] 180 | 181 | Similar to B, but download packages found to be outdated. Pass the 182 | B<--recurse> flag to download new dependencies of packages as well. 183 | 184 | =back 185 | 186 | =head1 CUSTOM FORMATTING 187 | 188 | Auracle provides the ability to customize the output format from B and 189 | B operations. The format specification is similar to that of python. 190 | Each field is wrapped in curly brackets and is optionally suffixed with a colon 191 | and additional formatting directives. 192 | 193 | B not all fields are available for B queries. 194 | 195 | String fields: B<{name}>, B<{version}>, B<{description}>, B<{submitter}>, 196 | B<{maintainer}>, B<{pkgbase}>, B<{url}>. 197 | 198 | Floating point fields: B<{votes}>, B<{popularity}>. 199 | 200 | Time fields: B<{submitted}>, B<{modified}>, B<{outofdate}>. These fields 201 | default to using your locale's default date and time, but can be further 202 | customized in their format, according to the specifier described in 203 | strftime(3), For example, B<{submitted:%Y.%m.%d}> would show year month and 204 | day in dotted form. 205 | 206 | String array fields: B<{depends}>, B<{makedepends}>, B<{checkdepends}>, 207 | B<{optdepends}>, B<{conflicts}>, B<{provides}>, B<{replaces}>, B<{groups}>, 208 | B<{keywords}>, B<{licenses}>, B<{comaintainers}>. These fields default to using 209 | a delimiter of two whitespace, but can be customized, e.g. B<{depends:,}> to 210 | comma delimit the values of the depends field. 211 | 212 | Example: recreating auracle's search output: 213 | 214 | auracle search -F $'aur/{name} ({votes}, {popularity})\n {description}' ... 215 | 216 | Example: listing dependencies of packages: 217 | 218 | auracle info -F '{depends} {makedepends} {checkdepends}' ... 219 | 220 | =head1 AUTHOR 221 | 222 | Dave Reisner Ed@falconindy.comE 223 | 224 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'auracle', 3 | 'cpp', 4 | version: '0', 5 | license: 'MIT', 6 | meson_version: '>=0.56', 7 | default_options: ['cpp_std=c++23', 'warning_level=2'], 8 | ) 9 | 10 | cpp = meson.get_compiler('cpp') 11 | 12 | add_project_arguments( 13 | '-DPROJECT_VERSION="@0@"'.format(meson.project_version()), 14 | language: 'cpp', 15 | ) 16 | 17 | add_project_arguments( 18 | cpp.get_supported_arguments(['-ffunction-sections', '-fdata-sections']), 19 | language: 'cpp', 20 | ) 21 | 22 | add_project_link_arguments( 23 | cpp.get_supported_link_arguments(['-Wl,--gc-sections']), 24 | language: 'cpp', 25 | ) 26 | 27 | if not cpp.has_header('glaze/glaze.hpp') 28 | error('glaze library not found in standard include paths') 29 | endif 30 | 31 | libalpm = dependency('libalpm') 32 | libcurl = dependency('libcurl') 33 | libfmt = dependency('fmt') 34 | libsystemd = dependency('libsystemd') 35 | gtest = dependency( 36 | 'gtest', 37 | version: '>=1.10.0', 38 | required: get_option('unittests'), 39 | disabler: true, 40 | ) 41 | gmock = dependency( 42 | 'gmock', 43 | version: '>=1.10.0', 44 | required: get_option('unittests'), 45 | disabler: true, 46 | ) 47 | 48 | abseil = declare_dependency( 49 | dependencies: [ 50 | dependency('absl_flat_hash_map'), 51 | dependency('absl_flat_hash_set'), 52 | dependency('absl_status'), 53 | dependency('absl_statusor'), 54 | dependency('absl_strings'), 55 | dependency('absl_time'), 56 | ], 57 | ) 58 | 59 | pod2man = find_program('pod2man') 60 | 61 | python = import('python') 62 | py3 = python.find_installation('python3') 63 | 64 | libaur = declare_dependency( 65 | link_with: [ 66 | static_library( 67 | 'aur', 68 | files( 69 | ''' 70 | src/aur/client.cc src/aur/client.hh 71 | src/aur/package.hh 72 | src/aur/request.cc src/aur/request.hh 73 | src/aur/response.cc src/aur/response.hh 74 | '''.split(), 75 | ), 76 | dependencies: [abseil, libcurl, libsystemd], 77 | include_directories: ['src'], 78 | ), 79 | ], 80 | include_directories: ['src'], 81 | ) 82 | 83 | libauracle = declare_dependency( 84 | link_with: [ 85 | static_library( 86 | 'auracle', 87 | files( 88 | ''' 89 | src/auracle/auracle.cc src/auracle/auracle.hh 90 | src/auracle/dependency.cc src/auracle/dependency.hh 91 | src/auracle/dependency_kind.cc src/auracle/dependency_kind.hh 92 | src/auracle/format.cc src/auracle/format.hh 93 | src/auracle/package_cache.cc src/auracle/package_cache.hh 94 | src/auracle/pacman.cc src/auracle/pacman.hh 95 | src/auracle/search_fragment.cc src/auracle/search_fragment.hh 96 | src/auracle/sort.cc src/auracle/sort.hh 97 | src/auracle/terminal.cc src/auracle/terminal.hh 98 | '''.split(), 99 | ), 100 | dependencies: [abseil, libalpm, libaur, libfmt], 101 | include_directories: ['src'], 102 | ), 103 | ], 104 | include_directories: ['src'], 105 | ) 106 | 107 | executable( 108 | 'auracle', 109 | files('src/auracle_main.cc'), 110 | dependencies: [abseil, libauracle], 111 | install: true, 112 | ) 113 | 114 | custom_target( 115 | 'man', 116 | output: 'auracle.1', 117 | input: 'man/auracle.1.pod', 118 | command: [ 119 | pod2man, 120 | '--section=1', 121 | '--center=Auracle Manual', 122 | '--name=AURACLE', 123 | '--release=Auracle @0@'.format(meson.project_version()), 124 | '@INPUT@', 125 | '@OUTPUT@', 126 | ], 127 | install: true, 128 | install_dir: join_paths(get_option('mandir'), 'man1'), 129 | ) 130 | 131 | install_data( 132 | files('extra/bash_completion'), 133 | rename: ['auracle'], 134 | install_dir: join_paths(get_option('datadir'), 'bash-completion/completions'), 135 | ) 136 | 137 | install_data( 138 | files('extra/zsh_completion'), 139 | rename: ['_auracle'], 140 | install_dir: join_paths(get_option('datadir'), 'zsh/site-functions'), 141 | ) 142 | 143 | run_target( 144 | 'fmt', 145 | command: [ 146 | join_paths(meson.project_source_root(), 'build-aux/source-format'), 147 | ], 148 | ) 149 | 150 | # unit tests 151 | test( 152 | 'libaur', 153 | executable( 154 | 'libaur_test', 155 | files( 156 | ''' 157 | src/test/gtest_main.cc 158 | src/aur/request_test.cc 159 | src/aur/response_test.cc 160 | '''.split(), 161 | ), 162 | dependencies: [abseil, gtest, gmock, libaur], 163 | ), 164 | protocol: 'gtest', 165 | suite: 'libaur', 166 | ) 167 | 168 | test( 169 | 'libauracle', 170 | executable( 171 | 'libauracle_test', 172 | files( 173 | ''' 174 | src/test/gtest_main.cc 175 | src/auracle/dependency_kind_test.cc 176 | src/auracle/package_cache_test.cc 177 | src/auracle/dependency_test.cc 178 | src/auracle/format_test.cc 179 | src/auracle/search_fragment_test.cc 180 | src/auracle/sort_test.cc 181 | '''.split(), 182 | ), 183 | dependencies: [abseil, gtest, gmock, libauracle], 184 | ), 185 | protocol: 'gtest', 186 | suite: 'libauracle', 187 | ) 188 | 189 | # integration tests 190 | python_requirement = '>=3.7' 191 | if py3.found() and py3.language_version().version_compare(python_requirement) 192 | foreach input : [ 193 | 'tests/test_buildorder.py', 194 | 'tests/test_clone.py', 195 | 'tests/test_custom_format.py', 196 | 'tests/test_info.py', 197 | 'tests/test_outdated.py', 198 | 'tests/test_raw_query.py', 199 | 'tests/test_regex_search.py', 200 | 'tests/test_resolve.py', 201 | 'tests/test_search.py', 202 | 'tests/test_show.py', 203 | 'tests/test_sort.py', 204 | 'tests/test_update.py', 205 | ] 206 | basename = input.split('/')[-1].split('.')[0] 207 | 208 | test( 209 | basename, 210 | py3, 211 | suite: 'auracle', 212 | args: ['-W', 'all', join_paths(meson.project_source_root(), input)], 213 | env: ['PYTHONDONTWRITEBYTECODE=1'], 214 | ) 215 | endforeach 216 | else 217 | message( 218 | 'Skipping integration tests, python @0@ not found'.format( 219 | python_requirement, 220 | ), 221 | ) 222 | endif 223 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('unittests', type : 'feature', value : 'auto', 2 | description : 'Include unit tests in the build. Depends on gtest and gmock.') 3 | -------------------------------------------------------------------------------- /src/aur/client.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AUR_CLIENT_HH_ 3 | #define AUR_CLIENT_HH_ 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "absl/functional/any_invocable.h" 10 | #include "aur/request.hh" 11 | #include "aur/response.hh" 12 | 13 | namespace aur { 14 | 15 | class Client { 16 | public: 17 | template 18 | using ResponseCallback = 19 | absl::AnyInvocable) &&>; 20 | 21 | using RpcResponseCallback = ResponseCallback; 22 | using RawResponseCallback = ResponseCallback; 23 | using CloneResponseCallback = ResponseCallback; 24 | 25 | struct Options { 26 | Options() = default; 27 | 28 | Options(const Options&) = default; 29 | Options& operator=(const Options&) = default; 30 | 31 | Options(Options&&) = default; 32 | Options& operator=(Options&&) = default; 33 | 34 | Options& set_baseurl(std::string baseurl) { 35 | this->baseurl = std::move(baseurl); 36 | return *this; 37 | } 38 | std::string baseurl; 39 | 40 | Options& set_useragent(std::string useragent) { 41 | this->useragent = std::move(useragent); 42 | return *this; 43 | } 44 | std::string useragent; 45 | }; 46 | 47 | static std::unique_ptr New(Client::Options options = {}); 48 | 49 | Client() = default; 50 | virtual ~Client() = default; 51 | 52 | Client(const Client&) = delete; 53 | Client& operator=(const Client&) = delete; 54 | 55 | Client(Client&&) = default; 56 | Client& operator=(Client&&) = default; 57 | 58 | // Asynchronously issue an RPC request using the REST API. The callback will 59 | // be invoked when the call completes. 60 | virtual void QueueRpcRequest(const RpcRequest& request, 61 | RpcResponseCallback callback) = 0; 62 | 63 | // Asynchronously issue a raw request. The callback will be invoked when the 64 | // call completes. 65 | virtual void QueueRawRequest(const HttpRequest& request, 66 | RawResponseCallback callback) = 0; 67 | 68 | // Clone a git repository. 69 | virtual void QueueCloneRequest(const CloneRequest& request, 70 | CloneResponseCallback callback) = 0; 71 | 72 | // Wait for all pending requests to complete. Returns non-zero if any request 73 | // failed or was cancelled by a callback. 74 | virtual int Wait() = 0; 75 | }; 76 | 77 | } // namespace aur 78 | 79 | #endif // AUR_AUR_HH_ 80 | -------------------------------------------------------------------------------- /src/aur/package.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AUR_PACKAGE_H 3 | #define AUR_PACKAGE_H 4 | 5 | #include 6 | #include 7 | 8 | #include "absl/time/time.h" 9 | 10 | namespace aur { 11 | 12 | struct Package { 13 | Package() = default; 14 | 15 | std::string name; 16 | std::string description; 17 | std::string submitter; 18 | std::string maintainer; 19 | std::string pkgbase; 20 | std::string upstream_url; 21 | std::string aur_urlpath; 22 | std::string version; 23 | 24 | int package_id = 0; 25 | int pkgbase_id = 0; 26 | int votes = 0; 27 | double popularity = 0.f; 28 | 29 | absl::Time out_of_date; 30 | absl::Time submitted; 31 | absl::Time modified; 32 | 33 | std::vector conflicts; 34 | std::vector groups; 35 | std::vector keywords; 36 | std::vector licenses; 37 | std::vector optdepends; 38 | std::vector provides; 39 | std::vector replaces; 40 | std::vector comaintainers; 41 | 42 | std::vector depends; 43 | std::vector makedepends; 44 | std::vector checkdepends; 45 | 46 | // glaze serialization helpers 47 | 48 | template 49 | void read_optional(const std::optional& v) { 50 | if (v) (*this).*F = *v; 51 | } 52 | 53 | template 54 | void read_time(std::optional seconds) { 55 | (*this).*F = absl::FromUnixSeconds(seconds.value_or(0)); 56 | } 57 | 58 | template 59 | int64_t write_time() { 60 | return absl::ToUnixSeconds((*this).*F); 61 | } 62 | }; 63 | 64 | inline bool operator==(const Package& a, const Package& b) { 65 | return a.package_id == b.package_id && a.pkgbase_id == b.pkgbase_id; 66 | } 67 | 68 | } // namespace aur 69 | 70 | #endif // AUR_PACKAGE_H 71 | -------------------------------------------------------------------------------- /src/aur/request.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "aur/request.hh" 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "absl/strings/str_cat.h" 11 | #include "absl/strings/str_join.h" 12 | 13 | namespace aur { 14 | 15 | namespace { 16 | 17 | std::string UrlEscape(const std::string_view sv) { 18 | char* ptr = curl_easy_escape(nullptr, sv.data(), sv.size()); 19 | std::string escaped(ptr); 20 | curl_free(ptr); 21 | 22 | return escaped; 23 | } 24 | 25 | void QueryParamFormatter(std::string* out, const HttpRequest::QueryParam& kv) { 26 | absl::StrAppend(out, kv.first, "=", UrlEscape(kv.second)); 27 | } 28 | 29 | } // namespace 30 | 31 | std::string RpcRequest::Url(std::string_view baseurl) const { 32 | return absl::StrCat(baseurl, endpoint_); 33 | } 34 | 35 | void RpcRequest::AddArg(std::string key, std::string value) { 36 | params_.push_back({std::move(key), std::move(value)}); 37 | } 38 | 39 | std::string RpcRequest::Payload() const { 40 | return absl::StrJoin(params_, "&", QueryParamFormatter); 41 | } 42 | 43 | // static 44 | RawRequest RawRequest::ForSourceFile(const Package& package, 45 | std::string_view filename) { 46 | return RawRequest(absl::StrCat("/cgit/aur.git/plain/", filename, 47 | "?h=", UrlEscape(package.pkgbase))); 48 | } 49 | 50 | std::string RawRequest::Url(std::string_view baseurl) const { 51 | return absl::StrCat(baseurl, urlpath_); 52 | } 53 | 54 | std::string CloneRequest::Url(std::string_view baseurl) const { 55 | return absl::StrCat(baseurl, "/", reponame_); 56 | } 57 | 58 | } // namespace aur 59 | -------------------------------------------------------------------------------- /src/aur/request.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AUR_REQUEST_HH_ 3 | #define AUR_REQUEST_HH_ 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "absl/strings/str_format.h" 10 | #include "aur/package.hh" 11 | 12 | namespace aur { 13 | 14 | // Abstract class describing a request for a resource. 15 | class Request { 16 | public: 17 | virtual ~Request() = default; 18 | 19 | virtual std::string Url(std::string_view baseurl) const = 0; 20 | }; 21 | 22 | class HttpRequest : public Request { 23 | public: 24 | enum class Command : int8_t { 25 | GET, 26 | POST, 27 | }; 28 | 29 | using QueryParam = std::pair; 30 | using QueryParams = std::vector; 31 | 32 | explicit HttpRequest(Command command) : command_(command) {} 33 | 34 | Command command() const { return command_; } 35 | 36 | virtual std::string Payload() const = 0; 37 | 38 | protected: 39 | Command command_; 40 | }; 41 | 42 | class RpcRequest : public HttpRequest { 43 | public: 44 | RpcRequest(Command command, std::string endpoint) 45 | : HttpRequest(command), endpoint_(std::move(endpoint)) {} 46 | 47 | std::string Url(std::string_view baseurl) const override; 48 | std::string Payload() const override; 49 | 50 | void AddArg(std::string key, std::string value); 51 | 52 | private: 53 | std::string endpoint_; 54 | QueryParams params_; 55 | }; 56 | 57 | // A class describing a GET request for an arbitrary URL on the AUR. 58 | class RawRequest : public HttpRequest { 59 | public: 60 | static RawRequest ForSourceFile(const Package& package, 61 | std::string_view filename); 62 | 63 | explicit RawRequest(std::string urlpath) 64 | : HttpRequest(HttpRequest::Command::GET), urlpath_(std::move(urlpath)) {} 65 | 66 | RawRequest(const RawRequest&) = delete; 67 | RawRequest& operator=(const RawRequest&) = delete; 68 | 69 | RawRequest(RawRequest&&) = default; 70 | RawRequest& operator=(RawRequest&&) = default; 71 | 72 | std::string Url(std::string_view baseurl) const override; 73 | std::string Payload() const override { return std::string(); } 74 | 75 | private: 76 | std::string urlpath_; 77 | }; 78 | 79 | // A class describing a url for a git repo hosted on the AUR. 80 | class CloneRequest : public Request { 81 | public: 82 | explicit CloneRequest(std::string reponame) 83 | : reponame_(std::move(reponame)) {} 84 | 85 | CloneRequest(const CloneRequest&) = delete; 86 | CloneRequest& operator=(const CloneRequest&) = delete; 87 | 88 | CloneRequest(CloneRequest&&) = default; 89 | CloneRequest& operator=(CloneRequest&&) = default; 90 | 91 | const std::string& reponame() const { return reponame_; } 92 | 93 | std::string Url(std::string_view baseurl) const override; 94 | 95 | private: 96 | std::string reponame_; 97 | }; 98 | 99 | class InfoRequest : public RpcRequest { 100 | public: 101 | explicit InfoRequest(const std::vector& args) : InfoRequest() { 102 | for (const auto& arg : args) { 103 | AddArg(arg); 104 | } 105 | } 106 | 107 | explicit InfoRequest(const std::vector& packages) : InfoRequest() { 108 | for (const auto& package : packages) { 109 | AddArg(package.name); 110 | } 111 | } 112 | 113 | InfoRequest(const InfoRequest&) = delete; 114 | InfoRequest& operator=(const InfoRequest&) = delete; 115 | 116 | InfoRequest(InfoRequest&&) = default; 117 | InfoRequest& operator=(InfoRequest&&) = default; 118 | 119 | InfoRequest() : RpcRequest(HttpRequest::Command::POST, "/rpc/v5/info") {} 120 | 121 | void AddArg(std::string arg) { RpcRequest::AddArg("arg[]", std::move(arg)); } 122 | }; 123 | 124 | class SearchRequest : public RpcRequest { 125 | public: 126 | enum class SearchBy { 127 | INVALID, 128 | NAME, 129 | NAME_DESC, 130 | MAINTAINER, 131 | DEPENDS, 132 | MAKEDEPENDS, 133 | OPTDEPENDS, 134 | CHECKDEPENDS, 135 | SUBMITTER, 136 | PROVIDES, 137 | CONFLICTS, 138 | REPLACES, 139 | KEYWORDS, 140 | GROUPS, 141 | COMAINTAINERS, 142 | }; 143 | 144 | static SearchBy ParseSearchBy(std::string_view searchby) { 145 | if (searchby == "name") { 146 | return SearchBy::NAME; 147 | } 148 | if (searchby == "name-desc") { 149 | return SearchBy::NAME_DESC; 150 | } 151 | if (searchby == "maintainer") { 152 | return SearchBy::MAINTAINER; 153 | } 154 | if (searchby == "depends") { 155 | return SearchBy::DEPENDS; 156 | } 157 | if (searchby == "makedepends") { 158 | return SearchBy::MAKEDEPENDS; 159 | } 160 | if (searchby == "optdepends") { 161 | return SearchBy::OPTDEPENDS; 162 | } 163 | if (searchby == "checkdepends") { 164 | return SearchBy::CHECKDEPENDS; 165 | } 166 | if (searchby == "submitter") { 167 | return SearchBy::SUBMITTER; 168 | } 169 | if (searchby == "provides") { 170 | return SearchBy::PROVIDES; 171 | } 172 | if (searchby == "conflicts") { 173 | return SearchBy::CONFLICTS; 174 | } 175 | if (searchby == "replaces") { 176 | return SearchBy::REPLACES; 177 | } 178 | if (searchby == "keywords") { 179 | return SearchBy::KEYWORDS; 180 | } 181 | if (searchby == "groups") { 182 | return SearchBy::GROUPS; 183 | } 184 | if (searchby == "comaintainers") { 185 | return SearchBy::COMAINTAINERS; 186 | } 187 | return SearchBy::INVALID; 188 | } 189 | 190 | SearchRequest(SearchBy by, std::string_view arg) 191 | : RpcRequest(HttpRequest::Command::GET, 192 | absl::StrFormat("/rpc/v5/search/%s?by=%s", arg, 193 | SearchByToString(by))) {} 194 | 195 | SearchRequest(const SearchRequest&) = delete; 196 | SearchRequest& operator=(const SearchRequest&) = delete; 197 | 198 | SearchRequest(SearchRequest&&) = default; 199 | SearchRequest& operator=(SearchRequest&&) = default; 200 | 201 | private: 202 | std::string SearchByToString(SearchBy by) { 203 | switch (by) { 204 | case SearchBy::NAME: 205 | return "name"; 206 | case SearchBy::NAME_DESC: 207 | return "name-desc"; 208 | case SearchBy::MAINTAINER: 209 | return "maintainer"; 210 | case SearchBy::DEPENDS: 211 | return "depends"; 212 | case SearchBy::MAKEDEPENDS: 213 | return "makedepends"; 214 | case SearchBy::OPTDEPENDS: 215 | return "optdepends"; 216 | case SearchBy::CHECKDEPENDS: 217 | return "checkdepends"; 218 | case SearchBy::SUBMITTER: 219 | return "submitter"; 220 | case SearchBy::PROVIDES: 221 | return "provides"; 222 | case SearchBy::CONFLICTS: 223 | return "conflicts"; 224 | case SearchBy::REPLACES: 225 | return "replaces"; 226 | case SearchBy::KEYWORDS: 227 | return "keywords"; 228 | case SearchBy::GROUPS: 229 | return "groups"; 230 | case SearchBy::COMAINTAINERS: 231 | return "comaintainers"; 232 | default: 233 | return ""; 234 | } 235 | } 236 | }; 237 | 238 | } // namespace aur 239 | 240 | #endif // AUR_REQUEST_HH_ 241 | -------------------------------------------------------------------------------- /src/aur/request_test.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "aur/request.hh" 3 | 4 | #include "gmock/gmock.h" 5 | #include "gtest/gtest.h" 6 | 7 | constexpr char kBaseUrl[] = "http://aur.archlinux.org"; 8 | 9 | using testing::AllOf; 10 | using testing::EndsWith; 11 | using testing::HasSubstr; 12 | using testing::Not; 13 | using testing::StartsWith; 14 | 15 | TEST(RequestTest, BuildsInfoRequests) { 16 | aur::InfoRequest request; 17 | 18 | request.AddArg("derp"); 19 | 20 | const auto url = request.Url(kBaseUrl); 21 | EXPECT_THAT(url, AllOf(EndsWith("/rpc/v5/info"))); 22 | 23 | const auto payload = request.Payload(); 24 | EXPECT_EQ(payload, "arg[]=derp"); 25 | } 26 | 27 | TEST(RequestTest, UrlEncodesParameterValues) { 28 | aur::InfoRequest request; 29 | 30 | request.AddArg("c++"); 31 | 32 | const auto payload = request.Payload(); 33 | EXPECT_EQ(payload, "arg[]=c%2B%2B"); 34 | } 35 | 36 | TEST(RequestTest, BuildsSearchRequests) { 37 | aur::SearchRequest request(aur::SearchRequest::SearchBy::MAINTAINER, "foo"); 38 | 39 | const std::string url = request.Url(kBaseUrl); 40 | 41 | EXPECT_THAT(url, 42 | AllOf(testing::EndsWith("/rpc/v5/search/foo?by=maintainer"))); 43 | } 44 | 45 | TEST(RequestTest, BuildsRawRequests) { 46 | aur::RawRequest request("/foo/bar/baz"); 47 | 48 | const auto url = request.Url(kBaseUrl); 49 | 50 | EXPECT_EQ(url, std::string(kBaseUrl) + "/foo/bar/baz"); 51 | } 52 | 53 | TEST(RequestTest, UrlForSourceFileEscapesReponame) { 54 | aur::Package p; 55 | p.pkgbase = "libc++"; 56 | auto request = aur::RawRequest::ForSourceFile(p, "PKGBUILD"); 57 | 58 | auto url = request.Url(kBaseUrl); 59 | 60 | EXPECT_THAT(url, EndsWith("/PKGBUILD?h=libc%2B%2B")); 61 | } 62 | 63 | TEST(RequestTest, BuildsCloneRequests) { 64 | const std::string kReponame = "auracle-git"; 65 | 66 | aur::CloneRequest request(kReponame); 67 | 68 | ASSERT_EQ(request.reponame(), kReponame); 69 | 70 | const auto url = request.Url(kBaseUrl); 71 | 72 | EXPECT_EQ(url, std::string(kBaseUrl) + "/" + kReponame); 73 | } 74 | -------------------------------------------------------------------------------- /src/aur/response.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "aur/response.hh" 3 | 4 | #include "glaze/glaze.hpp" 5 | 6 | template <> 7 | struct glz::meta { 8 | using T = aur::Package; 9 | static constexpr std::string_view name = "Package"; 10 | 11 | template 12 | static constexpr auto optional_string = 13 | custom<&T::read_optional, F>; 14 | 15 | template 16 | static constexpr auto absl_time = custom<&T::read_time, &T::write_time>; 17 | 18 | static constexpr auto value = object( // 19 | "CheckDepends", &T::checkdepends, // 20 | "CoMaintainers", &T::comaintainers, // 21 | "Conflicts", &T::conflicts, // 22 | "Depends", &T::depends, // 23 | "Description", optional_string<&T::description>, // 24 | "FirstSubmitted", absl_time<&T::submitted>, // 25 | "Groups", &T::groups, // 26 | "ID", &T::package_id, // 27 | "Keywords", &T::keywords, // 28 | "LastModified", absl_time<&T::modified>, // 29 | "License", &T::licenses, // 30 | "Maintainer", optional_string<&T::maintainer>, // 31 | "MakeDepends", &T::makedepends, // 32 | "Name", &T::name, // 33 | "NumVotes", &T::votes, // 34 | "OptDepends", &T::optdepends, // 35 | "OutOfDate", absl_time<&T::out_of_date>, // 36 | "PackageBase", &T::pkgbase, // 37 | "PackageBaseID", &T::pkgbase_id, // 38 | "Popularity", &T::popularity, // 39 | "Provides", &T::provides, // 40 | "Replaces", &T::replaces, // 41 | "Submitter", &T::submitter, // 42 | "URL", optional_string<&T::upstream_url>, // 43 | "URLPath", &T::aur_urlpath, // 44 | "Version", &T::version); 45 | }; 46 | 47 | namespace aur { 48 | 49 | struct Raw { 50 | std::vector packages; 51 | std::string error; 52 | 53 | struct glaze { 54 | static constexpr std::string_view name = "Raw"; 55 | static constexpr auto value = glz::object( // 56 | "results", &Raw::packages, // 57 | "error", &Raw::error); 58 | }; 59 | }; 60 | 61 | absl::StatusOr RpcResponse::Parse(std::string_view bytes) { 62 | static constexpr glz::opts opts{ 63 | .error_on_unknown_keys = false, 64 | .error_on_missing_keys = false, 65 | }; 66 | 67 | Raw raw; 68 | const auto ec = glz::read(raw, bytes, glz::context{}); 69 | if (ec) { 70 | return absl::InvalidArgumentError("parse error: " + 71 | glz::format_error(ec, bytes)); 72 | } else if (!raw.error.empty()) { 73 | return absl::UnknownError(raw.error); 74 | } 75 | 76 | return RpcResponse(std::move(raw.packages)); 77 | } 78 | 79 | } // namespace aur 80 | -------------------------------------------------------------------------------- /src/aur/response.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AUR_RESPONSE_HH_ 3 | #define AUR_RESPONSE_HH_ 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "absl/status/statusor.h" 10 | #include "aur/package.hh" 11 | 12 | namespace aur { 13 | 14 | struct CloneResponse { 15 | static absl::StatusOr Parse(std::string_view operation) { 16 | return CloneResponse(operation); 17 | } 18 | 19 | explicit CloneResponse(std::string_view operation) : operation(operation) {} 20 | 21 | CloneResponse(const CloneResponse&) = delete; 22 | CloneResponse& operator=(const CloneResponse&) = delete; 23 | 24 | CloneResponse(CloneResponse&&) = default; 25 | CloneResponse& operator=(CloneResponse&&) = default; 26 | 27 | std::string_view operation; 28 | }; 29 | 30 | struct RpcResponse { 31 | static absl::StatusOr Parse(std::string_view bytes); 32 | 33 | RpcResponse() = default; 34 | RpcResponse(std::vector packages) : packages(std::move(packages)) {} 35 | 36 | RpcResponse(const RpcResponse&) = default; 37 | RpcResponse& operator=(const RpcResponse&) = default; 38 | 39 | RpcResponse(RpcResponse&&) = default; 40 | RpcResponse& operator=(RpcResponse&&) = default; 41 | 42 | std::vector packages; 43 | }; 44 | 45 | struct RawResponse { 46 | static absl::StatusOr Parse(std::string bytes) { 47 | return RawResponse(std::move(bytes)); 48 | } 49 | 50 | RawResponse(std::string bytes) : bytes(std::move(bytes)) {} 51 | 52 | RawResponse(const RawResponse&) = delete; 53 | RawResponse& operator=(const RawResponse&) = delete; 54 | 55 | RawResponse(RawResponse&&) = default; 56 | RawResponse& operator=(RawResponse&&) = default; 57 | 58 | std::string bytes; 59 | }; 60 | 61 | } // namespace aur 62 | 63 | #if 0 64 | template <> 65 | struct glz::meta { 66 | using T = aur::RpcResponse; 67 | static constexpr std::string_view name = "RpcResponse"; 68 | 69 | static constexpr auto value = object( // 70 | "results", &T::packages, // 71 | "resultcount", glz::skip{}, // 72 | "version", glz::skip{}, // 73 | "type", glz::skip{}); 74 | }; 75 | #endif 76 | 77 | #endif // AUR_RESPONSE_HH_ 78 | -------------------------------------------------------------------------------- /src/aur/response_test.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "aur/response.hh" 3 | 4 | #include "gmock/gmock.h" 5 | #include "gtest/gtest.h" 6 | 7 | using aur::RpcResponse; 8 | using testing::Field; 9 | using testing::UnorderedElementsAre; 10 | 11 | TEST(ResponseTest, ParsesSuccessResponse) { 12 | const auto response = RpcResponse::Parse(R"({ 13 | "version": 5, 14 | "type": "multiinfo", 15 | "resultcount": 1, 16 | "results": [ 17 | { 18 | "ID": 534056, 19 | "Name": "auracle-git", 20 | "PackageBaseID": 123768, 21 | "PackageBase": "auracle-git", 22 | "Version": "r36.752e4ba-1", 23 | "Description": "A flexible client for the AUR", 24 | "URL": "https://github.com/falconindy/auracle.git", 25 | "NumVotes": 15, 26 | "Popularity": 0.095498, 27 | "OutOfDate": null, 28 | "Maintainer": "falconindy", 29 | "FirstSubmitted": 1499013608, 30 | "LastModified": 1534000474, 31 | "URLPath": "/cgit/aur.git/snapshot/auracle-git.tar.gz", 32 | "Depends": [ 33 | "pacman", 34 | "libarchive.so", 35 | "libcurl.so" 36 | ], 37 | "Groups": [ 38 | "whydoestheaurhavegroups" 39 | ], 40 | "CheckDepends": [ 41 | "python" 42 | ], 43 | "MakeDepends": [ 44 | "meson", 45 | "git", 46 | "nlohmann-json" 47 | ], 48 | "Conflicts": [ 49 | "auracle" 50 | ], 51 | "Provides": [ 52 | "auracle" 53 | ], 54 | "License": [ 55 | "MIT" 56 | ], 57 | "Replaces": [ 58 | "cower-git", 59 | "cower" 60 | ], 61 | "OptDepends": [ 62 | "awesomeness" 63 | ], 64 | "Keywords": [ 65 | "aur" 66 | ] 67 | } 68 | ] 69 | })"); 70 | 71 | ASSERT_TRUE(response.ok()) << response.status(); 72 | ASSERT_EQ(response->packages.size(), 1); 73 | 74 | const auto& result = response->packages[0]; 75 | EXPECT_EQ(result.package_id, 534056); 76 | EXPECT_EQ(result.name, "auracle-git"); 77 | EXPECT_EQ(result.pkgbase_id, 123768); 78 | EXPECT_EQ(result.version, "r36.752e4ba-1"); 79 | EXPECT_EQ(result.description, "A flexible client for the AUR"); 80 | EXPECT_EQ(result.upstream_url, "https://github.com/falconindy/auracle.git"); 81 | EXPECT_EQ(result.votes, 15); 82 | EXPECT_EQ(result.popularity, 0.095498); 83 | EXPECT_EQ(result.out_of_date, absl::UnixEpoch()); 84 | EXPECT_EQ(result.submitted, absl::FromUnixSeconds(1499013608)); 85 | EXPECT_EQ(result.modified, absl::FromUnixSeconds(1534000474)); 86 | EXPECT_EQ(result.maintainer, "falconindy"); 87 | EXPECT_EQ(result.aur_urlpath, "/cgit/aur.git/snapshot/auracle-git.tar.gz"); 88 | EXPECT_THAT(result.depends, 89 | UnorderedElementsAre("pacman", "libarchive.so", "libcurl.so")); 90 | EXPECT_THAT(result.makedepends, 91 | UnorderedElementsAre("meson", "git", "nlohmann-json")); 92 | EXPECT_THAT(result.checkdepends, UnorderedElementsAre("python")); 93 | EXPECT_THAT(result.optdepends, UnorderedElementsAre("awesomeness")); 94 | EXPECT_THAT(result.conflicts, UnorderedElementsAre("auracle")); 95 | EXPECT_THAT(result.replaces, UnorderedElementsAre("cower", "cower-git")); 96 | EXPECT_THAT(result.provides, UnorderedElementsAre("auracle")); 97 | EXPECT_THAT(result.licenses, UnorderedElementsAre("MIT")); 98 | EXPECT_THAT(result.keywords, UnorderedElementsAre("aur")); 99 | EXPECT_THAT(result.groups, UnorderedElementsAre("whydoestheaurhavegroups")); 100 | } 101 | 102 | TEST(ResponseTest, ParsesErrorResponse) { 103 | const auto response = RpcResponse::Parse(R"({ 104 | "version": 5, 105 | "type": "error", 106 | "resultcount": 0, 107 | "results": [], 108 | "error": "something" 109 | })"); 110 | 111 | EXPECT_FALSE(response.ok()); 112 | EXPECT_EQ(response.status().message(), "something"); 113 | } 114 | 115 | TEST(ResponseTest, GracefullyHandlesInvalidJson) { 116 | const auto response = RpcResponse::Parse(R"({ 117 | "version": 5, 118 | "type": "multiinfo, 119 | "resultcount": 0, 120 | "results": [], 121 | "error": "something" 122 | })"); 123 | 124 | ASSERT_THAT(response.status().message(), testing::HasSubstr("parse error")); 125 | } 126 | -------------------------------------------------------------------------------- /src/auracle/auracle.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AURACLE_AURACLE_HH_ 3 | #define AURACLE_AURACLE_HH_ 4 | 5 | #include 6 | #include 7 | 8 | #include "absl/container/btree_set.h" 9 | #include "aur/client.hh" 10 | #include "aur/request.hh" 11 | #include "auracle/dependency.hh" 12 | #include "auracle/dependency_kind.hh" 13 | #include "auracle/package_cache.hh" 14 | #include "auracle/pacman.hh" 15 | #include "auracle/sort.hh" 16 | 17 | namespace auracle { 18 | 19 | class Auracle { 20 | public: 21 | struct Options { 22 | Options& set_aur_baseurl(std::string aur_baseurl) { 23 | this->aur_baseurl = std::move(aur_baseurl); 24 | return *this; 25 | } 26 | 27 | Options& set_pacman(Pacman* pacman) { 28 | this->pacman = pacman; 29 | return *this; 30 | } 31 | 32 | Options& set_quiet(bool quiet) { 33 | this->quiet = quiet; 34 | return *this; 35 | } 36 | 37 | std::string aur_baseurl; 38 | Pacman* pacman = nullptr; 39 | bool quiet = false; 40 | }; 41 | 42 | explicit Auracle(Options options); 43 | 44 | ~Auracle() = default; 45 | 46 | Auracle(const Auracle&) = delete; 47 | Auracle& operator=(const Auracle&) = delete; 48 | 49 | Auracle(Auracle&&) = default; 50 | Auracle& operator=(Auracle&&) = default; 51 | 52 | struct CommandOptions { 53 | aur::SearchRequest::SearchBy search_by = 54 | aur::SearchRequest::SearchBy::NAME_DESC; 55 | std::string directory; 56 | bool recurse = false; 57 | bool allow_regex = true; 58 | bool quiet = false; 59 | std::string show_file = "PKGBUILD"; 60 | sort::Sorter sorter = 61 | sort::MakePackageSorter("name", sort::OrderBy::ORDER_ASC); 62 | std::string format; 63 | absl::btree_set resolve_depends = { 64 | DependencyKind::Depend, DependencyKind::CheckDepend, 65 | DependencyKind::MakeDepend}; 66 | }; 67 | 68 | int BuildOrder(const std::vector& args, 69 | const CommandOptions& options); 70 | int Clone(const std::vector& args, 71 | const CommandOptions& options); 72 | int Info(const std::vector& args, const CommandOptions& options); 73 | int Resolve(const std::vector& arg, 74 | const CommandOptions& options); 75 | int Show(const std::vector& args, const CommandOptions& options); 76 | int RawInfo(const std::vector& args, 77 | const CommandOptions& options); 78 | int RawSearch(const std::vector& args, 79 | const CommandOptions& options); 80 | int Search(const std::vector& args, 81 | const CommandOptions& options); 82 | int Outdated(const std::vector& args, 83 | const CommandOptions& options); 84 | int Update(const std::vector& args, 85 | const CommandOptions& options); 86 | 87 | private: 88 | struct PackageIterator { 89 | using PackageCallback = std::function; 90 | 91 | PackageIterator(bool recurse, 92 | absl::btree_set resolve_depends, 93 | PackageCallback callback) 94 | : recurse(recurse), 95 | resolve_depends(resolve_depends), 96 | callback(std::move(callback)) {} 97 | 98 | bool recurse; 99 | absl::btree_set resolve_depends; 100 | 101 | const PackageCallback callback; 102 | PackageCache package_cache; 103 | }; 104 | 105 | void ResolveMany(const std::vector& depstrings, 106 | aur::Client::RpcResponseCallback callback); 107 | 108 | int GetOutdatedPackages(const std::vector& args, 109 | std::vector* packages); 110 | 111 | void IteratePackages(std::vector args, PackageIterator* state); 112 | 113 | std::unique_ptr client_; 114 | Pacman* pacman_; 115 | }; 116 | 117 | } // namespace auracle 118 | 119 | #endif // AURACLE_AURACLE_HH_ 120 | -------------------------------------------------------------------------------- /src/auracle/dependency.cc: -------------------------------------------------------------------------------- 1 | #include "auracle/dependency.hh" 2 | 3 | #include 4 | 5 | #include "absl/algorithm/container.h" 6 | 7 | namespace auracle { 8 | 9 | namespace { 10 | 11 | int Vercmp(const std::string& a, const std::string& b) { 12 | return alpm_pkg_vercmp(a.c_str(), b.c_str()); 13 | } 14 | 15 | } // namespace 16 | 17 | Dependency::Dependency(std::string_view depstring) : depstring_(depstring) { 18 | name_ = depstring; 19 | if (auto pos = depstring.find("<="); pos != depstring.npos) { 20 | mod_ = Mod::LE; 21 | name_ = depstring.substr(0, pos); 22 | version_ = depstring.substr(pos + 2); 23 | } else if (auto pos = depstring.find(">="); pos != depstring.npos) { 24 | mod_ = Mod::GE; 25 | name_ = depstring.substr(0, pos); 26 | version_ = depstring.substr(pos + 2); 27 | } else if (auto pos = depstring.find_first_of("<>="); pos != depstring.npos) { 28 | switch (depstring[pos]) { 29 | case '<': 30 | mod_ = Mod::LT; 31 | break; 32 | case '>': 33 | mod_ = Mod::GT; 34 | break; 35 | case '=': 36 | mod_ = Mod::EQ; 37 | break; 38 | } 39 | 40 | name_ = depstring.substr(0, pos); 41 | version_ = depstring.substr(pos + 1); 42 | } else { 43 | name_ = depstring; 44 | } 45 | } 46 | 47 | bool Dependency::SatisfiedByVersion(const std::string& version) const { 48 | const int vercmp = Vercmp(version, version_); 49 | switch (mod_) { 50 | case Mod::EQ: 51 | return vercmp == 0; 52 | case Mod::GE: 53 | return vercmp >= 0; 54 | case Mod::GT: 55 | return vercmp > 0; 56 | case Mod::LE: 57 | return vercmp <= 0; 58 | case Mod::LT: 59 | return vercmp < 0; 60 | default: 61 | break; 62 | } 63 | 64 | return false; 65 | } 66 | 67 | bool Dependency::SatisfiedBy(const aur::Package& candidate) const { 68 | if (version_.empty()) { 69 | // exact match on package name 70 | if (name_ == candidate.name) { 71 | return true; 72 | } 73 | 74 | // Satisfied via provides without version comparison. 75 | for (const auto& depstring : candidate.provides) { 76 | if (name_ == Dependency(depstring).name_) { 77 | return true; 78 | } 79 | } 80 | } else { // !version_.empty() 81 | // Exact match on package name and satisfied version 82 | if (name_ == candidate.name && SatisfiedByVersion(candidate.version)) { 83 | return true; 84 | } 85 | 86 | // Satisfied via provides with version comparison. 87 | for (const auto& depstring : candidate.provides) { 88 | Dependency provide(depstring); 89 | 90 | // An unversioned or malformed provide can't satisfy a versioned 91 | // dependency. 92 | if (provide.mod_ != Mod::EQ) { 93 | continue; 94 | } 95 | 96 | // Names must match. 97 | if (name_ != provide.name_) { 98 | continue; 99 | } 100 | 101 | // Compare versions. 102 | if (SatisfiedByVersion(provide.version_)) { 103 | return true; 104 | } 105 | } 106 | } 107 | 108 | return false; 109 | } 110 | 111 | } // namespace auracle 112 | -------------------------------------------------------------------------------- /src/auracle/dependency.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AURACLE_DEPENDENCY_HH_ 3 | #define AURACLE_DEPENDENCY_HH_ 4 | 5 | #include 6 | #include 7 | 8 | #include "aur/package.hh" 9 | 10 | namespace auracle { 11 | 12 | // Dependency provides a simple interface around dependency resolution. 13 | class Dependency final { 14 | public: 15 | // Constructs a Dependency described by the given |depstring|. A 16 | // depstring follows the same format as that described by libalpm. 17 | explicit Dependency(std::string_view depstring); 18 | 19 | Dependency(Dependency&&) = default; 20 | Dependency& operator=(Dependency&&) = default; 21 | 22 | Dependency(const Dependency&) = default; 23 | Dependency& operator=(const Dependency&) = default; 24 | 25 | const std::string& name() const { return name_; } 26 | 27 | bool is_versioned() const { return !version_.empty(); } 28 | 29 | // Returns true if the given candidate package satisifes the dependency 30 | // requirement. A dependency is satisfied if: 31 | // a) The given |candidate| directly supplies the necessary name and possibly 32 | // version. 33 | // b) The given |candidate| offers a `provide' that can satisfy the given 34 | // dependency. Unlike situation 'a', an unversioned provide can never 35 | // satisfy a versioned dependency. 36 | bool SatisfiedBy(const aur::Package& candidate) const; 37 | 38 | private: 39 | enum class Mod { 40 | ANY, 41 | EQ, 42 | GE, 43 | GT, 44 | LE, 45 | LT, 46 | }; 47 | 48 | bool SatisfiedByVersion(const std::string& version) const; 49 | 50 | std::string depstring_; 51 | std::string name_; 52 | std::string version_; 53 | Mod mod_; 54 | }; 55 | 56 | } // namespace auracle 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /src/auracle/dependency_kind.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "dependency_kind.hh" 3 | 4 | #include 5 | 6 | #include "absl/strings/str_split.h" 7 | 8 | namespace auracle { 9 | 10 | bool ParseDependencyKinds(std::string_view input, 11 | absl::btree_set* kinds) { 12 | if (input.empty()) { 13 | return true; 14 | } 15 | 16 | enum : int8_t { 17 | MODE_OVERWRITE, 18 | MODE_REMOVE, 19 | MODE_APPEND, 20 | } mode = MODE_OVERWRITE; 21 | 22 | switch (input[0]) { 23 | case '^': 24 | case '!': 25 | input.remove_prefix(1); 26 | mode = MODE_REMOVE; 27 | break; 28 | case '+': 29 | input.remove_prefix(1); 30 | mode = MODE_APPEND; 31 | break; 32 | } 33 | 34 | absl::btree_set parsed_kinds; 35 | for (const auto kind : absl::StrSplit(input, ',')) { 36 | if (kind == "depends") { 37 | parsed_kinds.insert(DependencyKind::Depend); 38 | } else if (kind == "checkdepends") { 39 | parsed_kinds.insert(DependencyKind::CheckDepend); 40 | } else if (kind == "makedepends") { 41 | parsed_kinds.insert(DependencyKind::MakeDepend); 42 | } else { 43 | return false; 44 | } 45 | } 46 | 47 | switch (mode) { 48 | case MODE_OVERWRITE: 49 | *kinds = std::move(parsed_kinds); 50 | break; 51 | case MODE_REMOVE: 52 | for (auto kind : parsed_kinds) { 53 | kinds->erase(kind); 54 | } 55 | break; 56 | case MODE_APPEND: 57 | kinds->merge(parsed_kinds); 58 | break; 59 | } 60 | 61 | return true; 62 | } 63 | 64 | const std::vector& GetDependenciesByKind( 65 | const aur::Package* package, DependencyKind kind) { 66 | switch (kind) { 67 | case DependencyKind::Depend: 68 | return package->depends; 69 | case DependencyKind::MakeDepend: 70 | return package->makedepends; 71 | case DependencyKind::CheckDepend: 72 | return package->checkdepends; 73 | } 74 | 75 | throw std::logic_error("unhandled DependencyKind"); 76 | } 77 | 78 | } // namespace auracle 79 | -------------------------------------------------------------------------------- /src/auracle/dependency_kind.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AURACLE_DEPENDENCY_KIND_HH_ 3 | #define AURACLE_DEPENDENCY_KIND_HH_ 4 | 5 | #include 6 | #include 7 | 8 | #include "absl/container/btree_set.h" 9 | #include "aur/package.hh" 10 | 11 | namespace auracle { 12 | 13 | enum class DependencyKind : int { 14 | Depend, 15 | MakeDepend, 16 | CheckDepend, 17 | }; 18 | 19 | bool ParseDependencyKinds(std::string_view input, 20 | absl::btree_set* dependency_kinds); 21 | 22 | const std::vector& GetDependenciesByKind( 23 | const aur::Package* package, DependencyKind kind); 24 | 25 | } // namespace auracle 26 | 27 | #endif // AURACLE_DEPENDENCY_KIND_HH_ 28 | -------------------------------------------------------------------------------- /src/auracle/dependency_kind_test.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "auracle/auracle.hh" 3 | #include "gmock/gmock.h" 4 | #include "gtest/gtest.h" 5 | 6 | namespace auracle { 7 | 8 | void PrintTo(DependencyKind kind, std::ostream* os) { 9 | switch (kind) { 10 | case DependencyKind::Depend: 11 | *os << "Depend"; 12 | break; 13 | case DependencyKind::MakeDepend: 14 | *os << "MakeDepend"; 15 | break; 16 | case DependencyKind::CheckDepend: 17 | *os << "CheckDepend"; 18 | break; 19 | } 20 | } 21 | 22 | } // namespace auracle 23 | 24 | namespace { 25 | 26 | TEST(AuracleTest, ParseDependencyKinds) { 27 | using DK = auracle::DependencyKind; 28 | 29 | { 30 | // Overwrite 31 | absl::btree_set kinds; 32 | EXPECT_TRUE(ParseDependencyKinds("depends", &kinds)); 33 | EXPECT_THAT(kinds, testing::UnorderedElementsAre(DK::Depend)); 34 | EXPECT_TRUE( 35 | ParseDependencyKinds("depends,checkdepends,makedepends", &kinds)); 36 | EXPECT_THAT(kinds, testing::UnorderedElementsAre( 37 | DK::Depend, DK::CheckDepend, DK::MakeDepend)); 38 | } 39 | 40 | { 41 | // Remove 42 | absl::btree_set kinds = {DK::Depend, DK::MakeDepend, DK::CheckDepend}; 43 | EXPECT_TRUE(ParseDependencyKinds("^checkdepends", &kinds)); 44 | EXPECT_THAT(kinds, 45 | testing::UnorderedElementsAre(DK::Depend, DK::MakeDepend)); 46 | EXPECT_TRUE(ParseDependencyKinds("!depends,makedepends", &kinds)); 47 | EXPECT_THAT(kinds, testing::IsEmpty()); 48 | } 49 | 50 | { 51 | // Append 52 | absl::btree_set kinds; 53 | EXPECT_TRUE(ParseDependencyKinds("+depends", &kinds)); 54 | EXPECT_THAT(kinds, testing::UnorderedElementsAre(DK::Depend)); 55 | EXPECT_TRUE(ParseDependencyKinds("+makedepends,checkdepends", &kinds)); 56 | EXPECT_THAT(kinds, testing::UnorderedElementsAre(DK::Depend, DK::MakeDepend, 57 | DK::CheckDepend)); 58 | } 59 | 60 | { 61 | // Bad spelling 62 | absl::btree_set kinds = {DK::Depend}; 63 | EXPECT_FALSE(ParseDependencyKinds("derpends", &kinds)); 64 | EXPECT_THAT(kinds, testing::UnorderedElementsAre(DK::Depend)); 65 | } 66 | 67 | { 68 | // Negation in the middle of a string isn't allowed 69 | absl::btree_set kinds = {DK::Depend}; 70 | EXPECT_FALSE(ParseDependencyKinds("depends,!makedepends", &kinds)); 71 | EXPECT_THAT(kinds, testing::UnorderedElementsAre(DK::Depend)); 72 | } 73 | 74 | { 75 | // Bad second element still leaves out param untouched. 76 | absl::btree_set kinds = {DK::Depend}; 77 | EXPECT_FALSE(ParseDependencyKinds("depends,!makdepends", &kinds)); 78 | EXPECT_THAT(kinds, testing::UnorderedElementsAre(DK::Depend)); 79 | } 80 | 81 | { 82 | // Edge case of only a valid prefix 83 | absl::btree_set kinds; 84 | EXPECT_FALSE(ParseDependencyKinds("+", &kinds)); 85 | EXPECT_FALSE(ParseDependencyKinds("!", &kinds)); 86 | } 87 | } 88 | 89 | } // namespace 90 | -------------------------------------------------------------------------------- /src/auracle/dependency_test.cc: -------------------------------------------------------------------------------- 1 | #include "auracle/dependency.hh" 2 | 3 | #include "gtest/gtest.h" 4 | 5 | using aur::Package; 6 | using auracle::Dependency; 7 | 8 | namespace { 9 | 10 | TEST(DependencyTest, UnversionedRequirement) { 11 | Package foo; 12 | foo.name = "foo"; 13 | foo.version = "1.0.0"; 14 | 15 | Package bar; 16 | bar.name = "bar"; 17 | bar.version = "1.0.0"; 18 | 19 | Dependency dep("foo"); 20 | EXPECT_TRUE(dep.SatisfiedBy(foo)); 21 | EXPECT_FALSE(dep.SatisfiedBy(bar)); 22 | } 23 | 24 | TEST(DependencyTest, VersionedRequirement) { 25 | Package foo_0_9_9; 26 | foo_0_9_9.name = "foo"; 27 | foo_0_9_9.version = "0.9.9"; 28 | 29 | Package foo_1_0_0; 30 | foo_1_0_0.name = "foo"; 31 | foo_1_0_0.version = "1.0.0"; 32 | 33 | Package foo_1_1_0; 34 | foo_1_1_0.name = "foo"; 35 | foo_1_1_0.version = "1.1.0"; 36 | 37 | Package bar_1_0_0; 38 | bar_1_0_0.name = "bar"; 39 | bar_1_0_0.version = "1.0.0"; 40 | 41 | { 42 | Dependency dep("foo=1.0.0"); 43 | EXPECT_TRUE(dep.SatisfiedBy(foo_1_0_0)); 44 | EXPECT_FALSE(dep.SatisfiedBy(foo_1_1_0)); 45 | } 46 | 47 | { 48 | Dependency dep("foo>=1.0.0"); 49 | EXPECT_FALSE(dep.SatisfiedBy(foo_0_9_9)); 50 | EXPECT_TRUE(dep.SatisfiedBy(foo_1_0_0)); 51 | EXPECT_TRUE(dep.SatisfiedBy(foo_1_1_0)); 52 | } 53 | 54 | { 55 | Dependency dep("foo>1.0.0"); 56 | EXPECT_FALSE(dep.SatisfiedBy(foo_0_9_9)); 57 | EXPECT_FALSE(dep.SatisfiedBy(foo_1_0_0)); 58 | EXPECT_TRUE(dep.SatisfiedBy(foo_1_1_0)); 59 | } 60 | 61 | { 62 | Dependency dep("foo<=1.0.0"); 63 | EXPECT_TRUE(dep.SatisfiedBy(foo_0_9_9)); 64 | EXPECT_TRUE(dep.SatisfiedBy(foo_1_0_0)); 65 | EXPECT_FALSE(dep.SatisfiedBy(foo_1_1_0)); 66 | } 67 | 68 | { 69 | Dependency dep("foo<1.0.0"); 70 | EXPECT_TRUE(dep.SatisfiedBy(foo_0_9_9)); 71 | EXPECT_FALSE(dep.SatisfiedBy(foo_1_0_0)); 72 | EXPECT_FALSE(dep.SatisfiedBy(foo_1_1_0)); 73 | } 74 | 75 | { 76 | Dependency dep("foo=1.0.0"); 77 | EXPECT_FALSE(dep.SatisfiedBy(bar_1_0_0)); 78 | } 79 | } 80 | 81 | TEST(DependencyTest, UnversionedRequirementByProvision) { 82 | Package bar; 83 | bar.name = "bar"; 84 | bar.version = "9.9.9"; 85 | bar.provides.push_back("quux"); 86 | bar.provides.push_back("foo"); 87 | 88 | Package bar_2; 89 | bar_2.name = "bar"; 90 | bar_2.version = "9.9.9"; 91 | bar_2.provides.push_back("quux"); 92 | bar_2.provides.push_back("foo=42"); 93 | 94 | Dependency dep("foo"); 95 | EXPECT_TRUE(dep.SatisfiedBy(bar)); 96 | EXPECT_TRUE(dep.SatisfiedBy(bar_2)); 97 | } 98 | 99 | TEST(DependencyTest, VersionedRequirementByProvision) { 100 | Package bar_0_9_9; 101 | bar_0_9_9.name = "bar"; 102 | bar_0_9_9.version = "9.9.9"; 103 | bar_0_9_9.provides.push_back("quux"); 104 | bar_0_9_9.provides.push_back("foo=0.9.9"); 105 | 106 | Package bar_1_0_0; 107 | bar_1_0_0.name = "bar"; 108 | bar_1_0_0.version = "9.9.9"; 109 | bar_0_9_9.provides.push_back("quux"); 110 | bar_1_0_0.provides.push_back("foo=1.0.0"); 111 | 112 | Package bar_1_1_0; 113 | bar_1_1_0.name = "bar"; 114 | bar_1_1_0.version = "9.9.9"; 115 | bar_0_9_9.provides.push_back("quux"); 116 | bar_1_1_0.provides.push_back("foo=1.1.0"); 117 | 118 | { 119 | Dependency dep("foo=1.0.0"); 120 | EXPECT_TRUE(dep.SatisfiedBy(bar_1_0_0)); 121 | EXPECT_FALSE(dep.SatisfiedBy(bar_1_1_0)); 122 | } 123 | 124 | { 125 | Dependency dep("foo>=1.0.0"); 126 | EXPECT_FALSE(dep.SatisfiedBy(bar_0_9_9)); 127 | EXPECT_TRUE(dep.SatisfiedBy(bar_1_0_0)); 128 | EXPECT_TRUE(dep.SatisfiedBy(bar_1_1_0)); 129 | } 130 | 131 | { 132 | Dependency dep("foo>1.0.0"); 133 | EXPECT_FALSE(dep.SatisfiedBy(bar_0_9_9)); 134 | EXPECT_FALSE(dep.SatisfiedBy(bar_1_0_0)); 135 | EXPECT_TRUE(dep.SatisfiedBy(bar_1_1_0)); 136 | } 137 | 138 | { 139 | Dependency dep("foo<=1.0.0"); 140 | EXPECT_TRUE(dep.SatisfiedBy(bar_0_9_9)); 141 | EXPECT_TRUE(dep.SatisfiedBy(bar_1_0_0)); 142 | EXPECT_FALSE(dep.SatisfiedBy(bar_1_1_0)); 143 | } 144 | 145 | { 146 | Dependency dep("foo<1.0.0"); 147 | EXPECT_TRUE(dep.SatisfiedBy(bar_0_9_9)); 148 | EXPECT_FALSE(dep.SatisfiedBy(bar_1_0_0)); 149 | EXPECT_FALSE(dep.SatisfiedBy(bar_1_1_0)); 150 | } 151 | } 152 | 153 | TEST(DependencyTest, MalformedProvider) { 154 | Package foo; 155 | foo.provides.push_back("bar>=9"); 156 | 157 | Dependency dep("bar=9"); 158 | EXPECT_FALSE(dep.SatisfiedBy(foo)); 159 | } 160 | 161 | } // namespace 162 | -------------------------------------------------------------------------------- /src/auracle/format.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "auracle/format.hh" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "absl/time/time.h" 9 | #include "auracle/terminal.hh" 10 | #include "fmt/args.h" 11 | #include "fmt/printf.h" 12 | 13 | namespace { 14 | 15 | template 16 | struct Field { 17 | constexpr Field(std::string_view name, const T& value) 18 | : name(name), value(value) {} 19 | 20 | const std::string_view name; 21 | const T& value; 22 | }; 23 | 24 | template 25 | struct is_containerlike { 26 | private: 27 | template 28 | static decltype((void)std::declval().empty(), void(), std::true_type()) 29 | test(int); 30 | 31 | template 32 | static std::false_type test(...); 33 | 34 | public: 35 | enum { value = decltype(test(0))::value }; 36 | }; 37 | 38 | fmt::format_parse_context::iterator parse_format_or_default( 39 | fmt::format_parse_context& ctx, std::string_view default_value, 40 | std::string* out) { 41 | auto iter = std::find(ctx.begin(), ctx.end(), '}'); 42 | if (ctx.begin() == iter) { 43 | *out = default_value; 44 | } else { 45 | *out = {ctx.begin(), iter}; 46 | } 47 | 48 | return iter; 49 | } 50 | 51 | } // namespace 52 | 53 | FMT_BEGIN_NAMESPACE 54 | 55 | template <> 56 | struct formatter { 57 | auto parse(fmt::format_parse_context& ctx) { 58 | return parse_format_or_default(ctx, absl::RFC3339_sec, &tm_format); 59 | } 60 | 61 | auto format(const absl::Time t, fmt::format_context& ctx) const { 62 | return fmt::format_to( 63 | ctx.out(), "{}", absl::FormatTime(tm_format, t, absl::LocalTimeZone())); 64 | } 65 | 66 | std::string tm_format; 67 | }; 68 | 69 | // Specialization for formatting vectors of things with an optional custom 70 | // delimiter. 71 | template 72 | struct formatter> { 73 | auto parse(fmt::format_parse_context& ctx) { 74 | return parse_format_or_default(ctx, " ", &delimiter_); 75 | } 76 | 77 | auto format(const std::vector& vec, fmt::format_context& ctx) const { 78 | std::string_view sep; 79 | for (const auto& v : vec) { 80 | fmt::format_to(ctx.out(), "{}{}", sep, v); 81 | sep = delimiter_; 82 | } 83 | 84 | return ctx.out(); 85 | } 86 | 87 | private: 88 | std::string delimiter_; 89 | }; 90 | 91 | template 92 | struct formatter> : formatter { 93 | auto format(const Field& f, fmt::format_context& ctx) const { 94 | if constexpr (is_containerlike::value) { 95 | if (f.value.empty()) { 96 | return ctx.out(); 97 | } 98 | } 99 | 100 | // The value is not guaranteed to be consteval in this context, so the 101 | // format string has to be runtime evaluated. Icky, but at least this 102 | // is fully under our control. 103 | return fmt::format_to(ctx.out(), fmt::runtime("{:14s} : {}\n"), f.name, 104 | f.value); 105 | } 106 | }; 107 | 108 | FMT_END_NAMESPACE 109 | 110 | namespace format { 111 | 112 | void NameOnly(const aur::Package& package) { 113 | fmt::print("{}\n", terminal::Bold(package.name)); 114 | } 115 | 116 | void Short(const aur::Package& package, 117 | const std::optional& local_package) { 118 | namespace t = terminal; 119 | 120 | const auto& l = local_package; 121 | const auto& p = package; 122 | const auto ood_color = 123 | p.out_of_date > absl::UnixEpoch() ? &t::BoldRed : &t::BoldGreen; 124 | 125 | std::string installed_package; 126 | if (l) { 127 | const auto local_ver_color = 128 | auracle::Pacman::Vercmp(l->pkgver, p.version) < 0 ? &t::BoldRed 129 | : &t::BoldGreen; 130 | installed_package = 131 | fmt::format("[installed: {}]", local_ver_color(l->pkgver)); 132 | } 133 | 134 | fmt::print("{}{} {} ({}, {}) {}\n {}\n", t::BoldMagenta("aur/"), 135 | t::Bold(p.name), ood_color(p.version), p.votes, p.popularity, 136 | installed_package, p.description); 137 | } 138 | 139 | void Long(const aur::Package& package, 140 | const std::optional& local_package) { 141 | namespace t = terminal; 142 | 143 | const auto& l = local_package; 144 | const auto& p = package; 145 | 146 | const auto ood_color = 147 | p.out_of_date > absl::UnixEpoch() ? &t::BoldRed : &t::BoldGreen; 148 | 149 | std::string installed_package; 150 | if (l) { 151 | const auto local_ver_color = 152 | auracle::Pacman::Vercmp(l->pkgver, p.version) < 0 ? &t::BoldRed 153 | : &t::BoldGreen; 154 | installed_package = 155 | fmt::format(" [installed: {}]", local_ver_color(l->pkgver)); 156 | } 157 | 158 | fmt::print("{}", Field("Repository", t::BoldMagenta("aur"))); 159 | fmt::print("{}", Field("Name", p.name)); 160 | fmt::print("{}", Field("Version", ood_color(p.version) + installed_package)); 161 | 162 | if (p.name != p.pkgbase) { 163 | fmt::print("{}", Field("PackageBase", p.pkgbase)); 164 | } 165 | 166 | fmt::print("{}", Field("URL", t::BoldCyan(p.upstream_url))); 167 | fmt::print( 168 | "{}", Field("AUR Page", 169 | t::BoldCyan("https://aur.archlinux.org/packages/" + p.name))); 170 | fmt::print("{}", Field("Keywords", p.keywords)); 171 | fmt::print("{}", Field("Groups", p.groups)); 172 | fmt::print("{}", Field("Depends On", p.depends)); 173 | fmt::print("{}", Field("Makedepends", p.makedepends)); 174 | fmt::print("{}", Field("Checkdepends", p.checkdepends)); 175 | fmt::print("{}", Field("Provides", p.provides)); 176 | fmt::print("{}", Field("Conflicts With", p.conflicts)); 177 | fmt::print("{}", Field("Optional Deps", p.optdepends)); 178 | fmt::print("{}", Field("Replaces", p.replaces)); 179 | fmt::print("{}", Field("Licenses", p.licenses)); 180 | fmt::print("{}", Field("Votes", p.votes)); 181 | fmt::print("{}", Field("Popularity", p.popularity)); 182 | fmt::print("{}", Field("Submitter", p.submitter)); 183 | fmt::print("{}", Field("Maintainer", 184 | p.maintainer.empty() ? "(orphan)" : p.maintainer)); 185 | fmt::print("{}", Field("Co-maintainers", p.comaintainers)); 186 | fmt::print("{}", Field("Submitted", p.submitted)); 187 | fmt::print("{}", Field("Last Modified", p.modified)); 188 | if (p.out_of_date > absl::UnixEpoch()) { 189 | fmt::print("{}", Field("Out of Date", p.out_of_date)); 190 | } 191 | fmt::print("{}", Field("Description", p.description)); 192 | fmt::print("\n"); 193 | } 194 | 195 | void Update(const auracle::Pacman::Package& from, const aur::Package& to) { 196 | namespace t = terminal; 197 | 198 | fmt::print("{} {} -> {}\n", t::Bold(from.pkgname), t::BoldRed(from.pkgver), 199 | t::BoldGreen(to.version)); 200 | } 201 | 202 | namespace { 203 | 204 | void FormatCustomTo(std::string& out, std::string_view format, 205 | const aur::Package& package) { 206 | fmt::dynamic_format_arg_store store; 207 | 208 | store.push_back(fmt::arg("name", package.name)); 209 | store.push_back(fmt::arg("description", package.description)); 210 | store.push_back(fmt::arg("submitter", package.submitter)); 211 | store.push_back(fmt::arg("maintainer", package.maintainer)); 212 | store.push_back(fmt::arg("comaintainers", package.comaintainers)); 213 | store.push_back(fmt::arg("version", package.version)); 214 | store.push_back(fmt::arg("pkgbase", package.pkgbase)); 215 | store.push_back(fmt::arg("url", package.upstream_url)); 216 | store.push_back(fmt::arg("votes", package.votes)); 217 | store.push_back(fmt::arg("popularity", package.popularity)); 218 | store.push_back(fmt::arg("submitted", package.submitted)); 219 | store.push_back(fmt::arg("modified", package.modified)); 220 | store.push_back(fmt::arg("outofdate", package.out_of_date)); 221 | store.push_back(fmt::arg("depends", package.depends)); 222 | store.push_back(fmt::arg("makedepends", package.makedepends)); 223 | store.push_back(fmt::arg("checkdepends", package.checkdepends)); 224 | store.push_back(fmt::arg("conflicts", package.conflicts)); 225 | store.push_back(fmt::arg("groups", package.groups)); 226 | store.push_back(fmt::arg("keywords", package.keywords)); 227 | store.push_back(fmt::arg("licenses", package.licenses)); 228 | store.push_back(fmt::arg("optdepends", package.optdepends)); 229 | store.push_back(fmt::arg("provides", package.provides)); 230 | store.push_back(fmt::arg("replaces", package.replaces)); 231 | 232 | fmt::vformat_to(std::back_inserter(out), format, store); 233 | } 234 | 235 | } // namespace 236 | 237 | void Custom(const std::string_view format, const aur::Package& package) { 238 | std::string out; 239 | FormatCustomTo(out, format, package); 240 | std::cout << out << '\n'; 241 | } 242 | 243 | absl::Status Validate(std::string_view format) { 244 | try { 245 | std::string out; 246 | FormatCustomTo(out, format, aur::Package()); 247 | } catch (const fmt::format_error& e) { 248 | return absl::InvalidArgumentError(e.what()); 249 | } 250 | return absl::OkStatus(); 251 | } 252 | 253 | } // namespace format 254 | -------------------------------------------------------------------------------- /src/auracle/format.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AURACLE_FORMAT_HH_ 3 | #define AURACLE_FORMAT_HH_ 4 | 5 | #include 6 | 7 | #include "absl/status/status.h" 8 | #include "aur/package.hh" 9 | #include "auracle/pacman.hh" 10 | 11 | namespace format { 12 | 13 | void NameOnly(const aur::Package& package); 14 | void Update(const auracle::Pacman::Package& from, const aur::Package& to); 15 | void Short(const aur::Package& package, 16 | const std::optional& local_package); 17 | void Long(const aur::Package& package, 18 | const std::optional& local_package); 19 | void Custom(std::string_view format, const aur::Package& package); 20 | 21 | absl::Status Validate(std::string_view format); 22 | 23 | } // namespace format 24 | 25 | #endif // AURACLE_FORMAT_HH_ 26 | -------------------------------------------------------------------------------- /src/auracle/format_test.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "auracle/format.hh" 3 | 4 | #include 5 | #include 6 | 7 | #include "aur/package.hh" 8 | #include "gtest/gtest.h" 9 | 10 | class ScopedCapturer { 11 | public: 12 | ScopedCapturer(std::ostream& stream) : stream_(stream) { 13 | stream_.rdbuf(buffer_.rdbuf()); 14 | } 15 | 16 | ~ScopedCapturer() { stream_.rdbuf(original_sbuf_); } 17 | 18 | std::string GetCapturedOutput() { 19 | auto str = buffer_.str(); 20 | buffer_.str(std::string()); 21 | return str; 22 | } 23 | 24 | private: 25 | std::stringstream buffer_; 26 | std::streambuf* original_sbuf_ = std::cout.rdbuf(); 27 | std::ostream& stream_; 28 | }; 29 | 30 | aur::Package MakePackage() { 31 | aur::Package p; 32 | 33 | // string 34 | p.name = "cower"; 35 | p.version = "1.2.3"; 36 | 37 | // floating point 38 | p.popularity = 5.20238; 39 | 40 | // datetime 41 | p.submitted = absl::FromUnixSeconds(1499013608); 42 | 43 | // lists 44 | p.conflicts = { 45 | "auracle", 46 | "cower", 47 | "cower-git", 48 | }; 49 | 50 | return p; 51 | } 52 | 53 | TEST(FormatTest, DetectsInvalidFormats) { 54 | EXPECT_FALSE(format::Validate("{invalid}").ok()); 55 | } 56 | 57 | TEST(FormatTest, CustomStringFormat) { 58 | ScopedCapturer capture(std::cout); 59 | 60 | format::Custom("{name} -> {version}", MakePackage()); 61 | 62 | EXPECT_EQ(capture.GetCapturedOutput(), "cower -> 1.2.3\n"); 63 | } 64 | 65 | TEST(FormatTest, CustomFloatFormat) { 66 | ScopedCapturer capture(std::cout); 67 | 68 | auto p = MakePackage(); 69 | 70 | format::Custom("{popularity}", p); 71 | EXPECT_EQ(capture.GetCapturedOutput(), "5.20238\n"); 72 | 73 | format::Custom("{popularity:.2f}", p); 74 | EXPECT_EQ(capture.GetCapturedOutput(), "5.20\n"); 75 | } 76 | 77 | TEST(FormatTest, CustomDateTimeFormat) { 78 | ScopedCapturer capture(std::cout); 79 | 80 | auto p = MakePackage(); 81 | 82 | format::Custom("{submitted}", p); 83 | EXPECT_EQ(capture.GetCapturedOutput(), "2017-07-02T16:40:08+00:00\n"); 84 | 85 | format::Custom("{submitted:%s}", p); 86 | EXPECT_EQ(capture.GetCapturedOutput(), "1499013608\n"); 87 | } 88 | 89 | TEST(FormatTest, ListFormat) { 90 | ScopedCapturer capture(std::cout); 91 | 92 | auto p = MakePackage(); 93 | 94 | format::Custom("{conflicts}", p); 95 | EXPECT_EQ(capture.GetCapturedOutput(), "auracle cower cower-git\n"); 96 | 97 | format::Custom("{conflicts::,,}", p); 98 | EXPECT_EQ(capture.GetCapturedOutput(), "auracle:,,cower:,,cower-git\n"); 99 | } 100 | -------------------------------------------------------------------------------- /src/auracle/package_cache.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "auracle/package_cache.hh" 3 | 4 | #include 5 | 6 | #include "absl/algorithm/container.h" 7 | #include "absl/container/flat_hash_set.h" 8 | #include "auracle/dependency.hh" 9 | 10 | namespace auracle { 11 | 12 | std::pair PackageCache::AddPackage( 13 | aur::Package package) { 14 | const auto iter = absl::c_find(packages_, package); 15 | if (iter != packages_.end()) { 16 | return {&*iter, false}; 17 | } 18 | 19 | const auto& p = packages_.emplace_back(std::move(package)); 20 | int idx = packages_.size() - 1; 21 | index_by_pkgbase_.emplace(p.pkgbase, idx); 22 | index_by_pkgname_.emplace(p.name, idx); 23 | 24 | for (const auto& provide : p.provides) { 25 | index_by_provide_[Dependency(provide).name()].push_back(idx); 26 | } 27 | 28 | return {&p, true}; 29 | } 30 | 31 | const aur::Package* PackageCache::LookupByIndex(const PackageIndex& index, 32 | const std::string& item) const { 33 | const auto iter = index.find(item); 34 | return iter == index.end() ? nullptr : &packages_[iter->second]; 35 | } 36 | 37 | const aur::Package* PackageCache::LookupByPkgname( 38 | const std::string& pkgname) const { 39 | return LookupByIndex(index_by_pkgname_, pkgname); 40 | } 41 | 42 | const aur::Package* PackageCache::LookupByPkgbase( 43 | const std::string& pkgbase) const { 44 | return LookupByIndex(index_by_pkgbase_, pkgbase); 45 | } 46 | 47 | std::vector PackageCache::FindDependencySatisfiers( 48 | const Dependency& dep) const { 49 | std::vector satisfiers; 50 | if (auto iter = index_by_provide_.find(dep.name()); 51 | iter != index_by_provide_.end()) { 52 | for (const int idx : iter->second) { 53 | const auto& package = packages_[idx]; 54 | if (dep.SatisfiedBy(package)) { 55 | satisfiers.push_back(&package); 56 | } 57 | } 58 | } 59 | 60 | return satisfiers; 61 | } 62 | 63 | class DependencyPath : public std::vector { 64 | public: 65 | class Step { 66 | public: 67 | Step(DependencyPath& dependency_path, std::string step) 68 | : dependency_path_(dependency_path) { 69 | MaybeReportCycle(step); 70 | 71 | dependency_path_.push_back(step); 72 | } 73 | 74 | ~Step() { dependency_path_.pop_back(); } 75 | 76 | private: 77 | void MaybeReportCycle(const std::string& step) const { 78 | auto cycle_start = absl::c_find(dependency_path_, step); 79 | if (cycle_start == dependency_path_.end()) { 80 | return; 81 | } 82 | 83 | std::cerr << "warning: found dependency cycle:"; 84 | 85 | // Print the path leading up to the start of the cycle 86 | auto iter = dependency_path_.cbegin(); 87 | while (iter != cycle_start) { 88 | std::cerr << " " << *iter << " ->"; 89 | ++iter; 90 | } 91 | 92 | // Print the cycle itself, wrapped in brackets 93 | std::cerr << " [ " << *iter; 94 | ++iter; 95 | while (iter != dependency_path_.cend()) { 96 | std::cerr << " -> " << *iter; 97 | ++iter; 98 | } 99 | 100 | std::cerr << " -> " << *cycle_start << " ]\n"; 101 | } 102 | 103 | DependencyPath& dependency_path_; 104 | }; 105 | }; 106 | 107 | void PackageCache::WalkDependencies( 108 | const std::string& name, WalkDependenciesFn cb, 109 | const absl::btree_set& dependency_kinds) const { 110 | absl::flat_hash_set visited; 111 | DependencyPath dependency_path; 112 | 113 | std::function walk; 114 | walk = [&](const Dependency& dep) { 115 | DependencyPath::Step step(dependency_path, dep.name()); 116 | 117 | if (!visited.insert(dep.name()).second) { 118 | return; 119 | } 120 | 121 | const auto* pkg = LookupByPkgname(dep.name()); 122 | if (pkg != nullptr) { 123 | for (auto kind : dependency_kinds) { 124 | const auto& deplist = GetDependenciesByKind(pkg, kind); 125 | for (const auto& d : deplist) { 126 | walk(Dependency(d)); 127 | } 128 | } 129 | } 130 | 131 | cb(dep, pkg, dependency_path); 132 | }; 133 | 134 | walk(Dependency(name)); 135 | } 136 | 137 | } // namespace auracle 138 | -------------------------------------------------------------------------------- /src/auracle/package_cache.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef PACKAGE_AURACLE_CACHE_HH_ 3 | #define PACKAGE_AURACLE_CACHE_HH_ 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "absl/container/btree_set.h" 10 | #include "absl/container/flat_hash_map.h" 11 | #include "aur/package.hh" 12 | #include "auracle/dependency.hh" 13 | #include "auracle/dependency_kind.hh" 14 | 15 | namespace auracle { 16 | 17 | class PackageCache { 18 | public: 19 | PackageCache() = default; 20 | ~PackageCache() = default; 21 | 22 | PackageCache(const PackageCache&) = delete; 23 | PackageCache& operator=(const PackageCache&) = delete; 24 | 25 | PackageCache(PackageCache&&) = default; 26 | PackageCache& operator=(PackageCache&&) = default; 27 | 28 | std::pair AddPackage(aur::Package package); 29 | 30 | const aur::Package* LookupByPkgname(const std::string& pkgname) const; 31 | const aur::Package* LookupByPkgbase(const std::string& pkgbase) const; 32 | std::vector FindDependencySatisfiers( 33 | const Dependency& depstring) const; 34 | 35 | int size() const { return packages_.size(); } 36 | 37 | bool empty() const { return size() == 0; } 38 | 39 | using WalkDependenciesFn = 40 | std::function& dependency_path)>; 42 | void WalkDependencies( 43 | const std::string& name, WalkDependenciesFn cb, 44 | const absl::btree_set& dependency_kinds) const; 45 | 46 | private: 47 | std::vector packages_; 48 | 49 | using PackageIndex = absl::flat_hash_map; 50 | 51 | const aur::Package* LookupByIndex(const PackageIndex& index, 52 | const std::string& item) const; 53 | 54 | // We store integer indicies into the packages_ vector above rather than 55 | // pointers to the packages. This allows the vector to resize and not 56 | // invalidate our index maps. 57 | PackageIndex index_by_pkgname_; 58 | PackageIndex index_by_pkgbase_; 59 | absl::flat_hash_map> index_by_provide_; 60 | }; 61 | 62 | } // namespace auracle 63 | 64 | #endif // PACKAGE_AURACLE_CACHE_HH_ 65 | -------------------------------------------------------------------------------- /src/auracle/package_cache_test.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "auracle/package_cache.hh" 3 | 4 | #include "aur/package.hh" 5 | #include "gmock/gmock.h" 6 | #include "gtest/gtest.h" 7 | 8 | using auracle::Dependency; 9 | using testing::ElementsAre; 10 | using testing::Field; 11 | using testing::UnorderedElementsAre; 12 | 13 | TEST(PackageCacheTest, AddsPackages) { 14 | aur::Package package; 15 | package.package_id = 534056; 16 | package.name = "auracle-git"; 17 | package.pkgbase_id = 123768; 18 | package.pkgbase = "auracle-git"; 19 | 20 | auracle::PackageCache cache; 21 | ASSERT_TRUE(cache.empty()); 22 | 23 | { 24 | auto [p, added] = cache.AddPackage(package); 25 | EXPECT_TRUE(added); 26 | EXPECT_EQ(*p, package); 27 | EXPECT_EQ(1, cache.size()); 28 | } 29 | 30 | { 31 | auto [p, added] = cache.AddPackage(package); 32 | EXPECT_FALSE(added); 33 | EXPECT_EQ(*p, package); 34 | EXPECT_EQ(1, cache.size()); 35 | } 36 | } 37 | 38 | TEST(PackageCacheTest, LooksUpPackages) { 39 | auracle::PackageCache cache; 40 | 41 | { 42 | aur::Package package; 43 | package.package_id = 534056; 44 | package.name = "auracle-git"; 45 | package.pkgbase_id = 123768; 46 | package.pkgbase = "auracle-git"; 47 | cache.AddPackage(package); 48 | } 49 | 50 | { 51 | aur::Package package; 52 | package.package_id = 534055; 53 | package.name = "pkgfile-git"; 54 | package.pkgbase_id = 60915; 55 | package.pkgbase = "pkgfile-git"; 56 | cache.AddPackage(package); 57 | } 58 | 59 | { 60 | auto result = cache.LookupByPkgbase("pkgfile-git"); 61 | ASSERT_NE(result, nullptr); 62 | EXPECT_EQ(result->name, "pkgfile-git"); 63 | } 64 | 65 | { 66 | auto result = cache.LookupByPkgname("auracle-git"); 67 | ASSERT_NE(result, nullptr); 68 | EXPECT_EQ(result->name, "auracle-git"); 69 | } 70 | 71 | EXPECT_EQ(cache.LookupByPkgbase("notfound-pkgbase"), nullptr); 72 | EXPECT_EQ(cache.LookupByPkgname("notfound-pkgname"), nullptr); 73 | } 74 | 75 | std::string MakeDependency(const std::string& name) { return name; } 76 | 77 | TEST(PackageCacheTest, WalkDependencies) { 78 | auracle::PackageCache cache; 79 | { 80 | aur::Package package; 81 | package.package_id = 534055; 82 | package.name = "pkgfile-git"; 83 | package.pkgbase_id = 60915; 84 | package.pkgbase = "pkgfile-git"; 85 | package.depends = {"libarchive", "curl", "pcre", "pacman-git"}; 86 | cache.AddPackage(package); 87 | } 88 | { 89 | aur::Package package; 90 | package.package_id = 600011; 91 | package.name = "pacman-git"; 92 | package.pkgbase_id = 29937; 93 | package.pkgbase = "pacman-git"; 94 | package.depends = { 95 | "archlinux-keyring", "bash", "curl", "gpgme", "libarchive", 96 | "pacman-mirrorlist"}; 97 | package.makedepends = {"git", "asciidoc", "meson"}; 98 | package.checkdepends = {"python", "fakechroot"}; 99 | cache.AddPackage(package); 100 | } 101 | 102 | std::vector walked_packages; 103 | std::vector aur_packages; 104 | cache.WalkDependencies( 105 | "pkgfile-git", 106 | [&](const Dependency& dep, const aur::Package* pkg, 107 | const std::vector&) { 108 | walked_packages.push_back(dep.name()); 109 | if (pkg != nullptr) { 110 | aur_packages.push_back(pkg); 111 | } 112 | }, 113 | absl::btree_set{ 114 | auracle::DependencyKind::Depend, 115 | auracle::DependencyKind::CheckDepend, 116 | auracle::DependencyKind::MakeDepend, 117 | }); 118 | 119 | EXPECT_THAT( 120 | walked_packages, 121 | ElementsAre("libarchive", "curl", "pcre", "archlinux-keyring", "bash", 122 | "gpgme", "pacman-mirrorlist", "git", "asciidoc", "meson", 123 | "python", "fakechroot", "pacman-git", "pkgfile-git")); 124 | 125 | EXPECT_THAT(aur_packages, 126 | UnorderedElementsAre(Field(&aur::Package::name, "pkgfile-git"), 127 | Field(&aur::Package::name, "pacman-git"))); 128 | } 129 | 130 | TEST(PackageCacheTest, WalkDependenciesWithLimitedDeps) { 131 | auracle::PackageCache cache; 132 | { 133 | aur::Package package; 134 | package.package_id = 534055; 135 | package.name = "pkgfile-git"; 136 | package.pkgbase_id = 60915; 137 | package.pkgbase = "pkgfile-git"; 138 | package.depends = {"libarchive", "curl", "pcre", "pacman-git"}; 139 | cache.AddPackage(package); 140 | } 141 | { 142 | aur::Package package; 143 | package.package_id = 600011; 144 | package.name = "pacman-git"; 145 | package.pkgbase_id = 29937; 146 | package.pkgbase = "pacman-git"; 147 | package.depends = { 148 | "archlinux-keyring", "bash", "curl", "gpgme", "libarchive", 149 | "pacman-mirrorlist"}; 150 | package.makedepends = {"git", "asciidoc", "meson"}; 151 | package.checkdepends = {"python", "fakechroot"}; 152 | cache.AddPackage(package); 153 | } 154 | 155 | std::vector walked_packages; 156 | std::vector aur_packages; 157 | 158 | auto walk_dependencies_fn = [&](const Dependency& dep, 159 | const aur::Package* pkg, 160 | const std::vector&) { 161 | walked_packages.push_back(dep.name()); 162 | if (pkg != nullptr) { 163 | aur_packages.push_back(pkg); 164 | } 165 | }; 166 | 167 | cache.WalkDependencies("pkgfile-git", walk_dependencies_fn, 168 | absl::btree_set{ 169 | auracle::DependencyKind::Depend, 170 | auracle::DependencyKind::MakeDepend, 171 | }); 172 | EXPECT_THAT(walked_packages, 173 | ElementsAre("libarchive", "curl", "pcre", "archlinux-keyring", 174 | "bash", "gpgme", "pacman-mirrorlist", "git", 175 | "asciidoc", "meson", "pacman-git", "pkgfile-git")); 176 | walked_packages.clear(); 177 | aur_packages.clear(); 178 | 179 | cache.WalkDependencies("pkgfile-git", walk_dependencies_fn, 180 | absl::btree_set{ 181 | auracle::DependencyKind::Depend, 182 | }); 183 | EXPECT_THAT( 184 | walked_packages, 185 | ElementsAre("libarchive", "curl", "pcre", "archlinux-keyring", "bash", 186 | "gpgme", "pacman-mirrorlist", "pacman-git", "pkgfile-git")); 187 | walked_packages.clear(); 188 | aur_packages.clear(); 189 | 190 | cache.WalkDependencies("pacman-git", walk_dependencies_fn, 191 | absl::btree_set{ 192 | auracle::DependencyKind::CheckDepend, 193 | }); 194 | EXPECT_THAT(walked_packages, 195 | ElementsAre("python", "fakechroot", "pacman-git")); 196 | walked_packages.clear(); 197 | aur_packages.clear(); 198 | } 199 | 200 | TEST(PackageCacheTest, FindDependencySatisfiers) { 201 | auracle::PackageCache cache; 202 | { 203 | aur::Package package; 204 | package.package_id = 1; 205 | package.name = "pkgfile-super-shiny"; 206 | package.pkgbase = "pkgfile-super-shiny"; 207 | package.provides = {"pkgfile=11"}; 208 | cache.AddPackage(package); 209 | } 210 | { 211 | aur::Package package; 212 | package.package_id = 2; 213 | package.name = "pkgfile-git"; 214 | package.pkgbase = "pkgfile-git"; 215 | package.provides = {"pkgfile=10"}; 216 | cache.AddPackage(package); 217 | } 218 | { 219 | aur::Package package; 220 | package.package_id = 3; 221 | package.name = "pacman-git"; 222 | package.pkgbase = "pacman-git"; 223 | package.provides = {"pacman=6.1.0"}; 224 | cache.AddPackage(package); 225 | } 226 | 227 | EXPECT_THAT( 228 | cache.FindDependencySatisfiers(Dependency("pkgfile")), 229 | UnorderedElementsAre(Field(&aur::Package::name, "pkgfile-git"), 230 | Field(&aur::Package::name, "pkgfile-super-shiny"))); 231 | 232 | EXPECT_THAT(cache.FindDependencySatisfiers(Dependency("pkgfile=10")), 233 | UnorderedElementsAre(Field(&aur::Package::name, "pkgfile-git"))); 234 | 235 | EXPECT_THAT(cache.FindDependencySatisfiers(Dependency("pacman>6.1.0")), 236 | UnorderedElementsAre()); 237 | } 238 | -------------------------------------------------------------------------------- /src/auracle/pacman.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "auracle/pacman.hh" 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "absl/strings/ascii.h" 12 | #include "absl/types/span.h" 13 | 14 | namespace { 15 | 16 | class Glob { 17 | public: 18 | explicit Glob(std::string_view pattern) { 19 | glob_ok_ = 20 | glob(std::string(pattern).c_str(), GLOB_NOCHECK, nullptr, &glob_) == 0; 21 | if (glob_ok_) { 22 | results_ = absl::MakeSpan(glob_.gl_pathv, glob_.gl_pathc); 23 | } 24 | } 25 | 26 | bool ok() const { return glob_ok_; } 27 | 28 | ~Glob() { 29 | if (glob_ok_) { 30 | globfree(&glob_); 31 | } 32 | } 33 | 34 | using iterator = absl::Span::iterator; 35 | iterator begin() { return results_.begin(); } 36 | iterator end() { return results_.end(); } 37 | 38 | private: 39 | glob_t glob_; 40 | bool glob_ok_; 41 | absl::Span results_; 42 | }; 43 | 44 | class FileReader { 45 | public: 46 | explicit FileReader(const std::string& path) : file_(path) {} 47 | 48 | ~FileReader() { 49 | if (ok()) { 50 | file_.close(); 51 | } 52 | } 53 | 54 | bool GetLine(std::string& line) { return bool(std::getline(file_, line)); } 55 | 56 | bool ok() const { return file_.is_open(); } 57 | 58 | private: 59 | std::ifstream file_; 60 | }; 61 | 62 | bool IsSection(std::string_view s) { 63 | return s.size() > 2 && s.front() == '[' && s.back() == ']'; 64 | } 65 | 66 | std::pair SplitKeyValue( 67 | std::string_view line) { 68 | auto equals = line.find('='); 69 | if (equals == line.npos) { 70 | return {line, ""}; 71 | } 72 | 73 | return {absl::StripTrailingAsciiWhitespace(line.substr(0, equals)), 74 | absl::StripLeadingAsciiWhitespace(line.substr(equals + 1))}; 75 | } 76 | 77 | } // namespace 78 | 79 | namespace auracle { 80 | 81 | Pacman::Pacman(alpm_handle_t* alpm) 82 | : alpm_(alpm), local_db_(alpm_get_localdb(alpm_)) {} 83 | 84 | Pacman::~Pacman() { alpm_release(alpm_); } 85 | 86 | struct ParseState { 87 | std::string dbpath = "/var/lib/pacman"; 88 | std::string rootdir = "/"; 89 | 90 | std::string section; 91 | std::vector repos; 92 | }; 93 | 94 | bool ParseOneFile(const std::string& path, ParseState* state) { 95 | FileReader reader(path); 96 | 97 | for (std::string buffer; reader.GetLine(buffer);) { 98 | std::string_view line = absl::StripAsciiWhitespace(buffer); 99 | if (line.empty() || line[0] == '#') { 100 | continue; 101 | } 102 | 103 | if (IsSection(line)) { 104 | state->section = line.substr(1, line.size() - 2); 105 | continue; 106 | } 107 | 108 | auto [key, value] = SplitKeyValue(line); 109 | if (value.empty()) { 110 | // There aren't any directives we care about which are valueless. 111 | continue; 112 | } 113 | 114 | if (state->section == "options") { 115 | if (key == "DBPath") { 116 | state->dbpath = value; 117 | } else if (key == "RootDir") { 118 | state->rootdir = value; 119 | } 120 | } else { 121 | state->repos.emplace_back(state->section); 122 | } 123 | 124 | if (key == "Include") { 125 | Glob includes(value); 126 | if (!includes.ok()) { 127 | return false; 128 | } 129 | 130 | for (const auto* p : includes) { 131 | if (!ParseOneFile(p, state)) { 132 | return false; 133 | } 134 | } 135 | } 136 | } 137 | 138 | return reader.ok(); 139 | } 140 | 141 | // static 142 | std::unique_ptr Pacman::NewFromConfig(const std::string& config_file) { 143 | ParseState state; 144 | 145 | if (!ParseOneFile(config_file, &state)) { 146 | return nullptr; 147 | } 148 | 149 | alpm_errno_t err; 150 | alpm_handle_t* alpm = alpm_initialize("/", state.dbpath.c_str(), &err); 151 | if (alpm == nullptr) { 152 | return nullptr; 153 | } 154 | 155 | for (const auto& repo : state.repos) { 156 | alpm_register_syncdb(alpm, repo.c_str(), alpm_siglevel_t(0)); 157 | } 158 | 159 | return std::unique_ptr(new Pacman(alpm)); 160 | } 161 | 162 | std::string Pacman::RepoForPackage(const std::string& package) const { 163 | for (auto i = alpm_get_syncdbs(alpm_); i != nullptr; i = i->next) { 164 | auto db = static_cast(i->data); 165 | auto pkgcache = alpm_db_get_pkgcache(db); 166 | 167 | if (alpm_find_satisfier(pkgcache, package.c_str()) != nullptr) { 168 | return alpm_db_get_name(db); 169 | } 170 | } 171 | 172 | return std::string(); 173 | } 174 | 175 | bool Pacman::DependencyIsSatisfied(const std::string& package) const { 176 | auto* cache = alpm_db_get_pkgcache(local_db_); 177 | return alpm_find_satisfier(cache, package.c_str()) != nullptr; 178 | } 179 | 180 | std::optional Pacman::GetLocalPackage( 181 | const std::string& name) const { 182 | auto* pkg = alpm_db_get_pkg(local_db_, name.c_str()); 183 | if (pkg == nullptr) { 184 | return std::nullopt; 185 | } 186 | 187 | return Package{alpm_pkg_get_name(pkg), alpm_pkg_get_version(pkg)}; 188 | } 189 | 190 | std::vector Pacman::LocalPackages() const { 191 | std::vector packages; 192 | 193 | for (auto i = alpm_db_get_pkgcache(local_db_); i != nullptr; i = i->next) { 194 | const auto pkg = static_cast(i->data); 195 | 196 | packages.emplace_back(alpm_pkg_get_name(pkg), alpm_pkg_get_version(pkg)); 197 | } 198 | 199 | return packages; 200 | } 201 | 202 | // static 203 | int Pacman::Vercmp(const std::string& a, const std::string& b) { 204 | return alpm_pkg_vercmp(a.c_str(), b.c_str()); 205 | } 206 | 207 | } // namespace auracle 208 | -------------------------------------------------------------------------------- /src/auracle/pacman.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AURACLE_PACMAN_HH_ 3 | #define AURACLE_PACMAN_HH_ 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace auracle { 13 | 14 | class Pacman { 15 | public: 16 | struct Package { 17 | Package(std::string pkgname, std::string pkgver) 18 | : pkgname(std::move(pkgname)), pkgver(std::move(pkgver)) {} 19 | std::string pkgname; 20 | std::string pkgver; 21 | }; 22 | 23 | // Factory constructor. 24 | static std::unique_ptr NewFromConfig(const std::string& config_file); 25 | 26 | ~Pacman(); 27 | 28 | Pacman(const Pacman&) = delete; 29 | Pacman& operator=(const Pacman&) = delete; 30 | 31 | Pacman(Pacman&&) = default; 32 | Pacman& operator=(Pacman&&) = default; 33 | 34 | static int Vercmp(const std::string& a, const std::string& b); 35 | 36 | // Returns the name of the repo that the package belongs to, or empty string 37 | // if the package was not found in any repo. 38 | std::string RepoForPackage(const std::string& package) const; 39 | 40 | bool HasPackage(const std::string& package) const { 41 | return !RepoForPackage(package).empty(); 42 | } 43 | 44 | bool DependencyIsSatisfied(const std::string& package) const; 45 | 46 | std::vector LocalPackages() const; 47 | std::optional GetLocalPackage(const std::string& name) const; 48 | 49 | private: 50 | Pacman(alpm_handle_t* alpm); 51 | 52 | alpm_handle_t* alpm_; 53 | alpm_db_t* local_db_; 54 | }; 55 | 56 | } // namespace auracle 57 | 58 | #endif // AURACLE_PACMAN_HH_ 59 | -------------------------------------------------------------------------------- /src/auracle/search_fragment.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "auracle/search_fragment.hh" 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace auracle { 9 | 10 | namespace { 11 | 12 | // We know that the AUR will reject search strings shorter than 2 characters. 13 | constexpr std::string_view::size_type kMinCandidateSize = 2; 14 | constexpr std::string_view kRegexChars = R"(^.+*?$[](){}|\)"; 15 | 16 | } // namespace 17 | 18 | std::string_view GetSearchFragment(std::string_view input) { 19 | std::vector candidates; 20 | 21 | while (input.size() >= 2) { 22 | if (input.front() == '[' || input.front() == '{') { 23 | auto brace_end = input.find_first_of("]}"); 24 | if (brace_end == input.npos) { 25 | // This may or may not be an invalid regex, e.g. we might have 26 | // "foo\[bar". In practice, this should never happen because package 27 | // names shouldn't have such characters. 28 | return std::string_view(); 29 | } 30 | 31 | input.remove_prefix(brace_end); 32 | continue; 33 | } 34 | 35 | auto span = input.find_first_of(kRegexChars); 36 | if (span == input.npos) { 37 | span = input.size(); 38 | } else if (span == 0) { 39 | input.remove_prefix(1); 40 | continue; 41 | } 42 | 43 | // given 'cow?', we can't include w in the search 44 | // see if a ? or * follows cand 45 | auto cand = input.substr(0, span); 46 | if (input.size() > span && (input[span] == '?' || input[span] == '*')) { 47 | cand.remove_suffix(1); 48 | } 49 | 50 | if (cand.size() < kMinCandidateSize) { 51 | input.remove_prefix(1); 52 | continue; 53 | } 54 | 55 | candidates.push_back(cand); 56 | input.remove_prefix(span); 57 | } 58 | 59 | std::string_view longest; 60 | for (auto cand : candidates) { 61 | if (cand.size() > longest.size()) { 62 | longest = cand; 63 | } 64 | } 65 | 66 | return longest; 67 | } 68 | 69 | } // namespace auracle 70 | -------------------------------------------------------------------------------- /src/auracle/search_fragment.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AURACLE_SEARCH_FRAGMENT_HH_ 3 | #define AURACLE_SEARCH_FRAGMENT_HH_ 4 | 5 | #include 6 | 7 | namespace auracle { 8 | 9 | std::string_view GetSearchFragment(std::string_view input); 10 | 11 | } 12 | 13 | #endif // AURACLE_SEARCH_FRAGMENT_HH_ 14 | -------------------------------------------------------------------------------- /src/auracle/search_fragment_test.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "search_fragment.hh" 3 | 4 | #include "gtest/gtest.h" 5 | 6 | std::string GetSearchFragment(std::string_view input) { 7 | // In case we create a non-terminating loop in GetSearchFragment, this output 8 | // is helpful to understand what test case we're stuck on. 9 | printf("input: %s\n", std::string(input).c_str()); 10 | 11 | // Explanations for string_views aren't fantastic in gtest because the data 12 | // isn't guaranteed to be non-binary or null-terminated. We know better, so 13 | // coerce the return value to a string. 14 | return std::string(auracle::GetSearchFragment(input)); 15 | } 16 | 17 | TEST(SearchFragmentTest, ExtractsSuitableFragment) { 18 | EXPECT_EQ("foobar", GetSearchFragment("foobar")); 19 | EXPECT_EQ("foobar", GetSearchFragment("foobar$")); 20 | EXPECT_EQ("foobar", GetSearchFragment("^foobar")); 21 | 22 | EXPECT_EQ("foobar", GetSearchFragment("[invalid]foobar")); 23 | EXPECT_EQ("foobar", GetSearchFragment("foobar[invalid]")); 24 | EXPECT_EQ("moobarbaz", GetSearchFragment("foobar[invalid]moobarbaz")); 25 | 26 | EXPECT_EQ("foobar", GetSearchFragment("{invalid}foobar")); 27 | EXPECT_EQ("foobar", GetSearchFragment("foobar{invalid}")); 28 | EXPECT_EQ("moobarbaz", GetSearchFragment("foobar{invalid}moobarbaz")); 29 | 30 | EXPECT_EQ("co", GetSearchFragment("cow?fu")); 31 | EXPECT_EQ("fun", GetSearchFragment("co*fun")); 32 | 33 | EXPECT_EQ("co", GetSearchFragment("cow?fu?")); 34 | EXPECT_EQ("fu", GetSearchFragment("co*fun*")); 35 | 36 | EXPECT_EQ("foo", GetSearchFragment("fooo*")); 37 | EXPECT_EQ("foo", GetSearchFragment("fooo?")); 38 | EXPECT_EQ("fooo", GetSearchFragment("fooo+")); 39 | 40 | EXPECT_EQ("foo", GetSearchFragment("(foo|bar)")); 41 | EXPECT_EQ("foooo", GetSearchFragment("vim.*(foooo|barr)")); 42 | 43 | EXPECT_EQ("foobar", 44 | GetSearchFragment("^[derp]foobar[[inva$lid][{]}moo?bar{b}az")); 45 | 46 | EXPECT_EQ("", GetSearchFragment("[foobar]")); 47 | EXPECT_EQ("", GetSearchFragment("{foobar}")); 48 | EXPECT_EQ("", GetSearchFragment("{foobar")); 49 | EXPECT_EQ("", GetSearchFragment("foo[bar")); 50 | EXPECT_EQ("", GetSearchFragment("f+")); 51 | EXPECT_EQ("", GetSearchFragment("f+o+o+b+a+r")); 52 | } 53 | -------------------------------------------------------------------------------- /src/auracle/sort.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "sort.hh" 3 | 4 | namespace sort { 5 | 6 | template 7 | Sorter MakePackageSorter(T aur::Package::* field, OrderBy order_by) { 8 | switch (order_by) { 9 | case OrderBy::ORDER_ASC: 10 | return [=](const aur::Package& a, const aur::Package& b) { 11 | return (a.*field < b.*field) && !(a.*field > b.*field); 12 | }; 13 | case OrderBy::ORDER_DESC: 14 | return [=](const aur::Package& a, const aur::Package& b) { 15 | return (a.*field > b.*field) && !(a.*field < b.*field); 16 | }; 17 | } 18 | 19 | return nullptr; 20 | } 21 | 22 | Sorter MakePackageSorter(std::string_view field, OrderBy order_by) { 23 | if (field == "name") { 24 | return MakePackageSorter(&aur::Package::name, order_by); 25 | } else if (field == "popularity") { 26 | return MakePackageSorter(&aur::Package::popularity, order_by); 27 | } else if (field == "votes") { 28 | return MakePackageSorter(&aur::Package::votes, order_by); 29 | } else if (field == "firstsubmitted") { 30 | return MakePackageSorter(&aur::Package::submitted, order_by); 31 | } else if (field == "lastmodified") { 32 | return MakePackageSorter(&aur::Package::modified, order_by); 33 | } 34 | 35 | return nullptr; 36 | } 37 | 38 | } // namespace sort 39 | -------------------------------------------------------------------------------- /src/auracle/sort.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AURACLE_SORT_HH_ 3 | #define AURACLE_SORT_HH_ 4 | 5 | #include 6 | #include 7 | 8 | #include "aur/package.hh" 9 | 10 | namespace sort { 11 | 12 | enum class OrderBy : int8_t { ORDER_ASC, ORDER_DESC }; 13 | 14 | using Sorter = std::function; 15 | 16 | // Returns a binary predicate suitable for use with std::sort. 17 | Sorter MakePackageSorter(std::string_view field, OrderBy order_by); 18 | 19 | } // namespace sort 20 | 21 | #endif // AURACLE_SORT_HH_ 22 | -------------------------------------------------------------------------------- /src/auracle/sort_test.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "sort.hh" 3 | 4 | #include 5 | 6 | #include "aur/package.hh" 7 | #include "gmock/gmock.h" 8 | #include "gtest/gtest.h" 9 | 10 | using testing::ElementsAre; 11 | using testing::Field; 12 | 13 | TEST(SortOrderTest, RejectsInvalidSortField) { 14 | EXPECT_EQ(nullptr, sort::MakePackageSorter("", sort::OrderBy::ORDER_ASC)); 15 | EXPECT_EQ(nullptr, 16 | sort::MakePackageSorter("invalid", sort::OrderBy::ORDER_ASC)); 17 | EXPECT_EQ(nullptr, 18 | sort::MakePackageSorter("depends", sort::OrderBy::ORDER_ASC)); 19 | } 20 | 21 | std::vector MakePackages() { 22 | std::vector packages; 23 | 24 | { 25 | auto& p = packages.emplace_back(); 26 | p.name = "cower"; 27 | p.popularity = 1.2345; 28 | p.votes = 30; 29 | p.submitted = absl::FromUnixSeconds(10000); 30 | p.modified = absl::FromUnixSeconds(20000); 31 | } 32 | { 33 | auto& p = packages.emplace_back(); 34 | p.name = "auracle"; 35 | p.popularity = 5.3241; 36 | p.votes = 20; 37 | p.submitted = absl::FromUnixSeconds(20000); 38 | p.modified = absl::FromUnixSeconds(40000); 39 | } 40 | { 41 | auto& p = packages.emplace_back(); 42 | p.name = "pacman"; 43 | p.popularity = 5.3240; 44 | p.votes = 10; 45 | p.submitted = absl::FromUnixSeconds(30000); 46 | p.modified = absl::FromUnixSeconds(10000); 47 | } 48 | 49 | return packages; 50 | } 51 | 52 | class SortOrderTest : public testing::TestWithParam { 53 | protected: 54 | SortOrderTest() : packages_(MakePackages()) {} 55 | 56 | template 57 | void ExpectSorted(std::string_view field, ContainerMatcher matcher) { 58 | std::sort(packages_.begin(), packages_.end(), 59 | sort::MakePackageSorter(field, GetParam())); 60 | 61 | // lolhacky, but there's no easy way to express the reverse order of the 62 | // matchers without repeating it in the tests? 63 | if (GetParam() == sort::OrderBy::ORDER_DESC) { 64 | std::reverse(packages_.begin(), packages_.end()); 65 | } 66 | 67 | EXPECT_THAT(packages_, matcher); 68 | } 69 | 70 | private: 71 | std::vector packages_; 72 | }; 73 | 74 | TEST_P(SortOrderTest, ByName) { 75 | ExpectSorted("name", ElementsAre(Field(&aur::Package::name, "auracle"), 76 | Field(&aur::Package::name, "cower"), 77 | Field(&aur::Package::name, "pacman"))); 78 | } 79 | 80 | TEST_P(SortOrderTest, ByPopularity) { 81 | ExpectSorted("popularity", 82 | ElementsAre(Field(&aur::Package::popularity, 1.2345), 83 | Field(&aur::Package::popularity, 5.3240), 84 | Field(&aur::Package::popularity, 5.3241))); 85 | } 86 | 87 | TEST_P(SortOrderTest, ByVotes) { 88 | ExpectSorted("votes", ElementsAre(Field(&aur::Package::votes, 10), 89 | Field(&aur::Package::votes, 20), 90 | Field(&aur::Package::votes, 30))); 91 | } 92 | 93 | TEST_P(SortOrderTest, ByFirstSubmitted) { 94 | ExpectSorted( 95 | "firstsubmitted", 96 | ElementsAre( 97 | Field(&aur::Package::submitted, absl::FromUnixSeconds(10000)), 98 | Field(&aur::Package::submitted, absl::FromUnixSeconds(20000)), 99 | Field(&aur::Package::submitted, absl::FromUnixSeconds(30000)))); 100 | } 101 | 102 | TEST_P(SortOrderTest, ByLastModified) { 103 | ExpectSorted( 104 | "lastmodified", 105 | ElementsAre( 106 | Field(&aur::Package::modified, absl::FromUnixSeconds(10000)), 107 | Field(&aur::Package::modified, absl::FromUnixSeconds(20000)), 108 | Field(&aur::Package::modified, absl::FromUnixSeconds(40000)))); 109 | } 110 | 111 | INSTANTIATE_TEST_SUITE_P(BothOrderings, SortOrderTest, 112 | testing::Values(sort::OrderBy::ORDER_ASC, 113 | sort::OrderBy::ORDER_DESC), 114 | [](const auto& info) { 115 | switch (info.param) { 116 | case sort::OrderBy::ORDER_ASC: 117 | return "ORDER_ASC"; 118 | case sort::OrderBy::ORDER_DESC: 119 | return "ORDER_DESC"; 120 | } 121 | return "UNKNOWN"; 122 | }); 123 | -------------------------------------------------------------------------------- /src/auracle/terminal.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "terminal.hh" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "absl/strings/str_cat.h" 10 | 11 | namespace terminal { 12 | 13 | namespace { 14 | constexpr int kDefaultColumns = 80; 15 | int g_cached_columns = -1; 16 | WantColor g_want_color = WantColor::AUTO; 17 | 18 | std::string Color(const std::string& s, const char* color) { 19 | if (g_want_color == WantColor::NO) { 20 | return s; 21 | } 22 | 23 | return absl::StrCat(color, s, "\033[0m"); 24 | } 25 | 26 | } // namespace 27 | 28 | std::string Bold(const std::string& s) { return Color(s, "\033[1m"); } 29 | std::string BoldRed(const std::string& s) { return Color(s, "\033[1;31m"); } 30 | std::string BoldCyan(const std::string& s) { return Color(s, "\033[1;36m"); } 31 | std::string BoldGreen(const std::string& s) { return Color(s, "\033[1;32m"); } 32 | std::string BoldMagenta(const std::string& s) { return Color(s, "\033[1;35m"); } 33 | 34 | void Init(WantColor want) { 35 | if (want == WantColor::AUTO) { 36 | g_want_color = isatty(STDOUT_FILENO) == 1 ? WantColor::YES : WantColor::NO; 37 | } else { 38 | g_want_color = want; 39 | } 40 | } 41 | 42 | int Columns() { 43 | int c; 44 | struct winsize ws{}; 45 | 46 | if (g_cached_columns >= 0) { 47 | return g_cached_columns; 48 | } 49 | 50 | if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) { 51 | c = ws.ws_col; 52 | } else { 53 | c = isatty(STDOUT_FILENO) == 1 ? kDefaultColumns : 0; 54 | } 55 | 56 | g_cached_columns = c; 57 | return g_cached_columns; 58 | } 59 | 60 | } // namespace terminal 61 | -------------------------------------------------------------------------------- /src/auracle/terminal.hh: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef AURACLE_TERMINAL_HH_ 3 | #define AURACLE_TERMINAL_HH_ 4 | 5 | #include 6 | 7 | namespace terminal { 8 | 9 | enum class WantColor : short { 10 | YES, 11 | NO, 12 | AUTO, 13 | }; 14 | 15 | void Init(WantColor); 16 | 17 | int Columns(); 18 | 19 | std::string Bold(const std::string& s); 20 | std::string BoldCyan(const std::string& s); 21 | std::string BoldGreen(const std::string& s); 22 | std::string BoldMagenta(const std::string& s); 23 | std::string BoldRed(const std::string& s); 24 | 25 | } // namespace terminal 26 | 27 | #endif // AURACLE_TERMINAL_HH_ 28 | -------------------------------------------------------------------------------- /src/auracle_main.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #include "absl/container/flat_hash_map.h" 8 | #include "auracle/auracle.hh" 9 | #include "auracle/format.hh" 10 | #include "auracle/sort.hh" 11 | #include "auracle/terminal.hh" 12 | 13 | namespace { 14 | 15 | constexpr std::string_view kAurBaseurl = "https://aur.archlinux.org"; 16 | constexpr std::string_view kPacmanConf = "/etc/pacman.conf"; 17 | 18 | struct Flags { 19 | bool ParseFromArgv(int* argc, char*** argv); 20 | 21 | std::string baseurl = std::string(kAurBaseurl); 22 | std::string pacman_config = std::string(kPacmanConf); 23 | terminal::WantColor color = terminal::WantColor::AUTO; 24 | 25 | auracle::Auracle::CommandOptions command_options; 26 | }; 27 | 28 | [[noreturn]] void usage() { 29 | fputs( 30 | "auracle [options] command [args...]\n" 31 | "\n" 32 | "Query the AUR or clone packages.\n" 33 | "\n" 34 | " -h, --help Show this help\n" 35 | " --version Show software version\n" 36 | "\n" 37 | " -q, --quiet Output less, when possible\n" 38 | " -r, --recurse Recurse dependencies when cloning\n" 39 | " --literal Disallow regex in searches\n" 40 | " --searchby=BY Change search-by dimension\n" 41 | " --color=WHEN One of 'auto', 'never', or 'always'\n" 42 | " --sort=KEY Sort results in ascending order by KEY\n" 43 | " --rsort=KEY Sort results in descending order by KEY\n" 44 | " --resolve-deps=DEPS Include/exclude dependency types in " 45 | "recursive operations\n" 46 | " --show-file=FILE File to dump with 'show' command\n" 47 | " -C DIR, --chdir=DIR Change directory to DIR before cloning\n" 48 | " -F FMT, --format=FMT Specify custom output for search and info\n" 49 | "\n" 50 | "Commands:\n" 51 | " buildorder Show build order\n" 52 | " clone Clone or update git repos for packages\n" 53 | " info Show detailed information\n" 54 | " outdated Check for updates for foreign packages\n" 55 | " rawinfo Dump unformatted JSON for info query\n" 56 | " rawsearch Dump unformatted JSON for search query\n" 57 | " resolve Resolve dependency strings\n" 58 | " search Search for packages\n" 59 | " show Dump package source file\n" 60 | " update Clone out of date foreign packages\n", 61 | stdout); 62 | exit(0); 63 | } 64 | 65 | [[noreturn]] void version() { 66 | std::cout << "auracle " << PROJECT_VERSION << "\n"; 67 | exit(0); 68 | } 69 | 70 | bool Flags::ParseFromArgv(int* argc, char*** argv) { 71 | enum { 72 | ARG_COLOR = 1000, 73 | ARG_LITERAL, 74 | ARG_SEARCHBY, 75 | ARG_VERSION, 76 | ARG_BASEURL, 77 | ARG_SORT, 78 | ARG_RSORT, 79 | ARG_PACMAN_CONFIG, 80 | ARG_SHOW_FILE, 81 | ARG_RESOLVE_DEPS, 82 | }; 83 | 84 | static constexpr struct option opts[] = { 85 | // clang-format off 86 | { "help", no_argument, nullptr, 'h' }, 87 | { "quiet", no_argument, nullptr, 'q' }, 88 | { "recurse", no_argument, nullptr, 'r' }, 89 | { "chdir", required_argument, nullptr, 'C' }, 90 | { "color", required_argument, nullptr, ARG_COLOR }, 91 | { "literal", no_argument, nullptr, ARG_LITERAL }, 92 | { "resolve-deps", required_argument, nullptr, ARG_RESOLVE_DEPS }, 93 | { "rsort", required_argument, nullptr, ARG_RSORT }, 94 | { "searchby", required_argument, nullptr, ARG_SEARCHBY }, 95 | { "show-file", required_argument, nullptr, ARG_SHOW_FILE }, 96 | { "sort", required_argument, nullptr, ARG_SORT }, 97 | { "version", no_argument, nullptr, ARG_VERSION }, 98 | { "format", required_argument, nullptr, 'F' }, 99 | 100 | // These are "private", and intentionally not documented in the manual or 101 | // usage. 102 | { "baseurl", required_argument, nullptr, ARG_BASEURL }, 103 | { "pacmanconfig", required_argument, nullptr, ARG_PACMAN_CONFIG }, 104 | {}, 105 | // clang-format on 106 | }; 107 | 108 | int opt; 109 | while ((opt = getopt_long(*argc, *argv, "C:F:hqr", opts, nullptr)) != -1) { 110 | std::string_view sv_optarg(optarg ? optarg : ""); 111 | 112 | switch (opt) { 113 | case 'h': 114 | usage(); 115 | case 'q': 116 | command_options.quiet = true; 117 | break; 118 | case 'r': 119 | command_options.recurse = true; 120 | break; 121 | case 'C': 122 | if (sv_optarg.empty()) { 123 | std::cerr << "error: meaningless option: -C ''\n"; 124 | return false; 125 | } 126 | command_options.directory = optarg; 127 | break; 128 | case 'F': { 129 | auto status = format::Validate(sv_optarg); 130 | if (!status.ok()) { 131 | std::cerr << "error: invalid arg to --format (" << status.message() 132 | << "): " << sv_optarg << "\n"; 133 | return false; 134 | } 135 | command_options.format = optarg; 136 | break; 137 | } 138 | case ARG_LITERAL: 139 | command_options.allow_regex = false; 140 | break; 141 | case ARG_SEARCHBY: 142 | using SearchBy = aur::SearchRequest::SearchBy; 143 | 144 | command_options.search_by = 145 | aur::SearchRequest::ParseSearchBy(sv_optarg); 146 | if (command_options.search_by == SearchBy::INVALID) { 147 | std::cerr << "error: invalid arg to --searchby: " << sv_optarg 148 | << "\n"; 149 | return false; 150 | } 151 | break; 152 | case ARG_COLOR: 153 | if (sv_optarg == "auto") { 154 | color = terminal::WantColor::AUTO; 155 | } else if (sv_optarg == "never") { 156 | color = terminal::WantColor::NO; 157 | } else if (sv_optarg == "always") { 158 | color = terminal::WantColor::YES; 159 | } else { 160 | std::cerr << "error: invalid arg to --color: " << sv_optarg << "\n"; 161 | return false; 162 | } 163 | break; 164 | case ARG_SORT: 165 | command_options.sorter = 166 | sort::MakePackageSorter(sv_optarg, sort::OrderBy::ORDER_ASC); 167 | if (command_options.sorter == nullptr) { 168 | std::cerr << "error: invalid arg to --sort: " << sv_optarg << "\n"; 169 | return false; 170 | } 171 | break; 172 | case ARG_RSORT: 173 | command_options.sorter = 174 | sort::MakePackageSorter(sv_optarg, sort::OrderBy::ORDER_DESC); 175 | if (command_options.sorter == nullptr) { 176 | std::cerr << "error: invalid arg to --rsort: " << sv_optarg << "\n"; 177 | return false; 178 | } 179 | break; 180 | case ARG_RESOLVE_DEPS: 181 | if (!ParseDependencyKinds(sv_optarg, 182 | &command_options.resolve_depends)) { 183 | std::cerr << "error: invalid argument to --resolve-deps: " 184 | << sv_optarg << "\n"; 185 | return false; 186 | } 187 | break; 188 | case ARG_BASEURL: 189 | baseurl = optarg; 190 | break; 191 | case ARG_PACMAN_CONFIG: 192 | pacman_config = optarg; 193 | break; 194 | case ARG_VERSION: 195 | version(); 196 | break; 197 | case ARG_SHOW_FILE: 198 | command_options.show_file = optarg; 199 | break; 200 | default: 201 | return false; 202 | } 203 | } 204 | 205 | *argc -= optind - 1; 206 | *argv += optind - 1; 207 | 208 | return true; 209 | } 210 | 211 | } // namespace 212 | 213 | int main(int argc, char** argv) { 214 | Flags flags; 215 | if (!flags.ParseFromArgv(&argc, &argv)) { 216 | return 1; 217 | } 218 | 219 | if (argc < 2) { 220 | std::cerr << "error: no operation specified (use -h for help)\n"; 221 | return 1; 222 | } 223 | 224 | std::setlocale(LC_ALL, ""); 225 | terminal::Init(flags.color); 226 | 227 | const auto pacman = auracle::Pacman::NewFromConfig(flags.pacman_config); 228 | if (pacman == nullptr) { 229 | std::cerr << "error: failed to parse " << flags.pacman_config << "\n"; 230 | return 1; 231 | } 232 | 233 | auracle::Auracle auracle(auracle::Auracle::Options() 234 | .set_aur_baseurl(flags.baseurl) 235 | .set_pacman(pacman.get())); 236 | 237 | const std::string_view action(argv[1]); 238 | const std::vector args(argv + 2, argv + argc); 239 | 240 | const absl::flat_hash_map&, 243 | const auracle::Auracle::CommandOptions&)> 244 | cmds{ 245 | // clang-format off 246 | {"buildorder", &auracle::Auracle::BuildOrder}, 247 | {"clone", &auracle::Auracle::Clone}, 248 | {"download", &auracle::Auracle::Clone}, 249 | {"info", &auracle::Auracle::Info}, 250 | {"rawinfo", &auracle::Auracle::RawInfo}, 251 | {"rawsearch", &auracle::Auracle::RawSearch}, 252 | {"outdated", &auracle::Auracle::Outdated}, 253 | {"resolve", &auracle::Auracle::Resolve}, 254 | {"search", &auracle::Auracle::Search}, 255 | {"show", &auracle::Auracle::Show}, 256 | {"sync", &auracle::Auracle::Outdated}, 257 | {"update", &auracle::Auracle::Update}, 258 | // clang-format on 259 | }; 260 | 261 | const auto iter = cmds.find(action); 262 | if (iter == cmds.end()) { 263 | std::cerr << "Unknown action " << action << "\n"; 264 | return 1; 265 | } 266 | 267 | return (auracle.*iter->second)(args, flags.command_options) < 0 ? 1 : 0; 268 | } 269 | 270 | /* vim: set et ts=2 sw=2: */ 271 | -------------------------------------------------------------------------------- /src/test/gtest_main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "gtest/gtest.h" 4 | 5 | int main(int argc, char** argv) { 6 | setenv("TZ", "UTC", 1); 7 | setenv("LC_TIME", "C", 1); 8 | 9 | testing::InitGoogleTest(&argc, argv); 10 | return RUN_ALL_TESTS(); 11 | } 12 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falconindy/auracle/825327277c070f601ef613ffc76130a45a3a28a6/tests/__init__.py -------------------------------------------------------------------------------- /tests/auracle_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import fakeaur.server 5 | import glob 6 | import multiprocessing 7 | import os.path 8 | import subprocess 9 | import tempfile 10 | import textwrap 11 | import time 12 | import unittest 13 | 14 | __scriptdir__ = os.path.dirname(os.path.abspath(__file__)) 15 | 16 | 17 | def FindMesonBuildDir(): 18 | # When run through meson or ninja, we're already in the build dir 19 | if os.path.exists('.ninja_log'): 20 | return os.path.curdir 21 | 22 | # When run manually, we're probably in the repo root. 23 | paths = glob.glob('*/.ninja_log') 24 | if len(paths) > 1: 25 | raise ValueError( 26 | 'Multiple build directories found. Unable to proceed.') 27 | if len(paths) == 0: 28 | raise ValueError( 29 | 'No build directory found. Have you run "meson build" yet?') 30 | 31 | return os.path.dirname(paths[0]) 32 | 33 | 34 | class HTTPRequest: 35 | 36 | def __init__(self, request): 37 | requestline = request.pop(0) 38 | self.command, self.path, self.request_version = requestline.split() 39 | 40 | self.headers = {} 41 | for line in request: 42 | k, v = line.split(':', 1) 43 | self.headers[k.lower()] = v.strip() 44 | 45 | 46 | class TimeLoggingTestResult(unittest.runner.TextTestResult): 47 | 48 | def startTest(self, test): 49 | self._started_at = time.time() 50 | super().startTest(test) 51 | 52 | def addSuccess(self, test): 53 | elapsed = time.time() - self._started_at 54 | super(unittest.runner.TextTestResult, self).addSuccess(test) 55 | if self.showAll: 56 | self.stream.writeln(f'ok ({elapsed:.03}s)') 57 | elif self.dots: 58 | self.stream.write('.') 59 | self.stream.flush() 60 | 61 | 62 | unittest.runner.TextTestRunner.resultclass = TimeLoggingTestResult 63 | 64 | 65 | class AuracleRunResult: 66 | 67 | def _ProcessDebugOutput(self, requests_file): 68 | requests_sent = [] 69 | 70 | with open(requests_file) as f: 71 | header_text = [] 72 | for line in f.read().splitlines(): 73 | if line: 74 | header_text.append(line) 75 | else: 76 | requests_sent.append(HTTPRequest(header_text)) 77 | header_text = [] 78 | 79 | return requests_sent 80 | 81 | def __init__(self, process, request_log): 82 | self.process = process 83 | self.requests_sent = self._ProcessDebugOutput(request_log) 84 | self.request_uris = list(r.path for r in self.requests_sent) 85 | 86 | 87 | class TestCase(unittest.TestCase): 88 | 89 | def setUp(self): 90 | self.build_dir = FindMesonBuildDir() 91 | self._tempdir = tempfile.TemporaryDirectory() 92 | self.tempdir = self._tempdir.name 93 | 94 | q = multiprocessing.Queue() 95 | self.server = multiprocessing.Process(target=fakeaur.server.Serve, 96 | args=(q, )) 97 | self.server.start() 98 | self.baseurl = q.get() 99 | 100 | self._WritePacmanConf() 101 | 102 | def tearDown(self): 103 | self.server.terminate() 104 | self.server.join() 105 | self._tempdir.cleanup() 106 | self.assertEqual(0, self.server.exitcode, 107 | 'Server did not exit cleanly') 108 | 109 | def _WritePacmanConf(self): 110 | with open(os.path.join(self.tempdir, 'pacman.conf'), 'w') as f: 111 | f.write( 112 | textwrap.dedent(f'''\ 113 | [options] 114 | DBPath = {__scriptdir__}/fakepacman 115 | 116 | [extra] 117 | Server = file:///dev/null 118 | 119 | [community] 120 | Server = file:///dev/null 121 | ''')) 122 | 123 | def Auracle(self, args): 124 | requests_file = tempfile.NamedTemporaryFile(dir=self.tempdir, 125 | prefix='requests-', 126 | delete=False).name 127 | 128 | env = { 129 | 'PATH': f'{__scriptdir__}/fakeaur:{os.getenv("PATH")}', 130 | 'AURACLE_TEST_TMPDIR': self.tempdir, 131 | 'AURACLE_DEBUG': f'requests:{requests_file}', 132 | 'LC_TIME': 'C', 133 | 'TZ': 'UTC', 134 | } 135 | 136 | cmdline = [ 137 | os.path.join(self.build_dir, 'auracle'), 138 | '--color=never', 139 | f'--baseurl={self.baseurl}', 140 | f'--pacmanconfig={self.tempdir}/pacman.conf', 141 | f'--chdir={self.tempdir}', 142 | ] + args 143 | 144 | return AuracleRunResult( 145 | subprocess.run(cmdline, env=env, capture_output=True), 146 | requests_file) 147 | 148 | def assertPkgbuildExists(self, pkgname): 149 | self.assertTrue( 150 | os.path.exists(os.path.join(self.tempdir, pkgname, 'PKGBUILD'))) 151 | self.assertTrue( 152 | os.path.exists(os.path.join(self.tempdir, pkgname, '.git'))) 153 | 154 | def assertPkgbuildNotExists(self, pkgname): 155 | self.assertFalse( 156 | os.path.exists(os.path.join(self.tempdir, pkgname, 'PKGBUILD'))) 157 | self.assertFalse( 158 | os.path.exists(os.path.join(self.tempdir, pkgname, '.git'))) 159 | 160 | 161 | def main(): 162 | unittest.main() 163 | -------------------------------------------------------------------------------- /tests/fakeaur/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falconindy/auracle/825327277c070f601ef613ffc76130a45a3a28a6/tests/fakeaur/__init__.py -------------------------------------------------------------------------------- /tests/fakeaur/db/info/auracle-git: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":550791,"Name":"auracle-git","PackageBaseID":123768,"PackageBase":"auracle-git","Version":"r74.82e863f-1","Description":"A flexible client for the AUR","URL":"https:\/\/github.com\/falconindy\/auracle.git","NumVotes":16,"Popularity":1.02916,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1499013608,"LastModified":1539195709,"URLPath":"\/cgit\/aur.git\/snapshot\/auracle-git.tar.gz","Depends":["pacman","libarchive.so","libcurl.so","libsystemd.so"],"MakeDepends":["meson","git","nlohmann-json"],"CheckDepends":["gtest","gmock"],"Conflicts":["auracle"],"Provides":["auracle"],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/camlidl: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":539478,"Name":"camlidl","PackageBaseID":33880,"PackageBase":"camlidl","Version":"1.06-4","Description":"A stub code generator and COM binding for Objective Caml (OCaml)","URL":"https:\/\/github.com\/xavierleroy\/camlidl","NumVotes":42,"Popularity":0.176103,"OutOfDate":null,"Maintainer":"simon04","FirstSubmitted":1264250277,"LastModified":1535610009,"URLPath":"\/cgit\/aur.git\/snapshot\/camlidl.tar.gz","Depends":["ocaml"],"License":["custom"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/curl-c-ares: -------------------------------------------------------------------------------- 1 | {"resultcount":1,"results":[{"CheckDepends":["valgrind"],"Conflicts":["curl"],"Depends":["ca-certificates","brotli","libbrotlidec.so","c-ares","libcares.so","krb5","libgssapi_krb5.so","libidn2","libidn2.so","libnghttp2","libnghttp2.so","libnghttp3","libnghttp3.so","libpsl","libpsl.so","libssh2","libssh2.so","openssl","libcrypto.so","libssl.so","zlib","libz.so","zstd","libzstd.so"],"Description":"command line tool and library for transferring data with URLs (built with c-ares)","FirstSubmitted":1702572556,"ID":1447729,"Keywords":[],"LastModified":1712907891,"License":["MIT"],"Maintainer":"5long","MakeDepends":["git"],"Name":"curl-c-ares","NumVotes":1,"OutOfDate":null,"PackageBase":"curl-c-ares","PackageBaseID":200566,"Popularity":0.007858,"Provides":["curl","libcurl.so"],"Submitter":"5long","URL":"https://curl.se/","URLPath":"/cgit/aur.git/snapshot/curl-c-ares.tar.gz","Version":"8.7.1-2"}],"type":"multiinfo","version":5} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/curl-git: -------------------------------------------------------------------------------- 1 | {"resultcount":1,"results":[{"Conflicts":["curl"],"Depends":["glibc","ca-certificates","brotli","krb5","libbrotlidec.so","libgssapi_krb5.so","libidn2","libidn2.so","libnghttp2","libpsl","libpsl.so","libssh2","libssh2.so","libzstd.so","zlib","zstd","openssl"],"Description":"A command line tool and library for transferring data with URLs","FirstSubmitted":1287929834,"ID":1457911,"Keywords":[],"LastModified":1714306278,"License":["MIT"],"Maintainer":"Chocobo1","MakeDepends":["git","gnutls","openssl","patchelf"],"Name":"curl-git","NumVotes":7,"OutOfDate":null,"PackageBase":"curl-git","PackageBaseID":42150,"Popularity":0,"Provides":["curl=8.7.1.r201.gc8e0cd1de8","libcurl.so"],"Submitter":"falconindy","URL":"https://curl.se/","URLPath":"/cgit/aur.git/snapshot/curl-git.tar.gz","Version":"8.7.1.r201.gc8e0cd1de8-1"}],"type":"multiinfo","version":5} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/curl-http3-ngtcp2: -------------------------------------------------------------------------------- 1 | {"resultcount":1,"results":[{"Conflicts":["curl"],"Depends":["ca-certificates","brotli","libbrotlidec.so","krb5","libgssapi_krb5.so","libidn2","libidn2.so","libnghttp2","libpsl","libpsl.so","libssh2","libssh2.so","openssl","zlib","zstd","libzstd.so","nghttp3","libnghttp3.so","ngtcp2","libngtcp2.so","quictls-openssl"],"Description":"command line tool and library for transferring data with URLs - compiled with HTTP/3 support (using ngtcp2 and nghttp3)","FirstSubmitted":1682607075,"ID":1391018,"Keywords":[],"LastModified":1706014227,"License":["MIT"],"Maintainer":"receptacular","MakeDepends":["git","patchelf"],"Name":"curl-http3-ngtcp2","NumVotes":0,"OutOfDate":null,"PackageBase":"curl-http3-ngtcp2","PackageBaseID":193085,"Popularity":0,"Provides":["curl=8.5.0","libcurl.so"],"Submitter":"receptacular","URL":"https://curl.se/","URLPath":"/cgit/aur.git/snapshot/curl-http3-ngtcp2.tar.gz","Version":"8.5.0-1"}],"type":"multiinfo","version":5} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/curl-quiche-git: -------------------------------------------------------------------------------- 1 | {"resultcount":1,"results":[{"Conflicts":["curl"],"Depends":["ca-certificates","brotli","libbrotlidec.so","libgssapi_krb5.so","krb5","libidn2","libidn2.so","libnghttp2","libpsl","libpsl.so","libssh2","libssh2.so","openssl","zlib","zstd","libzstd.so"],"Description":"An URL retrieval utility and library with QUIC(quiche)/HTTP3 support","FirstSubmitted":1680699530,"ID":1236930,"Keywords":[],"LastModified":1680699530,"License":["MIT"],"Maintainer":"soh","MakeDepends":["git","cmake"],"Name":"curl-quiche-git","NumVotes":0,"OutOfDate":1717530603,"PackageBase":"curl-quiche-git","PackageBaseID":192378,"Popularity":0,"Provides":["libcurl.so","curl=8.0.1"],"Submitter":"soh","URL":"https://github.com/curl/curl","URLPath":"/cgit/aur.git/snapshot/curl-quiche-git.tar.gz","Version":"8.0.1.r104.g98fac31b0-1"}],"type":"multiinfo","version":5} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/gapi-ocaml: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":483516,"Name":"gapi-ocaml","PackageBaseID":71627,"PackageBase":"gapi-ocaml","Version":"0.3.6-3","Description":"A simple OCaml client for Google Services.","URL":"https:\/\/astrada.github.io\/gapi-ocaml\/","NumVotes":41,"Popularity":0.160735,"OutOfDate":null,"Maintainer":"nerflad","FirstSubmitted":1373255765,"LastModified":1517702946,"URLPath":"\/cgit\/aur.git\/snapshot\/gapi-ocaml.tar.gz","Depends":["ocaml>=4.02.3","ocaml-findlib>=1.2.7","ocamlnet>=4.1.4","ocaml-curl>=0.5.3","ocaml-cryptokit>=1.3.14","ocaml-extlib>=1.5.1","ocaml-yojson>=1.0.2","ocaml-xmlm>=1.0.2"],"MakeDepends":["dune"],"License":["MIT"],"Keywords":["api","gapi","google","ocaml"]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/google-drive-ocamlfuse: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":546096,"Name":"google-drive-ocamlfuse","PackageBaseID":74384,"PackageBase":"google-drive-ocamlfuse","Version":"0.7.0-1","Description":"FUSE-based file system backed by Google Drive, written in OCaml","URL":"https:\/\/astrada.github.io\/google-drive-ocamlfuse\/","NumVotes":50,"Popularity":1.194897,"OutOfDate":null,"Maintainer":"pricechrispy","FirstSubmitted":1381787084,"LastModified":1537668043,"URLPath":"\/cgit\/aur.git\/snapshot\/google-drive-ocamlfuse.tar.gz","Depends":["ocaml>=4.02.3","ocaml-findlib>=1.2.7","ocamlfuse>=2.7.1","gapi-ocaml>=0.3.6","ocaml-sqlite3>=1.6.1"],"MakeDepends":["dune","ocaml-ounit"],"License":["MIT"],"Keywords":["drive","fuse","google","ocaml"]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/mingw-w64-environment: -------------------------------------------------------------------------------- 1 | {"resultcount":1,"results":[{"Description":"Script providing common environment variables and functions for MinGW (mingw-w64)","FirstSubmitted":1575319559,"ID":1504025,"Keywords":[],"LastModified":1720940851,"License":["BSD"],"Maintainer":"xantares","Name":"mingw-w64-environment","NumVotes":9,"OutOfDate":null,"PackageBase":"mingw-w64-environment","PackageBaseID":147146,"Popularity":0.490759,"Submitter":"xantares","URL":"http://fedoraproject.org/wiki/MinGW","URLPath":"/cgit/aur.git/snapshot/mingw-w64-environment.tar.gz","Version":"1-6"}],"type":"multiinfo","version":5} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/nlohmann-json: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":549970,"Name":"nlohmann-json","PackageBaseID":126360,"PackageBase":"nlohmann-json","Version":"3.3.0-1","Description":"Header-only JSON library for Modern C++","URL":"https:\/\/github.com\/nlohmann\/json","NumVotes":10,"Popularity":0.376558,"OutOfDate":null,"Maintainer":"eduardosm","FirstSubmitted":1508086775,"LastModified":1538921881,"URLPath":"\/cgit\/aur.git\/snapshot\/nlohmann-json.tar.gz","MakeDepends":["cmake"],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-base: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":541600,"Name":"ocaml-base","PackageBaseID":133806,"PackageBase":"ocaml-base","Version":"0.11.1-1","Description":"Full standard library replacement for OCaml","URL":"https:\/\/github.com\/janestreet\/base","NumVotes":7,"Popularity":0.96854,"OutOfDate":null,"Maintainer":"J5lx","FirstSubmitted":1530128713,"LastModified":1536273195,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-base.tar.gz","Depends":["ocaml","ocaml-sexplib0"],"MakeDepends":["dune"],"License":["Apache"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-configurator: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":522611,"Name":"ocaml-configurator","PackageBaseID":133809,"PackageBase":"ocaml-configurator","Version":"0.11.0-1","Description":"Helper library for gathering system configuration","URL":"https:\/\/github.com\/janestreet\/configurator","NumVotes":6,"Popularity":0.819736,"OutOfDate":null,"Maintainer":"J5lx","FirstSubmitted":1530130354,"LastModified":1530130354,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-configurator.tar.gz","Depends":["ocaml","ocaml-base","ocaml-stdio"],"MakeDepends":["dune"],"License":["Apache"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-cryptokit: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":511017,"Name":"ocaml-cryptokit","PackageBaseID":33266,"PackageBase":"ocaml-cryptokit","Version":"1.13-2","Description":"Cryptographic primitives for OCaml","URL":"http:\/\/pauillac.inria.fr\/~xleroy\/software.html#cryptokit","NumVotes":62,"Popularity":0.781685,"OutOfDate":null,"Maintainer":"oriba","FirstSubmitted":1262280717,"LastModified":1526333106,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-cryptokit.tar.gz","Depends":["ocaml","zlib","ocaml-zarith"],"MakeDepends":["ocaml","ocaml-findlib","ocamlbuild"],"License":["LGPL"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-curl: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":532318,"Name":"ocaml-curl","PackageBaseID":98513,"PackageBase":"ocaml-curl","Version":"0.8.2-1","Description":"OCaml bindings to libcurl networking library","URL":"https:\/\/github.com\/ygrek\/ocurl","NumVotes":33,"Popularity":0.697543,"OutOfDate":null,"Maintainer":"nerflad","FirstSubmitted":1440538269,"LastModified":1533567840,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-curl.tar.gz","Depends":["ocaml","curl>=7.28.0"],"MakeDepends":["ocaml-findlib"],"License":["BSD"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-extlib: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":536533,"Name":"ocaml-extlib","PackageBaseID":98514,"PackageBase":"ocaml-extlib","Version":"1.7.5-1","Description":"Extends the OCaml standard library","URL":"https:\/\/github.com\/ygrek\/ocaml-extlib","NumVotes":33,"Popularity":0.721349,"OutOfDate":null,"Maintainer":"nerflad","FirstSubmitted":1440538298,"LastModified":1534705656,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-extlib.tar.gz","Depends":["ocaml"],"MakeDepends":["ocaml-findlib","cppo"],"License":["LGPL"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-ounit: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":474657,"Name":"ocaml-ounit","PackageBaseID":54942,"PackageBase":"ocaml-ounit","Version":"2.0.6-1","Description":"Unit testing framework for OCaml","URL":"http:\/\/ounit.forge.ocamlcore.org","NumVotes":23,"Popularity":0.354702,"OutOfDate":null,"Maintainer":"nerflad","FirstSubmitted":1323888312,"LastModified":1514964555,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-ounit.tar.gz","Depends":["ocaml"],"MakeDepends":["ocaml-findlib","ocamlbuild"],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-pcre: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":522612,"Name":"ocaml-pcre","PackageBaseID":52452,"PackageBase":"ocaml-pcre","Version":"7.3.4-1","Description":"Perl compatible regular expressions for OCaml","URL":"http:\/\/mmottl.github.io\/pcre-ocaml","NumVotes":69,"Popularity":0.299258,"OutOfDate":null,"Maintainer":"J5lx","FirstSubmitted":1316254609,"LastModified":1530130849,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-pcre.tar.gz","Depends":["ocaml","pcre"],"MakeDepends":["dune","ocaml-base","ocaml-stdio","ocaml-configurator"],"Conflicts":["pcre-ocaml"],"Replaces":["pcre-ocaml"],"License":["custom:LGPL2.1 with linking exception"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-sexplib0: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":522602,"Name":"ocaml-sexplib0","PackageBaseID":133804,"PackageBase":"ocaml-sexplib0","Version":"0.11.0-1","Description":"Library containing the definition of S-expressions and some base converters","URL":"https:\/\/github.com\/janestreet\/sexplib0","NumVotes":9,"Popularity":1.528041,"OutOfDate":null,"Maintainer":"J5lx","FirstSubmitted":1530128371,"LastModified":1530128371,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-sexplib0.tar.gz","Depends":["ocaml"],"MakeDepends":["dune"],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-sqlite3: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":541529,"Name":"ocaml-sqlite3","PackageBaseID":44914,"PackageBase":"ocaml-sqlite3","Version":"4.4.0-1","Description":"SQLite3 bindings for OCaml","URL":"https:\/\/github.com\/mmottl\/sqlite3-ocaml","NumVotes":44,"Popularity":0.163033,"OutOfDate":null,"Maintainer":"oriba","FirstSubmitted":1293672715,"LastModified":1536253353,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-sqlite3.tar.gz","MakeDepends":["ocaml","ocaml-findlib","sqlite3","ocamlbuild","jbuilder","opam"],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-stdio: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":522608,"Name":"ocaml-stdio","PackageBaseID":133807,"PackageBase":"ocaml-stdio","Version":"0.11.0-1","Description":"Standard IO library for OCaml","URL":"https:\/\/github.com\/janestreet\/stdio","NumVotes":6,"Popularity":0.819734,"OutOfDate":null,"Maintainer":"J5lx","FirstSubmitted":1530129737,"LastModified":1530129737,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-stdio.tar.gz","Depends":["ocaml","ocaml-base"],"MakeDepends":["dune"],"License":["Apache"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-xmlm: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":451162,"Name":"ocaml-xmlm","PackageBaseID":45414,"PackageBase":"ocaml-xmlm","Version":"1.3.0-1","Description":"An OCaml streaming codec to decode and encode the XML data format","URL":"http:\/\/erratique.ch\/software\/xmlm\/","NumVotes":44,"Popularity":0.714563,"OutOfDate":null,"Maintainer":"J5lx","FirstSubmitted":1295067763,"LastModified":1507817047,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-xmlm.tar.gz","Depends":["ocaml"],"MakeDepends":["ocamlbuild","ocaml-findlib","ocaml-topkg","opam"],"License":["ISC"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocaml-zarith: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":506914,"Name":"ocaml-zarith","PackageBaseID":131985,"PackageBase":"ocaml-zarith","Version":"1.7-1","Description":"Implements arithmetic and logical operations over arbitrary-precision integers and rational numbers","URL":"https:\/\/github.com\/ocaml\/Zarith","NumVotes":27,"Popularity":0.784905,"OutOfDate":null,"Maintainer":"oriba","FirstSubmitted":1525124440,"LastModified":1525124440,"URLPath":"\/cgit\/aur.git\/snapshot\/ocaml-zarith.tar.gz","Depends":["gmp"],"MakeDepends":["ocaml>=3.12.1","ocaml-findlib"],"Provides":["zarith"],"License":["GPL2"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocamlfuse: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":309499,"Name":"ocamlfuse","PackageBaseID":72744,"PackageBase":"ocamlfuse","Version":"2.7.1-6","Description":"An ocaml binding for fuse.","URL":"http:\/\/sourceforge.net\/apps\/mediawiki\/ocamlfuse","NumVotes":27,"Popularity":0.160799,"OutOfDate":null,"Maintainer":"jkl","FirstSubmitted":1376975050,"LastModified":1464993230,"URLPath":"\/cgit\/aur.git\/snapshot\/ocamlfuse.tar.gz","Depends":["camlidl","fuse","ocaml-findlib"],"MakeDepends":["fuse"],"License":["GPL2"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/ocamlnet: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":523545,"Name":"ocamlnet","PackageBaseID":18606,"PackageBase":"ocamlnet","Version":"4.1.6-1","Description":"A library for Web and Internet programming in OCaml","URL":"http:\/\/projects.camlcity.org\/projects\/ocamlnet.html","NumVotes":54,"Popularity":0.29463,"OutOfDate":null,"Maintainer":"oriba","FirstSubmitted":1216845128,"LastModified":1530490301,"URLPath":"\/cgit\/aur.git\/snapshot\/ocamlnet.tar.gz","Depends":["ncurses","gnutls","krb5"],"MakeDepends":["ocaml-findlib","ocaml","ocaml-pcre"],"License":["GPL","LGPL"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/pacman-fancy-progress-git: -------------------------------------------------------------------------------- 1 | {"resultcount":1,"results":[{"CheckDepends":["python","fakechroot"],"Conflicts":["pacman"],"Depends":["archlinux-keyring","bash","curl","gpgme","libarchive","pacman-mirrorlist"],"Description":"pacman-git but patched to have a unicode progressbar","FirstSubmitted":1686001751,"ID":1268608,"Keywords":[],"LastModified":1686002090,"License":["GPL"],"Maintainer":null,"MakeDepends":["git","asciidoc","doxygen","meson"],"Name":"pacman-fancy-progress-git","NumVotes":1,"OptDepends":["pacman-contrib","perl-locale-gettext"],"OutOfDate":1711053837,"PackageBase":"pacman-fancy-progress-git","PackageBaseID":194375,"Popularity":0.005887,"Provides":["pacman=6.0.1"],"Submitter":"EvyGarden","URL":"https://www.archlinux.org/pacman/","URLPath":"/cgit/aur.git/snapshot/pacman-fancy-progress-git.tar.gz","Version":"6.0.1.r123.g2c45e854-3"}],"type":"multiinfo","version":5} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/pacman-git: -------------------------------------------------------------------------------- 1 | {"resultcount":1,"results":[{"CheckDepends":["python","fakechroot"],"Conflicts":["pacman"],"Depends":["bash","glibc","libarchive","curl","gpgme","pacman-mirrorlist","gettext","gawk","coreutils","gnupg","grep"],"Description":"A library-based package manager with dependency support","FirstSubmitted":1252344761,"ID":1441249,"Keywords":[],"LastModified":1712005765,"License":["GPL-2.0-or-later"],"Maintainer":"eclairevoyant","MakeDepends":["git","asciidoc","doxygen","meson"],"Name":"pacman-git","NumVotes":27,"OptDepends":["pacman-contrib","perl-locale-gettext"],"OutOfDate":null,"PackageBase":"pacman-git","PackageBaseID":29937,"Popularity":0.000002,"Provides":["pacman=6.1.0"],"URL":"https://www.archlinux.org/pacman/","URLPath":"/cgit/aur.git/snapshot/pacman-git.tar.gz","Version":"6.1.0.r15.g01e64e8b-1"}],"type":"multiinfo","version":5} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/pacman-pb: -------------------------------------------------------------------------------- 1 | {"resultcount":1,"results":[{"CheckDepends":["python","fakechroot"],"Conflicts":["pacman"],"Depends":["bash","glibc","libarchive","curl","gpgme","pacman-mirrorlist","gettext","gawk","coreutils","gnupg","grep"],"Description":"A library-based package manager with dependency support - includes a patch to customize the progress bar","FirstSubmitted":1669759690,"Groups":["base-devel"],"ID":1201845,"Keywords":[],"LastModified":1674649131,"License":["GPL"],"Maintainer":"miepee","MakeDepends":["meson","asciidoc","doxygen"],"Name":"pacman-pb","NumVotes":2,"OptDepends":["perl-locale-gettext"],"OutOfDate":1697122939,"PackageBase":"pacman-pb","PackageBaseID":188330,"Popularity":0.000007,"Provides":["libalpm.so","pacman=${pkgver%.*.*}"],"Submitter":"miepee","URL":"https://www.archlinux.org/pacman/","URLPath":"/cgit/aur.git/snapshot/pacman-pb.tar.gz","Version":"6.0.2-4"}],"type":"multiinfo","version":5} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/pkgfile-git: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":659046,"Name":"pkgfile-git","PackageBaseID":60915,"PackageBase":"pkgfile-git","Version":"21.38.gaa06367-1","Description":"a pacman .files metadata explorer","URL":"http:\/\/github.com\/falconindy\/pkgfile","NumVotes":43,"Popularity":0.15677,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1342485230,"LastModified":1571863495,"URLPath":"\/cgit\/aur.git\/snapshot\/pkgfile-git.tar.gz","Depends":["libarchive","curl","pcre","pacman","systemd-libs"],"MakeDepends":["git","perl","meson"],"CheckDepends":["python","gtest","gmock"],"Conflicts":["pkgfile"],"Provides":["pkgfile"],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/python-booleanoperations: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":686265,"Name":"python-booleanoperations","PackageBaseID":118417,"PackageBase":"python-booleanoperations","Version":"0.9.0-1","Description":"Boolean operations on paths.","URL":"https:\/\/github.com\/typemytype\/booleanOperations","NumVotes":2,"Popularity":0.201883,"OutOfDate":null,"Maintainer":"thrasibule","FirstSubmitted":1483579097,"LastModified":1579050088,"URLPath":"\/cgit\/aur.git\/snapshot\/python-booleanoperations.tar.gz","Depends":["python","python-pyclipper","python-fonttools"],"CheckDepends":["python-defcon","python-fontpens"],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/python-defcon: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":681005,"Name":"python-defcon","PackageBaseID":118418,"PackageBase":"python-defcon","Version":"0.6.0-2","Description":"A set of UFO based objects for use in font editing applications.","URL":"https:\/\/pypi.org\/project\/defcon\/","NumVotes":2,"Popularity":0.201739,"OutOfDate":null,"Maintainer":"thrasibule","FirstSubmitted":1483579658,"LastModified":1577550879,"URLPath":"\/cgit\/aur.git\/snapshot\/python-defcon.tar.gz","Depends":["python","python-fonttools","python>=3.8"],"MakeDepends":["python-setuptools"],"CheckDepends":[],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/python-fontmath: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":737810,"Name":"python-fontmath","PackageBaseID":118419,"PackageBase":"python-fontmath","Version":"0.6.0-1","Description":"A set of objects for performing math operations on font data.","URL":"https:\/\/github.com\/robotools\/fontMath","NumVotes":1,"Popularity":0.201663,"OutOfDate":null,"Maintainer":"thrasibule","FirstSubmitted":1483579955,"LastModified":1589678519,"URLPath":"\/cgit\/aur.git\/snapshot\/python-fontmath.tar.gz","Depends":["python-fonttools"],"MakeDepends":["python-setuptools"],"CheckDepends":[],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/python-fontparts: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":677463,"Name":"python-fontparts","PackageBaseID":144381,"PackageBase":"python-fontparts","Version":"0.9.2-1","Description":"The replacement for RoboFab.","URL":"https:\/\/github.com\/robotools\/fontParts","NumVotes":1,"Popularity":0.221979,"OutOfDate":null,"Maintainer":"thrasibule","FirstSubmitted":1566092146,"LastModified":1576545974,"URLPath":"\/cgit\/aur.git\/snapshot\/python-fontparts.tar.gz","Depends":["python","python-booleanoperations","python-defcon","python-fontmath","python-fonttools"],"MakeDepends":["python-setuptools"],"CheckDepends":["python-fontpens"],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/python-fontpens: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":677466,"Name":"python-fontpens","PackageBaseID":144382,"PackageBase":"python-fontpens","Version":"0.2.4-1","Description":"A collection of classes implementing the pen protocol for manipulating glyphs.","URL":"https:\/\/pypi.org\/project\/fontPens\/","NumVotes":1,"Popularity":0.221908,"OutOfDate":null,"Maintainer":"thrasibule","FirstSubmitted":1566092251,"LastModified":1576547707,"URLPath":"\/cgit\/aur.git\/snapshot\/python-fontpens.tar.gz","Depends":["python","python-fonttools"],"MakeDepends":["python-setuptools"],"CheckDepends":["python-fontmath","python-fontparts","python>=3.8"],"License":["BSD"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/python-pyclipper: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":684645,"Name":"python-pyclipper","PackageBaseID":117292,"PackageBase":"python-pyclipper","Version":"1.1.0.post3-1","Description":"Cython wrapper for the C++ translation of the Angus Johnson's Clipper library","URL":"https:\/\/github.com\/fonttools\/pyclipper","NumVotes":2,"Popularity":0.201736,"OutOfDate":null,"Maintainer":"grandchild","FirstSubmitted":1479839982,"LastModified":1578662655,"URLPath":"\/cgit\/aur.git\/snapshot\/python-pyclipper.tar.gz","Depends":["python"],"MakeDepends":["python-setuptools"],"License":["MIT"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/info/yaourt: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"multiinfo","resultcount":1,"results":[{"ID":428821,"Name":"yaourt","PackageBaseID":5863,"PackageBase":"yaourt","Version":"1.9-1","Description":"A pacman wrapper with extended features and AUR support","URL":"https:\/\/github.com\/archlinuxfr\/yaourt","NumVotes":3031,"Popularity":6.41905,"OutOfDate":null,"Maintainer":"archlinuxfr","FirstSubmitted":1152045478,"LastModified":1500495751,"URLPath":"\/cgit\/aur.git\/snapshot\/yaourt.tar.gz","Depends":["diffutils","pacman>=5.0","package-query>=1.8","gettext"],"OptDepends":["aurvote","customizepkg","rsync"],"License":["GPL"],"Keywords":[]}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/refreshdb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | update_info() { 4 | local r res 5 | 6 | for res in "$1"/*; do 7 | r=${res##*/} 8 | 9 | auracle rawinfo "$r" >"$res" 10 | done 11 | } 12 | 13 | update_search() { 14 | local r res by 15 | 16 | for res in "$1"/*; do 17 | IFS='|' read -r by r <<<"${res##*/}" 18 | 19 | auracle rawsearch --searchby="$by" "$r" >"$res" 20 | done 21 | } 22 | 23 | update_all() { 24 | local d db dbdir=$1 25 | 26 | for d in "$dbdir"/*/; do 27 | db=${d%/} db=${db##*/} 28 | 29 | "update_$db" "$d" 30 | done 31 | } 32 | 33 | update_all "${0%/*}" 34 | -------------------------------------------------------------------------------- /tests/fakeaur/db/search/maintainer|falconindy: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"search","resultcount":11,"results":[{"ID":404277,"Name":"cower-git","PackageBaseID":35888,"PackageBase":"cower-git","Version":"17-1","Description":"A simple AUR agent with a pretentious name","URL":"http:\/\/github.com\/falconindy\/cower","NumVotes":85,"Popularity":0.336947,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1269401179,"LastModified":1493040653,"URLPath":"\/cgit\/aur.git\/snapshot\/cower-git.tar.gz"},{"ID":452080,"Name":"curl-git","PackageBaseID":42150,"PackageBase":"curl-git","Version":"7.56.0.55.g4440b6ad5-1","Description":"A URL retrieval utility and library","URL":"https:\/\/curl.haxx.se\/","NumVotes":6,"Popularity":0,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1287929834,"LastModified":1508105301,"URLPath":"\/cgit\/aur.git\/snapshot\/curl-git.tar.gz"},{"ID":516053,"Name":"bash3","PackageBaseID":43811,"PackageBase":"bash3","Version":"3.2.057-4","Description":"The GNU Bourne Again shell. Version 3.2. Binary and manpage only.","URL":"http:\/\/www.gnu.org\/software\/bash\/bash.html","NumVotes":1,"Popularity":0,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1290432614,"LastModified":1527952620,"URLPath":"\/cgit\/aur.git\/snapshot\/bash3.tar.gz"},{"ID":206811,"Name":"expac-git","PackageBaseID":44048,"PackageBase":"expac-git","Version":"3.9.g7a73405-1","Description":"pacman database extraction utility","URL":"http:\/\/github.com\/falconindy\/expac","NumVotes":39,"Popularity":1.794548,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1291165086,"LastModified":1436031996,"URLPath":"\/cgit\/aur.git\/snapshot\/expac-git.tar.gz"},{"ID":510261,"Name":"cower","PackageBaseID":44921,"PackageBase":"cower","Version":"18-1","Description":"A simple AUR agent with a pretentious name","URL":"http:\/\/github.com\/falconindy\/cower","NumVotes":1010,"Popularity":6.772098,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1293676237,"LastModified":1526136002,"URLPath":"\/cgit\/aur.git\/snapshot\/cower.tar.gz"},{"ID":206815,"Name":"kmod-git","PackageBaseID":54820,"PackageBase":"kmod-git","Version":"19.33-1","Description":"interface to kernel module operations","URL":"http:\/\/git.kernel.org\/?p=utils\/kernel\/kmod\/kmod.git;a=summary","NumVotes":5,"Popularity":0.004466,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1323821730,"LastModified":1436032035,"URLPath":"\/cgit\/aur.git\/snapshot\/kmod-git.tar.gz"},{"ID":534055,"Name":"pkgfile-git","PackageBaseID":60915,"PackageBase":"pkgfile-git","Version":"18.2.g6e6150d-1","Description":"a pacman .files metadata explorer","URL":"http:\/\/github.com\/falconindy\/pkgfile","NumVotes":41,"Popularity":0.251408,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1342485230,"LastModified":1534000387,"URLPath":"\/cgit\/aur.git\/snapshot\/pkgfile-git.tar.gz"},{"ID":206810,"Name":"ponymix-git","PackageBaseID":61976,"PackageBase":"ponymix-git","Version":"2.8.gc46779e-1","Description":"CLI PulseAudio Volume Control","URL":"http:\/\/github.com\/falconindy\/ponymix","NumVotes":37,"Popularity":0.003679,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1344949345,"LastModified":1436031992,"URLPath":"\/cgit\/aur.git\/snapshot\/ponymix-git.tar.gz"},{"ID":232503,"Name":"pkgbuild-introspection-git","PackageBaseID":78018,"PackageBase":"pkgbuild-introspection-git","Version":"6.4.ga135f86-1","Description":"Tools for generating .SRCINFO files and PKGBUILD data extraction (mkaurball)","URL":"https:\/\/github.com\/falconindy\/pkgbuild-introspection","NumVotes":34,"Popularity":0.034968,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1390190142,"LastModified":1442837855,"URLPath":"\/cgit\/aur.git\/snapshot\/pkgbuild-introspection-git.tar.gz"},{"ID":411490,"Name":"asp-git","PackageBaseID":84251,"PackageBase":"asp-git","Version":"1.3.g375b035-1","Description":"Arch Linux build source file management (git version)","URL":"https:\/\/github.com\/falconindy\/asp","NumVotes":24,"Popularity":8.6e-5,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1406825827,"LastModified":1494931531,"URLPath":"\/cgit\/aur.git\/snapshot\/asp-git.tar.gz"},{"ID":550791,"Name":"auracle-git","PackageBaseID":123768,"PackageBase":"auracle-git","Version":"r74.82e863f-1","Description":"A flexible client for the AUR","URL":"https:\/\/github.com\/falconindy\/auracle.git","NumVotes":16,"Popularity":1.02916,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1499013608,"LastModified":1539195709,"URLPath":"\/cgit\/aur.git\/snapshot\/auracle-git.tar.gz"}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/search/name-desc|aura: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"search","resultcount":16,"results":[{"ID":218777,"Name":"libbs2b-git","PackageBaseID":96719,"PackageBase":"libbs2b-git","Version":"r75.5ca2d59-1","Description":"Bauer stereophonic-to-binaural DSP effect library (GIT version)","URL":"https:\/\/github.com\/alexmarsev\/libbs2b","NumVotes":1,"Popularity":0,"OutOfDate":null,"Maintainer":"dark-saber","FirstSubmitted":1438700196,"LastModified":1438972409,"URLPath":"\/cgit\/aur.git\/snapshot\/libbs2b-git.tar.gz"},{"ID":290204,"Name":"gnaural-presets","PackageBaseID":66056,"PackageBase":"gnaural-presets","Version":"1.2-1","Description":"Preset files for Gnaural","URL":"http:\/\/sourceforge.net\/projects\/gnaural\/files\/Presets\/","NumVotes":9,"Popularity":1.9e-5,"OutOfDate":null,"Maintainer":"Morn","FirstSubmitted":1357426636,"LastModified":1459602693,"URLPath":"\/cgit\/aur.git\/snapshot\/gnaural-presets.tar.gz"},{"ID":292401,"Name":"openal-hrtf","PackageBaseID":78392,"PackageBase":"openal-hrtf","Version":"1.0-1","Description":"Enable binaural audio globally in 3d applications","URL":"https:\/\/wiki.archlinux.org\/index.php\/Gaming","NumVotes":9,"Popularity":0.929953,"OutOfDate":null,"Maintainer":"Freso","FirstSubmitted":1391141023,"LastModified":1460177925,"URLPath":"\/cgit\/aur.git\/snapshot\/openal-hrtf.tar.gz"},{"ID":311905,"Name":"ladspa-bs2b","PackageBaseID":30513,"PackageBase":"ladspa-bs2b","Version":"0.9.1-3","Description":"Bauer stereophonic-to-binaural DSP effect library - LADSPA plugin","URL":"http:\/\/bs2b.sourceforge.net","NumVotes":37,"Popularity":0.082788,"OutOfDate":null,"Maintainer":"ewhal","FirstSubmitted":1254120890,"LastModified":1465560378,"URLPath":"\/cgit\/aur.git\/snapshot\/ladspa-bs2b.tar.gz"},{"ID":449898,"Name":"aura","PackageBaseID":60019,"PackageBase":"aura","Version":"1.4.0-1","Description":"A secure package manager for Arch Linux and the AUR written in Haskell.","URL":"https:\/\/github.com\/fosskers\/aura","NumVotes":154,"Popularity":6.6e-5,"OutOfDate":1509394396,"Maintainer":"fosskers","FirstSubmitted":1339580942,"LastModified":1507418476,"URLPath":"\/cgit\/aur.git\/snapshot\/aura.tar.gz"},{"ID":449899,"Name":"aura-bin","PackageBaseID":69882,"PackageBase":"aura-bin","Version":"1.4.0-1","Description":"A secure package manager for Arch Linux and the AUR written in Haskell - Prebuilt binary","URL":"https:\/\/github.com\/fosskers\/aura","NumVotes":161,"Popularity":0.586022,"OutOfDate":null,"Maintainer":"fosskers","FirstSubmitted":1368359427,"LastModified":1507418907,"URLPath":"\/cgit\/aur.git\/snapshot\/aura-bin.tar.gz"},{"ID":491728,"Name":"cauralho-git","PackageBaseID":130265,"PackageBase":"cauralho-git","Version":"r6.144727d-1","Description":"A small tool to help with updating AUR packages installed in the system.","URL":"https:\/\/github.com\/qrwteyrutiyoup\/cauralho","NumVotes":2,"Popularity":0.021112,"OutOfDate":null,"Maintainer":"qrwteyrutiyoup","FirstSubmitted":1519414210,"LastModified":1520200273,"URLPath":"\/cgit\/aur.git\/snapshot\/cauralho-git.tar.gz"},{"ID":493365,"Name":"ssr-git","PackageBaseID":96618,"PackageBase":"ssr-git","Version":"0.4.2.r78.g989568c-7","Description":"A tool for real-time spatial audio reproduction providing a variety of rendering algorithms, e.g. Wave Field Synthesis, Higher-Order Ambisonics and binaural techniques.","URL":"http:\/\/spatialaudio.net\/ssr\/","NumVotes":3,"Popularity":0.000875,"OutOfDate":null,"Maintainer":"dvzrv","FirstSubmitted":1438440411,"LastModified":1520771028,"URLPath":"\/cgit\/aur.git\/snapshot\/ssr-git.tar.gz"},{"ID":502906,"Name":"ssr","PackageBaseID":96617,"PackageBase":"ssr","Version":"0.4.2-7","Description":"A tool for real-time spatial audio reproduction providing a variety of rendering algorithms, e.g. Wave Field Synthesis, Higher-Order Ambisonics and binaural techniques.","URL":"http:\/\/spatialaudio.net\/ssr\/","NumVotes":8,"Popularity":0.916647,"OutOfDate":null,"Maintainer":"dvzrv","FirstSubmitted":1438440405,"LastModified":1523715262,"URLPath":"\/cgit\/aur.git\/snapshot\/ssr.tar.gz"},{"ID":506255,"Name":"lib32-libbs2b","PackageBaseID":116744,"PackageBase":"lib32-libbs2b","Version":"3.1.0-2","Description":"Bauer stereophonic-to-binaural DSP effect library","URL":"http:\/\/bs2b.sourceforge.net","NumVotes":1,"Popularity":0.929712,"OutOfDate":null,"Maintainer":"SolarAquarion","FirstSubmitted":1478226413,"LastModified":1524870207,"URLPath":"\/cgit\/aur.git\/snapshot\/lib32-libbs2b.tar.gz"},{"ID":512823,"Name":"bs2b-lv2","PackageBaseID":132714,"PackageBase":"bs2b-lv2","Version":"1.0.0-1","Description":"A lv2 plugin for using Bauer stereophonic-to-binaural DSP library","URL":"https:\/\/github.com\/nilninull\/bs2b-lv2","NumVotes":0,"Popularity":0,"OutOfDate":null,"Maintainer":"GalaxyMaster","FirstSubmitted":1526983813,"LastModified":1526983813,"URLPath":"\/cgit\/aur.git\/snapshot\/bs2b-lv2.tar.gz"},{"ID":528328,"Name":"gnaural","PackageBaseID":43882,"PackageBase":"gnaural","Version":"20110606-2","Description":"An opensource binaural-beat generator","URL":"http:\/\/gnaural.sourceforge.net\/","NumVotes":32,"Popularity":3.9e-5,"OutOfDate":null,"Maintainer":"dustball","FirstSubmitted":1290657493,"LastModified":1532179416,"URLPath":"\/cgit\/aur.git\/snapshot\/gnaural.tar.gz"},{"ID":530383,"Name":"ash-ir-dataset-git","PackageBaseID":134607,"PackageBase":"ash-ir-dataset-git","Version":"r395.f22942d37-2","Description":"An impulse response dataset for binaural synthesis of spatial audio systems on headphones","URL":"https:\/\/github.com\/ShanonPearce\/ASH-IR-Dataset","NumVotes":1,"Popularity":0.232665,"OutOfDate":null,"Maintainer":"blackhole","FirstSubmitted":1532938999,"LastModified":1532942212,"URLPath":"\/cgit\/aur.git\/snapshot\/ash-ir-dataset-git.tar.gz"},{"ID":530663,"Name":"aura-git","PackageBaseID":134593,"PackageBase":"aura-git","Version":"2.0.0.r1465.4c6c481-2","Description":"A package manager for Arch Linux and its AUR","URL":"https:\/\/github.com\/aurapm\/aura","NumVotes":1,"Popularity":0.229854,"OutOfDate":null,"Maintainer":"nick3ero","FirstSubmitted":1532906402,"LastModified":1533039694,"URLPath":"\/cgit\/aur.git\/snapshot\/aura-git.tar.gz"},{"ID":532075,"Name":"mingw-w64-libbs2b","PackageBaseID":134812,"PackageBase":"mingw-w64-libbs2b","Version":"3.1.0-1","Description":"Bauer stereophonic-to-binaural DSP effect library (mingw-w64)","URL":"http:\/\/bs2b.sourceforge.net","NumVotes":1,"Popularity":0.263051,"OutOfDate":null,"Maintainer":"adsun","FirstSubmitted":1533483332,"LastModified":1533483332,"URLPath":"\/cgit\/aur.git\/snapshot\/mingw-w64-libbs2b.tar.gz"},{"ID":550791,"Name":"auracle-git","PackageBaseID":123768,"PackageBase":"auracle-git","Version":"r74.82e863f-1","Description":"A flexible client for the AUR","URL":"https:\/\/github.com\/falconindy\/auracle.git","NumVotes":16,"Popularity":1.02916,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1499013608,"LastModified":1539195709,"URLPath":"\/cgit\/aur.git\/snapshot\/auracle-git.tar.gz"}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/search/name-desc|aurac: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"search","resultcount":1,"results":[{"ID":550791,"Name":"auracle-git","PackageBaseID":123768,"PackageBase":"auracle-git","Version":"r74.82e863f-1","Description":"A flexible client for the AUR","URL":"https:\/\/github.com\/falconindy\/auracle.git","NumVotes":16,"Popularity":1.02916,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1499013608,"LastModified":1539195709,"URLPath":"\/cgit\/aur.git\/snapshot\/auracle-git.tar.gz"}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/search/name-desc|auracle: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"search","resultcount":1,"results":[{"ID":550791,"Name":"auracle-git","PackageBaseID":123768,"PackageBase":"auracle-git","Version":"r74.82e863f-1","Description":"A flexible client for the AUR","URL":"https:\/\/github.com\/falconindy\/auracle.git","NumVotes":16,"Popularity":1.02916,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1499013608,"LastModified":1539195709,"URLPath":"\/cgit\/aur.git\/snapshot\/auracle-git.tar.gz"}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/search/name-desc|auracle-git: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"search","resultcount":1,"results":[{"ID":550791,"Name":"auracle-git","PackageBaseID":123768,"PackageBase":"auracle-git","Version":"r74.82e863f-1","Description":"A flexible client for the AUR","URL":"https:\/\/github.com\/falconindy\/auracle.git","NumVotes":16,"Popularity":1.02916,"OutOfDate":null,"Maintainer":"falconindy","FirstSubmitted":1499013608,"LastModified":1539195709,"URLPath":"\/cgit\/aur.git\/snapshot\/auracle-git.tar.gz"}]} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/search/name-desc|git: -------------------------------------------------------------------------------- 1 | {"version":5,"type":"error","resultcount":0,"results":[],"error":"Too many package results."} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/search/provides|curl: -------------------------------------------------------------------------------- 1 | {"resultcount":4,"results":[{"Description":"command line tool and library for transferring data with URLs (built with c-ares)","FirstSubmitted":1702572556,"ID":1447729,"LastModified":1712907891,"Maintainer":"5long","Name":"curl-c-ares","NumVotes":1,"OutOfDate":null,"PackageBase":"curl-c-ares","PackageBaseID":200566,"Popularity":0.007858,"URL":"https://curl.se/","URLPath":"/cgit/aur.git/snapshot/curl-c-ares.tar.gz","Version":"8.7.1-2"},{"Description":"A command line tool and library for transferring data with URLs","FirstSubmitted":1287929834,"ID":1457911,"LastModified":1714306278,"Maintainer":"Chocobo1","Name":"curl-git","NumVotes":7,"OutOfDate":null,"PackageBase":"curl-git","PackageBaseID":42150,"Popularity":0,"URL":"https://curl.se/","URLPath":"/cgit/aur.git/snapshot/curl-git.tar.gz","Version":"8.7.1.r201.gc8e0cd1de8-1"},{"Description":"command line tool and library for transferring data with URLs - compiled with HTTP/3 support (using ngtcp2 and nghttp3)","FirstSubmitted":1682607075,"ID":1391018,"LastModified":1706014227,"Maintainer":"receptacular","Name":"curl-http3-ngtcp2","NumVotes":0,"OutOfDate":null,"PackageBase":"curl-http3-ngtcp2","PackageBaseID":193085,"Popularity":0,"URL":"https://curl.se/","URLPath":"/cgit/aur.git/snapshot/curl-http3-ngtcp2.tar.gz","Version":"8.5.0-1"},{"Description":"An URL retrieval utility and library with QUIC(quiche)/HTTP3 support","FirstSubmitted":1680699530,"ID":1236930,"LastModified":1680699530,"Maintainer":"soh","Name":"curl-quiche-git","NumVotes":0,"OutOfDate":1717530603,"PackageBase":"curl-quiche-git","PackageBaseID":192378,"Popularity":0,"URL":"https://github.com/curl/curl","URLPath":"/cgit/aur.git/snapshot/curl-quiche-git.tar.gz","Version":"8.0.1.r104.g98fac31b0-1"}],"type":"search","version":5} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/db/search/provides|pacman: -------------------------------------------------------------------------------- 1 | {"resultcount":3,"results":[{"Description":"pacman-git but patched to have a unicode progressbar","FirstSubmitted":1686001751,"ID":1268608,"LastModified":1686002090,"Maintainer":null,"Name":"pacman-fancy-progress-git","NumVotes":1,"OutOfDate":1711053837,"PackageBase":"pacman-fancy-progress-git","PackageBaseID":194375,"Popularity":0.005887,"URL":"https://www.archlinux.org/pacman/","URLPath":"/cgit/aur.git/snapshot/pacman-fancy-progress-git.tar.gz","Version":"6.0.1.r123.g2c45e854-3"},{"Description":"A library-based package manager with dependency support","FirstSubmitted":1252344761,"ID":1441249,"LastModified":1712005765,"Maintainer":"eclairevoyant","Name":"pacman-git","NumVotes":27,"OutOfDate":null,"PackageBase":"pacman-git","PackageBaseID":29937,"Popularity":0.000002,"URL":"https://www.archlinux.org/pacman/","URLPath":"/cgit/aur.git/snapshot/pacman-git.tar.gz","Version":"6.1.0.r15.g01e64e8b-1"},{"Description":"A library-based package manager with dependency support - includes a patch to customize the progress bar","FirstSubmitted":1669759690,"ID":1201845,"LastModified":1674649131,"Maintainer":"miepee","Name":"pacman-pb","NumVotes":2,"OutOfDate":1697122939,"PackageBase":"pacman-pb","PackageBaseID":188330,"Popularity":0.000007,"URL":"https://www.archlinux.org/pacman/","URLPath":"/cgit/aur.git/snapshot/pacman-pb.tar.gz","Version":"6.0.2-4"}],"type":"search","version":5} 2 | -------------------------------------------------------------------------------- /tests/fakeaur/git: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | non_option_argv=() 4 | 5 | for arg; do 6 | case $arg in 7 | -*) 8 | ;; 9 | pull|clone) 10 | action=$arg 11 | ;; 12 | *) 13 | # not strictly true because we don't implement a full parser for git(1). 14 | # For example, this grabs the directory name from "-C somedir" as a 15 | # non-option arg. Doesn't matter too much for our purposes. 16 | non_option_argv+=("$arg") 17 | ;; 18 | esac 19 | done 20 | 21 | pkgname=${non_option_argv[-1]##*/} 22 | 23 | case $pkgname in 24 | yaourt) 25 | exit 1 26 | ;; 27 | esac 28 | 29 | mkdir -p "$AURACLE_TEST_TMPDIR/$pkgname/.git" 30 | touch \ 31 | "$AURACLE_TEST_TMPDIR/$pkgname/PKGBUILD" \ 32 | "$AURACLE_TEST_TMPDIR/$pkgname/$action" 33 | -------------------------------------------------------------------------------- /tests/fakeaur/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import gzip 5 | import http.server 6 | import io 7 | import json 8 | import os.path 9 | import signal 10 | import sys 11 | import tarfile 12 | import tempfile 13 | import urllib.parse 14 | 15 | DBROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'db') 16 | AUR_SERVER_VERSION = 5 17 | 18 | 19 | class FakeAurHandler(http.server.BaseHTTPRequestHandler): 20 | 21 | def do_GET(self): 22 | handlers = { 23 | '/rpc': self.handle_rpc, 24 | '/cgit/aur.git/snapshot': self.handle_download, 25 | '/cgit/aur.git/plain/': self.handle_source_file, 26 | } 27 | 28 | url = urllib.parse.urlparse(self.path) 29 | 30 | for path, handler in handlers.items(): 31 | if url.path.startswith(path): 32 | return handler('GET', url) 33 | 34 | return self.respond(status_code=404) 35 | 36 | def do_POST(self): 37 | handlers = { 38 | '/rpc': self.handle_rpc, 39 | } 40 | url = urllib.parse.urlparse(self.path) 41 | 42 | for path, handler in handlers.items(): 43 | if url.path.startswith(path): 44 | return handler('POST', url) 45 | 46 | return self.respond(status_code=404) 47 | 48 | @staticmethod 49 | def last_of(l): 50 | return l[-1] if l else None 51 | 52 | def make_json_reply(self, querytype, results=[], error=None): 53 | response = { 54 | 'version': AUR_SERVER_VERSION, 55 | 'type': querytype, 56 | 'resultcount': len(results), 57 | 'results': results, 58 | } 59 | 60 | if error: 61 | response['error'] = error 62 | 63 | return json.dumps(response).encode() 64 | 65 | def make_package_reply(self, querytype, results): 66 | return self.make_json_reply(querytype, results) 67 | 68 | def make_error_reply(self, error_message): 69 | return self.make_json_reply('error', [], error_message) 70 | 71 | def make_pkgbuild(self, pkgname): 72 | return f'pkgname={pkgname}\npkgver=1.2.3\n'.encode() 73 | 74 | def lookup_response(self, querytype, fragment): 75 | path = os.path.join(DBROOT, querytype, fragment) 76 | try: 77 | with open(path) as f: 78 | return f.read().strip().encode() 79 | except FileNotFoundError: 80 | return self.make_package_reply(querytype, []) 81 | 82 | def handle_rpc_info(self, args): 83 | results = [] 84 | 85 | if len(args) == 1: 86 | try: 87 | status_code = int(next(iter(args))) 88 | return self.respond( 89 | status_code=status_code, 90 | response=f'{status_code}: fridge too loud\n'.encode()) 91 | except ValueError: 92 | pass 93 | 94 | for arg in args: 95 | reply = self.lookup_response('info', arg) 96 | # extract the results from each DB file 97 | results.extend(json.loads(reply)['results']) 98 | 99 | return self.respond( 100 | response=self.make_package_reply('multiinfo', results)) 101 | 102 | def handle_rpc_search(self, arg, by): 103 | if len(arg) < 2: 104 | return self.respond( 105 | response=self.make_error_reply('Query arg too small.')) 106 | 107 | reply = self.lookup_response('search', f'{by}|{arg}' if by else arg) 108 | 109 | return self.respond(response=reply) 110 | 111 | def handle_rpc(self, command, url): 112 | if command == 'GET': 113 | queryparams = urllib.parse.parse_qs(url.query) 114 | else: 115 | content_len = int(self.headers.get('Content-Length')) 116 | queryparams = urllib.parse.parse_qs( 117 | self.rfile.read(content_len).decode()) 118 | 119 | if url.path.startswith('/rpc/v5/search'): 120 | return self.handle_rpc_search( 121 | url.path.rsplit('/')[-1], self.last_of(queryparams.get('by'))) 122 | elif url.path.startswith('/rpc/v5/info'): 123 | return self.handle_rpc_info(set(queryparams.get('arg[]', []))) 124 | else: 125 | return self.respond(response=self.make_error_reply( 126 | 'Incorrect request type specified.')) 127 | 128 | def handle_download(self, command, url): 129 | pkgname = os.path.basename(url.path).split('.')[0] 130 | 131 | # error injection for specific package name 132 | if pkgname == 'yaourt': 133 | return self.respond(response=b'you should use a better AUR helper') 134 | 135 | with tempfile.NamedTemporaryFile() as f: 136 | with tarfile.open(f.name, mode='w') as tar: 137 | b = self.make_pkgbuild(pkgname) 138 | 139 | t = tarfile.TarInfo(f'{pkgname}/PKGBUILD') 140 | t.size = len(b) 141 | tar.addfile(t, io.BytesIO(b)) 142 | 143 | headers = { 144 | 'content-disposition': f'inline, filename={pkgname}.tar.gz' 145 | } 146 | 147 | response = gzip.compress(f.read()) 148 | 149 | return self.respond(headers=headers, response=response) 150 | 151 | def handle_source_file(self, command, url): 152 | queryparams = urllib.parse.parse_qs(url.query) 153 | pkgname = self.last_of(queryparams.get('h')) 154 | 155 | source_file = os.path.basename(url.path) 156 | if source_file == 'PKGBUILD': 157 | return self.respond(response=self.make_pkgbuild(pkgname)) 158 | else: 159 | return self.respond(status_code=404) 160 | 161 | def respond(self, status_code=200, headers=[], response=None): 162 | self.send_response(status_code) 163 | 164 | for k, v in headers: 165 | self.send_header(k, v) 166 | self.end_headers() 167 | 168 | if response: 169 | self.wfile.write(response) 170 | 171 | def log_message(self, format, *args): 172 | # TODO: would be nice to make these visible: 173 | # 1) when the server is being run outside of a test context 174 | # 2) at the end of a failing test 175 | pass 176 | 177 | 178 | def Serve(queue=None, port=0): 179 | 180 | class FakeAurServer(http.server.HTTPServer): 181 | 182 | def handle_error(self, request, client_address): 183 | raise 184 | 185 | with FakeAurServer(('localhost', port), FakeAurHandler) as server: 186 | host, port = server.socket.getsockname()[:2] 187 | endpoint = f'http://{host}:{port}' 188 | 189 | if queue: 190 | queue.put(endpoint) 191 | else: 192 | print(f'serving on {endpoint}') 193 | 194 | signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit(0)) 195 | 196 | try: 197 | server.serve_forever() 198 | except KeyboardInterrupt: 199 | pass 200 | 201 | 202 | if __name__ == '__main__': 203 | port = 9001 204 | if len(sys.argv) >= 2: 205 | port = int(sys.argv[1]) 206 | Serve(port=port) 207 | -------------------------------------------------------------------------------- /tests/fakepacman/local/ALPM_DB_VERSION: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /tests/fakepacman/local/auracle-git-r20.fb982ca-1/desc: -------------------------------------------------------------------------------- 1 | %NAME% 2 | auracle-git 3 | 4 | %VERSION% 5 | r20.fb982ca-1 6 | 7 | %BASE% 8 | auracle-git 9 | 10 | %DESC% 11 | A flexible client for the AUR 12 | 13 | %URL% 14 | https://github.com/falconindy/auracle.git 15 | 16 | %ARCH% 17 | x86_64 18 | 19 | %BUILDDATE% 20 | 1539433664 21 | 22 | %INSTALLDATE% 23 | 1539433697 24 | 25 | %PACKAGER% 26 | Dave Reisner 27 | 28 | %SIZE% 29 | 419840 30 | 31 | %LICENSE% 32 | MIT 33 | 34 | %VALIDATION% 35 | none 36 | 37 | %DEPENDS% 38 | pacman 39 | libarchive.so=13-64 40 | libcurl.so=4-64 41 | libsystemd.so=0-64 42 | 43 | %CONFLICTS% 44 | auracle 45 | 46 | %PROVIDES% 47 | auracle 48 | 49 | -------------------------------------------------------------------------------- /tests/fakepacman/local/ocaml-4.07.0-1/desc: -------------------------------------------------------------------------------- 1 | %NAME% 2 | ocaml 3 | 4 | %VERSION% 5 | 4.07.0-1 6 | 7 | %BASE% 8 | ocaml 9 | 10 | %DESC% 11 | A functional language with OO extensions 12 | 13 | %URL% 14 | http://caml.inria.fr/ 15 | 16 | %ARCH% 17 | x86_64 18 | 19 | %BUILDDATE% 20 | 1534229348 21 | 22 | %INSTALLDATE% 23 | 1542637336 24 | 25 | %PACKAGER% 26 | Juergen Hoetzel 27 | 28 | %SIZE% 29 | 177992704 30 | 31 | %LICENSE% 32 | LGPL2.1 33 | custom: QPL-1.0 34 | 35 | %VALIDATION% 36 | pgp 37 | 38 | %DEPENDS% 39 | gdbm 40 | 41 | %OPTDEPENDS% 42 | ncurses: advanced ncurses features 43 | tk: advanced tk features 44 | 45 | -------------------------------------------------------------------------------- /tests/fakepacman/local/ocaml-4.07.0-1/mtree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falconindy/auracle/825327277c070f601ef613ffc76130a45a3a28a6/tests/fakepacman/local/ocaml-4.07.0-1/mtree -------------------------------------------------------------------------------- /tests/fakepacman/local/pkgfile-git-18.5.gf31f10b-1/desc: -------------------------------------------------------------------------------- 1 | %NAME% 2 | pkgfile-git 3 | 4 | %VERSION% 5 | 18.5.gf31f10b-1 6 | 7 | %BASE% 8 | pkgfile-git 9 | 10 | %DESC% 11 | a pacman .files metadata explorer 12 | 13 | %URL% 14 | http://github.com/falconindy/pkgfile 15 | 16 | %ARCH% 17 | x86_64 18 | 19 | %BUILDDATE% 20 | 1534030147 21 | 22 | %INSTALLDATE% 23 | 1534030154 24 | 25 | %PACKAGER% 26 | Dave Reisner 27 | 28 | %SIZE% 29 | 138240 30 | 31 | %LICENSE% 32 | MIT 33 | 34 | %VALIDATION% 35 | none 36 | 37 | %DEPENDS% 38 | libarchive 39 | curl 40 | pcre 41 | pacman 42 | 43 | %CONFLICTS% 44 | pkgfile 45 | 46 | %PROVIDES% 47 | pkgfile 48 | 49 | -------------------------------------------------------------------------------- /tests/fakepacman/sync/community.db: -------------------------------------------------------------------------------- 1 | community.db.tar.gz -------------------------------------------------------------------------------- /tests/fakepacman/sync/community.db.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falconindy/auracle/825327277c070f601ef613ffc76130a45a3a28a6/tests/fakepacman/sync/community.db.tar.gz -------------------------------------------------------------------------------- /tests/fakepacman/sync/extra.db: -------------------------------------------------------------------------------- 1 | extra.db.tar.gz -------------------------------------------------------------------------------- /tests/fakepacman/sync/extra.db.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falconindy/auracle/825327277c070f601ef613ffc76130a45a3a28a6/tests/fakepacman/sync/extra.db.tar.gz -------------------------------------------------------------------------------- /tests/test_buildorder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | 6 | import textwrap 7 | 8 | 9 | class TestBuildOrder(auracle_test.TestCase): 10 | 11 | def testSinglePackage(self): 12 | r = self.Auracle(['buildorder', 'ocaml-configurator']) 13 | self.assertEqual(0, r.process.returncode) 14 | self.assertMultiLineEqual( 15 | textwrap.dedent('''\ 16 | SATISFIEDREPOS ocaml 17 | REPOS dune 18 | AUR ocaml-sexplib0 ocaml-sexplib0 19 | AUR ocaml-base ocaml-base 20 | AUR ocaml-stdio ocaml-stdio 21 | TARGETAUR ocaml-configurator ocaml-configurator 22 | '''), r.process.stdout.decode()) 23 | 24 | def testMultiplePackage(self): 25 | r = self.Auracle( 26 | ['buildorder', 'ocaml-configurator', 'ocaml-cryptokit']) 27 | self.assertEqual(0, r.process.returncode) 28 | self.assertMultiLineEqual( 29 | textwrap.dedent('''\ 30 | SATISFIEDREPOS ocaml 31 | REPOS dune 32 | AUR ocaml-sexplib0 ocaml-sexplib0 33 | AUR ocaml-base ocaml-base 34 | AUR ocaml-stdio ocaml-stdio 35 | TARGETAUR ocaml-configurator ocaml-configurator 36 | REPOS zlib 37 | REPOS gmp 38 | REPOS ocaml-findlib 39 | AUR ocaml-zarith ocaml-zarith 40 | REPOS ocamlbuild 41 | TARGETAUR ocaml-cryptokit ocaml-cryptokit 42 | '''), r.process.stdout.decode()) 43 | 44 | def testDuplicatePackage(self): 45 | r = self.Auracle(['buildorder'] + 2 * ['ocaml-configurator']) 46 | self.assertEqual(0, r.process.returncode) 47 | self.assertMultiLineEqual( 48 | textwrap.dedent('''\ 49 | SATISFIEDREPOS ocaml 50 | REPOS dune 51 | AUR ocaml-sexplib0 ocaml-sexplib0 52 | AUR ocaml-base ocaml-base 53 | AUR ocaml-stdio ocaml-stdio 54 | TARGETAUR ocaml-configurator ocaml-configurator 55 | '''), r.process.stdout.decode()) 56 | 57 | def testOverlappingSubtrees(self): 58 | r = self.Auracle( 59 | ['buildorder', 'google-drive-ocamlfuse', 'ocaml-configurator']) 60 | self.assertEqual(0, r.process.returncode) 61 | self.assertMultiLineEqual( 62 | textwrap.dedent('''\ 63 | SATISFIEDREPOS ocaml 64 | REPOS ocaml-findlib 65 | AUR camlidl camlidl 66 | REPOS fuse 67 | AUR ocamlfuse ocamlfuse 68 | REPOS ncurses 69 | REPOS gnutls 70 | REPOS krb5 71 | REPOS pcre 72 | REPOS dune 73 | AUR ocaml-sexplib0 ocaml-sexplib0 74 | AUR ocaml-base ocaml-base 75 | AUR ocaml-stdio ocaml-stdio 76 | TARGETAUR ocaml-configurator ocaml-configurator 77 | AUR ocaml-pcre ocaml-pcre 78 | AUR ocamlnet ocamlnet 79 | REPOS curl 80 | AUR ocaml-curl ocaml-curl 81 | REPOS zlib 82 | REPOS gmp 83 | AUR ocaml-zarith ocaml-zarith 84 | REPOS ocamlbuild 85 | AUR ocaml-cryptokit ocaml-cryptokit 86 | REPOS cppo 87 | AUR ocaml-extlib ocaml-extlib 88 | REPOS ocaml-yojson 89 | REPOS ocaml-topkg 90 | REPOS opam 91 | AUR ocaml-xmlm ocaml-xmlm 92 | AUR gapi-ocaml gapi-ocaml 93 | REPOS sqlite3 94 | REPOS jbuilder 95 | AUR ocaml-sqlite3 ocaml-sqlite3 96 | AUR ocaml-ounit ocaml-ounit 97 | TARGETAUR google-drive-ocamlfuse google-drive-ocamlfuse 98 | '''), r.process.stdout.decode()) 99 | 100 | def testUnsatisfiedPackage(self): 101 | r = self.Auracle(['buildorder', 'auracle-git']) 102 | self.assertEqual(1, r.process.returncode) 103 | self.assertMultiLineEqual( 104 | textwrap.dedent('''\ 105 | UNKNOWN pacman auracle-git 106 | UNKNOWN libarchive.so auracle-git 107 | REPOS libcurl.so 108 | UNKNOWN libsystemd.so auracle-git 109 | UNKNOWN meson auracle-git 110 | UNKNOWN git auracle-git 111 | UNKNOWN cmake nlohmann-json auracle-git 112 | AUR nlohmann-json nlohmann-json 113 | UNKNOWN gtest auracle-git 114 | UNKNOWN gmock auracle-git 115 | TARGETAUR auracle-git auracle-git 116 | '''), r.process.stdout.decode()) 117 | 118 | def testDependencyCycle(self): 119 | r = self.Auracle(['buildorder', 'python-fontpens']) 120 | self.assertEqual(0, r.process.returncode) 121 | self.assertIn( 122 | 'warning: found dependency cycle: [ python-fontpens -> python-fontparts -> python-fontpens ]', 123 | r.process.stderr.decode().strip().splitlines()) 124 | 125 | def testNoDependencies(self): 126 | r = self.Auracle(['buildorder', 'mingw-w64-environment']) 127 | self.assertEqual(0, r.process.returncode) 128 | self.assertIn('TARGETAUR mingw-w64-environment mingw-w64-environment', 129 | r.process.stdout.decode().strip().splitlines()) 130 | 131 | 132 | if __name__ == '__main__': 133 | auracle_test.main() 134 | -------------------------------------------------------------------------------- /tests/test_clone.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | import os 6 | 7 | 8 | class TestClone(auracle_test.TestCase): 9 | 10 | def testCloneSingle(self): 11 | r = self.Auracle(['clone', 'auracle-git']) 12 | self.assertEqual(r.process.returncode, 0) 13 | self.assertPkgbuildExists('auracle-git') 14 | 15 | self.assertCountEqual(r.request_uris, ['/rpc/v5/info']) 16 | 17 | def testCloneNotFound(self): 18 | r = self.Auracle(['clone', 'packagenotfound']) 19 | self.assertNotEqual(r.process.returncode, 0) 20 | self.assertIn('no results found', r.process.stderr.decode()) 21 | 22 | def testCloneMultiple(self): 23 | r = self.Auracle(['clone', 'auracle-git', 'pkgfile-git']) 24 | self.assertEqual(r.process.returncode, 0) 25 | self.assertPkgbuildExists('auracle-git') 26 | self.assertPkgbuildExists('pkgfile-git') 27 | 28 | self.assertCountEqual(r.request_uris, ['/rpc/v5/info']) 29 | 30 | def testCloneRecursive(self): 31 | r = self.Auracle(['clone', '-r', 'auracle-git']) 32 | self.assertEqual(r.process.returncode, 0) 33 | self.assertPkgbuildExists('auracle-git') 34 | self.assertPkgbuildExists('nlohmann-json') 35 | 36 | self.assertGreater(len(r.request_uris), 1) 37 | self.assertIn('/rpc/v5/info', r.request_uris) 38 | 39 | def testCloneRecursiveWithRestrictedDeps(self): 40 | r = self.Auracle( 41 | ['clone', '-r', 'auracle-git', '--resolve-deps=^makedepends']) 42 | self.assertEqual(r.process.returncode, 0) 43 | self.assertPkgbuildExists('auracle-git') 44 | self.assertPkgbuildNotExists('nlohmann-json') 45 | 46 | self.assertGreater(len(r.request_uris), 1) 47 | self.assertIn('/rpc/v5/info', r.request_uris) 48 | 49 | def testCloneUpdatesExistingCheckouts(self): 50 | # Package doesn't initially exist, expect a clone. 51 | r = self.Auracle(['clone', 'auracle-git']) 52 | self.assertTrue(r.process.stdout.decode().startswith('clone')) 53 | self.assertTrue( 54 | os.path.exists(os.path.join(self.tempdir, 'auracle-git', 'clone'))) 55 | 56 | # Package now exists, expect a pull 57 | r = self.Auracle(['clone', 'auracle-git']) 58 | self.assertTrue(r.process.stdout.decode().startswith('update')) 59 | self.assertTrue( 60 | os.path.exists(os.path.join(self.tempdir, 'auracle-git', 'pull'))) 61 | 62 | def testCloneFailureReportsError(self): 63 | r = self.Auracle(['clone', 'yaourt']) 64 | self.assertNotEqual(r.process.returncode, 0) 65 | self.assertEqual( 66 | r.process.stderr.decode().strip(), 67 | ('error: clone failed for yaourt: ' 68 | 'INTERNAL: git exited with unexpected exit status 1')) 69 | 70 | 71 | if __name__ == '__main__': 72 | auracle_test.main() 73 | -------------------------------------------------------------------------------- /tests/test_custom_format.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | 6 | 7 | class TestInfo(auracle_test.TestCase): 8 | 9 | def testStringFormat(self): 10 | r = self.Auracle(['info', '-F', '{name} {version}', 'auracle-git']) 11 | self.assertEqual(0, r.process.returncode) 12 | self.assertEqual('auracle-git r74.82e863f-1\n', 13 | r.process.stdout.decode()) 14 | 15 | def testFloatingPointFormat(self): 16 | r = self.Auracle(['info', '-F', '{popularity}', 'auracle-git']) 17 | self.assertEqual(0, r.process.returncode) 18 | self.assertEqual('1.02916', r.process.stdout.decode().strip()) 19 | 20 | r = self.Auracle(['info', '-F', '{popularity:.2f}', 'auracle-git']) 21 | self.assertEqual(0, r.process.returncode) 22 | self.assertEqual('1.03', r.process.stdout.decode().strip()) 23 | 24 | def testDateTimeFormat(self): 25 | r = self.Auracle(['info', '-F', '{submitted}', 'auracle-git']) 26 | self.assertEqual(0, r.process.returncode) 27 | self.assertEqual('2017-07-02T16:40:08+00:00', 28 | r.process.stdout.decode().strip()) 29 | 30 | r = self.Auracle(['info', '-F', '{submitted::%s}', 'auracle-git']) 31 | self.assertEqual(0, r.process.returncode) 32 | self.assertEqual(':1499013608', r.process.stdout.decode().strip()) 33 | 34 | def testListFormat(self): 35 | r = self.Auracle(['info', '-F', '{depends}', 'auracle-git']) 36 | self.assertEqual(0, r.process.returncode) 37 | self.assertEqual('pacman libarchive.so libcurl.so libsystemd.so\n', 38 | r.process.stdout.decode()) 39 | 40 | r = self.Auracle(['info', '-F', '{depends::,,}', 'auracle-git']) 41 | self.assertEqual(0, r.process.returncode) 42 | self.assertEqual( 43 | 'pacman:,,libarchive.so:,,libcurl.so:,,libsystemd.so\n', 44 | r.process.stdout.decode()) 45 | 46 | def testInvalidFormat(self): 47 | r = self.Auracle(['info', '-F', '{invalid}']) 48 | self.assertNotEqual(0, r.process.returncode) 49 | self.assertIn('invalid arg', r.process.stderr.decode()) 50 | 51 | 52 | if __name__ == '__main__': 53 | auracle_test.main() 54 | -------------------------------------------------------------------------------- /tests/test_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | import json 6 | 7 | 8 | class TestInfo(auracle_test.TestCase): 9 | 10 | def testSingleInfoQuery(self): 11 | r = self.Auracle(['info', 'auracle-git']) 12 | self.assertEqual(0, r.process.returncode) 13 | 14 | def testExitSuccessIfAtLeastOneFound(self): 15 | r = self.Auracle(['info', 'auracle-git', 'packagenotfoundbro']) 16 | self.assertEqual(0, r.process.returncode) 17 | 18 | def testExitFailureOnNotFound(self): 19 | r = self.Auracle(['info', 'packagenotfoundbro']) 20 | self.assertNotEqual(0, r.process.returncode) 21 | 22 | def testBadResponsesFromAur(self): 23 | r = self.Auracle(['info', '503']) 24 | self.assertNotEqual(0, r.process.returncode) 25 | self.assertEqual(r.process.stderr.decode(), 26 | 'error: INTERNAL: HTTP 503\n') 27 | 28 | r = self.Auracle(['info', '429']) 29 | self.assertNotEqual(0, r.process.returncode) 30 | self.assertEqual(('error: RESOURCE_EXHAUSTED: Too many requests: ' 31 | 'the AUR has throttled your IP.\n'), 32 | r.process.stderr.decode()) 33 | 34 | 35 | if __name__ == '__main__': 36 | auracle_test.main() 37 | -------------------------------------------------------------------------------- /tests/test_outdated.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | 6 | 7 | class TestDownload(auracle_test.TestCase): 8 | 9 | def testOutdatedFindsPackagesNeedingUpgrade(self): 10 | r = self.Auracle(['outdated', '--quiet']) 11 | self.assertEqual(0, r.process.returncode) 12 | self.assertListEqual(r.process.stdout.decode().strip().splitlines(), 13 | ['auracle-git', 'pkgfile-git']) 14 | 15 | self.assertCountEqual(r.request_uris, ['/rpc/v5/info']) 16 | 17 | def testOutdatedFiltersUpdatesToArgs(self): 18 | r = self.Auracle(['outdated', 'auracle-git']) 19 | self.assertEqual(0, r.process.returncode) 20 | 21 | self.assertCountEqual(['/rpc/v5/info'], r.request_uris) 22 | 23 | def testExitsNonZeroWithoutUpgrades(self): 24 | r = self.Auracle(['outdated', '--quiet', 'ocaml']) 25 | self.assertEqual(1, r.process.returncode) 26 | 27 | 28 | if __name__ == '__main__': 29 | auracle_test.main() 30 | -------------------------------------------------------------------------------- /tests/test_raw_query.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | import json 6 | 7 | 8 | class TestRawQuery(auracle_test.TestCase): 9 | 10 | def testRawInfo(self): 11 | r = self.Auracle(['rawinfo', 'auracle-git']) 12 | self.assertEqual(0, r.process.returncode) 13 | 14 | parsed = json.loads(r.process.stdout) 15 | self.assertEqual(1, parsed['resultcount']) 16 | 17 | names = (r['Name'] for r in parsed['results']) 18 | self.assertIn('auracle-git', names) 19 | 20 | self.assertCountEqual(['/rpc/v5/info'], r.request_uris) 21 | 22 | def testRawSearch(self): 23 | r = self.Auracle(['rawsearch', 'aura']) 24 | self.assertEqual(0, r.process.returncode) 25 | 26 | parsed = json.loads(r.process.stdout) 27 | self.assertGreater(parsed['resultcount'], 1) 28 | 29 | names = (r['Name'] for r in parsed['results']) 30 | self.assertIn('auracle-git', names) 31 | 32 | self.assertCountEqual([ 33 | '/rpc/v5/search/aura?by=name-desc', 34 | ], r.request_uris) 35 | 36 | def testMultipleRawSearch(self): 37 | r = self.Auracle(['rawsearch', 'aura', 'systemd']) 38 | self.assertEqual(0, r.process.returncode) 39 | 40 | for line in r.process.stdout.splitlines(): 41 | parsed = json.loads(line) 42 | self.assertGreater(parsed['resultcount'], 1) 43 | 44 | self.assertCountEqual([ 45 | '/rpc/v5/search/aura?by=name-desc', 46 | '/rpc/v5/search/systemd?by=name-desc', 47 | ], r.request_uris) 48 | 49 | def testRawSearchBy(self): 50 | r = self.Auracle(['rawsearch', '--searchby=maintainer', 'falconindy']) 51 | self.assertEqual(0, r.process.returncode) 52 | 53 | parsed = json.loads(r.process.stdout) 54 | self.assertGreaterEqual(parsed['resultcount'], 1) 55 | 56 | names = (r['Name'] for r in parsed['results']) 57 | self.assertIn('auracle-git', names) 58 | 59 | self.assertCountEqual([ 60 | '/rpc/v5/search/falconindy?by=maintainer', 61 | ], r.request_uris) 62 | 63 | def testLiteralSearch(self): 64 | r = self.Auracle(['rawsearch', '--literal', '^aurac.+']) 65 | self.assertEqual(0, r.process.returncode) 66 | 67 | self.assertListEqual([ 68 | '/rpc/v5/search/^aurac.+?by=name-desc', 69 | ], r.request_uris) 70 | 71 | 72 | if __name__ == '__main__': 73 | auracle_test.main() 74 | -------------------------------------------------------------------------------- /tests/test_regex_search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | 6 | 7 | class TestRegexSearch(auracle_test.TestCase): 8 | 9 | def testFragmentTooShort(self): 10 | r = self.Auracle(['search', 'f']) 11 | self.assertNotEqual(0, r.process.returncode) 12 | self.assertIn('insufficient for searching by regular expression', 13 | r.process.stderr.decode()) 14 | 15 | def testInvalidRegex(self): 16 | r = self.Auracle(['search', '*invalid']) 17 | self.assertNotEqual(0, r.process.returncode) 18 | self.assertIn('invalid regex', r.process.stderr.decode()) 19 | 20 | def testMultipleSearchesWithFiltering(self): 21 | r = self.Auracle( 22 | ['search', '--quiet', '^aurac.+', '.le-git$', 'auracle']) 23 | self.assertEqual(0, r.process.returncode) 24 | self.assertEqual('auracle-git', r.process.stdout.decode().strip()) 25 | 26 | self.assertCountEqual([ 27 | '/rpc/v5/search/aurac?by=name-desc', 28 | '/rpc/v5/search/le-git?by=name-desc', 29 | '/rpc/v5/search/auracle?by=name-desc', 30 | ], r.request_uris) 31 | 32 | 33 | if __name__ == '__main__': 34 | auracle_test.main() 35 | -------------------------------------------------------------------------------- /tests/test_resolve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | 6 | 7 | class TestResolve(auracle_test.TestCase): 8 | 9 | def testUnversionedDependency(self): 10 | r = self.Auracle(['resolve', '-q', 'curl']) 11 | self.assertEqual(0, r.process.returncode) 12 | 13 | self.assertCountEqual([ 14 | 'curl-c-ares', 'curl-git', 'curl-http3-ngtcp2', 'curl-quiche-git' 15 | ], 16 | r.process.stdout.decode().splitlines()) 17 | 18 | def testVersionedDependencyAtLeast(self): 19 | r = self.Auracle(['resolve', '-q', 'curl>8']) 20 | self.assertEqual(0, r.process.returncode) 21 | 22 | # curl-c-ares provides curl, but not a versioned curl. 23 | self.assertCountEqual( 24 | ['curl-git', 'curl-http3-ngtcp2', 'curl-quiche-git'], 25 | r.process.stdout.decode().splitlines()) 26 | 27 | def testVersionedDependencyEquals(self): 28 | r = self.Auracle(['resolve', '-q', 'curl=8.7.1.r201.gc8e0cd1de8']) 29 | self.assertEqual(0, r.process.returncode) 30 | 31 | self.assertCountEqual(['curl-git'], 32 | r.process.stdout.decode().splitlines()) 33 | 34 | def testMultipleDependencies(self): 35 | r = self.Auracle(['resolve', '-q', 'pacman=6.1.0', 'curl>8.7.1']) 36 | self.assertEqual(0, r.process.returncode) 37 | self.assertCountEqual(['pacman-git', 'curl-git'], 38 | r.process.stdout.decode().splitlines()) 39 | 40 | def testMultipleOverlappingDependencies(self): 41 | r = self.Auracle(['resolve', '-q', 'curl', 'curl>8']) 42 | self.assertEqual(0, r.process.returncode) 43 | self.assertCountEqual([ 44 | 'curl-c-ares', 'curl-git', 'curl-http3-ngtcp2', 'curl-quiche-git' 45 | ], 46 | r.process.stdout.decode().splitlines()) 47 | 48 | def testNoProvidersFound(self): 49 | r = self.Auracle(['resolve', 'curl=42']) 50 | self.assertEqual(0, r.process.returncode) 51 | 52 | 53 | if __name__ == '__main__': 54 | auracle_test.main() 55 | -------------------------------------------------------------------------------- /tests/test_search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | import json 6 | 7 | 8 | class TestSearch(auracle_test.TestCase): 9 | 10 | def testExitSuccessOnNoResults(self): 11 | r = self.Auracle(['search', 'wontfindanypackages']) 12 | self.assertEqual(0, r.process.returncode) 13 | self.assertEqual('', r.process.stdout.decode()) 14 | 15 | def testExitFailureOnAurError(self): 16 | r = self.Auracle(['search', 'git']) 17 | self.assertNotEqual(0, r.process.returncode) 18 | 19 | def testSearchByDimensions(self): 20 | dimensions = [ 21 | 'name', 22 | 'name-desc', 23 | 'maintainer', 24 | 'comaintainers', 25 | 'depends', 26 | 'makedepends', 27 | 'optdepends', 28 | 'checkdepends', 29 | 'provides', 30 | 'replaces', 31 | 'conflicts', 32 | ] 33 | 34 | for dim in dimensions: 35 | r = self.Auracle(['search', '--searchby', dim, 'somesearchterm']) 36 | self.assertEqual(0, r.process.returncode) 37 | 38 | self.assertEqual(1, len(r.requests_sent)) 39 | self.assertIn(f'by={dim}', r.requests_sent[0].path) 40 | 41 | for dim in dimensions[2:]: 42 | r = self.Auracle(['search', '--searchby', dim, 'libfoo.so']) 43 | self.assertEqual(0, r.process.returncode) 44 | 45 | self.assertEqual(1, len(r.requests_sent)) 46 | self.assertIn('/libfoo.so', r.requests_sent[0].path) 47 | self.assertIn(f'by={dim}', r.requests_sent[0].path) 48 | 49 | def testSearchByInvalidDimension(self): 50 | r = self.Auracle(['search', '--searchby=notvalid', 'somesearchterm']) 51 | self.assertNotEqual(0, r.process.returncode) 52 | 53 | def testResultsAreUnique(self): 54 | # search once to get the expected count 55 | r1 = self.Auracle(['search', '--quiet', 'aura']) 56 | packagecount = len(r1.process.stdout.decode().splitlines()) 57 | self.assertGreater(packagecount, 0) 58 | 59 | # search again with the term duplicated. two requests are made, but the 60 | # resultcount is the same as the results are deduped. 61 | r2 = self.Auracle(['search', '--quiet', 'aura', 'aura']) 62 | self.assertCountEqual([ 63 | '/rpc/v5/search/aura?by=name-desc', 64 | '/rpc/v5/search/aura?by=name-desc', 65 | ], r2.request_uris) 66 | self.assertEqual(packagecount, 67 | len(r2.process.stdout.decode().splitlines())) 68 | 69 | def testLiteralSearch(self): 70 | r = self.Auracle(['search', '--literal', '^aurac.+']) 71 | self.assertEqual(0, r.process.returncode) 72 | 73 | self.assertListEqual([ 74 | '/rpc/v5/search/^aurac.+?by=name-desc', 75 | ], r.request_uris) 76 | 77 | def testLiteralSearchWithShortTerm(self): 78 | r = self.Auracle(['search', '--literal', 'a']) 79 | self.assertEqual(1, r.process.returncode) 80 | 81 | self.assertListEqual([ 82 | '/rpc/v5/search/a?by=name-desc', 83 | ], r.request_uris) 84 | 85 | 86 | if __name__ == '__main__': 87 | auracle_test.main() 88 | -------------------------------------------------------------------------------- /tests/test_show.py: -------------------------------------------------------------------------------- 1 | import auracle_test 2 | # SPDX-License-Identifier: MIT 3 | 4 | 5 | class TestPkgbuild(auracle_test.TestCase): 6 | 7 | def testSinglePkgbuild(self): 8 | r = self.Auracle(['show', 'auracle-git']) 9 | self.assertEqual(0, r.process.returncode) 10 | 11 | pkgbuild = r.process.stdout.decode() 12 | 13 | self.assertIn('pkgname=auracle-git', pkgbuild) 14 | 15 | self.assertCountEqual( 16 | ['/rpc/v5/info', '/cgit/aur.git/plain/PKGBUILD?h=auracle-git'], 17 | r.request_uris) 18 | 19 | def testMultiplePkgbuilds(self): 20 | r = self.Auracle(['show', 'auracle-git', 'pkgfile-git']) 21 | self.assertEqual(0, r.process.returncode) 22 | 23 | pkgbuilds = r.process.stdout.decode() 24 | 25 | self.assertIn('### BEGIN auracle-git/PKGBUILD', pkgbuilds) 26 | self.assertIn('### BEGIN pkgfile-git/PKGBUILD', pkgbuilds) 27 | 28 | def testPkgbuildNotFound(self): 29 | r = self.Auracle(['show', 'totesnotfoundpackage']) 30 | self.assertNotEqual(0, r.process.returncode) 31 | 32 | self.assertCountEqual([ 33 | '/rpc/v5/info', 34 | ], r.request_uris) 35 | 36 | def testFileNotFound(self): 37 | r = self.Auracle(['show', '--show-file=NOTAPKGBUILD', 'auracle-git']) 38 | self.assertNotEqual(0, r.process.returncode) 39 | 40 | self.assertIn('not found for package', r.process.stderr.decode()) 41 | 42 | 43 | if __name__ == '__main__': 44 | auracle_test.main() 45 | -------------------------------------------------------------------------------- /tests/test_sort.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | 6 | 7 | class SortTest(auracle_test.TestCase): 8 | 9 | def testSortInfoByPopularity(self): 10 | r = self.Auracle([ 11 | '--sort', 'popularity', 'info', 'auracle-git', 'pkgfile-git', 12 | 'nlohmann-json' 13 | ]) 14 | self.assertEqual(0, r.process.returncode) 15 | 16 | v = [] 17 | for line in r.process.stdout.decode().splitlines(): 18 | if line.startswith('Popularity'): 19 | v.append(float(line.rsplit(':')[-1].strip())) 20 | 21 | self.assertTrue(all(v[i] <= v[i + 1] for i in range(len(v) - 1))) 22 | 23 | def testRSortInfoByPopularity(self): 24 | r = self.Auracle([ 25 | '--rsort', 'popularity', 'search', 'auracle-git', 'pkgfile-git', 26 | 'nlohmann-json' 27 | ]) 28 | self.assertEqual(0, r.process.returncode) 29 | 30 | v = [] 31 | for line in r.process.stdout.decode().splitlines(): 32 | if line.startswith('Popularity'): 33 | v.append(float(line.rsplit(':')[-3].strip())) 34 | 35 | self.assertTrue(all(v[i] >= v[i + 1] for i in range(len(v) - 1))) 36 | 37 | def testSortSearchByVotes(self): 38 | r = self.Auracle([ 39 | '--sort', 'votes', 'search', '--searchby=maintainer', 'falconindy' 40 | ]) 41 | self.assertEqual(0, r.process.returncode) 42 | 43 | v = [] 44 | for line in r.process.stdout.decode().splitlines(): 45 | if line.startswith('aur/'): 46 | v.append(int(line.split()[2][1:-1])) 47 | 48 | self.assertTrue(all(v[i] <= v[i + 1] for i in range(len(v) - 1))) 49 | 50 | def testRSortSearchByVotes(self): 51 | r = self.Auracle([ 52 | '--rsort', 'votes', 'search', '--searchby=maintainer', 'falconindy' 53 | ]) 54 | self.assertEqual(0, r.process.returncode) 55 | 56 | v = [] 57 | for line in r.process.stdout.decode().splitlines(): 58 | if line.startswith('aur/'): 59 | v.append(int(line.split()[2][1:-1])) 60 | 61 | self.assertTrue(all(v[i] >= v[i + 1] for i in range(len(v) - 1))) 62 | 63 | def testSortByInvalidKey(self): 64 | r = self.Auracle(['--sort', 'nonsense', 'search', 'aura']) 65 | self.assertNotEqual(0, r.process.returncode) 66 | self.assertCountEqual([], r.requests_sent) 67 | 68 | def testRSortByInvalidKey(self): 69 | r = self.Auracle(['--rsort', 'nonsense', 'search', 'aura']) 70 | self.assertNotEqual(0, r.process.returncode) 71 | self.assertCountEqual([], r.requests_sent) 72 | 73 | 74 | if __name__ == '__main__': 75 | auracle_test.main() 76 | -------------------------------------------------------------------------------- /tests/test_update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: MIT 3 | 4 | import auracle_test 5 | 6 | 7 | class TestDownload(auracle_test.TestCase): 8 | 9 | def testOutdatedFindsPackagesNeedingUpgrade(self): 10 | r = self.Auracle(['update', '--quiet']) 11 | self.assertEqual(0, r.process.returncode) 12 | self.assertPkgbuildExists('auracle-git') 13 | self.assertPkgbuildExists('pkgfile-git') 14 | 15 | def testOutdatedFiltersUpdatesToArgs(self): 16 | r = self.Auracle(['update', 'auracle-git']) 17 | self.assertEqual(0, r.process.returncode) 18 | self.assertPkgbuildExists('auracle-git') 19 | 20 | def testExitsNonZeroWithoutUpgrades(self): 21 | r = self.Auracle(['update', 'ocaml']) 22 | self.assertEqual(1, r.process.returncode) 23 | 24 | 25 | if __name__ == '__main__': 26 | auracle_test.main() 27 | --------------------------------------------------------------------------------