SonicDE-rpmspecs/ci/generate-build-order.py

120 lines
3.7 KiB
Python
Executable file

#!/usr/bin/env python3
"""Recompute build-order.txt tiers from the BuildRequires of every spec.
The tiers generated from the SonicDE builder dependency graph only describe
source-level dependencies; RPM build dependencies are wider (devel subpackages,
cmake config files, pkgconfig files). This derives the tiers from the specs
themselves so a tier only needs packages from earlier tiers.
"""
import glob
import os
import re
import subprocess
import sys
from collections import defaultdict
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
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 = {} # capability -> package dir name
brs = defaultdict(set)
def parse(spec):
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)
return pkg
pkgs = [parse(s) for s in specs]
edges = defaultdict(set)
for pkg in pkgs:
for cap in brs[pkg]:
owner = provides.get(cap)
if owner and owner != pkg:
edges[pkg].add(owner)
# longest-path tier assignment, ignoring back edges of cycles
tier = {}
state = {}
def depth(pkg, stack=()):
if pkg in tier:
return tier[pkg]
if pkg in stack:
return 0
d = 0
for dep in edges[pkg]:
d = max(d, depth(dep, stack + (pkg,)) + 1)
tier[pkg] = d
return d
sys.setrecursionlimit(10000)
for pkg in pkgs:
depth(pkg)
cycles = []
for pkg in pkgs:
for dep in edges[pkg]:
if tier[dep] >= tier[pkg]:
cycles.append((pkg, dep))
groups = defaultdict(list)
for pkg in pkgs:
groups[tier[pkg]].append(pkg)
lines = [
"# Build order for the SonicDE packages, derived from the BuildRequires of",
"# every spec in this repository (see ci/generate-build-order.py).",
"#",
"# Packages inside one tier are independent and can be built in parallel;",
"# the tiers themselves must be built in order.",
"",
]
for i, t in enumerate(sorted(groups), start=1):
lines.append(f"[tier{i}]")
lines += sorted(groups[t])
lines.append("")
open(os.path.join(REPO, "build-order.txt"), "w").write("\n".join(lines).rstrip() + "\n")
print("tiers:", len(groups), "packages:", len(pkgs))
if cycles:
print("dependency cycles (built with the earlier tier's package from the repo):")
for a, b in sorted(set(cycles)):
print(f" {a} <- {b}")