usm-spm-pacman 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. #!/bin/sh
  2. # USM system-package-manager helper for pacman-based systems (Arch).
  3. #
  4. # Implements the USM SPM contract (see usm/README.md, "System package
  5. # manager integration") for Arch-style systems:
  6. #
  7. # usm-spm-pacman query <usm-ref>... -> contract JSON on STDOUT
  8. # usm-spm-pacman install <native-name>... -> contract JSONL events on STDOUT
  9. #
  10. # API choice: pure POSIX shell on purpose -- the arch base group ships
  11. # neither python3 nor jq, and the SPM bootstrap must stay at zero extra
  12. # packages. bsdtar (libarchive, a hard pacman dependency) decompresses
  13. # the repository databases under /var/lib/pacman/sync so provides and
  14. # file lists are parsed directly instead of shelling pattern queries
  15. # through pacman:
  16. #
  17. # lib: X -> the sync dbs' %PROVIDES% entries; makepkg auto-
  18. # generates soname provides ("libfoo.so=2-64"), so the
  19. # "=major-arch" suffix is stripped before matching;
  20. # file ownership of /usr/lib/X is the fallback
  21. # everything file-backed ->
  22. # the opt-in files databases (<repo>.files, fetched by
  23. # `pacman -Fy` -- the arch analogue of apt-file update)
  24. # are parsed into a path -> package map; without them,
  25. # only files of INSTALLED packages resolve (via
  26. # `pacman -Qo`), the same offline stance as the emerge
  27. # helper. No package-name heuristics are used.
  28. #
  29. # Every package object also carries a "conflicts" array, parsed from the
  30. # same sync databases' %CONFLICTS% stanzas (the field `pacman -Si`
  31. # prints as "Conflicts With :") during the single index pass, with the
  32. # installed database's desc files scanned the same way so candidates
  33. # known only locally are covered too. Entries may carry a "=version"
  34. # suffix (e.g. "shadow=4.14.0"); the suffix is stripped and the bare
  35. # name kept, as with soname provides.
  36. #
  37. # dependency-count / installed-dependency-count model "a solo install on
  38. # a from-scratch machine": the closure comes from `pacman -S --print`
  39. # run against a throwaway --dbpath whose local database is empty and
  40. # whose sync databases symlink the system's, so every dependency
  41. # resolves as an install; the overlap with the installed set
  42. # (`pacman -Qq`) is installed-dependency-count. If that run cannot
  43. # resolve, the counts degrade to a (1, n) estimate with a warning.
  44. #
  45. # pacman exposes no per-download progress on a non-tty stdout; it names
  46. # each package once ("(k/N) installing pkg" in older versions,
  47. # "installing pkg..." since pacman 7), so "package" events are emitted
  48. # at package granularity (progress 1.0) and begin.total comes from a
  49. # `pacman -S --print` pass against the current system. Install runs
  50. # with --needed, making repeated installs idempotent.
  51. #
  52. # No INSTALL_COMPANIONS table is needed: arch does not split toolchain
  53. # packages, glibc already ships the crt objects.
  54. #
  55. # The query subcommand never modifies system state: databases are read
  56. # from their normal locations, and the only writes land in a throwaway
  57. # temp directory. The install subcommand must run as root; --noconfirm
  58. # guarantees pacman never prompts interactively: unresolved targets or
  59. # bad signatures fail instead.
  60. #
  61. # Arch path table (merged-usr layout, no multiarch): /usr/bin (sbin is
  62. # a symlink), /usr/lib (lib64 is a symlink), /usr/include, /usr/share
  63. # (+/usr/share/pkgconfig), /usr/share/vala*/vapi.
  64. set -eu
  65. export LC_ALL=C
  66. PROG=usm-spm-pacman
  67. EXIT_OK=0
  68. EXIT_FAILURE=1
  69. EXIT_USAGE=2
  70. EXIT_RESOLVE=3
  71. EXIT_DOWNLOAD=4
  72. EXIT_TRANSACTION=5
  73. WORK=""
  74. SYNC_DIR=/var/lib/pacman/sync
  75. REPO_INDEXED=0
  76. cleanup() {
  77. if [ -n "$WORK" ]; then
  78. rm -rf "$WORK"
  79. fi
  80. }
  81. warn() {
  82. printf '%s: %s\n' "$PROG" "$*" >&2
  83. }
  84. die_usage() {
  85. printf 'usage: %s query <usm-ref>...\n' "$PROG" >&2
  86. printf ' %s plan <native-name>...\n' "$PROG" >&2
  87. printf ' %s install <native-name>...\n' "$PROG" >&2
  88. exit "$EXIT_USAGE"
  89. }
  90. emit() {
  91. printf '%s\n' "$1"
  92. }
  93. json_escape() {
  94. printf '%s' "$1" | awk '
  95. {
  96. s = $0
  97. gsub(/\\/, "\\\\", s)
  98. gsub(/"/, "\\\"", s)
  99. gsub(/\t/, "\\t", s)
  100. gsub(/[[:cntrl:]]/, "", s)
  101. print s
  102. }'
  103. }
  104. mkwork() {
  105. WORK=$(mktemp -d "${TMPDIR:-/tmp}/usm-spm-pacman.XXXXXX" 2>/dev/null) || {
  106. WORK="${TMPDIR:-/tmp}/usm-spm-pacman.$$"
  107. (umask 077 && mkdir "$WORK")
  108. }
  109. trap cleanup EXIT
  110. }
  111. not_found_add() {
  112. printf '%s\n' "$1" >>"$WORK/nf"
  113. }
  114. map_add() {
  115. printf '%s\t%s\n' "$1" "$2" >>"$WORK/map"
  116. }
  117. # Parse the sync/files databases ONCE per invocation into flat lookup
  118. # tables: $WORK/provides maps "soname-or-virtual<TAB>package" (the
  119. # "=major-arch" suffix is stripped from soname provides so lib: refs
  120. # carrying the full soname match exactly), $WORK/filemap maps
  121. # "path-without-leading-slash<TAB>package" from the %FILES% sections of
  122. # the .files databases, and $WORK/conflicts maps
  123. # "package<TAB>conflict" from the %CONFLICTS% sections of the sync .db
  124. # files and the installed database's desc files. All stay empty (and the
  125. # lookups degrade to the installed database) when the databases are
  126. # missing.
  127. build_repo_index() {
  128. [ "$REPO_INDEXED" = 0 ] || return 0
  129. REPO_INDEXED=1
  130. : >"$WORK/provides"
  131. : >"$WORK/filemap"
  132. : >"$WORK/conflicts"
  133. # installed packages' conflicts come from the local database's desc
  134. # files (same %CONFLICTS% stanza as the sync dbs), covering
  135. # candidates the sync databases do not know; one pass over all desc
  136. # files, no bsdtar needed
  137. cat /var/lib/pacman/local/*/desc 2>/dev/null | awk -v conflfile="$WORK/conflicts" '
  138. /^%NAME%$/ { mode = "name"; next }
  139. /^%CONFLICTS%$/ { mode = "confl"; next }
  140. /^%/ { mode = ""; next }
  141. NF == 0 { next }
  142. mode == "name" { pkg = $1; mode = ""; next }
  143. mode == "confl" && pkg != "" {
  144. c = $1
  145. sub(/=.*$/, "", c)
  146. print pkg "\t" c >> conflfile
  147. }
  148. ' || true
  149. if ! command -v bsdtar >/dev/null 2>&1; then
  150. warn "bsdtar not found in PATH; repository lookups are unavailable"
  151. return 0
  152. fi
  153. _ri_any=0
  154. for _ri_db in "$SYNC_DIR"/*.db; do
  155. [ -f "$_ri_db" ] || continue
  156. _ri_any=1
  157. bsdtar -xOf "$_ri_db" 2>>"$WORK/index.err" | awk -v conflfile="$WORK/conflicts" '
  158. /^%NAME%$/ { mode = "name"; next }
  159. /^%PROVIDES%$/ { mode = "prov"; next }
  160. /^%CONFLICTS%$/ { mode = "confl"; next }
  161. /^%/ { mode = ""; next }
  162. NF == 0 { next }
  163. mode == "name" { pkg = $1; mode = ""; next }
  164. mode == "prov" && pkg != "" {
  165. p = $1
  166. sub(/=.*$/, "", p)
  167. print pkg "\t" p
  168. }
  169. mode == "confl" && pkg != "" {
  170. c = $1
  171. sub(/=.*$/, "", c)
  172. print pkg "\t" c >> conflfile
  173. }
  174. ' >>"$WORK/provides" || true
  175. done
  176. for _ri_db in "$SYNC_DIR"/*.files; do
  177. [ -f "$_ri_db" ] || continue
  178. _ri_any=1
  179. bsdtar -xOf "$_ri_db" 2>>"$WORK/index.err" | awk '
  180. /^%NAME%$/ { mode = "name"; next }
  181. /^%FILES%$/ { mode = "files"; next }
  182. /^%/ { mode = ""; next }
  183. NF == 0 { next }
  184. mode == "name" { pkg = $1; mode = ""; next }
  185. mode == "files" && pkg != "" { print pkg "\t" $1 }
  186. ' >>"$WORK/filemap" || true
  187. done
  188. if [ "$_ri_any" != 1 ]; then
  189. warn "no databases under $SYNC_DIR (run pacman -Sy as root); repository lookups are unavailable"
  190. elif [ ! -s "$WORK/filemap" ]; then
  191. warn "no files databases under $SYNC_DIR (run pacman -Fy as root); only installed files resolve"
  192. fi
  193. }
  194. # makepkg turns soname "libc.so.6" into the provide "libc.so=6-64", so a
  195. # lib: ref's trailing ".<major>" is optionally stripped before matching:
  196. # "lib:libc.so.6" matches "libc.so" exactly as an unversioned
  197. # "lib:libc.so" would.
  198. repo_provider_owners() {
  199. awk -F'\t' -v p="$1" '
  200. {
  201. r = p
  202. sub(/\.[0-9]+$/, "", r)
  203. if ($2 == p || ($2 == r && r != p)) { print $1 }
  204. }' "$WORK/provides" 2>/dev/null
  205. }
  206. repo_file_owners() {
  207. awk -F'\t' -v p="$1" '$2 == p { print $1 }' "$WORK/filemap" 2>/dev/null
  208. }
  209. repo_file_prefix_owners() {
  210. # exact file OR any file below the directory prefix
  211. awk -F'\t' -v p="$1" '$2 == p || index($2, p "/") == 1 { print $1 }' \
  212. "$WORK/filemap" 2>/dev/null
  213. }
  214. repo_conflict_owners() {
  215. awk -F'\t' -v p="$1" '$1 == p { print $2 }' "$WORK/conflicts" 2>/dev/null
  216. }
  217. # Print the "conflicts" JSON array for a package: the bare names its
  218. # %CONFLICTS% stanza lists in the sync/local databases (query mode
  219. # only; empty array when nothing conflicts or no database knows the
  220. # package).
  221. conflicts_for() {
  222. _cq_name=$1
  223. build_repo_index
  224. _cq_json=""
  225. _cq_first=1
  226. for _cq_c in $(repo_conflict_owners "$_cq_name" | sort -u); do
  227. if [ "$_cq_first" = 1 ]; then _cq_first=0; else _cq_json="$_cq_json,"; fi
  228. _cq_json="$_cq_json\"$(json_escape "$_cq_c")\""
  229. done
  230. printf '[%s]' "$_cq_json"
  231. }
  232. # Map every owner name read from STDIN onto _om_ref; succeeds when at
  233. # least one name arrived.
  234. owners_to_map() {
  235. _om_ref=$1
  236. _om_any=0
  237. while IFS= read -r _om_nm; do
  238. [ -n "$_om_nm" ] || continue
  239. map_add "$_om_nm" "$_om_ref"
  240. _om_any=1
  241. done
  242. [ "$_om_any" = 1 ]
  243. }
  244. # pacman prefixes diagnostics with "error:" and, for solver failures,
  245. # follows the headline with ":: " detail lines naming the conflicting
  246. # packages (":: dropbear and openssh are in conflict"). Return the
  247. # headline plus those details (up to 8, joined with "; ") on one line so
  248. # error events carry the whole report instead of only its first line.
  249. pacman_diag_line() {
  250. [ -r "$1" ] || return 0
  251. awk '
  252. /^[[:space:]]*error:[[:space:]]*/ && diag == "" {
  253. sub(/^[[:space:]]*error:[[:space:]]*/, "")
  254. diag = $0
  255. details = 0
  256. next
  257. }
  258. diag != "" && details < 8 && /^[[:space:]]*::[[:space:]]*/ {
  259. line = $0
  260. sub(/^[[:space:]]*::[[:space:]]*/, "", line)
  261. diag = diag "; " line
  262. details++
  263. next
  264. }
  265. diag != "" && /^[[:space:]]+[^:[:space:]]/ { exit }
  266. END { if (diag != "") print diag }
  267. ' "$1"
  268. }
  269. record_providers() {
  270. _rp_ref=$1
  271. _rp_prov=$2
  272. build_repo_index
  273. if repo_provider_owners "$_rp_prov" | sort -u | owners_to_map "$_rp_ref"; then
  274. return 0
  275. fi
  276. not_found_add "$_rp_ref"
  277. }
  278. record_file_owner() {
  279. _fo_ref=$1
  280. shift
  281. build_repo_index
  282. _fo_hit=0
  283. for _fo_p in "$@"; do
  284. _fo_names=
  285. if [ -s "$WORK/filemap" ]; then
  286. _fo_names=$(repo_file_owners "${_fo_p#/}" | sort -u)
  287. fi
  288. if [ -z "$_fo_names" ]; then
  289. # installed-database fallback, exact for present files only
  290. _fo_f=
  291. if [ -d "$_fo_p" ]; then
  292. _fo_f=$(find "$_fo_p" -type f 2>/dev/null | head -n 1) || _fo_f=
  293. elif [ -f "$_fo_p" ]; then
  294. _fo_f=$_fo_p
  295. fi
  296. if [ -n "$_fo_f" ]; then
  297. _fo_names=$(pacman -Qo -- "$_fo_f" 2>/dev/null \
  298. | awk '{ print $(NF-1) }') || _fo_names=
  299. fi
  300. fi
  301. if [ -n "$_fo_names" ]; then
  302. printf '%s\n' "$_fo_names" | owners_to_map "$_fo_ref" && _fo_hit=1
  303. fi
  304. done
  305. if [ "$_fo_hit" != 1 ]; then
  306. not_found_add "$_fo_ref"
  307. fi
  308. }
  309. record_dir_owner() {
  310. # Like record_file_owner, but the ref names a directory (inc:), so
  311. # any file at or below the prefix satisfies it.
  312. _do_ref=$1
  313. _do_p=$2
  314. build_repo_index
  315. if [ -s "$WORK/filemap" ]; then
  316. if repo_file_prefix_owners "${_do_p#/}" | sort -u | owners_to_map "$_do_ref"; then
  317. return 0
  318. fi
  319. fi
  320. record_file_owner "$_do_ref" "$_do_p"
  321. }
  322. handle_ref() {
  323. _hr_ref=$1
  324. _hr_type=${_hr_ref%%:*}
  325. _hr_res=${_hr_ref#*:}
  326. if [ "$_hr_type" = "$_hr_ref" ] || [ -z "$_hr_res" ]; then
  327. warn "resource type of \"$_hr_ref\" has no system-package-manager translation"
  328. not_found_add "$_hr_ref"
  329. return
  330. fi
  331. case $_hr_res in
  332. *[!A-Za-z0-9._+/-]*)
  333. warn "resource name of \"$_hr_ref\" has no system-package-manager translation"
  334. not_found_add "$_hr_ref"
  335. return
  336. ;;
  337. esac
  338. case $_hr_type in
  339. bin)
  340. record_file_owner "$_hr_ref" "/usr/bin/$_hr_res"
  341. ;;
  342. sbin)
  343. record_file_owner "$_hr_ref" "/usr/bin/$_hr_res" "/usr/sbin/$_hr_res"
  344. ;;
  345. lib|libres)
  346. # soname provides first (exact, suffix-stripped), file second
  347. build_repo_index
  348. if ! repo_provider_owners "$_hr_res" | sort -u | owners_to_map "$_hr_ref"; then
  349. record_file_owner "$_hr_ref" "/usr/lib/$_hr_res"
  350. fi
  351. ;;
  352. pc)
  353. record_file_owner "$_hr_ref" \
  354. "/usr/lib/pkgconfig/$_hr_res" "/usr/share/pkgconfig/$_hr_res"
  355. ;;
  356. libexec)
  357. record_file_owner "$_hr_ref" "/usr/libexec/$_hr_res"
  358. ;;
  359. gir)
  360. record_file_owner "$_hr_ref" "/usr/share/gir-1.0/$_hr_res"
  361. ;;
  362. typelib)
  363. record_file_owner "$_hr_ref" "/usr/lib/girepository-1.0/$_hr_res"
  364. ;;
  365. gio)
  366. record_file_owner "$_hr_ref" "/usr/lib/gio/modules/$_hr_res"
  367. ;;
  368. res)
  369. record_file_owner "$_hr_ref" "/usr/share/$_hr_res"
  370. ;;
  371. cfg)
  372. record_file_owner "$_hr_ref" "/etc/$_hr_res"
  373. ;;
  374. man)
  375. record_file_owner "$_hr_ref" "/usr/share/man/$_hr_res"
  376. ;;
  377. info)
  378. record_file_owner "$_hr_ref" "/usr/share/info/$_hr_res"
  379. ;;
  380. locale)
  381. record_file_owner "$_hr_ref" "/usr/share/locale/$_hr_res"
  382. ;;
  383. inc)
  384. record_dir_owner "$_hr_ref" "/usr/include/$_hr_res"
  385. ;;
  386. vapi)
  387. build_repo_index
  388. if [ -s "$WORK/filemap" ]; then
  389. : >"$WORK/vown"
  390. repo_file_owners "usr/share/vala/vapi/$_hr_res" >>"$WORK/vown"
  391. awk -F'\t' -v suf="/vapi/$_hr_res" \
  392. 'substr($2, 1, 15) == "usr/share/vala" &&
  393. substr($2, length($2) - length(suf) + 1) == suf { print $1 }' \
  394. "$WORK/filemap" >>"$WORK/vown"
  395. if sort -u "$WORK/vown" | owners_to_map "$_hr_ref"; then
  396. return 0
  397. fi
  398. fi
  399. set -- "/usr/share/vala/vapi/$_hr_res"
  400. for _hr_d in /usr/share/vala-*/vapi/"$_hr_res"; do
  401. if [ -f "$_hr_d" ]; then
  402. set -- "$@" "$_hr_d"
  403. fi
  404. done
  405. record_file_owner "$_hr_ref" "$@"
  406. ;;
  407. rootpath)
  408. record_file_owner "$_hr_ref" "/$_hr_res"
  409. ;;
  410. tag)
  411. _hr_t=${_hr_res%.tag}
  412. record_file_owner "$_hr_ref" \
  413. "/usr/share/usm-tags/$(printf '%s' "$_hr_t" | tr '.' '/').tag"
  414. ;;
  415. *)
  416. warn "resource type of \"$_hr_ref\" has no system-package-manager translation"
  417. not_found_add "$_hr_ref"
  418. return
  419. ;;
  420. esac
  421. }
  422. counts_for() {
  423. _cf_name=$1
  424. _cf_cached=
  425. if [ -s "$WORK/counts" ]; then
  426. _cf_cached=$(awk -F'\t' -v n="$_cf_name" '$1 == n { print $2 " " $3; exit }' "$WORK/counts") || _cf_cached=
  427. fi
  428. if [ -n "$_cf_cached" ]; then
  429. printf '%s\n' "$_cf_cached"
  430. return
  431. fi
  432. # Repo-only closure: a throwaway database whose empty local db makes
  433. # every package resolve as an install and whose sync symlink reuses
  434. # the system's repositories.
  435. _cf_root="$WORK/rootdb"
  436. if [ ! -d "$_cf_root" ]; then
  437. mkdir -p "$_cf_root/local"
  438. ln -s "$SYNC_DIR" "$_cf_root/sync" 2>/dev/null || true
  439. fi
  440. _cf_dep=0
  441. _cf_inst=0
  442. _cf_cl=$(pacman --dbpath "$_cf_root" -S --print --print-format '%n' \
  443. --noconfirm -- "$_cf_name" 2>"$WORK/cnt.err" | grep -E '^[A-Za-z0-9@._+-]+$' || true)
  444. if [ -n "$_cf_cl" ]; then
  445. printf '%s\n' "$_cf_cl" >"$WORK/cl"
  446. _cf_dep=$(awk 'END { print NR }' "$WORK/cl")
  447. if [ -s "$WORK/installed" ]; then
  448. _cf_inst=$(grep -x -F -f "$WORK/installed" "$WORK/cl" | awk 'END { print NR }') || _cf_inst=0
  449. fi
  450. else
  451. warn "could not resolve solo install of $_cf_name, estimating dependency counts"
  452. if grep -qx -F "$_cf_name" "$WORK/installed" 2>/dev/null; then
  453. _cf_dep=1
  454. _cf_inst=1
  455. else
  456. _cf_dep=1
  457. _cf_inst=0
  458. fi
  459. fi
  460. printf '%s\t%s\t%s\n' "$_cf_name" "$_cf_dep" "$_cf_inst" >>"$WORK/counts"
  461. printf '%s %s\n' "$_cf_dep" "$_cf_inst"
  462. }
  463. cmd_query() {
  464. [ $# -ge 1 ] || die_usage
  465. mkwork
  466. pacman -Qq >"$WORK/installed" 2>/dev/null || : >"$WORK/installed"
  467. : >"$WORK/map"
  468. : >"$WORK/nf"
  469. : >"$WORK/counts"
  470. : >"$WORK/seen"
  471. for _q_ref in "$@"; do
  472. if [ -n "$_q_ref" ] && grep -x -F -q "$_q_ref" "$WORK/seen" 2>/dev/null; then
  473. continue
  474. fi
  475. printf '%s\n' "$_q_ref" >>"$WORK/seen"
  476. handle_ref "$_q_ref"
  477. done
  478. _q_out='{"not-found":['
  479. _q_first=1
  480. while IFS= read -r _q_r; do
  481. [ -n "$_q_r" ] || continue
  482. if [ "$_q_first" = 1 ]; then _q_first=0; else _q_out="$_q_out,"; fi
  483. _q_out="$_q_out\"$(json_escape "$_q_r")\""
  484. done <"$WORK/nf"
  485. _q_out="$_q_out],\"packages\":["
  486. _q_first=1
  487. for _q_nm in $(awk -F'\t' '{ print $1 }' "$WORK/map" | sort -u); do
  488. if [ "$_q_first" = 1 ]; then _q_first=0; else _q_out="$_q_out,"; fi
  489. awk -F'\t' -v n="$_q_nm" '$1 == n { print $2 }' "$WORK/map" >"$WORK/tmpres"
  490. _q_res=""
  491. _q_rf=1
  492. while IFS= read -r _q_rr; do
  493. if [ "$_q_rf" = 1 ]; then _q_rf=0; else _q_res="$_q_res,"; fi
  494. _q_res="$_q_res\"$(json_escape "$_q_rr")\""
  495. done <"$WORK/tmpres"
  496. _q_cnts=$(counts_for "$_q_nm")
  497. _q_dep=${_q_cnts%% *}
  498. _q_inst=${_q_cnts##* }
  499. _q_conf=$(conflicts_for "$_q_nm")
  500. _q_out="$_q_out{\"name\":\"$(json_escape "$_q_nm")\",\"resources\":[${_q_res}],\"dependency-count\":$_q_dep,\"installed-dependency-count\":$_q_inst,\"conflicts\":$_q_conf}"
  501. done
  502. _q_out="$_q_out]}"
  503. emit "$_q_out"
  504. exit "$EXIT_OK"
  505. }
  506. classify_failure() {
  507. if grep -Eqi 'target not found|could not satisfy dependencies|conflicting|unresolvable|failed to prepare transaction|hold onto' "$1" 2>/dev/null; then
  508. printf '%s\n' "$EXIT_RESOLVE"
  509. elif grep -Eqi 'network|download|retriev|signature|keyring|gpg|invalid or corrupted|checksum|integrity|connection|timed out|mirror' "$1" 2>/dev/null; then
  510. printf '%s\n' "$EXIT_DOWNLOAD"
  511. else
  512. printf '%s\n' "$EXIT_TRANSACTION"
  513. fi
  514. }
  515. cmd_install() {
  516. [ $# -ge 1 ] || die_usage
  517. for _i_n in "$@"; do
  518. case $_i_n in
  519. -*) die_usage ;;
  520. esac
  521. done
  522. mkwork
  523. _i_simrc=0
  524. _i_sim=$(pacman -S --needed --noconfirm --print --print-format '%n' \
  525. -- "$@" 2>"$WORK/sim.err") || _i_simrc=$?
  526. if [ "$_i_simrc" != 0 ]; then
  527. _i_msg=$(pacman_diag_line "$WORK/sim.err")
  528. [ -n "$_i_msg" ] || _i_msg="pacman --print failed with status $_i_simrc"
  529. emit "{\"type\":\"error\",\"message\":\"$(json_escape "$_i_msg")\"}"
  530. exit "$(classify_failure "$WORK/sim.err")"
  531. fi
  532. _i_total=$(printf '%s\n' "$_i_sim" | grep -Ec '^[A-Za-z0-9@._+-]+$') || _i_total=0
  533. emit "{\"type\":\"begin\",\"total\":$_i_total}"
  534. {
  535. # || keeps set -e from aborting the pipeline group before the
  536. # status reaches the rc file
  537. _i_status=0
  538. pacman -S --needed --noconfirm --noprogressbar -- "$@" \
  539. 2>"$WORK/inst.err" || _i_status=$?
  540. printf '%s\n' "$_i_status" >"$WORK/rc"
  541. } | tr '\r' '\n' | {
  542. _i_done=0
  543. _i_lastpkg=
  544. while IFS= read -r _i_line || [ -n "$_i_line" ]; do
  545. # pacman's transaction output names each package exactly
  546. # once: older versions redraw "(k/N) installing pkg" lines
  547. # (\r-flattened here), pacman 7 pipes plain "installing
  548. # pkg..." lines. Dedupe on the package name either way.
  549. case $_i_line in
  550. \(*\)\ *)
  551. _i_kn=${_i_line#"("}
  552. _i_kn=${_i_kn%%")"*}
  553. _i_rest=${_i_line#*") "}
  554. _i_verb=${_i_rest%% *}
  555. _i_pkg=${_i_rest#* }
  556. _i_pkg=${_i_pkg%% *}
  557. _i_k=${_i_kn%%/*}
  558. _i_n=${_i_kn##*/}
  559. ;;
  560. installing\ *|upgrading\ *|downgrading\ *|reinstalling\ *)
  561. _i_verb=${_i_line%% *}
  562. _i_pkg=${_i_line#* }
  563. _i_pkg=${_i_pkg%% *}
  564. _i_pkg=${_i_pkg%"..."}
  565. _i_k=$((_i_done + 1))
  566. _i_n=$_i_total
  567. ;;
  568. *)
  569. continue
  570. ;;
  571. esac
  572. case $_i_verb in
  573. installing|upgrading|downgrading|reinstalling)
  574. if [ "$_i_pkg" != "$_i_lastpkg" ]; then
  575. _i_lastpkg=$_i_pkg
  576. emit "{\"type\":\"package\",\"name\":\"$(json_escape "$_i_pkg")\",\"current\":$_i_k,\"total\":$_i_n,\"progress\":1.0}"
  577. emit "{\"type\":\"package-complete\",\"name\":\"$(json_escape "$_i_pkg")\"}"
  578. _i_done=$((_i_done + 1))
  579. fi
  580. ;;
  581. esac
  582. done
  583. printf '%s\n' "$_i_done" >"$WORK/done"
  584. }
  585. _i_rc=$(cat "$WORK/rc" 2>/dev/null) || _i_rc=1
  586. if [ "$_i_rc" = 0 ]; then
  587. _i_done=$(cat "$WORK/done" 2>/dev/null) || _i_done=0
  588. emit "{\"type\":\"complete\",\"status\":\"ok\",\"installed\":$_i_done}"
  589. exit "$EXIT_OK"
  590. fi
  591. _i_msg=$(pacman_diag_line "$WORK/inst.err")
  592. [ -n "$_i_msg" ] || _i_msg="pacman -S failed with status $_i_rc"
  593. emit "{\"type\":\"error\",\"message\":\"$(json_escape "$_i_msg")\"}"
  594. exit "$(classify_failure "$WORK/inst.err")"
  595. }
  596. cmd_plan() {
  597. [ $# -ge 1 ] || die_usage
  598. for _p_n in "$@"; do
  599. case $_p_n in
  600. -*) die_usage ;;
  601. esac
  602. done
  603. mkwork
  604. _p_rc=0
  605. _p_names=$(pacman -S --needed --noconfirm --print --print-format '%n' \
  606. -- "$@" 2>"$WORK/plan.err") || _p_rc=$?
  607. if [ "$_p_rc" != 0 ]; then
  608. _p_msg=$(pacman_diag_line "$WORK/plan.err")
  609. [ -n "$_p_msg" ] || _p_msg="pacman --print failed with status $_p_rc"
  610. printf '%s: %s\n' "$PROG" "$_p_msg" >&2
  611. exit "$(classify_failure "$WORK/plan.err")"
  612. fi
  613. _p_json=$(printf '%s\n' "$_p_names" \
  614. | sed -n '/^[A-Za-z0-9@._+-]\{1,\}$/s/.*/"&"/p' \
  615. | awk 'NR>1{printf ","} {printf "%s", $0}')
  616. printf '{"packages":[%s]}\n' "$_p_json"
  617. exit "$EXIT_OK"
  618. }
  619. main() {
  620. if [ $# -lt 1 ]; then
  621. die_usage
  622. fi
  623. if ! command -v pacman >/dev/null 2>&1; then
  624. if [ "$1" = install ]; then
  625. emit '{"type":"error","message":"pacman not found in PATH"}'
  626. exit "$EXIT_FAILURE"
  627. fi
  628. warn "pacman not found in PATH"
  629. exit "$EXIT_RESOLVE"
  630. fi
  631. _m_cmd=$1
  632. shift
  633. case $_m_cmd in
  634. query)
  635. cmd_query "$@"
  636. ;;
  637. plan)
  638. cmd_plan "$@"
  639. ;;
  640. install)
  641. cmd_install "$@"
  642. ;;
  643. -h|--help|help)
  644. printf 'usage: %s query <usm-ref>...\n' "$PROG"
  645. printf ' %s plan <native-name>...\n' "$PROG"
  646. printf ' %s install <native-name>...\n' "$PROG"
  647. exit "$EXIT_OK"
  648. ;;
  649. *)
  650. die_usage
  651. ;;
  652. esac
  653. }
  654. main "$@"