diff --git a/ebuild/build/dispatch.py b/ebuild/build/dispatch.py index 3cf77d5b..4b139065 100644 --- a/ebuild/build/dispatch.py +++ b/ebuild/build/dispatch.py @@ -11,6 +11,7 @@ import logging import subprocess +import shutil import sys from pathlib import Path from typing import Any, Dict, List, Optional @@ -90,6 +91,19 @@ def _run_or_log( return subprocess.run(cmd, check=check, cwd=cwd) +def ninja_command() -> list: + """How to invoke ninja on this machine. + + A real ninja on PATH is preferred; `python -m ninja` only works when the + PyPI `ninja` wheel is installed, so hardcoding it made a machine with + ninja properly installed fail with "No module named ninja". + """ + exe = shutil.which("ninja") + if exe: + return [exe] + return [sys.executable, "-m", "ninja"] + + class BackendDispatcher: """Dispatch configure/build/clean to external build systems. @@ -117,7 +131,7 @@ def configure( dry_run: If True, log commands instead of executing them. Raises: - ValueError: If the backend is not recognized. + RuntimeError: If the backend is not recognized. """ config = config or {} self.build_dir.mkdir(parents=True, exist_ok=True) @@ -164,7 +178,7 @@ def build( dry_run: If True, log commands instead of executing them. Raises: - ValueError: If the backend is not recognized. + RuntimeError: If the backend is not recognized. """ config = config or {} @@ -196,10 +210,8 @@ def build( else: raise RuntimeError( - f"BackendDispatcher cannot build backend '{backend}'. " - "Supported here: cargo, cmake, kbuild, make, meson. ebuild's own " - "ninja backend is generated and invoked by the CLI, not through " - "this dispatcher." + f"Unknown build backend '{backend}'. " + f"Supported backends: {', '.join(sorted(ALL_BACKENDS))}" ) def clean( @@ -215,9 +227,7 @@ def clean( dry_run: If True, log commands instead of executing them. Raises: - RuntimeError: If the backend is not recognized. configure() and - build() raise the same type for the same condition, so a - caller can guard all three with one ``except``. + RuntimeError: If the backend is not recognized. """ if backend == "cmake": _run_or_log( diff --git a/ebuild/build/ninja_backend.py b/ebuild/build/ninja_backend.py index 1282545e..dc49e5ee 100644 --- a/ebuild/build/ninja_backend.py +++ b/ebuild/build/ninja_backend.py @@ -32,7 +32,7 @@ class PackagePaths: def _shared_flag() -> str: - """The compiler flag that produces a shared object on this platform. + """The compiler flag that builds a shared object on this platform. macOS links dynamic libraries with -dynamiclib; ELF platforms use -shared. """ @@ -116,6 +116,9 @@ def _resolve_target_cflags(self, target) -> List[str]: def _object_path(self, target, src: str) -> Path: """Object file for one source within one target. + Called from two places and defined in neither, so every ninja build + raised AttributeError before this existed. + Namespaced by target name: a source shared by two targets must produce two distinct objects. Ninja rejects two edges writing the same output, and the targets may compile it with different cflags. @@ -216,8 +219,8 @@ def _write_ninja(self) -> None: else: # Shared libraries need the same -L/-l wiring executables # get, which the rule preamble alone does not supply. The - # "build a shared object" flag itself lives in the - # link_shared rule, so it must not be repeated here. + # shared-object flag itself lives in the link_shared rule, + # so it must not be repeated here. ldflags = list(target.ldflags) libs = [] for pkg_name in target.uses: diff --git a/tests/ebuild/test_dispatch.py b/tests/ebuild/test_dispatch.py index c6061472..fd49e359 100644 --- a/tests/ebuild/test_dispatch.py +++ b/tests/ebuild/test_dispatch.py @@ -78,7 +78,7 @@ def test_configure_unknown_raises(self, tmp_path): def test_build_unknown_raises(self, tmp_path): d = BackendDispatcher(tmp_path, tmp_path / "build") - with pytest.raises(RuntimeError, match="gradle"): + with pytest.raises(RuntimeError, match="Unknown build backend"): d.build("gradle") def test_clean_unknown_raises(self, tmp_path): diff --git a/tests/unit/test_ninja_backend.py b/tests/unit/test_ninja_backend.py index 66cdf9c9..6297aa6d 100644 --- a/tests/unit/test_ninja_backend.py +++ b/tests/unit/test_ninja_backend.py @@ -41,7 +41,7 @@ def test_shared_library_gets_shared_flag(self): shared_flag = "-dynamiclib" if sys.platform == "darwin" else "-shared" self.assertIn(shared_flag, ninja) # It must go through a compiler-driver rule, not the `ar` archiver. - # link_shared is that rule, and it carries the shared-object flag so + # link_shared is that rule, and it carries the shared-object flag, so # the flag is never repeated in the edge's ldflags. lib_line = next(line for line in ninja.splitlines() if "libmylib" in line and line.startswith("build")) self.assertIn(": link_shared ", lib_line) @@ -67,8 +67,8 @@ def test_static_library_unaffected(self): self.assertIn(": ar_rule", ninja) # The link_shared *rule* is always declared in the preamble, so the - # bare string "-shared" is present in every generated file. What must - # be absent is any build *edge* that uses it. + # bare string "-shared" appears in every generated file. What must be + # absent is any build *edge* that uses it. edges = [line for line in ninja.splitlines() if line.startswith("build ")] self.assertTrue(edges, "no build edges were generated") for edge in edges: