meta_package_manager.execution module

CLI-execution engine shared by every package manager.

Runs one manager’s CLI in one subprocess: the meta_package_manager.execution.CLIExecutor mixin (which meta_package_manager.manager.PackageManager inherits) locates the binary and runs it, the meta_package_manager.execution.CLIError exception carries a failed call’s result, and meta_package_manager.execution.highlight_cli_name() themes a binary’s name.

Scheduling many managers at once is the next altitude up, and lives in meta_package_manager.dispatch: the concurrent fan-out primitives, the lock families and the shared ✓/✘ trail. The sudo machinery that cuts across both altitudes (credential priming, the keepalive, the hidden-prompt stall watchdog) lives in meta_package_manager.sudo: this module only consumes it, to wrap escalated commands and diagnose their failures.

Note

The name and intent mirror click_extra.execution from the sibling click-extra project, where the generic layers now live: the concurrency primitives (run_jobs/run_lanes driven by mpm --jobs), the single-subprocess engine (click_extra.execution.run_cli(), which disclosed invocations and streams output to the logs), and the Ctrl+C machinery (click_extra.execution.install_interrupt_handler() terminating the in-flight children registered by run_cli). This module keeps what is package-manager policy: per-operation timeouts, sudo escalation, cooldown enforcement and dry-run.

meta_package_manager.execution.DIAGNOSIS_HEAD_LINES: Final = 3

Leading lines of a failed command’s report relayed at WARNING.

Some CLIs open with their error and print a backtrace after it: RubyGems names the exception and its message first, then every frame. A tail alone then holds frames and no reason. The head stays short, since it is noise for the CLIs that conclude with their error.

meta_package_manager.execution.DIAGNOSIS_TAIL_LINES: Final = 10

Trailing lines of a failed command’s report relayed at WARNING.

Most CLIs conclude with their actual error, so the tail is where the diagnosis lives; the cap keeps a verbose failure (a source build’s compiler spew) from flooding the default view. The raw streams are always available in full, live, at DEBUG.

meta_package_manager.execution.UNRUNNABLE_ERRNOS: Final[frozenset[int]] = frozenset({1, 2, 8, 13, 21})

OSError numbers meaning the CLI cannot be spawned, rather than that it failed.

Each marks the manager unavailable and yields empty output, the same way WinError 193 does on Windows: something was found at that path, and it is not a program this host can run. ENOEXEC is the POSIX twin of that Windows code, raised for a wrong-architecture binary, a truncated download, or a text file carrying the executable bit. GitHub’s ubuntu-26.04-arm image ships one, an x86 pnpm at /usr/local/bin/pnpm, and the pool probes every manager it can see, so without this set that one binary aborts every mpm run on the host.

Anything outside this set (a file-descriptor ceiling, an out-of-memory refusal) describes the machine rather than the CLI, and still propagates.

meta_package_manager.execution.WIN_DEFAULT_PATHEXT: Final = '.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC'

Executable suffixes to search on Windows when %PATHEXT% is unset.

A copy of CPython’s own shutil._WIN_DEFAULT_PATHEXT, which CLIExecutor.search_all_cli() reads to mirror shutil.which()’s behaviour. That attribute is private, so it can be renamed or dropped by any release without notice, so reading it unguarded leaves every manager detection on Windows one AttributeError away from failing. This constant is the fallback; test_windows_pathext_tracks_cpython compares the two on every platform and every interpreter the matrix covers, so a divergence surfaces as a named test failure rather than as a Windows-only crash.

exception meta_package_manager.execution.CLIError(code, output, error, detail=None)[source]

Bases: Exception

An error occurred when running package manager CLI.

The exception internally keeps the result of CLI execution.

detail: str | None

The cause in a word or two, as the ✘ trail line of the failed attempt shows it in parentheses.

It stays None for a failure whose cause mpm does not know: the glyph already says the attempt failed. See failure_detail().

property diagnosis: str

The command’s own account of its failure, capped for log relay.

Prefers <stderr>, the conventional stream for error reporting, and falls back on <stdout> for the tools that report failures there (steamcmd); a command that died silently is reduced to its exit code. Only the first DIAGNOSIS_HEAD_LINES and the last DIAGNOSIS_TAIL_LINES lines are kept, around a counter of the truncated ones: an error either concludes a stream or opens it.

meta_package_manager.execution.VERSION_PROBE: Final = 'version'

Pseudo-operation stamped on _active_operation during version detection.

Not a member of meta_package_manager.capabilities.Operations (no subcommand routes it), but it participates in the same per-operation machinery: OPERATION_TIMEOUTS binds it to the short read-only cap, and CLIExecutor.run() demotes its command disclosure to DEBUG so the per-candidate probes cannot drown the INFO narration.

meta_package_manager.execution.format_plan_command(cmd_args, extra_env=None)[source]

Render a captured mpm --plan command as a copy-pasteable shell line.

Unlike click_extra.execution.format_cli_prompt() (styled, and prefixed with a $ prompt sigil for logs and dry-runs), this returns a plain, unstyled, shell-quoted line: the forced environment assignments followed by the resolved binary and its arguments, ready to paste into a terminal or pipe into a shell. See the plan-mode branch of CLIExecutor.run().

Return type:

str

meta_package_manager.execution.PLAN_RECORDER: Final = <meta_package_manager.execution._PlanRecorder object>

Process-wide sink for CLIExecutor.run()’s plan-mode captures.

A module-level singleton because run executes in the fan-out’s worker threads, where the click context is not reliably reachable. See _PlanRecorder.

meta_package_manager.execution.highlight_cli_name(path, match_names)[source]

Highlight the binary name in the provided path.

The name is only highlighted when it matches one of the recognized match_names, so an unrecognized binary stays plain. Matching is insensitive to case on Windows and case-sensitive on other platforms, thanks to os.path.normcase.

The rendering is delegated to click_extra.execution.highlight_bin_name(), the same helper behind the $-prompt and spawn-trace log lines, so the mpm managers table and the logs can never drift apart.

Return type:

str | None

meta_package_manager.execution.READ_ONLY_TIMEOUT: Final = 120

Default timeout (seconds) for read-only probes and queries.

These operations only inspect state, so a short cap lets a wedged binary fail fast instead of stalling the whole run. The value is generous enough for legitimately slow scans (a guix search from a development checkout, which evaluates every package definition) while still being far below MUTATING_TIMEOUT.

meta_package_manager.execution.MUTATING_TIMEOUT: Final = 500

Default timeout (seconds) for operations that change system state.

Installs, upgrades, removals, channel syncs and cleanups routinely build from source, download large archives or pull entire channels, so they need a long cap. Kept identical to the historical global default so these operations behave exactly as before when no explicit --timeout is given.

meta_package_manager.execution.DEFAULT_TIMEOUT: Final = 500

Fallback timeout (seconds) for a CLI call whose operation is unknown.

Defaults to the conservative MUTATING_TIMEOUT: when in doubt, wait rather than risk killing a legitimate long-running command.

meta_package_manager.execution.OPERATION_TIMEOUTS: Final[dict[str, int]] = {'cleanup': 500, 'doctor': 500, 'install': 500, 'installed': 120, 'orphans': 120, 'outdated': 120, 'remove': 500, 'search': 120, 'sync': 500, 'upgrade': 500, 'upgrade_all': 500, 'version': 120}

Per-operation timeout defaults, applied only when the user has set no explicit --timeout (or per-manager timeout override).

Keyed by the meta_package_manager.capabilities.Operations member name, plus the special "version" detection probe. The keys are validated against the Operations enum by the test suite so the two never drift apart. An operation absent from this map resolves to DEFAULT_TIMEOUT.

meta_package_manager.execution.SPINNER_DELAY: Final = 0.1

Seconds a CLI call must run before its progress spinner appears.

Kept short so the spinner surfaces almost immediately on any call that is not instant: prompt feedback makes mpm feel responsive from the start rather than stalled during the first second. Only the quickest calls (cached version probes, trivial metadata queries) finish within this delay and stay silent; anything slower (a guix search, a source build) shows the spinner right away.

meta_package_manager.execution.STILL_CALL_MARKER: Final = '·'

Marker opening the static line a call held still prints in place of its spinner.

A call that may raise a hidden sudo prompt cannot animate (see _hidden_prompt_risk), so it prints its spinner’s label once instead, before the child starts. Deliberately not a spinner frame: a frozen ⠋ reads as a hung animation, where a neutral dot reads as a line that is not meant to move.

meta_package_manager.execution.STILL_CALL_HINT: Final = 'may ask for your password'

Hint closing the static line of a call held still, faint like a timer.

It says why the line does not move and what may appear below it. It says “may”, since most such calls never escalate, and it never tells the user to type, for the reason the stall notice does not: the prompt may never come.

meta_package_manager.execution.ITALIC_CAPABLE_TERMS: Final = ('xterm', 'tmux')

TERM prefixes whose terminfo declares the italic capability (sitm).

An allow-list, because the terminals that lack italic outnumber the ones that have it: infocmp reports no sitm for the screen, linux, vt, ansi, rxvt, Eterm, cons25 and sun entries, against xterm and tmux which declare sitm=\E[3m. Missing the capability is not benign: SGR 3 is then undefined rather than ignored, and some terminals render it as reverse video.

The two prefixes cover what modern emulators actually export, xterm-256color being the default of iTerm2, Apple Terminal, VS Code and Alacritty, and xterm-kitty / xterm-ghostty the exceptions that ship their own entry. A terminal outside them (wezterm, an alacritty entry where it is installed) loses the italic and keeps the plain rendering, which costs emphasis and never correctness. See _spinner_label().

meta_package_manager.execution.operation_subject(manager_id, operation=None)[source]

Name what a manager is doing, as a manager.operation attribute path.

The subject both live indicators show: the per-call spinner of CLIExecutor._make_spinner and the ✓/✘ trail line of collect_from_managers(), so one manager reads the same whichever of the two is on screen. Shared rather than spelled twice, a drift between them being invisible until both are watched at once.

Only the subject is shared. The trail keeps naming the manager alone in its command half’s place, a trail line covering a whole task where a spinner covers one invocation: cleanup dispatches several commands per manager, and picking one of them to print would be arbitrary.

Parameters:

operation (str | None) – the stamped CLIExecutor._active_operation. A call made outside any operation falls back to the bare manager ID.

Return type:

str

meta_package_manager.execution.elapsed_clock(seconds)[source]

Render how long a finished call took the way a running spinner shows it.

The same (2.3s) fragment the spinner’s timer draws, faint when styling is allowed, so a ✓/✘ trail line closes on the clock its spinner was counting.

The elapsed time is the least important part of the line, and the faint attribute (SGR 2) says so without spending a color: every hue on this stream already carries a status, which is the same reason _spinner_label() leaves the command uncolored. Faint also beats bright_black for the job, that being a color rather than an attribute, and one this project has already been bitten by (see VERSION_PREFIX_COLOR).

Return type:

str

class meta_package_manager.execution.CLIExecutor[source]

Bases: object

Locate a manager’s CLI on the system and run it.

Mixin inherited by meta_package_manager.manager.PackageManager. Owns the CLI-invocation configuration (names, search paths, environment, arguments, timeout) and the engine that searches for the binary, executes it, captures and normalizes its output, accumulates errors, and parses its self-reported version.

Initialize cli_errors list.

cli_names: tuple[str, ...]

List of CLI names the package manager is known as.

This list of recognized CLI names is ordered by priority. That way we can influence the search of the right binary.

..hint::

This was helpful in the case of the Python transition from 2.x to 3.x, where multiple versions of the same executable were named python or python3.

By default, this property’s value is derived from the manager’s ID (see the MetaPackageManager.__init__ method above).

cli_search_path: tuple[str, ...] = ()

List of additional path to help mpm hunt down the package manager CLI.

Must be a list of strings whose order dictates the search sequence.

Most of the time unnecessary: meta_package_manager.execution.CLIExecutor.cli_path works well on all platforms.

extra_env: ClassVar[Mapping[str, str | None] | None] = None

Additional environment variables to add to the current context.

Automatically applied on each meta_package_manager.execution.CLIExecutor.run_cli() calls.

pre_cmds: tuple[str, ...] = ()

Global list of pre-commands to add before before invoked CLI.

Automatically added to each meta_package_manager.execution.CLIExecutor.run_cli() call.

Used to prepend sudo or other system utilities.

pre_args: tuple[str, ...] = ()
post_args: tuple[str, ...] = ()

Global list of options used before and after the invoked package manager CLI.

Automatically added to each meta_package_manager.execution.CLIExecutor.run_cli() call.

Essentially used to force silencing, low verbosity or no-color output.

version_cli_options: tuple[str, ...] = ('--version',)

CLI options used to produce the version of the package manager.

The raw output produced by the package manager CLI will be parsed with the version_regexes below to extract the version number.

version_cli: str | None = None

Alternate binary probed for the manager’s version, instead of the main CLI.

Some manager suites expose no version flag on any of their own binaries (OpenBSD’s pkg_add/pkg_info, Solaris’ pkgadd/pkginfo): they ship with the base system and are versioned with the OS itself. Naming a version_cli (like uname) makes the version probe run that binary with version_cli_options and parse its output with version_regexes, while every operation keeps using the manager’s own cli_path. The binary is resolved with which(); the version resolves to None (manager not fresh) when it is not found.

version_regexes: tuple[str, ...] = ('(?P<version>\\S+)',)

Regular expressions used to extract the version number.

This property must be a tuple of strings, each of which is a valid regular expression that must contain a group named <version>.

The first of these regexes producing a match and returning non-empty <version> group will be used as the version string of the package manager.

That version string will then be sanitized and normalized by meta_package_manager.execution.CLIExecutor.version.

By default match the first part that is space-separated.

Caution

These regexes are compiled with re.MULTILINE only. They are not compiled with re.VERBOSE, so literal whitespace in the pattern is significant and matches whitespace in the CLI output.

version_from_stderr: bool = False

Search <stderr> too for the version, after <stdout>.

Some tools print their version on <stderr> and leave <stdout> empty: LURE reaches Go’s builtin println, and roswell prints its banner there. The probe reads the return value of run_cli(), which is <stdout> alone, so it would find nothing and leave such a manager permanently unavailable.

Both streams are searched, <stdout> first, rather than <stderr> alone: a version that moves to <stdout> in a later release must not take the manager offline. Off by default on purpose. A tool that prints nothing on <stdout> when broken often prints a warning on <stderr>, and the default version_regexes would read a version out of that warning.

stderr_noise: Pattern[str] | None = None

Lines of <stderr> that report no failure, like a tool’s routine warnings.

The failure gate of run() drops them before it reads <stderr>. A non-zero exit whose <stderr> holds nothing else then reads as a status, the way a silent <stderr> already does, and the diagnosis of a real failure keeps only the lines that explain it. The run snapshot keeps the whole stream. None keeps every line.

stop_on_error: bool = False

Tell the manager to either raise or continue on errors.

dry_run: bool = False

Do not actually perform any action, just simulate CLI calls.

plan: bool = False

Capture state-changing CLI calls for inspection instead of running them.

Set by mpm --plan. Unlike dry_run (which simulates every call, read-only queries included), plan mode lets the read-only queries (installed, outdated, search) run for real so the resolved plan reflects actual system state, and records only the state-changing commands (see _MUTATING_OPERATIONS) into PLAN_RECORDER.

timeout: int | None = None

Maximum number of seconds to wait for a CLI call to complete.

None means the user expressed no explicit preference: the effective cap is then resolved per-operation by _resolve_timeout() from OPERATION_TIMEOUTS. A non-None value (the --timeout flag or a per-manager override) wins for every operation.

progress: bool = False

Whether CLI calls may show a progress spinner while they block.

Set by the CLI to an interactive, human-facing run only (a TTY, no serialized output, not at DEBUG verbosity). Even when True the spinner still self-suppresses off a TTY: see _make_spinner(). Defaults to False so programmatic use stays silent.

cooldown: timedelta | None = None

Minimum age a release must have before it can be installed or upgraded.

When set, the manager refuses to bring in any package version published more recently than cooldown ago. This is a mitigation against supply-chain attacks: a malicious release is typically detected and pulled within days of publication, so a waiting period keeps freshly-published (and potentially compromised) versions out of the system. None disables the gate.

Only managers able to enforce a release-age limit honor this, natively through cooldown_env_var or through the per-package release-date probe of release_date(); see supports_cooldown.

cooldown_policy: CooldownPolicy | None = None

Per-manager enforcement posture of an active release-age cooldown.

None (the default) inherits the policy resolved from --cooldown and the [mpm.cooldown] configuration, which the pool’s selection applies to every manager per run. Pin it explicitly through a [mpm.overrides.<id>] override, which keeps precedence over the global resolution: enforce holds the manager fail-closed even under a best-effort run, best-effort waives the requirement and runs it without the safeguard, and off exempts it from the gate entirely, suppressing the cutoff injection even where the manager could honor one.

sudo: bool | None = None

User escalation policy: run this manager’s privileged commands with sudo.

None (the default) means the user expressed no preference, so the built-in default_sudo decides. True/False force escalation on or off for every operation this manager marks privileged (a build_cli(..., sudo=True) call). Set globally by mpm --sudo / mpm --no-sudo and per manager by the [mpm.overrides.<id>] sudo config key, the latter winning (see _select_managers).

Only privileged operations are ever escalated, and only on a host carrying an escalator. Carrying one is the whole platform test, Windows included, since gsudo and Microsoft’s sudo.exe give it one. A manager that escalates internally (internal_sudo) has no such markers and is never wrapped in sudo by mpm: its own sudo reuses the credential cache when prime_sudo() finds it already warm, and is otherwise covered by the silent-call notice in run().

default_sudo: bool = False

Built-in escalation default, used when sudo is None.

False on the base: most managers install into user-writable trees and never need root. The system package managers whose privileged operations require root (apt, dnf, pacman, zypper, …) set this to True so their build_cli(..., sudo=True) operations escalate out of the box, while staying switchable off through sudo (--no-sudo or config) for rootless setups.

internal_sudo: bool = False

Marks a manager whose CLI invokes sudo itself mid-run.

Homebrew cask runs it from installer artifacts, fink re-execs its root commands through it, and the AUR helpers call sudo pacman for their install steps. mpm never wraps such a manager’s commands: either none of its operations carry a build_cli(..., sudo=True) marker (cask, fink), or its default_sudo = False policy leaves the markers it inherits unescalated (the AUR helpers). Running the tool under sudo is often forbidden outright (brew refuses root, makepkg refuses to build). Consumed by prime_sudo(), whose opportunistic probe keeps an already-warm credential cache alive for these internal escalations, and by the silent-call notice in run(), which flags a possibly-hidden password prompt on a cold cache.

Forcing sudo = true on such a manager (config key or --sudo) still never wraps its commands, but does promote it into the up-front prompt path of prime_sudo(), unless the tool drops the cached credentials itself (resets_sudo_timestamp).

internal_sudo_operations: ClassVar[frozenset[str]] = frozenset({'cleanup', 'install', 'remove', 'sync', 'upgrade', 'upgrade_all'})

Operations in which an internal escalator may run its own sudo.

Every mutating operation by default: fink re-execs fink selfupdate and fink cleanup through sudo like its installs, shelly escalates in each mutating verb, and the AUR helpers call sudo pacman to refresh their databases. (restore stamps "install", so it is covered.) A manager narrows the set when some of its commands never escalate, like cask.

Matched against _active_operation by _hidden_prompt_risk and _spends_warm_cache. On a terminal with a cold credential cache, a call in one of these operations runs last, without a spinner and under the stall watchdog, so the prompt of the tool’s own sudo is answerable. Detached like any other call, that sudo has no terminal to ask on and gives up at once. On a warm cache, the call stays in the session of the terminal, where that sudo can spend the cached credentials. mpm handles a tool that drops them itself (resets_sudo_timestamp) as on a cold cache.

Only read on a manager marked internal_sudo.

resets_sudo_timestamp: bool = False

Marks an internal escalator whose CLI drops the cached credentials itself.

Homebrew runs sudo --reset-timestamp on purpose, when a command first needs sudo, and at the start of every command before 7.0.8. A warm cache therefore never spares the prompt of its own sudo. So _hidden_prompt_risk holds for such a manager whatever the cache holds: a call that can prompt still runs last, without a spinner and under the stall watchdog. A forced sudo = true asks no password up front for it either: prime_sudo() warns that the policy does nothing.

Only read on a manager marked internal_sudo.

privileged_operations: ClassVar[frozenset[str]] = frozenset({})

Operations with at least one command this manager marks privileged.

Keyed by the Operations member name, like operation_notes. An operation belongs here when a method implementing it passes sudo=True to run_cli() or build_cli(), with the method-to-operation map of METHOD_OPERATIONS. Those commands are the only ones the escalation policy (sudo, else default_sudo) wraps in the escalator. A definition derives the set from the operations it marks sudo = true.

prime_sudo() reads it to prompt only for the managers that escalate a command of the current run. With [mpm.overrides.gem] sudo = true, mpm sync asks for no password: the gem sources --update command of sync carries no marker. test_privileged_operations holds the set to the markers in the method bodies.

cooldown_env_var: ClassVar[str | None] = None

Environment variable this manager reads to honor a cooldown.

None (the default) means the manager has no native release-age mechanism to feed through the environment; it may still honor a cooldown through the per-package release-date probe (see _probes_release_date). A subclass that sets this string advertises native support (see supports_cooldown); the value produced by cooldown_env_value() is then injected into the environment of every CLI call.

windows_creation_flags: int = 0

Additional Windows process creation flags OR-ed with CREATE_NO_WINDOW.

Use this on individual managers to control how their subprocess is attached to the calling process’s console. For example, setting this to subprocess.DETACHED_PROCESS (0x8) fully detaches the child from the parent’s console. Any grandchild process (like a COM server or installer EXE) that calls GenerateConsoleCtrlEvent(0) on exit will then fail silently because there is no console to broadcast to.

No-op on non-Windows platforms (getattr returns 0 for Windows-only flags).

windows_processes_to_cleanup: tuple[str, ...] = ()

Windows process image names to forcibly terminate after each CLI call.

When a package manager spawns grandchild processes that outlive the direct subprocess (like winget’s WindowsPackageManagerServer.exe COM server), those orphans can linger and consume resources. List the image names here so they are killed after communicate() returns.

No-op on non-Windows platforms.

run_cache: dict[tuple, tuple[int, str, str]] | None = None

Optional cache that de-duplicates identical CLI runs across a lane’s managers.

None by default, which disables caching: every run() call spawns its own subprocess. Two callers install a shared dict, each for the duration of one lane:

The replay still walks run()’s logging and failure gate, so a failed shared command is attributed to every member. Keyed on the resolved command line, its environment and whether the operation in flight changes the system (see _MUTATING_OPERATIONS), so only genuinely identical invocations collapse.

A cached read holds until the lane changes the system. A state-changing command that runs for real first drops every read its lane cached, so the next one spawns again and observes what the command did. The two readings of ChangeReport depend on it: both run one command line, and a second reading replayed from the first reports that nothing moved. The results of state-changing commands stay for the whole lane, their replay being the purpose of the cache, and are never served to a read. A command that mpm --plan captures or mpm --dry-run simulates changes nothing, and drops nothing.

Caution

Not thread-safe, and it does not need to be: both callers bind a cache to a lane, and click_extra.execution.run_lanes() runs a lane’s items serially on a single worker. Handing one dict to managers that run concurrently would race two peers into spawning the same command anyway, losing the de-duplication rather than corrupting anything, so the lane is what makes the cache work at all.

cli_errors: list[CLIError]

Accumulate all CLI errors encountered by the package manager.

Every CLIError produced by run() lands here, whether or not it is also raised: a failure the caller goes on to swallow (installed_or_empty() and its peers) is still a failure this manager committed, and the end-of-run summary, the serialized errors payload and the ✓/✘ trail all read this list to say so.

Recording only the non-raising half made a manager’s visibility hinge on whether its query happened to pass must_succeed: mpm list printed “Could not list installed packages.” and still scored the manager ✓, leaving it out of the closing count.

A failed VERSION_PROBE stays out, and is the only thing that does. The probe is a liveness question about whatever binary cli_path resolved in PATH, not work the user asked for, and unavailable_reason already reports its answer. Kept here, it named a manager mpm had correctly dropped in the end-of-run summary of every command, on every host where an unrelated binary shadows a manager’s name: PearCleaner’s pear symlink had mpm outdated warn about a PHP manager the user never installed (issue 2119). A probe that never completed keeps its entry, a timeout or an interrupt being plumbing rather than a verdict: that path appends from _spawn and never reaches the gate.

property supports_cooldown: bool

Whether this manager can enforce a release-age cooldown.

Either natively, by injecting cooldown_env_var into every CLI call, or through the per-package release-date probe advertised by _probes_release_date.

cooldown_env_value()[source]

Render cooldown as the value of cooldown_env_var.

Defaults to the RFC 3339 timestamp of the most recent release date still allowed, i.e. now minus the cooldown. Managers whose environment variable expects another format (a number of minutes, a bare day count, …) override this.

Return type:

str

cooldown_rounded_up(unit_seconds)[source]

Render cooldown as an integer count of unit_seconds-long units, rounded up.

Helper for the cooldown_env_value() overrides of managers whose native release-age knob expects a unit count rather than the default RFC 3339 timestamp (npm’s day-based min-release-age, pnpm’s minute-based minimumReleaseAge). Sub-unit cooldowns round up so the gate over-protects rather than silently collapsing to 0 (the “no cooldown” sentinel).

Return type:

str

cooldown_env()[source]

Environment fragment enforcing the cooldown, empty when inactive.

Returns an empty mapping unless a cooldown is set and the manager supports it and its cooldown_policy has not exempted it (off). Merged into the environment of every run() call.

Return type:

Mapping[str, str | None]

search_all_cli(cli_names, env=None)[source]

Search for all binary files matching the CLI names, in all environment path.

This is like our own implementation of shutil.which(), with the difference that it is capable of returning all the possible paths of the provided file names, in all environment path, not just the first one that match. And on Windows, prevents matching of CLI in the current directory, which takes precedence on other paths.

Returns all files matching any cli_names, by iterating over all folders in this order:

  • folders provided by cli_search_path,

  • then in all the default places specified by the environment variable (i.e. os.getenv("PATH")).

Only returns files that exists and are not empty.

Caution

Symlinks are not resolved, because some manager like Homebrew on Linux relies on some sort of symlink-based trickery to set environment variables.

Return type:

Generator[Path, None, None]

which(cli_name)[source]

Emulates the which command.

Based on the search_all_cli() method.

Return type:

Path | None

sibling_cli(name, *, same_dir=False)[source]

Resolve the path of a sibling binary of the manager’s main CLI.

Some managers ship as a suite of binaries (xbps-install/xbps-query, pkg_add/pkg_info, emerge’s qlist): an operation then runs a sibling instead of the main CLI. By default the sibling is searched like the main CLI itself (which(), honoring cli_search_path), and a missing binary raises FileNotFoundError rather than silently falling back to the wrong program.

same_dir=True instead takes the sibling from the directory of cli_path, without an existence probe: suites installing all their binaries side by side (XBPS, Nix) guarantee the neighbor, and resolving it from the same directory can never mix two installations. A genuinely missing file then surfaces at spawn time.

Return type:

Path

property cli_path: Path | None[source]

Fully qualified path to the canonical package manager binary.

Try each CLI names provided by cli_names, in each system path provided by cli_search_path. In that order. Then returns the first match.

Executability of the CLI will be separately assessed later by the meta_package_manager.execution.CLIExecutor.executable property below.

property version: TokenizedString | None[source]

Invoke the manager and extract its own reported version string.

Returns a parsed and normalized version in the form of a meta_package_manager.version.TokenizedString instance.

Skipped on platforms where the manager is not supported, even if cli_path resolved to an executable: that binary almost certainly belongs to a different tool that happens to share the same name (e.g. GNU make on macOS getting matched by the FreeBSD ports manager), so probing it would either misreport the version or surface confusing error output.

property executable: bool[source]

Is the package manager CLI can be executed by the current user?

property subject: str

The manager.operation path naming what this manager is doing now.

Labels every line logged inside an operation (warning:brew.install:) and names the spinner and the trail line of the same call, so one call reads alike on all three. Outside any operation it is the bare ID: see operation_subject(). A line about the manager itself, logged while no operation runs (discovery, selection, capability checks), labels it with the bare id instead.

acting_as(operation=None, *, stop_on_error=None)[source]

Temporarily adjust the manager’s execution state, restoring it on exit.

operation re-stamps _active_operation (the per-operation timeout and watchdog key) for the duration of the block; None leaves the current stamp untouched. stop_on_error likewise overrides the failure policy when set: the per-package state changers run their action under stop_on_error=True so a botched operation raises and is recorded by the caller instead of being silently accumulated.

The public seam for callers needing a scoped state override: the CLI layer must never poke _active_operation or stop_on_error directly.

Return type:

Iterator[None]

new_errors()[source]

Collect the cli_errors this manager accumulates inside the block.

The yielded list is filled when the block exits, whatever the exit: a fan-out task reads it after the operation ran to decide whether the manager failed its ✓/✘ trail line. Only the errors of this block are reported, since the pool’s instances live for the whole process and carry the errors of every previous invocation.

Return type:

Iterator[list[CLIError]]

run(*args, extra_env=None, must_succeed=False)[source]

Run a shell command, return the output and accumulate error messages.

args is allowed to be a nested structure of iterables, in which case it will be recursively flatten, then None will be discarded, and finally each item casted to strings.

Running commands with that method takes care of:
  • disclosing the invocation at INFO (the reproducible $-prompt line with forced environment variables) and streaming the raw output live to DEBUG, prefixed with the manager ID, via click_extra.execution.run_cli()

  • flagging, on a terminal, the call of an internal escalator that goes silent while it may be blocked on a hidden password prompt (see _hidden_prompt_risk and _StallWatchdog)

  • detaching every other call into its own POSIX session and process group, so a timeout or Ctrl+C reaps the whole process tree and a wedged grandchild cannot linger as an orphan; the flagged call above keeps the controlling terminal so its sudo prompt stays answerable

  • removing ANSI escape codes from subprocess.CompletedProcess.stdout and subprocess.CompletedProcess.stderr

  • returning ready-to-use normalized strings (dedented and stripped)

  • letting mpm --dry-run and mpm --stop-on-error have expected effect on execution

The exception above has two more members, which both spend the credential cache: a call mpm escalates itself, and the call of an internal escalator that may run its own sudo on a warm cache (see _spends_warm_cache). sudo keys that cache to the terminal and to its session, which a session of its own would hide.

A call mpm escalates itself leads no process group of its own, so its timeout kill reaches the sudo process alone. sudo runs the command under a monitor process whenever use_pty is on, its upstream default since 1.9.14, so the manager binary survives the kill: an apt upgrade outlasting the state-change timeout keeps /var/lib/dpkg/lock-frontend, and the next apt or aptitude call fails on a lock mpm already declared finished.

Todo

Delete the paragraph above once the click-extra floor reaches the release whose run_cli also kills a child’s descendants one PID at a time. It reads the process tree before the kill and signals every descendant, which reaps the escalated command with the sudo that spawned it. mpm cannot do this itself: it never sees the child’s PID, and the kill reparents the descendants before subprocess.TimeoutExpired surfaces, cutting the links a walk follows.

Parameters:

must_succeed (bool) – if True, raise meta_package_manager.execution.CLIError when the command fails, regardless of the user-facing stop_on_error preference, rather than accumulating the error for an end-of-run summary. Use for calls whose output is parsed (JSON, XML, regex), where a swallowed failure would be indistinguishable from empty results. A non-zero exit that leaves <stderr> empty, or holding only stderr_noise, is tolerated as a benign status code (npm and pnpm outdated exit 1 when updates exist); only the per-package state changers, which run under a patched stop_on_error, treat every non-zero exit as a failure. See the failure gate below for details.

Return type:

str

build_cli(*args, auto_pre_cmds=True, auto_pre_args=True, auto_post_args=True, override_pre_cmds=None, override_cli_path=None, override_pre_args=None, override_post_args=None, sudo=False)[source]

Build the package manager CLI by combining the custom *args with the package manager’s global parameters.

Returns a tuple of strings.

Helps the construction of CLI’s repeating patterns and makes the code easier to read. Just pass the specific *args and the full CLI string will be composed out of the globals, following this schema:

$ [<pre_cmds>|sudo --non-interactive] <cli_path> <pre_args> <*args> <post_args>
Return type:

tuple[str, ...]

Each additional set of elements can be disabled with their respective flag:

  • auto_pre_cmds=False to skip the automatic addition of self.pre_cmds

  • auto_pre_args=False to skip the automatic addition of self.pre_args

  • auto_post_args=False to skip the automatic addition of self.post_args

Each global set of elements can be locally overridden with:

  • override_pre_cmds=tuple()

  • override_cli_path=str

  • override_pre_args=tuple()

  • override_post_args=tuple()

On UNIX, an operation marked privileged (sudo=True) is escalated only when the per-manager policy opts in (sudo, falling back to default_sudo). It is then run through sudo with --non-interactive (it spends the credential cache warmed by prime_sudo() and fails fast rather than blocking on a password prompt). When escalation applies, override_pre_cmds is not allowed to be set and auto_pre_cmds is forced to False. A non-UNIX host never escalates.

run_cli(*args, auto_extra_env=True, auto_pre_cmds=True, auto_pre_args=True, auto_post_args=True, override_extra_env=None, override_pre_cmds=None, override_cli_path=None, override_pre_args=None, override_post_args=None, force_exec=False, must_succeed=False, sudo=False)[source]

Build and run the package manager CLI by combining the custom *args with the package manager’s global parameters.

After the CLI is built with the meta_package_manager.execution.CLIExecutor.build_cli() method, it is executed with the meta_package_manager.execution.CLIExecutor.run() method, augmented with environment variables from self.extra_env.

All parameters are the same as meta_package_manager.execution.CLIExecutor.build_cli(), plus:

  • auto_extra_env=False to skip the automatic addition of self.extra_env

  • override_extra_env=dict() to locally overrides the later

  • force_exec ignores the mpm --dry-run, mpm --stop-on-error and mpm --plan options to force the execution and completion of the command. It is used for reads whose output is needed regardless (version detection, yarn global dir), which must run for real even when the user asked to simulate or to only plan mutations.

  • must_succeed raises on non-zero exit regardless of mpm --stop-on-error. See run() for details.

Return type:

str