fester/backend/graph/plan.py

124 lines
4.0 KiB
Python

"""
Build action graph — convert a project spec into a DAG of executable actions.
Supports two input shapes:
1. Legacy: project = {"targets": {"name": "cmd", ...}}
→ flat list of independent actions
2. Rich: project = {"stages": [{"name": "...", "command": "...",
"deps": [...], "target": "..."}, ...]}
→ real DAG with dependency edges
"""
from backend.graph.actions import hash_action
def build_action_graph(project):
"""
Convert a project spec into a list of action dicts.
Each action has:
- name: unique action name
- command: shell command to run
- inputs: list of input globs
- outputs: list of output paths
- deps: list of action names this depends on
- env: environment dict
- runtime: "host" | "lxc" | "libvirt"
- target: build target name (e.g. "x86_64-linux-gnu")
- hash: sha256 of the action dict (for cache keys)
"""
graph = []
if "stages" in project:
# Rich DAG input
build_dir = project.get("build_dir")
for stage in project["stages"]:
action = {
"name": stage["name"],
"command": stage.get("command", "true"),
"inputs": stage.get("inputs", ["src/**"]),
"outputs": stage.get("outputs", [f"build/{stage['name']}.out"]),
"deps": stage.get("deps", []),
"env": stage.get("env", project.get("build_env", {})),
"runtime": stage.get("runtime", "host"),
"target": stage.get("target", project.get("default_target", "native")),
"cost": stage.get("cost", 1),
"dir": stage.get("dir", build_dir),
}
action["hash"] = hash_action(action)
graph.append(action)
return graph
if "targets" in project:
# Legacy flat input — synthesize a minimal pipeline:
# fetch_source → configure → per-target build → package
default_target = project.get("default_target", "native")
build_env = project.get("build_env", {})
build_dir = project.get("build_dir")
# Always include a fetch + configure stage
graph.append(_make_action(
name="fetch_source",
command=f"git clone {project.get('source', project.get('repo', '.'))} .",
deps=[],
target=default_target,
env=build_env,
cost=2,
dir=build_dir,
))
graph.append(_make_action(
name="configure",
command="./configure",
deps=["fetch_source"],
target=default_target,
env=build_env,
cost=1,
dir=build_dir,
))
# Per-target build actions (all depend on configure)
for target_name, cmd in project["targets"].items():
graph.append(_make_action(
name=f"build_{target_name}",
command=cmd,
deps=["configure"],
target=target_name,
env=build_env,
cost=3,
dir=build_dir,
))
# Package stage depends on all builds
build_names = [f"build_{t}" for t in project["targets"].keys()]
graph.append(_make_action(
name="package",
command="tar czf build.tar.gz build/",
deps=build_names,
target=default_target,
env=build_env,
cost=2,
dir=build_dir,
))
return graph
# Fallback: empty graph
return graph
def _make_action(name, command, deps, target, env, cost=1, dir=None):
action = {
"name": name,
"command": command,
"inputs": ["src/**"],
"outputs": [f"build/{name}.out"],
"deps": deps,
"env": env,
"runtime": "host",
"target": target,
"cost": cost,
"dir": dir, # working directory for the command
}
action["hash"] = hash_action(action)
return action