📦 Modern strongly typed Python library for managing system dependencies with package managers like apt, brew, pip, npm, etc.
28
stars
1,397
commits
Python
primary language
Sep 2, 2026
updated
abxpkg 📦 apt brew pip uv npm pnpm yarn bun deno cargo gem goget nix docker bash puppeteer playwright chromewebstore ansible pyinfra
Use abxpkg to detect & auto-install dependencies at runtime, serialize your dependencies to DB/config, and manage bins across many ecosystems.
This is a Python library and all-in-one CLI for managing packages locally with a variety of package managers.
It's designed for when you have to detect or install binary or source dependencies at runtime.
Stop distributing your apps via curl | sh! Instead you can bake package installation into your app, or use our uv-style abxpkg run --script shebang headers to auto-install dependencies for you.
pip install abxpkg # uv tool install abxpkg
abxpkg --version
from pathlib import Path
from tempfile import TemporaryDirectory
from abxpkg import Binary, env, npm, brew
prettier = env.load('prettier') or npm.install('prettier') or brew.install('prettier')
# or equivalent:
prettier = Binary(name='prettier', binproviders=[env, npm, brew]).install()
print(prettier.abspath, prettier.version)
# ~/.cache/abx/lib/npm/bin/prettier 2.2.1
with TemporaryDirectory() as temp_dir:
example = Path(temp_dir) / 'example.js'
example.write_text('const answer=42\n')
prettier.exec(cmd=['--write', str(example)])
# Search a provider's package index for matches:
matches = npm.search('puppeteer') # -> list[Binary] with name + install_args populated
assert isinstance(matches, list)
📦 Provides consistent interfaces for runtime dependency resolution & installation across multiple package managers & OSs ✨ Built with
pydanticv2 for strong static typing guarantees and easy conversion to/from json 🌈 Usable withdjango>= 4.0,django-ninja, and OpenAPI +django-jsonformto build UIs & APIs 🦄 Driver layer can bepyinfra/ansible/ or built-inabxpkgengine
Built by ArchiveBox to install & auto-update our extractor dependencies at runtime (chrome, wget, curl, etc.) on macOS/Linux/Docker.
Source Code: https://github.com/ArchiveBox/abxpkg/
Documentation: https://github.com/ArchiveBox/abxpkg/blob/main/README.md
from abxpkg import Binary, apt, brew, docker, env, npm, pip, playwright, pnpm, puppeteer, uv
# Provider singletons are available as simple imports — no manual instantiation needed
dependencies = [
Binary(name='curl', binproviders=[env, apt, brew]),
Binary(name='yt-dlp', binproviders=[env, pip, uv, apt, brew]),
Binary(name='playwright', binproviders=[env, npm, pnpm]),
Binary(name='chromium', binproviders=[env, playwright, puppeteer, apt]),
Binary(name='postgres', binproviders=[env, docker, apt, brew]),
]
assert dependencies[0].binproviders == [env, apt, brew]
assert dependencies[1].binproviders == [env, pip, uv, apt, brew]
[!TIP] 🔒 Stay safe from supply-chain attcaks with
abxpkg: We default to safe behavior (when providers allow):
min_release_age=7(we only install packages that have been published for 7 days or longer)postinstall_scripts=False(we don't run post-install scripts for packages by default)install_root=<platform default abx lib dir>(the CLI defaults to a dedicated provider-rooted library dir so host system stays clean)You can customize these defaults on
BinaryorBinProvider, or withABXPKG_MIN_RELEASE_AGE/ABXPKG_POSTINSTALL_SCRIPTS/ABXPKG_LIB_DIR(see Configuration below).
pip install abxpkg
abxpkg --version
Or install the isolated CLI tool:
uv tool install abxpkg
abxpkg --version
Installing abxpkg also provides an abxpkg CLI entrypoint:
abxpkg --version
abxpkg version
abxpkg list
abxpkg install yt-dlp
abxpkg load yt-dlp
abxpkg env yt-dlp
abxpkg activate yt-dlp
abxpkg update yt-dlp
abxpkg uninstall yt-dlp
Search package indexes:
abxpkg search chromium # search all providers in parallel
abxpkg --binproviders=apt,npm,brew search node # restrict to specific providers
abxpkg --version and abxpkg version stream the package version first, then a host/env summary line, then one section per selected provider showing its current resolved runtime state (INSTALLER_BINARY, PATH, ENV, install_root, bin_dir, and any active cached dependency / installed binaries).
abxpkg version <binary> is a thin alias for abxpkg load <binary>.
abxpkg list prints the full active cache for the selected providers, grouping provider installer binaries first and normal cached binaries after a blank line. You can optionally pass binary names and/or provider names positionally to filter the output:
abxpkg list
abxpkg list yt-dlp chromium
abxpkg list env puppeteer chromium
abxpkg run yt-dlp --help # resolves yt-dlp via the configured providers and execs it
abxpkg --binproviders=pip,brew run pip show black # restrict provider resolution (exercises PipProvider.exec)
abxpkg --binproviders=pip --install run yt-dlp # load first, then install via selected providers if needed
abxpkg --binproviders=pip --update run yt-dlp # ensure the binary is available, then update before exec
abxpkg --binproviders=pip --no-cache --install run yt-dlp # bypass cached/current-state checks during resolution + install
abxpkg env yt-dlp # print dotenv-style KEY=value lines for yt-dlp's runtime env
abxpkg --binproviders=pip env --install black # install if needed, then print the runtime env in .env format
eval "$(abxpkg --binproviders=pip activate black)" # emit bash export lines and apply them to the current shell
abxpkg activate --fish black | source # emit fish set -x lines and source them into fish
eval "$(abxpkg --binproviders=pip activate --zsh black)" # emit zsh export lines
abxpkg env resolves binaries the same way as run, then prints the runtime env as dotenv-style KEY=value lines. abxpkg activate emits a short usage comment plus shell-specific activation commands: bash by default, --zsh for zsh export KEY=value, and --fish for fish set -x KEY VALUE.
abxpkg options (e.g. --binproviders, --lib, --install, --update, --no-cache) must appear before the run subcommand; every argument after the binary name is forwarded verbatim to the underlying binary. The same install/update flags also apply to env / activate. run exits with the child's exit code, passes its stdout/stderr through unbuffered, and routes any abxpkg install/load logs to stderr only — no headers, no footers, no parsing.
abx: auto-install-and-run shortcutThink npx / uvx / pipx run — but for every package manager abxpkg supports. abx is a thin alias for abxpkg --install run ...: it resolves the binary via the configured providers, installs it if missing, then execs it with the forwarded arguments.
abx --binproviders=env python3 --version # run an existing host binary through the same resolution path
abx --binproviders=env python3 -c 'print("abx works")'
Options before the binary name (--lib, --binproviders, --dry-run, --debug, --no-cache, --update) are forwarded to abxpkg; everything after the binary name is forwarded to the binary itself.
Binary / per-BinProvider options as CLI flagsEvery Binary / BinProvider configuration field is exposed as a CLI flag on the group and on subcommands (install, update, uninstall, load), and is also available to run / abx via group-level flags placed before the binary name. Providers that can't enforce a given option emit a warning to stderr and continue — no hard failure.
abxpkg --min-version=1.2.3 --min-release-age=7 install yt-dlp
abxpkg --postinstall-scripts=False --binproviders=apt,uv,pip install black
abxpkg --no-cache install black
abxpkg --install-root=/tmp/yt-dlp-root --bin-dir=/tmp/yt-dlp-bin install yt-dlp
abxpkg --overrides='{"pip":{"install_args":["yt-dlp[default]"]}}' install yt-dlp
abxpkg --install-timeout=600 --version-timeout=20 --euid=1000 install yt-dlp
abxpkg --global install yt-dlp
abx --min-version=2024.1.1 --min-release-age=3 yt-dlp --help
| Flag | Type | Meaning |
|---|---|---|
--min-version=SEMVER | str | Minimum acceptable version (set on Binary.min_version). |
--postinstall-scripts[=BOOL] | bool | Allow post-install scripts. Bare --postinstall-scripts = True. Providers that can't disable them warn-and-ignore. |
--min-release-age=DAYS | float | Minimum days since publication. Non-supporting providers warn-and-ignore. |
--no-cache[=BOOL] | bool | Skip cached/current-state checks and force fresh install/update/load probes. Bare --no-cache = True. |
--overrides=JSON | dict | Per-provider Binary.overrides patches for shared provider fields (PATH, INSTALLER_BIN, install_root, bin_dir, euid, postinstall_scripts, min_release_age, dry_run, install_timeout, version_timeout) plus per-binary handler replacements (install_args, abspath, version, install, update, uninstall). |
--global[=BOOL] | bool | Thin alias for --lib=None. Bare --global = True. |
--install-root=PATH | Path | Override the per-provider install directory. |
--bin-dir=PATH | Path | Override the per-provider bin directory. |
--euid=UID | int | Pin the UID used when providers shell out. |
--install-timeout=SECONDS | int | Seconds to wait for install/update/uninstall subprocesses. |
--version-timeout=SECONDS | int | Seconds to wait for version/metadata probes. |
--dry-run[=BOOL] | bool | Show installer commands without executing them. Bare --dry-run = True. |
--debug[=BOOL] | bool | Emit DEBUG logs to stderr. Bare --debug = True. Defaults to ABXPKG_DEBUG or False. |
Every value-taking flag also accepts the literal string None / null / "" to reset to the provider's default resolution path. For postinstall_scripts / min_release_age, that means the action-specific effective default for that provider (False / 7 on supporting providers, True / 0 otherwise). The precedence is: explicit per-subcommand flag > group-level flag > environment variable > built-in default.
abxpkg install --binproviders=env,uv,pip,apt,brew prettier
# or
env ABXPKG_BINPROVIDERS=env,uv,pip,apt,brew abxpkg install yt-dlp
abxpkg --lib=./vendor/abxpkg --binproviders=env load python3
env ABXPKG_LIB_DIR=./vendor/abxpkg abxpkg --binproviders=env load python3
abxpkg install --dry-run some-dangerous-package # outputs commands that would be run without executing them
# or
env ABXPKG_DRY_RUN=1 abxpkg install some-dangerous-package
CLI result lines are written to stdout. Progress logging is written to stderr at INFO by default. Enable DEBUG logging with ABXPKG_DEBUG=1 or --debug.
⚡️ Inspired by uv's inline script metadata, abxpkg lets you declare arbitrary package dependencies at the top of any script.
It will automatically fetch, install, and make the packages available to your script across a wide variety of languages.
#!/usr/bin/env -S abxpkg run --script node
// /// script
// dependencies = [
// {name = "node", binproviders = ["env", "apt", "brew"], min_version = "22.0.0"},
// {name = "playwright", binproviders = ["env", "pnpm", "npm"], install_args = ["playwright@next"]},
// {name = "chromium", binproviders = ["env", "playwright", "puppeteer", "apt"], min_version = "131.0.0"},
// ]
// [tool.abxpkg]
// ABXPKG_POSTINSTALL_SCRIPTS = true
// ///
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();
})();
<30ms cold-start overhead once cached.
The metadata parser is comment-syntax-agnostic — it looks for /// script and /// delimiters and strips the first whitespace-delimited token from each line, so #, //, --, ;, and any other single-token comment prefix all work.
All built-in providers are available as lazy singletons — just import them by name:
from abxpkg import apt, brew, pip, npm, env
apt.install('curl')
env.load('wget')
These are instantiated on first access and cached for reuse. If you need custom configuration, you can still instantiate provider classes directly:
from pathlib import Path
from abxpkg import PipProvider
custom_pip = PipProvider(install_root=Path("/tmp/abxpkg-pip"), min_release_age=3)
Use the Binary class to declare a package that can be installed by one of several ordered providers, with an optional version floor:
from abxpkg import Binary, SemVer, env, brew
curl = Binary(
name="curl",
min_version=SemVer("7.0.0"),
binproviders=[env, brew],
).install()
min_version is enforced after a provider resolves or installs a binary — provider discovery can still succeed, but the final Binary is rejected if the loaded version is below the floor. Use min_version=None to disable the check.
Pass no_cache=True to load() / install() / update() / uninstall() when you want to bypass cached/current-state checks. For install(), no_cache=True skips the initial load() check and forces a fresh install path. The equivalent CLI and env controls are --no-cache and ABXPKG_NO_CACHE=1.
Provider installer binaries also resolve lazily through the active provider chain. If a provider needs pip, npm, cargo, or another installer tool and it is missing, abxpkg will auto-install that dependency using the currently selected providers and the same ABXPKG_LIB_DIR / --lib / --global settings.
Binary subclass with per-provider overridesfrom pydantic import InstanceOf
from abxpkg import BinProvider, Binary, BinProviderName, BinName, HandlerDict, BrewProvider
from abxpkg import env, pip, apt
class CustomBrewProvider(BrewProvider):
name: BinProviderName = 'custom_brew'
def get_macos_packages(self, bin_name: str, **context) -> list[str]:
return ['yt-dlp'] if bin_name == 'ytdlp' else [bin_name]
class YtdlpBinary(Binary):
name: BinName = 'ytdlp'
description: str = 'YT-DLP (Replacement for YouTube-DL) Media Downloader'
# define the providers this binary supports
binproviders: list[InstanceOf[BinProvider]] = [env, pip, apt, CustomBrewProvider()]
# customize installed package names for specific package managers
overrides: dict[BinProviderName, HandlerDict] = {
'pip': {'install_args': ['yt-dlp[default,curl-cffi]']}, # literal values
'apt': {'install_args': lambda: ['yt-dlp', 'ffmpeg']}, # any pure Callable
'custom_brew': {'install_args': 'self.get_macos_packages'}, # or a string ref to a method on self
}
ytdlp = YtdlpBinary()
assert [provider.name for provider in ytdlp.binproviders] == ['env', 'pip', 'apt', 'custom_brew']
assert ytdlp.overrides['pip']['install_args'] == ['yt-dlp[default,curl-cffi]']
Binary objects as a stable typed interface to interact with installed packagesfrom abxpkg import Binary, env
# Use providers directly for host binary discovery
python = env.load('python3')
assert python and python.is_valid
print(env.PATH, env.get_abspaths('python3'), env.get_version('python3'))
# our Binary API provides a nice type-checkable, validated, serializable handle
python = Binary(name='python3', binproviders=[env]).load()
print(python) # Binary(name='python3', abspath=Path(...), version=SemVer(...), sha256='...', mtime=1712890123456789000)
print(python.abspaths) # show all matching binaries found via each provider PATH
print(python.model_dump(mode='json')) # JSON-ready dict
print(python.model_json_schema()) # ... OpenAPI-ready JSON schema showing all available fields
from pydantic import InstanceOf
from abxpkg import Binary, BinProvider, BrewProvider, EnvProvider
# You can also instantiate provider classes manually for custom configuration,
# or define binaries as classes for type checking
class CurlBinary(Binary):
name: str = 'curl'
binproviders: list[InstanceOf[BinProvider]] = [EnvProvider(), BrewProvider()]
curl = CurlBinary().install()
assert isinstance(curl, CurlBinary) # CurlBinary is a unique type you can use in annotations now
print(curl.abspath, curl.version, curl.binprovider, curl.is_valid) # Path(...) SemVer(...) BrewProvider()/EnvProvider() True
curl.exec(cmd=['--version']) # curl 8.4.0 (x86_64-apple-darwin23.0) libcurl/8.4.0 ...
import platform
import shutil
from pydantic import InstanceOf
from abxpkg import BinProvider, Binary, BinProviderName, BinName, HandlerDict
from abxpkg import env, apt
class DockerBinary(Binary):
name: BinName = 'docker'
binproviders: list[InstanceOf[BinProvider]] = [env, apt]
overrides: dict[BinProviderName, HandlerDict] = {
'env': {
# prefer podman if installed, fall back to docker
'abspath': lambda: shutil.which('podman') or shutil.which('docker') or shutil.which('docker-ce'),
},
'apt': {
# vary the installed package name based on CPU architecture
'install_args': {
'x86_64': ['docker.io'],
'armv7l': ['docker.io'],
'aarch64': ['docker.io'],
}.get(platform.machine(), ['docker.io']),
},
}
docker = DockerBinary().install()
BinProvider to add support for a new package managerfrom pathlib import Path
from abxpkg import (
BinProvider,
BinProviderName,
BinName,
HostBinPath,
InstallArgs,
SemVer,
bin_abspath,
)
class CargoProvider(BinProvider):
name: BinProviderName = 'cargo'
INSTALLER_BIN: BinName = 'cargo'
PATH: str = str(Path.home() / '.cargo/bin')
def default_install_args_handler(self, bin_name: BinName, **context) -> InstallArgs:
return [bin_name]
def default_install_handler(
self,
bin_name: BinName,
install_args: InstallArgs | None = None,
postinstall_scripts: bool | None = None,
min_release_age: float | None = None,
min_version: SemVer | None = None,
timeout: int | None = None,
) -> str:
install_args = install_args or self.get_install_args(bin_name)
installer = self.INSTALLER_BINARY()
assert installer and installer.loaded_abspath
proc = self.exec(bin_name=installer.loaded_abspath, cmd=['install', *install_args], timeout=timeout)
if proc.returncode != 0:
self._raise_proc_error('install', install_args, proc)
return proc.stdout.strip() or proc.stderr.strip()
def default_abspath_handler(self, bin_name: BinName, **context) -> HostBinPath | None:
return bin_abspath(bin_name, PATH=self.PATH)
def default_version_handler(
self,
bin_name: BinName,
abspath: HostBinPath | None = None,
timeout: int | None = None,
**context,
) -> SemVer | None:
return self._version_from_exec(bin_name, abspath=abspath, timeout=timeout)
cargo = CargoProvider()
assert cargo.get_install_args('ripgrep') == ('ripgrep',)
assert cargo.default_install_args_handler('ripgrep') == ['ripgrep']
logging to customize the stderr/stdout loggingabxpkg uses the standard Python logging module. By default it stays quiet unless your application configures logging explicitly.
import logging
from abxpkg import Binary, env, configure_logging
configure_logging(logging.INFO)
python = Binary(name='python', binproviders=[env]).load()
To enable Rich logging:
pip install "abxpkg[rich]"
import logging
from abxpkg import Binary, EnvProvider, configure_rich_logging
configure_rich_logging(logging.DEBUG)
python = Binary(name='python', binproviders=[EnvProvider()]).load()
Debug logging is hardened so logging itself does not become the failure. If a provider/model object has a broken or overly-expensive repr(), abxpkg falls back to a short ClassName(...) summary instead of raising while formatting log output.
configure_rich_logging(...) uses rich.logging.RichHandler under the hood, so log levels, paths, arguments, and command lines render with terminal colors when supported.
You can also manage it with standard logging primitives:
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("abxpkg").setLevel(logging.DEBUG)
BinProvider / Binary in DB models and render them in the AdminWith a few more packages, you get type-checked Django fields & forms that support BinProvider and Binary.
[!TIP] For the full Django experience, we recommend installing these 3 excellent packages:
django-admin-data-viewsdjango-pydantic-fielddjango-jsonformpip install abxpkg django-admin-data-views django-pydantic-field django-jsonform
Django model fields:
from django.conf import settings
if not settings.configured:
settings.configure(
INSTALLED_APPS=[
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
],
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:'}},
SECRET_KEY='abxpkg-docs',
)
import django
django.setup()
from django.db import models
from django.db import connection
from abxpkg import Binary, EnvProvider, SemVer
from django_pydantic_field import SchemaField
provider_runtime_fields = {name: True for name in EnvProvider.model_computed_fields}
binary_runtime_fields = {name: True for name in Binary.model_computed_fields}
binary_runtime_fields['binproviders'] = True
binary_runtime_fields['loaded_binprovider'] = True
class Dependency(models.Model):
label = models.CharField(max_length=63)
default_binprovider: EnvProvider = SchemaField(exclude=provider_runtime_fields)
binaries: list[Binary] = SchemaField(
default=[],
exclude={'__all__': binary_runtime_fields},
)
min_version: SemVer = SchemaField(default=(0, 0, 1))
class Meta:
app_label = 'abxpkg_docs'
with connection.schema_editor() as schema_editor:
schema_editor.create_model(Dependency)
Saving a Binary using the model:
from abxpkg import Binary, env
python = Binary(name='python3', binproviders=[env]).load()
obj = Dependency(
label='runtime tools',
default_binprovider=env, # store BinProvider values directly
binaries=[python], # store Binary/SemVer values directly
)
obj.save()
When fetching back from the DB, Binary fields are auto-deserialized and immediately usable:
obj = Dependency.objects.get(label='runtime tools')
assert obj.binaries[0].abspath == python.abspath
obj.binaries[0].exec(cmd=['--version'])
For a full example see the bundled django_example_project/.
Django Admin integration:
# settings.py
INSTALLED_APPS = [
# ...
'admin_data_views',
'abxpkg',
]
ABXPKG_GET_ALL_BINARIES = 'project.views.get_all_binaries'
ABXPKG_GET_BINARY = 'project.views.get_binary'
ADMIN_DATA_VIEWS = {
"NAME": "Environment",
"URLS": [
{
"route": "binaries/",
"view": "abxpkg.views.binaries_list_view",
"name": "binaries",
"items": {
"route": "<str:key>/",
"view": "abxpkg.views.binary_detail_view",
"name": "binary",
},
},
],
}
If you override the default site admin, register the views manually:
from django.conf import settings
if not settings.configured:
settings.configure(INSTALLED_APPS=[])
import django
django.setup()
from django.contrib.admin import AdminSite
from abxpkg.admin import register_admin_views
custom_admin = AdminSite(name='custom')
register_admin_views(custom_admin)
All abxpkg env vars are read once at import time and only apply when set. Explicit constructor kwargs always override these defaults.
Behavioral controls (apply across all providers):
| Variable | Default | Effect |
|---|---|---|
ABXPKG_DRY_RUN / DRY_RUN | 0 | Flips the shared dry_run default. ABXPKG_DRY_RUN wins if both are set. Provider subprocesses are logged and skipped, install() / update() return a placeholder, uninstall() returns True. |
ABXPKG_NO_CACHE | 0 | Flips the shared no_cache default. When enabled, install() skips the initial load() check and forces a fresh install path, while load() / update() / uninstall() bypass cached probe results. |
ABXPKG_DEBUG | 0 | Enables DEBUG-level CLI logging on stderr for abxpkg / abx. The matching CLI flag is --debug. Default CLI logging level is INFO. |
ABXPKG_INSTALL_TIMEOUT | 120 | Seconds to wait for install() / update() / uninstall() handler subprocesses. |
ABXPKG_VERSION_TIMEOUT | 10 | Seconds to wait for version / metadata probes (--version, npm show, pip show, etc.). |
ABXPKG_POSTINSTALL_SCRIPTS | unset | Hydrates the provider-level default for the postinstall_scripts kwarg on every provider that supports it (pip, uv, npm, pnpm, yarn, bun, deno, brew, chromewebstore, puppeteer). When left unset, action execution resolves to the provider/action default (False on supporting providers, True otherwise). |
ABXPKG_MIN_RELEASE_AGE | 7 | Hydrates the provider-level default (in days) for the min_release_age kwarg on every provider that supports it (pip, uv, npm, pnpm, yarn, bun, deno). When left unset, action execution resolves to the provider/action default (7 on supporting providers, 0 otherwise). |
ABXPKG_BINPROVIDERS | shared default order | Comma-separated list of provider names to enable (and their order) for the abxpkg CLI. By default this uses DEFAULT_PROVIDER_NAMES from abxpkg.__init__ (which excludes ansible / pyinfra, and also excludes apt on macOS). |
Install-root controls (one global default + one per-provider override):
| Variable | Applies to | Effect |
|---|---|---|
ABXPKG_LIB_DIR | providers whose default install_root is abxpkg-managed | Centralized library root. When set, each matching provider points its default install_root at $ABXPKG_LIB_DIR/<provider name> (e.g. <lib>/env, <lib>/npm, <lib>/pip, <lib>/gem, <lib>/playwright). Accepts relative (./lib), tilde (~/.config/abx/lib), and absolute (/tmp/abxlib) paths. --global is a thin alias for --lib=None, which clears this root for the current CLI invocation. |
ABXPKG_<BINPROVIDER>_ROOT | the matching provider's install_root | Generic per-provider override; beats ABXPKG_LIB_DIR/<provider name>. Examples: ABXPKG_PIP_ROOT, ABXPKG_UV_ROOT, ABXPKG_NPM_ROOT, ABXPKG_GOGET_ROOT, ABXPKG_CHROMEWEBSTORE_ROOT. The <BINPROVIDER> token is the provider name uppercased. |
Install-root precedence (most specific wins): explicit install_root= / provider alias kwarg > ABXPKG_<NAME>_ROOT > ABXPKG_LIB_DIR/<name> > provider-specific built-in default / native global mode.
Provider-specific binary overrides:
Each provider also honors a <NAME>_BINARY=/abs/path/to/<name> env var to pin the exact executable it shells out to — PIP_BINARY, UV_BINARY, NPM_BINARY, PNPM_BINARY, YARN_BINARY, BUN_BINARY, DENO_BINARY, etc.
Per-Binary / per-BinProvider fields (constructor kwargs, most-specific wins):
min_version can be set on any individual Binary.min_release_age can be set on Binary or BinProvider, or via ABXPKG_MIN_RELEASE_AGE (days).postinstall_scripts can be set on Binary or BinProvider, or via ABXPKG_POSTINSTALL_SCRIPTS.no_cache can be passed per-call to load() / install() / update() / uninstall(), or enabled globally for the CLI via ABXPKG_NO_CACHE.install_root / bin_dir can be set on any BinProvider with an isolated install location, or default to ABXPKG_<NAME>_ROOT / ABXPKG_LIB_DIR/<provider name> / the provider's own built-in default.dry_run can be set on BinProvider or passed per-call to install() / update() / uninstall(), or via ABXPKG_DRY_RUN / DRY_RUN.install_timeout can be set on BinProvider or via ABXPKG_INSTALL_TIMEOUT (seconds).version_timeout can be set on BinProvider or via ABXPKG_VERSION_TIMEOUT (seconds).euid can be set on BinProvider to pin the UID used to sudo/drop into when running provider subprocesses; otherwise it's auto-detected from install_root ownership.overrides is a dict[BinProviderName, HandlerDict] (on Binary) or dict[BinName, HandlerDict] (on BinProvider) mapping to per-provider field patches and per-binary handler replacements. Supported keys are PATH, INSTALLER_BIN, euid, install_root, bin_dir, dry_run, postinstall_scripts, min_release_age, install_timeout, version_timeout, install_args / packages, abspath, version, install, update, and uninstall. See Advanced Usage for examples.Precedence is always: explicit action kwarg > Binary(...) field > BinProvider(...) field > env var > built-in default.
BinProviderBuilt-in implementations: EnvProvider, AptProvider, BrewProvider, PipProvider, UvProvider, NpmProvider, PnpmProvider, YarnProvider, BunProvider, DenoProvider, CargoProvider, GemProvider, GoGetProvider, NixProvider, DockerProvider, PyinfraProvider, AnsibleProvider, BashProvider, ChromeWebstoreProvider, PuppeteerProvider, PlaywrightProvider
This type represents a provider of binaries, e.g. a package manager like apt / pip / npm, or env (which only resolves binaries already present in $PATH).
Every provider exposes the same lifecycle surface:
load() / install() / update() / uninstall()search() to discover installable package matches from a provider indexget_install_args() to resolve package names / formulae / image refs / module specsget_abspath() / get_abspaths() / get_version() / get_sha256() / get_docs_url()Shared base defaults come from abxpkg/binprovider.py and apply unless a concrete provider overrides them:
import sys
from pathlib import Path
INSTALLER_BIN = "env" # base-class placeholder; real providers override this
PATH = str(Path(sys.executable).parent)
postinstall_scripts = None # some providers override this with ABXPKG_POSTINSTALL_SCRIPTS
min_release_age = None # some providers override this with ABXPKG_MIN_RELEASE_AGE
install_timeout = 120 # or ABXPKG_INSTALL_TIMEOUT=120
version_timeout = 10 # or ABXPKG_VERSION_TIMEOUT=10
dry_run = False # or ABXPKG_DRY_RUN=1 / DRY_RUN=1
dry_run: use provider.get_provider_with_overrides(dry_run=True), pass dry_run=True directly to install() / update() / uninstall(), or set ABXPKG_DRY_RUN=1 / DRY_RUN=1. If both env vars are set, ABXPKG_DRY_RUN wins. Provider subprocesses are logged and skipped, install() / update() return a placeholder loaded binary, and uninstall() returns True without mutating the host.no_cache: use --no-cache / ABXPKG_NO_CACHE=1 on the CLI, or pass no_cache=True directly to load() / install() / update() / uninstall(). For install(), this skips the initial load() check and forces a fresh install path.install_timeout: shared provider-level timeout used by install(), update(), and uninstall() handler execution paths. Can also be set with ABXPKG_INSTALL_TIMEOUT.version_timeout: shared provider-level timeout used by version / metadata probes such as --version, npm show, npm list, pip show, go version -m, and brew lookups. Can also be set with ABXPKG_VERSION_TIMEOUT.postinstall_scripts and min_release_age are standard provider/binary/action kwargs. Supporting providers hydrate defaults from ABXPKG_POSTINSTALL_SCRIPTS and ABXPKG_MIN_RELEASE_AGE; when those remain unset/None, install/update/uninstall resolve them to effective action defaults (False / 7 on supporting providers, True / 0 otherwise).None. If you pass an explicit unsupported value during install() / update(), it is logged as a warning and ignored.INSTALLER_BINARY() overrides call the base resolver when the installer is an externally supplied tool that can be found through the selected upstream providers (pip, npm, cargo, gem, etc.). Providers whose installer CLI is produced by their own setup path, such as Puppeteer and Playwright, first resolve an already-bootstrapped CLI from the shared ABXPKG_LIB_DIR npm space, a custom provider-local npm prefix, or ambient PATH, then raise unavailable so setup can bootstrap it through the intended provider when missing.Binary(...) defaults > provider defaults.For the full list of env vars that hydrate these defaults, see Configuration above.
Supported override keys are the same everywhere:
from pathlib import Path
from abxpkg import PipProvider
provider = PipProvider(install_root=Path("/tmp/venv")).get_provider_with_overrides(
overrides={
"black": {
"install_args": ["black==24.4.2"],
"version": "self.default_version_handler",
"abspath": "self.default_abspath_handler",
"docs_url": "self.default_docs_url_handler",
"search": "self.default_search_handler",
},
},
dry_run=True,
version_timeout=30,
)
install_args / packages: package-manager arguments for that provider. packages is the legacy alias.abspath, version, install, update, uninstall, docs_url, search: literal values, callables, or "self.method_name" references that replace the provider handler for a specific binary.PATH, INSTALLER_BIN, euid, install_root, bin_dir, dry_run, postinstall_scripts, min_release_age, install_timeout, version_timeout: shared provider field patches applied to the copied provider instance before handler resolution.Providers with isolated install locations also expose a shared constructor surface:
install_root: shared provider root for package state, metadata, caches, venvs, project dirs, profiles, or downloaded assets, depending on the provider.bin_dir: shared executable output dir when a provider separates package state from runnable binaries.provider.install_root / provider.bin_dir: normalized computed properties you can inspect after construction, regardless of which provider-specific args were used.install_root / bin_dir at construction time instead of silently ignoring them.PATH entries.BinProvidersEnvProvider (env)Source: abxpkg/binprovider.py • Tests: tests/test_envprovider.py
from abxpkg.binprovider import DEFAULT_ENV_PATH
INSTALLER_BIN = "which"
PATH = DEFAULT_ENV_PATH # current PATH + current Python bin dir
ABXPKG_ENV_ROOT, or ABXPKG_LIB_DIR/env, or the platform default abx lib dir under env/. env is still read-only: it only resolves binaries that already exist on the host PATH, but when an install root is configured it also keeps a managed bin/ symlink dir and derived.env cache there.min_release_age and postinstall_scripts are unsupported here and are ignored with a warning if explicitly passed to install() / update().abspath / version are the useful ones here. python has a built-in override to the current sys.executable and interpreter version.env/bin/<name> and resolved through that stable symlink. install() / update() return explanatory no-op messages, and uninstall() is a no-op.AptProvider (apt)Source: abxpkg/binprovider_apt.py • Tests: tests/test_aptprovider.py
INSTALLER_BIN = "apt-get"
PATH = "" # populated from `dpkg -L bash` bin/sbin roots
euid = 0 # always runs as root
apt-get directly.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args becomes apt-get install -y -qq --no-install-recommends ...; update() uses apt-get install --only-upgrade ...; uninstall() uses apt-get remove -y -qq ....dpkg-query metadata.BrewProvider (brew)Source: abxpkg/binprovider_brew.py • Tests: tests/test_brewprovider.py
INSTALLER_BIN = "brew"
PATH = "/home/linuxbrew/.linuxbrew/bin:/opt/homebrew/bin:/usr/local/bin"
brew_prefix = "/opt/homebrew" # guessed host prefix: /opt/homebrew, /usr/local, or linuxbrew
brew_prefix is the Homebrew prefix used for discovery and shelling out to brew. By default it resolves from ABXPKG_BREW_ROOT, or ABXPKG_LIB_DIR/brew, or a guessed host prefix (/opt/homebrew, /usr/local, or linuxbrew). bin_dir is used for linked formula binaries when abxpkg manages them separately.brew directly.dry_run: shared behavior.min_release_age is unsupported and is ignored with a warning if explicitly requested. postinstall_scripts=False is supported on brew install via --skip-post-install, and ABXPKG_POSTINSTALL_SCRIPTS hydrates the provider default here. Homebrew has no equivalent flag for brew upgrade, so updates run without it.install_args maps to formula / cask args passed to brew install, brew upgrade, and brew uninstall.brew update at most once per day. Explicit --skip-post-install args in install_args win over derived defaults for installs.PipProvider (pip)Source: abxpkg/binprovider_pip.py • Tests: tests/test_pipprovider.py, tests/test_security_controls.py
INSTALLER_BIN = "pip"
PATH = "" # auto-built from global/user Python bin dirs
install_root = None # None = ambient/global mode, Path(...) = provider root
install_root=None uses the system/user Python environment. Set install_root=Path(...) for a hermetic provider root whose actual virtualenv lives at <install_root>/venv, with executables under <install_root>/venv/bin and provider metadata like derived.env kept at <install_root>.pip directly. Honors PIP_BINARY=/abs/path/to/pip. Use UvProvider for uv-backed installs.dry_run: shared behavior.postinstall_scripts=False (always) and min_release_age (on pip >= 26.0 or in a freshly bootstrapped pip venv). Hydrated from ABXPKG_POSTINSTALL_SCRIPTS and ABXPKG_MIN_RELEASE_AGE. For stricter enforcement on hosts with older system pip, use UvProvider instead.install_args is passed as pip requirement specs; unpinned specs get a >=min_version floor when min_version is supplied.postinstall_scripts=False adds pip --only-binary :all: (wheels only, no arbitrary sdist build scripts). min_release_age is enforced with pip --uploaded-prior-to=<ISO8601> on pip >= 26.0 (see pypa/pip#13625); older pip silently skips the flag. Explicit conflicting flags already present in install_args win over the derived defaults. get_version / get_abspath fall back to parsing pip show <package> output when the console script can't report its own version.UvProvider (uv)Source: abxpkg/binprovider_uv.py • Tests: tests/test_uvprovider.py
INSTALLER_BIN = "uv"
PATH = "" # prepends <install_root>/venv/bin or the uv tool bin dir
install_root = None # None = global uv tool mode, Path(...) = provider root
install_root is set.
install_root=Path(...)): treats install_root as a provider root, creates the real venv at <install_root>/venv via uv venv, and installs packages into it with uv pip install --python <install_root>/venv/bin/python .... Binaries land in <install_root>/venv/bin/<name>, while provider metadata like derived.env stays at <install_root>. This matches PipProvider's layout.install_root=None): delegates to uv tool install which creates a fresh venv per tool under UV_TOOL_DIR (default ~/.local/share/uv/tools) and writes shims into UV_TOOL_BIN_DIR (default ~/.local/bin). Pass bin_dir=Path(...) to override the shim dir. This is the idiomatic "install a CLI tool globally" path.UV_BINARY=/abs/path/to/uv. If uv isn't on the host, the provider is unavailable.dry_run: shared behavior.min_release_age and postinstall_scripts=False, and hydrates their provider defaults from ABXPKG_MIN_RELEASE_AGE and ABXPKG_POSTINSTALL_SCRIPTS. In both modes, postinstall_scripts=False becomes --no-build (wheels-only, no arbitrary sdist build scripts) and min_release_age becomes --exclude-newer=<ISO8601> (uv 0.4+). Explicit conflicting flags already present in install_args win over the derived defaults.install_args is passed as requirement specs; unpinned specs get a >=min_version floor when min_version is supplied.uv pip install --upgrade; update in global mode is uv tool install --force (re-installs the tool's venv). Uninstall in venv mode uses uv pip uninstall --python <venv>/bin/python; in global mode it uses uv tool uninstall <name>.NpmProvider (npm)Source: abxpkg/binprovider_npm.py • Tests: tests/test_npmprovider.py, tests/test_security_controls.py
INSTALLER_BIN = "npm"
PATH = "" # auto-built from npm local + global bin dirs
install_root = None # None = global install, Path(...) = prefix/project root
install_root=None installs globally (walks up from the host's npm prefix / npm prefix -g to seed PATH). Set install_root=Path(...) to install under <prefix>/node_modules/.bin; that prefix bin dir becomes the provider's active executable search path.npm directly and expects npm to be installed on the host. Honors NPM_BINARY=/abs/path/to/npm. Use PnpmProvider for pnpm.dry_run: shared behavior.postinstall_scripts=False and min_release_age, hydrated from ABXPKG_POSTINSTALL_SCRIPTS and ABXPKG_MIN_RELEASE_AGE. min_release_age requires an npm build that ships --min-release-age (detected once by probing npm install --help).install_args is passed as npm package specs; unpinned specs get rewritten to pkg@>=<min_version> when min_version is supplied.postinstall_scripts=False adds --ignore-scripts; min_release_age adds --min-release-age=<days>; and installs always include npm's standard non-interactive flags (--force --no-audit --no-fund --loglevel=error). puppeteer is special-cased to install both puppeteer and @puppeteer/browsers, and puppeteer-browsers resolves to @puppeteer/browsers. Explicit conflicting flags already present in install_args win over the derived defaults. get_version / get_abspath fall back to parsing npm show --json <package> and npm list --json --depth=0 output when the console script can't report its own version.PnpmProvider (pnpm)Source: abxpkg/binprovider_pnpm.py • Tests: tests/test_pnpmprovider.py
INSTALLER_BIN = "pnpm"
PATH = "" # auto-built from pnpm local + global bin dirs
install_root = None # None = global install, Path(...) = prefix/project root
install_root=None installs globally. Set install_root=Path(...) to install under <prefix>/node_modules/.bin; that prefix bin dir becomes the provider's active executable search path.pnpm directly. Honors PNPM_BINARY=/abs/path/to/pnpm. Use NpmProvider for npm.dry_run: shared behavior.min_release_age and postinstall_scripts=False, and hydrates their provider defaults from ABXPKG_MIN_RELEASE_AGE and ABXPKG_POSTINSTALL_SCRIPTS. min_release_age requires pnpm 10.16+, and supports_min_release_age() returns False on older hosts (then it logs a warning and continues).install_args is passed as pnpm package specs; unpinned specs get rewritten to pkg@>=<min_version> when min_version is supplied.--min-release-age CLI flag; this provider passes --config.minimumReleaseAge=<minutes> (the camelCase / kebab-case form pnpm exposes via its --config.<key>=<value> override). Installs always include --loglevel=error, and PNPM_HOME is auto-populated so pnpm add -g works without polluting the user's shell config. puppeteer is special-cased to install both puppeteer and @puppeteer/browsers, and puppeteer-browsers resolves to @puppeteer/browsers.YarnProvider (yarn)Source: abxpkg/binprovider_yarn.py • Tests: tests/test_yarnprovider.py
INSTALLER_BIN = "yarn"
PATH = "" # prepends <install_root>/node_modules/.bin
install_root = None # project dir, defaults to ABXPKG_YARN_ROOT or ABXPKG_LIB_DIR/yarn
install_root=Path(...) for an isolated project dir; that directory is auto-initialized with a stub package.json and .yarnrc.yml (nodeLinker: node-modules so binaries land in <install_root>/node_modules/.bin). When unset, the provider relies on $ABXPKG_YARN_ROOT or $ABXPKG_LIB_DIR/yarn; if neither is configured, the provider is unavailable.YARN_BINARY=/abs/path/to/yarn. Both Yarn classic (1.x) and Yarn Berry (2+) work for basic install/update/uninstall, but only Yarn 4.10+ supports the security flags.dry_run: shared behavior.min_release_age and postinstall_scripts=False, and hydrates their provider defaults from ABXPKG_MIN_RELEASE_AGE and ABXPKG_POSTINSTALL_SCRIPTS. Both controls require Yarn 4.10+; on older hosts supports_min_release_age() / supports_postinstall_disable() return False and explicit values are logged-and-ignored.install_args is passed as Yarn package specs; unpinned specs get rewritten to pkg@>=<min_version> when min_version is supplied.--ignore-scripts / --minimum-release-age CLI flags; the provider writes npmMinimalAgeGate: 7d (or whatever days value is configured) and enableScripts: false into <install_root>/.yarnrc.yml and additionally passes --mode skip-build to yarn add / yarn up when postinstall_scripts=False. Updates use yarn up <pkg> (Berry) or yarn upgrade <pkg> (classic). YARN_GLOBAL_FOLDER and YARN_CACHE_FOLDER are pointed at the provider cache dir so installs share a single cache across workspaces. puppeteer is special-cased to install both puppeteer and @puppeteer/browsers, and puppeteer-browsers resolves to @puppeteer/browsers.BunProvider (bun)Source: abxpkg/binprovider_bun.py • Tests: tests/test_bunprovider.py
INSTALLER_BIN = "bun"
PATH = "" # prepends <install_root>/bin
install_root = None # mirrors $BUN_INSTALL, None = ~/.bun (host-default)
install_root=None writes into the host $BUN_INSTALL (default ~/.bun). Set install_root=Path(...) to install under <install_root>/bin; the provider also creates <install_root>/install/global for the global node_modules dir, which is where bun puts the actual package state. The bin dir becomes the provider's active executable search path.BUN_BINARY=/abs/path/to/bun.dry_run: shared behavior.min_release_age and postinstall_scripts=False, and hydrates their provider defaults from ABXPKG_MIN_RELEASE_AGE and ABXPKG_POSTINSTALL_SCRIPTS. min_release_age requires Bun 1.3+, and supports_min_release_age() returns False on older hosts.install_args is passed as Bun package specs; unpinned specs get rewritten to pkg@>=<min_version> when min_version is supplied.bun add -g (with --force as the update fallback). The provider passes --ignore-scripts for postinstall_scripts=False and --minimum-release-age=<seconds> (Bun's unit is seconds; this provider converts from days). puppeteer is special-cased to install both puppeteer and @puppeteer/browsers, and puppeteer-browsers resolves to @puppeteer/browsers. Explicit conflicting flags already present in install_args win over the derived defaults.DenoProvider (deno)Source: abxpkg/binprovider_deno.py • Tests: tests/test_denoprovider.py
INSTALLER_BIN = "deno"
PATH = "" # prepends <install_root>/bin
install_root = None # mirrors $DENO_INSTALL_ROOT, None = ~/.deno
install_root=None writes into the host $DENO_INSTALL_ROOT (default ~/.deno). Set install_root=Path(...) for a hermetic root with executables under <install_root>/bin; DENO_DIR is then derived as <install_root>/.cache.DENO_BINARY=/abs/path/to/deno.dry_run: shared behavior.min_release_age and postinstall_scripts=False / True, and hydrates their provider defaults from ABXPKG_MIN_RELEASE_AGE and ABXPKG_POSTINSTALL_SCRIPTS. min_release_age requires Deno 2.5+, and supports_min_release_age() returns False on older hosts.install_args is passed as deno install package specs and is auto-prefixed with npm: when an unqualified bare name is supplied. Already-qualified specs (npm:, jsr:, https://...) are passed through verbatim. Unpinned specs get rewritten to pkg@>=<min_version> when min_version is supplied.deno install -g --force --allow-all -n <bin_name> <pkg> because Deno's idiomatic update path is just a fresh global install. Deno's npm lifecycle scripts are opt-in (the opposite of npm), so the provider only adds --allow-scripts when postinstall_scripts=True. min_release_age is passed as --minimum-dependency-age=<minutes> (Deno's preferred unit; this provider converts from days). puppeteer is special-cased to install both puppeteer and @puppeteer/browsers, and puppeteer-browsers resolves to @puppeteer/browsers. DENO_TLS_CA_STORE=system is set so installs work on hosts with corporate / sandboxed CA bundles.BashProvider (bash)Source: abxpkg/binprovider_bash.py • Tests: tests/test_bashprovider.py
import os
from pathlib import Path
INSTALLER_BIN = "bash"
PATH = ""
lib_dir = Path(os.environ.get("ABXPKG_LIB_DIR", "~/.config/abx/lib")).expanduser()
install_root = Path(os.environ.get("ABXPKG_BASH_ROOT", lib_dir / "bash"))
bin_dir = install_root / "bin"
install_root for the state dir, and bin_dir for the executable output dir.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install, update, and uninstall.INSTALL_ROOT, BIN_DIR, BASH_INSTALL_ROOT, and BASH_BIN_DIR into the shell environment for those commands.CargoProvider (cargo)Source: abxpkg/binprovider_cargo.py • Tests: tests/test_cargoprovider.py
INSTALLER_BIN = "cargo"
PATH = "" # prepends cargo_root/bin and cargo_home/bin
cargo_root = None # set this for hermetic installs
install_root=Path(...) or cargo_root=Path(...) for isolated installs under <cargo_root>/bin; otherwise installs go through cargo_home.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args is passed to cargo install; min_version becomes cargo install --version >=....CARGO_HOME, CARGO_TARGET_DIR, and CARGO_INSTALL_ROOT when applicable.GemProvider (gem)Source: abxpkg/binprovider_gem.py • Tests: tests/test_gemprovider.py
from abxpkg.binprovider import DEFAULT_ENV_PATH
INSTALLER_BIN = "gem"
PATH = DEFAULT_ENV_PATH
install_root = None # defaults to $GEM_HOME or ~/.local/share/gem
bin_dir = None # defaults to <install_root>/bin
install_root, and optionally bin_dir, for hermetic installs; otherwise it uses $GEM_HOME or ~/.local/share/gem.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args maps to gem install ..., gem update ..., and gem uninstall ...; min_version becomes --version >=....GEM_HOME instead of the host default.GoGetProvider (goget)Source: abxpkg/binprovider_goget.py • Tests: tests/test_gogetprovider.py
from abxpkg.binprovider import DEFAULT_ENV_PATH
INSTALLER_BIN = "go"
PATH = DEFAULT_ENV_PATH
install_root = None # defaults to $GOPATH or ~/go
bin_dir = None # defaults to <install_root>/bin
install_root for the Go install tree, and optionally bin_dir for the executable dir; otherwise installs land in <install_root>/bin.go first, then installs Go through Apt or Homebrew if it is missing.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args is passed to go install ...; the default is ["<bin_name>@latest"].update() is just install() again. Version detection prefers go version -m <binary> and falls back to the generic version probe. The provider name is goget, not go_get.NixProvider (nix)Source: abxpkg/binprovider_nix.py • Tests: tests/test_nixprovider.py
import os
from pathlib import Path
INSTALLER_BIN = "nix"
PATH = "" # prepends <install_root>/bin
install_root = Path(os.environ.get("ABXPKG_NIX_PROFILE", "~/.nix-profile")).expanduser()
install_root=Path(...) for a custom profile.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args is passed to nix profile install ...; the default is [bin_name]. Search results use the explicit official nixpkgs-unstable channel archive instead of the host's Nix registry.DockerProvider (docker)Source: abxpkg/binprovider_docker.py • Tests: tests/test_dockerprovider.py
import os
from pathlib import Path
INSTALLER_BIN = "docker"
PATH = "" # prepends bin_dir
lib_dir = Path(os.environ.get("ABXPKG_LIB_DIR", "~/.config/abx/lib")).expanduser()
docker_root = Path(os.environ.get("ABXPKG_DOCKER_ROOT", lib_dir / "docker"))
bin_dir = docker_root / "bin"
install_root=Path(...) for the shim/metadata root or bin_dir=Path(...) for the shim dir directly.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args is a list of Docker image refs. The first item is treated as the main image and becomes the generated shim target.["<bin_name>:latest"]. install() / update() run docker pull, write metadata JSON, and create an executable wrapper that runs docker run .... Expects image refs as install args, typically via overrides on a Binary. It writes a local wrapper script for the binary and executes it via docker run ...; the binary version is parsed from the image tag, so semver-like tags work best.ChromeWebstoreProvider (chromewebstore)Source: abxpkg/binprovider_chromewebstore.py • Tests: tests/test_chromewebstoreprovider.py
import os
from pathlib import Path
INSTALLER_BIN = "node"
PATH = ""
lib_dir = Path(os.environ.get("ABXPKG_LIB_DIR", "~/.config/abx/lib")).expanduser()
install_root = Path(os.environ.get("ABXPKG_CHROMEWEBSTORE_ROOT", lib_dir / "chromewebstore"))
bin_dir = install_root / "extensions"
install_root for the extension cache root, and bin_dir for the unpacked extension output dir.dry_run: shared behavior.min_release_age is unsupported and is ignored with a warning if explicitly requested. postinstall_scripts=False is supported as a standard kwarg and ABXPKG_POSTINSTALL_SCRIPTS hydrates the provider default here, but there is no extra install-time toggle beyond the packaged JS helper this provider already uses.install_args are [webstore_id, "--name=<extension_name>"].chromewebstore_utils.js helper is used to download, unpack, and cache the extension, and the resolved binary path is the unpacked manifest.json. no_cache=True bypasses that metadata cache on the next install/update without deleting the unpacked extension tree.PuppeteerProvider (puppeteer)Source: abxpkg/binprovider_puppeteer.py • Tests: tests/test_puppeteerprovider.py
import os
from pathlib import Path
INSTALLER_BIN = "puppeteer-browsers"
PATH = ""
lib_dir = Path(os.environ.get("ABXPKG_LIB_DIR", "~/.config/abx/lib")).expanduser()
install_root = Path(os.environ.get("ABXPKG_PUPPETEER_ROOT", lib_dir / "puppeteer"))
bin_dir = install_root / "bin"
install_root for the root dir and bin_dir for symlinked executables. Leave it unset for ambient/global mode, where cache ownership stays with the host. INSTALLER_BINARY() intentionally resolves only an already-bootstrapped puppeteer-browsers CLI from the shared ABXPKG_LIB_DIR/npm bin dir, a custom provider-local npm prefix, or ambient PATH; it does not delegate to the generic cross-provider installer resolver.install_root is pinned, abxpkg manages <install_root>/cache end-to-end — it's exported as PUPPETEER_CACHE_DIR to every subprocess, used for --path= on puppeteer-browsers install / list, and uninstall() resolves the real browser directory via load() then rmtrees it. When install_root is unset the provider is in pure passthrough mode: the caller's ambient $PUPPETEER_CACHE_DIR (or the CLI's ~/.cache/puppeteer default) flows through to subprocesses unchanged, load() trusts whatever path puppeteer-browsers list reports, and uninstall() still rmtrees the real browser directory returned by load() — leaving any unrelated browsers in the shared cache alone.@puppeteer/browsers through NpmProvider and then uses that CLI for browser installs.dry_run: shared behavior.min_release_age is unsupported for browser installs and is ignored with a warning if explicitly requested. postinstall_scripts=False is supported for the underlying npm bootstrap path, and ABXPKG_POSTINSTALL_SCRIPTS hydrates the provider default here.install_args are passed through to @puppeteer/browsers install ..., with the provider appending --path=<cache_dir>. Installing puppeteer-browsers itself is treated as the CLI bootstrap case, not as a browser target.node and puppeteer-browsers as dependency cache entries when they are resolved through upstream providers.PlaywrightProvider (playwright)Source: abxpkg/binprovider_playwright.py • Tests: tests/test_playwrightprovider.py
from pathlib import Path
INSTALLER_BIN = "playwright"
PATH = ""
install_root = None # abxpkg-managed root dir for bin_dir / nested npm prefix
bin_dir = Path("/tmp/abxpkg-playwright/bin") # symlink dir when install_root is configured
euid = 0 # routes exec() through sudo-first-then-fallback
install_root to pin the abxpkg-managed root dir (where bin_dir symlinks and the nested npm prefix live). Leave it unset to let playwright use its own OS-default browsers path (~/.cache/ms-playwright on Linux etc.) — in that case abxpkg maintains no symlink dir or npm prefix at all, the playwright npm CLI bootstraps against the host's npm default, and load() returns the resolved executablePath() directly. bin_dir overrides the symlink directory when install_root is pinned. INSTALLER_BINARY() intentionally resolves only an already-bootstrapped playwright CLI from the shared ABXPKG_LIB_DIR/npm bin dir, a custom provider-local npm prefix, or ambient PATH; it does not delegate to the generic cross-provider installer resolver.install_root is pinned, abxpkg manages <install_root>/cache end-to-end — exported as PLAYWRIGHT_BROWSERS_PATH to every subprocess (including the env KEY=VAL -- ... wrapper used when we go through sudo), used to scope executablePath() hits on load(), and uninstall() resolves the real browser directory via load() then rmtrees it. When install_root is unset the provider is in pure passthrough mode: the caller's ambient $PLAYWRIGHT_BROWSERS_PATH (or playwright's ~/.cache/ms-playwright default on Linux) flows through to subprocesses unchanged, load() trusts whatever path executablePath() reports, and uninstall() still rmtrees the real browser directory returned by load().playwright npm package through NpmProvider, then runs playwright install --with-deps <install_args> against it. Resolves each installed browser's real executable via the playwright-core Node.js API (chromium.executablePath() etc.) and writes a symlink into bin_dir when one is configured.dry_run: shared behavior — the install handler short-circuits to a placeholder without touching the host.--with-deps installs system packages and requires root on Linux. euid defaults to 0, which routes every exec() call through the base BinProvider.exec sudo-first-then-fallback path — it tries sudo -n -- playwright install --with-deps ... first on non-root hosts, falls back to running the command directly if sudo fails or isn't available, and merges both stderr outputs into the final error if both attempts fail.min_release_age and postinstall_scripts=False are unsupported for browser installs and are ignored with a warning if explicitly requested.install_args are appended onto playwright install after playwright_install_args (defaults to ["--with-deps"]) and passed through verbatim — use whatever browser names / flags the playwright install CLI accepts (chromium, firefox, webkit, --no-shell, --only-shell, --force, etc.).update() bumps the playwright npm package in install_root first (via NpmProvider.update) so its pinned browser versions refresh, then re-runs playwright install --force <install_args> to pull any new browser builds. uninstall() resolves the browser's real install directory via playwright-core's executablePath(), walks up to the containing <bin_name>-<buildId>/ dir, and rmtrees that dir — in both managed and passthrough modes — because playwright uninstall itself has no per-browser argument and only drops unused browsers wholesale.PyinfraProvider (pyinfra)Source: abxpkg/binprovider_pyinfra.py • Tests: tests/test_pyinfraprovider.py
import os
from abxpkg.binprovider import DEFAULT_PATH
INSTALLER_BIN = "pyinfra"
PATH = os.environ.get("PATH", DEFAULT_PATH)
pyinfra_installer_module = "auto"
pyinfra_installer_kwargs = {}
installer_module="auto" resolves to operations.brew.packages on macOS and operations.server.packages on Linux.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args is the package list passed to the selected pyinfra operation.AnsibleProvider (ansible)Source: abxpkg/binprovider_ansible.py • Tests: tests/test_ansibleprovider.py
import os
from abxpkg.binprovider import DEFAULT_PATH
from abxpkg.binprovider_ansible import ANSIBLE_INSTALL_PLAYBOOK_TEMPLATE
INSTALLER_BIN = "ansible"
PATH = os.environ.get("PATH", DEFAULT_PATH)
ansible_installer_module = "auto"
ansible_playbook_template = ANSIBLE_INSTALL_PLAYBOOK_TEMPLATE
ansible-runner.installer_module="auto" resolves to community.general.homebrew on macOS and ansible.builtin.package on Linux.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args becomes the playbook loop input for the chosen Ansible module.BinaryRepresents a single binary dependency aka a package (e.g. wget, curl, ffmpeg). Each Binary can declare one or more BinProviders it supports, along with per-provider overrides.
Binarys implement the following interface:
load(), install(), update(), uninstall() -> Binarybinprovidersbinprovider / loaded_binproviderabspath / loaded_abspathabspaths / loaded_abspathsversion / loaded_versionsha256 / loaded_sha256mtime / loaded_mtimeeuid / loaded_euidBinary.install() and Binary.update() return a fresh loaded Binary. Binary.uninstall() returns a Binary with binprovider, abspath, version, sha256, mtime, and euid cleared after removal. Binary.load(), Binary.install(), and Binary.update() all enforce min_version consistently. All four lifecycle methods also accept no_cache=True to bypass cached/current-state checks.
from abxpkg import Binary, SemVer, env, brew
curl = Binary(
name="curl",
min_version=SemVer("7.0.0"),
binproviders=[env, brew],
).load()
print(curl.binprovider) # EnvProvider(...) or BrewProvider(...)
print(curl.abspath) # Path('/usr/local/bin/curl')
print(curl.version) # SemVer(7, 88, 1) or newer
print(curl.is_valid) # True
assert curl.is_valid
For reusable Binary subclasses with per-provider overrides, see Advanced Usage above.
SemVerfrom abxpkg import SemVer
### Example: Use the SemVer type directly for parsing & verifying version strings
SemVer.parse('Google Chrome 124.0.6367.208+beta_234. 234.234.123') # SemVer(124, 0, 6367)
SemVer.parse('2024.04.05') # SemVer(2024, 4, 5)
SemVer.parse('1.9+beta') # SemVer(1, 9, 0)
str(SemVer(1, 9, 0)) # '1.9.0'
These types are all meant to be used library-style to make writing your own apps easier.
e.g. you can use it to build things likeplaywright install --with-deps.
abxpkg uses uv for local development, dependency sync, linting, and tests.
checkout_dir="$(mktemp -d)"
trap 'rm -rf "$checkout_dir"' EXIT
git clone --depth=1 https://github.com/ArchiveBox/abxpkg "$checkout_dir"
cd "$checkout_dir"
# setup the venv and install packages
uv sync --all-extras
# run formatting/lint/type checks
uv run prek run --all-files
# Exercise representative core, environment, and provider behavior.
uv run pytest -s \
tests/test_semver.py \
tests/test_binary.py \
tests/test_envprovider.py \
tests/test_module_api.py
The mandatory per-file CI matrix runs the complete standard suite, the
host-mutating provider files, and every root_required and docker_required
file on equipped isolated runners.
# build distributions and validate the publish command without uploading
uv build
uv publish --dry-run dist/*
tests/.uv run pytest -s tests/test_npmprovider.py or a specific node like uv run pytest -s tests/test_npmprovider.py::TestNpmProvider::test_provider_dry_run_does_not_install_zx when iterating on one provider.Note: this package used to be called pydantic-pkgr, it was renamed to abxpkg on 2024-11-12.
Python
97.8%
Shell
1.6%
📦 Modern strongly typed Python library for managing system dependencies with package managers like apt, brew, pip, npm, etc.
28
stars
1,397
commits
Python
primary language
Sep 2, 2026
updated
abxpkg 📦 apt brew pip uv npm pnpm yarn bun deno cargo gem goget nix docker bash puppeteer playwright chromewebstore ansible pyinfra
Use abxpkg to detect & auto-install dependencies at runtime, serialize your dependencies to DB/config, and manage bins across many ecosystems.
This is a Python library and all-in-one CLI for managing packages locally with a variety of package managers.
It's designed for when you have to detect or install binary or source dependencies at runtime.
Stop distributing your apps via curl | sh! Instead you can bake package installation into your app, or use our uv-style abxpkg run --script shebang headers to auto-install dependencies for you.
pip install abxpkg # uv tool install abxpkg
abxpkg --version
from pathlib import Path
from tempfile import TemporaryDirectory
from abxpkg import Binary, env, npm, brew
prettier = env.load('prettier') or npm.install('prettier') or brew.install('prettier')
# or equivalent:
prettier = Binary(name='prettier', binproviders=[env, npm, brew]).install()
print(prettier.abspath, prettier.version)
# ~/.cache/abx/lib/npm/bin/prettier 2.2.1
with TemporaryDirectory() as temp_dir:
example = Path(temp_dir) / 'example.js'
example.write_text('const answer=42\n')
prettier.exec(cmd=['--write', str(example)])
# Search a provider's package index for matches:
matches = npm.search('puppeteer') # -> list[Binary] with name + install_args populated
assert isinstance(matches, list)
📦 Provides consistent interfaces for runtime dependency resolution & installation across multiple package managers & OSs ✨ Built with
pydanticv2 for strong static typing guarantees and easy conversion to/from json 🌈 Usable withdjango>= 4.0,django-ninja, and OpenAPI +django-jsonformto build UIs & APIs 🦄 Driver layer can bepyinfra/ansible/ or built-inabxpkgengine
Built by ArchiveBox to install & auto-update our extractor dependencies at runtime (chrome, wget, curl, etc.) on macOS/Linux/Docker.
Source Code: https://github.com/ArchiveBox/abxpkg/
Documentation: https://github.com/ArchiveBox/abxpkg/blob/main/README.md
from abxpkg import Binary, apt, brew, docker, env, npm, pip, playwright, pnpm, puppeteer, uv
# Provider singletons are available as simple imports — no manual instantiation needed
dependencies = [
Binary(name='curl', binproviders=[env, apt, brew]),
Binary(name='yt-dlp', binproviders=[env, pip, uv, apt, brew]),
Binary(name='playwright', binproviders=[env, npm, pnpm]),
Binary(name='chromium', binproviders=[env, playwright, puppeteer, apt]),
Binary(name='postgres', binproviders=[env, docker, apt, brew]),
]
assert dependencies[0].binproviders == [env, apt, brew]
assert dependencies[1].binproviders == [env, pip, uv, apt, brew]
[!TIP] 🔒 Stay safe from supply-chain attcaks with
abxpkg: We default to safe behavior (when providers allow):
min_release_age=7(we only install packages that have been published for 7 days or longer)postinstall_scripts=False(we don't run post-install scripts for packages by default)install_root=<platform default abx lib dir>(the CLI defaults to a dedicated provider-rooted library dir so host system stays clean)You can customize these defaults on
BinaryorBinProvider, or withABXPKG_MIN_RELEASE_AGE/ABXPKG_POSTINSTALL_SCRIPTS/ABXPKG_LIB_DIR(see Configuration below).
pip install abxpkg
abxpkg --version
Or install the isolated CLI tool:
uv tool install abxpkg
abxpkg --version
Installing abxpkg also provides an abxpkg CLI entrypoint:
abxpkg --version
abxpkg version
abxpkg list
abxpkg install yt-dlp
abxpkg load yt-dlp
abxpkg env yt-dlp
abxpkg activate yt-dlp
abxpkg update yt-dlp
abxpkg uninstall yt-dlp
Search package indexes:
abxpkg search chromium # search all providers in parallel
abxpkg --binproviders=apt,npm,brew search node # restrict to specific providers
abxpkg --version and abxpkg version stream the package version first, then a host/env summary line, then one section per selected provider showing its current resolved runtime state (INSTALLER_BINARY, PATH, ENV, install_root, bin_dir, and any active cached dependency / installed binaries).
abxpkg version <binary> is a thin alias for abxpkg load <binary>.
abxpkg list prints the full active cache for the selected providers, grouping provider installer binaries first and normal cached binaries after a blank line. You can optionally pass binary names and/or provider names positionally to filter the output:
abxpkg list
abxpkg list yt-dlp chromium
abxpkg list env puppeteer chromium
abxpkg run yt-dlp --help # resolves yt-dlp via the configured providers and execs it
abxpkg --binproviders=pip,brew run pip show black # restrict provider resolution (exercises PipProvider.exec)
abxpkg --binproviders=pip --install run yt-dlp # load first, then install via selected providers if needed
abxpkg --binproviders=pip --update run yt-dlp # ensure the binary is available, then update before exec
abxpkg --binproviders=pip --no-cache --install run yt-dlp # bypass cached/current-state checks during resolution + install
abxpkg env yt-dlp # print dotenv-style KEY=value lines for yt-dlp's runtime env
abxpkg --binproviders=pip env --install black # install if needed, then print the runtime env in .env format
eval "$(abxpkg --binproviders=pip activate black)" # emit bash export lines and apply them to the current shell
abxpkg activate --fish black | source # emit fish set -x lines and source them into fish
eval "$(abxpkg --binproviders=pip activate --zsh black)" # emit zsh export lines
abxpkg env resolves binaries the same way as run, then prints the runtime env as dotenv-style KEY=value lines. abxpkg activate emits a short usage comment plus shell-specific activation commands: bash by default, --zsh for zsh export KEY=value, and --fish for fish set -x KEY VALUE.
abxpkg options (e.g. --binproviders, --lib, --install, --update, --no-cache) must appear before the run subcommand; every argument after the binary name is forwarded verbatim to the underlying binary. The same install/update flags also apply to env / activate. run exits with the child's exit code, passes its stdout/stderr through unbuffered, and routes any abxpkg install/load logs to stderr only — no headers, no footers, no parsing.
abx: auto-install-and-run shortcutThink npx / uvx / pipx run — but for every package manager abxpkg supports. abx is a thin alias for abxpkg --install run ...: it resolves the binary via the configured providers, installs it if missing, then execs it with the forwarded arguments.
abx --binproviders=env python3 --version # run an existing host binary through the same resolution path
abx --binproviders=env python3 -c 'print("abx works")'
Options before the binary name (--lib, --binproviders, --dry-run, --debug, --no-cache, --update) are forwarded to abxpkg; everything after the binary name is forwarded to the binary itself.
Binary / per-BinProvider options as CLI flagsEvery Binary / BinProvider configuration field is exposed as a CLI flag on the group and on subcommands (install, update, uninstall, load), and is also available to run / abx via group-level flags placed before the binary name. Providers that can't enforce a given option emit a warning to stderr and continue — no hard failure.
abxpkg --min-version=1.2.3 --min-release-age=7 install yt-dlp
abxpkg --postinstall-scripts=False --binproviders=apt,uv,pip install black
abxpkg --no-cache install black
abxpkg --install-root=/tmp/yt-dlp-root --bin-dir=/tmp/yt-dlp-bin install yt-dlp
abxpkg --overrides='{"pip":{"install_args":["yt-dlp[default]"]}}' install yt-dlp
abxpkg --install-timeout=600 --version-timeout=20 --euid=1000 install yt-dlp
abxpkg --global install yt-dlp
abx --min-version=2024.1.1 --min-release-age=3 yt-dlp --help
| Flag | Type | Meaning |
|---|---|---|
--min-version=SEMVER | str | Minimum acceptable version (set on Binary.min_version). |
--postinstall-scripts[=BOOL] | bool | Allow post-install scripts. Bare --postinstall-scripts = True. Providers that can't disable them warn-and-ignore. |
--min-release-age=DAYS | float | Minimum days since publication. Non-supporting providers warn-and-ignore. |
--no-cache[=BOOL] | bool | Skip cached/current-state checks and force fresh install/update/load probes. Bare --no-cache = True. |
--overrides=JSON | dict | Per-provider Binary.overrides patches for shared provider fields (PATH, INSTALLER_BIN, install_root, bin_dir, euid, postinstall_scripts, min_release_age, dry_run, install_timeout, version_timeout) plus per-binary handler replacements (install_args, abspath, version, install, update, uninstall). |
--global[=BOOL] | bool | Thin alias for --lib=None. Bare --global = True. |
--install-root=PATH | Path | Override the per-provider install directory. |
--bin-dir=PATH | Path | Override the per-provider bin directory. |
--euid=UID | int | Pin the UID used when providers shell out. |
--install-timeout=SECONDS | int | Seconds to wait for install/update/uninstall subprocesses. |
--version-timeout=SECONDS | int | Seconds to wait for version/metadata probes. |
--dry-run[=BOOL] | bool | Show installer commands without executing them. Bare --dry-run = True. |
--debug[=BOOL] | bool | Emit DEBUG logs to stderr. Bare --debug = True. Defaults to ABXPKG_DEBUG or False. |
Every value-taking flag also accepts the literal string None / null / "" to reset to the provider's default resolution path. For postinstall_scripts / min_release_age, that means the action-specific effective default for that provider (False / 7 on supporting providers, True / 0 otherwise). The precedence is: explicit per-subcommand flag > group-level flag > environment variable > built-in default.
abxpkg install --binproviders=env,uv,pip,apt,brew prettier
# or
env ABXPKG_BINPROVIDERS=env,uv,pip,apt,brew abxpkg install yt-dlp
abxpkg --lib=./vendor/abxpkg --binproviders=env load python3
env ABXPKG_LIB_DIR=./vendor/abxpkg abxpkg --binproviders=env load python3
abxpkg install --dry-run some-dangerous-package # outputs commands that would be run without executing them
# or
env ABXPKG_DRY_RUN=1 abxpkg install some-dangerous-package
CLI result lines are written to stdout. Progress logging is written to stderr at INFO by default. Enable DEBUG logging with ABXPKG_DEBUG=1 or --debug.
⚡️ Inspired by uv's inline script metadata, abxpkg lets you declare arbitrary package dependencies at the top of any script.
It will automatically fetch, install, and make the packages available to your script across a wide variety of languages.
#!/usr/bin/env -S abxpkg run --script node
// /// script
// dependencies = [
// {name = "node", binproviders = ["env", "apt", "brew"], min_version = "22.0.0"},
// {name = "playwright", binproviders = ["env", "pnpm", "npm"], install_args = ["playwright@next"]},
// {name = "chromium", binproviders = ["env", "playwright", "puppeteer", "apt"], min_version = "131.0.0"},
// ]
// [tool.abxpkg]
// ABXPKG_POSTINSTALL_SCRIPTS = true
// ///
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();
})();
<30ms cold-start overhead once cached.
The metadata parser is comment-syntax-agnostic — it looks for /// script and /// delimiters and strips the first whitespace-delimited token from each line, so #, //, --, ;, and any other single-token comment prefix all work.
All built-in providers are available as lazy singletons — just import them by name:
from abxpkg import apt, brew, pip, npm, env
apt.install('curl')
env.load('wget')
These are instantiated on first access and cached for reuse. If you need custom configuration, you can still instantiate provider classes directly:
from pathlib import Path
from abxpkg import PipProvider
custom_pip = PipProvider(install_root=Path("/tmp/abxpkg-pip"), min_release_age=3)
Use the Binary class to declare a package that can be installed by one of several ordered providers, with an optional version floor:
from abxpkg import Binary, SemVer, env, brew
curl = Binary(
name="curl",
min_version=SemVer("7.0.0"),
binproviders=[env, brew],
).install()
min_version is enforced after a provider resolves or installs a binary — provider discovery can still succeed, but the final Binary is rejected if the loaded version is below the floor. Use min_version=None to disable the check.
Pass no_cache=True to load() / install() / update() / uninstall() when you want to bypass cached/current-state checks. For install(), no_cache=True skips the initial load() check and forces a fresh install path. The equivalent CLI and env controls are --no-cache and ABXPKG_NO_CACHE=1.
Provider installer binaries also resolve lazily through the active provider chain. If a provider needs pip, npm, cargo, or another installer tool and it is missing, abxpkg will auto-install that dependency using the currently selected providers and the same ABXPKG_LIB_DIR / --lib / --global settings.
Binary subclass with per-provider overridesfrom pydantic import InstanceOf
from abxpkg import BinProvider, Binary, BinProviderName, BinName, HandlerDict, BrewProvider
from abxpkg import env, pip, apt
class CustomBrewProvider(BrewProvider):
name: BinProviderName = 'custom_brew'
def get_macos_packages(self, bin_name: str, **context) -> list[str]:
return ['yt-dlp'] if bin_name == 'ytdlp' else [bin_name]
class YtdlpBinary(Binary):
name: BinName = 'ytdlp'
description: str = 'YT-DLP (Replacement for YouTube-DL) Media Downloader'
# define the providers this binary supports
binproviders: list[InstanceOf[BinProvider]] = [env, pip, apt, CustomBrewProvider()]
# customize installed package names for specific package managers
overrides: dict[BinProviderName, HandlerDict] = {
'pip': {'install_args': ['yt-dlp[default,curl-cffi]']}, # literal values
'apt': {'install_args': lambda: ['yt-dlp', 'ffmpeg']}, # any pure Callable
'custom_brew': {'install_args': 'self.get_macos_packages'}, # or a string ref to a method on self
}
ytdlp = YtdlpBinary()
assert [provider.name for provider in ytdlp.binproviders] == ['env', 'pip', 'apt', 'custom_brew']
assert ytdlp.overrides['pip']['install_args'] == ['yt-dlp[default,curl-cffi]']
Binary objects as a stable typed interface to interact with installed packagesfrom abxpkg import Binary, env
# Use providers directly for host binary discovery
python = env.load('python3')
assert python and python.is_valid
print(env.PATH, env.get_abspaths('python3'), env.get_version('python3'))
# our Binary API provides a nice type-checkable, validated, serializable handle
python = Binary(name='python3', binproviders=[env]).load()
print(python) # Binary(name='python3', abspath=Path(...), version=SemVer(...), sha256='...', mtime=1712890123456789000)
print(python.abspaths) # show all matching binaries found via each provider PATH
print(python.model_dump(mode='json')) # JSON-ready dict
print(python.model_json_schema()) # ... OpenAPI-ready JSON schema showing all available fields
from pydantic import InstanceOf
from abxpkg import Binary, BinProvider, BrewProvider, EnvProvider
# You can also instantiate provider classes manually for custom configuration,
# or define binaries as classes for type checking
class CurlBinary(Binary):
name: str = 'curl'
binproviders: list[InstanceOf[BinProvider]] = [EnvProvider(), BrewProvider()]
curl = CurlBinary().install()
assert isinstance(curl, CurlBinary) # CurlBinary is a unique type you can use in annotations now
print(curl.abspath, curl.version, curl.binprovider, curl.is_valid) # Path(...) SemVer(...) BrewProvider()/EnvProvider() True
curl.exec(cmd=['--version']) # curl 8.4.0 (x86_64-apple-darwin23.0) libcurl/8.4.0 ...
import platform
import shutil
from pydantic import InstanceOf
from abxpkg import BinProvider, Binary, BinProviderName, BinName, HandlerDict
from abxpkg import env, apt
class DockerBinary(Binary):
name: BinName = 'docker'
binproviders: list[InstanceOf[BinProvider]] = [env, apt]
overrides: dict[BinProviderName, HandlerDict] = {
'env': {
# prefer podman if installed, fall back to docker
'abspath': lambda: shutil.which('podman') or shutil.which('docker') or shutil.which('docker-ce'),
},
'apt': {
# vary the installed package name based on CPU architecture
'install_args': {
'x86_64': ['docker.io'],
'armv7l': ['docker.io'],
'aarch64': ['docker.io'],
}.get(platform.machine(), ['docker.io']),
},
}
docker = DockerBinary().install()
BinProvider to add support for a new package managerfrom pathlib import Path
from abxpkg import (
BinProvider,
BinProviderName,
BinName,
HostBinPath,
InstallArgs,
SemVer,
bin_abspath,
)
class CargoProvider(BinProvider):
name: BinProviderName = 'cargo'
INSTALLER_BIN: BinName = 'cargo'
PATH: str = str(Path.home() / '.cargo/bin')
def default_install_args_handler(self, bin_name: BinName, **context) -> InstallArgs:
return [bin_name]
def default_install_handler(
self,
bin_name: BinName,
install_args: InstallArgs | None = None,
postinstall_scripts: bool | None = None,
min_release_age: float | None = None,
min_version: SemVer | None = None,
timeout: int | None = None,
) -> str:
install_args = install_args or self.get_install_args(bin_name)
installer = self.INSTALLER_BINARY()
assert installer and installer.loaded_abspath
proc = self.exec(bin_name=installer.loaded_abspath, cmd=['install', *install_args], timeout=timeout)
if proc.returncode != 0:
self._raise_proc_error('install', install_args, proc)
return proc.stdout.strip() or proc.stderr.strip()
def default_abspath_handler(self, bin_name: BinName, **context) -> HostBinPath | None:
return bin_abspath(bin_name, PATH=self.PATH)
def default_version_handler(
self,
bin_name: BinName,
abspath: HostBinPath | None = None,
timeout: int | None = None,
**context,
) -> SemVer | None:
return self._version_from_exec(bin_name, abspath=abspath, timeout=timeout)
cargo = CargoProvider()
assert cargo.get_install_args('ripgrep') == ('ripgrep',)
assert cargo.default_install_args_handler('ripgrep') == ['ripgrep']
logging to customize the stderr/stdout loggingabxpkg uses the standard Python logging module. By default it stays quiet unless your application configures logging explicitly.
import logging
from abxpkg import Binary, env, configure_logging
configure_logging(logging.INFO)
python = Binary(name='python', binproviders=[env]).load()
To enable Rich logging:
pip install "abxpkg[rich]"
import logging
from abxpkg import Binary, EnvProvider, configure_rich_logging
configure_rich_logging(logging.DEBUG)
python = Binary(name='python', binproviders=[EnvProvider()]).load()
Debug logging is hardened so logging itself does not become the failure. If a provider/model object has a broken or overly-expensive repr(), abxpkg falls back to a short ClassName(...) summary instead of raising while formatting log output.
configure_rich_logging(...) uses rich.logging.RichHandler under the hood, so log levels, paths, arguments, and command lines render with terminal colors when supported.
You can also manage it with standard logging primitives:
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("abxpkg").setLevel(logging.DEBUG)
BinProvider / Binary in DB models and render them in the AdminWith a few more packages, you get type-checked Django fields & forms that support BinProvider and Binary.
[!TIP] For the full Django experience, we recommend installing these 3 excellent packages:
django-admin-data-viewsdjango-pydantic-fielddjango-jsonformpip install abxpkg django-admin-data-views django-pydantic-field django-jsonform
Django model fields:
from django.conf import settings
if not settings.configured:
settings.configure(
INSTALLED_APPS=[
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
],
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:'}},
SECRET_KEY='abxpkg-docs',
)
import django
django.setup()
from django.db import models
from django.db import connection
from abxpkg import Binary, EnvProvider, SemVer
from django_pydantic_field import SchemaField
provider_runtime_fields = {name: True for name in EnvProvider.model_computed_fields}
binary_runtime_fields = {name: True for name in Binary.model_computed_fields}
binary_runtime_fields['binproviders'] = True
binary_runtime_fields['loaded_binprovider'] = True
class Dependency(models.Model):
label = models.CharField(max_length=63)
default_binprovider: EnvProvider = SchemaField(exclude=provider_runtime_fields)
binaries: list[Binary] = SchemaField(
default=[],
exclude={'__all__': binary_runtime_fields},
)
min_version: SemVer = SchemaField(default=(0, 0, 1))
class Meta:
app_label = 'abxpkg_docs'
with connection.schema_editor() as schema_editor:
schema_editor.create_model(Dependency)
Saving a Binary using the model:
from abxpkg import Binary, env
python = Binary(name='python3', binproviders=[env]).load()
obj = Dependency(
label='runtime tools',
default_binprovider=env, # store BinProvider values directly
binaries=[python], # store Binary/SemVer values directly
)
obj.save()
When fetching back from the DB, Binary fields are auto-deserialized and immediately usable:
obj = Dependency.objects.get(label='runtime tools')
assert obj.binaries[0].abspath == python.abspath
obj.binaries[0].exec(cmd=['--version'])
For a full example see the bundled django_example_project/.
Django Admin integration:
# settings.py
INSTALLED_APPS = [
# ...
'admin_data_views',
'abxpkg',
]
ABXPKG_GET_ALL_BINARIES = 'project.views.get_all_binaries'
ABXPKG_GET_BINARY = 'project.views.get_binary'
ADMIN_DATA_VIEWS = {
"NAME": "Environment",
"URLS": [
{
"route": "binaries/",
"view": "abxpkg.views.binaries_list_view",
"name": "binaries",
"items": {
"route": "<str:key>/",
"view": "abxpkg.views.binary_detail_view",
"name": "binary",
},
},
],
}
If you override the default site admin, register the views manually:
from django.conf import settings
if not settings.configured:
settings.configure(INSTALLED_APPS=[])
import django
django.setup()
from django.contrib.admin import AdminSite
from abxpkg.admin import register_admin_views
custom_admin = AdminSite(name='custom')
register_admin_views(custom_admin)
All abxpkg env vars are read once at import time and only apply when set. Explicit constructor kwargs always override these defaults.
Behavioral controls (apply across all providers):
| Variable | Default | Effect |
|---|---|---|
ABXPKG_DRY_RUN / DRY_RUN | 0 | Flips the shared dry_run default. ABXPKG_DRY_RUN wins if both are set. Provider subprocesses are logged and skipped, install() / update() return a placeholder, uninstall() returns True. |
ABXPKG_NO_CACHE | 0 | Flips the shared no_cache default. When enabled, install() skips the initial load() check and forces a fresh install path, while load() / update() / uninstall() bypass cached probe results. |
ABXPKG_DEBUG | 0 | Enables DEBUG-level CLI logging on stderr for abxpkg / abx. The matching CLI flag is --debug. Default CLI logging level is INFO. |
ABXPKG_INSTALL_TIMEOUT | 120 | Seconds to wait for install() / update() / uninstall() handler subprocesses. |
ABXPKG_VERSION_TIMEOUT | 10 | Seconds to wait for version / metadata probes (--version, npm show, pip show, etc.). |
ABXPKG_POSTINSTALL_SCRIPTS | unset | Hydrates the provider-level default for the postinstall_scripts kwarg on every provider that supports it (pip, uv, npm, pnpm, yarn, bun, deno, brew, chromewebstore, puppeteer). When left unset, action execution resolves to the provider/action default (False on supporting providers, True otherwise). |
ABXPKG_MIN_RELEASE_AGE | 7 | Hydrates the provider-level default (in days) for the min_release_age kwarg on every provider that supports it (pip, uv, npm, pnpm, yarn, bun, deno). When left unset, action execution resolves to the provider/action default (7 on supporting providers, 0 otherwise). |
ABXPKG_BINPROVIDERS | shared default order | Comma-separated list of provider names to enable (and their order) for the abxpkg CLI. By default this uses DEFAULT_PROVIDER_NAMES from abxpkg.__init__ (which excludes ansible / pyinfra, and also excludes apt on macOS). |
Install-root controls (one global default + one per-provider override):
| Variable | Applies to | Effect |
|---|---|---|
ABXPKG_LIB_DIR | providers whose default install_root is abxpkg-managed | Centralized library root. When set, each matching provider points its default install_root at $ABXPKG_LIB_DIR/<provider name> (e.g. <lib>/env, <lib>/npm, <lib>/pip, <lib>/gem, <lib>/playwright). Accepts relative (./lib), tilde (~/.config/abx/lib), and absolute (/tmp/abxlib) paths. --global is a thin alias for --lib=None, which clears this root for the current CLI invocation. |
ABXPKG_<BINPROVIDER>_ROOT | the matching provider's install_root | Generic per-provider override; beats ABXPKG_LIB_DIR/<provider name>. Examples: ABXPKG_PIP_ROOT, ABXPKG_UV_ROOT, ABXPKG_NPM_ROOT, ABXPKG_GOGET_ROOT, ABXPKG_CHROMEWEBSTORE_ROOT. The <BINPROVIDER> token is the provider name uppercased. |
Install-root precedence (most specific wins): explicit install_root= / provider alias kwarg > ABXPKG_<NAME>_ROOT > ABXPKG_LIB_DIR/<name> > provider-specific built-in default / native global mode.
Provider-specific binary overrides:
Each provider also honors a <NAME>_BINARY=/abs/path/to/<name> env var to pin the exact executable it shells out to — PIP_BINARY, UV_BINARY, NPM_BINARY, PNPM_BINARY, YARN_BINARY, BUN_BINARY, DENO_BINARY, etc.
Per-Binary / per-BinProvider fields (constructor kwargs, most-specific wins):
min_version can be set on any individual Binary.min_release_age can be set on Binary or BinProvider, or via ABXPKG_MIN_RELEASE_AGE (days).postinstall_scripts can be set on Binary or BinProvider, or via ABXPKG_POSTINSTALL_SCRIPTS.no_cache can be passed per-call to load() / install() / update() / uninstall(), or enabled globally for the CLI via ABXPKG_NO_CACHE.install_root / bin_dir can be set on any BinProvider with an isolated install location, or default to ABXPKG_<NAME>_ROOT / ABXPKG_LIB_DIR/<provider name> / the provider's own built-in default.dry_run can be set on BinProvider or passed per-call to install() / update() / uninstall(), or via ABXPKG_DRY_RUN / DRY_RUN.install_timeout can be set on BinProvider or via ABXPKG_INSTALL_TIMEOUT (seconds).version_timeout can be set on BinProvider or via ABXPKG_VERSION_TIMEOUT (seconds).euid can be set on BinProvider to pin the UID used to sudo/drop into when running provider subprocesses; otherwise it's auto-detected from install_root ownership.overrides is a dict[BinProviderName, HandlerDict] (on Binary) or dict[BinName, HandlerDict] (on BinProvider) mapping to per-provider field patches and per-binary handler replacements. Supported keys are PATH, INSTALLER_BIN, euid, install_root, bin_dir, dry_run, postinstall_scripts, min_release_age, install_timeout, version_timeout, install_args / packages, abspath, version, install, update, and uninstall. See Advanced Usage for examples.Precedence is always: explicit action kwarg > Binary(...) field > BinProvider(...) field > env var > built-in default.
BinProviderBuilt-in implementations: EnvProvider, AptProvider, BrewProvider, PipProvider, UvProvider, NpmProvider, PnpmProvider, YarnProvider, BunProvider, DenoProvider, CargoProvider, GemProvider, GoGetProvider, NixProvider, DockerProvider, PyinfraProvider, AnsibleProvider, BashProvider, ChromeWebstoreProvider, PuppeteerProvider, PlaywrightProvider
This type represents a provider of binaries, e.g. a package manager like apt / pip / npm, or env (which only resolves binaries already present in $PATH).
Every provider exposes the same lifecycle surface:
load() / install() / update() / uninstall()search() to discover installable package matches from a provider indexget_install_args() to resolve package names / formulae / image refs / module specsget_abspath() / get_abspaths() / get_version() / get_sha256() / get_docs_url()Shared base defaults come from abxpkg/binprovider.py and apply unless a concrete provider overrides them:
import sys
from pathlib import Path
INSTALLER_BIN = "env" # base-class placeholder; real providers override this
PATH = str(Path(sys.executable).parent)
postinstall_scripts = None # some providers override this with ABXPKG_POSTINSTALL_SCRIPTS
min_release_age = None # some providers override this with ABXPKG_MIN_RELEASE_AGE
install_timeout = 120 # or ABXPKG_INSTALL_TIMEOUT=120
version_timeout = 10 # or ABXPKG_VERSION_TIMEOUT=10
dry_run = False # or ABXPKG_DRY_RUN=1 / DRY_RUN=1
dry_run: use provider.get_provider_with_overrides(dry_run=True), pass dry_run=True directly to install() / update() / uninstall(), or set ABXPKG_DRY_RUN=1 / DRY_RUN=1. If both env vars are set, ABXPKG_DRY_RUN wins. Provider subprocesses are logged and skipped, install() / update() return a placeholder loaded binary, and uninstall() returns True without mutating the host.no_cache: use --no-cache / ABXPKG_NO_CACHE=1 on the CLI, or pass no_cache=True directly to load() / install() / update() / uninstall(). For install(), this skips the initial load() check and forces a fresh install path.install_timeout: shared provider-level timeout used by install(), update(), and uninstall() handler execution paths. Can also be set with ABXPKG_INSTALL_TIMEOUT.version_timeout: shared provider-level timeout used by version / metadata probes such as --version, npm show, npm list, pip show, go version -m, and brew lookups. Can also be set with ABXPKG_VERSION_TIMEOUT.postinstall_scripts and min_release_age are standard provider/binary/action kwargs. Supporting providers hydrate defaults from ABXPKG_POSTINSTALL_SCRIPTS and ABXPKG_MIN_RELEASE_AGE; when those remain unset/None, install/update/uninstall resolve them to effective action defaults (False / 7 on supporting providers, True / 0 otherwise).None. If you pass an explicit unsupported value during install() / update(), it is logged as a warning and ignored.INSTALLER_BINARY() overrides call the base resolver when the installer is an externally supplied tool that can be found through the selected upstream providers (pip, npm, cargo, gem, etc.). Providers whose installer CLI is produced by their own setup path, such as Puppeteer and Playwright, first resolve an already-bootstrapped CLI from the shared ABXPKG_LIB_DIR npm space, a custom provider-local npm prefix, or ambient PATH, then raise unavailable so setup can bootstrap it through the intended provider when missing.Binary(...) defaults > provider defaults.For the full list of env vars that hydrate these defaults, see Configuration above.
Supported override keys are the same everywhere:
from pathlib import Path
from abxpkg import PipProvider
provider = PipProvider(install_root=Path("/tmp/venv")).get_provider_with_overrides(
overrides={
"black": {
"install_args": ["black==24.4.2"],
"version": "self.default_version_handler",
"abspath": "self.default_abspath_handler",
"docs_url": "self.default_docs_url_handler",
"search": "self.default_search_handler",
},
},
dry_run=True,
version_timeout=30,
)
install_args / packages: package-manager arguments for that provider. packages is the legacy alias.abspath, version, install, update, uninstall, docs_url, search: literal values, callables, or "self.method_name" references that replace the provider handler for a specific binary.PATH, INSTALLER_BIN, euid, install_root, bin_dir, dry_run, postinstall_scripts, min_release_age, install_timeout, version_timeout: shared provider field patches applied to the copied provider instance before handler resolution.Providers with isolated install locations also expose a shared constructor surface:
install_root: shared provider root for package state, metadata, caches, venvs, project dirs, profiles, or downloaded assets, depending on the provider.bin_dir: shared executable output dir when a provider separates package state from runnable binaries.provider.install_root / provider.bin_dir: normalized computed properties you can inspect after construction, regardless of which provider-specific args were used.install_root / bin_dir at construction time instead of silently ignoring them.PATH entries.BinProvidersEnvProvider (env)Source: abxpkg/binprovider.py • Tests: tests/test_envprovider.py
from abxpkg.binprovider import DEFAULT_ENV_PATH
INSTALLER_BIN = "which"
PATH = DEFAULT_ENV_PATH # current PATH + current Python bin dir
ABXPKG_ENV_ROOT, or ABXPKG_LIB_DIR/env, or the platform default abx lib dir under env/. env is still read-only: it only resolves binaries that already exist on the host PATH, but when an install root is configured it also keeps a managed bin/ symlink dir and derived.env cache there.min_release_age and postinstall_scripts are unsupported here and are ignored with a warning if explicitly passed to install() / update().abspath / version are the useful ones here. python has a built-in override to the current sys.executable and interpreter version.env/bin/<name> and resolved through that stable symlink. install() / update() return explanatory no-op messages, and uninstall() is a no-op.AptProvider (apt)Source: abxpkg/binprovider_apt.py • Tests: tests/test_aptprovider.py
INSTALLER_BIN = "apt-get"
PATH = "" # populated from `dpkg -L bash` bin/sbin roots
euid = 0 # always runs as root
apt-get directly.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args becomes apt-get install -y -qq --no-install-recommends ...; update() uses apt-get install --only-upgrade ...; uninstall() uses apt-get remove -y -qq ....dpkg-query metadata.BrewProvider (brew)Source: abxpkg/binprovider_brew.py • Tests: tests/test_brewprovider.py
INSTALLER_BIN = "brew"
PATH = "/home/linuxbrew/.linuxbrew/bin:/opt/homebrew/bin:/usr/local/bin"
brew_prefix = "/opt/homebrew" # guessed host prefix: /opt/homebrew, /usr/local, or linuxbrew
brew_prefix is the Homebrew prefix used for discovery and shelling out to brew. By default it resolves from ABXPKG_BREW_ROOT, or ABXPKG_LIB_DIR/brew, or a guessed host prefix (/opt/homebrew, /usr/local, or linuxbrew). bin_dir is used for linked formula binaries when abxpkg manages them separately.brew directly.dry_run: shared behavior.min_release_age is unsupported and is ignored with a warning if explicitly requested. postinstall_scripts=False is supported on brew install via --skip-post-install, and ABXPKG_POSTINSTALL_SCRIPTS hydrates the provider default here. Homebrew has no equivalent flag for brew upgrade, so updates run without it.install_args maps to formula / cask args passed to brew install, brew upgrade, and brew uninstall.brew update at most once per day. Explicit --skip-post-install args in install_args win over derived defaults for installs.PipProvider (pip)Source: abxpkg/binprovider_pip.py • Tests: tests/test_pipprovider.py, tests/test_security_controls.py
INSTALLER_BIN = "pip"
PATH = "" # auto-built from global/user Python bin dirs
install_root = None # None = ambient/global mode, Path(...) = provider root
install_root=None uses the system/user Python environment. Set install_root=Path(...) for a hermetic provider root whose actual virtualenv lives at <install_root>/venv, with executables under <install_root>/venv/bin and provider metadata like derived.env kept at <install_root>.pip directly. Honors PIP_BINARY=/abs/path/to/pip. Use UvProvider for uv-backed installs.dry_run: shared behavior.postinstall_scripts=False (always) and min_release_age (on pip >= 26.0 or in a freshly bootstrapped pip venv). Hydrated from ABXPKG_POSTINSTALL_SCRIPTS and ABXPKG_MIN_RELEASE_AGE. For stricter enforcement on hosts with older system pip, use UvProvider instead.install_args is passed as pip requirement specs; unpinned specs get a >=min_version floor when min_version is supplied.postinstall_scripts=False adds pip --only-binary :all: (wheels only, no arbitrary sdist build scripts). min_release_age is enforced with pip --uploaded-prior-to=<ISO8601> on pip >= 26.0 (see pypa/pip#13625); older pip silently skips the flag. Explicit conflicting flags already present in install_args win over the derived defaults. get_version / get_abspath fall back to parsing pip show <package> output when the console script can't report its own version.UvProvider (uv)Source: abxpkg/binprovider_uv.py • Tests: tests/test_uvprovider.py
INSTALLER_BIN = "uv"
PATH = "" # prepends <install_root>/venv/bin or the uv tool bin dir
install_root = None # None = global uv tool mode, Path(...) = provider root
install_root is set.
install_root=Path(...)): treats install_root as a provider root, creates the real venv at <install_root>/venv via uv venv, and installs packages into it with uv pip install --python <install_root>/venv/bin/python .... Binaries land in <install_root>/venv/bin/<name>, while provider metadata like derived.env stays at <install_root>. This matches PipProvider's layout.install_root=None): delegates to uv tool install which creates a fresh venv per tool under UV_TOOL_DIR (default ~/.local/share/uv/tools) and writes shims into UV_TOOL_BIN_DIR (default ~/.local/bin). Pass bin_dir=Path(...) to override the shim dir. This is the idiomatic "install a CLI tool globally" path.UV_BINARY=/abs/path/to/uv. If uv isn't on the host, the provider is unavailable.dry_run: shared behavior.min_release_age and postinstall_scripts=False, and hydrates their provider defaults from ABXPKG_MIN_RELEASE_AGE and ABXPKG_POSTINSTALL_SCRIPTS. In both modes, postinstall_scripts=False becomes --no-build (wheels-only, no arbitrary sdist build scripts) and min_release_age becomes --exclude-newer=<ISO8601> (uv 0.4+). Explicit conflicting flags already present in install_args win over the derived defaults.install_args is passed as requirement specs; unpinned specs get a >=min_version floor when min_version is supplied.uv pip install --upgrade; update in global mode is uv tool install --force (re-installs the tool's venv). Uninstall in venv mode uses uv pip uninstall --python <venv>/bin/python; in global mode it uses uv tool uninstall <name>.NpmProvider (npm)Source: abxpkg/binprovider_npm.py • Tests: tests/test_npmprovider.py, tests/test_security_controls.py
INSTALLER_BIN = "npm"
PATH = "" # auto-built from npm local + global bin dirs
install_root = None # None = global install, Path(...) = prefix/project root
install_root=None installs globally (walks up from the host's npm prefix / npm prefix -g to seed PATH). Set install_root=Path(...) to install under <prefix>/node_modules/.bin; that prefix bin dir becomes the provider's active executable search path.npm directly and expects npm to be installed on the host. Honors NPM_BINARY=/abs/path/to/npm. Use PnpmProvider for pnpm.dry_run: shared behavior.postinstall_scripts=False and min_release_age, hydrated from ABXPKG_POSTINSTALL_SCRIPTS and ABXPKG_MIN_RELEASE_AGE. min_release_age requires an npm build that ships --min-release-age (detected once by probing npm install --help).install_args is passed as npm package specs; unpinned specs get rewritten to pkg@>=<min_version> when min_version is supplied.postinstall_scripts=False adds --ignore-scripts; min_release_age adds --min-release-age=<days>; and installs always include npm's standard non-interactive flags (--force --no-audit --no-fund --loglevel=error). puppeteer is special-cased to install both puppeteer and @puppeteer/browsers, and puppeteer-browsers resolves to @puppeteer/browsers. Explicit conflicting flags already present in install_args win over the derived defaults. get_version / get_abspath fall back to parsing npm show --json <package> and npm list --json --depth=0 output when the console script can't report its own version.PnpmProvider (pnpm)Source: abxpkg/binprovider_pnpm.py • Tests: tests/test_pnpmprovider.py
INSTALLER_BIN = "pnpm"
PATH = "" # auto-built from pnpm local + global bin dirs
install_root = None # None = global install, Path(...) = prefix/project root
install_root=None installs globally. Set install_root=Path(...) to install under <prefix>/node_modules/.bin; that prefix bin dir becomes the provider's active executable search path.pnpm directly. Honors PNPM_BINARY=/abs/path/to/pnpm. Use NpmProvider for npm.dry_run: shared behavior.min_release_age and postinstall_scripts=False, and hydrates their provider defaults from ABXPKG_MIN_RELEASE_AGE and ABXPKG_POSTINSTALL_SCRIPTS. min_release_age requires pnpm 10.16+, and supports_min_release_age() returns False on older hosts (then it logs a warning and continues).install_args is passed as pnpm package specs; unpinned specs get rewritten to pkg@>=<min_version> when min_version is supplied.--min-release-age CLI flag; this provider passes --config.minimumReleaseAge=<minutes> (the camelCase / kebab-case form pnpm exposes via its --config.<key>=<value> override). Installs always include --loglevel=error, and PNPM_HOME is auto-populated so pnpm add -g works without polluting the user's shell config. puppeteer is special-cased to install both puppeteer and @puppeteer/browsers, and puppeteer-browsers resolves to @puppeteer/browsers.YarnProvider (yarn)Source: abxpkg/binprovider_yarn.py • Tests: tests/test_yarnprovider.py
INSTALLER_BIN = "yarn"
PATH = "" # prepends <install_root>/node_modules/.bin
install_root = None # project dir, defaults to ABXPKG_YARN_ROOT or ABXPKG_LIB_DIR/yarn
install_root=Path(...) for an isolated project dir; that directory is auto-initialized with a stub package.json and .yarnrc.yml (nodeLinker: node-modules so binaries land in <install_root>/node_modules/.bin). When unset, the provider relies on $ABXPKG_YARN_ROOT or $ABXPKG_LIB_DIR/yarn; if neither is configured, the provider is unavailable.YARN_BINARY=/abs/path/to/yarn. Both Yarn classic (1.x) and Yarn Berry (2+) work for basic install/update/uninstall, but only Yarn 4.10+ supports the security flags.dry_run: shared behavior.min_release_age and postinstall_scripts=False, and hydrates their provider defaults from ABXPKG_MIN_RELEASE_AGE and ABXPKG_POSTINSTALL_SCRIPTS. Both controls require Yarn 4.10+; on older hosts supports_min_release_age() / supports_postinstall_disable() return False and explicit values are logged-and-ignored.install_args is passed as Yarn package specs; unpinned specs get rewritten to pkg@>=<min_version> when min_version is supplied.--ignore-scripts / --minimum-release-age CLI flags; the provider writes npmMinimalAgeGate: 7d (or whatever days value is configured) and enableScripts: false into <install_root>/.yarnrc.yml and additionally passes --mode skip-build to yarn add / yarn up when postinstall_scripts=False. Updates use yarn up <pkg> (Berry) or yarn upgrade <pkg> (classic). YARN_GLOBAL_FOLDER and YARN_CACHE_FOLDER are pointed at the provider cache dir so installs share a single cache across workspaces. puppeteer is special-cased to install both puppeteer and @puppeteer/browsers, and puppeteer-browsers resolves to @puppeteer/browsers.BunProvider (bun)Source: abxpkg/binprovider_bun.py • Tests: tests/test_bunprovider.py
INSTALLER_BIN = "bun"
PATH = "" # prepends <install_root>/bin
install_root = None # mirrors $BUN_INSTALL, None = ~/.bun (host-default)
install_root=None writes into the host $BUN_INSTALL (default ~/.bun). Set install_root=Path(...) to install under <install_root>/bin; the provider also creates <install_root>/install/global for the global node_modules dir, which is where bun puts the actual package state. The bin dir becomes the provider's active executable search path.BUN_BINARY=/abs/path/to/bun.dry_run: shared behavior.min_release_age and postinstall_scripts=False, and hydrates their provider defaults from ABXPKG_MIN_RELEASE_AGE and ABXPKG_POSTINSTALL_SCRIPTS. min_release_age requires Bun 1.3+, and supports_min_release_age() returns False on older hosts.install_args is passed as Bun package specs; unpinned specs get rewritten to pkg@>=<min_version> when min_version is supplied.bun add -g (with --force as the update fallback). The provider passes --ignore-scripts for postinstall_scripts=False and --minimum-release-age=<seconds> (Bun's unit is seconds; this provider converts from days). puppeteer is special-cased to install both puppeteer and @puppeteer/browsers, and puppeteer-browsers resolves to @puppeteer/browsers. Explicit conflicting flags already present in install_args win over the derived defaults.DenoProvider (deno)Source: abxpkg/binprovider_deno.py • Tests: tests/test_denoprovider.py
INSTALLER_BIN = "deno"
PATH = "" # prepends <install_root>/bin
install_root = None # mirrors $DENO_INSTALL_ROOT, None = ~/.deno
install_root=None writes into the host $DENO_INSTALL_ROOT (default ~/.deno). Set install_root=Path(...) for a hermetic root with executables under <install_root>/bin; DENO_DIR is then derived as <install_root>/.cache.DENO_BINARY=/abs/path/to/deno.dry_run: shared behavior.min_release_age and postinstall_scripts=False / True, and hydrates their provider defaults from ABXPKG_MIN_RELEASE_AGE and ABXPKG_POSTINSTALL_SCRIPTS. min_release_age requires Deno 2.5+, and supports_min_release_age() returns False on older hosts.install_args is passed as deno install package specs and is auto-prefixed with npm: when an unqualified bare name is supplied. Already-qualified specs (npm:, jsr:, https://...) are passed through verbatim. Unpinned specs get rewritten to pkg@>=<min_version> when min_version is supplied.deno install -g --force --allow-all -n <bin_name> <pkg> because Deno's idiomatic update path is just a fresh global install. Deno's npm lifecycle scripts are opt-in (the opposite of npm), so the provider only adds --allow-scripts when postinstall_scripts=True. min_release_age is passed as --minimum-dependency-age=<minutes> (Deno's preferred unit; this provider converts from days). puppeteer is special-cased to install both puppeteer and @puppeteer/browsers, and puppeteer-browsers resolves to @puppeteer/browsers. DENO_TLS_CA_STORE=system is set so installs work on hosts with corporate / sandboxed CA bundles.BashProvider (bash)Source: abxpkg/binprovider_bash.py • Tests: tests/test_bashprovider.py
import os
from pathlib import Path
INSTALLER_BIN = "bash"
PATH = ""
lib_dir = Path(os.environ.get("ABXPKG_LIB_DIR", "~/.config/abx/lib")).expanduser()
install_root = Path(os.environ.get("ABXPKG_BASH_ROOT", lib_dir / "bash"))
bin_dir = install_root / "bin"
install_root for the state dir, and bin_dir for the executable output dir.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install, update, and uninstall.INSTALL_ROOT, BIN_DIR, BASH_INSTALL_ROOT, and BASH_BIN_DIR into the shell environment for those commands.CargoProvider (cargo)Source: abxpkg/binprovider_cargo.py • Tests: tests/test_cargoprovider.py
INSTALLER_BIN = "cargo"
PATH = "" # prepends cargo_root/bin and cargo_home/bin
cargo_root = None # set this for hermetic installs
install_root=Path(...) or cargo_root=Path(...) for isolated installs under <cargo_root>/bin; otherwise installs go through cargo_home.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args is passed to cargo install; min_version becomes cargo install --version >=....CARGO_HOME, CARGO_TARGET_DIR, and CARGO_INSTALL_ROOT when applicable.GemProvider (gem)Source: abxpkg/binprovider_gem.py • Tests: tests/test_gemprovider.py
from abxpkg.binprovider import DEFAULT_ENV_PATH
INSTALLER_BIN = "gem"
PATH = DEFAULT_ENV_PATH
install_root = None # defaults to $GEM_HOME or ~/.local/share/gem
bin_dir = None # defaults to <install_root>/bin
install_root, and optionally bin_dir, for hermetic installs; otherwise it uses $GEM_HOME or ~/.local/share/gem.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args maps to gem install ..., gem update ..., and gem uninstall ...; min_version becomes --version >=....GEM_HOME instead of the host default.GoGetProvider (goget)Source: abxpkg/binprovider_goget.py • Tests: tests/test_gogetprovider.py
from abxpkg.binprovider import DEFAULT_ENV_PATH
INSTALLER_BIN = "go"
PATH = DEFAULT_ENV_PATH
install_root = None # defaults to $GOPATH or ~/go
bin_dir = None # defaults to <install_root>/bin
install_root for the Go install tree, and optionally bin_dir for the executable dir; otherwise installs land in <install_root>/bin.go first, then installs Go through Apt or Homebrew if it is missing.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args is passed to go install ...; the default is ["<bin_name>@latest"].update() is just install() again. Version detection prefers go version -m <binary> and falls back to the generic version probe. The provider name is goget, not go_get.NixProvider (nix)Source: abxpkg/binprovider_nix.py • Tests: tests/test_nixprovider.py
import os
from pathlib import Path
INSTALLER_BIN = "nix"
PATH = "" # prepends <install_root>/bin
install_root = Path(os.environ.get("ABXPKG_NIX_PROFILE", "~/.nix-profile")).expanduser()
install_root=Path(...) for a custom profile.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args is passed to nix profile install ...; the default is [bin_name]. Search results use the explicit official nixpkgs-unstable channel archive instead of the host's Nix registry.DockerProvider (docker)Source: abxpkg/binprovider_docker.py • Tests: tests/test_dockerprovider.py
import os
from pathlib import Path
INSTALLER_BIN = "docker"
PATH = "" # prepends bin_dir
lib_dir = Path(os.environ.get("ABXPKG_LIB_DIR", "~/.config/abx/lib")).expanduser()
docker_root = Path(os.environ.get("ABXPKG_DOCKER_ROOT", lib_dir / "docker"))
bin_dir = docker_root / "bin"
install_root=Path(...) for the shim/metadata root or bin_dir=Path(...) for the shim dir directly.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args is a list of Docker image refs. The first item is treated as the main image and becomes the generated shim target.["<bin_name>:latest"]. install() / update() run docker pull, write metadata JSON, and create an executable wrapper that runs docker run .... Expects image refs as install args, typically via overrides on a Binary. It writes a local wrapper script for the binary and executes it via docker run ...; the binary version is parsed from the image tag, so semver-like tags work best.ChromeWebstoreProvider (chromewebstore)Source: abxpkg/binprovider_chromewebstore.py • Tests: tests/test_chromewebstoreprovider.py
import os
from pathlib import Path
INSTALLER_BIN = "node"
PATH = ""
lib_dir = Path(os.environ.get("ABXPKG_LIB_DIR", "~/.config/abx/lib")).expanduser()
install_root = Path(os.environ.get("ABXPKG_CHROMEWEBSTORE_ROOT", lib_dir / "chromewebstore"))
bin_dir = install_root / "extensions"
install_root for the extension cache root, and bin_dir for the unpacked extension output dir.dry_run: shared behavior.min_release_age is unsupported and is ignored with a warning if explicitly requested. postinstall_scripts=False is supported as a standard kwarg and ABXPKG_POSTINSTALL_SCRIPTS hydrates the provider default here, but there is no extra install-time toggle beyond the packaged JS helper this provider already uses.install_args are [webstore_id, "--name=<extension_name>"].chromewebstore_utils.js helper is used to download, unpack, and cache the extension, and the resolved binary path is the unpacked manifest.json. no_cache=True bypasses that metadata cache on the next install/update without deleting the unpacked extension tree.PuppeteerProvider (puppeteer)Source: abxpkg/binprovider_puppeteer.py • Tests: tests/test_puppeteerprovider.py
import os
from pathlib import Path
INSTALLER_BIN = "puppeteer-browsers"
PATH = ""
lib_dir = Path(os.environ.get("ABXPKG_LIB_DIR", "~/.config/abx/lib")).expanduser()
install_root = Path(os.environ.get("ABXPKG_PUPPETEER_ROOT", lib_dir / "puppeteer"))
bin_dir = install_root / "bin"
install_root for the root dir and bin_dir for symlinked executables. Leave it unset for ambient/global mode, where cache ownership stays with the host. INSTALLER_BINARY() intentionally resolves only an already-bootstrapped puppeteer-browsers CLI from the shared ABXPKG_LIB_DIR/npm bin dir, a custom provider-local npm prefix, or ambient PATH; it does not delegate to the generic cross-provider installer resolver.install_root is pinned, abxpkg manages <install_root>/cache end-to-end — it's exported as PUPPETEER_CACHE_DIR to every subprocess, used for --path= on puppeteer-browsers install / list, and uninstall() resolves the real browser directory via load() then rmtrees it. When install_root is unset the provider is in pure passthrough mode: the caller's ambient $PUPPETEER_CACHE_DIR (or the CLI's ~/.cache/puppeteer default) flows through to subprocesses unchanged, load() trusts whatever path puppeteer-browsers list reports, and uninstall() still rmtrees the real browser directory returned by load() — leaving any unrelated browsers in the shared cache alone.@puppeteer/browsers through NpmProvider and then uses that CLI for browser installs.dry_run: shared behavior.min_release_age is unsupported for browser installs and is ignored with a warning if explicitly requested. postinstall_scripts=False is supported for the underlying npm bootstrap path, and ABXPKG_POSTINSTALL_SCRIPTS hydrates the provider default here.install_args are passed through to @puppeteer/browsers install ..., with the provider appending --path=<cache_dir>. Installing puppeteer-browsers itself is treated as the CLI bootstrap case, not as a browser target.node and puppeteer-browsers as dependency cache entries when they are resolved through upstream providers.PlaywrightProvider (playwright)Source: abxpkg/binprovider_playwright.py • Tests: tests/test_playwrightprovider.py
from pathlib import Path
INSTALLER_BIN = "playwright"
PATH = ""
install_root = None # abxpkg-managed root dir for bin_dir / nested npm prefix
bin_dir = Path("/tmp/abxpkg-playwright/bin") # symlink dir when install_root is configured
euid = 0 # routes exec() through sudo-first-then-fallback
install_root to pin the abxpkg-managed root dir (where bin_dir symlinks and the nested npm prefix live). Leave it unset to let playwright use its own OS-default browsers path (~/.cache/ms-playwright on Linux etc.) — in that case abxpkg maintains no symlink dir or npm prefix at all, the playwright npm CLI bootstraps against the host's npm default, and load() returns the resolved executablePath() directly. bin_dir overrides the symlink directory when install_root is pinned. INSTALLER_BINARY() intentionally resolves only an already-bootstrapped playwright CLI from the shared ABXPKG_LIB_DIR/npm bin dir, a custom provider-local npm prefix, or ambient PATH; it does not delegate to the generic cross-provider installer resolver.install_root is pinned, abxpkg manages <install_root>/cache end-to-end — exported as PLAYWRIGHT_BROWSERS_PATH to every subprocess (including the env KEY=VAL -- ... wrapper used when we go through sudo), used to scope executablePath() hits on load(), and uninstall() resolves the real browser directory via load() then rmtrees it. When install_root is unset the provider is in pure passthrough mode: the caller's ambient $PLAYWRIGHT_BROWSERS_PATH (or playwright's ~/.cache/ms-playwright default on Linux) flows through to subprocesses unchanged, load() trusts whatever path executablePath() reports, and uninstall() still rmtrees the real browser directory returned by load().playwright npm package through NpmProvider, then runs playwright install --with-deps <install_args> against it. Resolves each installed browser's real executable via the playwright-core Node.js API (chromium.executablePath() etc.) and writes a symlink into bin_dir when one is configured.dry_run: shared behavior — the install handler short-circuits to a placeholder without touching the host.--with-deps installs system packages and requires root on Linux. euid defaults to 0, which routes every exec() call through the base BinProvider.exec sudo-first-then-fallback path — it tries sudo -n -- playwright install --with-deps ... first on non-root hosts, falls back to running the command directly if sudo fails or isn't available, and merges both stderr outputs into the final error if both attempts fail.min_release_age and postinstall_scripts=False are unsupported for browser installs and are ignored with a warning if explicitly requested.install_args are appended onto playwright install after playwright_install_args (defaults to ["--with-deps"]) and passed through verbatim — use whatever browser names / flags the playwright install CLI accepts (chromium, firefox, webkit, --no-shell, --only-shell, --force, etc.).update() bumps the playwright npm package in install_root first (via NpmProvider.update) so its pinned browser versions refresh, then re-runs playwright install --force <install_args> to pull any new browser builds. uninstall() resolves the browser's real install directory via playwright-core's executablePath(), walks up to the containing <bin_name>-<buildId>/ dir, and rmtrees that dir — in both managed and passthrough modes — because playwright uninstall itself has no per-browser argument and only drops unused browsers wholesale.PyinfraProvider (pyinfra)Source: abxpkg/binprovider_pyinfra.py • Tests: tests/test_pyinfraprovider.py
import os
from abxpkg.binprovider import DEFAULT_PATH
INSTALLER_BIN = "pyinfra"
PATH = os.environ.get("PATH", DEFAULT_PATH)
pyinfra_installer_module = "auto"
pyinfra_installer_kwargs = {}
installer_module="auto" resolves to operations.brew.packages on macOS and operations.server.packages on Linux.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args is the package list passed to the selected pyinfra operation.AnsibleProvider (ansible)Source: abxpkg/binprovider_ansible.py • Tests: tests/test_ansibleprovider.py
import os
from abxpkg.binprovider import DEFAULT_PATH
from abxpkg.binprovider_ansible import ANSIBLE_INSTALL_PLAYBOOK_TEMPLATE
INSTALLER_BIN = "ansible"
PATH = os.environ.get("PATH", DEFAULT_PATH)
ansible_installer_module = "auto"
ansible_playbook_template = ANSIBLE_INSTALL_PLAYBOOK_TEMPLATE
ansible-runner.installer_module="auto" resolves to community.general.homebrew on macOS and ansible.builtin.package on Linux.dry_run: shared behavior.min_release_age and postinstall_scripts=False are unsupported and are ignored with a warning if explicitly requested.install_args becomes the playbook loop input for the chosen Ansible module.BinaryRepresents a single binary dependency aka a package (e.g. wget, curl, ffmpeg). Each Binary can declare one or more BinProviders it supports, along with per-provider overrides.
Binarys implement the following interface:
load(), install(), update(), uninstall() -> Binarybinprovidersbinprovider / loaded_binproviderabspath / loaded_abspathabspaths / loaded_abspathsversion / loaded_versionsha256 / loaded_sha256mtime / loaded_mtimeeuid / loaded_euidBinary.install() and Binary.update() return a fresh loaded Binary. Binary.uninstall() returns a Binary with binprovider, abspath, version, sha256, mtime, and euid cleared after removal. Binary.load(), Binary.install(), and Binary.update() all enforce min_version consistently. All four lifecycle methods also accept no_cache=True to bypass cached/current-state checks.
from abxpkg import Binary, SemVer, env, brew
curl = Binary(
name="curl",
min_version=SemVer("7.0.0"),
binproviders=[env, brew],
).load()
print(curl.binprovider) # EnvProvider(...) or BrewProvider(...)
print(curl.abspath) # Path('/usr/local/bin/curl')
print(curl.version) # SemVer(7, 88, 1) or newer
print(curl.is_valid) # True
assert curl.is_valid
For reusable Binary subclasses with per-provider overrides, see Advanced Usage above.
SemVerfrom abxpkg import SemVer
### Example: Use the SemVer type directly for parsing & verifying version strings
SemVer.parse('Google Chrome 124.0.6367.208+beta_234. 234.234.123') # SemVer(124, 0, 6367)
SemVer.parse('2024.04.05') # SemVer(2024, 4, 5)
SemVer.parse('1.9+beta') # SemVer(1, 9, 0)
str(SemVer(1, 9, 0)) # '1.9.0'
These types are all meant to be used library-style to make writing your own apps easier.
e.g. you can use it to build things likeplaywright install --with-deps.
abxpkg uses uv for local development, dependency sync, linting, and tests.
checkout_dir="$(mktemp -d)"
trap 'rm -rf "$checkout_dir"' EXIT
git clone --depth=1 https://github.com/ArchiveBox/abxpkg "$checkout_dir"
cd "$checkout_dir"
# setup the venv and install packages
uv sync --all-extras
# run formatting/lint/type checks
uv run prek run --all-files
# Exercise representative core, environment, and provider behavior.
uv run pytest -s \
tests/test_semver.py \
tests/test_binary.py \
tests/test_envprovider.py \
tests/test_module_api.py
The mandatory per-file CI matrix runs the complete standard suite, the
host-mutating provider files, and every root_required and docker_required
file on equipped isolated runners.
# build distributions and validate the publish command without uploading
uv build
uv publish --dry-run dist/*
tests/.uv run pytest -s tests/test_npmprovider.py or a specific node like uv run pytest -s tests/test_npmprovider.py::TestNpmProvider::test_provider_dry_run_does_not_install_zx when iterating on one provider.Note: this package used to be called pydantic-pkgr, it was renamed to abxpkg on 2024-11-12.
Python
97.8%
Shell
1.6%