usm-spm-pacman 21 KB

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