SonicDE-rpmspecs/ci/build-plan.py
test d27cb7278a
Some checks failed
Build RPMs / plan (push) Failing after 27s
Build RPMs / build (push) Has been skipped
Add incremental Forgejo Actions build workflow
2026-08-25 11:45:05 +00:00

167 lines
5.4 KiB
Python
Executable file

#!/usr/bin/env python3
"""Compute which packages to build based on git changes and tier ordering.
Outputs a JSON object describing the build plan:
{"tiers": [{"tier": 1, "packages": ["pkg-a", "pkg-b"]}, ...]}
Usage:
ci/build-plan.py [BASE_SHA] [HEAD_SHA]
If no SHAs are given, outputs all packages (full rebuild).
If BASE_SHA is given, only packages that changed OR transitively depend on a
changed package are included.
"""
import glob
import json
import os
import re
import subprocess
import sys
from collections import defaultdict
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ORDER_FILE = os.path.join(REPO, "build-order.txt")
def parse_build_order():
"""Return list of (tier_number, [packages]) from build-order.txt."""
tiers = []
current_tier = None
current_pkgs = []
with open(ORDER_FILE) as f:
for line in f:
line = line.strip()
m = re.match(r"^\[tier(\d+)\]$", line)
if m:
if current_tier is not None:
tiers.append((current_tier, current_pkgs))
current_tier = int(m.group(1))
current_pkgs = []
elif line and not line.startswith("#"):
current_pkgs.append(line)
if current_tier is not None:
tiers.append((current_tier, current_pkgs))
return tiers
def build_dependency_graph():
"""Return forward-dependency graph: pkg -> set of pkgs it depends on."""
specs = sorted(glob.glob(os.path.join(REPO, "*", "*.spec")))
specs = [s for s in specs
if os.path.basename(s)[:-5] == os.path.basename(os.path.dirname(s))]
provides = {}
brs = defaultdict(set)
for spec in specs:
pkg = os.path.basename(spec)[:-5]
out = subprocess.run(
["rpmspec", "-P", spec], capture_output=True, text=True
).stdout
if not out:
out = open(spec, errors="replace").read()
names = {pkg}
in_files = False
for line in out.splitlines():
s = line.strip()
m = re.match(r"%package\s+(?:-n\s+(\S+)|(\S+))", s)
if m:
names.add(m.group(1) or f"{pkg}-{m.group(2)}")
continue
if s.startswith("%files"):
in_files = True
elif s.startswith("%") and re.match(
r"%(prep|build|install|conf|check|changelog|description|post|pre)", s
):
in_files = s.startswith("%files")
m = re.match(r"(Provides|BuildRequires)\s*:\s*(.+)", s)
if m:
for dep in re.split(r"\s+(?=[A-Za-z_%(/])", m.group(2).strip()):
cap = dep.split()[0].replace("%{?_isa}", "").strip()
if not cap or cap.startswith(("%", "(")):
continue
if m.group(1) == "Provides":
names.add(cap)
else:
brs[pkg].add(cap)
if in_files:
for cm in re.findall(r"/cmake/([A-Za-z0-9_.+-]+)/?", s):
names.add(f"cmake({cm})")
for pc in re.findall(r"/([A-Za-z0-9_.+-]+)\.pc\b", s):
names.add(f"pkgconfig({pc})")
for n in names:
provides.setdefault(n, pkg)
forward = defaultdict(set)
for pkg, caps in brs.items():
for cap in caps:
owner = provides.get(cap)
if owner and owner != pkg:
forward[pkg].add(owner)
return forward
def compute_affected(changed_pkgs, forward_graph):
"""Given changed packages, compute all packages that need rebuilding."""
reverse = defaultdict(set)
for pkg, deps in forward_graph.items():
for dep in deps:
reverse[dep].add(pkg)
affected = set(changed_pkgs)
queue = list(changed_pkgs)
while queue:
pkg = queue.pop(0)
for dependent in reverse.get(pkg, ()):
if dependent not in affected:
affected.add(dependent)
queue.append(dependent)
return affected
def main():
tiers = parse_build_order()
all_pkgs = [pkg for _, pkgs in tiers for pkg in pkgs]
if len(sys.argv) >= 3:
base, head = sys.argv[1], sys.argv[2]
changed_files = subprocess.run(
["git", "diff", "--name-only", base, head],
capture_output=True, text=True, cwd=REPO,
).stdout.split()
changed_pkgs = {
os.path.basename(f).replace(".spec", "")
for f in changed_files if f.endswith(".spec")
}
for f in changed_files:
if "/" in f and not f.endswith(".spec"):
pkg = f.split("/")[0]
if pkg in all_pkgs:
changed_pkgs.add(pkg)
else:
changed_pkgs = set(all_pkgs)
if not changed_pkgs:
print(json.dumps({"tiers": [], "changed": [], "total": 0}))
return
forward = build_dependency_graph()
affected = compute_affected(changed_pkgs, forward)
plan = []
for tier_num, pkgs in tiers:
tier_affected = [p for p in pkgs if p in affected]
if tier_affected:
plan.append({"tier": tier_num, "packages": sorted(tier_affected)})
result = {
"tiers": plan,
"changed": sorted(changed_pkgs),
"total": sum(len(t["packages"]) for t in plan),
}
print(json.dumps(result))
if __name__ == "__main__":
main()