120 lines
3.7 KiB
Python
Executable file
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}")
|