usm-spm-apk 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. #!/bin/sh
  2. # USM system-package-manager helper for apk-based systems (Alpine).
  3. #
  4. # Implements the USM SPM contract (see usm/README.md, "System package
  5. # manager integration") for Alpine-style systems:
  6. #
  7. # usm-spm-apk query <usm-ref>... -> contract JSON on STDOUT
  8. # usm-spm-apk install <native-name>... -> contract JSONL events on STDOUT
  9. #
  10. # API choice: pure POSIX shell (busybox ash) on purpose -- the Alpine
  11. # base image ships neither python3 nor jq, and the SPM bootstrap must
  12. # stay at zero extra packages (busybox is already there). apk-tools
  13. # >= 2.10 (alpine:latest currently ships apk-tools 3.0.6) expose
  14. # provides namespaces that map USM refs natively, so file-list indexes
  15. # are not needed for the hot types:
  16. #
  17. # bin:/sbin: X -> provides "cmd:X" (abuild auto-generates for
  18. # every packaged executable, /usr/bin and /sbin)
  19. # lib: X -> provides "so:X" (verbatim soname)
  20. # pc: X -> provides "pc:${X%.pc}" -- apk indexes pkg-config
  21. # module names WITHOUT the ".pc" suffix (observed:
  22. # glib-dev provides pc:glib-2.0, vala provides
  23. # pc:libvala-0.56), so one trailing ".pc" is
  24. # stripped from the ref before querying
  25. #
  26. # Providers are found with `apk search -x <provide>` (exact match that
  27. # also matches provides); the from-scratch solo-install closure comes
  28. # from `apk search --recursive <name>` (runs the solver against the
  29. # repositories only -- same semantics as the dnf helper's repo-only
  30. # sack); installed-dependency-count is the overlap of that closure with
  31. # the installed database (`apk info`). Residual resource types (inc:,
  32. # vapi:, gir:, typelib:, gio:, res:, cfg:, man:, info:, locale:,
  33. # libexec:, rootpath:, tag:) are resolved against the installed database only via
  34. # `apk info -W`/directory sampling: APKINDEX carries no file lists, so
  35. # files of not-yet-installed packages are unresolvable and land in
  36. # "not-found" (documented limitation, the same offline stance as the
  37. # emerge helper).
  38. #
  39. # Every package object also carries a "conflicts" array: apk has no
  40. # standalone conflict field, but dependency blockers ("!virtual", e.g.
  41. # dropbear-scp depends on !openssh-client) name the provider families
  42. # the package cannot coexist with. `apk info --depends <pkg>` (which,
  43. # with a primed index cache, also answers for NOT-YET-INSTALLED
  44. # packages) lists them, and each blocker virtual is resolved to concrete
  45. # provider names with the same exact-provides search used for refs.
  46. #
  47. # apk has no per-download progress output on a non-tty stdout; it
  48. # prints one "(k/N) Installing pkg (ver)" line per package instead, so
  49. # "package" events are emitted at package-completion granularity
  50. # (progress 1.0) driven by those lines, and begin.total comes from an
  51. # `apk add --simulate` pass against the current system.
  52. #
  53. # Install transactions expand INSTALL_COMPANIONS (currently gcc ->
  54. # musl-dev): apk's gcc package deliberately ships no crt objects, and a
  55. # USM manifest cannot express them as resource refs.
  56. #
  57. # The query subcommand never modifies system state: repository indexes
  58. # are fetched into a throwaway cache directory (at most refreshing
  59. # package-metadata caches). The install subcommand must run as root;
  60. # apk never prompts interactively: unresolved dependencies or bad
  61. # signatures fail instead.
  62. #
  63. # Alpine path table (musl/busybox layout, no lib64): /usr/bin, /sbin,
  64. # /lib, /usr/lib, /usr/include, /usr/share (+/usr/share/pkgconfig).
  65. set -eu
  66. export LC_ALL=C
  67. PROG=usm-spm-apk
  68. EXIT_OK=0
  69. EXIT_FAILURE=1
  70. EXIT_USAGE=2
  71. EXIT_RESOLVE=3
  72. EXIT_DOWNLOAD=4
  73. EXIT_TRANSACTION=5
  74. WORK=""
  75. cleanup() {
  76. if [ -n "$WORK" ]; then
  77. rm -rf "$WORK"
  78. fi
  79. }
  80. warn() {
  81. printf '%s: %s\n' "$PROG" "$*" >&2
  82. }
  83. die_usage() {
  84. printf 'usage: %s query <usm-ref>...\n' "$PROG" >&2
  85. printf ' %s plan <native-name>...\n' "$PROG" >&2
  86. printf ' %s install <native-name>...\n' "$PROG" >&2
  87. exit "$EXIT_USAGE"
  88. }
  89. emit() {
  90. printf '%s\n' "$1"
  91. }
  92. json_escape() {
  93. printf '%s' "$1" | awk '
  94. {
  95. s = $0
  96. gsub(/\\/, "\\\\", s)
  97. gsub(/"/, "\\\"", s)
  98. gsub(/\t/, "\\t", s)
  99. gsub(/[[:cntrl:]]/, "", s)
  100. print s
  101. }'
  102. }
  103. mkwork() {
  104. WORK=$(mktemp -d "${TMPDIR:-/tmp}/usm-spm-apk.XXXXXX" 2>/dev/null) || {
  105. WORK="${TMPDIR:-/tmp}/usm-spm-apk.$$"
  106. (umask 077 && mkdir "$WORK")
  107. }
  108. trap cleanup EXIT
  109. }
  110. prime_index_cache() {
  111. CACHE="$WORK/cache"
  112. PRIMED=0
  113. if mkdir -p "$CACHE" && apk update -q --cache-dir "$CACHE" >/dev/null 2>"$WORK/prime.err"; then
  114. if [ -n "$(ls -A "$CACHE" 2>/dev/null)" ]; then
  115. PRIMED=1
  116. fi
  117. fi
  118. if [ "$PRIMED" != 1 ]; then
  119. warn "could not prime a repository index cache; every lookup will fetch indexes"
  120. fi
  121. }
  122. sp_search() {
  123. if [ "$PRIMED" = 1 ]; then
  124. apk search --cache-dir "$CACHE" "$@"
  125. else
  126. apk search --no-cache "$@"
  127. fi
  128. }
  129. sp_info() {
  130. if [ "$PRIMED" = 1 ]; then
  131. apk info --cache-dir "$CACHE" "$@"
  132. else
  133. apk info --no-cache "$@"
  134. fi
  135. }
  136. # apk prefixes diagnostics with "ERROR:"/"WARNING:" and, for solver
  137. # failures, follows the headline with indented detail lines naming the
  138. # actual conflicting packages ("conflicts:", "breaks:", "satisfies:").
  139. # Return the headline plus those details (up to 8, joined with "; ") on
  140. # one line so error events carry the whole report instead of only its
  141. # first line.
  142. apk_diag_line() {
  143. [ -r "$1" ] || return 0
  144. awk '
  145. /^(ERROR|WARNING):[[:space:]]*/ && diag == "" {
  146. sub(/^(ERROR|WARNING):[[:space:]]*/, "")
  147. diag = $0
  148. details = 0
  149. next
  150. }
  151. diag != "" && details < 8 && /^[[:space:]]+[^[:space:]]/ {
  152. line = $0
  153. sub(/^[[:space:]]+/, "", line)
  154. diag = diag "; " line
  155. details++
  156. next
  157. }
  158. diag != "" && /^[^[:space:]]/ { exit }
  159. END { if (diag != "") print diag }
  160. ' "$1"
  161. }
  162. strip_version() {
  163. sed -e 's/-r[0-9][0-9]*$//' -e 's/-[0-9][0-9A-Za-z._+]*$//'
  164. }
  165. not_found_add() {
  166. printf '%s\n' "$1" >>"$WORK/nf"
  167. }
  168. map_add() {
  169. printf '%s\t%s\n' "$1" "$2" >>"$WORK/map"
  170. }
  171. record_providers() {
  172. _rp_ref=$1
  173. _rp_prov=$2
  174. _rp_out=$(sp_search -x -- "$_rp_prov" 2>"$WORK/search.err" || true)
  175. if { [ -z "$_rp_out" ] && [ -s "$WORK/search.err" ]; } || grep -q '^ERROR' "$WORK/search.err" 2>/dev/null; then
  176. warn "apk search failed for \"$_rp_prov\": $(apk_diag_line "$WORK/search.err")"
  177. exit "$EXIT_RESOLVE"
  178. fi
  179. if [ -n "$_rp_out" ]; then
  180. printf '%s\n' "$_rp_out" | strip_version | sort -u >"$WORK/pv"
  181. while IFS= read -r _rp_nm; do
  182. [ -n "$_rp_nm" ] || continue
  183. map_add "$_rp_nm" "$_rp_ref"
  184. done <"$WORK/pv"
  185. else
  186. not_found_add "$_rp_ref"
  187. fi
  188. }
  189. record_file_owner() {
  190. _fo_ref=$1
  191. shift
  192. _fo_hit=0
  193. for _fo_p in "$@"; do
  194. _fo_f=
  195. if [ -d "$_fo_p" ]; then
  196. _fo_f=$(find "$_fo_p" -type f 2>/dev/null | head -n 1) || _fo_f=
  197. elif [ -f "$_fo_p" ]; then
  198. _fo_f=$_fo_p
  199. else
  200. continue
  201. fi
  202. [ -n "$_fo_f" ] || continue
  203. _fo_own=$(apk info -W "$_fo_f" 2>/dev/null) || continue
  204. _fo_nm=$(printf '%s\n' "$_fo_own" | sed -n 's/^.* is owned by //p' | strip_version)
  205. [ -n "$_fo_nm" ] || continue
  206. map_add "$_fo_nm" "$_fo_ref"
  207. _fo_hit=1
  208. done
  209. if [ "$_fo_hit" != 1 ]; then
  210. not_found_add "$_fo_ref"
  211. fi
  212. }
  213. handle_ref() {
  214. _hr_ref=$1
  215. _hr_type=${_hr_ref%%:*}
  216. _hr_res=${_hr_ref#*:}
  217. if [ "$_hr_type" = "$_hr_ref" ] || [ -z "$_hr_res" ]; then
  218. warn "resource type of \"$_hr_ref\" has no system-package-manager translation"
  219. not_found_add "$_hr_ref"
  220. return
  221. fi
  222. case $_hr_res in
  223. *[!A-Za-z0-9._+/-]*)
  224. warn "resource name of \"$_hr_ref\" has no system-package-manager translation"
  225. not_found_add "$_hr_ref"
  226. return
  227. ;;
  228. esac
  229. case $_hr_type in
  230. bin|sbin)
  231. record_providers "$_hr_ref" "cmd:$_hr_res"
  232. ;;
  233. lib)
  234. record_providers "$_hr_ref" "so:$_hr_res"
  235. ;;
  236. pc)
  237. record_providers "$_hr_ref" "pc:${_hr_res%.pc}"
  238. ;;
  239. libexec)
  240. record_file_owner "$_hr_ref" "/usr/libexec/$_hr_res"
  241. ;;
  242. gir)
  243. record_file_owner "$_hr_ref" "/usr/share/gir-1.0/$_hr_res"
  244. ;;
  245. typelib)
  246. record_file_owner "$_hr_ref" "/usr/lib/girepository-1.0/$_hr_res"
  247. ;;
  248. gio)
  249. record_file_owner "$_hr_ref" "/usr/lib/gio/modules/$_hr_res"
  250. ;;
  251. res)
  252. record_file_owner "$_hr_ref" "/usr/share/$_hr_res"
  253. ;;
  254. cfg)
  255. record_file_owner "$_hr_ref" "/etc/$_hr_res"
  256. ;;
  257. man)
  258. record_file_owner "$_hr_ref" "/usr/share/man/$_hr_res"
  259. ;;
  260. info)
  261. record_file_owner "$_hr_ref" "/usr/share/info/$_hr_res"
  262. ;;
  263. locale)
  264. record_file_owner "$_hr_ref" "/usr/share/locale/$_hr_res"
  265. ;;
  266. inc)
  267. record_file_owner "$_hr_ref" "/usr/include/$_hr_res"
  268. ;;
  269. vapi)
  270. set -- "/usr/share/vala/vapi/$_hr_res"
  271. for _hr_d in /usr/share/vala-*/vapi/"$_hr_res"; do
  272. if [ -f "$_hr_d" ]; then
  273. set -- "$@" "$_hr_d"
  274. fi
  275. done
  276. record_file_owner "$_hr_ref" "$@"
  277. ;;
  278. rootpath)
  279. record_file_owner "$_hr_ref" "/$_hr_res"
  280. ;;
  281. tag)
  282. _hr_t=${_hr_res%.tag}
  283. record_file_owner "$_hr_ref" "/usr/share/usm-tags/$(printf '%s' "$_hr_t" | tr '.' '/').tag"
  284. ;;
  285. *)
  286. warn "resource type of \"$_hr_ref\" has no system-package-manager translation"
  287. not_found_add "$_hr_ref"
  288. return
  289. ;;
  290. esac
  291. }
  292. counts_for() {
  293. _cf_name=$1
  294. _cf_cached=
  295. if [ -s "$WORK/counts" ]; then
  296. _cf_cached=$(awk -F'\t' -v n="$_cf_name" '$1 == n { print $2 " " $3; exit }' "$WORK/counts") || _cf_cached=
  297. fi
  298. if [ -n "$_cf_cached" ]; then
  299. printf '%s\n' "$_cf_cached"
  300. return
  301. fi
  302. _cf_cl=$(sp_search --recursive -- "$_cf_name" 2>"$WORK/rec.err" || true)
  303. if [ -z "$_cf_cl" ] || grep -q '^ERROR' "$WORK/rec.err" 2>/dev/null; then
  304. warn "could not resolve solo install of $_cf_name, estimating dependency counts"
  305. if grep -qx -F "$_cf_name" "$WORK/installed" 2>/dev/null; then
  306. printf '1 1\n'
  307. else
  308. printf '1 0\n'
  309. fi
  310. return
  311. fi
  312. printf '%s\n' "$_cf_cl" | strip_version >"$WORK/cl"
  313. _cf_dep=$(awk 'END { print NR }' "$WORK/cl")
  314. _cf_inst=0
  315. if [ -s "$WORK/installed" ]; then
  316. _cf_inst=$(grep -x -F -f "$WORK/installed" "$WORK/cl" | awk 'END { print NR }') || _cf_inst=0
  317. fi
  318. printf '%s\t%s\t%s\n' "$_cf_name" "$_cf_dep" "$_cf_inst" >>"$WORK/counts"
  319. printf '%s %s\n' "$_cf_dep" "$_cf_inst"
  320. }
  321. # Print the "conflicts" JSON array for a package: apk's dependency
  322. # blockers ("!virtual") resolve to the concrete provider names the
  323. # package cannot coexist with (e.g. dropbear-scp blocks every provider
  324. # of openssh-client). One `apk info --depends` plus one exact search per
  325. # blocker, only in query mode; empty array when nothing conflicts.
  326. conflicts_for() {
  327. _cq_name=$1
  328. _cq_deps=$(sp_info --depends -- "$_cq_name" 2>"$WORK/cf.err" || true)
  329. printf '%s\n' "$_cq_deps" \
  330. | sed -n 's/^[[:space:]]*!\([^[:space:]=<>~]*\).*/\1/p' >"$WORK/cfblk"
  331. while IFS= read -r _cq_blk; do
  332. [ -n "$_cq_blk" ] || continue
  333. sp_search -x -- "$_cq_blk" 2>>"$WORK/cf.err" || true
  334. done <"$WORK/cfblk" | strip_version | sort -u >"$WORK/cfn"
  335. _cq_json=""
  336. _cq_first=1
  337. while IFS= read -r _cq_c; do
  338. [ -n "$_cq_c" ] || continue
  339. if [ "$_cq_first" = 1 ]; then _cq_first=0; else _cq_json="$_cq_json,"; fi
  340. _cq_json="$_cq_json\"$(json_escape "$_cq_c")\""
  341. done <"$WORK/cfn"
  342. printf '[%s]' "$_cq_json"
  343. }
  344. cmd_query() {
  345. [ $# -ge 1 ] || die_usage
  346. mkwork
  347. prime_index_cache
  348. apk info >"$WORK/installed" 2>/dev/null || : >"$WORK/installed"
  349. : >"$WORK/map"
  350. : >"$WORK/nf"
  351. : >"$WORK/counts"
  352. : >"$WORK/seen"
  353. for _q_ref in "$@"; do
  354. if [ -n "$_q_ref" ] && grep -x -F -q "$_q_ref" "$WORK/seen" 2>/dev/null; then
  355. continue
  356. fi
  357. printf '%s\n' "$_q_ref" >>"$WORK/seen"
  358. handle_ref "$_q_ref"
  359. done
  360. _q_out='{"not-found":['
  361. _q_first=1
  362. while IFS= read -r _q_r; do
  363. [ -n "$_q_r" ] || continue
  364. if [ "$_q_first" = 1 ]; then _q_first=0; else _q_out="$_q_out,"; fi
  365. _q_out="$_q_out\"$(json_escape "$_q_r")\""
  366. done <"$WORK/nf"
  367. _q_out="$_q_out],\"packages\":["
  368. _q_first=1
  369. for _q_nm in $(awk -F'\t' '{ print $1 }' "$WORK/map" | sort -u); do
  370. if [ "$_q_first" = 1 ]; then _q_first=0; else _q_out="$_q_out,"; fi
  371. awk -F'\t' -v n="$_q_nm" '$1 == n { print $2 }' "$WORK/map" >"$WORK/tmpres"
  372. _q_res=""
  373. _q_rf=1
  374. while IFS= read -r _q_rr; do
  375. if [ "$_q_rf" = 1 ]; then _q_rf=0; else _q_res="$_q_res,"; fi
  376. _q_res="$_q_res\"$(json_escape "$_q_rr")\""
  377. done <"$WORK/tmpres"
  378. _q_cnts=$(counts_for "$_q_nm")
  379. _q_dep=${_q_cnts%% *}
  380. _q_inst=${_q_cnts##* }
  381. _q_conf=$(conflicts_for "$_q_nm")
  382. _q_out="$_q_out{\"name\":\"$(json_escape "$_q_nm")\",\"resources\":[${_q_res}],\"dependency-count\":$_q_dep,\"installed-dependency-count\":$_q_inst,\"conflicts\":$_q_conf}"
  383. done
  384. _q_out="$_q_out]}"
  385. emit "$_q_out"
  386. exit "$EXIT_OK"
  387. }
  388. classify_failure() {
  389. if grep -Eqi 'fetch|download|network|temporary failure|connection refused|timed out|untrusted|checksum|signature|mirror|404|503' "$1" 2>/dev/null; then
  390. printf '%s\n' "$EXIT_DOWNLOAD"
  391. elif grep -Eq 'unable to select|constraint|conflict|world' "$1" 2>/dev/null; then
  392. printf '%s\n' "$EXIT_RESOLVE"
  393. else
  394. printf '%s\n' "$EXIT_TRANSACTION"
  395. fi
  396. }
  397. # Packages whose apk split leaves a toolchain unusable alone: gcc ships no
  398. # crt objects (musl-dev owns Scrt1.o/crti.o/libssp_nonshared.a), so linking
  399. # fails without it. USM manifests cannot express crt files as resource
  400. # refs; this table completes the toolchain at install time instead.
  401. INSTALL_COMPANIONS="gcc:musl-dev"
  402. cmd_install() {
  403. [ $# -ge 1 ] || die_usage
  404. for _i_n in "$@"; do
  405. case $_i_n in
  406. -*) die_usage ;;
  407. esac
  408. done
  409. _i_expanded=""
  410. for _i_n in "$@"; do
  411. _i_expanded="$_i_expanded $_i_n"
  412. for _i_c in $INSTALL_COMPANIONS; do
  413. if [ "${_i_c%%:*}" = "$_i_n" ]; then
  414. _i_expanded="$_i_expanded ${_i_c#*:}"
  415. fi
  416. done
  417. done
  418. set -- $_i_expanded
  419. mkwork
  420. _i_simrc=0
  421. _i_sim=$(apk add --simulate --no-cache --no-progress -- "$@" 2>"$WORK/sim.err") || _i_simrc=$?
  422. if [ "$_i_simrc" != 0 ]; then
  423. _i_msg=$(apk_diag_line "$WORK/sim.err")
  424. [ -n "$_i_msg" ] || _i_msg="apk add --simulate failed with status $_i_simrc"
  425. emit "{\"type\":\"error\",\"message\":\"$(json_escape "$_i_msg")\"}"
  426. exit "$(classify_failure "$WORK/sim.err")"
  427. fi
  428. _i_total=$(printf '%s\n' "$_i_sim" | sed -n 's/^(\([0-9][0-9]*\)\/\([0-9][0-9]*\)).*/\2/p' | tail -n 1)
  429. [ -n "$_i_total" ] || _i_total=0
  430. emit "{\"type\":\"begin\",\"total\":$_i_total}"
  431. {
  432. apk add --no-cache --no-progress -- "$@" 2>"$WORK/inst.err"
  433. printf '%s\n' "$?" >"$WORK/rc"
  434. } | {
  435. _i_done=0
  436. while IFS= read -r _i_line || [ -n "$_i_line" ]; do
  437. case $_i_line in
  438. \(*\))
  439. _i_kn=${_i_line#"("}
  440. _i_kn=${_i_kn%%")"*}
  441. _i_k=${_i_kn%%/*}
  442. _i_n=${_i_kn##*/}
  443. _i_rest=${_i_line#*") "}
  444. _i_verb=${_i_rest%% *}
  445. _i_pkg=${_i_rest#* }
  446. _i_pkg=${_i_pkg%% *}
  447. case $_i_verb in
  448. Installing|Upgrading|Reinstalling|Downgrading)
  449. emit "{\"type\":\"package\",\"name\":\"$(json_escape "$_i_pkg")\",\"current\":$_i_k,\"total\":$_i_n,\"progress\":1.0}"
  450. emit "{\"type\":\"package-complete\",\"name\":\"$(json_escape "$_i_pkg")\"}"
  451. _i_done=$((_i_done + 1))
  452. ;;
  453. esac
  454. ;;
  455. esac
  456. done
  457. printf '%s\n' "$_i_done" >"$WORK/done"
  458. }
  459. _i_rc=$(cat "$WORK/rc" 2>/dev/null) || _i_rc=1
  460. if [ "$_i_rc" = 0 ]; then
  461. _i_done=$(cat "$WORK/done" 2>/dev/null) || _i_done=0
  462. emit "{\"type\":\"complete\",\"status\":\"ok\",\"installed\":$_i_done}"
  463. exit "$EXIT_OK"
  464. fi
  465. _i_msg=$(apk_diag_line "$WORK/inst.err")
  466. [ -n "$_i_msg" ] || _i_msg="apk add failed with status $_i_rc"
  467. emit "{\"type\":\"error\",\"message\":\"$(json_escape "$_i_msg")\"}"
  468. exit "$(classify_failure "$WORK/inst.err")"
  469. }
  470. cmd_plan() {
  471. [ $# -ge 1 ] || die_usage
  472. for _p_n in "$@"; do
  473. case $_p_n in
  474. -*) die_usage ;;
  475. esac
  476. done
  477. _p_expanded=""
  478. for _p_n in "$@"; do
  479. _p_expanded="$_p_expanded $_p_n"
  480. for _p_c in $INSTALL_COMPANIONS; do
  481. if [ "${_p_c%%:*}" = "$_p_n" ]; then
  482. _p_expanded="$_p_expanded ${_p_c#*:}"
  483. fi
  484. done
  485. done
  486. set -- $_p_expanded
  487. mkwork
  488. _p_rc=0
  489. _p_sim=$(apk add --simulate --no-cache --no-progress -- "$@" 2>"$WORK/plan.err") || _p_rc=$?
  490. if [ "$_p_rc" != 0 ]; then
  491. _p_msg=$(apk_diag_line "$WORK/plan.err")
  492. [ -n "$_p_msg" ] || _p_msg="apk add --simulate failed with status $_p_rc"
  493. printf '%s: %s\n' "$PROG" "$_p_msg" >&2
  494. exit "$(classify_failure "$WORK/plan.err")"
  495. fi
  496. # simulate output names every package the transaction would touch:
  497. # "(3/7) Installing musl (1.2.5-r1)"
  498. _p_json=$(printf '%s\n' "$_p_sim" \
  499. | sed -n 's/^(\([0-9][0-9]*\)\/\([0-9][0-9]*\)) Installing \([^ ]*\) .*/"\3"/p' \
  500. | awk 'NR>1{printf ","} {printf "%s", $0}')
  501. printf '{"packages":[%s]}\n' "$_p_json"
  502. exit "$EXIT_OK"
  503. }
  504. main() {
  505. if [ $# -lt 1 ]; then
  506. die_usage
  507. fi
  508. if ! command -v apk >/dev/null 2>&1; then
  509. if [ "$1" = install ]; then
  510. emit '{"type":"error","message":"apk not found in PATH"}'
  511. exit "$EXIT_FAILURE"
  512. fi
  513. warn "apk not found in PATH"
  514. exit "$EXIT_RESOLVE"
  515. fi
  516. _m_cmd=$1
  517. shift
  518. case $_m_cmd in
  519. query)
  520. cmd_query "$@"
  521. ;;
  522. plan)
  523. cmd_plan "$@"
  524. ;;
  525. install)
  526. cmd_install "$@"
  527. ;;
  528. -h|--help|help)
  529. printf 'usage: %s query <usm-ref>...\n' "$PROG"
  530. printf ' %s install <native-name>...\n' "$PROG"
  531. exit "$EXIT_OK"
  532. ;;
  533. *)
  534. die_usage
  535. ;;
  536. esac
  537. }
  538. main "$@"