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.
- ybox/__init__.py +2 -0
- ybox/cmd.py +307 -0
- ybox/conf/completions/ybox.fish +93 -0
- ybox/conf/distros/arch/add-gpg-key.sh +29 -0
- ybox/conf/distros/arch/distro.ini +192 -0
- ybox/conf/distros/arch/init-base.sh +10 -0
- ybox/conf/distros/arch/init-user.sh +35 -0
- ybox/conf/distros/arch/init.sh +82 -0
- ybox/conf/distros/arch/list_fmt_long.py +76 -0
- ybox/conf/distros/arch/pkgdeps.py +276 -0
- ybox/conf/distros/deb-generic/check-package.sh +77 -0
- ybox/conf/distros/deb-generic/distro.ini +190 -0
- ybox/conf/distros/deb-generic/fetch-gpg-key-id.sh +30 -0
- ybox/conf/distros/deb-generic/init-base.sh +11 -0
- ybox/conf/distros/deb-generic/init-user.sh +3 -0
- ybox/conf/distros/deb-generic/init.sh +136 -0
- ybox/conf/distros/deb-generic/list_fmt_long.py +114 -0
- ybox/conf/distros/deb-generic/pkgdeps.py +208 -0
- ybox/conf/distros/deb-oldstable/distro.ini +21 -0
- ybox/conf/distros/deb-stable/distro.ini +21 -0
- ybox/conf/distros/supported.list +5 -0
- ybox/conf/distros/ubuntu2204/distro.ini +21 -0
- ybox/conf/distros/ubuntu2404/distro.ini +21 -0
- ybox/conf/profiles/apps.ini +26 -0
- ybox/conf/profiles/basic.ini +310 -0
- ybox/conf/profiles/dev.ini +25 -0
- ybox/conf/profiles/games.ini +39 -0
- ybox/conf/resources/entrypoint-base.sh +170 -0
- ybox/conf/resources/entrypoint-common.sh +23 -0
- ybox/conf/resources/entrypoint-cp.sh +32 -0
- ybox/conf/resources/entrypoint-root.sh +20 -0
- ybox/conf/resources/entrypoint-user.sh +21 -0
- ybox/conf/resources/entrypoint.sh +249 -0
- ybox/conf/resources/prime-run +13 -0
- ybox/conf/resources/run-in-dir +60 -0
- ybox/conf/resources/run-user-bash-cmd +14 -0
- ybox/config.py +255 -0
- ybox/env.py +205 -0
- ybox/filelock.py +77 -0
- ybox/migrate/0.9.0-0.9.7:0.9.8.py +33 -0
- ybox/pkg/__init__.py +0 -0
- ybox/pkg/clean.py +33 -0
- ybox/pkg/info.py +40 -0
- ybox/pkg/inst.py +638 -0
- ybox/pkg/list.py +191 -0
- ybox/pkg/mark.py +68 -0
- ybox/pkg/repair.py +150 -0
- ybox/pkg/repo.py +251 -0
- ybox/pkg/search.py +52 -0
- ybox/pkg/uninst.py +92 -0
- ybox/pkg/update.py +56 -0
- ybox/print.py +121 -0
- ybox/run/__init__.py +0 -0
- ybox/run/cmd.py +54 -0
- ybox/run/control.py +102 -0
- ybox/run/create.py +1116 -0
- ybox/run/destroy.py +64 -0
- ybox/run/graphics.py +367 -0
- ybox/run/logs.py +57 -0
- ybox/run/ls.py +64 -0
- ybox/run/pkg.py +445 -0
- ybox/schema/0.9.1-added.sql +27 -0
- ybox/schema/0.9.6-added.sql +18 -0
- ybox/schema/init.sql +39 -0
- ybox/schema/migrate/0.9.0:0.9.1.sql +42 -0
- ybox/schema/migrate/0.9.1:0.9.2.sql +8 -0
- ybox/schema/migrate/0.9.2:0.9.3.sql +2 -0
- ybox/schema/migrate/0.9.5:0.9.6.sql +2 -0
- ybox/state.py +914 -0
- ybox/util.py +351 -0
- ybox-0.9.8.dist-info/LICENSE +19 -0
- ybox-0.9.8.dist-info/METADATA +533 -0
- ybox-0.9.8.dist-info/RECORD +76 -0
- ybox-0.9.8.dist-info/WHEEL +5 -0
- ybox-0.9.8.dist-info/entry_points.txt +8 -0
- 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
|