| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701 |
- #!/usr/bin/env python3
- """USM system-package-manager helper for Portage (Gentoo) systems.
- Implements the USM SPM contract (see usm/README.md, "System package manager
- integration") for Gentoo systems:
- usm-spm-emerge query <usm-ref>... -> contract JSON on STDOUT
- usm-spm-emerge plan <atom>... -> contract JSON on STDOUT
- usm-spm-emerge install [--test] <atom>... -> contract JSONL events on STDOUT
- API choice: a Gentoo stage3 ships Portage and python3, so this helper reads
- the installed-package database through the portage Python API (with a raw
- /var/db/pkg CONTENTS scan as fallback) and shells out to `emerge` for
- dependency resolution (pretend) and installation. There is no binary
- metadata to bind against beyond that: ebuild repositories carry no file
- lists, so unlike the dnf/apt/apk helpers this one cannot search "which
- package contains file X" for packages that are not installed yet.
- File-backed refs are therefore resolved in two documented stages:
- 1. INSTALLED files (exact): the ref is translated to candidate filesystem
- paths (gentoo layout: /usr/bin, /usr/sbin, /usr/lib64, /usr/share, ...)
- and matched against the portage vdb (/var/db/pkg/*/CONTENTS). A ref
- resolves to the installed package that owns the file.
- 2. NOT-installed files (exception table only): Gentoo has no offline
- file-provides (PFL, the portage file list, is an online service), so a
- small curated table maps the USM stack's build/platform refs to Gentoo
- package atoms (valac -> dev-lang/vala, glib-2.0.pc -> dev-libs/glib,
- ...). Refs outside the table land in not-found; anything else would be
- guesswork.
- dependency-count / installed-dependency-count are honest numbers parsed
- from `emerge --pretend --quiet=y --ask=n <atom>`: dependency-count is the
- number of packages in the merge list including the package itself,
- installed-dependency-count is the subset of the merge list already
- installed (action letters without N, e.g. [ebuild R]/[ebuild U] reinstalls
- and upgrades; a fresh stage3 typically shows an all-N list). If the pretend
- run fails (atom missing from the tree, masked, unresolvable) an
- exception-table candidate falls back to not-found.
- Install maps `emerge --ask=n --quiet=y <atoms>` onto the JSONL contract,
- linking the unversioned tool names Gentoo ships versioned (UNVERSIONED_TOOL_LINKS):
- `begin`/`total` come from a pretend run first, a `package` event is emitted
- per ">>> Emerging (n of N)" status line, `package-complete` per
- ">>> Installing (n of N)" line (the moment a merge lands in the vdb),
- `complete` carries the number of completed merges, and any failure emits a
- terminal `error` event before a non-zero exit (resolution failures map to
- exit 3, download failures to 4, build/merge failures to 5).
- Documented Gentoo limitations:
- * No offline file-provides for not-yet-installed packages (see stage 2
- above); installed-file queries are exact.
- * USE flags are never touched: emerge runs with the system's configured
- defaults. The stack's platform libraries (glib, json-glib, libgee, ...)
- enable their introspection USE flag by default, but a system that
- disabled it (package.use/make.conf) will lack the matching .gir/
- .typelib/.vapi artifacts after install; re-enable the flag and re-emerge
- if a build needs them.
- * Packages are compiled from source; even small installs take minutes.
- Progress granularity is per package; emerge exposes no useful
- intra-package progress, so `package` events carry progress 0.0 and the
- completion signal is the ">>> Installing" line.
- * The query subcommand never modifies system state (at most Portage
- regenerates its dependency cache); install must run as root and never
- prompts (--ask=n answers Portage's own prompt for it).
- """
- import argparse
- import glob
- import json
- import os
- import re
- import subprocess
- import sys
- import threading
- EMERGE = "/usr/bin/emerge"
- EXIT_OK = 0
- EXIT_FAILURE = 1
- EXIT_RESOLVE = 3
- EXIT_DOWNLOAD = 4
- EXIT_TRANSACTION = 5
- FILE_ROOTS = {
- "sbin": ["/usr/sbin", "/sbin"],
- "libexec": ["/usr/libexec"],
- "gir": ["/usr/share/gir-1.0"],
- "typelib": [
- "/usr/lib64/girepository-1.0",
- "/usr/lib/girepository-1.0",
- "/lib64/girepository-1.0",
- "/lib/girepository-1.0",
- ],
- "gio": [
- "/usr/lib64/gio/modules",
- "/usr/lib/gio/modules",
- ],
- "res": ["/usr/share"],
- "cfg": ["/etc"],
- "man": ["/usr/share/man"],
- "info": ["/usr/share/info"],
- "locale": ["/usr/share/locale"],
- "app": ["/usr/share/applications"],
- "opt": ["/opt"],
- "rootpath": [""],
- "tag": ["/usr/share/usm-tags"],
- }
- LIB_ROOTS = ["/usr/lib64", "/lib64", "/usr/lib", "/lib"]
- PC_ROOTS = ["/usr/lib64/pkgconfig", "/usr/share/pkgconfig", "/usr/lib/pkgconfig"]
- TRANSLATED_PREFIXES = set(FILE_ROOTS) | {
- "bin", "lib", "libres", "pc", "vapi", "inc"}
- USR_MERGE_ALIASES = [
- ("/lib64/", "/usr/lib64/"),
- ("/lib/", "/usr/lib/"),
- ("/bin/", "/usr/bin/"),
- ("/sbin/", "/usr/sbin/"),
- ]
- EXCEPTIONS = {
- "bin:valac": "dev-lang/vala",
- "bin:vapigen": "dev-lang/vala",
- "bin:meson": "dev-build/meson",
- "bin:ninja": "dev-build/ninja",
- "bin:pkg-config": "dev-util/pkgconf",
- "bin:pkgconf": "dev-util/pkgconf",
- "bin:g-ir-scanner": "dev-libs/gobject-introspection",
- "bin:g-ir-compiler": "dev-libs/gobject-introspection",
- "bin:gcc": "sys-devel/gcc",
- "bin:g++": "sys-devel/gcc",
- "bin:cc": "sys-devel/gcc",
- "bin:c++": "sys-devel/gcc",
- "bin:ld": "sys-devel/binutils",
- "bin:make": "dev-build/make",
- "bin:python3": "dev-lang/python",
- "bin:bash": "app-shells/bash",
- "bin:git": "dev-vcs/git",
- "bin:curl": "net-misc/curl",
- "bin:tar": "app-arch/tar",
- "bin:xz": "app-arch/xz-utils",
- "bin:gzip": "app-arch/gzip",
- "bin:sed": "sys-apps/sed",
- "bin:awk": "sys-apps/gawk",
- "bin:grep": "sys-apps/grep",
- "bin:ldconfig": "sys-libs/glibc",
- "pc:glib-2.0.pc": "dev-libs/glib",
- "pc:gobject-2.0.pc": "dev-libs/glib",
- "pc:gio-2.0.pc": "dev-libs/glib",
- "pc:gio-unix-2.0.pc": "dev-libs/glib",
- "pc:gmodule-2.0.pc": "dev-libs/glib",
- "pc:gmodule-no-export-2.0.pc": "dev-libs/glib",
- "pc:gthread-2.0.pc": "dev-libs/glib",
- "pc:json-glib-1.0.pc": "dev-libs/json-glib",
- "pc:gee-0.8.pc": "dev-libs/libgee",
- "pc:libsodium.pc": "dev-libs/libsodium",
- "pc:libarchive.pc": "app-arch/libarchive",
- "pc:sqlite3.pc": "dev-db/sqlite",
- "pc:libxml-2.0.pc": "dev-libs/libxml2",
- "pc:libsoup-3.0.pc": "net-libs/libsoup",
- "pc:libmicrohttpd.pc": "net-libs/libmicrohttpd",
- "pc:gobject-introspection-1.0.pc": "dev-libs/gobject-introspection",
- "pc:zlib.pc": "sys-libs/zlib",
- "pc:libzstd.pc": "app-arch/zstd",
- "pc:libbrotlienc.pc": "app-arch/brotli",
- "pc:libbrotlidec.pc": "app-arch/brotli",
- "pc:libbrotlicommon.pc": "app-arch/brotli",
- "pc:libffi.pc": "dev-libs/libffi",
- "pc:pcre2.pc": "dev-libs/libpcre2",
- "vapi:glib-2.0.vapi": "dev-lang/vala",
- "vapi:gobject-2.0.vapi": "dev-lang/vala",
- "vapi:gio-2.0.vapi": "dev-lang/vala",
- "vapi:gio-unix-2.0.vapi": "dev-lang/vala",
- "vapi:posix.vapi": "dev-lang/vala",
- "vapi:json-glib-1.0.vapi": "dev-lang/vala",
- "vapi:libxml-2.0.vapi": "dev-lang/vala",
- "vapi:sqlite3.vapi": "dev-lang/vala",
- "vapi:gee-0.8.vapi": "dev-libs/libgee",
- "gir:GLib-2.0.gir": "dev-libs/gobject-introspection",
- "gir:GObject-2.0.gir": "dev-libs/gobject-introspection",
- "gir:Gio-2.0.gir": "dev-libs/gobject-introspection",
- "gir:GioUnix-2.0.gir": "dev-libs/gobject-introspection",
- "typelib:GLib-2.0.typelib": "dev-libs/gobject-introspection",
- "typelib:GObject-2.0.typelib": "dev-libs/gobject-introspection",
- "typelib:Gio-2.0.typelib": "dev-libs/gobject-introspection",
- "typelib:GioUnix-2.0.typelib": "dev-libs/gobject-introspection",
- "lib:libglib-2.0.so.0": "dev-libs/glib",
- "lib:libgobject-2.0.so.0": "dev-libs/glib",
- "lib:libgio-2.0.so.0": "dev-libs/glib",
- "lib:libgmodule-2.0.so.0": "dev-libs/glib",
- "lib:libgthread-2.0.so.0": "dev-libs/glib",
- "lib:libjson-glib-1.0.so.0": "dev-libs/json-glib",
- "lib:libgee-0.8.so.2": "dev-libs/libgee",
- "lib:libsodium.so.26": "dev-libs/libsodium",
- "lib:libarchive.so.13": "app-arch/libarchive",
- "lib:libsqlite3.so.0": "dev-db/sqlite",
- "lib:libxml2.so.2": "dev-libs/libxml2",
- "lib:libsoup-3.0.so.0": "net-libs/libsoup",
- "lib:libmicrohttpd.so.12": "net-libs/libmicrohttpd",
- "lib:libz.so.1": "sys-libs/zlib",
- "lib:libzstd.so.1": "app-arch/zstd",
- "lib:libbrotlienc.so.1": "app-arch/brotli",
- "lib:libbrotlidec.so.1": "app-arch/brotli",
- "lib:libbrotlicommon.so.1": "app-arch/brotli",
- "lib:libffi.so.8": "dev-libs/libffi",
- "lib:libpcre2-8.so.0": "dev-libs/libpcre2",
- "gio:libgiognutls.so": "net-libs/glib-networking",
- "lib:libc.so.6": "sys-libs/glibc",
- "lib:libm.so.6": "sys-libs/glibc",
- "lib:libblkid.so.1": "sys-apps/util-linux",
- "lib:libmount.so.1": "sys-apps/util-linux",
- "lib:libuuid.so.1": "sys-apps/util-linux",
- "inc:glib-2.0": "dev-libs/glib",
- "inc:json-glib-1.0": "dev-libs/json-glib",
- "inc:gee-0.8": "dev-libs/libgee",
- "inc:libsodium.h": "dev-libs/libsodium",
- "inc:archive.h": "app-arch/libarchive",
- "inc:sqlite3.h": "dev-db/sqlite",
- }
- class HelperError(Exception):
- """Fatal helper failure; message is emitted as a contract error event."""
- def __init__(self, message, exit_code=EXIT_FAILURE):
- super(HelperError, self).__init__(message)
- self.exit_code = exit_code
- def emit_event(event):
- """Write one contract JSONL event to STDOUT and flush it."""
- sys.stdout.write(json.dumps(event) + "\n")
- sys.stdout.flush()
- def fail(message, exit_code=EXIT_FAILURE):
- """Emit the terminal error event and exit non-zero."""
- emit_event({"type": "error", "message": message})
- sys.exit(exit_code)
- def fail_plain(message, exit_code=EXIT_FAILURE):
- """Report a query failure on STDERR only, keeping STDOUT contract-clean."""
- sys.stderr.write("usm-spm-emerge: %s\n" % message)
- sys.exit(exit_code)
- def category_package(cpv):
- """Strip version and ::repository suffix from a cpv -> category/package."""
- atom = cpv.split("::", 1)[0]
- try:
- import portage
- key = portage.cpv_getkey(atom)
- if key:
- return key
- except Exception:
- pass
- category, sep, package_version = atom.partition("/")
- if not sep:
- return atom
- return re.sub(r"-\d.*$", "", package_version) or package_version
- def parse_contents_line(line):
- """Path recorded on one CONTENTS line, or None for junk lines."""
- fields = line.split()
- if len(fields) >= 2 and fields[0] in ("obj", "dir", "sym", "dev", "fif"):
- return fields[1]
- return None
- def build_vdb_index():
- """Map every installed file path -> cpv via the portage vdb.
- Prefers the portage API (aux_get CONTENTS, present on any portage
- system); falls back to a raw /var/db/pkg/<cat>/<pkg>/CONTENTS scan.
- """
- index = {}
- try:
- import portage
- root = portage.settings.get("ROOT", "/")
- vdb = portage.db[root]["vartree"].dbapi
- for cpv in vdb.cpv_all():
- raw = vdb.aux_get(cpv, ["CONTENTS"])[0] or ""
- for line in raw.splitlines():
- path = parse_contents_line(line)
- if path is not None:
- index.setdefault(path, cpv)
- if index:
- return index
- except Exception as e:
- sys.stderr.write("usm-spm-emerge: portage API vdb read failed (%s), "
- "falling back to /var/db/pkg scan\n" % e)
- for contents_path in glob.glob("/var/db/pkg/*/*/CONTENTS"):
- category = os.path.basename(os.path.dirname(os.path.dirname(contents_path)))
- package_version = os.path.basename(os.path.dirname(contents_path))
- cpv = category + "/" + package_version
- try:
- with open(contents_path, "r", errors="replace") as handle:
- for line in handle:
- path = parse_contents_line(line)
- if path is not None:
- index.setdefault(path, cpv)
- except OSError:
- continue
- return index
- def path_aliases(path):
- """path plus its /usr-merge variant(s) (/lib64/x -> /usr/lib64/x...)."""
- aliases = [path]
- for merged, unmerged in USR_MERGE_ALIASES:
- if path.startswith(merged):
- variant = unmerged + path[len(merged):]
- elif path.startswith(unmerged):
- variant = merged + path[len(unmerged):]
- else:
- continue
- if variant not in aliases:
- aliases.append(variant)
- return aliases
- def bin_candidates(resource):
- """PATH directories in order (plus /usr/bin) as candidate directories."""
- directories = [d for d in os.environ.get("PATH", "").split(os.pathsep) if d]
- if "/usr/bin" not in directories:
- directories.append("/usr/bin")
- return [os.path.join(d, resource) for d in directories]
- def translate_paths(prefix, resource):
- """Candidate filesystem paths for a file-backed ref prefix."""
- if prefix == "bin":
- return bin_candidates(resource)
- if prefix == "tag":
- return ["/usr/share/usm-tags/" + resource.replace(".", "/") + ".tag"]
- if prefix in FILE_ROOTS:
- return ["/".join(FILE_ROOTS[prefix] + [resource])]
- if prefix in ("lib", "libres"):
- return [os.path.join(d, resource) for d in LIB_ROOTS]
- if prefix == "pc":
- return [os.path.join(d, resource) for d in PC_ROOTS]
- return None
- def resolve_installed(ref, index):
- """cpv of the installed package owning the ref's file, or None.
- Candidate paths (PATH order for bin:, gentoo root table otherwise) are
- matched exactly against the vdb index, with /usr-merge aliases tried on
- both sides; files present on disk but owned by no package do not count
- as provided by the system package manager.
- """
- prefix, sep, resource = ref.partition(":")
- if not sep or not resource:
- return None
- resource = resource.strip("/")
- if prefix == "vapi":
- pattern = re.compile(
- r"^/usr/share/vala[^/]*/vapi/" + re.escape(resource) + "$")
- for path, cpv in index.items():
- if pattern.match(path):
- return cpv
- return None
- if prefix == "inc":
- directory = "/usr/include/" + resource.strip("/")
- for path, cpv in index.items():
- if path == directory or path.startswith(directory + "/"):
- return cpv
- return None
- candidates = translate_paths(prefix, resource)
- if candidates is None:
- return None
- for path in candidates:
- for alias in path_aliases(path):
- if alias in index:
- return index[alias]
- return None
- def emerge_pretend(atoms):
- """Run emerge --pretend; returns (returncode, stdout, stderr)."""
- command = [EMERGE, "--pretend", "--quiet=y", "--ask=n"] + list(atoms)
- try:
- proc = subprocess.run(
- command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- text=True, errors="replace")
- except OSError as e:
- return EXIT_FAILURE, "", str(e)
- return proc.returncode, proc.stdout, proc.stderr
- MERGE_LINE = re.compile(r"^\[(ebuild|binary)\s+([A-Za-z*]+)\s*\]\s+(\S+)")
- def parse_merge_list(output):
- """(total, already_installed, atoms) from pretend output merge lines."""
- total = 0
- installed = 0
- atoms = []
- for line in output.splitlines():
- match = MERGE_LINE.match(line)
- if not match:
- continue
- total += 1
- if "N" not in match.group(2):
- installed += 1
- atoms.append(match.group(3))
- return total, installed, atoms
- def pretend_counts(atom, cache):
- """(dependency-count, installed-dependency-count) for a solo emerge.
- None means the atom cannot be resolved against the tree at all (no
- ebuilds, masked, conflicting): exception-table candidates then fall
- back to not-found; installed candidates degrade to (1, 1).
- """
- if atom in cache:
- return cache[atom]
- returncode, stdout, stderr = emerge_pretend([atom])
- if returncode != 0:
- cache[atom] = None
- return None
- total, installed, _ = parse_merge_list(stdout)
- counts = (total, installed) if total else (1, 1)
- cache[atom] = counts
- return counts
- def cmd_query(args):
- try:
- return run_query(args)
- except SystemExit:
- raise
- except Exception as e:
- fail_plain("query failed: %s" % e, EXIT_RESOLVE)
- def run_query(args):
- index = build_vdb_index()
- refs = list(dict.fromkeys(args.refs))
- candidates = {}
- resolved_ref = {}
- for ref in refs:
- cpv = resolve_installed(ref, index)
- if cpv is not None:
- name = category_package(cpv)
- else:
- name = EXCEPTIONS.get(ref)
- if name is None:
- if ref.partition(":")[0] not in TRANSLATED_PREFIXES:
- sys.stderr.write(
- "usm-spm-emerge: resource type of \"%s\" has no "
- "system-package-manager translation\n" % ref)
- continue
- resolved_ref[ref] = name
- entry = candidates.setdefault(name, {"resources": [], "installed": False})
- entry["resources"].append(ref)
- if cpv is not None:
- entry["installed"] = True
- counts_cache = {}
- package_counts = {}
- for name, entry in candidates.items():
- counts = pretend_counts(name, counts_cache)
- if counts is None and entry["installed"]:
- sys.stderr.write(
- "usm-spm-emerge: %s is installed but emerge cannot resolve "
- "its atom; estimating dependency counts as installed-only\n"
- % name)
- counts = (1, 1)
- package_counts[name] = counts
- not_found = []
- for ref in refs:
- name = resolved_ref.get(ref)
- if name is None or package_counts[name] is None:
- not_found.append(ref)
- packages = []
- for name in sorted(candidates):
- if package_counts[name] is None:
- continue
- dependency_count, installed_dependency_count = package_counts[name]
- packages.append({
- "name": name,
- "resources": candidates[name]["resources"],
- "dependency-count": dependency_count,
- "installed-dependency-count": installed_dependency_count,
- })
- sys.stdout.write(json.dumps({
- "not-found": not_found,
- "packages": packages,
- }) + "\n")
- sys.stdout.flush()
- return EXIT_OK
- EMERGING_LINE = re.compile(r">>> Emerging \((\d+) of (\d+)\) (\S+)")
- INSTALLING_LINE = re.compile(r">>> Installing \((\d+) of (\d+)\) (\S+)")
- def classify_failure(output):
- """Map an emerge failure transcript onto a contract exit code."""
- lowered = output.lower()
- if ("all ebuilds that could satisfy" in lowered
- or "dependency conflict" in lowered
- or "conflicting requests" in lowered
- or "blocked" in lowered
- or "no ebuilds to satisfy" in lowered):
- return EXIT_RESOLVE
- if ("couldn't download" in lowered
- or "fetch instructions" in lowered
- or "failed to fetch" in lowered):
- return EXIT_DOWNLOAD
- return EXIT_TRANSACTION
- NOISE_LINE = re.compile(r"^\s*\*\s+(IMPORTANT:|Use eselect news)")
- def failure_message(lines, limit=15):
- """Last few meaningful output lines as a terminal error message."""
- meaningful = [line.strip() for line in lines
- if line.strip() and not NOISE_LINE.match(line)]
- tail = meaningful[-limit:]
- message = " | ".join(tail)
- return message[:800] if message else "emerge exited non-zero"
- # Gentoo installs some tool binaries under versioned names only (vala's
- # ebuild ships valac-0.56 without an unversioned valac), while USM
- # manifests reference the unversioned bin:. After a transaction that
- # installed one of these atoms, link each unversioned name to the newest
- # versioned binary so bin: refs resolve on the filesystem.
- UNVERSIONED_TOOL_LINKS = {
- "dev-lang/vala": ["valac", "vala-gen-introspect", "vapigen"],
- }
- def link_unversioned_tools(atoms):
- """Ensure unversioned tool symlinks exist for versioned ebuild tools."""
- wanted = set()
- for atom in atoms:
- for prefix, tools in UNVERSIONED_TOOL_LINKS.items():
- if atom == prefix or atom.startswith(prefix + ":"):
- wanted.update(tools)
- for tool in sorted(wanted):
- target = os.path.join("/usr/bin", tool)
- if os.path.exists(target):
- continue
- candidates = sorted(glob.glob("/usr/bin/%s-[0-9]*" % tool))
- if candidates:
- try:
- os.symlink(os.path.basename(candidates[-1]), target)
- except OSError:
- pass
- def cmd_plan(args):
- """Resolve the material install set without touching the system.
- One emerge --pretend for every atom; the merge list's names (in
- emerge's own transaction order) print as one contract JSON object,
- matching the names the query contract reports.
- """
- returncode, stdout, stderr = emerge_pretend(args.names)
- if returncode != 0:
- fail_plain("dependency resolution failed: %s"
- % failure_message((stdout + "\n" + stderr).splitlines()),
- EXIT_RESOLVE)
- _, _, atoms = parse_merge_list(stdout)
- sys.stdout.write(json.dumps(
- {"packages": [category_package(atom) for atom in atoms]}) + "\n")
- sys.stdout.flush()
- return EXIT_OK
- def cmd_install(args):
- returncode, stdout, stderr = emerge_pretend(args.names)
- if returncode != 0:
- fail("dependency resolution failed: %s"
- % failure_message((stdout + "\n" + stderr).splitlines()),
- EXIT_RESOLVE)
- total, _, atoms = parse_merge_list(stdout)
- emit_event({"type": "begin", "total": total})
- if args.test:
- emit_event({"type": "complete", "status": "ok", "installed": 0})
- return EXIT_OK
- command = [EMERGE, "--ask=n", "--quiet=y"] + list(args.names)
- try:
- proc = subprocess.Popen(
- command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- text=True, errors="replace", bufsize=1)
- except OSError as e:
- fail("could not run emerge: %s" % e, EXIT_FAILURE)
- transcript = []
- def drain_stderr():
- for line in proc.stderr:
- transcript.append(line)
- sys.stderr.write(line)
- sys.stderr.flush()
- reader = threading.Thread(target=drain_stderr)
- reader.daemon = True
- reader.start()
- completed = []
- for line in proc.stdout:
- transcript.append(line)
- sys.stderr.write(line)
- sys.stderr.flush()
- emerging = EMERGING_LINE.match(line)
- if emerging:
- emit_event({
- "type": "package",
- "name": category_package(emerging.group(3)),
- "current": int(emerging.group(1)),
- "total": int(emerging.group(2)),
- "progress": 0.0,
- })
- continue
- installing = INSTALLING_LINE.match(line)
- if installing:
- name = category_package(installing.group(3))
- completed.append(name)
- emit_event({"type": "package-complete", "name": name})
- returncode = proc.wait()
- reader.join(timeout=5)
- if returncode != 0:
- fail("emerge failed (%s): %s" % (returncode, failure_message(transcript)),
- classify_failure("".join(transcript)))
- link_unversioned_tools(args.names)
- emit_event({
- "type": "complete",
- "status": "ok",
- "installed": len(completed),
- })
- return EXIT_OK
- def main():
- parser = argparse.ArgumentParser(
- prog="usm-spm-emerge",
- description="USM system-package-manager helper for Portage (Gentoo)")
- subparsers = parser.add_subparsers(dest="command", required=True)
- query_parser = subparsers.add_parser(
- "query", help="resolve USM resource refs to system packages")
- query_parser.add_argument(
- "refs", nargs="+", metavar="USM-REF",
- help="resource ref, e.g. bin:valac or pc:glib-2.0.pc")
- query_parser.set_defaults(handler=cmd_query)
- plan_parser = subparsers.add_parser(
- "plan", help="resolve the material install set for atoms")
- plan_parser.add_argument(
- "names", nargs="+", metavar="ATOM",
- help="native gentoo package atom, e.g. dev-build/meson")
- plan_parser.set_defaults(handler=cmd_plan)
- install_parser = subparsers.add_parser(
- "install", help="install system packages, streaming progress events")
- install_parser.add_argument(
- "--test", action="store_true",
- help="resolve only (emerge --pretend); no packages are built")
- install_parser.add_argument(
- "names", nargs="+", metavar="ATOM",
- help="native gentoo package atom, e.g. dev-build/meson")
- install_parser.set_defaults(handler=cmd_install)
- args = parser.parse_args()
- try:
- return args.handler(args)
- except HelperError as e:
- fail(str(e), e.exit_code)
- except SystemExit:
- raise
- except Exception as e:
- message = "unexpected failure: %s" % e
- if args.command == "install":
- fail(message, EXIT_FAILURE)
- fail_plain(message, EXIT_FAILURE)
- if __name__ == "__main__":
- sys.exit(main())
|