|
@@ -0,0 +1,583 @@
|
|
|
|
|
+#!/bin/sh
|
|
|
|
|
+# USM system-package-manager helper for pacman-based systems (Arch).
|
|
|
|
|
+#
|
|
|
|
|
+# Implements the USM SPM contract (see usm/README.md, "System package
|
|
|
|
|
+# manager integration") for Arch-style systems:
|
|
|
|
|
+#
|
|
|
|
|
+# usm-spm-pacman query <usm-ref>... -> contract JSON on STDOUT
|
|
|
|
|
+# usm-spm-pacman install <native-name>... -> contract JSONL events on STDOUT
|
|
|
|
|
+#
|
|
|
|
|
+# API choice: pure POSIX shell on purpose -- the arch base group ships
|
|
|
|
|
+# neither python3 nor jq, and the SPM bootstrap must stay at zero extra
|
|
|
|
|
+# packages. bsdtar (libarchive, a hard pacman dependency) decompresses
|
|
|
|
|
+# the repository databases under /var/lib/pacman/sync so provides and
|
|
|
|
|
+# file lists are parsed directly instead of shelling pattern queries
|
|
|
|
|
+# through pacman:
|
|
|
|
|
+#
|
|
|
|
|
+# lib: X -> the sync dbs' %PROVIDES% entries; makepkg auto-
|
|
|
|
|
+# generates soname provides ("libfoo.so=2-64"), so the
|
|
|
|
|
+# "=major-arch" suffix is stripped before matching;
|
|
|
|
|
+# file ownership of /usr/lib/X is the fallback
|
|
|
|
|
+# everything file-backed ->
|
|
|
|
|
+# the opt-in files databases (<repo>.files, fetched by
|
|
|
|
|
+# `pacman -Fy` -- the arch analogue of apt-file update)
|
|
|
|
|
+# are parsed into a path -> package map; without them,
|
|
|
|
|
+# only files of INSTALLED packages resolve (via
|
|
|
|
|
+# `pacman -Qo`), the same offline stance as the emerge
|
|
|
|
|
+# helper. No package-name heuristics are used.
|
|
|
|
|
+#
|
|
|
|
|
+# dependency-count / installed-dependency-count model "a solo install on
|
|
|
|
|
+# a from-scratch machine": the closure comes from `pacman -S --print`
|
|
|
|
|
+# run against a throwaway --dbpath whose local database is empty and
|
|
|
|
|
+# whose sync databases symlink the system's, so every dependency
|
|
|
|
|
+# resolves as an install; the overlap with the installed set
|
|
|
|
|
+# (`pacman -Qq`) is installed-dependency-count. If that run cannot
|
|
|
|
|
+# resolve, the counts degrade to a (1, n) estimate with a warning.
|
|
|
|
|
+#
|
|
|
|
|
+# pacman exposes no per-download progress on a non-tty stdout; it names
|
|
|
|
|
+# each package once ("(k/N) installing pkg" in older versions,
|
|
|
|
|
+# "installing pkg..." since pacman 7), so "package" events are emitted
|
|
|
|
|
+# at package granularity (progress 1.0) and begin.total comes from a
|
|
|
|
|
+# `pacman -S --print` pass against the current system. Install runs
|
|
|
|
|
+# with --needed, making repeated installs idempotent.
|
|
|
|
|
+#
|
|
|
|
|
+# No INSTALL_COMPANIONS table is needed: arch does not split toolchain
|
|
|
|
|
+# packages, glibc already ships the crt objects.
|
|
|
|
|
+#
|
|
|
|
|
+# The query subcommand never modifies system state: databases are read
|
|
|
|
|
+# from their normal locations, and the only writes land in a throwaway
|
|
|
|
|
+# temp directory. The install subcommand must run as root; --noconfirm
|
|
|
|
|
+# guarantees pacman never prompts interactively: unresolved targets or
|
|
|
|
|
+# bad signatures fail instead.
|
|
|
|
|
+#
|
|
|
|
|
+# Arch path table (merged-usr layout, no multiarch): /usr/bin (sbin is
|
|
|
|
|
+# a symlink), /usr/lib (lib64 is a symlink), /usr/include, /usr/share
|
|
|
|
|
+# (+/usr/share/pkgconfig), /usr/share/vala*/vapi.
|
|
|
|
|
+
|
|
|
|
|
+set -eu
|
|
|
|
|
+
|
|
|
|
|
+export LC_ALL=C
|
|
|
|
|
+
|
|
|
|
|
+PROG=usm-spm-pacman
|
|
|
|
|
+
|
|
|
|
|
+EXIT_OK=0
|
|
|
|
|
+EXIT_FAILURE=1
|
|
|
|
|
+EXIT_USAGE=2
|
|
|
|
|
+EXIT_RESOLVE=3
|
|
|
|
|
+EXIT_DOWNLOAD=4
|
|
|
|
|
+EXIT_TRANSACTION=5
|
|
|
|
|
+
|
|
|
|
|
+WORK=""
|
|
|
|
|
+
|
|
|
|
|
+SYNC_DIR=/var/lib/pacman/sync
|
|
|
|
|
+
|
|
|
|
|
+REPO_INDEXED=0
|
|
|
|
|
+
|
|
|
|
|
+cleanup() {
|
|
|
|
|
+ if [ -n "$WORK" ]; then
|
|
|
|
|
+ rm -rf "$WORK"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+warn() {
|
|
|
|
|
+ printf '%s: %s\n' "$PROG" "$*" >&2
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+die_usage() {
|
|
|
|
|
+ printf 'usage: %s query <usm-ref>...\n' "$PROG" >&2
|
|
|
|
|
+ printf ' %s install <native-name>...\n' "$PROG" >&2
|
|
|
|
|
+ exit "$EXIT_USAGE"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+emit() {
|
|
|
|
|
+ printf '%s\n' "$1"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+json_escape() {
|
|
|
|
|
+ printf '%s' "$1" | awk '
|
|
|
|
|
+ {
|
|
|
|
|
+ s = $0
|
|
|
|
|
+ gsub(/\\/, "\\\\", s)
|
|
|
|
|
+ gsub(/"/, "\\\"", s)
|
|
|
|
|
+ gsub(/\t/, "\\t", s)
|
|
|
|
|
+ gsub(/[[:cntrl:]]/, "", s)
|
|
|
|
|
+ print s
|
|
|
|
|
+ }'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+mkwork() {
|
|
|
|
|
+ WORK=$(mktemp -d "${TMPDIR:-/tmp}/usm-spm-pacman.XXXXXX" 2>/dev/null) || {
|
|
|
|
|
+ WORK="${TMPDIR:-/tmp}/usm-spm-pacman.$$"
|
|
|
|
|
+ (umask 077 && mkdir "$WORK")
|
|
|
|
|
+ }
|
|
|
|
|
+ trap cleanup EXIT
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+not_found_add() {
|
|
|
|
|
+ printf '%s\n' "$1" >>"$WORK/nf"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+map_add() {
|
|
|
|
|
+ printf '%s\t%s\n' "$1" "$2" >>"$WORK/map"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# Parse the sync/files databases ONCE per invocation into flat lookup
|
|
|
|
|
+# tables: $WORK/provides maps "soname-or-virtual<TAB>package" (the
|
|
|
|
|
+# "=major-arch" suffix is stripped from soname provides so lib: refs
|
|
|
|
|
+# carrying the full soname match exactly), $WORK/filemap maps
|
|
|
|
|
+# "path-without-leading-slash<TAB>package" from the %FILES% sections of
|
|
|
|
|
+# the .files databases. Both stay empty (and the lookups degrade to the
|
|
|
|
|
+# installed database) when the databases are missing.
|
|
|
|
|
+build_repo_index() {
|
|
|
|
|
+ [ "$REPO_INDEXED" = 0 ] || return 0
|
|
|
|
|
+ REPO_INDEXED=1
|
|
|
|
|
+ : >"$WORK/provides"
|
|
|
|
|
+ : >"$WORK/filemap"
|
|
|
|
|
+ if ! command -v bsdtar >/dev/null 2>&1; then
|
|
|
|
|
+ warn "bsdtar not found in PATH; repository lookups are unavailable"
|
|
|
|
|
+ return 0
|
|
|
|
|
+ fi
|
|
|
|
|
+ _ri_any=0
|
|
|
|
|
+ for _ri_db in "$SYNC_DIR"/*.db; do
|
|
|
|
|
+ [ -f "$_ri_db" ] || continue
|
|
|
|
|
+ _ri_any=1
|
|
|
|
|
+ bsdtar -xOf "$_ri_db" 2>>"$WORK/index.err" | awk '
|
|
|
|
|
+ /^%NAME%$/ { mode = "name"; next }
|
|
|
|
|
+ /^%PROVIDES%$/ { mode = "prov"; next }
|
|
|
|
|
+ /^%/ { mode = ""; next }
|
|
|
|
|
+ NF == 0 { next }
|
|
|
|
|
+ mode == "name" { pkg = $1; mode = ""; next }
|
|
|
|
|
+ mode == "prov" && pkg != "" {
|
|
|
|
|
+ p = $1
|
|
|
|
|
+ sub(/=.*$/, "", p)
|
|
|
|
|
+ print pkg "\t" p
|
|
|
|
|
+ }
|
|
|
|
|
+ ' >>"$WORK/provides" || true
|
|
|
|
|
+ done
|
|
|
|
|
+ for _ri_db in "$SYNC_DIR"/*.files; do
|
|
|
|
|
+ [ -f "$_ri_db" ] || continue
|
|
|
|
|
+ _ri_any=1
|
|
|
|
|
+ bsdtar -xOf "$_ri_db" 2>>"$WORK/index.err" | awk '
|
|
|
|
|
+ /^%NAME%$/ { mode = "name"; next }
|
|
|
|
|
+ /^%FILES%$/ { mode = "files"; next }
|
|
|
|
|
+ /^%/ { mode = ""; next }
|
|
|
|
|
+ NF == 0 { next }
|
|
|
|
|
+ mode == "name" { pkg = $1; mode = ""; next }
|
|
|
|
|
+ mode == "files" && pkg != "" { print pkg "\t" $1 }
|
|
|
|
|
+ ' >>"$WORK/filemap" || true
|
|
|
|
|
+ done
|
|
|
|
|
+ if [ "$_ri_any" != 1 ]; then
|
|
|
|
|
+ warn "no databases under $SYNC_DIR (run pacman -Sy as root); repository lookups are unavailable"
|
|
|
|
|
+ elif [ ! -s "$WORK/filemap" ]; then
|
|
|
|
|
+ warn "no files databases under $SYNC_DIR (run pacman -Fy as root); only installed files resolve"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# makepkg turns soname "libc.so.6" into the provide "libc.so=6-64", so a
|
|
|
|
|
+# lib: ref's trailing ".<major>" is optionally stripped before matching:
|
|
|
|
|
+# "lib:libc.so.6" matches "libc.so" exactly as an unversioned
|
|
|
|
|
+# "lib:libc.so" would.
|
|
|
|
|
+repo_provider_owners() {
|
|
|
|
|
+ awk -F'\t' -v p="$1" '
|
|
|
|
|
+ {
|
|
|
|
|
+ r = p
|
|
|
|
|
+ sub(/\.[0-9]+$/, "", r)
|
|
|
|
|
+ if ($2 == p || ($2 == r && r != p)) { print $1 }
|
|
|
|
|
+ }' "$WORK/provides" 2>/dev/null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+repo_file_owners() {
|
|
|
|
|
+ awk -F'\t' -v p="$1" '$2 == p { print $1 }' "$WORK/filemap" 2>/dev/null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+repo_file_prefix_owners() {
|
|
|
|
|
+ # exact file OR any file below the directory prefix
|
|
|
|
|
+ awk -F'\t' -v p="$1" '$2 == p || index($2, p "/") == 1 { print $1 }' \
|
|
|
|
|
+ "$WORK/filemap" 2>/dev/null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# Map every owner name read from STDIN onto _om_ref; succeeds when at
|
|
|
|
|
+# least one name arrived.
|
|
|
|
|
+owners_to_map() {
|
|
|
|
|
+ _om_ref=$1
|
|
|
|
|
+ _om_any=0
|
|
|
|
|
+ while IFS= read -r _om_nm; do
|
|
|
|
|
+ [ -n "$_om_nm" ] || continue
|
|
|
|
|
+ map_add "$_om_nm" "$_om_ref"
|
|
|
|
|
+ _om_any=1
|
|
|
|
|
+ done
|
|
|
|
|
+ [ "$_om_any" = 1 ]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+pacman_diag_line() {
|
|
|
|
|
+ [ -r "$1" ] || return 0
|
|
|
|
|
+ sed -n 's/^[[:space:]]*error:[[:space:]]*//p' "$1" | head -n 1
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+record_providers() {
|
|
|
|
|
+ _rp_ref=$1
|
|
|
|
|
+ _rp_prov=$2
|
|
|
|
|
+ build_repo_index
|
|
|
|
|
+ if repo_provider_owners "$_rp_prov" | sort -u | owners_to_map "$_rp_ref"; then
|
|
|
|
|
+ return 0
|
|
|
|
|
+ fi
|
|
|
|
|
+ not_found_add "$_rp_ref"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+record_file_owner() {
|
|
|
|
|
+ _fo_ref=$1
|
|
|
|
|
+ shift
|
|
|
|
|
+ build_repo_index
|
|
|
|
|
+ _fo_hit=0
|
|
|
|
|
+ for _fo_p in "$@"; do
|
|
|
|
|
+ _fo_names=
|
|
|
|
|
+ if [ -s "$WORK/filemap" ]; then
|
|
|
|
|
+ _fo_names=$(repo_file_owners "${_fo_p#/}" | sort -u)
|
|
|
|
|
+ fi
|
|
|
|
|
+ if [ -z "$_fo_names" ]; then
|
|
|
|
|
+ # installed-database fallback, exact for present files only
|
|
|
|
|
+ _fo_f=
|
|
|
|
|
+ if [ -d "$_fo_p" ]; then
|
|
|
|
|
+ _fo_f=$(find "$_fo_p" -type f 2>/dev/null | head -n 1) || _fo_f=
|
|
|
|
|
+ elif [ -f "$_fo_p" ]; then
|
|
|
|
|
+ _fo_f=$_fo_p
|
|
|
|
|
+ fi
|
|
|
|
|
+ if [ -n "$_fo_f" ]; then
|
|
|
|
|
+ _fo_names=$(pacman -Qo -- "$_fo_f" 2>/dev/null \
|
|
|
|
|
+ | awk '{ print $(NF-1) }') || _fo_names=
|
|
|
|
|
+ fi
|
|
|
|
|
+ fi
|
|
|
|
|
+ if [ -n "$_fo_names" ]; then
|
|
|
|
|
+ printf '%s\n' "$_fo_names" | owners_to_map "$_fo_ref" && _fo_hit=1
|
|
|
|
|
+ fi
|
|
|
|
|
+ done
|
|
|
|
|
+ if [ "$_fo_hit" != 1 ]; then
|
|
|
|
|
+ not_found_add "$_fo_ref"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+record_dir_owner() {
|
|
|
|
|
+ # Like record_file_owner, but the ref names a directory (inc:), so
|
|
|
|
|
+ # any file at or below the prefix satisfies it.
|
|
|
|
|
+ _do_ref=$1
|
|
|
|
|
+ _do_p=$2
|
|
|
|
|
+ build_repo_index
|
|
|
|
|
+ if [ -s "$WORK/filemap" ]; then
|
|
|
|
|
+ if repo_file_prefix_owners "${_do_p#/}" | sort -u | owners_to_map "$_do_ref"; then
|
|
|
|
|
+ return 0
|
|
|
|
|
+ fi
|
|
|
|
|
+ fi
|
|
|
|
|
+ record_file_owner "$_do_ref" "$_do_p"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+handle_ref() {
|
|
|
|
|
+ _hr_ref=$1
|
|
|
|
|
+ _hr_type=${_hr_ref%%:*}
|
|
|
|
|
+ _hr_res=${_hr_ref#*:}
|
|
|
|
|
+ if [ "$_hr_type" = "$_hr_ref" ] || [ -z "$_hr_res" ]; then
|
|
|
|
|
+ warn "resource type of \"$_hr_ref\" has no system-package-manager translation"
|
|
|
|
|
+ not_found_add "$_hr_ref"
|
|
|
|
|
+ return
|
|
|
|
|
+ fi
|
|
|
|
|
+ case $_hr_res in
|
|
|
|
|
+ *[!A-Za-z0-9._+/-]*)
|
|
|
|
|
+ warn "resource name of \"$_hr_ref\" has no system-package-manager translation"
|
|
|
|
|
+ not_found_add "$_hr_ref"
|
|
|
|
|
+ return
|
|
|
|
|
+ ;;
|
|
|
|
|
+ esac
|
|
|
|
|
+ case $_hr_type in
|
|
|
|
|
+ bin)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/bin/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ sbin)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/bin/$_hr_res" "/usr/sbin/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ lib|libres)
|
|
|
|
|
+ # soname provides first (exact, suffix-stripped), file second
|
|
|
|
|
+ build_repo_index
|
|
|
|
|
+ if ! repo_provider_owners "$_hr_res" | sort -u | owners_to_map "$_hr_ref"; then
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/lib/$_hr_res"
|
|
|
|
|
+ fi
|
|
|
|
|
+ ;;
|
|
|
|
|
+ pc)
|
|
|
|
|
+ record_file_owner "$_hr_ref" \
|
|
|
|
|
+ "/usr/lib/pkgconfig/$_hr_res" "/usr/share/pkgconfig/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ libexec)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/libexec/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ gir)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/share/gir-1.0/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ typelib)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/lib/girepository-1.0/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ gio)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/lib/gio/modules/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ res)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/share/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ cfg)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/etc/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ man)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/share/man/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ info)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/share/info/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ locale)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/usr/share/locale/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ inc)
|
|
|
|
|
+ record_dir_owner "$_hr_ref" "/usr/include/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ vapi)
|
|
|
|
|
+ build_repo_index
|
|
|
|
|
+ if [ -s "$WORK/filemap" ]; then
|
|
|
|
|
+ : >"$WORK/vown"
|
|
|
|
|
+ repo_file_owners "usr/share/vala/vapi/$_hr_res" >>"$WORK/vown"
|
|
|
|
|
+ awk -F'\t' -v suf="/vapi/$_hr_res" \
|
|
|
|
|
+ 'substr($2, 1, 15) == "usr/share/vala" &&
|
|
|
|
|
+ substr($2, length($2) - length(suf) + 1) == suf { print $1 }' \
|
|
|
|
|
+ "$WORK/filemap" >>"$WORK/vown"
|
|
|
|
|
+ if sort -u "$WORK/vown" | owners_to_map "$_hr_ref"; then
|
|
|
|
|
+ return 0
|
|
|
|
|
+ fi
|
|
|
|
|
+ fi
|
|
|
|
|
+ set -- "/usr/share/vala/vapi/$_hr_res"
|
|
|
|
|
+ for _hr_d in /usr/share/vala-*/vapi/"$_hr_res"; do
|
|
|
|
|
+ if [ -f "$_hr_d" ]; then
|
|
|
|
|
+ set -- "$@" "$_hr_d"
|
|
|
|
|
+ fi
|
|
|
|
|
+ done
|
|
|
|
|
+ record_file_owner "$_hr_ref" "$@"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ rootpath)
|
|
|
|
|
+ record_file_owner "$_hr_ref" "/$_hr_res"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ tag)
|
|
|
|
|
+ _hr_t=${_hr_res%.tag}
|
|
|
|
|
+ record_file_owner "$_hr_ref" \
|
|
|
|
|
+ "/usr/share/usm-tags/$(printf '%s' "$_hr_t" | tr '.' '/').tag"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ *)
|
|
|
|
|
+ warn "resource type of \"$_hr_ref\" has no system-package-manager translation"
|
|
|
|
|
+ not_found_add "$_hr_ref"
|
|
|
|
|
+ return
|
|
|
|
|
+ ;;
|
|
|
|
|
+ esac
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+counts_for() {
|
|
|
|
|
+ _cf_name=$1
|
|
|
|
|
+ _cf_cached=
|
|
|
|
|
+ if [ -s "$WORK/counts" ]; then
|
|
|
|
|
+ _cf_cached=$(awk -F'\t' -v n="$_cf_name" '$1 == n { print $2 " " $3; exit }' "$WORK/counts") || _cf_cached=
|
|
|
|
|
+ fi
|
|
|
|
|
+ if [ -n "$_cf_cached" ]; then
|
|
|
|
|
+ printf '%s\n' "$_cf_cached"
|
|
|
|
|
+ return
|
|
|
|
|
+ fi
|
|
|
|
|
+ # Repo-only closure: a throwaway database whose empty local db makes
|
|
|
|
|
+ # every package resolve as an install and whose sync symlink reuses
|
|
|
|
|
+ # the system's repositories.
|
|
|
|
|
+ _cf_root="$WORK/rootdb"
|
|
|
|
|
+ if [ ! -d "$_cf_root" ]; then
|
|
|
|
|
+ mkdir -p "$_cf_root/local"
|
|
|
|
|
+ ln -s "$SYNC_DIR" "$_cf_root/sync" 2>/dev/null || true
|
|
|
|
|
+ fi
|
|
|
|
|
+ _cf_dep=0
|
|
|
|
|
+ _cf_inst=0
|
|
|
|
|
+ _cf_cl=$(pacman --dbpath "$_cf_root" -S --print --print-format '%n' \
|
|
|
|
|
+ --noconfirm -- "$_cf_name" 2>"$WORK/cnt.err" | grep -E '^[A-Za-z0-9@._+-]+$' || true)
|
|
|
|
|
+ if [ -n "$_cf_cl" ]; then
|
|
|
|
|
+ printf '%s\n' "$_cf_cl" >"$WORK/cl"
|
|
|
|
|
+ _cf_dep=$(awk 'END { print NR }' "$WORK/cl")
|
|
|
|
|
+ if [ -s "$WORK/installed" ]; then
|
|
|
|
|
+ _cf_inst=$(grep -x -F -f "$WORK/installed" "$WORK/cl" | awk 'END { print NR }') || _cf_inst=0
|
|
|
|
|
+ fi
|
|
|
|
|
+ else
|
|
|
|
|
+ warn "could not resolve solo install of $_cf_name, estimating dependency counts"
|
|
|
|
|
+ if grep -qx -F "$_cf_name" "$WORK/installed" 2>/dev/null; then
|
|
|
|
|
+ _cf_dep=1
|
|
|
|
|
+ _cf_inst=1
|
|
|
|
|
+ else
|
|
|
|
|
+ _cf_dep=1
|
|
|
|
|
+ _cf_inst=0
|
|
|
|
|
+ fi
|
|
|
|
|
+ fi
|
|
|
|
|
+ printf '%s\t%s\t%s\n' "$_cf_name" "$_cf_dep" "$_cf_inst" >>"$WORK/counts"
|
|
|
|
|
+ printf '%s %s\n' "$_cf_dep" "$_cf_inst"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_query() {
|
|
|
|
|
+ [ $# -ge 1 ] || die_usage
|
|
|
|
|
+ mkwork
|
|
|
|
|
+ pacman -Qq >"$WORK/installed" 2>/dev/null || : >"$WORK/installed"
|
|
|
|
|
+ : >"$WORK/map"
|
|
|
|
|
+ : >"$WORK/nf"
|
|
|
|
|
+ : >"$WORK/counts"
|
|
|
|
|
+ : >"$WORK/seen"
|
|
|
|
|
+ for _q_ref in "$@"; do
|
|
|
|
|
+ if [ -n "$_q_ref" ] && grep -x -F -q "$_q_ref" "$WORK/seen" 2>/dev/null; then
|
|
|
|
|
+ continue
|
|
|
|
|
+ fi
|
|
|
|
|
+ printf '%s\n' "$_q_ref" >>"$WORK/seen"
|
|
|
|
|
+ handle_ref "$_q_ref"
|
|
|
|
|
+ done
|
|
|
|
|
+ _q_out='{"not-found":['
|
|
|
|
|
+ _q_first=1
|
|
|
|
|
+ while IFS= read -r _q_r; do
|
|
|
|
|
+ [ -n "$_q_r" ] || continue
|
|
|
|
|
+ if [ "$_q_first" = 1 ]; then _q_first=0; else _q_out="$_q_out,"; fi
|
|
|
|
|
+ _q_out="$_q_out\"$(json_escape "$_q_r")\""
|
|
|
|
|
+ done <"$WORK/nf"
|
|
|
|
|
+ _q_out="$_q_out],\"packages\":["
|
|
|
|
|
+ _q_first=1
|
|
|
|
|
+ for _q_nm in $(awk -F'\t' '{ print $1 }' "$WORK/map" | sort -u); do
|
|
|
|
|
+ if [ "$_q_first" = 1 ]; then _q_first=0; else _q_out="$_q_out,"; fi
|
|
|
|
|
+ awk -F'\t' -v n="$_q_nm" '$1 == n { print $2 }' "$WORK/map" >"$WORK/tmpres"
|
|
|
|
|
+ _q_res=""
|
|
|
|
|
+ _q_rf=1
|
|
|
|
|
+ while IFS= read -r _q_rr; do
|
|
|
|
|
+ if [ "$_q_rf" = 1 ]; then _q_rf=0; else _q_res="$_q_res,"; fi
|
|
|
|
|
+ _q_res="$_q_res\"$(json_escape "$_q_rr")\""
|
|
|
|
|
+ done <"$WORK/tmpres"
|
|
|
|
|
+ _q_cnts=$(counts_for "$_q_nm")
|
|
|
|
|
+ _q_dep=${_q_cnts%% *}
|
|
|
|
|
+ _q_inst=${_q_cnts##* }
|
|
|
|
|
+ _q_out="$_q_out{\"name\":\"$(json_escape "$_q_nm")\",\"resources\":[${_q_res}],\"dependency-count\":$_q_dep,\"installed-dependency-count\":$_q_inst}"
|
|
|
|
|
+ done
|
|
|
|
|
+ _q_out="$_q_out]}"
|
|
|
|
|
+ emit "$_q_out"
|
|
|
|
|
+ exit "$EXIT_OK"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+classify_failure() {
|
|
|
|
|
+ if grep -Eqi 'target not found|could not satisfy dependencies|conflicting|unresolvable|failed to prepare transaction|hold onto' "$1" 2>/dev/null; then
|
|
|
|
|
+ printf '%s\n' "$EXIT_RESOLVE"
|
|
|
|
|
+ elif grep -Eqi 'network|download|retriev|signature|keyring|gpg|invalid or corrupted|checksum|integrity|connection|timed out|mirror' "$1" 2>/dev/null; then
|
|
|
|
|
+ printf '%s\n' "$EXIT_DOWNLOAD"
|
|
|
|
|
+ else
|
|
|
|
|
+ printf '%s\n' "$EXIT_TRANSACTION"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_install() {
|
|
|
|
|
+ [ $# -ge 1 ] || die_usage
|
|
|
|
|
+ for _i_n in "$@"; do
|
|
|
|
|
+ case $_i_n in
|
|
|
|
|
+ -*) die_usage ;;
|
|
|
|
|
+ esac
|
|
|
|
|
+ done
|
|
|
|
|
+ mkwork
|
|
|
|
|
+ _i_simrc=0
|
|
|
|
|
+ _i_sim=$(pacman -S --needed --noconfirm --print --print-format '%n' \
|
|
|
|
|
+ -- "$@" 2>"$WORK/sim.err") || _i_simrc=$?
|
|
|
|
|
+ if [ "$_i_simrc" != 0 ]; then
|
|
|
|
|
+ _i_msg=$(pacman_diag_line "$WORK/sim.err")
|
|
|
|
|
+ [ -n "$_i_msg" ] || _i_msg="pacman --print failed with status $_i_simrc"
|
|
|
|
|
+ emit "{\"type\":\"error\",\"message\":\"$(json_escape "$_i_msg")\"}"
|
|
|
|
|
+ exit "$(classify_failure "$WORK/sim.err")"
|
|
|
|
|
+ fi
|
|
|
|
|
+ _i_total=$(printf '%s\n' "$_i_sim" | grep -Ec '^[A-Za-z0-9@._+-]+$') || _i_total=0
|
|
|
|
|
+ emit "{\"type\":\"begin\",\"total\":$_i_total}"
|
|
|
|
|
+ {
|
|
|
|
|
+ # || keeps set -e from aborting the pipeline group before the
|
|
|
|
|
+ # status reaches the rc file
|
|
|
|
|
+ _i_status=0
|
|
|
|
|
+ pacman -S --needed --noconfirm --noprogressbar -- "$@" \
|
|
|
|
|
+ 2>"$WORK/inst.err" || _i_status=$?
|
|
|
|
|
+ printf '%s\n' "$_i_status" >"$WORK/rc"
|
|
|
|
|
+ } | tr '\r' '\n' | {
|
|
|
|
|
+ _i_done=0
|
|
|
|
|
+ _i_lastpkg=
|
|
|
|
|
+ while IFS= read -r _i_line || [ -n "$_i_line" ]; do
|
|
|
|
|
+ # pacman's transaction output names each package exactly
|
|
|
|
|
+ # once: older versions redraw "(k/N) installing pkg" lines
|
|
|
|
|
+ # (\r-flattened here), pacman 7 pipes plain "installing
|
|
|
|
|
+ # pkg..." lines. Dedupe on the package name either way.
|
|
|
|
|
+ case $_i_line in
|
|
|
|
|
+ \(*\)\ *)
|
|
|
|
|
+ _i_kn=${_i_line#"("}
|
|
|
|
|
+ _i_kn=${_i_kn%%")"*}
|
|
|
|
|
+ _i_rest=${_i_line#*") "}
|
|
|
|
|
+ _i_verb=${_i_rest%% *}
|
|
|
|
|
+ _i_pkg=${_i_rest#* }
|
|
|
|
|
+ _i_pkg=${_i_pkg%% *}
|
|
|
|
|
+ _i_k=${_i_kn%%/*}
|
|
|
|
|
+ _i_n=${_i_kn##*/}
|
|
|
|
|
+ ;;
|
|
|
|
|
+ installing\ *|upgrading\ *|downgrading\ *|reinstalling\ *)
|
|
|
|
|
+ _i_verb=${_i_line%% *}
|
|
|
|
|
+ _i_pkg=${_i_line#* }
|
|
|
|
|
+ _i_pkg=${_i_pkg%% *}
|
|
|
|
|
+ _i_pkg=${_i_pkg%"..."}
|
|
|
|
|
+ _i_k=$((_i_done + 1))
|
|
|
|
|
+ _i_n=$_i_total
|
|
|
|
|
+ ;;
|
|
|
|
|
+ *)
|
|
|
|
|
+ continue
|
|
|
|
|
+ ;;
|
|
|
|
|
+ esac
|
|
|
|
|
+ case $_i_verb in
|
|
|
|
|
+ installing|upgrading|downgrading|reinstalling)
|
|
|
|
|
+ if [ "$_i_pkg" != "$_i_lastpkg" ]; then
|
|
|
|
|
+ _i_lastpkg=$_i_pkg
|
|
|
|
|
+ emit "{\"type\":\"package\",\"name\":\"$(json_escape "$_i_pkg")\",\"current\":$_i_k,\"total\":$_i_n,\"progress\":1.0}"
|
|
|
|
|
+ emit "{\"type\":\"package-complete\",\"name\":\"$(json_escape "$_i_pkg")\"}"
|
|
|
|
|
+ _i_done=$((_i_done + 1))
|
|
|
|
|
+ fi
|
|
|
|
|
+ ;;
|
|
|
|
|
+ esac
|
|
|
|
|
+ done
|
|
|
|
|
+ printf '%s\n' "$_i_done" >"$WORK/done"
|
|
|
|
|
+ }
|
|
|
|
|
+ _i_rc=$(cat "$WORK/rc" 2>/dev/null) || _i_rc=1
|
|
|
|
|
+ if [ "$_i_rc" = 0 ]; then
|
|
|
|
|
+ _i_done=$(cat "$WORK/done" 2>/dev/null) || _i_done=0
|
|
|
|
|
+ emit "{\"type\":\"complete\",\"status\":\"ok\",\"installed\":$_i_done}"
|
|
|
|
|
+ exit "$EXIT_OK"
|
|
|
|
|
+ fi
|
|
|
|
|
+ _i_msg=$(pacman_diag_line "$WORK/inst.err")
|
|
|
|
|
+ [ -n "$_i_msg" ] || _i_msg="pacman -S failed with status $_i_rc"
|
|
|
|
|
+ emit "{\"type\":\"error\",\"message\":\"$(json_escape "$_i_msg")\"}"
|
|
|
|
|
+ exit "$(classify_failure "$WORK/inst.err")"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+main() {
|
|
|
|
|
+ if [ $# -lt 1 ]; then
|
|
|
|
|
+ die_usage
|
|
|
|
|
+ fi
|
|
|
|
|
+ if ! command -v pacman >/dev/null 2>&1; then
|
|
|
|
|
+ if [ "$1" = install ]; then
|
|
|
|
|
+ emit '{"type":"error","message":"pacman not found in PATH"}'
|
|
|
|
|
+ exit "$EXIT_FAILURE"
|
|
|
|
|
+ fi
|
|
|
|
|
+ warn "pacman not found in PATH"
|
|
|
|
|
+ exit "$EXIT_RESOLVE"
|
|
|
|
|
+ fi
|
|
|
|
|
+ _m_cmd=$1
|
|
|
|
|
+ shift
|
|
|
|
|
+ case $_m_cmd in
|
|
|
|
|
+ query)
|
|
|
|
|
+ cmd_query "$@"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ install)
|
|
|
|
|
+ cmd_install "$@"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ -h|--help|help)
|
|
|
|
|
+ printf 'usage: %s query <usm-ref>...\n' "$PROG"
|
|
|
|
|
+ printf ' %s install <native-name>...\n' "$PROG"
|
|
|
|
|
+ exit "$EXIT_OK"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ *)
|
|
|
|
|
+ die_usage
|
|
|
|
|
+ ;;
|
|
|
|
|
+ esac
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+main "$@"
|