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:
2026-07-05 15:17:03 -07:00
parent add356dcf2
commit b9d38be707
2 changed files with 32 additions and 2 deletions

View File

@@ -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