Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
565487f
Honor PUPPETEER_CACHE_DIR + PLAYWRIGHT_BROWSERS_PATH from ambient env
claude Apr 21, 2026
47a6ef5
apt/brew setup_PATH: rebuild when PATH is still empty
claude Apr 21, 2026
4fe3ef2
Playwright: separate browsers_path from install_root, honour env prec…
claude Apr 21, 2026
30c1e97
README: document PUPPETEER_CACHE_DIR / PLAYWRIGHT_BROWSERS_PATH prece…
claude Apr 21, 2026
08ab1ae
Playwright: rename browsers_path → browser_cache_dir for parity with …
claude Apr 21, 2026
2d52f0b
Puppeteer/Playwright: collapse browser_cache_dir → single writable ca…
claude Apr 21, 2026
76c80cf
Puppeteer/Playwright: drop cache_dir field, derive purely from instal…
claude Apr 21, 2026
061d436
Puppeteer/Playwright cache_dir: honour env var fallback when install_…
claude Apr 21, 2026
20b1d6f
Puppeteer/Playwright cache_dir: drop env-var fallback, go passthrough…
claude Apr 21, 2026
10c3759
Puppeteer/Playwright uninstall: resolve path via load() then rmtree
claude Apr 21, 2026
836123b
Puppeteer/Playwright: drop cache_dir entirely, inline install_root/cache
claude Apr 21, 2026
d0aebe8
All providers: never treat managed shims as source-of-truth for load()
claude Apr 21, 2026
5c8f9e4
Fix CI regressions from cache_dir removal + brew shim-first cleanup
claude Apr 21, 2026
c19901c
Puppeteer/Playwright uninstall: resolve path without round-tripping l…
claude Apr 21, 2026
1788b18
tests/test_playwrightprovider: assert browsers live under install_roo…
claude Apr 21, 2026
6885e3a
Puppeteer abspath_handler: guard _refresh_symlink when bin_dir is None
claude Apr 21, 2026
9be4a6c
Puppeteer/Playwright: auto-apply sandbox NO_PROXY default when ambien…
claude Apr 21, 2026
72c1825
Puppeteer _resolve_installed_browser_path: resolve alias via configur…
claude Apr 21, 2026
ef93382
Puppeteer _resolve_installed_browser_path: fall back to newest mtime …
claude Apr 22, 2026
c58da69
Puppeteer INSTALLER_BINARY: bootstrap from install_root/npm/node_modu…
claude Apr 22, 2026
85106ad
Merge branch 'main' into claude/magical-pascal-iR9iO
pirate Apr 22, 2026
38982f9
PlaywrightProvider: self-contain CLAUDE_SANDBOX_NO_PROXY + forward vi…
claude Apr 22, 2026
a0a4184
Merge branch 'main' into claude/magical-pascal-iR9iO
pirate Apr 22, 2026
b47f7bb
Puppeteer/Playwright _refresh_symlink: idempotent when shim already m…
claude Apr 22, 2026
c8ad32b
Merge branch 'claude/magical-pascal-iR9iO' of http://127.0.0.1:33764/…
claude Apr 22, 2026
7cbe281
All providers: make managed shim refresh idempotent
claude Apr 22, 2026
a464e53
PuppeteerProvider uninstall: forward install_args to browser-path res…
claude Apr 22, 2026
29d2fe5
tests/playwright: resolve shim target through shell-script shims on m…
claude Apr 22, 2026
57e14c4
macOS test fix: key _resolve_shim_target off is_symlink + switch Linu…
claude Apr 22, 2026
6d5bc99
CI: use runs-on group 'Default' so jobs can schedule on either GH-hos…
claude Apr 22, 2026
1134af0
CI precheck: install Node 22 so pyright hook has a modern runtime
claude Apr 22, 2026
ab8907a
CI: pin XDG_CACHE_HOME=/runner/root/.cache on self-hosted runners
claude Apr 22, 2026
4aa46ab
Revert "CI: pin XDG_CACHE_HOME=/runner/root/.cache on self-hosted run…
claude Apr 22, 2026
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 @@ -16,7 +16,8 @@ concurrency:

jobs:
build:
runs-on: ubuntu-latest
runs-on:
group: Default
timeout-minutes: 20
steps:
- name: Checkout
Expand Down Expand Up @@ -48,7 +49,8 @@ jobs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
runs-on:
group: Default
needs: build
timeout-minutes: 20
steps:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ concurrency:

jobs:
release-state:
runs-on: ubuntu-latest
runs-on:
group: Default
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
Expand Down
39 changes: 28 additions & 11 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ concurrency:

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

steps:
Expand All @@ -30,6 +31,11 @@ jobs:
with:
python-version: '3.12'

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '22'

- name: Install uv
uses: astral-sh/setup-uv@v8.0.0
with:
Expand All @@ -49,7 +55,8 @@ jobs:

discover-standard-tests:
needs: precheck
runs-on: ubuntu-latest
runs-on:
group: Default
timeout-minutes: 20
outputs:
test-files: ${{ steps.set-matrix.outputs.test-files }}
Expand Down Expand Up @@ -78,7 +85,7 @@ jobs:
echo "$json_array"

build:
name: ${{ matrix.target.os }} py${{ matrix.target.python_version }} ${{ matrix.test.name }}
name: ${{ matrix.target.os_name }} py${{ matrix.target.python_version }} ${{ matrix.test.name }}
needs: [precheck, discover-standard-tests]
runs-on: ${{ matrix.target.os }}
timeout-minutes: 20
Expand All @@ -88,11 +95,16 @@ jobs:
max-parallel: 20
matrix:
target:
- os: ubuntu-latest
- os:
group: Default
os_name: linux
python_version: '3.11'
- os: ubuntu-latest
- os:
group: Default
os_name: linux
python_version: '3.14'
- os: macOS-latest
os_name: macOS
python_version: '3.13'
test: ${{ fromJson(needs.discover-standard-tests.outputs.test-files) }}

Expand Down Expand Up @@ -219,7 +231,8 @@ jobs:

discover-live-tests:
needs: precheck
runs-on: ubuntu-latest
runs-on:
group: Default
timeout-minutes: 20
outputs:
live-tests: ${{ steps.set-matrix.outputs.live-tests }}
Expand All @@ -239,16 +252,20 @@ jobs:
needs_docker=true
fi

os_targets="ubuntu-latest macOS-latest"
os_targets="linux macOS"
if grep -q "@pytest.mark.docker_required" "$test_file" || grep -q "@pytest.mark.root_required" "$test_file" || grep -q 'skipif("darwin"' "$test_file"; then
os_targets="ubuntu-latest"
os_targets="linux"
elif grep -q 'require_tool("brew")' "$test_file" && ! grep -q 'require_tool("apt-get")' "$test_file" && ! grep -q "operations.apt.packages" "$test_file" && ! grep -q "ansible.builtin.apt" "$test_file"; then
os_targets="macOS-latest"
os_targets="macOS"
fi

for os_target in $os_targets; do
os_name=$(printf '%s' "$os_target" | tr '[:upper:]' '[:lower:]' | sed 's/-latest//')
entry="{\"name\":\"${test_name}-${os_name}\",\"path\":\"$test_file\",\"os\":\"$os_target\",\"needs_docker\":$needs_docker}"
if [ "$os_target" = "linux" ]; then
os_json='{"group":"Default"}'
else
os_json='"macOS-latest"'
fi
entry="{\"name\":\"${test_name}-${os_target}\",\"path\":\"$test_file\",\"os\":${os_json},\"os_name\":\"${os_target}\",\"needs_docker\":$needs_docker}"
if [ "$first" = true ]; then first=false; else json_array+=","; fi
json_array+="$entry"
done
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,8 @@ install_root = $ABXPKG_PUPPETEER_ROOT or $ABXPKG_LIB_DIR/puppeteer
bin_dir = <install_root>/bin
```

- Install root: set `install_root` for the root dir and `bin_dir` for symlinked executables. Downloaded browser artifacts live under `<install_root>/cache` when an install root is pinned. Leave it unset for ambient/global mode, where cache ownership stays with the host and `INSTALLER_BINARY` must already be resolvable from the ambient provider set.
- Install root: set `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 and `INSTALLER_BINARY` must already be resolvable from the ambient provider set.
- Browser cache: when `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.
- Auto-switching: bootstraps `@puppeteer/browsers` through `NpmProvider` and then uses that CLI for browser installs.
- `dry_run`: shared behavior.
- Security: `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.
Expand All @@ -1094,18 +1095,19 @@ Source: [`abxpkg/binprovider_playwright.py`](./abxpkg/binprovider_playwright.py)
```python
INSTALLER_BIN = "playwright"
PATH = ""
install_root = None # when set, doubles as PLAYWRIGHT_BROWSERS_PATH
install_root = None # abxpkg-managed root dir for bin_dir / nested npm prefix
bin_dir = <install_root>/bin # symlink dir for resolved browsers
euid = 0 # routes exec() through sudo-first-then-fallback
```

- Install root: set `install_root` to pin both the abxpkg root dir AND `PLAYWRIGHT_BROWSERS_PATH` to the same directory. 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.
- Install root: set `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.
- Browser cache: when `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()`.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
- Auto-switching: bootstraps the `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.
- Privilege handling: `--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.
- Security: `min_release_age` and `postinstall_scripts=False` are unsupported for browser installs and are ignored with a warning if explicitly requested.
- Overrides: `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.).
- Notes: `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()` removes the relevant `<bin_name>-*/` directories from `install_root` alongside the bin-dir symlink, since `playwright uninstall` only drops *unused* browsers on its own. Both `update()` and `uninstall()` leave playwright's OS-default cache untouched when `install_root` is unset.
- Notes: `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.

</details>

Expand Down
10 changes: 8 additions & 2 deletions abxpkg/binprovider_apt.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ class AptProvider(BinProvider):

def setup_PATH(self, no_cache: bool = False) -> None:
"""Populate PATH on first use from dpkg-discovered package runtime bin dirs, not from apt-get itself."""
if no_cache or (
self._INSTALLER_BINARY is None
# Rebuild PATH on first use, when the caller forces no_cache, or when
# PATH is still empty — the last case covers the "INSTALLER_BINARY was
# resolved out-of-band (hook preflight etc.), so _INSTALLER_BINARY is
# non-None but self.PATH was never populated" race.
if (
no_cache
or not self.PATH
or self._INSTALLER_BINARY is None
or self._INSTALLER_BINARY.loaded_abspath is None
):
dpkg_binary = EnvProvider().load("dpkg")
Expand Down
28 changes: 21 additions & 7 deletions abxpkg/binprovider_brew.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,29 @@ def _refresh_bin_link(
except Exception:
break
walk_path = walk_path.parent
# Idempotent refresh: skip when shim already points at target.
# Rewriting on every load() bumps mtime and churns the inode,
# which invalidates fingerprint caches unnecessarily.
if link_path.is_symlink():
try:
if link_path.readlink() == Path(target):
return TypeAdapter(HostBinPath).validate_python(link_path)
except OSError:
pass
if link_path.exists() or link_path.is_symlink():
link_path.unlink(missing_ok=True)
link_path.symlink_to(target)
return TypeAdapter(HostBinPath).validate_python(link_path)

def setup_PATH(self, no_cache: bool = False) -> None:
"""Populate PATH on first use from the resolved brew prefix and known runtime brew bin dirs."""
if no_cache or (
self._INSTALLER_BINARY is None
# Rebuild PATH on first use, when the caller forces no_cache, or when
# PATH is still empty — the last case covers provider copies that
# inherited a resolved ``_INSTALLER_BINARY`` but an unset ``PATH``.
if (
no_cache
or not self.PATH
or self._INSTALLER_BINARY is None
or self._INSTALLER_BINARY.loaded_abspath is None
):
install_root = self.install_root
Expand Down Expand Up @@ -391,12 +405,12 @@ def default_abspath_handler(
if not self.PATH:
return None

# Authoritative lookup: search brew's own Cellar / opt / PATH
# entries for the real formula binary. The managed ``bin_dir``
# shim is a convenience side-effect of install — never a source
# of truth — so we always consult brew's paths first and only
# refresh the shim to match the freshly-resolved target.
linked_bin = self._linked_bin_path(bin_name)
if linked_bin is not None:
linked_abspath = bin_abspath(bin_name, PATH=str(self.bin_dir))
if linked_abspath:
return linked_abspath

search_paths = self._brew_search_paths(bin_name, no_cache=no_cache)
abspath = bin_abspath(bin_name, PATH=search_paths)
if abspath:
Expand Down
9 changes: 9 additions & 0 deletions abxpkg/binprovider_goget.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,15 @@ def default_abspath_handler(
assert bin_dir is not None
link_path = bin_dir / str(bin_name)
link_path.parent.mkdir(parents=True, exist_ok=True)
# Idempotent refresh: skip when shim already points at target.
# Rewriting on every load() bumps mtime and churns the inode,
# which invalidates fingerprint caches unnecessarily.
if link_path.is_symlink():
try:
if link_path.readlink() == Path(direct_abspath):
return TypeAdapter(HostBinPath).validate_python(link_path)
except OSError:
pass
if link_path.exists() or link_path.is_symlink():
link_path.unlink(missing_ok=True)
link_path.symlink_to(direct_abspath)
Expand Down
9 changes: 9 additions & 0 deletions abxpkg/binprovider_npm.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,15 @@ def _refresh_bin_link(
link_path = self._linked_bin_path(bin_name)
assert link_path is not None, "_refresh_bin_link requires bin_dir to be set"
link_path.parent.mkdir(parents=True, exist_ok=True)
# Idempotent refresh: skip when shim already points at target.
# Rewriting on every load() bumps mtime and churns the inode,
# which invalidates fingerprint caches unnecessarily.
if link_path.is_symlink():
try:
if link_path.readlink() == Path(target):
return TypeAdapter(HostBinPath).validate_python(link_path)
except OSError:
pass
if link_path.exists() or link_path.is_symlink():
link_path.unlink(missing_ok=True)
link_path.symlink_to(target)
Expand Down
Loading
Loading