This commit is contained in:
2026-06-02 21:38:47 +02:00
parent d3c949b8a2
commit f51dab51db
70 changed files with 1632 additions and 21 deletions
+55 -11
View File
@@ -40,11 +40,11 @@ def _source_tree(layout: Layout, r: Recipe, key: str | None) -> Path:
return base / key
def _prepare_all_sources(layout: Layout, r: Recipe) -> None:
def _prepare_all_sources(layout: Layout, r: Recipe, *, strict: bool = False) -> None:
for key, src in r.sources.items():
tree = _source_tree(layout, r, key)
tree.parent.mkdir(parents=True, exist_ok=True)
fetch.prepare_source(layout, r.dir, src, tree)
fetch.prepare_source(layout, r.dir, src, tree, strict=strict)
def _build_path_env(host_deps_order: list[str], layout: Layout) -> str:
@@ -204,22 +204,64 @@ def _finalize_host(c: Container, layout: Layout, r: Recipe) -> None:
layout.host_pkg_marker(r.name, r.version, r.revision).write_text("ok\n")
def _sysroot_sync(c: Container, r: Recipe) -> None:
direct_deps = list(dict.fromkeys((*r.deps, *r.build_deps)))
if not direct_deps:
def _target_output_index(rs: RecipeSet) -> dict[str, Recipe]:
"""Map every installable target package name (incl. subpackages) to its recipe."""
idx: dict[str, Recipe] = {}
for rec in rs.target.values():
for out in rec.outputs:
idx[out] = rec
return idx
def _transitive_runtime_deps(rs: RecipeSet, r: Recipe) -> list[str]:
"""Full closure of `r`'s deps, so the sysroot carries everything its
dependencies need (e.g. pkg-config Requires.private / linked SONAMEs).
Starts from `r`'s direct deps + build_deps and follows each package's own
`deps`. Unknown names (external packages, subpackages we can't resolve) are
still installed but not recursed into.
"""
idx = _target_output_index(rs)
order: list[str] = []
visited: set[str] = set()
def visit(name: str) -> None:
if name in visited:
return
visited.add(name)
order.append(name)
dep_recipe = idx.get(name)
if dep_recipe is not None and dep_recipe.name != r.name:
for d in dep_recipe.deps:
visit(d)
for name in (*r.deps, *r.build_deps):
visit(name)
return order
def _sysroot_sync(c: Container, rs: RecipeSet, r: Recipe) -> None:
pkgs = _transitive_runtime_deps(rs, r)
if not pkgs:
return
initdb = not apk.sysroot_initialized(c)
apk.sysroot_install(c, direct_deps, initdb=initdb)
apk.sysroot_install(c, pkgs, initdb=initdb)
def build_one(
rs: RecipeSet, layout: Layout, profile: Profile, r: Recipe, *, forced: bool = False
rs: RecipeSet,
layout: Layout,
profile: Profile,
r: Recipe,
*,
forced: bool = False,
strict: bool = False,
) -> None:
log.info_field("recipe", _recipe_ref(r))
if forced:
_clear_stamps(layout, r)
_wipe_workdir(layout, r)
_prepare_all_sources(layout, r)
_prepare_all_sources(layout, r, strict=strict)
c, _host_order = _container_for(rs, layout, profile, r)
c.start()
@@ -227,7 +269,7 @@ def build_one(
# Ensure base output dest dir exists.
c.exec(["mkdir", "-p", f"/dest/{r.name}"])
_sysroot_sync(c, r)
_sysroot_sync(c, rs, r)
ctx = RecipeContext(
recipe=r, profile=profile, container=c, jobs=os.cpu_count() or 1
@@ -247,13 +289,15 @@ def build_one(
log.ok_field("done", _recipe_ref(r))
def execute(plan: Plan, rs: RecipeSet, layout: Layout, profile: Profile) -> None:
def execute(
plan: Plan, rs: RecipeSet, layout: Layout, profile: Profile, *, strict: bool = False
) -> None:
if not plan.order:
log.info_field("plan", "nothing to do")
return
for k in plan.order:
r = plan.recipes[k]
build_one(rs, layout, profile, r, forced=k in plan.forced)
build_one(rs, layout, profile, r, forced=k in plan.forced, strict=strict)
def install_to(
+12 -2
View File
@@ -120,7 +120,7 @@ def cmd_build(args) -> int:
p = plan.build_plan(rs, layout, args.recipes or None, rebuild=args.rebuild)
if args.dry_run:
return cmd_plan(args)
builder.execute(p, rs, layout, prof)
builder.execute(p, rs, layout, prof, strict=args.strict)
return 0
@@ -159,7 +159,7 @@ def cmd_fetch(args) -> int:
for k in targets:
r = rs.get(k)
for key, src in r.sources.items():
fetch_mod.fetch(layout, src)
fetch_mod.fetch(layout, src, strict=args.strict)
return 0
@@ -197,6 +197,11 @@ def make_parser() -> argparse.ArgumentParser:
_common(p_build)
p_build.add_argument("--rebuild", action="store_true")
p_build.add_argument("-n", "--dry-run", action="store_true")
p_build.add_argument(
"--strict",
action="store_true",
help='fail on recipes with a placeholder checksum (sha256="?")',
)
p_build.set_defaults(func=cmd_build)
p_inst = sub.add_parser(
@@ -221,6 +226,11 @@ def make_parser() -> argparse.ArgumentParser:
p_fetch = sub.add_parser("fetch", help="fetch sources only")
_common(p_fetch)
p_fetch.add_argument(
"--strict",
action="store_true",
help='fail on recipes with a placeholder checksum (sha256="?")',
)
p_fetch.set_defaults(func=cmd_fetch)
return parser
+11
View File
@@ -108,3 +108,14 @@ class RecipeContext:
merged.update(env)
cwd_s = str(cwd) if cwd is not None else "/build"
self.container.exec(flat, env=merged, cwd=cwd_s)
def write_text(self, path, content: str) -> None:
"""Write *content* to *path* inside the container.
Round-tripped through base64 so arbitrary text (quotes, newlines,
shell metacharacters) survives the shell without escaping.
"""
import base64
data = base64.b64encode(content.encode()).decode()
self.run("sh", "-c", f"echo '{data}' | base64 -d > '{path}'")
+12 -5
View File
@@ -26,11 +26,16 @@ def cache_lock(layout: Layout):
f.close()
def fetch_tarball(layout: Layout, src: Tarball) -> Path:
def fetch_tarball(layout: Layout, src: Tarball, *, strict: bool = False) -> Path:
dest = layout.tarball_cache / src.sha256
if dest.is_file():
return dest
if src.sha256 == "?":
if strict:
raise RuntimeError(
f"{src.url}: missing checksum (sha256=\"?\") is not allowed in "
f"strict mode; pin the real sha256 in the recipe"
)
log.warn(f"fetching {src.url} (sha256 unknown)")
else:
log.info(f"fetching {src.url}")
@@ -82,10 +87,10 @@ def fetch_git(layout: Layout, src: Git) -> Path:
return dest
def fetch(layout: Layout, src) -> Path:
def fetch(layout: Layout, src, *, strict: bool = False) -> Path:
with cache_lock(layout):
if isinstance(src, Tarball):
return fetch_tarball(layout, src)
return fetch_tarball(layout, src, strict=strict)
if isinstance(src, Git):
return fetch_git(layout, src)
raise TypeError(f"unknown source type {type(src).__name__}")
@@ -165,9 +170,11 @@ def apply_patches(tree: Path, recipe_dir: Path, patches: tuple[str, ...]) -> Non
_patched_marker(tree).write_text("\n".join(patches) + "\n")
def prepare_source(layout: Layout, recipe_dir: Path, src, tree: Path) -> None:
def prepare_source(
layout: Layout, recipe_dir: Path, src, tree: Path, *, strict: bool = False
) -> None:
"""Fetch + extract + patch into `tree`. Idempotent via marker files."""
cache_path = fetch(layout, src)
cache_path = fetch(layout, src, strict=strict)
expected_patches = "\n".join(src.patches) + "\n" if src.patches else "\n"
if (
_patched_marker(tree).is_file()
+95
View File
@@ -1,5 +1,98 @@
# A cross `llvm-config`. The target llvm-config is a glibc binary that can't run
# on the (musl) build host, so instead of executing it we emulate the queries
# meson/mesa make, reporting paths inside the sysroot. Self-configuring: it reads
# the LLVM version, library soname, and built targets from the installed sysroot,
# so it works for any LLVM consumer (mesa now, rust/etc. later) with no hardcoding.
_LLVM_CONFIG_EMULATOR = r'''#!/usr/bin/env python3
import glob, re, sys
SYSROOT = "/sysroot"
LIBDIR = SYSROOT + "/usr/lib"
INCDIR = SYSROOT + "/usr/include"
_major = ""
for _p in sorted(glob.glob(LIBDIR + "/libLLVM-*.so*")):
_m = re.search(r"libLLVM-(\d+)", _p)
if _m:
_major = _m.group(1)
break
_version = (_major + ".0.0") if _major else "0.0.0"
try:
with open(INCDIR + "/llvm/Config/llvm-config.h") as _f:
_m = re.search(r'LLVM_VERSION_STRING\s+"([^"]+)"', _f.read())
if _m:
_version = _m.group(1)
except OSError:
pass
_targets = []
try:
with open(INCDIR + "/llvm/Config/Targets.def") as _f:
_targets = re.findall(r"LLVM_TARGET\((\w+)\)", _f.read())
except OSError:
pass
if not _targets:
_targets = ["X86", "AMDGPU"]
_GENERIC = (
"aggressiveinstcombine all all-targets analysis asmparser asmprinter "
"binaryformat bitreader bitstreamreader bitwriter cfguard codegen "
"codegentypes core coroutines coverage debuginfocodeview debuginfodwarf "
"debuginfomsf debuginfopdb demangle dlltooldriver engine executionengine "
"extensions frontenddriver frontendhlsl frontendoffloading frontendopenmp "
"fuzzmutate globalisel instcombine instrumentation interpreter ipo irreader "
"irprinter jitlink libdriver lineeditor linker lto mc mca mcdisassembler "
"mcjit mcparser native nativecodegen object objectyaml option orcjit "
"orcshared orctargetprocess passes profiledata remarks runtimedyld "
"scalaropts selectiondag support symbolize target targetparser textapi "
"transformutils vectorize windowsdriver windowsmanifest"
).split()
def _components():
comps = list(_GENERIC)
for t in _targets:
tl = t.lower()
comps += [tl, tl + "asmparser", tl + "codegen", tl + "desc",
tl + "disassembler", tl + "info", tl + "targetmca", tl + "utils"]
return " ".join(sorted(set(comps)))
_H = {
"--version": lambda: _version,
"--components": _components,
"--targets-built": lambda: " ".join(_targets),
"--prefix": lambda: SYSROOT + "/usr",
"--bindir": lambda: SYSROOT + "/usr/bin",
"--includedir": lambda: INCDIR,
"--libdir": lambda: LIBDIR,
"--cmakedir": lambda: LIBDIR + "/cmake/llvm",
"--has-rtti": lambda: "YES",
"--shared-mode": lambda: "shared",
"--libs": lambda: "-lLLVM-" + _major,
"--system-libs": lambda: "",
"--cflags": lambda: "-I" + INCDIR,
"--cppflags": lambda: ("-I" + INCDIR
+ " -D__STDC_CONSTANT_MACROS"
+ " -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS"),
"--cxxflags": lambda: ("-I" + INCDIR
+ " -D__STDC_CONSTANT_MACROS"
+ " -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS"),
"--ldflags": lambda: "-L" + LIBDIR,
}
for _a in sys.argv[1:]:
if _a in _H:
print(_H[_a]())
'''
def meson_cross_file(self):
cross = self.build_dir / "meson-cross.ini"
llvm_config = self.build_dir / "llvm-config"
self.write_text(llvm_config, _LLVM_CONFIG_EMULATOR)
self.run("chmod", "+x", llvm_config)
self.write_text(
cross,
f"""\
@@ -12,6 +105,8 @@ objcopy = '{self.triple}-objcopy'
ranlib = '{self.triple}-ranlib'
strip = '{self.triple}-strip'
pkg-config = '{self.triple}-pkg-config'
cmake = '/usr/bin/cmake'
llvm-config = '{llvm_config}'
[host_machine]
system = 'linux'
+7 -2
View File
@@ -158,8 +158,8 @@ def _load_one(
else:
for k, v in multi_items:
sources[k] = v
else:
raise ValueError(f"{name}: 'source' or 'sources' required")
# else: no source. Allowed for config-only recipes (e.g. base-files); the
# presence of at least one phase is validated below.
if pure:
for s in sources.values():
@@ -191,6 +191,11 @@ def _load_one(
# Purely declarative pkgs are unusual but allowed
pass
if not sources and not phases:
raise ValueError(
f"{name}: define a 'source'/'sources' or at least one build phase"
)
enabled = bool(getattr(mod, "build_if", True))
return Recipe(