Files
voculinux/scripts/check_build_deps.py
T
xavierk 80a3792576
CI / security (pull_request) Failing after 1m25s
CI / lint (pull_request) Failing after 34s
CI / ai-review (pull_request) Successful in 25s
chore: import vocalinux project
Import the vocalinux source tree (tray app, Next.js site in web/,
packaging, installers, docs and tests) on top of the Gitea template
commit. The project README replaces the template README, and the
template CI files (.gitea/workflows/ci.yml, .jscpd.json, CI.md) are kept.
2026-11-06 11:51:44 +05:30

213 lines
7.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Fail fast when the native headers `just deps` needs are missing.
`uv sync` builds pyaudio, pycairo and PyGObject from source on Linux. Without
their development headers it fails deep inside a C compiler invocation, which
reads as a uv or dependency problem even though the fix is a distro package.
This check runs before the sync and names what is missing.
"""
from __future__ import annotations
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
VENV_PYTHON = ROOT / ".venv" / "bin" / "python"
# pkg-config modules the source builds query. Labels explain the module to a
# contributor who does not know which package owns it.
BUILD_MODULES = (("portaudio-2.0", "PortAudio headers (pyaudio)"),)
# PyGObject and its pycairo dependency are selected only on Linux by the
# sys_platform marker in pyproject.toml. They must not gate macOS tooling.
PYGOBJECT_BUILD_MODULES = (
("cairo", "cairo headers (pycairo)"),
("cairo-gobject", "cairo-gobject headers (PyGObject)"),
("girepository-2.0", "GObject Introspection headers (glib 2.80+)"),
("libffi", "libffi headers (PyGObject)"),
)
MIN_GIREPOSITORY_VERSION = "2.80"
APT_HINT = [
"Ubuntu 24.04+ / Debian 13+:",
" sudo apt install -y build-essential pkg-config libcairo2-dev portaudio19-dev \\",
" libgirepository-2.0-dev libffi-dev",
]
DEBIAN_12_NOTE = [
"",
"Debian 12 cannot build PyGObject 3.56 (glib is too old). Use ./install.sh --dev",
"there and run the suite with venv/bin/pytest.",
]
DNF_HINT = [
"Fedora 40+ / RHEL-based systems with glib 2.80+:",
" sudo dnf install -y gcc gcc-c++ make pkg-config cairo-devel \\",
" cairo-gobject-devel portaudio-devel glib2-devel \\",
" gobject-introspection-devel libffi-devel",
"If your system's glib is older than 2.80, use ./install.sh --dev instead.",
]
FEDORA_OLD_NOTE = [
"Fedora 39 and older cannot build PyGObject 3.56 (glib is too old).",
"Use ./install.sh --dev there and run the suite with venv/bin/pytest.",
]
PACMAN_HINT = [
"Arch / Manjaro:",
" sudo pacman -S --needed base-devel pkgconf cairo portaudio gobject-introspection",
]
ZYPPER_HINT = [
"openSUSE:",
" sudo zypper install -y gcc gcc-c++ make pkg-config cairo-devel \\",
" portaudio-devel gobject-introspection-devel libffi-devel",
]
GENERIC_HINT = [
"Install the development packages for PortAudio, cairo, GObject Introspection",
"(glib 2.80+) and libffi, plus a C compiler and pkg-config.",
]
MACOS_HINT = [
"macOS (Homebrew):",
" brew install portaudio pkg-config",
"Install the Xcode Command Line Tools if a C compiler is missing:",
" xcode-select --install",
]
def read_os_release(path: Path = Path("/etc/os-release")) -> dict[str, str]:
"""Parse os-release without executing it as shell."""
values: dict[str, str] = {}
try:
lines = path.read_text(encoding="utf-8").splitlines()
except OSError:
return values
for line in lines:
key, separator, value = line.partition("=")
if not separator or not key or key.startswith("#"):
continue
values[key] = value.strip().strip('"').strip("'")
return values
def install_hint(release: dict[str, str]) -> list[str]:
"""Return distro-specific install commands for the missing build headers."""
distro = release.get("ID", "").lower()
like = release.get("ID_LIKE", "").lower()
if distro == "ubuntu" or "ubuntu" in like:
return APT_HINT
if distro == "debian" or "debian" in like:
return [*APT_HINT, *DEBIAN_12_NOTE]
if distro in {"fedora", "rhel", "centos", "rocky", "almalinux"} or any(
family in like for family in ("fedora", "rhel")
):
if distro == "fedora" and release.get("VERSION_ID", "").isdigit():
if int(release["VERSION_ID"]) < 40:
return FEDORA_OLD_NOTE
return DNF_HINT
if distro in {"arch", "manjaro", "endeavouros"} or "arch" in like:
return PACMAN_HINT
if distro == "opensuse" or distro.startswith("opensuse-") or "suse" in like:
return ZYPPER_HINT
return GENERIC_HINT
def venv_has_compiled_packages() -> bool:
"""True when `.venv` has the native packages selected for this platform."""
if not VENV_PYTHON.is_file():
return False
packages = ("pyaudio",)
if sys.platform == "linux":
packages += ("pycairo", "pygobject")
check = "import importlib.metadata as metadata; " + "; ".join(
f"metadata.version({package!r})" for package in packages
)
result = subprocess.run(
[str(VENV_PYTHON), "-c", check],
capture_output=True,
check=False,
)
return result.returncode == 0
def missing_build_dependencies() -> list[str]:
"""Return the compiler or pkg-config modules uv cannot find, if any."""
missing: list[str] = []
if shutil.which("pkg-config") is None:
missing.append("pkg-config")
if not any(shutil.which(compiler) for compiler in ("cc", "gcc", "clang")):
missing.append("a C compiler")
# Module probes need pkg-config; report the missing tool instead of a list
# of modules the user cannot check yet.
if missing:
return missing
modules = BUILD_MODULES
if sys.platform == "linux":
modules += PYGOBJECT_BUILD_MODULES
for module, description in modules:
# PyGObject 3.56 requires girepository-2.0 >= 2.80; finding an older
# .pc file is not enough to build it successfully.
args = ["pkg-config", "--exists", module]
if module == "girepository-2.0":
args = ["pkg-config", f"--atleast-version={MIN_GIREPOSITORY_VERSION}", module]
result = subprocess.run(
args,
capture_output=True,
check=False,
)
if result.returncode != 0:
missing.append(description)
return missing
def main() -> int:
"""Print an actionable dependency report when the build cannot succeed."""
if venv_has_compiled_packages():
return 0
missing = missing_build_dependencies()
if not missing:
return 0
print("error: missing native build dependencies for .venv/", file=sys.stderr)
print(file=sys.stderr)
print(
"`just deps` builds native Python packages from source, so uv needs their",
file=sys.stderr,
)
print("development headers before it can create the environment.", file=sys.stderr)
print(file=sys.stderr)
print("Missing:", file=sys.stderr)
for item in missing:
print(f" - {item}", file=sys.stderr)
print(file=sys.stderr)
print("Install them for your system:", file=sys.stderr)
hints = MACOS_HINT if sys.platform == "darwin" else install_hint(read_os_release())
for line in hints:
print(line, file=sys.stderr)
print(file=sys.stderr)
print(
"See the Manual setup section in CONTRIBUTING.md for the full package list.",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
raise SystemExit(main())