diff --git a/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py b/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py index e0add4a..d575639 100755 --- a/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py +++ b/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py @@ -50,7 +50,9 @@ def main( # Use abxpkg AptProvider to install binary provider = AptProvider() - if not provider.INSTALLER_BIN_ABSPATH: + try: + provider.INSTALLER_BINARY() + except Exception: click.echo( "AptProvider.INSTALLER_BIN is not available on this host", err=True, @@ -78,7 +80,7 @@ def main( err=True, ) - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"apt install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/base/utils.py b/abx_plugins/plugins/base/utils.py index 7d65389..91c0a37 100755 --- a/abx_plugins/plugins/base/utils.py +++ b/abx_plugins/plugins/base/utils.py @@ -780,8 +780,13 @@ def enforce_lib_permissions(config_dir: Path | str | None = None) -> None: for fname in filenames: fp = dp / fname _chown_if_needed(fp, target_uid, target_gid) - # Preserve execute bit for binaries - current = fp.stat().st_mode + # Preserve execute bit for binaries. Use lstat() so dangling + # symlinks (which are valid, just unresolved) don't crash the + # permission enforcer; skip chmod for symlinks since chmod() + # would follow them and fail the same way. + current = fp.lstat().st_mode + if stat.S_ISLNK(current): + continue if current & stat.S_IXUSR: fp.chmod(0o755) # rwxr-xr-x (executable) else: @@ -799,9 +804,15 @@ def enforce_lib_permissions(config_dir: Path | str | None = None) -> None: for fname in filenames: fp = dp / fname _chown_if_needed(fp, target_uid, target_gid) + # Skip symlinks — chmod would follow them and raise on + # a dangling target, mirroring the lib/ tree walk above. + if stat.S_ISLNK(fp.lstat().st_mode): + continue fp.chmod(0o644) elif entry.is_file(): _chown_if_needed(entry, target_uid, target_gid) + if stat.S_ISLNK(entry.lstat().st_mode): + continue entry.chmod(0o644) diff --git a/abx_plugins/plugins/bash/on_BinaryRequest__14_bash.py b/abx_plugins/plugins/bash/on_BinaryRequest__14_bash.py index a357f60..f361b23 100755 --- a/abx_plugins/plugins/bash/on_BinaryRequest__14_bash.py +++ b/abx_plugins/plugins/bash/on_BinaryRequest__14_bash.py @@ -79,7 +79,7 @@ def main( sys.exit(1) try: - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"bash install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/bash/tests/test_bash_provider.py b/abx_plugins/plugins/bash/tests/test_bash_provider.py index 9e0997c..3964111 100755 --- a/abx_plugins/plugins/bash/tests/test_bash_provider.py +++ b/abx_plugins/plugins/bash/tests/test_bash_provider.py @@ -128,7 +128,7 @@ def test_hook_fails_for_missing_binary_after_command(self): # Should fail since binary not found after command assert result.returncode == 1 - assert "unable to load or install binary nonexistent_binary_xyz123" in ( + assert "unable to install binary nonexistent_binary_xyz123" in ( result.stderr.lower() ) diff --git a/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py b/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py index 712d3b0..9a1ff92 100755 --- a/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py +++ b/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py @@ -50,11 +50,8 @@ def main( # Use abxpkg BrewProvider to install binary provider = BrewProvider() - if not provider.INSTALLER_BIN_ABSPATH: - click.echo("brew not available on this system", err=True) - sys.exit(0) try: - Binary(name=provider.INSTALLER_BIN, binproviders=[provider]).load() + provider.INSTALLER_BINARY() except Exception: click.echo("brew not available on this system", err=True) sys.exit(0) @@ -79,7 +76,7 @@ def main( err=True, ) - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"brew install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py b/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py index 6239fbb..e751dfc 100755 --- a/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py +++ b/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py @@ -50,7 +50,9 @@ def main( sys.exit(0) provider = CargoProvider() - if not provider.INSTALLER_BIN_ABSPATH: + try: + provider.INSTALLER_BINARY() + except Exception: click.echo("cargo not available on this system", err=True) sys.exit(0) @@ -74,7 +76,7 @@ def main( err=True, ) - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"cargo install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py b/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py index 86cadcd..1bbf868 100755 --- a/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py +++ b/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py @@ -57,7 +57,7 @@ def main( ]: sys.exit(0) - provider = ChromeWebstoreProvider(extensions_dir=_extensions_dir()) + provider = ChromeWebstoreProvider(bin_dir=_extensions_dir()) if not provider.is_valid: click.echo("chromewebstore provider is not available on this host", err=True) sys.exit(0) @@ -72,7 +72,7 @@ def main( "min_version": min_version or extra_kwargs.get("min_version") or None, "overrides": json.loads(overrides) if overrides else {}, }, - ).load_or_install() + ).install() if not binary.abspath: click.echo(f"{name} not resolved as Chrome Web Store extension", err=True) diff --git a/abx_plugins/plugins/claudecode/config.json b/abx_plugins/plugins/claudecode/config.json index aeb2846..87b2354 100644 --- a/abx_plugins/plugins/claudecode/config.json +++ b/abx_plugins/plugins/claudecode/config.json @@ -22,6 +22,7 @@ "name": "{CLAUDECODE_BINARY}", "binproviders": "env,npm", "min_version": null, + "postinstall_scripts": true, "overrides": { "npm": { "install_args": [ diff --git a/abx_plugins/plugins/defuddle/tests/test_defuddle.py b/abx_plugins/plugins/defuddle/tests/test_defuddle.py index 5ad4e20..c6f3ac8 100755 --- a/abx_plugins/plugins/defuddle/tests/test_defuddle.py +++ b/abx_plugins/plugins/defuddle/tests/test_defuddle.py @@ -62,7 +62,7 @@ def get_defuddle_binary_path() -> str | None: name="defuddle", binproviders=[NpmProvider(), EnvProvider()], overrides={"npm": {"install_args": ["defuddle"]}}, - ).load_or_install() + ).install() if binary and binary.abspath: _defuddle_binary_path = str(binary.abspath) return _defuddle_binary_path diff --git a/abx_plugins/plugins/forumdl/tests/test_forumdl.py b/abx_plugins/plugins/forumdl/tests/test_forumdl.py index 7bc980c..85c3efe 100755 --- a/abx_plugins/plugins/forumdl/tests/test_forumdl.py +++ b/abx_plugins/plugins/forumdl/tests/test_forumdl.py @@ -74,7 +74,7 @@ def get_forumdl_binary_path() -> str | None: ], }, }, - ).load_or_install() + ).install() if binary and binary.abspath: _forumdl_binary_path = str(binary.abspath) return _forumdl_binary_path diff --git a/abx_plugins/plugins/gallerydl/tests/test_gallerydl.py b/abx_plugins/plugins/gallerydl/tests/test_gallerydl.py index 6e79f10..b8c2ace 100755 --- a/abx_plugins/plugins/gallerydl/tests/test_gallerydl.py +++ b/abx_plugins/plugins/gallerydl/tests/test_gallerydl.py @@ -55,7 +55,7 @@ def get_gallerydl_binary_path() -> str | None: binary = Binary( name="gallery-dl", binproviders=[PipProvider(), EnvProvider()], - ).load_or_install() + ).install() if binary and binary.abspath: _gallerydl_binary_path = str(binary.abspath) return _gallerydl_binary_path diff --git a/abx_plugins/plugins/liteparse/tests/test_liteparse.py b/abx_plugins/plugins/liteparse/tests/test_liteparse.py index f5a7f8e..05b1233 100755 --- a/abx_plugins/plugins/liteparse/tests/test_liteparse.py +++ b/abx_plugins/plugins/liteparse/tests/test_liteparse.py @@ -51,7 +51,7 @@ def get_liteparse_binary_path() -> str | None: name="lit", binproviders=[NpmProvider(), EnvProvider()], overrides={"npm": {"install_args": ["@llamaindex/liteparse"]}}, - ).load_or_install() + ).install() if binary and binary.abspath: _liteparse_binary_path = str(binary.abspath) return _liteparse_binary_path diff --git a/abx_plugins/plugins/mercury/tests/test_mercury.py b/abx_plugins/plugins/mercury/tests/test_mercury.py index 8c90990..b195a43 100755 --- a/abx_plugins/plugins/mercury/tests/test_mercury.py +++ b/abx_plugins/plugins/mercury/tests/test_mercury.py @@ -62,7 +62,7 @@ def get_mercury_binary_path() -> str | None: name="postlight-parser", binproviders=[NpmProvider(), EnvProvider()], overrides={"npm": {"install_args": ["@postlight/parser"]}}, - ).load_or_install() + ).install() if binary and binary.abspath: _mercury_binary_path = str(binary.abspath) return _mercury_binary_path diff --git a/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py b/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py index c2f1f31..7282a84 100755 --- a/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py +++ b/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py @@ -61,8 +61,10 @@ def main( npm_prefix.mkdir(parents=True, exist_ok=True) # Use abxpkg NpmProvider to install binary with custom prefix - provider = NpmProvider(npm_prefix=npm_prefix) - if not provider.INSTALLER_BIN_ABSPATH: + provider = NpmProvider(install_root=npm_prefix) + try: + provider.INSTALLER_BINARY() + except Exception: click.echo("npm not available on this system", err=True) sys.exit(0) @@ -96,7 +98,7 @@ def main( os.environ["PUPPETEER_SKIP_DOWNLOAD"] = "true" os.environ["PUPPETEER_SKIP_CHROMIUM_DOWNLOAD"] = "true" - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"npm install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/opendataloader/tests/test_opendataloader.py b/abx_plugins/plugins/opendataloader/tests/test_opendataloader.py index aa4a851..1e541d3 100755 --- a/abx_plugins/plugins/opendataloader/tests/test_opendataloader.py +++ b/abx_plugins/plugins/opendataloader/tests/test_opendataloader.py @@ -47,7 +47,7 @@ def get_opendataloader_binary_path() -> str | None: name="opendataloader-pdf", binproviders=[PipProvider(), EnvProvider()], overrides={"pip": {"install_args": ["opendataloader-pdf"]}}, - ).load_or_install() + ).install() if binary and binary.abspath: _opendataloader_binary_path = str(binary.abspath) return _opendataloader_binary_path @@ -83,7 +83,7 @@ def get_java_binary_path() -> str | None: "brew": {"install_args": ["openjdk"]}, "apt": {"install_args": ["default-jre"]}, }, - ).load_or_install() + ).install() if binary and binary.abspath: _java_binary_path = str(binary.abspath) return _java_binary_path diff --git a/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py b/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py index c90fdc3..9be4c7c 100755 --- a/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py +++ b/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py @@ -169,8 +169,10 @@ def main( _seed_pip_venv(pip_venv_path, preferred_python) # Use abxpkg PipProvider to install binary with custom venv - provider = PipProvider(pip_venv=pip_venv_path) - if not provider.INSTALLER_BIN_ABSPATH: + provider = PipProvider(install_root=pip_venv_path) + try: + provider.INSTALLER_BINARY() + except Exception: click.echo("pip not available on this system", err=True) sys.exit(0) @@ -197,7 +199,7 @@ def main( err=True, ) - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"pip install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/puppeteer/config.json b/abx_plugins/plugins/puppeteer/config.json index e39cd03..11b6417 100644 --- a/abx_plugins/plugins/puppeteer/config.json +++ b/abx_plugins/plugins/puppeteer/config.json @@ -34,11 +34,6 @@ "type": "boolean", "default": true, "description": "Enable Puppeteer dependency installation during crawl setup" - }, - "PUPPETEER_CACHE_DIR": { - "type": "string", - "default": "", - "description": "Override the Puppeteer browser cache directory" } } } diff --git a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py index 1d94271..8e17d7a 100755 --- a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py +++ b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py @@ -67,18 +67,9 @@ def main( if not lib_dir: lib_dir = str(Path.home() / ".config" / "abx" / "lib") - configured_cache_dir = (config.PUPPETEER_CACHE_DIR or "").strip() - if configured_cache_dir: - browser_cache_dir = Path(configured_cache_dir).expanduser().resolve() - browser_cache_dir.mkdir(parents=True, exist_ok=True) - provider = PuppeteerProvider( - browser_cache_dir=browser_cache_dir, - browser_bin_dir=browser_cache_dir.parent / "bin", - ) - else: - install_root = (Path(lib_dir) / "puppeteer").resolve() - install_root.mkdir(parents=True, exist_ok=True) - provider = PuppeteerProvider(install_root=install_root) + install_root = (Path(lib_dir) / "puppeteer").resolve() + install_root.mkdir(parents=True, exist_ok=True) + provider = PuppeteerProvider(install_root=install_root) raw_overrides = json.loads(overrides) if overrides else {} if not isinstance(raw_overrides, dict): @@ -115,7 +106,7 @@ def main( "binproviders": [provider], "overrides": raw_overrides, }, - ).load_or_install() + ).install() except Exception as e: error_output = str(e) hint = _get_install_failure_hint(error_output) diff --git a/abx_plugins/plugins/readability/tests/test_readability.py b/abx_plugins/plugins/readability/tests/test_readability.py index c292eda..536fd8d 100755 --- a/abx_plugins/plugins/readability/tests/test_readability.py +++ b/abx_plugins/plugins/readability/tests/test_readability.py @@ -110,7 +110,7 @@ def get_readability_binary_path() -> str | None: "install_args": ["https://github.com/ArchiveBox/readability-extractor"], }, }, - ).load_or_install() + ).install() if binary and binary.abspath: _readability_binary_path = str(binary.abspath) return _readability_binary_path diff --git a/abx_plugins/plugins/ytdlp/tests/test_ytdlp.py b/abx_plugins/plugins/ytdlp/tests/test_ytdlp.py index 0fab289..58be8a8 100755 --- a/abx_plugins/plugins/ytdlp/tests/test_ytdlp.py +++ b/abx_plugins/plugins/ytdlp/tests/test_ytdlp.py @@ -103,7 +103,7 @@ def get_ytdlp_binary_path() -> str | None: name="yt-dlp", binproviders=[PipProvider(), EnvProvider()], overrides={"pip": {"install_args": ["yt-dlp[default]"]}}, - ).load_or_install() + ).install() if binary and binary.abspath: _ytdlp_binary_path = str(binary.abspath) return _ytdlp_binary_path @@ -118,7 +118,7 @@ def require_ffmpeg_binary() -> str: binary = Binary( name="ffmpeg", binproviders=[EnvProvider(), BrewProvider(), AptProvider()], - ).load_or_install() + ).install() assert binary and binary.abspath, ( "ffmpeg installation failed. ytdlp tests require a real ffmpeg binary." ) diff --git a/conftest.py b/conftest.py index 233d955..5d2e7cd 100755 --- a/conftest.py +++ b/conftest.py @@ -311,6 +311,20 @@ def install_claude_code_with_hooks() -> str: overrides = binary_record.get("overrides") if overrides: npm_cmd.append(f"--overrides={json.dumps(overrides)}") + # Forward any remaining top-level binary-record fields (e.g. + # ``postinstall_scripts``, ``min_release_age``) so the npm hook + # sees them as Binary kwargs via ``parse_extra_hook_args``. + _forwarded = { + key: value + for key, value in binary_record.items() + if key not in {"name", "binproviders", "overrides", "min_version"} + and value is not None + } + for key, value in _forwarded.items(): + flag = "--" + key.replace("_", "-") + npm_cmd.append( + f"{flag}={json.dumps(value) if not isinstance(value, str) else value}", + ) npm_result = subprocess.run( npm_cmd, diff --git a/pyproject.toml b/pyproject.toml index e09c1b4..3836125 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ addopts = ["-p", "abx_plugins.pytest_bootstrap", "-p", "no:cacheprovider"] [tool.uv] exclude-newer = "14 days" -exclude-newer-package = { abxpkg = "1 second"} +exclude-newer-package = { abxpkg = "1 second", abx-plugins = "1 second" } [tool.pyright] include = [