Enhance Nix flake installer to remove old profile entries before installation and update unit tests accordingly.

Includes:
- Added best-effort removal of existing 'package-manager' Nix profile entry.
- Updated install logic to avoid file conflicts.
- Extended unit tests to verify removal behavior and SystemExit-safe handling.

Conversation reference: https://chatgpt.com/share/69332bc4-a128-800f-a69c-fdc24c4cc7fe
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-06 15:47:31 +01:00
parent aae852995e
commit 96a0409dbb
2 changed files with 89 additions and 9 deletions

View File

@@ -6,6 +6,12 @@ Installer for Nix flakes.
If a repository contains flake.nix and the 'nix' command is available, this
installer will try to install profile outputs from the flake.
Behavior:
- If flake.nix is present and `nix` exists on PATH:
* First remove any existing `package-manager` profile entry (best-effort).
* Then install the flake outputs (pkgmgr, default) via `nix profile install`.
- Any failure in `nix profile install` is treated as fatal (SystemExit).
"""
import os
@@ -20,6 +26,7 @@ class NixFlakeInstaller(BaseInstaller):
"""Install Nix flake profiles for repositories that define flake.nix."""
FLAKE_FILE = "flake.nix"
PROFILE_NAME = "package-manager"
def supports(self, ctx: RepoContext) -> bool:
"""
@@ -32,11 +39,39 @@ class NixFlakeInstaller(BaseInstaller):
flake_path = os.path.join(ctx.repo_dir, self.FLAKE_FILE)
return os.path.exists(flake_path)
def _ensure_old_profile_removed(self, ctx: RepoContext) -> None:
"""
Best-effort removal of an existing profile entry.
This handles the "already provides the following file" conflict by
removing previous `package-manager` installations before we install
the new one.
Any error in `nix profile remove` is intentionally ignored, because
a missing profile entry is not a fatal condition.
"""
if shutil.which("nix") is None:
return
# We do NOT use run_command here, because we explicitly want to ignore
# the failure of `nix profile remove` (e.g. entry not present).
# Using `|| true` makes this idempotent.
cmd = f"nix profile remove {self.PROFILE_NAME} || true"
# This will still respect preview mode inside run_command.
try:
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
except SystemExit:
# Ignore any error here: if the profile entry does not exist,
# that's fine and not a fatal condition.
pass
def run(self, ctx: RepoContext) -> None:
"""
Install Nix flake profile outputs (pkgmgr, default).
Any failure in `nix profile install` is treated as fatal (SystemExit).
The "already installed / file conflict" situation is avoided by
removing the existing profile entry beforehand.
"""
flake_path = os.path.join(ctx.repo_dir, self.FLAKE_FILE)
if not os.path.exists(flake_path):
@@ -46,7 +81,11 @@ class NixFlakeInstaller(BaseInstaller):
print("Warning: flake.nix found but 'nix' command not available. Skipping flake setup.")
return
print("Nix flake detected, attempting to install profile output...")
print("Nix flake detected, attempting to install profile outputs...")
# Handle the "already installed" case up-front:
self._ensure_old_profile_removed(ctx)
for output in ("pkgmgr", "default"):
cmd = f"nix profile install {ctx.repo_dir}#{output}"
try: