Files
distro/src/plan.py
T
2026-05-26 03:06:26 +02:00

172 lines
5.1 KiB
Python

import graphlib
from dataclasses import dataclass
from pathlib import Path
from src.layout import Layout
from src.recipe import Recipe, RecipeSet
BUILD_PHASES = ("prepare", "configure", "build")
PHASE_STAMPS = {
"prepare": "prepared",
"configure": "configured",
"build": "built",
}
def _key(r: Recipe) -> str:
return r.key # "host:<name>" or "<name>"
def transitive_host_deps(rs: RecipeSet, r: Recipe) -> list[str]:
"""Returns transitive host_deps for `r`, in topological order (deepest first)."""
seen: dict[str, Recipe] = {}
order: list[str] = []
def visit(name: str) -> None:
if name in seen:
return
h = rs.host.get(name)
if h is None:
raise KeyError(f"unknown host dep: {name}")
seen[name] = h
for hh in h.host_deps:
visit(hh)
order.append(name)
for d in r.host_deps:
visit(d)
return order
def is_built(layout: Layout, r: Recipe) -> bool:
if r.kind == "host":
return layout.host_pkg_marker(r.name, r.version, r.revision).is_file()
for out in r.outputs:
if not layout.apk_path(out, r.version, r.revision).is_file():
return False
return True
def stamp_dir(layout: Layout, r: Recipe) -> Path:
wd = layout.build_workdir(r.name if r.kind == "target" else f"host-{r.name}")
return wd / ".orchid-stamps"
def stamp_token(r: Recipe) -> str:
return f"{r.version}-r{r.revision}\n"
def stamp_valid(layout: Layout, r: Recipe, phase: str) -> bool:
name = PHASE_STAMPS.get(phase)
if name is None:
return False
p = stamp_dir(layout, r) / name
try:
return p.read_text() == stamp_token(r)
except FileNotFoundError:
return False
def planned_stages(
layout: Layout, r: Recipe, *, forced: bool = False
) -> tuple[str, ...]:
stages: list[str] = []
for phase in BUILD_PHASES:
if phase in r.phases and (forced or not stamp_valid(layout, r, phase)):
stages.append(phase)
if "install" in r.phases:
stages.append("install")
stages.append("package" if r.kind == "target" else "finalize")
return tuple(stages)
@dataclass
class Plan:
order: list[str] # ordered list of recipe keys
recipes: dict[str, Recipe] # all referenced recipes
forced: set[str] # keys to rebuild even if built
stages: dict[str, tuple[str, ...]] # stages each planned recipe will run
def __iter__(self):
return iter(self.order)
def _collect_targets(rs: RecipeSet, requested: list[str] | None) -> list[Recipe]:
if not requested:
return [r for r in rs.target.values() if r.enabled] + [
r for r in rs.host.values() if r.enabled
]
out: list[Recipe] = []
for spec in requested:
r = rs.get(spec)
if not r.enabled:
raise ValueError(f"{spec}: disabled by build_if = False")
out.append(r)
return out
def build_plan(
rs: RecipeSet, layout: Layout, requested: list[str] | None, *, rebuild: bool = False
) -> Plan:
seen: dict[str, Recipe] = {}
ts: graphlib.TopologicalSorter[str] = graphlib.TopologicalSorter()
forced: set[str] = set()
def add(r: Recipe) -> None:
k = _key(r)
if k in seen:
return
seen[k] = r
deps: list[str] = []
for h in r.host_deps:
hr = rs.host.get(h)
if hr is None:
raise KeyError(f"{r.name}: unknown host dep {h!r}")
if not hr.enabled:
raise ValueError(f"{r.name}: host dep {h!r} disabled by build_if")
add(hr)
deps.append(_key(hr))
if r.kind == "target":
for d in (*r.deps, *r.run_deps):
tr = rs.target.get(d)
if tr is None:
raise KeyError(f"{r.name}: unknown dep {d!r}")
if not tr.enabled:
raise ValueError(f"{r.name}: dep {d!r} disabled by build_if")
add(tr)
deps.append(_key(tr))
else:
# host recipes may declare target `deps` that need to land in /sysroot
for d in r.deps:
tr = rs.target.get(d)
if tr is None:
raise KeyError(f"host:{r.name}: unknown target dep {d!r}")
if not tr.enabled:
raise ValueError(f"host:{r.name}: dep {d!r} disabled by build_if")
add(tr)
deps.append(_key(tr))
ts.add(k, *deps)
requested_recipes = _collect_targets(rs, requested)
for r in requested_recipes:
add(r)
if rebuild:
forced.add(_key(r))
order = list(ts.static_order())
stages: dict[str, tuple[str, ...]] = {}
final_order: list[str] = []
for k in order:
r = seen[k]
if k in forced:
stages[k] = planned_stages(layout, r, forced=True)
final_order.append(k)
continue
if is_built(layout, r):
continue
stages[k] = planned_stages(layout, r)
final_order.append(k)
return Plan(order=final_order, recipes=seen, forced=forced, stages=stages)