Add a new package manager¶
Implement support for a new package manager in mpm, or complete an incomplete integration. If adding a manager requested via a GitHub issue, extract CLI output samples from the issue body to guide the implementation.
Completing an incomplete integration¶
External contributors often submit a working manager module (managers/<name>.py, pool.py, conftest.py) but skip the documentation and metadata files. See kdeldycke/meta-package-manager#1758 for a typical example: the PR added code and tests but was missing 10+ files.
When asked to āintegrate furtherā, āfill gapsā, or āfinishā a manager that already has code:
Read the existing manager module to understand supported operations and platforms.
Walk the file checklist below and check every file for the managerās presence. The most commonly missed files are:
readme.md(Sankey + operations matrix),docs/meta_package_manager.managers.md,extra-labels/mpm.toml,labels.py,test_pool.py(manager count), andchangelog.md.Verify the
requirementversion specifier by fetching the upstream release history. Check when the features the code depends on (like--jsonoutput) were actually introduced. Contributors often default to>=1.0.0without checking.If the manager wraps or complements another (like sfsu wraps Scoop), merge their label rules under a single
š¦ manager:label. Use the-basedsuffix convention for the group name inlabels.py(likescoop-based) to avoid colliding with the manager ID itself.Fetch the upstream repository (README, releases, changelog) to verify CLI output formats match the parsing code.
Check class attribute ordering against the base class. The
test_content_ordertest enforces that class-level attributes and methods follow the canonical order defined inPackageManager. Common mistakes:version_regexesbeforepost_args, ornameafterhomepage_url.If the manager delegates operations to another managerās CLI, use the
Delegatedescriptor fromcapabilities.pyinstead of repeatingoverride_cli_pathboilerplate. See the Delegating operations section below.
Choose a template¶
Pick an existing manager with a similar CLI as your starting point. Read the template file in full before starting.
Pattern |
Example |
When to use |
|---|---|---|
Simple regex parsing |
|
CLI outputs fixed-width or whitespace-delimited text |
JSON output |
|
CLI supports |
Multiple compiled regexes |
|
Complex text output requiring several capture patterns |
Shell function wrapper |
|
Manager is a shell function, not a standalone binary |
Sibling binaries |
|
Different operations use different CLI binaries in the same directory |
Subclass of existing manager |
|
Manager is a drop-in replacement or wrapper for another manager already implemented |
Delegate to another manager |
|
Manager has its own CLI for read operations but delegates mutating operations (install, upgrade, remove) to another managerās binary |
Subclassing is the lightest option: yay.py is only 39 lines because it inherits almost everything from pacman.py. If the new manager shares the same CLI interface as an existing one, subclass it and override only what differs.
Delegation via Delegate is for managers that share the same package ecosystem but have different CLI interfaces. Unlike subclassing, the read operations (list, search, outdated) have completely different implementations, but mutating operations reuse the other managerās methods verbatim.
Typical manager modules range from 140 to 260 lines. Larger implementations (350-570 lines) tend to involve managers with unusual output formats or many edge cases like fwupd.py, winget.py, or pkg.py.
Implementation¶
Create meta_package_manager/managers/<name>.py. Follow the import pattern, class structure, and TYPE_CHECKING block from your template exactly.
Class-level attributes and methods must follow the canonical order defined in PackageManager (enforced by test_content_order). The order is: homepage_url, platforms, requirement, cli_names, cli_search_path, extra_env, pre_cmds, pre_args, post_args, version_cli_options, version_regexes, then operations (installed, outdated, search, install, upgrade_all_cli, upgrade_one_cli, remove, sync, cleanup).
Class attributes¶
Required:
homepage_url: official project URL.platforms: use constants fromextra_platforms(ALL_PLATFORMS,LINUX_LIKE,MACOS,WINDOWS,UNIX_WITHOUT_MACOS, etc.). Combine with tuples:platforms = LINUX_LIKE, MACOS.
Common optional:
requirement: minimum version specifier (e.g.,">=2.0.0"). Set this to the earliest version that supports all features the implementation depends on. If the code parses--jsonoutput, check the upstream release history to find when that flag was introduced. Do not default to>=1.0.0without verification.cli_names: tuple of binary names to search for. Defaults to(lowercase_class_name,). Set explicitly when the binary name differs from the class name (e.g.,cli_names = ("nix-env",)for classNix).version_regexes: tuple of regex strings with a(?P<version>...)named group.version_cli_options: tuple of args to get version. Defaults to("--version",).pre_args,post_args: global arguments prepended/appended to every CLI call. Use these for flags like--no-coloror--quietthat apply to all operations.extra_env: dict of environment variables to suppress colors, pagers, interactive prompts, etc.cli_search_path: extra directories to find the binary (e.g.,("~/.sdkman/bin",)).
Operations¶
Each operation maps to one of these methods. Implement as many as the manager supports. Unimplemented operations are automatically skipped by mpm.
Operation |
Method signature |
Returns |
Notes |
|---|---|---|---|
Installed |
|
|
Yield packages with |
Outdated |
|
|
Yield packages with |
Search |
|
|
Decorate with |
Install |
|
|
Decorate with |
Upgrade all |
|
|
Return |
Upgrade one |
|
|
Same as above. Decorate with |
Remove |
|
|
Optional. |
Sync |
|
|
Optional. For refreshing package metadata from remote sources. |
Cleanup |
|
|
Optional. For garbage collection, cache clearing, orphan removal. |
Key helpers from the base class:
self.run_cli(*args, **kwargs)executes the manager CLI and returns stdout.self.build_cli(*args)builds a command tuple without executing it (used byupgrade_all_cliandupgrade_one_cli).self.package(id=..., ...)creates aPackagewithmanager_idpre-filled.self.cli_pathresolves to the discovered binary path. Use.parentto find sibling binaries for operations that use a different CLI (seenix.pyforsyncandcleanup).
Delegating operations to another manager¶
When a manager uses its own CLI for read operations but delegates mutating operations to another managerās binary, use the Delegate descriptor from capabilities.py:
from ..capabilities import Delegate
from .scoop import Scoop
class SFSU(PackageManager):
_scoop = Delegate(Scoop)
# Read operations use sfsu's own CLI with JSON output.
@property
def installed(self) -> Iterator[Package]:
output = self.run_cli("list", "--json")
...
# Mutating operations delegate to scoop.
install = _scoop.install
upgrade_all_cli = _scoop.upgrade_all_cli
upgrade_one_cli = _scoop.upgrade_one_cli
remove = _scoop.remove
The Delegate factory resolves the target managerās CLI binary via self.which() and temporarily sets _delegate_cli_path on the instance so that build_cli routes the command through the target binary. The host managerās post_args are automatically suppressed during delegation.
Place _scoop = Delegate(Scoop) at the top of the class body (before homepage_url). Place individual delegation assignments (install = _scoop.install) in the canonical operation order, interspersed with the other operations.
Do not subclass when the two managers have completely different output formats for read operations. Subclassing is for managers that share the same CLI interface. Delegation is for managers that share the same package ecosystem but have different CLIs.
CLI output guidelines¶
Use
--long-form-optionsfor self-documenting CLIs.Suppress colors and emoji (
--no-color,--color=never, etc.) viapost_argsorextra_env.Prefer machine-readable output (JSON, XML, CSV) over text parsing. When parsing text, use class-level compiled regexes with named groups.
Include at least one CLI output sample in each methodās docstring as a
.. code-block:: shell-sessionblock. This helps future maintainers verify parsing without access to the actual manager.Read Falsehoods programmers believe about package managers to anticipate edge cases in package naming and versioning.
File checklist¶
Every new manager touches the same set of files. This list is derived from all 30 manager-addition commits in the project history.
Always required¶
File |
Change |
|---|---|
|
The new manager implementation. |
|
Add import (sorted by module name) and class to |
|
Add |
|
Increment both count assertions in |
|
Add |
|
Add entry to the Sankey diagram (alphabetical) and a row to the operations matrix with correct platform and operation flags. |
|
Add |
|
Add manager name (and ecosystem name if different) to |
|
Add a |
|
If the manager belongs to an ecosystem group, add it to the appropriate frozenset in |
When applicable¶
File |
When |
Change |
|---|---|---|
|
Manager can be installed on CI runners. Check if itās available via an existing package manager (like Scoop, apt, brew) on the target OS. |
Add an install step in the manager setup section, near related managers. |
|
Manager already appears in the comparison table. |
Add |
|
Manager is a distributor of mpm itself (like Homebrew, Scoop, Nix, or an AUR helper). Most managers are not. |
Add a CI job testing |
Validate¶
$ uv run -- pytest tests/test_pool.py tests/test_managers.py -x -q
$ uv run --group typing mypy meta_package_manager/managers/<name>.py
The test suite enforces: valid ID format, homepage URL, platform declarations, version regexes, no duplicate IDs, correct pool count, canonical attribute ordering (test_content_order), and label group disjointness.
Common validation failures after adding a manager:
test_manager_count: forgot to increment the count intest_pool.py.test_content_order: class attributes are not in the canonical order (likeversion_regexesbeforepost_args).Label group collision: the group name in
labels.pycollides with a manager ID. Use the-basedsuffix (likescoop-based,pip-based).