Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,7 @@ PATH = "" # prepends cargo_root/bin and cargo_home/bi
cargo_root = None # set this for hermetic installs
```

- Install root: set `install_root=Path(...)` or `install_root=Path(...)` for isolated installs under `<cargo_root>/bin`; otherwise installs go through `cargo_home`.
- Install root: set `install_root=Path(...)` or `cargo_root=Path(...)` for isolated installs under `<cargo_root>/bin`; otherwise installs go through `cargo_home`.
- Auto-switching: none.
- `dry_run`: shared behavior.
- Security: `min_release_age` and `postinstall_scripts=False` are unsupported and are ignored with a warning if explicitly requested.
Expand Down
35 changes: 35 additions & 0 deletions abxpkg/binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,41 @@ def load(
)
raise BinaryLoadError(self.name, provider_names, errors) from inner_exc

@validate_call
@log_method_call(include_result=True)
def load_or_install(
self,
binproviders: list[BinProviderName] | None = None,
no_cache: bool = False,
dry_run: bool | None = None,
postinstall_scripts: bool | None = None,
min_release_age: float | None = None,
**extra_overrides,
) -> Self:
"""Try ``load()`` first; fall back to ``install()`` if no provider
resolves the binary. Returns the loaded/installed copy of ``self``.
"""
try:
loaded = self.load(
binproviders=binproviders,
no_cache=no_cache,
**extra_overrides,
)
except BinaryLoadError:
loaded = self
# ``load()`` was already called with the caller's ``no_cache`` flag, so
# a valid result is already fresh — trust it and skip ``install()``.
if loaded.is_valid:
return loaded
return self.install(
binproviders=binproviders,
no_cache=no_cache,
dry_run=dry_run,
postinstall_scripts=postinstall_scripts,
min_release_age=min_release_age,
**extra_overrides,
)

@validate_call
@log_method_call(include_result=True)
def update(
Expand Down
10 changes: 9 additions & 1 deletion abxpkg/binprovider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1732,10 +1732,18 @@ def drop_privileges():
sudo_proc.stderr,
)

# When running as root but dropping to a non-root user (e.g. brew),
# use the target user's HOME/LOGNAME/USER env so the dropped-privilege
# subprocess finds its own cache/config dirs instead of root's.
dropped_env = (
sudo_env
if current_euid == 0 and run_as_uid != current_euid
else fallback_env
)
proc = subprocess.run(
cmd,
cwd=str(cwd_path),
env=fallback_env,
env=dropped_env,
preexec_fn=drop_privileges,
**kwargs,
)
Expand Down
20 changes: 20 additions & 0 deletions abxpkg/binprovider_brew.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,26 @@ 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)
# When running as root but brew itself drops to its owner uid via
# ``self.exec``, we also need the managed shim dir (and its parents)
# traversable by that target uid so version probes / load() calls from
# the dropped-privilege subprocess can reach the symlink.
current_euid = os.geteuid()
target_uid = self.EUID
if current_euid == 0 and target_uid not in (0, current_euid):
try:
pw_record = self.get_pw_record(target_uid)
os.chown(link_path.parent, target_uid, pw_record.pw_gid)
except Exception:
pass
walk_path = link_path.parent
while walk_path != walk_path.parent:
try:
mode = walk_path.stat().st_mode
walk_path.chmod(mode | 0o055)
except Exception:
break
walk_path = walk_path.parent
if link_path.exists() or link_path.is_symlink():
link_path.unlink(missing_ok=True)
link_path.symlink_to(target)
Expand Down
9 changes: 8 additions & 1 deletion abxpkg/binprovider_puppeteer.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class PuppeteerProvider(BinProvider):
# Only set in managed mode: setup()/default_abspath_handler() use it to expose stable
# browser launch shims under ``<install_root>/bin``; global mode leaves it unset.
bin_dir: Path | None = None
# Explicit override for the directory browsers get downloaded into. When
# unset, cache_dir defaults to ``<install_root>/cache``; when set, it wins
# so callers can point ``PUPPETEER_CACHE_DIR`` at an arbitrary path.
browser_cache_dir: Path | None = None

@computed_field
@property
Expand All @@ -80,7 +84,10 @@ def supports_postinstall_disable(self, action, no_cache: bool = False) -> bool:
@computed_field
@property
def cache_dir(self) -> Path | None:
# Browser downloads always live under ``install_root/cache`` when we
# Explicit override wins so PUPPETEER_CACHE_DIR can be any path.
if self.browser_cache_dir is not None:
return self.browser_cache_dir
# Otherwise browser downloads live under ``install_root/cache`` when we
# manage an install root; global mode leaves cache ownership to the host.
if self.install_root is not None:
return self.install_root / "cache"
Expand Down
Loading