Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ permissions:
id-token: write

concurrency:
group: "pages"
cancel-in-progress: false
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -49,6 +50,7 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
timeout-minutes: 20
steps:
- name: Deploy to GitHub Pages
id: deployment
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
release-state:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
precheck:
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -45,6 +50,7 @@ jobs:
discover-standard-tests:
needs: precheck
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
test-files: ${{ steps.set-matrix.outputs.test-files }}

Expand Down Expand Up @@ -75,6 +81,7 @@ jobs:
name: ${{ matrix.target.os }} py${{ matrix.target.python_version }} ${{ matrix.test.name }}
needs: [precheck, discover-standard-tests]
runs-on: ${{ matrix.target.os }}
timeout-minutes: 20
if: ${{ needs.discover-standard-tests.outputs.test-files != '[]' }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -204,6 +211,7 @@ jobs:
discover-live-tests:
needs: precheck
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
live-tests: ${{ steps.set-matrix.outputs.live-tests }}

Expand Down Expand Up @@ -245,6 +253,7 @@ jobs:
name: ${{ matrix.live.name }}
needs: [precheck, discover-live-tests]
runs-on: ${{ matrix.live.os }}
timeout-minutes: 20
if: ${{ needs.discover-live-tests.outputs.live-tests != '[]' }}
strategy:
fail-fast: false
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ venv/
.env
*.log
.DS_Store
uv.lock
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,36 @@ abx --binproviders=env,uv,pip,apt,brew yt-dlp # restrict provider resolution

Options before the binary name (`--lib`, `--binproviders`, `--dry-run`, `--debug`, `--no-cache`, `--update`) are forwarded to `abx-pkg`; everything after the binary name is forwarded to the binary itself.

#### Shebang Line in Scripts

Inspired by [`uv`'s inline script metadata](https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies), `abx-pkg` lets you declare **arbitrary package dependencies** at the top of any script using a `/// script` metadata block.

```javascript
#!/usr/bin/env -S abx-pkg run --script node

// /// script
// dependencies = [
// {name = "node", binproviders = ["env", "apt", "brew"], min_version = "22.0.0"},
// {name = "playwright", binproviders = ["pnpm", "npm"]},
// {name = "chromium", binproviders = ["playwright", "puppeteer", "apt"], min_version = "131.0.0"},
// ]
// [tool.abx-pkg]
// ABX_PKG_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();
})();
```

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.

#### Per-`Binary` / per-`BinProvider` options as CLI flags

Every [`Binary` / `BinProvider` configuration field](#configuration) 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.
Expand Down Expand Up @@ -373,8 +403,9 @@ class CargoProvider(BinProvider):
timeout: int | None = None,
) -> str:
install_args = install_args or self.get_install_args(bin_name)
installer = self._require_installer_bin()
proc = self.exec(bin_name=installer, cmd=['install', *install_args], timeout=timeout)
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()
Expand Down
12 changes: 5 additions & 7 deletions abx_pkg/base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@
# provider; explicit constructor kwargs override both.
DEFAULT_LIB_DIR: Path = user_config_path("abx") / "lib"

_lib_dir_env = os.environ.get("ABX_PKG_LIB_DIR", "").strip()
ABX_PKG_LIB_DIR: Path | None = (
Path(_lib_dir_env).expanduser().resolve() if _lib_dir_env else None
)


def abx_pkg_install_root_default(provider_name: str) -> Path | None:
"""Resolve a provider's default install root from env vars.
Expand All @@ -42,8 +37,11 @@ def abx_pkg_install_root_default(provider_name: str) -> Path | None:
specific = os.environ.get(f"ABX_PKG_{provider_name.upper()}_ROOT", "").strip()
if specific:
return Path(specific).expanduser().resolve()
if ABX_PKG_LIB_DIR is not None:
return ABX_PKG_LIB_DIR / provider_name
# Read from os.environ directly (not the cached module-level
# ABX_PKG_LIB_DIR) because the CLI sets it at runtime via --lib.
lib_dir = os.environ.get("ABX_PKG_LIB_DIR", "").strip()
if lib_dir:
return Path(lib_dir).expanduser().resolve() / provider_name
return None


Expand Down
Loading
Loading