usm-spm-apt 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. #!/usr/bin/env python3
  2. """USM system-package-manager helper for APT-based systems.
  3. Implements the USM SPM contract (see usm/README.md, "System package manager
  4. integration") for Debian-style systems:
  5. usm-spm-apt query <usm-ref>... -> contract JSON on STDOUT
  6. usm-spm-apt install <name>... -> contract JSONL events on STDOUT
  7. File queries are answered from apt-file's Contents index ONLY. This helper
  8. therefore requires apt-file to be installed and its index updated
  9. (`apt-file update`; ~100 MB of Contents per enabled architecture -- the
  10. container bootstrap used by `usm deploy --spm apt` performs both). There
  11. are no package-name heuristics: any ref whose translated path has no owner
  12. in the index lands in not-found. The two sanctioned exceptions are curated
  13. tables: SONAME_PACKAGES (sonames whose Debian package lags the upstream
  14. soname the manifests reference) and PACKAGE_REMAPS (owners remapped to the
  15. package that makes the toolchain usable, e.g. valac-bin to the valac
  16. meta-package carrying the split-out vapis). One
  17. deviation from the DNF helper, which
  18. unions repository filelists with the local rpmdb: files that exist only on
  19. the local system (packages installed from .debs outside the configured
  20. repositories) are invisible to apt-file, so such refs land in not-found.
  21. The DPKG database is consulted for package NAMES only, to compute
  22. installed-dependency-count.
  23. The query translation mirrors spm/dnf with Debian multiarch paths,
  24. preferring x86_64 and following the machine architecture otherwise:
  25. bin/sbin/libexec -> /usr/bin, /usr/sbin, /usr/bin (Debian ships helper
  26. executables under /usr/bin; no /usr/libexec tree)
  27. lib -> /usr/lib/<triple>/<soname> and /lib/<triple>/<soname>
  28. (usr-merge: both path forms appear across suites)
  29. gio -> /usr/lib/<triple>/gio/modules/<module>
  30. pc -> /usr/lib/<triple>/pkgconfig/<n> and /usr/share/pkgconfig/<n>
  31. inc -> /usr/include/<P> prefix
  32. typelib -> /usr/lib/<triple>/girepository-1.0/<n>
  33. vapi -> /usr/share/vala/vapi/<n> and /usr/share/vala-*/vapi/<n>
  34. gir/res/cfg/man/info/locale/rootpath/tag -> as in the DNF table
  35. apt-file is invoked as `apt-file search --fixed-string` (exact paths) and
  36. `apt-file search --substring-match` (anchored prefixes: inc: and the
  37. versioned vapi directory), with all patterns of one mode batched via
  38. --from-file (falling back to one invocation per pattern on apt-file builds
  39. without --from-file). apt-file 3.3 reads --fixed-string as a full-path
  40. exact match and exits 1 without diagnostics when a pattern has no owner,
  41. which this helper treats as not-found rather than failure; every result is
  42. additionally re-validated against an exact or anchored-prefix path
  43. predicate before being credited to a ref, so no over-matches leak into the
  44. contract output.
  45. dependency-count models "a solo install on this machine" exactly like the
  46. DNF helper's repository-only sack: python3-apt resolves the package with
  47. DPkg status pointed at /dev/null (nothing installed), so every dependency
  48. resolves as an install; the overlap with the real DPKG status is reported
  49. as installed-dependency-count. If the python3-apt cache cannot be built or
  50. a package cannot be marked, counts degrade to the DNF helper's fallback
  51. (1, installed ? 1 : 0) with a note on STDERR.
  52. Install runs one `apt-get install -y` transaction with
  53. DEBIAN_FRONTEND=noninteractive and never prompts. An `apt-get --simulate`
  54. pass first provides the transaction size and order for the begin event;
  55. real-run "Get:", "Unpacking" and "Setting up" lines are then mapped to
  56. (coalesced) package progress events and package-complete events. There is
  57. no --test mode: apt cannot run the dpkg transaction phases that the DNF
  58. --test mode exercises. Exit codes match the contract table, with 5 meaning
  59. a dpkg transaction failure (apt's analogue of the rpm code).
  60. """
  61. import argparse
  62. import json
  63. import os
  64. import platform
  65. import re
  66. import shutil
  67. import subprocess
  68. import sys
  69. import tempfile
  70. import apt.progress.base
  71. import apt_pkg
  72. MULTIARCH_TRIPLES = {
  73. "x86_64": "x86_64-linux-gnu",
  74. "aarch64": "aarch64-linux-gnu",
  75. "armv7l": "arm-linux-gnueabihf",
  76. "i386": "i386-linux-gnu",
  77. "i686": "i386-linux-gnu",
  78. "ppc64le": "powerpc64le-linux-gnu",
  79. "s390x": "s390x-linux-gnu",
  80. }
  81. TRIPLE = MULTIARCH_TRIPLES.get(platform.machine(), "x86_64-linux-gnu")
  82. # Sonames whose Debian package lags the soname the Web-Stack manifests
  83. # reference: the Contents index has no owner for the exact path, but the
  84. # library is present under an older soname (libsodium23 ships
  85. # libsodium.so.23 while the manifests reference the upstream .26). Curated
  86. # like the emerge helper's atom exception table; entries are validated
  87. # against the apt cache so a stale mapping degrades to not-found instead
  88. # of resolving a ghost package.
  89. SONAME_PACKAGES = {
  90. "libsodium.so.26": "libsodium23",
  91. }
  92. # Owners the Contents index reports for a file, remapped to the package
  93. # that actually makes the toolchain usable: Debian splits valac's vapis
  94. # into valac-<version>-vapi, which valac-bin does not depend on, and keeps
  95. # g-ir-compiler's real binary in gobject-introspection-bin-linux, which
  96. # gobject-introspection-bin (owner of the dangling /usr/bin symlink) does
  97. # not depend on either; the valac and gobject-introspection meta-packages
  98. # pull each split together. Answering with the meta-package keeps one
  99. # install transaction sufficient.
  100. PACKAGE_REMAPS = {
  101. "valac-bin": "valac",
  102. "gobject-introspection-bin": "gobject-introspection",
  103. }
  104. EXIT_OK = 0
  105. EXIT_FAILURE = 1
  106. EXIT_RESOLVE = 3
  107. EXIT_DOWNLOAD = 4
  108. EXIT_TRANSACTION = 5
  109. GET_LINE = re.compile(r"^Get:\d+\s")
  110. UNPACK_LINE = re.compile(r"^Unpacking\s+(\S+)")
  111. SETUP_LINE = re.compile(r"^Setting up\s+(\S+)")
  112. INST_LINE = re.compile(r"^Inst\s+(\S+)")
  113. ERROR_LINE = re.compile(r"^E:\s+")
  114. class HelperError(Exception):
  115. """Fatal helper failure; message is emitted as a contract error event."""
  116. def __init__(self, message, exit_code=EXIT_FAILURE):
  117. super(HelperError, self).__init__(message)
  118. self.exit_code = exit_code
  119. def emit_event(event):
  120. """Write one contract JSONL event to STDOUT and flush it."""
  121. sys.stdout.write(json.dumps(event) + "\n")
  122. sys.stdout.flush()
  123. def fail(message, exit_code=EXIT_FAILURE):
  124. """Emit the terminal error event and exit non-zero."""
  125. emit_event({"type": "error", "message": message})
  126. sys.exit(exit_code)
  127. def fail_plain(message, exit_code=EXIT_FAILURE):
  128. """Report a query failure on STDERR only, keeping STDOUT contract-clean."""
  129. sys.stderr.write("usm-spm-apt: %s\n" % message)
  130. sys.exit(exit_code)
  131. def plain_name(token):
  132. """Strip a multiarch :arch qualifier from an apt/dpkg package token."""
  133. return token.split(":", 1)[0]
  134. def exact(path):
  135. """Descriptor for one exact indexed path (apt-file --fixed-string)."""
  136. return ("exact", path, lambda candidate, path=path: candidate == path)
  137. def prefixed(path):
  138. """Descriptor matching indexed paths under path (--substring-match)."""
  139. return ("substring", path,
  140. lambda candidate, path=path: candidate.startswith(path))
  141. def vapi_versioned(resource):
  142. """Descriptor for /usr/share/vala-*/vapi/<resource>."""
  143. return (
  144. "substring",
  145. "/usr/share/vala-",
  146. lambda candidate, resource=resource: (
  147. candidate.startswith("/usr/share/vala-")
  148. and candidate.endswith("/vapi/" + resource)
  149. ),
  150. )
  151. def translate_ref(ref):
  152. """Map a USM resource ref to apt-file (mode, pattern, predicate) triples.
  153. Mode is "exact" for --fixed-string full-path matches and "substring"
  154. for --substring-match anchored prefixes. Returns None for refs whose
  155. type has no file translation.
  156. """
  157. prefix, sep, resource = ref.partition(":")
  158. if not sep or not resource or any(ord(c) < 32 for c in ref):
  159. return None
  160. file_roots = {
  161. "bin": "/usr/bin",
  162. "sbin": "/usr/sbin",
  163. "libexec": "/usr/bin",
  164. "gir": "/usr/share/gir-1.0",
  165. "res": "/usr/share",
  166. "cfg": "/etc",
  167. "man": "/usr/share/man",
  168. "info": "/usr/share/info",
  169. "locale": "/usr/share/locale",
  170. }
  171. if prefix in file_roots:
  172. return [exact(file_roots[prefix] + "/" + resource)]
  173. if prefix == "typelib":
  174. return [exact("/usr/lib/%s/girepository-1.0/%s" % (TRIPLE, resource))]
  175. if prefix == "vapi":
  176. return [
  177. exact("/usr/share/vala/vapi/" + resource),
  178. vapi_versioned(resource),
  179. ]
  180. if prefix == "lib":
  181. return [
  182. exact("/usr/lib/%s/%s" % (TRIPLE, resource)),
  183. exact("/lib/%s/%s" % (TRIPLE, resource)),
  184. ]
  185. if prefix == "gio":
  186. return [exact("/usr/lib/%s/gio/modules/%s" % (TRIPLE, resource))]
  187. if prefix == "pc":
  188. return [
  189. exact("/usr/lib/%s/pkgconfig/%s" % (TRIPLE, resource)),
  190. exact("/usr/share/pkgconfig/" + resource),
  191. ]
  192. if prefix == "inc":
  193. return [prefixed("/usr/include/" + resource)]
  194. if prefix == "rootpath":
  195. return [exact("/" + resource.lstrip("/"))]
  196. if prefix == "tag":
  197. return [exact("/usr/share/usm-tags/" + resource.replace(".", "/") + ".tag")]
  198. return None
  199. def first_error_line(text):
  200. for line in text.splitlines():
  201. if ERROR_LINE.match(line):
  202. return line.strip()
  203. return None
  204. def parse_owner_lines(text):
  205. """Extract (package, path) pairs from apt-file's "package: path" lines.
  206. apt-file's progress chatter and anything that does not look like an
  207. absolute path after "package: " is ignored.
  208. """
  209. owners = []
  210. for line in text.splitlines():
  211. package, sep, path = line.strip().partition(": ")
  212. if not sep or not path.startswith("/") or " " in package:
  213. continue
  214. owners.append((plain_name(package), path))
  215. return owners
  216. def run_apt_file(arguments):
  217. if shutil.which("apt-file") is None:
  218. raise HelperError(
  219. "apt-file is not installed; the SPM bootstrap must install it "
  220. "and run apt-file update", EXIT_RESOLVE)
  221. return subprocess.run(
  222. ["apt-file", "search"] + arguments,
  223. stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  224. universal_newlines=True,
  225. )
  226. def option_unsupported(stderr):
  227. lowered = stderr.lower()
  228. return any(marker in lowered for marker in
  229. ("unknown option", "unrecognized", "usage:", "usage error"))
  230. def apt_file_search(mode, patterns):
  231. """Search the apt-file Contents index for every pattern in one mode."""
  232. results = []
  233. if not patterns:
  234. return results
  235. flags = ["--fixed-string"] if mode == "exact" else ["--substring-match"]
  236. handle = tempfile.NamedTemporaryFile(
  237. "w", suffix=".usm-spm-apt-patterns", delete=False)
  238. try:
  239. handle.write("\n".join(patterns) + "\n")
  240. handle.close()
  241. process = run_apt_file(flags + ["--from-file", handle.name])
  242. if process.returncode == 0:
  243. return parse_owner_lines(process.stdout)
  244. if option_unsupported(process.stderr):
  245. for pattern in patterns:
  246. process = run_apt_file(flags + [pattern])
  247. if process.returncode != 0 and first_error_line(
  248. process.stderr + process.stdout) is not None:
  249. raise HelperError(
  250. first_error_line(process.stderr + process.stdout),
  251. EXIT_RESOLVE)
  252. results.extend(parse_owner_lines(process.stdout))
  253. return results
  254. error = first_error_line(process.stderr + process.stdout)
  255. if error is not None:
  256. raise HelperError(error, EXIT_RESOLVE)
  257. return results
  258. finally:
  259. os.unlink(handle.name)
  260. def dpkg_installed_names():
  261. """Names of every installed package, from the dpkg status database."""
  262. try:
  263. process = subprocess.run(
  264. ["dpkg-query", "--show", "--showformat", "${Package}\n"],
  265. stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  266. universal_newlines=True)
  267. except OSError:
  268. return set()
  269. if process.returncode != 0:
  270. return set()
  271. return set(process.stdout.split())
  272. def build_solo_cache():
  273. """An apt cache whose dpkg status is empty (nothing installed).
  274. The DNF helper builds its solo-install estimates against a repository-
  275. only sack; pointing Dir::State::status at /dev/null is apt's analogue,
  276. so every dependency of a marked package resolves as an install.
  277. """
  278. apt_pkg.init()
  279. apt_pkg.config.set("Dir::State::status", os.devnull)
  280. return apt_pkg.Cache(apt.progress.base.OpProgress())
  281. def solo_closure(cache, installed_names, name, closure_cache):
  282. """(dependency-count, installed-dependency-count) for a solo install."""
  283. if name in closure_cache:
  284. return closure_cache[name]
  285. counts = (1, 1 if name in installed_names else 0)
  286. if cache is None:
  287. sys.stderr.write(
  288. "usm-spm-apt: no python3-apt cache available, estimating "
  289. "dependency counts of %s as installed-only\n" % name)
  290. closure_cache[name] = counts
  291. return counts
  292. try:
  293. package = cache[name]
  294. except KeyError:
  295. package = None
  296. if package is None:
  297. sys.stderr.write(
  298. "usm-spm-apt: %s not found in enabled repositories, "
  299. "estimating dependency counts as installed-only\n" % name)
  300. closure_cache[name] = counts
  301. return counts
  302. depcache = apt_pkg.DepCache(cache)
  303. depcache.init()
  304. if depcache.get_candidate_ver(package) is None:
  305. sys.stderr.write(
  306. "usm-spm-apt: %s has no installation candidate, "
  307. "estimating dependency counts as installed-only\n" % name)
  308. closure_cache[name] = counts
  309. return counts
  310. try:
  311. depcache.mark_install(package, True, True)
  312. closure = [p.name for p in cache.packages if depcache.marked_install(p)]
  313. counts = (
  314. len(closure),
  315. sum(1 for member in closure if member in installed_names),
  316. )
  317. except SystemError as e:
  318. sys.stderr.write(
  319. "usm-spm-apt: could not resolve solo install of %s: %s\n"
  320. % (name, e))
  321. closure_cache[name] = counts
  322. return counts
  323. def soname_exception_package(soname):
  324. """The cached package for a {@link SONAME_PACKAGES} soname, or None.
  325. The mapping is only trusted when the apt cache actually offers the
  326. package, so a stale table entry degrades to not-found.
  327. """
  328. name = SONAME_PACKAGES.get(soname)
  329. if name is None:
  330. return None
  331. try:
  332. cache = apt.Cache()
  333. if name in cache and cache[name].candidate is not None:
  334. return name
  335. except Exception:
  336. pass
  337. return None
  338. def cached_package_name(name):
  339. """The remapped {@link PACKAGE_REMAPS} target for an owner, or None.
  340. The remap is only trusted when the apt cache actually offers the
  341. target, so a stale table entry keeps the index-reported owner.
  342. """
  343. target = PACKAGE_REMAPS.get(name)
  344. if target is None:
  345. return None
  346. try:
  347. cache = apt.Cache()
  348. if target in cache and cache[target].candidate is not None:
  349. return target
  350. except Exception:
  351. pass
  352. return None
  353. def run_query(args):
  354. refs = list(dict.fromkeys(args.refs))
  355. translated = {}
  356. patterns = {"exact": set(), "substring": set()}
  357. for ref in refs:
  358. descriptors = translate_ref(ref)
  359. if descriptors is not None:
  360. translated[ref] = descriptors
  361. for mode, pattern, _ in descriptors:
  362. patterns[mode].add(pattern)
  363. index = []
  364. for mode in ("exact", "substring"):
  365. index.extend(apt_file_search(mode, sorted(patterns[mode])))
  366. not_found = []
  367. candidates = {}
  368. for ref in refs:
  369. descriptors = translated.get(ref)
  370. if descriptors is None:
  371. sys.stderr.write(
  372. "usm-spm-apt: resource type of \"%s\" has no "
  373. "system-package-manager translation\n" % ref)
  374. not_found.append(ref)
  375. continue
  376. matched = set()
  377. for mode, pattern, predicate in descriptors:
  378. for name, path in index:
  379. if predicate(path):
  380. matched.add(name)
  381. remapped = set()
  382. for name in sorted(matched):
  383. remapped.add(cached_package_name(name) or name)
  384. matched = remapped
  385. if not matched and ref.startswith("lib:"):
  386. mapped = soname_exception_package(ref[len("lib:"):])
  387. if mapped is not None:
  388. matched.add(mapped)
  389. if not matched:
  390. not_found.append(ref)
  391. continue
  392. for name in matched:
  393. entry = candidates.get(name)
  394. if entry is None:
  395. entry = {"resources": []}
  396. candidates[name] = entry
  397. entry["resources"].append(ref)
  398. installed_names = dpkg_installed_names()
  399. solo_cache = build_solo_cache() if candidates else None
  400. closure_cache = {}
  401. packages = []
  402. for name in sorted(candidates):
  403. dependency_count, installed_dependency_count = solo_closure(
  404. solo_cache, installed_names, name, closure_cache)
  405. packages.append({
  406. "name": name,
  407. "resources": candidates[name]["resources"],
  408. "dependency-count": dependency_count,
  409. "installed-dependency-count": installed_dependency_count,
  410. })
  411. sys.stdout.write(json.dumps({
  412. "not-found": not_found,
  413. "packages": packages,
  414. }) + "\n")
  415. sys.stdout.flush()
  416. return EXIT_OK
  417. def apt_environment():
  418. environment = dict(os.environ)
  419. environment["DEBIAN_FRONTEND"] = "noninteractive"
  420. environment["LC_ALL"] = "C"
  421. return environment
  422. def classify_apt_failure(output):
  423. lowered = output.lower()
  424. if "failed to fetch" in lowered or "unable to fetch" in lowered:
  425. return EXIT_DOWNLOAD
  426. if ("unable to locate" in lowered or "unmet dependencies" in lowered
  427. or "no installation candidate" in lowered):
  428. return EXIT_RESOLVE
  429. return EXIT_TRANSACTION
  430. def collect_error_messages(output, limit=5):
  431. messages = [line.strip() for line in output.splitlines()
  432. if ERROR_LINE.match(line)]
  433. return "; ".join(messages[-limit:])
  434. def simulate_install(names, environment):
  435. """Ordered package list of the would-be transaction, from -s output."""
  436. process = subprocess.run(
  437. ["apt-get", "--simulate", "install"] + names,
  438. stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  439. universal_newlines=True, env=environment)
  440. if process.returncode != 0:
  441. combined = process.stderr + process.stdout
  442. raise HelperError(
  443. collect_error_messages(combined)
  444. or "apt-get simulation failed for %s" % " ".join(names),
  445. classify_apt_failure(combined))
  446. order = []
  447. for line in process.stdout.splitlines():
  448. match = INST_LINE.match(line)
  449. if match:
  450. name = plain_name(match.group(1))
  451. if name not in order:
  452. order.append(name)
  453. return order
  454. def cmd_query(args):
  455. try:
  456. return run_query(args)
  457. except SystemExit:
  458. raise
  459. except HelperError as e:
  460. fail_plain(str(e), e.exit_code)
  461. except Exception as e:
  462. fail_plain("query failed: %s" % e, EXIT_RESOLVE)
  463. class InstallProgress:
  464. """Maps apt-get output lines to contract package/complete events."""
  465. def __init__(self, order):
  466. self.position = {name: index for index, name in enumerate(order, 1)}
  467. self.total = len(order)
  468. self.completed = 0
  469. self.last = None
  470. def emit_package(self, name, progress):
  471. if name not in self.position:
  472. return
  473. event = ("package", name, self.position[name], self.total,
  474. round(progress, 4))
  475. if event == self.last:
  476. return
  477. self.last = event
  478. emit_event({
  479. "type": "package",
  480. "name": name,
  481. "current": self.position[name],
  482. "total": self.total,
  483. "progress": round(progress, 4),
  484. })
  485. def feed(self, line):
  486. match = GET_LINE.match(line)
  487. if match:
  488. for token in line.split()[1:]:
  489. candidate = plain_name(token)
  490. if candidate in self.position:
  491. self.emit_package(candidate, 0.0)
  492. break
  493. return
  494. match = UNPACK_LINE.match(line)
  495. if match:
  496. self.emit_package(plain_name(match.group(1)), 0.5)
  497. return
  498. match = SETUP_LINE.match(line)
  499. if match:
  500. name = plain_name(match.group(1))
  501. if name in self.position:
  502. self.emit_package(name, 1.0)
  503. emit_event({"type": "package-complete", "name": name})
  504. self.completed += 1
  505. def cmd_install(args):
  506. environment = apt_environment()
  507. try:
  508. order = simulate_install(args.names, environment)
  509. except HelperError as e:
  510. fail(str(e), e.exit_code)
  511. emit_event({"type": "begin", "total": len(order)})
  512. progress = InstallProgress(order)
  513. process = subprocess.Popen(
  514. ["apt-get", "install", "-y"] + args.names,
  515. stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
  516. universal_newlines=True, env=environment)
  517. output = []
  518. for line in process.stdout:
  519. output.append(line)
  520. progress.feed(line.rstrip("\r\n"))
  521. process.stdout.close()
  522. returncode = process.wait()
  523. transcript = "".join(output)
  524. if returncode != 0:
  525. fail(collect_error_messages(transcript)
  526. or "apt-get install failed with exit code %d" % returncode,
  527. classify_apt_failure(transcript))
  528. emit_event({
  529. "type": "complete",
  530. "status": "ok",
  531. "installed": progress.completed,
  532. })
  533. return EXIT_OK
  534. def main():
  535. parser = argparse.ArgumentParser(
  536. prog="usm-spm-apt",
  537. description="USM system-package-manager helper for APT")
  538. subparsers = parser.add_subparsers(dest="command", required=True)
  539. query_parser = subparsers.add_parser(
  540. "query", help="resolve USM resource refs to system packages")
  541. query_parser.add_argument(
  542. "refs", nargs="+", metavar="USM-REF",
  543. help="resource ref, e.g. bin:valac or lib:libglib-2.0.so.0")
  544. query_parser.set_defaults(handler=cmd_query)
  545. install_parser = subparsers.add_parser(
  546. "install", help="install system packages, streaming progress events")
  547. install_parser.add_argument(
  548. "names", nargs="+", metavar="NAME",
  549. help="native system package name")
  550. install_parser.set_defaults(handler=cmd_install)
  551. args = parser.parse_args()
  552. try:
  553. return args.handler(args)
  554. except HelperError as e:
  555. fail(str(e), e.exit_code)
  556. except SystemExit:
  557. raise
  558. except Exception as e:
  559. fail("unexpected failure: %s" % e, EXIT_FAILURE)
  560. if __name__ == "__main__":
  561. sys.exit(main())