fix: system requirement check verifies the package, not a PATH command
`system_dependencies` holds apt PACKAGE names; checking `which <name>` only coincides with "installed" when the name equals its command (pandoc, rsync). Names like poppler-utils / texlive-latex-base / docker-compose-plugin have no binary of that name, so the functional? predicate wrongly reported them unmet. Fall back to `dpkg -s` (the real "installed" check) after the PATH fast-path.
This commit is contained in:
@@ -15,6 +15,7 @@ dependencies — env is generated *from* requirements, not the reverse.
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -134,10 +135,25 @@ def requirements_of(config: CastleConfig, dep_name: str) -> list[Requirement]:
|
||||
return out
|
||||
|
||||
|
||||
def _dpkg_installed(pkg: str) -> bool:
|
||||
"""Whether a dpkg package is installed (the authoritative 'installed' check on
|
||||
Debian/Ubuntu). Returns False where dpkg isn't available."""
|
||||
dpkg = shutil.which("dpkg")
|
||||
if not dpkg:
|
||||
return False
|
||||
r = subprocess.run([dpkg, "-s", pkg], capture_output=True, text=True)
|
||||
return r.returncode == 0 and "install ok installed" in r.stdout
|
||||
|
||||
|
||||
def _check(config: CastleConfig, req: Requirement) -> bool:
|
||||
"""Is a single requirement satisfied? (The check is fixed by its kind.)"""
|
||||
if req.kind == "system":
|
||||
return shutil.which(req.ref) is not None
|
||||
# `system_dependencies` holds PACKAGE names, not executables. A PATH lookup
|
||||
# only coincides with 'installed' when the package name equals its command
|
||||
# (pandoc, rsync) — for names like poppler-utils / texlive-latex-base /
|
||||
# docker-compose-plugin it never matches. Fast-path `which`, then ask the
|
||||
# package manager (the real meaning of 'installed').
|
||||
return shutil.which(req.ref) is not None or _dpkg_installed(req.ref)
|
||||
if req.kind == "deployment":
|
||||
return req.ref in config.deployments
|
||||
return True
|
||||
|
||||
@@ -91,13 +91,27 @@ def test_functional_predicate_reports_unmet(monkeypatch: pytest.MonkeyPatch) ->
|
||||
{"web": prog, "api": ProgramSpec(id="api")},
|
||||
{"web": _dep("web"), "api": _dep("api")},
|
||||
)
|
||||
monkeypatch.setattr(R.shutil, "which", lambda _: None) # nothing installed
|
||||
monkeypatch.setattr(R.shutil, "which", lambda _: None) # not on PATH
|
||||
monkeypatch.setattr(R, "_dpkg_installed", lambda _: False) # nor as a package
|
||||
m = R.build_model(cfg, check=True)
|
||||
web = next(n for n in m.nodes if n.name == "web")
|
||||
assert web.unmet == ["system:pandoc"] # deployment:api exists → satisfied
|
||||
assert web.functional is False
|
||||
|
||||
|
||||
def test_system_requirement_satisfied_by_package_not_on_path(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""A package name that isn't a command (poppler-utils) is satisfied when the
|
||||
package is installed — a PATH lookup alone would wrongly report it unmet."""
|
||||
prog = ProgramSpec(id="t", system_dependencies=["poppler-utils"])
|
||||
cfg = _cfg({"t": prog}, {"t": _dep("t")})
|
||||
monkeypatch.setattr(R.shutil, "which", lambda _: None) # no `poppler-utils` binary
|
||||
monkeypatch.setattr(R, "_dpkg_installed", lambda pkg: pkg == "poppler-utils")
|
||||
t = next(n for n in R.build_model(cfg, check=True).nodes if n.name == "t")
|
||||
assert t.functional is True and t.unmet == []
|
||||
|
||||
|
||||
def test_missing_deployment_requirement_is_unmet() -> None:
|
||||
prog = ProgramSpec(id="web", requires=[Requirement(kind="deployment", ref="ghost")])
|
||||
cfg = _cfg({"web": prog}, {"web": _dep("web")})
|
||||
|
||||
Reference in New Issue
Block a user