├── .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 | 
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