Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ Desktop.ini
build-*/
node_modules/
target/

# ebuild build output (the default --build-dir)
_build/
44 changes: 29 additions & 15 deletions ebuild/build/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@
ALL_BACKENDS = {"cmake", "make", "meson", "cargo", "kbuild", "ninja"}


def ninja_command():
"""Return the argv prefix that runs ninja on this machine.

Prefer a `ninja` executable on PATH -- that is what a developer who
followed any ordinary install guide has, and what CMake and Meson already
use. Fall back to the `ninja` PyPI wheel only when no binary is present.

ebuild used to invoke `sys.executable -m ninja` unconditionally, so a
machine with ninja correctly installed still failed with "No module named
ninja" on the first build of a new project.
"""
import shutil

exe = shutil.which("ninja")
return [exe] if exe else [sys.executable, "-m", "ninja"]


def detect_backend(source_dir: Path) -> str:
"""Auto-detect the build system from project files.

Expand Down Expand Up @@ -121,22 +138,15 @@ def configure(
elif backend == "cargo":
pass # Cargo does not have a separate configure step

elif backend in ("make", "kbuild", "ninja"):
elif backend in ("make", "kbuild"):
pass # No separate configure step

else:
raise ValueError(
f"Unknown build backend '{backend}'. "
f"Supported backends: {', '.join(sorted(ALL_BACKENDS))}"
)

else:
raise RuntimeError(
f"BackendDispatcher cannot configure backend '{backend}'. "
"This dispatcher only handles cmake, meson, and cargo "
"(make/kbuild need no configure step). ebuild's own ninja "
"backend is invoked directly and requires 'targets' in "
"build.yaml -- add targets or choose another backend."
"backend is generated and invoked by the CLI, not here."
)

def build(
Expand Down Expand Up @@ -185,9 +195,11 @@ def build(
_run_or_log(cmd, dry_run)

else:
raise ValueError(
f"Unknown build backend '{backend}'. "
f"Supported backends: {', '.join(sorted(ALL_BACKENDS))}"
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."
)

def clean(
Expand All @@ -203,7 +215,9 @@ def clean(
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. configure() and
build() raise the same type for the same condition, so a
caller can guard all three with one ``except``.
"""
if backend == "cmake":
_run_or_log(
Expand Down Expand Up @@ -232,12 +246,12 @@ def clean(
)
elif backend == "ninja":
_run_or_log(
[sys.executable, "-m", "ninja", "-C", str(self.build_dir), "-t", "clean"],
ninja_command() + ["-C", str(self.build_dir), "-t", "clean"],
dry_run,
check=False,
)
else:
raise ValueError(
raise RuntimeError(
f"Unknown build backend '{backend}'. "
f"Supported backends: {', '.join(sorted(ALL_BACKENDS))}"
)
38 changes: 31 additions & 7 deletions ebuild/build/ninja_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from __future__ import annotations

import json
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
Expand All @@ -30,6 +31,14 @@ class PackagePaths:
"-fno-pie", "-fno-PIE"}


def _shared_flag() -> str:
"""The compiler flag that produces a shared object on this platform.

macOS links dynamic libraries with -dynamiclib; ELF platforms use -shared.
"""
return "-dynamiclib" if sys.platform == "darwin" else "-shared"


class NinjaBackend:
"""Generate build.ninja from a ProjectConfig and resolved toolchain.

Expand Down Expand Up @@ -104,6 +113,21 @@ def _resolve_target_cflags(self, target) -> List[str]:

return cflags

def _object_path(self, target, src: str) -> Path:
"""Object file for one source within one target.

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.

The source's directory structure is flattened into the filename rather
than mirrored beneath the build directory. Mirroring lets a source from
outside the project -- ``../shared/util.c`` -- place its object outside
the build directory too, where ``clean`` will not find it.
"""
flat = re.sub(r"[^A-Za-z0-9_.-]", "_", str(src).replace("\\", "/"))
return self.build_dir / "obj" / target.name / f"{flat}.o"

def _write_ninja(self) -> None:
"""Write the build.ninja file."""
ninja_path = self.build_dir / "build.ninja"
Expand All @@ -124,7 +148,7 @@ def _write_ninja(self) -> None:
" description = LINK $out",
"",
"rule link_shared",
" command = $cc -shared $ldflags $in -o $out $libs",
f" command = $cc {_shared_flag()} $ldflags $in -o $out $libs",
" description = LINK_SHARED $out",
"",
"rule ar_rule",
Expand All @@ -149,7 +173,7 @@ def _write_ninja(self) -> None:
lines.append(f" cflags = {' '.join(cflags)}")
lines.append("")

if target.target_type == "executable":
if target.target_type in ("executable", "test"):
ldflags = toolchain_ldflags + list(target.ldflags)
libs = []
dep_archives = []
Expand Down Expand Up @@ -190,11 +214,11 @@ def _write_ninja(self) -> None:
if target.target_type == "static_library":
lines.append(f"build {out}: ar_rule {' '.join(obj_files)}")
else:
# Shared libraries need the platform's "build a shared
# object" flag and the same -L/-l wiring executables get,
# neither of which the generic `link` rule provides.
# 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.
ldflags = list(target.ldflags)
ldflags.insert(0, "-dynamiclib" if sys.platform == "darwin" else "-shared")
libs = []
for pkg_name in target.uses:
pkg = self.package_paths.get(pkg_name)
Expand All @@ -204,7 +228,7 @@ def _write_ninja(self) -> None:
for lib in pkg.libraries:
libs.append(f"-l{lib}")

lines.append(f"build {out}: link {' '.join(obj_files)}")
lines.append(f"build {out}: link_shared {' '.join(obj_files)}")
if ldflags:
lines.append(f" ldflags = {' '.join(ldflags)}")
if libs:
Expand Down
Loading
Loading