Skip to content

Commit 2d52f0b

Browse files
committed
Puppeteer/Playwright: collapse browser_cache_dir → single writable cache_dir
Per maintainer feedback (#33): providers should expose only ``install_root``, ``cache_dir``, and ``bin_dir`` — no duplicate ``browser_cache_dir`` field. Refactor to match: - Drop the separate ``browser_cache_dir`` field on both providers. - Promote ``cache_dir`` from a computed property to a writable field with a ``default_factory`` that hydrates from ``PUPPETEER_CACHE_DIR`` / ``PLAYWRIGHT_BROWSERS_PATH``. - A ``@model_validator(mode="after")`` on each provider fills ``cache_dir = install_root/cache`` when ``cache_dir`` is still ``None`` but an ``install_root`` is pinned (preserving the previous default). - Precedence at validate time: 1. explicit ``cache_dir`` kwarg wins 2. else env var (PUPPETEER_CACHE_DIR / PLAYWRIGHT_BROWSERS_PATH) 3. else ``<install_root>/cache`` when an install root is pinned 4. else ``None`` (global / OS default) Uninstall + abspath checks: - ``PlaywrightProvider.default_uninstall_handler`` now iterates the resolved ``cache_dir`` (= ``PLAYWRIGHT_BROWSERS_PATH`` tree) instead of ``install_root`` when cleaning ``<browser>-*/`` dirs. - ``PlaywrightProvider.default_abspath_handler`` now scopes ``executablePath()`` hits to ``cache_dir.resolve()`` ancestry instead of ``install_root`` ancestry, so an explicit ``PLAYWRIGHT_BROWSERS_PATH`` outside ``install_root`` still satisfies ``load()``. - ``PuppeteerProvider`` already routed all cache ops through ``self.cache_dir`` — no behaviour change there, just field cleanup. README updated to reflect the collapsed API surface for both providers.
1 parent 08ab1ae commit 2d52f0b

3 files changed

Lines changed: 48 additions & 66 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,11 +1074,11 @@ INSTALLER_BIN = "puppeteer-browsers"
10741074
PATH = ""
10751075
install_root = $ABXPKG_PUPPETEER_ROOT or $ABXPKG_LIB_DIR/puppeteer
10761076
bin_dir = <install_root>/bin
1077-
browser_cache_dir = None # hydrated from PUPPETEER_CACHE_DIR env; overrides install_root/cache
1077+
cache_dir = <install_root>/cache # hydrated from PUPPETEER_CACHE_DIR env when install_root is unset
10781078
```
10791079

10801080
- 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.
1081-
- Cache dir / `PUPPETEER_CACHE_DIR`: browser downloads land in the directory resolved with three-tier precedence: explicit `browser_cache_dir` / `PUPPETEER_CACHE_DIR` env wins → else `<install_root>/cache` when an install root is pinned → else `None`. The resolved value is exported as `PUPPETEER_CACHE_DIR` to every subprocess the provider runs.
1081+
- Cache dir / `PUPPETEER_CACHE_DIR`: browser downloads land in `cache_dir`, resolved with four-tier precedence at validate time: explicit `cache_dir` kwarg wins → else `PUPPETEER_CACHE_DIR` env var → else `<install_root>/cache` when an install root is pinned → else `None`. The resolved value is exported as `PUPPETEER_CACHE_DIR` to every subprocess the provider runs.
10821082
- Auto-switching: bootstraps `@puppeteer/browsers` through `NpmProvider` and then uses that CLI for browser installs.
10831083
- `dry_run`: shared behavior.
10841084
- 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.
@@ -1097,12 +1097,12 @@ INSTALLER_BIN = "playwright"
10971097
PATH = ""
10981098
install_root = None # abxpkg-managed root dir for bin_dir / nested npm prefix
10991099
bin_dir = <install_root>/bin # symlink dir for resolved browsers
1100-
browser_cache_dir = None # hydrated from PLAYWRIGHT_BROWSERS_PATH env; overrides install_root/cache
1100+
cache_dir = <install_root>/cache # hydrated from PLAYWRIGHT_BROWSERS_PATH env when install_root is unset
11011101
euid = 0 # routes exec() through sudo-first-then-fallback
11021102
```
11031103

11041104
- 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.
1105-
- Cache dir / `PLAYWRIGHT_BROWSERS_PATH`: the actual browsers cache dir is resolved via the `cache_dir` computed property with three-tier precedence: explicit `browser_cache_dir` / `PLAYWRIGHT_BROWSERS_PATH` env wins → else `<install_root>/cache` when an install root is pinned → else `None` (let playwright pick its OS default). The final value is exported as `PLAYWRIGHT_BROWSERS_PATH` to every subprocess the provider runs.
1105+
- Cache dir / `PLAYWRIGHT_BROWSERS_PATH`: browser downloads land in `cache_dir`, resolved with four-tier precedence at validate time: explicit `cache_dir` kwarg wins → else `PLAYWRIGHT_BROWSERS_PATH` env var → else `<install_root>/cache` when an install root is pinned → else `None` (let playwright pick its OS default). The final value is exported as `PLAYWRIGHT_BROWSERS_PATH` to every subprocess the provider runs. `uninstall()` deletes matching `<browser>-*/` dirs from the resolved `cache_dir` (never from playwright's untouched OS-default cache when nothing is pinned).
11061106
- 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.
11071107
- `dry_run`: shared behavior — the install handler short-circuits to a placeholder without touching the host.
11081108
- 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.

abxpkg/binprovider_playwright.py

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
import sys
99
import platform
1010
from pathlib import Path
11+
from typing import Self
1112

12-
from pydantic import Field, TypeAdapter, computed_field
13+
from pydantic import Field, TypeAdapter, computed_field, model_validator
1314

1415
from .base_types import (
1516
BinName,
@@ -72,14 +73,14 @@ class PlaywrightProvider(BinProvider):
7273
# Only set in managed mode: setup()/default_abspath_handler() use it to create and read
7374
# stable browser shims under ``<install_root>/bin``; global mode leaves it unset.
7475
bin_dir: Path | None = None
75-
# Explicit override for the directory browsers get downloaded into.
76-
# Hydrated from ``PLAYWRIGHT_BROWSERS_PATH`` (playwright's own standard
77-
# env var) when unset so callers can pin the cache dir the same way they
78-
# would for a vanilla ``playwright install``. When both this and
79-
# ``install_root`` are unset, playwright falls back to its OS default; when
80-
# only ``install_root`` is set, ``cache_dir`` defaults to
81-
# ``<install_root>/cache`` (see ``ENV``).
82-
browser_cache_dir: Path | None = Field(
76+
# Where browsers get downloaded. Exported as ``PLAYWRIGHT_BROWSERS_PATH``
77+
# to every subprocess. Precedence at validate time:
78+
# 1. explicit ``cache_dir`` kwarg wins (more-specific override)
79+
# 2. else ``PLAYWRIGHT_BROWSERS_PATH`` env var wins (same semantics
80+
# for external callers invoking via env)
81+
# 3. else ``<install_root>/cache`` when an install root is pinned
82+
# 4. else ``None`` (global mode — let playwright pick its OS default)
83+
cache_dir: Path | None = Field(
8384
default_factory=lambda: (
8485
Path(os.environ["PLAYWRIGHT_BROWSERS_PATH"]).expanduser()
8586
if os.environ.get("PLAYWRIGHT_BROWSERS_PATH")
@@ -92,32 +93,18 @@ class PlaywrightProvider(BinProvider):
9293
# run as the normal user by default.
9394
euid: int | None = 0 if platform.system().lower() == "linux" else None
9495

96+
@model_validator(mode="after")
97+
def _fill_cache_dir_from_install_root(self) -> Self:
98+
if self.cache_dir is None and self.install_root is not None:
99+
self.cache_dir = self.install_root / "cache"
100+
return self
101+
95102
@computed_field
96103
@property
97104
def ENV(self) -> "dict[str, str]":
98-
resolved = self.cache_dir
99-
if resolved is None:
105+
if not self.cache_dir:
100106
return {}
101-
return {"PLAYWRIGHT_BROWSERS_PATH": str(resolved)}
102-
103-
@computed_field
104-
@property
105-
def cache_dir(self) -> Path | None:
106-
"""PLAYWRIGHT_BROWSERS_PATH precedence:
107-
108-
1. Explicit ``browser_cache_dir`` (field / ``PLAYWRIGHT_BROWSERS_PATH`` env)
109-
wins — the more specific cache-dir override always beats the less
110-
specific install-root.
111-
2. Else, when ``install_root`` is pinned, default to
112-
``<install_root>/cache`` so managed installs stay self-contained.
113-
3. Else leave it unset and let playwright pick its own OS-default
114-
browsers cache directory.
115-
"""
116-
if self.browser_cache_dir is not None:
117-
return self.browser_cache_dir
118-
if self.install_root is not None:
119-
return self.install_root / "cache"
120-
return None
107+
return {"PLAYWRIGHT_BROWSERS_PATH": str(self.cache_dir)}
121108

122109
def supports_min_release_age(self, action, no_cache: bool = False) -> bool:
123110
return False
@@ -605,14 +592,15 @@ def default_abspath_handler(
605592
)
606593
if not resolved:
607594
return None
608-
# When ``playwright_root`` is pinned, a hit from
609-
# ``executablePath()`` that points outside that install_root tree
610-
# (e.g. an ambient system install) should not satisfy
611-
# ``load()`` — otherwise an unrelated host-wide playwright
612-
# install would silently hijack resolution.
613-
if self.install_root is not None:
614-
root_real = self.install_root.resolve(strict=False)
615-
if root_real not in resolved.resolve(strict=False).parents:
595+
# When ``cache_dir`` is pinned (either explicitly, via
596+
# ``PLAYWRIGHT_BROWSERS_PATH``, or derived from ``install_root``),
597+
# an ``executablePath()`` hit that points outside that tree
598+
# (e.g. an ambient system install) should not satisfy ``load()``
599+
# — otherwise an unrelated host-wide playwright install would
600+
# silently hijack resolution.
601+
if self.cache_dir is not None:
602+
cache_real = self.cache_dir.resolve(strict=False)
603+
if cache_real not in resolved.resolve(strict=False).parents:
616604
return None
617605
if self.bin_dir is None:
618606
return resolved
@@ -763,11 +751,13 @@ def default_uninstall_handler(
763751
(self.bin_dir / bin_name).unlink(missing_ok=True)
764752

765753
# ``playwright uninstall`` only removes *unused* browsers from
766-
# the entire host, so drop the matching directories ourselves.
767-
# Only touch ``playwright_root`` if the caller pinned one — we
768-
# don't delete from playwright's own OS-default cache.
769-
if self.install_root is not None and self.install_root.is_dir():
770-
for entry in self.install_root.iterdir():
754+
# the entire host, so drop the matching directories ourselves
755+
# from the resolved ``cache_dir`` (= ``PLAYWRIGHT_BROWSERS_PATH``).
756+
# Only touch it if the caller pinned one explicitly or via
757+
# ``install_root`` — we don't delete from playwright's own
758+
# OS-default cache.
759+
if self.cache_dir is not None and self.cache_dir.is_dir():
760+
for entry in self.cache_dir.iterdir():
771761
if entry.is_dir() and entry.name.startswith(f"{bin_name}-"):
772762
logger.info("$ %s", format_command(["rm", "-rf", str(entry)]))
773763
shutil.rmtree(entry, ignore_errors=True)

abxpkg/binprovider_puppeteer.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@ class PuppeteerProvider(BinProvider):
6666
# Only set in managed mode: setup()/default_abspath_handler() use it to expose stable
6767
# browser launch shims under ``<install_root>/bin``; global mode leaves it unset.
6868
bin_dir: Path | None = None
69-
# Explicit override for the directory browsers get downloaded into.
70-
# Hydrated from ``PUPPETEER_CACHE_DIR`` (puppeteer's own standard env var)
71-
# when unset so callers can pin the cache dir the same way they would for
72-
# a vanilla ``puppeteer-browsers install``. When still unset, ``cache_dir``
73-
# falls back to ``<install_root>/cache``.
74-
browser_cache_dir: Path | None = Field(
69+
# Where browsers get downloaded. Precedence at validate time:
70+
# 1. explicit ``cache_dir`` kwarg wins (more-specific override)
71+
# 2. else ``PUPPETEER_CACHE_DIR`` env var wins (same semantics for
72+
# external callers invoking via env)
73+
# 3. else ``<install_root>/cache`` when an install root is pinned
74+
# 4. else ``None`` (global mode — let puppeteer-browsers pick its
75+
# own default cache)
76+
cache_dir: Path | None = Field(
7577
default_factory=lambda: (
7678
Path(os.environ["PUPPETEER_CACHE_DIR"]).expanduser()
7779
if os.environ.get("PUPPETEER_CACHE_DIR")
@@ -89,22 +91,12 @@ def ENV(self) -> "dict[str, str]":
8991
def supports_postinstall_disable(self, action, no_cache: bool = False) -> bool:
9092
return action in ("install", "update")
9193

92-
@computed_field
93-
@property
94-
def cache_dir(self) -> Path | None:
95-
# Explicit override wins so PUPPETEER_CACHE_DIR can be any path.
96-
if self.browser_cache_dir is not None:
97-
return self.browser_cache_dir
98-
# Otherwise browser downloads live under ``install_root/cache`` when we
99-
# manage an install root; global mode leaves cache ownership to the host.
100-
if self.install_root is not None:
101-
return self.install_root / "cache"
102-
return None
103-
10494
@model_validator(mode="after")
10595
def detect_euid_to_use(self) -> Self:
10696
if self.bin_dir is None and self.install_root is not None:
10797
self.bin_dir = self.install_root / "bin"
98+
if self.cache_dir is None and self.install_root is not None:
99+
self.cache_dir = self.install_root / "cache"
108100
return self
109101

110102
def setup_PATH(self, no_cache: bool = False) -> None:

0 commit comments

Comments
 (0)