From 595e66a6f107fe0553ea43f986ce9266850ee9c8 Mon Sep 17 00:00:00 2001 From: Daniel Casota Date: Mon, 14 Sep 2026 14:13:33 +0200 Subject: [PATCH] spec-generator: read kernel specs as the current subrelease sees them The kernel-deps generator took the first Version: and Release: lines of each kernel spec and its build_if. A spec that serves several subreleases - SPECS/linux/linux.spec builds kernel 6.1 up to photon_subrelease 90 and 6.12 from 91 - keeps each kernel's Version behind a subrelease conditional and its Release in an included file, so the first Version belonged to the wrong kernel, Release was not found and the generator crashed, stopping make image. The spec is now resolved for the current subrelease first: plain photon_subrelease comparisons are decided and %include'd files are inlined. Without a build_if, the subrelease condition around Version: becomes the build target, as the separate per-subrelease spec stated it. Includes are inlined up to _MAX_INCLUDE_DEPTH (8) levels, four times the nesting the kernel specs use; deeper nesting is taken for an include cycle and stops the generator with the file named, instead of leaving the %include unresolved. Subrelease conditions are evaluated as SpecParser evaluates them, compound ones included: comparisons of %{photon_subrelease} joined by && and || ("%{photon_subrelease} >= 91 && %{photon_subrelease} <= 92", the form 5.0 now uses for its 91/92 sub-branches). That holds for an %if around Version:, for its %else, for nested conditionals, and for a template's build_if. Before, only a single comparison was understood: a compound %if kept both kernels and the first Version: (6.1) won at every subrelease, and a 91/92-only template override was also taken at 93. For specs without subrelease conditionals nothing changes: versions, releases and build targets for subreleases 90, 91, 92 and 93 are the same as before, for both the current specs and the single-source kernel specs. Change-Id: Ied11d8260b9ea3d37c4fe322e5c2c5ea880535b3 Co-Authored-By: Claude Opus 5.5 --- .../create-kernel-deps-specs-from-template.py | 176 ++++++++++++++++-- 1 file changed, 162 insertions(+), 14 deletions(-) diff --git a/support/spec-generator/create-kernel-deps-specs-from-template.py b/support/spec-generator/create-kernel-deps-specs-from-template.py index 6aac1193b1..c9883fd76b 100644 --- a/support/spec-generator/create-kernel-deps-specs-from-template.py +++ b/support/spec-generator/create-kernel-deps-specs-from-template.py @@ -12,10 +12,80 @@ "<": operator.lt, ">": operator.gt, "==": operator.eq, + "!=": operator.ne, } +# A photon_subrelease condition as SpecParser evaluates it: comparisons of +# %{photon_subrelease} with an integer, joined by && and || (&& binds +# tighter). Held as or-of-ands: [[(op, n), ...], ...]. +_SUBREL_TOKEN = re.compile(r"\s*(&&|\|\||>=|<=|==|!=|>|<|0?%\{\??photon_subrelease\}|\d+)") +_FLIP = {"<": ">", ">": "<", "<=": ">=", ">=": "<=", "==": "==", "!=": "!="} +# not (s op n), kept in the <=/>= form the single-comparison code produced. +_NEGATE = {"<=": (">=", 1), "<": (">=", 0), ">=": ("<=", -1), ">": ("<=", 0), + "==": ("!=", 0), "!=": ("==", 0)} +# %include nesting accepted when inlining a kernel spec. Photon's kernel +# specs nest two deep; deeper nesting is taken for an include cycle. +_MAX_INCLUDE_DEPTH = 8 + + +def _subrel_condition(expr): + """Parse a subrelease-only condition; None if it holds anything else.""" + s, pos, tokens = expr.strip(), 0, [] + while pos < len(s): + m = _SUBREL_TOKEN.match(s, pos) + if not m: + return None + tokens.append(m.group(1)) + pos = m.end() + ors, ands, i = [], [], 0 + while True: + if i + 3 > len(tokens): + return None + a, op, b = tokens[i:i + 3] + if op not in _BUILD_IF_OPS: + return None + if "photon_subrelease" in a and b.isdigit(): + ands.append((op, int(b))) + elif a.isdigit() and "photon_subrelease" in b: + ands.append((_FLIP[op], int(a))) + else: + return None + i += 3 + if i == len(tokens): + ors.append(ands) + return ors + if tokens[i] == "&&": + i += 1 + elif tokens[i] == "||": + ors.append(ands) + ands = [] + i += 1 + else: + return None + + +def _subrel_true(cond, value): + return any(all(_BUILD_IF_OPS[op](value, n) for op, n in ands) for ands in cond) + + +def _subrel_and(a, b): + return [x + y for x in a for y in b] + + +def _subrel_not(cond): + out = [[]] + for ands in cond: + out = [o + [(_NEGATE[op][0], n + _NEGATE[op][1])] for o in out for op, n in ands] + return out + + +def _subrel_text(cond): + return " || ".join(" && ".join(f"%{{photon_subrelease}} {op} {n}" for op, n in ands) + for ands in cond) + class KernelSpecProcessor: + def __init__(self, driver_info_file, spec_paths): self.kvers = defaultdict(list) self.krels = defaultdict(list) @@ -52,17 +122,16 @@ def __load_current_subrelease(self): continue return None - # Evaluate a "%{photon_subrelease} N" build_if expression; unparseable defaults to active. + # Evaluate a build_if over photon_subrelease, compound ones included + # ("%{photon_subrelease} >= 91 && %{photon_subrelease} <= 92"); one that + # is not a subrelease condition defaults to active. def __is_build_if_active(self, build_for_value): if self.current_subrelease is None: return True - match = re.match( - r"%\{photon_subrelease\}\s*(<=|>=|<|>|==)\s*(\d+)", build_for_value.strip() - ) - if not match: + cond = _subrel_condition(build_for_value) + if cond is None: return True - op, num = match.group(1), int(match.group(2)) - return _BUILD_IF_OPS[op](self.current_subrelease, num) + return _subrel_true(cond, self.current_subrelease) # Load the JSON data from the given file def __load_data(self, driver_info_file): @@ -139,22 +208,101 @@ def __delete_older_specs(self): for spec_file in k_specs: spec_file.unlink() + # The spec as the current subrelease sees it: plain photon_subrelease + # comparisons decided and %include'd files inlined. A spec that serves + # several subreleases (SPECS/linux/linux.spec builds kernel 6.1 up to 90 + # and 6.12 from 91) keeps each kernel's Version behind such a conditional + # and its Release in an included file. Other conditionals are kept. + def __resolved_spec(self, spec_path): + out, stack, sources = [], [], {} + # The subrelease condition in force where Version: is read: what a + # separate per-subrelease spec used to state as its build_if. + self.__version_condition = None + + def active(): + return all(f is None or f[0] for f in stack) + + def walk(path, depth): + with open(path, "r") as spec_file: + lines = spec_file.read().splitlines() + for line in lines: + s = line.strip() + cond = None + if self.current_subrelease is not None and re.match(r"^%if\s", s): + cond = _subrel_condition(s[3:]) + if cond is not None: + stack.append([_subrel_true(cond, self.current_subrelease), cond]) + continue + if s.startswith("%if"): + if active(): + out.append(line) + stack.append(None) + continue + if s.startswith("%else") and stack: + if stack[-1] is not None: + taken, cond = stack[-1] + stack[-1] = [not taken, _subrel_not(cond)] + elif active(): + out.append(line) + continue + if s.startswith("%endif") and stack: + if stack.pop() is None and active(): + out.append(line) + continue + if not active(): + continue + if s.startswith("Version:") and self.__version_condition is None: + conds = [f[1] for f in stack if f is not None] + if conds: + cond = conds[0] + for c in conds[1:]: + cond = _subrel_and(cond, c) + self.__version_condition = _subrel_text(cond) + src = re.match(r"^Source(\d+)\s*:\s*(\S+)", s, re.IGNORECASE) + if src: + sources[src.group(1)] = src.group(2) + inc = re.match(r"^%include\s+(\S+)\s*$", s) + if inc and depth >= _MAX_INCLUDE_DEPTH: + print(f"Error: {path}: %include nested deeper than " + f"{_MAX_INCLUDE_DEPTH} levels") + sys.exit(1) + if inc: + name = inc.group(1) + ref = re.fullmatch(r"%\{SOURCE(\d+)\}", name) + if ref: + name = sources.get(ref.group(1), "") + name = os.path.basename(name) + if name and "%" not in name: + target = Path(path).parent / name + if target.is_file(): + walk(target, depth + 1) + continue + out.append(line) + + walk(spec_path, 0) + return "\n".join(out) + "\n" + def __extract_kernel_data(self): # # Extract kernel versions, releases, and build targets for linux_flavour in self.linux_flavours: spec_paths = self.__find_spec_files(linux_flavour) for spec_path in spec_paths: - with open(spec_path, 'r') as spec_file: - spec_content = spec_file.read() - - version_match = re.search(r"^Version:\s*(\S+)", - spec_content, re.MULTILINE).group(1) - release_match = re.search(r"^Release:\s*(\S+)", - spec_content, re.MULTILINE).group(1) + spec_content = self.__resolved_spec(spec_path) + + version = re.search(r"^Version:\s*(\S+)", spec_content, re.MULTILINE) + release = re.search(r"^Release:\s*(\S+)", spec_content, re.MULTILINE) + if not version or not release: + print(f"Error: {spec_path}: no Version/Release at " + f"subrelease {self.current_subrelease}") + sys.exit(1) + version_match = version.group(1) + release_match = release.group(1) build_for_match = re.search(r"^\s*%(?:global|define)\s+build_if\s+(.*)", spec_content, re.MULTILINE) if build_for_match: build_for_value = build_for_match.group(1).strip() + elif self.__version_condition: + build_for_value = self.__version_condition else: build_for_value = "1"