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.
Confirm the tool is a candidate¶
Not everything that installs software is one. Two grounds disqualify a tool outright, whatever its popularity, and neither is a judgement about effort:
A dead upstream. The stability policy flags an abandoned manager
unmaintained, hides it from default selection, drops it from the test matrices and eventually removes it altogether. A wrapper written for an already-retired tool starts at the end of that lifecycle, so it is not written.No registry of its own. A tool that merely unifies syntax or declarations across other package managers reaches no package
mpmcannot already reach through the backend it wraps directly, so wrapping it buys a translation or reconciliation layer and not one extra package. This is what rules out the declarative multi-backend managers (metapac,decman,declaro) and the syntax shims (upt) — several of them actively developed. Such a tool may still deserve a benchmark column as a peer ofmpm; that is a separate question from wrapping it.
Target the live end of a lineage rather than whichever name is most familiar. Shougo’s Vim managers ran neobundle → dein → dpp.vim before Neovim absorbed the job into core, and mpm wraps vim-pack alongside lazy; on the Zsh side antibody gave way to antidote, which mpm wraps next to zinit. Siblings are not a lineage: two live tools serving one ecosystem are each judged on their own, and wrapping one says nothing about the other. A lineage can also fork, and then each successor is judged separately: packer.nvim’s unmaintained notice names both lazy.nvim and pckr.nvim.
That fork is the worked example of the criterion that decides an editor or shell plugin manager, since neither ships a package database mpm can query directly. Ask whether the tool can be driven to completion with nobody at the keyboard. lazy.nvim can, through a headless Neovim plus a JSON lockfile listing installed plugins without starting the editor, which is why it is wrapped. pckr.nvim cannot: no documented completion signal, a confirmation prompt that blocks by default, and a maintainer declining synchronous behavior (lewis6991/pckr.nvim#12). dpp.vim cannot either: its work runs in a Deno process the editor starts, with no headless entry point documented at all. Design that entry point in from the start when proposing a manager upstream; retrofitting one meets resistance.
A candidate is not required to support every operation. Inventorying and updating are enough on their own, and a missing install disqualifies nothing. That pair already beats what the competing wrappers do for the same tools, which is a coarse upgrade of a whole category with no listing at all, so wrapping at that level turns an opaque bulk update into something a user can inspect. topgrade is wrapped on upgrade_all alone, and lazy on inventory plus update: lazy.nvim materializes only the plugin set the user’s own Lua configuration names, so an install would mean mpm editing configuration it does not own. Declare the operations the tool genuinely supports, let mpm auto-skip the rest, and record in the class docstring why each absent one is absent. Never fake an operation with a mutating command.
There is a floor under that, and topgrade is where everything below it goes. topgrade sits in the pool precisely as the catch-all for tools too thin to wrap on their own: it auto-detects and upgrades whatever it finds on the host, so every tool it drives is already reachable through mpm upgrade --topgrade without mpm learning any of them. That makes it the sinkhole, and it changes what a marginal candidate has to prove.
Apply the test in this order:
Does the candidate offer an inventory? If it can list what it has installed, wrap it. That is the one thing
topgradecategorically cannot do for any tool, having no listing at all, so the inventory is the whole gain and a missinginstall/remove/outdateddoes not diminish it. This is whylazy,vim-pack,zimandzplugare wrapped on inventory plus update.If not, does it offer any per-package operation? A tool with no listing but real
installorremoveverbs still reaches packages one at a time, whichtopgradecannot.steamcmdis wrapped oninstallalone on those grounds, andsheldononremoveplus update.If neither, and
topgradealready drives it, decline. A wrapper whose entire honest surface isupgrade_allbuys one more way to run an upgradempmalready reaches, at the cost of a full manager’s checklist and its ongoing maintenance.zris the worked example: plugins are the arguments you hand it, so it owns no inventory, andzr --updateis already insidetopgrade’s catalog.
Check docs/benchmark.yaml before leaning on step 3: it is only an argument for tools topgrade actually covers. A thin tool topgrade does not drive reaches nobody, and is judged on its own merits.
Three further requirements are enforced in code. They are not preferences: a tool that cannot clear them cannot be wrapped in the current architecture, however much work is thrown at it.
Requirement |
Enforced by |
What it rules out |
|---|---|---|
An executable CLI |
|
A tool shipped only as a file meant to be sourced, with no binary anywhere and no interpreter to key the manager on. |
A reportable version |
|
A tool that reports no version through any binary: without one the manager is never considered available. |
System scope |
|
A tool whose packages live inside one project tree rather than on the machine. |
The first two have escape hatches worth trying before declaring a tool impossible. A shell-function manager can key on the interpreter that runs it instead of on its own sources: zinit and antidote are both wrapped that way, with Zsh as their CLI and their version probe doubling as the presence check. Check the file mode before assuming otherwise, since a shebang is not an executable bit: antidote ships its CLI mode 644 and Homebrew installs it as package data under share/, never linking it into bin. A manager whose own binaries expose no version can name a companion binary through CLIExecutor.version_cli.
Keying on a shared interpreter raises a second problem, which that same probe solves. Two managers may legitimately want the same binary: vim-pack and lazy both run nvim. What separates them is that each probe reports a version only once its own tool is found, lazy by testing for the lazy.nvim checkout before putting it on the runtime path, so a host with a bare editor leaves it unavailable rather than shadowing every machine that has one. Guard the probe so an absent tool exits cleanly and prints nothing, instead of raising.
dein.vim is the worked example of both hatches failing, and the shape to compare a candidate against. It is Vimscript with no binary anywhere, and nothing to test for that would make an nvim or vim probe conclusive the way lazy’s is. It reports no version either: g:dein#_cache_version is an internal state-format counter, and its releases are Git tags on a checkout the user places freely.
When a tool is rejected, record the decision rather than leaving it implicit: add a row to docs/unsupported.md and its id to the unsupported list of docs/benchmark.yaml. The benchmark then renders a ❌ linking to the reason instead of a blank cell. That changes the rendered table, so run click-extra refresh-directives readme.md docs afterwards or test_mirror_blocks_in_sync fails: a decline is never a docs-only edit.
Every tool assessed gets one of the two outcomes, always. Wrapped, or written down as unsupported with a rationale. Nothing is left in between, including a tool waved off in passing during a discussion: that is still a decision, and an unrecorded one is indistinguishable from an unexamined one. The target is total coverage of everything that installs software, so a blank benchmark cell is a gap to close rather than a neutral state, and “not worth wrapping” is a rationale to write out, not a reason to skip the row.
Ground the reason upstream whenever the blocking behavior has been raised there. Search the tool’s tracker for the missing capability and link what you find: a feature request closed not-planned, a maintainer stating the position, or an open request left unaddressed for years. Quote the deciding sentence and anchor the link on the exact #issuecomment-<id> when a comment is what settles it, exactly as the benchmark’s ❌ cells do. zgenom is the worked example: its decline rests on reporting no version, and the request to tag releases was closed on the maintainer’s own “I consider everything merged into main as a stable release”. That turns a verdict a reader has to trust into one they can audit, and it dates the decision, so a tool whose upstream later changes course can be reassessed against the same link. When no such discussion exists, say so rather than implying one: an absence that is deliberate design (zr treats plugins as arguments and so owns no inventory) is itself the reason.
Choose an implementation strategy: class-based or config-based¶
Before writing anything, decide how the manager will be implemented. mpm supports two paths:
Class-based: a Python module in
meta_package_manager/managers/. Full power: multi-line or stateful output parsing, version pinning, per-operation search flags, conditionalsudo, delegation, arbitrary logic. It is the most capable path, and what the rest of this document describes.Config-based: a declarative
[mpm.managers.<id>]block thatmpmturns into a live manager at startup, with no Python (documented in Per-manager overrides, “Define a new manager”). Quick to write, but constrained: each operation is a fixed argument list, and listings must parse either line-by-line with a single regex or as one flat top-level JSON array. The DSL covers sibling binaries (a per-operationclikey), unconditionally privileged operations (a per-operationsudo = truekey plus a manager-leveldefault_sudo), and version probes on a companion binary (version_cli, for suites versioned with the OS). A definition can live two places: in a user’s own trusted configuration file (a private, per-machine manager), or bundled withmpmas read-only package data (a manager shipped to every user, like a built-in). The bundled path is howmpmdistributes a simple manager as data instead of code.
Reach for config-based only when every one of these holds. If any fails, the manager needs a class:
Requirement |
Rules out config-based when |
|---|---|
A version command prints a regex-extractable version string, on the manager’s own CLI or a |
No binary reports a usable version at all (like macOS |
|
Records span multiple lines ( |
One command per operation fully populates each package record. |
A record needs enriching through a second, per-package call ( |
Every mutating operation is one argument list with |
An operation needs conditional |
The manager installs globally. |
Packages are scoped to an activated project or environment ( |
Version pinning and native exact/extended search filtering are both unnecessary. |
The manager’s whole point is selecting versions, or search must be resolved exactly server-side. |
Config-based skips the class machinery: no Python module, no pool.py registration, no version pinning or delegation. A private definition needs nothing beyond your own config file; shipping one bundled adds only a short metadata checklist (see below). Reach for a class when the manager needs power the DSL cannot express, and upstream it if it would help others: Per-manager overrides and Security model explain why a reviewed, shipped manager beats executable configuration.
Whatever the path, identify the tool’s escalation model before mapping operations — each demands a different treatment:
Plain root-requiring (most system managers): mark mutating operations privileged (
sudo = truein a definition;run_cli(..., sudo=True)plusdefault_sudo = Truein a class).Self-escalating (fink re-execs itself under
/usr/bin/sudoand no-ops when already root): never mark operations privileged, or sudo stacks on sudo.Broker-based (pkcon hands transactions to a polkit-authorized daemon): no escalation at all; note that unattended runs depend on the broker’s policy.
Root-refusing (chromebrew hard-aborts as root): no escalation, and never wrap in sudo manually.
Also check whether the platforms tokens exist in extra-platforms (VALID_PLATFORM_TOKENS accepts any platform or group ID). A missing distro detection is an upstream extra-platforms addition (same author): land it there, track git main via [tool.uv.sources] until the release, then relax to the PyPI floor. The new-manager issue template’s platform checklist derives from MAIN_PLATFORMS and is enforced by test_new_package_manager_issue_template, so regenerate it when platforms land.
Config-based managers¶
The declarative schema (required keys, every operation, the regex and JSON parsers, placeholders, worked examples) is the “Define a new manager” section of Per-manager overrides, which is the source of truth. This section adds only the authoring workflow and the pitfalls that decide success.
Capture real output first. For each operation you plan to declare, run the actual CLI and paste its output. Confirm a single per-line regex or one flat JSON array can extract
package_id(plusinstalled_versionforinstalled,latest_versionforoutdated). Never assume a format. When the platform cannot run locally (OpenBSD, SliTaz, Solaris, …), derive the format from upstream instead: read the exactprintf/echo/printstatements that emit each line in the tool’s source, cite them, and mark reconstructed samples as source-derived in comments. Never invent output.Write the block. Add
[mpm.managers.<id>]with an<id>that no built-in uses. Setplatforms, theoperationstable, and the identity fields (cli_names,requirement,version_regexes, …). Silence color and interactivity viapre_args,post_argsorextra_env(likeNO_COLOR = "1") so the parser sees clean text.mpm config-templateprints the built-ins’ overridable fields as a formatting reference.Declare only expressible operations. A manager with no non-mutating “list upgradable” command (common:
soar,appman,gh extension) omitsoutdated;mpmauto-skips it andupgrade --allstill works. Never fake an operation with a mutating command.Validate against the real CLI.
mpmchecks the definition at load and reports the first problem with a precise path:$ mpm --config ./my-managers.toml managers $ mpm --config ./my-managers.toml --<id> installed
Add tests. For a private definition, mirror
tests/test_manager_definition.py:parse_manager_definitionfor validation cases,build_manager_class(...)with a monkeypatchedrun_clifor parsing, and thefake_toolfixture for an end-to-end run through a real subprocess. For a bundled definition, ship the[samples]fixtures in the TOML file itself instead (see the checklist below): the suite globs the shipped files and derives its checks from them.
Design around the DSL’s fixed limits (all detailed in Per-manager overrides): no version pinning (install and upgrade always take the latest, {version} is never substituted); listings are line-by-line regex or a single flat JSON array, with no multi-line records, pagination, or value transforms; search cannot declare native exact or extended filtering, so mpm refilters the results itself. If any of these is load-bearing for the manager, stop and write a class instead.
Where a config-based definition lives¶
A definition has two homes:
Private (a user’s config). Drop the
[mpm.managers.<id>]block into your own configuration file.mpmpicks it up on the next run: nothing else to touch, and it never leaves your machine.Bundled (shipped with
mpm). Put the block in its ownmeta_package_manager/managers/<id>.tomlfile.mpmloads every shipped*.tomlat startup and registers it like a built-in, so every user gets its--<id>flag. Bundled files are read-only package data, so they load without the config-file trust gate that guards a user’s own definitions (see Security model).meta_package_manager/managers/gh_ext.tomlis the worked example.
Shipping a bundled definition is far lighter than the class-based checklist below, with no module:
File |
Change |
|---|---|
|
The definition (one |
|
Optional: a |
|
Optional: a well-known ecosystem alias in |
|
Add a |
|
Increment the |
|
A |
|
If the manager already had a competitor row, delete its |
|
Two hand-maintained tables, each needing a row. The Supported managers table takes the manager’s cooldown status ( |
Then regenerate the pool-derived blocks, both run by repomatic’s update-docs job (a manual run is just a pre-check): docs/docs_update.py writes the PyPI keywords and labeller rules in pyproject.toml, the readme’s operation-matrix platform footnotes and the manager’s docs/managers/<id>.md page stub; click-extra refresh-directives readme.md refreshes the readme’s Sankey diagram and operation matrix, which are <!-- mirror-src --> blocks. The benchmark, augmentations and per-manager pages need no content regeneration: their tables and sections render live at Sphinx build time. A bundled config manager needs no pool.py import or docs/meta_package_manager.managers.md automodule: those are class-only.
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:
docs/meta_package_manager.managers.md,labels.py(group and synonyms),test_pool.py(manager count), andchangelog.md. Also regenerate the pool-derived blocks:docs/docs_update.py(keywords, labeller rules, readme footnotes, manager page stubs) andclick-extra refresh-directives readme.md(the readme’s Sankey and operation-matrix mirror-src blocks).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 them under a single
📦 manager:label by grouping them inlabels.py. Use the-basedsuffix convention for the group name (likescoop-based) to avoid colliding with the manager ID itself; the label and its rules regenerate from the group.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, logo, 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:
logo: slug of the brand mark shown atop the manager’s documentation page, naming an SVG vendored underdocs/assets/managers/. Runuv run -- python docs/logos_update.py --scan-gapsto see whether Simple Icons carries one; if it does, declare the slug and re-run the tool without the flag to vendor the file and refreshlogos.yaml. Leave it unset when there is none, which is the right outcome for roughly a quarter of the pool: the page keeps its generic package glyph, and no placeholder is invented. Managers wrapping the same upstream share one slug (brewandcaskare bothhomebrew), declared once on their virtual base when they have one. A tool with no mark of its own takes its ecosystem’s (aptunder Debian’s,cargounder Rust’s). Never hand-vendor a mark whose brand had its icons pulled from Simple Icons after a legal request: see the comments inwinget.pyandsun_tools.py.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.
Choosing the destructive-test package (PACKAGE_IDS)¶
The destructive suite runs mpm --<id> install <pkg> then mpm --<id> remove <pkg> against the real host, so PACKAGE_IDS[<id>] in tests/conftest.py must name a package that installs and uninstalls cleanly:
Tiny and fast: no dependency tree, no services/daemons, no
/etcconfig, a single self-contained binary.Not relied upon: avoid ubiquitous tools (
wget,curl,git,jq,openssl). They are usually already installed (so the install step is a no-op) and removing them can break the host or the test runner.Self-contained, ideally a Rust or Go binary.
Verified to exist in that manager’s repo/registry, with the exact ID format the manager expects (a bare name,
category/name,bucket/name,Publisher.Package, a numeric ID, …). Check the real index before committing the choice: do not guess.
Reuse the established picks for consistency instead of inventing new ones:
Ecosystem |
Package |
Notes |
|---|---|---|
Linux distros, Homebrew, FreeBSD (apt, dnf, pacman, apk, brew, …) |
|
Single-file C binary in nearly every distro, Homebrew and FreeBSD; zero reverse-deps. |
Distros lacking |
|
Fall back only where |
Source-compiling managers (emerge, FreeBSD ports) |
|
Compiles in seconds from one C file; use the |
Functional managers (Guix, Nix) |
|
The canonical GNU demo package. |
Windows binary stores (choco, scoop, sfsu, winget) and |
|
One self-contained Rust binary; use the manager’s ID format. |
npm, Yarn |
|
Zero-dependency, ~7 KB. |
pip, uv |
|
Pure-Python, zero-dependency. |
pipx, uvx |
|
Must expose a console-script entry point (a library like |
gem, cpan, composer |
|
Smallest inert zero-dependency package native to the language. |
Special cases: managers that only ship large artifacts use their lightest option (sdkman → jbang); managers with no real per-package install reference themselves (deb-get, topgrade); fwupd must never use an ID that flashes firmware on real hardware. Add a short inline comment for any non-obvious ID (numeric App Store/Steam IDs, firmware GUIDs).
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 |
|
Sankey + matrix: |
|
Regenerated by |
|
Add |
|
Two hand-maintained tables, each needing a row. The Supported managers table takes the manager’s cooldown status ( |
|
Regenerated by |
|
If the manager belongs to an ecosystem group, add it to the appropriate frozenset in |
|
Add a well-known ecosystem alias to |
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. |
Delete its |
|
Manager declares a |
Run |
|
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).test_manager_logos_resolve(intests/test_docs.py, so theValidatecommand above does not catch it): a declaredlogoslug with no vendored SVG, or a vendored mark no manager claims. Rundocs/logos_update.py.Label group collision: the group name in
labels.pycollides with a manager ID. Use the-basedsuffix (likescoop-based,pypi-based).A content rule silently disappearing:
MANAGER_CONTENT_KEYWORDSis keyed by the ID a label derives from, so folding a manager into a group (or renaming one) orphans its entry, which then generates nothing. No test catches it. Re-key it to the group and diffpyproject.tomlfor a droppedpatterns =line.Whole-suite collection abort:
tests/conftest.pyassertsPACKAGE_IDScovers exactly the class managers at import time; a missing class entry (or a stray bundled one) kills every test, not one.test_docstring_corpus: the$ ...shell-session samples in operation docstrings are checked against the real CLI construction. Write them in build order: binary,pre_args, the declared arguments with the package ID exactly where the code puts it,post_argslast (pkcon install --noninteractive hello --plain, notpkcon install hello --noninteractive --plain).test_new_package_manager_issue_template: the issue template’s platform checklist is generated fromMAIN_PLATFORMS; it goes stale when an extra-platforms release adds detections.