Skip to content
Open
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
10 changes: 5 additions & 5 deletions 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 the alias `cargo_root=Path(...)`) for isolated installs under `<install_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 Expand Up @@ -1116,8 +1116,8 @@ Source: [`abxpkg/binprovider_pyinfra.py`](./abxpkg/binprovider_pyinfra.py) • T
```python
INSTALLER_BIN = "pyinfra"
PATH = os.environ.get("PATH", DEFAULT_PATH)
pyinfra_installer_module = "auto"
pyinfra_installer_kwargs = {}
# installer_module defaults to "auto" (operations.brew.packages on macOS, operations.server.packages on Linux)
# installer_kwargs defaults to {}
```

- Install root: **no hermetic prefix support**. It delegates to host package managers through pyinfra operations.
Expand All @@ -1137,8 +1137,8 @@ Source: [`abxpkg/binprovider_ansible.py`](./abxpkg/binprovider_ansible.py) • T
```python
INSTALLER_BIN = "ansible"
PATH = os.environ.get("PATH", DEFAULT_PATH)
ansible_installer_module = "auto"
ansible_playbook_template = ANSIBLE_INSTALL_PLAYBOOK_TEMPLATE
# installer_module defaults to "auto" (community.general.homebrew on macOS, ansible.builtin.package on Linux)
# playbook_template defaults to ANSIBLE_INSTALL_PLAYBOOK_TEMPLATE
```

- Install root: **no hermetic prefix support**. It delegates to the host via `ansible-runner`.
Expand Down
2 changes: 0 additions & 2 deletions abxpkg/binprovider.py
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Moving dry_run check after handler call in uninstall() causes real file deletions during dry_run

The base class BinProvider.uninstall() previously short-circuited before calling the handler when self.dry_run is true (abxpkg/binprovider.py:2380, old code). This PR moved the check to after the handler runs at line 2410. While self.exec() has its own dry_run guard (line 1699), multiple providers' uninstall handlers perform direct file operations that bypass exec() and will now execute even during dry_run:

  • GoGet (abxpkg/binprovider_goget.py:255): Path(abspath).unlink() deletes the binary
  • Docker (abxpkg/binprovider_docker.py:280-281): deletes wrapper script and metadata JSON
  • Puppeteer (abxpkg/binprovider_puppeteer.py:702,707): deletes symlink and shutil.rmtree() on browser dir
  • Playwright (abxpkg/binprovider_playwright.py:726,736): deletes symlink and shutil.rmtree() on browser dirs
  • ChromeWebstore (abxpkg/binprovider_chromewebstore.py:264-269): deletes cache, CRX, and unpacked extension

None of these uninstall handlers have their own dry_run checks, unlike the Ansible/Pyinfra handlers that this PR explicitly patched. A provider.uninstall(bin_name, dry_run=True) call will now irreversibly delete files.

(Refers to lines 2410-2411)

Prompt for agents
The dry_run guard in BinProvider.uninstall() was moved from before _call_handler_for_action to after it (line 2410). This breaks dry_run for any provider whose uninstall handler performs direct file operations (Path.unlink, shutil.rmtree) instead of going through self.exec() (which has its own dry_run guard at line 1699). Affected providers: GoGet (binprovider_goget.py:242-264), Docker (binprovider_docker.py:261-291), Puppeteer (binprovider_puppeteer.py:690-708), Playwright (binprovider_playwright.py:716-737), ChromeWebstore (binprovider_chromewebstore.py:251-270). Fix either by (a) moving the dry_run check in BinProvider.uninstall() back before the handler call (between setup_PATH and the handler invocation, around line 2381), or (b) adding dry_run early-return checks to each of those five uninstall handlers, matching the pattern used for Ansible/Pyinfra in this PR.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -2126,8 +2126,6 @@ def install(
ACTIVE_EXEC_LOG_PREFIX.reset(exec_log_prefix_token)
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Dry-run checks are executed after handler calls, so install/update/uninstall still run provider code during dry runs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At abxpkg/binprovider.py, line 2128:

<comment>Dry-run checks are executed after handler calls, so install/update/uninstall still run provider code during dry runs.</comment>

<file context>
@@ -2146,6 +2125,19 @@ def install(
         finally:
             ACTIVE_EXEC_LOG_PREFIX.reset(exec_log_prefix_token)
 
+        if self.dry_run:
+            return ShallowBinary.model_construct(
+                name=bin_name,
</file context>
Fix with Cubic


if self.dry_run:
# return fake ShallowBinary if we're just doing a dry run
# no point trying to get real abspath or version if nothing was actually installed
return ShallowBinary.model_construct(
name=bin_name,
description=bin_name,
Expand Down
24 changes: 24 additions & 0 deletions abxpkg/binprovider_ansible.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,14 @@ def default_install_handler(
ansible_playbook = self.INSTALLER_BINARY(no_cache=no_cache).loaded_abspath
assert ansible_playbook

if self.dry_run:
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new dry-run check still runs after INSTALLER_BINARY(...), so dry runs execute provider-loading work before returning. Move the dry-run short-circuit before resolving ansible_playbook.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At abxpkg/binprovider_ansible.py, line 376:

<comment>The new dry-run check still runs after `INSTALLER_BINARY(...)`, so dry runs execute provider-loading work before returning. Move the dry-run short-circuit before resolving `ansible_playbook`.</comment>

<file context>
@@ -373,6 +373,14 @@ def default_install_handler(
         ansible_playbook = self.INSTALLER_BINARY(no_cache=no_cache).loaded_abspath
         assert ansible_playbook
 
+        if self.dry_run:
+            logger.info(
+                "DRY RUN (%s): ansible-playbook install %s",
</file context>
Fix with Cubic

logger.info(
"DRY RUN (%s): ansible-playbook install %s",
self.__class__.__name__,
" ".join(install_args),
)
return f"DRY RUN: would install {install_args} via ansible"

module_extra_kwargs = self.get_ansible_module_extra_kwargs()

return ansible_package_install(
Expand Down Expand Up @@ -401,6 +409,14 @@ def default_update_handler(
ansible_playbook = self.INSTALLER_BINARY(no_cache=no_cache).loaded_abspath
assert ansible_playbook

if self.dry_run:
logger.info(
"DRY RUN (%s): ansible-playbook update %s",
self.__class__.__name__,
" ".join(install_args),
)
return f"DRY RUN: would update {install_args} via ansible"

module_extra_kwargs = self.get_ansible_module_extra_kwargs()
if module_extra_kwargs:
return ansible_package_install(
Expand Down Expand Up @@ -439,6 +455,14 @@ def default_uninstall_handler(
ansible_playbook = self.INSTALLER_BINARY(no_cache=no_cache).loaded_abspath
assert ansible_playbook

if self.dry_run:
logger.info(
"DRY RUN (%s): ansible-playbook uninstall %s",
self.__class__.__name__,
" ".join(install_args),
)
return True

module_extra_kwargs = self.get_ansible_module_extra_kwargs()
if module_extra_kwargs:
ansible_package_install(
Expand Down
24 changes: 24 additions & 0 deletions abxpkg/binprovider_pyinfra.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,14 @@ def default_install_handler(
pyinfra_abspath = self.INSTALLER_BINARY(no_cache=no_cache).loaded_abspath
assert pyinfra_abspath

if self.dry_run:
logger.info(
"DRY RUN (%s): pyinfra install %s",
self.__class__.__name__,
" ".join(install_args),
)
return f"DRY RUN: would install {install_args} via pyinfra"

return pyinfra_package_install(
pkg_names=install_args,
pyinfra_abspath=str(pyinfra_abspath),
Expand All @@ -317,6 +325,14 @@ def default_update_handler(
pyinfra_abspath = self.INSTALLER_BINARY(no_cache=no_cache).loaded_abspath
assert pyinfra_abspath

if self.dry_run:
logger.info(
"DRY RUN (%s): pyinfra update %s",
self.__class__.__name__,
" ".join(install_args),
)
return f"DRY RUN: would update {install_args} via pyinfra"

return pyinfra_package_install(
pkg_names=install_args,
pyinfra_abspath=str(pyinfra_abspath),
Expand All @@ -342,6 +358,14 @@ def default_uninstall_handler(
pyinfra_abspath = self.INSTALLER_BINARY(no_cache=no_cache).loaded_abspath
assert pyinfra_abspath

if self.dry_run:
logger.info(
"DRY RUN (%s): pyinfra uninstall %s",
self.__class__.__name__,
" ".join(install_args),
)
return True

pyinfra_package_install(
pkg_names=install_args,
pyinfra_abspath=str(pyinfra_abspath),
Expand Down
Loading