ybox 0.9.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. ybox/__init__.py +2 -0
  2. ybox/cmd.py +307 -0
  3. ybox/conf/completions/ybox.fish +93 -0
  4. ybox/conf/distros/arch/add-gpg-key.sh +29 -0
  5. ybox/conf/distros/arch/distro.ini +192 -0
  6. ybox/conf/distros/arch/init-base.sh +10 -0
  7. ybox/conf/distros/arch/init-user.sh +35 -0
  8. ybox/conf/distros/arch/init.sh +82 -0
  9. ybox/conf/distros/arch/list_fmt_long.py +76 -0
  10. ybox/conf/distros/arch/pkgdeps.py +276 -0
  11. ybox/conf/distros/deb-generic/check-package.sh +77 -0
  12. ybox/conf/distros/deb-generic/distro.ini +190 -0
  13. ybox/conf/distros/deb-generic/fetch-gpg-key-id.sh +30 -0
  14. ybox/conf/distros/deb-generic/init-base.sh +11 -0
  15. ybox/conf/distros/deb-generic/init-user.sh +3 -0
  16. ybox/conf/distros/deb-generic/init.sh +136 -0
  17. ybox/conf/distros/deb-generic/list_fmt_long.py +114 -0
  18. ybox/conf/distros/deb-generic/pkgdeps.py +208 -0
  19. ybox/conf/distros/deb-oldstable/distro.ini +21 -0
  20. ybox/conf/distros/deb-stable/distro.ini +21 -0
  21. ybox/conf/distros/supported.list +5 -0
  22. ybox/conf/distros/ubuntu2204/distro.ini +21 -0
  23. ybox/conf/distros/ubuntu2404/distro.ini +21 -0
  24. ybox/conf/profiles/apps.ini +26 -0
  25. ybox/conf/profiles/basic.ini +310 -0
  26. ybox/conf/profiles/dev.ini +25 -0
  27. ybox/conf/profiles/games.ini +39 -0
  28. ybox/conf/resources/entrypoint-base.sh +170 -0
  29. ybox/conf/resources/entrypoint-common.sh +23 -0
  30. ybox/conf/resources/entrypoint-cp.sh +32 -0
  31. ybox/conf/resources/entrypoint-root.sh +20 -0
  32. ybox/conf/resources/entrypoint-user.sh +21 -0
  33. ybox/conf/resources/entrypoint.sh +249 -0
  34. ybox/conf/resources/prime-run +13 -0
  35. ybox/conf/resources/run-in-dir +60 -0
  36. ybox/conf/resources/run-user-bash-cmd +14 -0
  37. ybox/config.py +255 -0
  38. ybox/env.py +205 -0
  39. ybox/filelock.py +77 -0
  40. ybox/migrate/0.9.0-0.9.7:0.9.8.py +33 -0
  41. ybox/pkg/__init__.py +0 -0
  42. ybox/pkg/clean.py +33 -0
  43. ybox/pkg/info.py +40 -0
  44. ybox/pkg/inst.py +638 -0
  45. ybox/pkg/list.py +191 -0
  46. ybox/pkg/mark.py +68 -0
  47. ybox/pkg/repair.py +150 -0
  48. ybox/pkg/repo.py +251 -0
  49. ybox/pkg/search.py +52 -0
  50. ybox/pkg/uninst.py +92 -0
  51. ybox/pkg/update.py +56 -0
  52. ybox/print.py +121 -0
  53. ybox/run/__init__.py +0 -0
  54. ybox/run/cmd.py +54 -0
  55. ybox/run/control.py +102 -0
  56. ybox/run/create.py +1116 -0
  57. ybox/run/destroy.py +64 -0
  58. ybox/run/graphics.py +367 -0
  59. ybox/run/logs.py +57 -0
  60. ybox/run/ls.py +64 -0
  61. ybox/run/pkg.py +445 -0
  62. ybox/schema/0.9.1-added.sql +27 -0
  63. ybox/schema/0.9.6-added.sql +18 -0
  64. ybox/schema/init.sql +39 -0
  65. ybox/schema/migrate/0.9.0:0.9.1.sql +42 -0
  66. ybox/schema/migrate/0.9.1:0.9.2.sql +8 -0
  67. ybox/schema/migrate/0.9.2:0.9.3.sql +2 -0
  68. ybox/schema/migrate/0.9.5:0.9.6.sql +2 -0
  69. ybox/state.py +914 -0
  70. ybox/util.py +351 -0
  71. ybox-0.9.8.dist-info/LICENSE +19 -0
  72. ybox-0.9.8.dist-info/METADATA +533 -0
  73. ybox-0.9.8.dist-info/RECORD +76 -0
  74. ybox-0.9.8.dist-info/WHEEL +5 -0
  75. ybox-0.9.8.dist-info/entry_points.txt +8 -0
  76. ybox-0.9.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+
7
+ source "$SCRIPT_DIR/entrypoint-common.sh"
8
+
9
+ current_user="$(id -un)"
10
+ # install binaries for paru from paru-bin (paru takes too long to compile)
11
+ PARU="paru --noconfirm"
12
+ echo_color "$fg_cyan" "Installing AUR helper 'paru'" >> $status_file
13
+ export HOME="$(eval echo "~$current_user")"
14
+ cd ~
15
+ rm -rf paru-bin
16
+ git clone https://aur.archlinux.org/paru-bin.git
17
+ cd paru-bin
18
+ makepkg --noconfirm -si
19
+ cd ..
20
+ if [ -n "$EXTRA_PKGS" ]; then
21
+ echo_color "$fg_cyan" "Installing $EXTRA_PKGS" >> $status_file
22
+ $PARU -S --needed $EXTRA_PKGS
23
+ fi
24
+ echo_color "$fg_cyan" "Clearing package cache and updating packages" >> $status_file
25
+ yes | paru -Sccd
26
+ $PARU -Syu
27
+ rm -rf paru-bin
28
+
29
+ # add current user to realtime group (if present)
30
+ # (does not work with rootless docker and is likely ineffective even if it does seem to work)
31
+ #if getent group realtime 2>/dev/null >/dev/null; then
32
+ # echo_color "$fg_cyan" "Adding '$current_user' and root to realtime group" >> $status_file
33
+ # sudo usermod -aG realtime $current_user
34
+ # sudo usermod -aG realtime root
35
+ #fi
@@ -0,0 +1,82 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+
7
+ source "$SCRIPT_DIR/entrypoint-common.sh"
8
+
9
+ # pacman configuration
10
+ PAC="pacman --noconfirm"
11
+ echo_color "$fg_cyan" "Configuring pacman" >> $status_file
12
+ pacman-key --init
13
+ sed -i 's/^#[ ]*Color.*/Color/;s/^[ ]*NoProgressBar.*/#NoProgressBar/' /etc/pacman.conf
14
+ sed -i 's,^[ ]*NoExtract[ ]*=[ ]*/\?usr/share/man.*,#\0,' /etc/pacman.conf
15
+ sed -i 's,^[ ]*NoExtract[ ]*=[ ]*.*usr/share/i18n.*,#\0,' /etc/pacman.conf
16
+ # disable the sandbox in the newer pacman versions that does not work in container
17
+ sed -i 's/^#[ ]*DisableSandbox/DisableSandbox/' /etc/pacman.conf
18
+ if ! grep -q '^[ ]*\[multilib\]' /etc/pacman.conf; then
19
+ echo -e '[multilib]\nInclude = /etc/pacman.d/mirrorlist' >> /etc/pacman.conf
20
+ fi
21
+ $PAC -Sy archlinux-keyring
22
+
23
+ # generate the configured locale and assume it is UTF-8
24
+ if [ -n "$LANG" -a "$LANG" != "C.UTF-8" ] && ! grep -q "^$LANG UTF-8" /etc/locale.gen; then
25
+ echo_color "$fg_cyan" "Configuring locale" >> $status_file
26
+ echo "$LANG UTF-8" >> /etc/locale.gen
27
+ # always add en_US.UTF-8 regardless since some apps seem to depend on it
28
+ if [ "$LANG" != "en_US.UTF-8" ]; then
29
+ echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen
30
+ fi
31
+ if ! locale-gen; then
32
+ # reinstall glibc to obtain /usr/share/i18/locales/* files and try again
33
+ $PAC -Sy
34
+ $PAC -S glibc
35
+ if ! locale-gen; then
36
+ echo_color "$fg_red" "FAILED to generate locale for $LANG, fallback to en_US.UTF-8" >> $status_file
37
+ export LANG=en_US.UTF-8
38
+ export LANGUAGE="en_US:en"
39
+ fi
40
+ fi
41
+ echo "LANG=$LANG" > /etc/locale.conf
42
+ if [ -n "$LANGUAGE" ]; then
43
+ echo "LANGUAGE=\"$LANGUAGE\"" >> /etc/locale.conf
44
+ fi
45
+ fi
46
+
47
+ # setup fastest mirrors and update the installation
48
+ if [ -n "$CONFIGURE_FASTEST_MIRRORS" ] && ! pacman -Qq reflector 2>/dev/null >/dev/null; then
49
+ echo_color "$fg_cyan" "Installing reflector and searching for the fastest mirrors" >> $status_file
50
+ $PAC -Syy
51
+ $PAC -S --needed reflector
52
+ sed -i 's/^--latest.*/--latest 30\n--number 5\n--threads 5/' /etc/xdg/reflector/reflector.conf
53
+ sed -i 's/^--sort.*/--sort rate/' /etc/xdg/reflector/reflector.conf
54
+ reflector @/etc/xdg/reflector/reflector.conf || true
55
+ fi
56
+ $PAC -Syu
57
+
58
+ # install packages most users will need for working comfortably
59
+ echo_color "$fg_cyan" "Installing base set of packages" >> $status_file
60
+ $PAC -S --needed $REQUIRED_PKGS $RECOMMENDED_PKGS $SUGGESTED_PKGS
61
+ $PAC -S --needed --asdeps $REQUIRED_DEPS $RECOMMENDED_DEPS $SUGGESTED_DEPS
62
+
63
+ echo_color "$fg_cyan" "Configuring makepkg and system-wide bashrc" >> $status_file
64
+ # use reasonable MAKEFLAGS and zstd compression level for AUR packages
65
+ sed -i "s/^#MAKEFLAGS=.*/MAKEFLAGS=\"-j`/usr/bin/nproc --all`\"/" /etc/makepkg.conf
66
+ sed -i 's/^COMPRESSZST=.*/COMPRESSZST=(zstd -c -T0 -8 -)/' /etc/makepkg.conf
67
+ # remove debug from options
68
+ sed -i 's/^OPTIONS\(.*\b[^!]\)debug/OPTIONS\1!debug/' /etc/makepkg.conf
69
+
70
+ # common environment variables
71
+ if ! grep -q '^export EDITOR=' /etc/bash.bashrc && $PAC -Qq neovim 2>/dev/null >/dev/null; then
72
+ echo -e '\nexport EDITOR=nvim\nexport VISUAL=nvim' >> /etc/bash.bashrc
73
+ fi
74
+ if ! grep -q '^export LESSOPEN=' /etc/bash.bashrc && $PAC -Qq lesspipe 2>/dev/null >/dev/null; then
75
+ echo -e '\nexport PAGER="less -RL"\nexport LESSOPEN="|/usr/bin/lesspipe.sh %s"' >> /etc/bash.bashrc
76
+ fi
77
+ if ! grep -q '^export LANG=' /etc/bash.bashrc && [ -n "$LANG" -a "$LANG" != "C.UTF-8" ]; then
78
+ echo -e "\nexport LANG=$LANG" >> /etc/bash.bashrc
79
+ if [ -n "$LANGUAGE" ]; then
80
+ echo "export LANGUAGE=\"$LANGUAGE\"" >> /etc/bash.bashrc
81
+ fi
82
+ fi
@@ -0,0 +1,76 @@
1
+ """
2
+ Format output of `pacman -Qi ...` as a table having four columns (Name, Version, Dependency Of,
3
+ Description) string-separated fields using a user provided separator.
4
+ """
5
+
6
+ import argparse
7
+ import re
8
+ import sys
9
+ from collections import defaultdict
10
+
11
+ _VAL_RE = re.compile(r"\s*([^:]+?)\s*:\s*(.*?)\s*")
12
+ _WS_RE = re.compile(r"\s{2,}")
13
+
14
+
15
+ def parse_separator() -> str:
16
+ """expect a single argument which will be used as the separator between the fields"""
17
+ parser = argparse.ArgumentParser(description="Format output of 'pacman -Qi ...' into a table.")
18
+ parser.add_argument("separator", type=str,
19
+ help="separator to use between the fields of the output")
20
+ args = parser.parse_args()
21
+ return args.separator
22
+
23
+
24
+ def format_dep_of(req_by: list[str], opt_for: list[str]) -> str:
25
+ """format the `Dependency Of` column to include the required and optional dependencies"""
26
+ if req_by:
27
+ req_by = [] if req_by[0] == "None" else [_WS_RE.sub(" ", req) for req in req_by]
28
+ if opt_for:
29
+ opt_for = [] if opt_for[0] == "None" else [_WS_RE.sub(" ", opt) for opt in opt_for]
30
+
31
+ dep_of_parts: list[str] = []
32
+ if req_by:
33
+ dep_of_parts.extend(("req(", *req_by, ")"))
34
+ if opt_for:
35
+ if req_by:
36
+ dep_of_parts.append(",")
37
+ dep_of_parts.extend(("opt(", *opt_for, ")"))
38
+ return "".join(dep_of_parts)
39
+
40
+
41
+ def process() -> None:
42
+ """process pacman output on stdin to create fields separated by a given separator"""
43
+ sep = parse_separator()
44
+ pkg_map: defaultdict[str, list[str]] = defaultdict(list[str])
45
+ key = ""
46
+
47
+ def k_val(key: str) -> str:
48
+ """return the value of the key in `pkg_map` as a single string"""
49
+ return "".join(pkg_map[key])
50
+
51
+ def format_package() -> None:
52
+ """format details of a package as a table or a plain line with given separator"""
53
+ dep_of = format_dep_of(pkg_map["Required By"], pkg_map["Optional For"])
54
+ print(f"{k_val('Name')}{sep}{k_val('Version')}{sep}{dep_of}{sep}{k_val('Description')}")
55
+
56
+ for line in sys.stdin:
57
+ if (match := _VAL_RE.fullmatch(line)):
58
+ key, value = match.groups()
59
+ if key == "Name" and pkg_map:
60
+ # indicates start of fields of a new package, so output previous one and clear
61
+ format_package()
62
+ pkg_map.clear()
63
+ pkg_map[key].append(value)
64
+ elif line and line[0].isspace():
65
+ # "Description", "Required By" and "Optional For" can have multiline output
66
+ val_list = pkg_map[key]
67
+ # add space separately to avoid adding it in `format_dep_of``
68
+ val_list.append(" ")
69
+ val_list.append(line.strip())
70
+ # output the last package
71
+ if pkg_map:
72
+ format_package()
73
+
74
+
75
+ if __name__ == "__main__":
76
+ process()
@@ -0,0 +1,276 @@
1
+ """
2
+ Show the optional dependencies for a package that may be in a pacman repository or the AUR.
3
+ The output is in the format:
4
+
5
+ {header}
6
+ {prefix}<name>{separator}<level>{separator}<order>{separator}<installed>{separator}<description>
7
+
8
+ where:
9
+ * <name>: name of the optional dependency
10
+ * <level>: level of the dependency i.e. 1 for direct dependency, 2 for dependency of dependency
11
+ and so on; resolution of level > 2 is not required since caller currently ignores those
12
+ * <order>: this is a simple counter assigned to the dependencies where the value itself is of no
13
+ significance but if multiple dependencies have the same value then it means that they
14
+ are ORed dependencies and only one of them should normlly be selected for installation
15
+ * <installed>: true if the dependency already installed and false otherwise
16
+ * <description>: detailed description of the dependency; it can contain literal \n to indicate
17
+ newlines in the description
18
+ """
19
+
20
+ import gzip
21
+ import os
22
+ import re
23
+ import sys
24
+ import time
25
+ import zlib
26
+ from collections import defaultdict
27
+ from pathlib import Path
28
+ from typing import Optional
29
+
30
+ import ijson # type: ignore
31
+
32
+ from ybox.cmd import parse_opt_deps_args, run_command
33
+ from ybox.config import Consts
34
+ from ybox.print import print_error, print_notice, print_warn
35
+
36
+ _AUR_META_URL = "https://aur.archlinux.org/packages-meta-ext-v1.json.gz"
37
+ _PKG_CACHE_SUBDIR = os.path.basename(__file__).removesuffix(".py")
38
+ _AUR_META_CACHE_DIR = f"{os.path.expanduser('~/.cache')}/{_PKG_CACHE_SUBDIR}"
39
+ _AUR_META_FILE = f"{_AUR_META_CACHE_DIR}/packages-meta-ext-v1.json.gz"
40
+ # parallel download using aria2 is much faster on slower networks
41
+ _FETCH_AUR_META = f"/usr/bin/aria2c -x8 -j8 -s8 -k1M -d{_AUR_META_CACHE_DIR} {_AUR_META_URL}"
42
+ _REFRESH_AGE = 24.0 * 60 * 60 # consider AUR metadata file as stale after a day
43
+ _PACKAGE_NAME_RE = re.compile(r"^[\w@.+-]+") # used to strip out version comparisons
44
+
45
+ # fields: name of original package, description, required dependencies, optional dependencies
46
+ PackageAlternate = tuple[str, str, list[str], list[str]]
47
+
48
+
49
+ def main() -> None:
50
+ """main function for `pkgdeps.py` script"""
51
+ main_argv(sys.argv[1:])
52
+
53
+
54
+ def main_argv(argv: list[str]) -> None:
55
+ """
56
+ Main entrypoint of `pkgdeps.py` that takes a list of arguments which are usually the
57
+ command-line arguments of the `main()` function. Pass ["-h"]/["--help"] to see all the
58
+ available arguments with help message for each.
59
+
60
+ :param argv: arguments to the function (main function passes `sys.argv[1:]`)
61
+ """
62
+ args = parse_opt_deps_args(argv)
63
+
64
+ print_notice(f"Searching dependencies of '{args.package}' in base Arch repositories")
65
+ # first get the list of all installed packages to eliminate installed packages
66
+ # (include their provides too)
67
+ installed_packages = set(
68
+ str(run_command(r"/usr/bin/expac %n\t%S", capture_output=True)).split())
69
+ # next build a map of all packages in pacman database from their provides, dependencies and
70
+ # optional dependencies; the map key will be original package name and their provides mapped
71
+ # to a tuple having the name, description with list of required and optional dependencies
72
+ sep: str = args.separator
73
+ all_packages = build_pacman_db_map(sep)
74
+
75
+ opt_deps: dict[str, tuple[str, int, bool]] = {}
76
+ find_opt_deps(args.package, installed_packages, all_packages, opt_deps, args.level)
77
+ # columns below are expected by ybox-pkg
78
+ if opt_deps:
79
+ if args.header:
80
+ print(args.header)
81
+ prefix = args.prefix
82
+ for key, val in opt_deps.items():
83
+ desc, level, installed = val
84
+ print(f"{prefix}{key}{sep}{level}{sep}{installed}{sep}{desc}")
85
+
86
+
87
+ def build_pacman_db_map(sep: str) -> defaultdict[str, list[PackageAlternate]]:
88
+ """
89
+ Build a map of all packages in pacman repositories returning a `defaultdict` having name of
90
+ package as the key with a list of `PackageAlternate` objects as values which captures
91
+ the name, description, required depedencies and optional dependencies in the tuple.
92
+ The first element element in the list is always the package itself while subsequent ones
93
+ are all packages that provide the same package, if any.
94
+
95
+ This is actually faster than querying the sync database multiple times (at least twice)
96
+ using pacman/expac for the package and its optional dependencies since the sync databases are
97
+ just a tarball of the packages that have to be read in entirety either way.
98
+
99
+ :param sep: the separator string to use between the output fields of `expac`
100
+ :return: a `defaultdict` having package names as keys with list of `PackageAlternate` values
101
+ """
102
+ arch_packages = defaultdict[str, list[PackageAlternate]](list[PackageAlternate])
103
+ for package_list in str(run_command(f"/usr/bin/expac -S %n{sep}%S{sep}%E{sep}%o{sep}%d",
104
+ capture_output=True)).splitlines():
105
+ if not package_list:
106
+ continue
107
+ name, provides, required, optional, desc = package_list.split(sep)
108
+ deps = _process_pkg_names(required.split()) if required else []
109
+ opt_deps = _process_pkg_names(optional.split()) if optional else []
110
+ # arch linux packages are always lower case which is enforced below for the map
111
+ map_val = (name.lower(), desc, deps, opt_deps)
112
+ arch_packages[map_val[0]].append(map_val)
113
+ for provide in _process_pkg_names(provides.split()):
114
+ arch_packages[provide].append(map_val)
115
+ return arch_packages
116
+
117
+
118
+ def _process_pkg_names(pkgs: Optional[list[str]]) -> list[str]:
119
+ """internal method to strip out versions from each package name of a list of package names"""
120
+ return [m.group(0) for pkg in pkgs if (m := _PACKAGE_NAME_RE.match(pkg))] if pkgs else []
121
+
122
+
123
+ def refresh_aur_metadata(raise_error: bool) -> bool:
124
+ """
125
+ refresh AUR metadata having details on all available AUR packages which is refreshed if it
126
+ is missing or older than 24 hours
127
+ """
128
+ os.makedirs(_AUR_META_CACHE_DIR, mode=Consts.default_directory_mode(), exist_ok=True)
129
+ # fetch AUR metadata if not present or older than a day
130
+ if (not os.access(_AUR_META_FILE, os.R_OK) or
131
+ time.time() > os.path.getctime(_AUR_META_FILE) + _REFRESH_AGE):
132
+ meta_file = Path(_AUR_META_FILE)
133
+ meta_file.unlink(missing_ok=True)
134
+ # delete any partial file in case of download failure
135
+ if (code := int(run_command(_FETCH_AUR_META, exit_on_error=False))) != 0:
136
+ meta_file.unlink(missing_ok=True)
137
+ if raise_error:
138
+ raise RuntimeError(f"Download of AUR metadata failed with exit code {code}")
139
+ return False
140
+ return True
141
+
142
+ # using ijson instead of the standard json because latter always loads the entire JSON in memory
143
+ # whereas only a few fields are required for the map, and hence using ijson is a bit faster as
144
+ # well as far less memory consuming
145
+
146
+
147
+ def build_aur_db_map(aur_packages: defaultdict[str, list[PackageAlternate]],
148
+ raise_error: bool) -> bool:
149
+ """
150
+ Like :func:`build_pacman_db_map` this will build a map of package names to a list of
151
+ `PackageAlternate` objects which contains the name, description, required dependencies and
152
+ optional dependencies of the package as well as any packages that provide this package.
153
+ The result is added to the passed `aur_packages` argument which should be a `defaultdict`.
154
+
155
+ This downloads the AUR metadata explicitly and builds the map from the downloaded file.
156
+ The AUR metadata is refreshed if it is missing or older than 24 hours.
157
+ The alternative of using paru/yay to dump information of all available packages is much much
158
+ slower. Querying using paru/yay for virtual packages in AUR database does not work since they
159
+ maintain just the names of AUR packages locally, so cannot just query the optional deps.
160
+
161
+ :param aur_packages: a `defaultdict` which will be populated wih the package names as keys
162
+ and list of `PackageAlternate` objects as the values
163
+ :param raise_error: if True, then raise an error if there was one while reading the AUR
164
+ metadata else the method will return a boolean indicating success/failure
165
+ :return: True if `aur_packages` was successfully populated and False if there was an error
166
+ in reading the fetched AUR metadata
167
+ """
168
+ try:
169
+ with gzip.open(_AUR_META_FILE, mode="rb") as aur_meta:
170
+ for package in ijson.items(aur_meta, "item", use_float=True):
171
+ desc = package.get("Description") or ""
172
+ deps = _process_pkg_names(package.get("Depends"))
173
+ opt_deps = _process_pkg_names(package.get("OptDepends"))
174
+ # arch linux packages are always lower case which is enforced below for the map
175
+ map_val = (package.get("Name").lower(), desc, deps, opt_deps)
176
+ aur_packages[map_val[0]].append(map_val)
177
+ for provide in _process_pkg_names(package.get("Provides")):
178
+ aur_packages[provide].append(map_val)
179
+ return True
180
+ except (gzip.BadGzipFile, EOFError, zlib.error, ijson.JSONError):
181
+ if raise_error:
182
+ raise
183
+ return False
184
+
185
+
186
+ def find_opt_deps(package: str, installed: set[str],
187
+ all_packages: defaultdict[str, list[PackageAlternate]],
188
+ opt_deps: dict[str, tuple[str, int, bool]], max_level: int,
189
+ level: int = 1) -> None:
190
+ """
191
+ Find and populate optional dependencies of a package till the given `level`. This will
192
+ recursively keep finding optional dependencies of all required and optional dependencies till
193
+ the `max_level` is not reached. For example, max_level=1 will obtain just the immediate
194
+ optional dependencies, max_level=2 will go one level down and also obtain any optional
195
+ dependencies of those as well as the required dependencies of the package, and so on.
196
+
197
+ The result is populated in the given `opt_deps` argument which is a dictionary having
198
+ package name as the key with a tuple having its description, the level it was found and
199
+ a boolean indicating whether the package is already installed or not.
200
+
201
+ :param package: name of the package whose optional dependencies have to be searched
202
+ :param installed: a set of all installed packages in the system
203
+ :param all_packages: a dictionary having all the available packages which includes those
204
+ in pacman as well as AUR repositories
205
+ :param opt_deps: a dictionary which will be populated with the result having dependency name
206
+ as the key with a tuple of description, level and whether package is installed
207
+ :param max_level: the maximum level to which the search will extend
208
+ :param level: the current level of search which will terminate if it exceed the `max_level`,
209
+ defaults to 1 which should be what external callers should use while it will
210
+ be incremented in recursive calls
211
+ """
212
+ if level > max_level:
213
+ return
214
+ # arch linux names are always lower case though sometimes upper case parts can appear
215
+ # in opt-depends field (e.g. 'hunspell-en_US' while package is 'hunspell-en_us')
216
+ package = package.lower()
217
+ # search all_packages to obtain the required and optional dependencies
218
+ if not (alternates := all_packages.get(package)):
219
+ if level == 1:
220
+ print_notice(f"Searching dependencies of '{package}' in AUR")
221
+ # fetch AUR metadata, populate into all_packages and try again
222
+ # else if download fails or AUR metadata file is broken, then refresh it and try again
223
+ if not refresh_aur_metadata(raise_error=False) or not build_aur_db_map(
224
+ all_packages, raise_error=False):
225
+ os.unlink(_AUR_META_FILE)
226
+ refresh_aur_metadata(raise_error=True)
227
+ build_aur_db_map(all_packages, raise_error=True)
228
+ alternates = all_packages.get(package)
229
+ if not alternates:
230
+ if level == 1:
231
+ print_error(f"Package '{package}' not found")
232
+ sys.exit(1)
233
+ else:
234
+ print_warn(f"Skipping unknown dependency '{package}'")
235
+ return
236
+
237
+ # choose the alternative with the same name else the first one
238
+ (_, _, required, opts) = search_alternates(package, alternates)
239
+
240
+ if opts:
241
+ for pkg in opts:
242
+ pkg = pkg.lower()
243
+ if pkg in opt_deps: # don't recurse on already encountered optional dependencies
244
+ continue
245
+ # lookup description
246
+ dep_desc = ""
247
+ if opt_dep := all_packages.get(pkg):
248
+ dep_desc = search_alternates(pkg, opt_dep)[1]
249
+ opt_deps[pkg] = (dep_desc, level, pkg in installed)
250
+ if not required:
251
+ return
252
+ for pkg in required:
253
+ if pkg not in installed:
254
+ find_opt_deps(pkg, installed, all_packages, opt_deps, max_level, level + 1)
255
+
256
+
257
+ def search_alternates(package_name: str, alternates: list[PackageAlternate]) -> PackageAlternate:
258
+ """
259
+ Search for a package in the given list of `PackageAlternate` objects, else if the package
260
+ is not found then the first element from the list is returned (which is the original package
261
+ name as populated by :func:`build_pacman_db_map` and :func:`build_aur_db_map`)
262
+
263
+ :param package_name: the package name to search
264
+ :param alternates: the list of `PackageAlternate` objects which is to be searched
265
+ :return: the `PackageAlternate` with matching package name or the first element in the
266
+ `alternates` list if the package was not found
267
+ """
268
+ # choose the alternative with the same name else the first one
269
+ for alternate in alternates:
270
+ if alternate[0] == package_name:
271
+ return alternate
272
+ return alternates[0]
273
+
274
+
275
+ if __name__ == "__main__":
276
+ main()
@@ -0,0 +1,77 @@
1
+ #!/bin/sh
2
+
3
+ # Script to list all matches for an installed package (or virtual package), and sort them by
4
+ # installation time with most recently installed packages first.
5
+
6
+ set -e
7
+
8
+ # dash is faster than bash, hence there are no bashisms in this script
9
+ # (e.g. uses `echo "$var" |` instead of `<<< "$var"`)
10
+
11
+ # Check if all arguments are provided
12
+ if [ "$1" = "installed" ]; then
13
+ grep_cmd=grep-status
14
+ elif [ "$1" = "available" ]; then
15
+ grep_cmd=grep-aptavail
16
+ fi
17
+ if [ $# -ne 2 -o -z "$grep_cmd" ]; then
18
+ echo "Usage: $0 (installed|available) <package>"
19
+ exit 1
20
+ fi
21
+
22
+ # ensure that only system paths are searched for all the system utilities
23
+ export PATH="/usr/sbin:/usr/bin:/sbin:/bin"
24
+
25
+ sys_arch="$(dpkg --print-architecture)"
26
+ # if package name has architecture, remove it for the command and match against the `Architecture`
27
+ # field in its output instead (or allow for `all` architecture)
28
+ echo "$2" | {
29
+ if IFS=':' read -r name expected_arch; then
30
+ pkgs="$($grep_cmd -FPackage,Provides -sPackage,Architecture -n -w "$name")"
31
+ if [ -z "$pkgs" ]; then
32
+ exit 1
33
+ fi
34
+ # if architecture is not provided for the package, then its the system architecture
35
+ expected_arch="${expected_arch:-$sys_arch}"
36
+ echo "$pkgs" | {
37
+ # output is in the format: package\narchitecture\n\n
38
+ while read -r pkg; do
39
+ read -r arch
40
+ read -r empty || true
41
+ if [ "$arch" = "$expected_arch" -o "$arch" = "all" ]; then
42
+ if [ "$arch" = "$sys_arch" -o "$arch" = "all" ]; then
43
+ pkg_files="/var/lib/dpkg/info/$pkg.list /var/lib/dpkg/info/$pkg:$arch.list"
44
+ else
45
+ pkg_files="/var/lib/dpkg/info/$pkg:$arch.list"
46
+ fi
47
+ if [ "$1" = "installed" ]; then
48
+ pkg_list="$pkg_list $pkg_files"
49
+ else
50
+ # skip installed packages when listing available packages
51
+ if [ -n "$(/bin/ls -1U $pkg_files 2>/dev/null || true)" ]; then
52
+ continue
53
+ fi
54
+ if [ "$arch" = "$sys_arch" ]; then
55
+ available_pkgs="$available_pkgs$pkg\n"
56
+ else
57
+ available_pkgs="$available_pkgs$pkg:$arch\n"
58
+ fi
59
+ fi
60
+ fi
61
+ done
62
+ if [ -z "$pkg_list" -a -z "$available_pkgs" ]; then
63
+ exit 1
64
+ fi
65
+ if [ "$1" = "installed" ]; then
66
+ # sort by installation time and remove system architecture name if present
67
+ /bin/ls -1t $pkg_list 2>/dev/null | sed "s|^.*/||;s|\(:$sys_arch\)\?.list$||"
68
+ else
69
+ /bin/echo -ne "$available_pkgs" # this prints newlines in dash as well as bash
70
+ fi
71
+ }
72
+ else
73
+ exit 1
74
+ fi
75
+ }
76
+
77
+ exit 0