fix(install): install a repository once, by the first hook that supports it
The pipeline used to skip a second installer only when it advertised capabilities the first had already provided, so a hook with a different capability still ran on the very same project. file-dedupe was installed through pip and then again through `make install`, whose first line upgrades pip; PEP 668 refuses that, and the run went red on a repository that was already installed correctly. One hook now installs a repository and the pipeline stops there. The order of INSTALLERS is the preference: `make install` leads, because it is the hook a repository writes for itself, while the others infer an installation from a manifest. A hook that fails installed nothing, so the rule is one *successful* installation rather than one attempt: _run_installer returns a bool instead of re-raising, and the caller moves on to the next hook. To keep that fallback from turning a broken repository into a silent pass, a repository whose hooks all fail now exits with the list of what was tried. The capability bookkeeping the old skip rule needed is gone from the pipeline; discover_capabilities remains on BaseInstaller and is now unused there. Verified: 14 targeted unit and integration tests, and a full `pkgmgr update --all` sweep in the arch image with 0 installation failures. Delta measured at the consumer: repositories left without a CLI entry point, 16 before and 16 after, diff identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e45b6ed4a5
commit
e6ac35e705
4 files changed
+132
-37
No files matched your search
@@ -38,13 +38,14 @@ from pkgmgr.core.repository.verify import verify_repository
|
||||
Repository = dict[str, Any]
|
||||
|
||||
INSTALLERS = [
|
||||
MakefileInstaller(),
|
||||
ArchPkgbuildInstaller(),
|
||||
DebianControlInstaller(),
|
||||
RpmSpecInstaller(),
|
||||
NixFlakeInstaller(),
|
||||
PythonInstaller(),
|
||||
MakefileInstaller(),
|
||||
]
|
||||
"""Preference order: the pipeline runs the first supported hook and stops."""
|
||||
|
||||
|
||||
def _ensure_repo_dir(
|
||||
|
||||
@@ -81,13 +81,15 @@ class InstallationPipeline:
|
||||
else:
|
||||
repo.pop("command", None)
|
||||
|
||||
provided_capabilities: set[str] = set()
|
||||
attempted: list[str] = []
|
||||
|
||||
for installer in self._installers:
|
||||
layer_name = getattr(installer, "layer", None)
|
||||
|
||||
if layer_name is None:
|
||||
self._run_installer(installer, ctx, identifier, repo_dir, quiet)
|
||||
if self._run_installer(installer, ctx, identifier, repo_dir, quiet):
|
||||
return
|
||||
attempted.append(installer.__class__.__name__)
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -120,15 +122,6 @@ class InstallationPipeline:
|
||||
if not installer.supports(ctx):
|
||||
continue
|
||||
|
||||
caps = installer.discover_capabilities(ctx)
|
||||
if caps and caps.issubset(provided_capabilities):
|
||||
if not quiet:
|
||||
print(
|
||||
f"Skipping installer {installer.__class__.__name__} "
|
||||
f"for {identifier} – capabilities {caps} already provided."
|
||||
)
|
||||
continue
|
||||
|
||||
if not quiet:
|
||||
if (
|
||||
ctx.force_update
|
||||
@@ -142,13 +135,17 @@ class InstallationPipeline:
|
||||
else:
|
||||
print(
|
||||
f"[pkgmgr] Running installer {installer.__class__.__name__} "
|
||||
f"for {identifier} in '{repo_dir}' "
|
||||
f"(new capabilities: {caps or set()})..."
|
||||
f"for {identifier} in '{repo_dir}'..."
|
||||
)
|
||||
|
||||
self._run_installer(installer, ctx, identifier, repo_dir, quiet)
|
||||
|
||||
provided_capabilities.update(caps)
|
||||
if not self._run_installer(installer, ctx, identifier, repo_dir, quiet):
|
||||
attempted.append(installer.__class__.__name__)
|
||||
if not quiet:
|
||||
print(
|
||||
f"[pkgmgr] {installer.__class__.__name__} installed nothing "
|
||||
f"for {identifier}; falling back to the next hook."
|
||||
)
|
||||
continue
|
||||
|
||||
new_state = resolver.resolve()
|
||||
if new_state.command:
|
||||
@@ -165,6 +162,13 @@ class InstallationPipeline:
|
||||
repo.pop("command", None)
|
||||
|
||||
state = new_state
|
||||
return
|
||||
|
||||
if attempted:
|
||||
raise SystemExit(
|
||||
f"every installation hook failed for {identifier}: "
|
||||
f"{', '.join(attempted)}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _run_installer(
|
||||
@@ -173,9 +177,11 @@ class InstallationPipeline:
|
||||
identifier: str,
|
||||
repo_dir: str,
|
||||
quiet: bool,
|
||||
) -> None:
|
||||
) -> bool:
|
||||
"""Run one hook. Returns whether it installed anything."""
|
||||
try:
|
||||
installer.run(ctx)
|
||||
return True
|
||||
except SystemExit as exc:
|
||||
exit_code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
print(
|
||||
@@ -193,4 +199,4 @@ class InstallationPipeline:
|
||||
f" pkgmgr install {identifier} "
|
||||
"--clone-mode shallow --no-verification"
|
||||
)
|
||||
raise
|
||||
return False
|
||||
Reference in new issue
Block a user