285 lines
7.9 KiB
Python
285 lines
7.9 KiB
Python
import os
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from src import apk, fetch, log
|
|
from src.container import Container, Mount
|
|
from src.context import RecipeContext
|
|
from src.layout import Layout
|
|
from src.profile import Profile
|
|
from src.plan import (
|
|
PHASE_STAMPS,
|
|
Plan,
|
|
stamp_dir,
|
|
stamp_token,
|
|
stamp_valid,
|
|
transitive_host_deps,
|
|
)
|
|
from src.recipe import Recipe, RecipeSet
|
|
|
|
|
|
def _container_name(*parts: str) -> str:
|
|
name = "-".join(parts)
|
|
name = re.sub(
|
|
r"[^a-zA-Z0-9_.-]",
|
|
lambda m: f"_{ord(m.group(0)):02x}",
|
|
name,
|
|
)
|
|
if not name or not name[0].isalnum():
|
|
name = f"orchid-{name}"
|
|
return name
|
|
|
|
|
|
def _source_tree(layout: Layout, r: Recipe, key: str | None) -> Path:
|
|
base = layout.source_tree(r.name, r.version)
|
|
if len(r.sources) == 1:
|
|
return base
|
|
if key is None:
|
|
raise ValueError(f"{r.name}: multi-source entries must be named")
|
|
return base / key
|
|
|
|
|
|
def _prepare_all_sources(layout: Layout, r: Recipe) -> 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)
|
|
|
|
|
|
def _build_path_env(host_deps_order: list[str], layout: Layout) -> str:
|
|
parts: list[str] = []
|
|
for h in reversed(host_deps_order):
|
|
parts += [
|
|
f"/tools/{h}/sbin",
|
|
f"/tools/{h}/bin",
|
|
]
|
|
parts += [
|
|
"/usr/local/sbin",
|
|
"/usr/local/bin",
|
|
"/usr/sbin",
|
|
"/usr/bin",
|
|
"/sbin",
|
|
"/bin",
|
|
]
|
|
return ":".join(parts)
|
|
|
|
|
|
def _container_for(
|
|
rs: RecipeSet, layout: Layout, profile: Profile, r: Recipe
|
|
) -> tuple[Container, list[str]]:
|
|
host_order = transitive_host_deps(rs, r)
|
|
name = _container_name("orchid", layout.build.name, r.kind, r.name)
|
|
|
|
mounts: list[Mount] = []
|
|
tmpfs: list[str] = ["/sysroot"]
|
|
# sources
|
|
if len(r.sources) == 1:
|
|
key = next(iter(r.sources.keys()))
|
|
mounts.append(Mount(_source_tree(layout, r, key), "/sources", readonly=True))
|
|
else:
|
|
for k in r.sources.keys():
|
|
mounts.append(
|
|
Mount(_source_tree(layout, r, k), f"/sources/{k}", readonly=True)
|
|
)
|
|
# build dir
|
|
wd = layout.build_workdir(r.name if r.kind == "target" else f"host-{r.name}")
|
|
wd.mkdir(parents=True, exist_ok=True)
|
|
mounts.append(Mount(wd, "/build", readonly=False))
|
|
# files dir
|
|
if r.files_dir is not None:
|
|
mounts.append(Mount(r.files_dir, "/files", readonly=True))
|
|
# tools
|
|
for h in host_order:
|
|
mounts.append(Mount(layout.host_pkg_dir(h), f"/tools/{h}", readonly=True))
|
|
# pkgs (produced packages persist on disk); sysroot lives in tmpfs
|
|
mounts.append(Mount(layout.pkgs_dir, "/pkgs", readonly=False))
|
|
|
|
env = {
|
|
"PATH": _build_path_env(host_order, layout),
|
|
"ORCHID_ARCH": profile.arch,
|
|
"ORCHID_TRIPLE": profile.triple,
|
|
"ORCHID_JOBS": str(os.cpu_count() or 1),
|
|
}
|
|
c = Container(
|
|
name=name,
|
|
image=profile.container_image,
|
|
mounts=mounts,
|
|
tmpfs=tmpfs,
|
|
network=False,
|
|
extra_env=env,
|
|
)
|
|
return c, host_order
|
|
|
|
|
|
def _recipe_ref(r: Recipe) -> str:
|
|
return f"{r.key} ({r.version}-r{r.revision})"
|
|
|
|
|
|
def _mark_stamp(layout: Layout, r: Recipe, phase: str) -> None:
|
|
name = PHASE_STAMPS.get(phase)
|
|
if name is None:
|
|
return
|
|
d = stamp_dir(layout, r)
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
(d / name).write_text(stamp_token(r))
|
|
|
|
|
|
def _clear_stamps(layout: Layout, r: Recipe) -> None:
|
|
d = stamp_dir(layout, r)
|
|
if d.is_dir():
|
|
shutil.rmtree(d)
|
|
|
|
|
|
def _wipe_workdir(layout: Layout, r: Recipe) -> None:
|
|
wd = layout.build_workdir(r.name if r.kind == "target" else f"host-{r.name}")
|
|
if wd.is_dir():
|
|
shutil.rmtree(wd)
|
|
|
|
|
|
def _run_phases(ctx: RecipeContext, r: Recipe, layout: Layout) -> None:
|
|
for phase in ("prepare", "configure", "build"):
|
|
fn = r.phases.get(phase)
|
|
if fn is None:
|
|
continue
|
|
if stamp_valid(layout, r, phase):
|
|
log.info_field(phase, f"{_recipe_ref(r)} (cached)")
|
|
continue
|
|
log.info_field(phase, _recipe_ref(r))
|
|
fn(ctx)
|
|
_mark_stamp(layout, r, phase)
|
|
|
|
|
|
def _do_install(ctx: RecipeContext, r: Recipe) -> None:
|
|
fn = r.phases.get("install")
|
|
if fn is None:
|
|
return
|
|
log.info_field("install", _recipe_ref(r))
|
|
ctx._dest_output = r.name
|
|
try:
|
|
fn(ctx)
|
|
finally:
|
|
ctx._dest_output = None
|
|
|
|
|
|
def _split_subpackages(c: Container, r: Recipe) -> None:
|
|
for sub in r.subpackages:
|
|
c.exec(["mkdir", "-p", f"/dest/{sub.name}"])
|
|
for pat in sub.files:
|
|
c.exec_shell(apk.split_subpackage_script(r.name, sub.name, pat))
|
|
|
|
|
|
def _package_target(c: Container, r: Recipe, arch: str) -> None:
|
|
apk.mkpkg_base(c, r, arch)
|
|
for sub in r.subpackages:
|
|
apk.mkpkg_subpackage(c, r, sub, arch)
|
|
apk.index(c)
|
|
|
|
|
|
def _finalize_host(c: Container, layout: Layout, r: Recipe) -> None:
|
|
import subprocess
|
|
|
|
out = layout.host_pkg_dir(r.name)
|
|
if out.exists():
|
|
shutil.rmtree(out)
|
|
out.mkdir(parents=True)
|
|
p1 = subprocess.Popen(
|
|
[
|
|
"podman",
|
|
"exec",
|
|
c.name,
|
|
"sh",
|
|
"-c",
|
|
f"cd /dest/{r.name}/tools/{r.name} && tar -cf - .",
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
p2 = subprocess.Popen(["tar", "-xf", "-", "-C", str(out)], stdin=p1.stdout)
|
|
if p1.stdout is not None:
|
|
p1.stdout.close()
|
|
rc = p2.wait()
|
|
p1.wait()
|
|
if rc != 0 or p1.returncode != 0:
|
|
raise RuntimeError(f"host {r.name}: copy-out failed")
|
|
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:
|
|
return
|
|
initdb = not apk.sysroot_initialized(c)
|
|
apk.sysroot_install(c, direct_deps, initdb=initdb)
|
|
|
|
|
|
def build_one(
|
|
rs: RecipeSet, layout: Layout, profile: Profile, r: Recipe, *, forced: 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)
|
|
|
|
c, _host_order = _container_for(rs, layout, profile, r)
|
|
c.start()
|
|
try:
|
|
# Ensure base output dest dir exists.
|
|
c.exec(["mkdir", "-p", f"/dest/{r.name}"])
|
|
|
|
_sysroot_sync(c, r)
|
|
|
|
ctx = RecipeContext(
|
|
recipe=r, profile=profile, container=c, jobs=os.cpu_count() or 1
|
|
)
|
|
_run_phases(ctx, r, layout)
|
|
_do_install(ctx, r)
|
|
|
|
if r.kind == "target":
|
|
log.info_field("package", _recipe_ref(r))
|
|
_split_subpackages(c, r)
|
|
_package_target(c, r, profile.arch)
|
|
else:
|
|
log.info_field("finalize", _recipe_ref(r))
|
|
_finalize_host(c, layout, r)
|
|
finally:
|
|
c.stop()
|
|
log.ok_field("done", _recipe_ref(r))
|
|
|
|
|
|
def execute(plan: Plan, rs: RecipeSet, layout: Layout, profile: Profile) -> 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)
|
|
|
|
|
|
def install_to(
|
|
layout: Layout,
|
|
profile: Profile,
|
|
dest: Path,
|
|
pkgs: list[str],
|
|
*,
|
|
initdb: bool = True,
|
|
) -> None:
|
|
if not pkgs:
|
|
log.info_field("install", "nothing to install")
|
|
return
|
|
c = Container(
|
|
name=_container_name("orchid", layout.build.name, "install"),
|
|
image=profile.container_image,
|
|
mounts=[
|
|
Mount(layout.pkgs_dir, "/pkgs", readonly=True),
|
|
Mount(dest, "/sysroot", readonly=False),
|
|
],
|
|
network=False,
|
|
)
|
|
c.start()
|
|
try:
|
|
log.info_field("install", f"{dest}: {', '.join(pkgs)}")
|
|
apk.sysroot_install(c, pkgs, initdb=initdb)
|
|
finally:
|
|
c.stop()
|