Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 162 additions & 14 deletions support/spec-generator/create-kernel-deps-specs-from-template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -52,17 +122,16 @@ def __load_current_subrelease(self):
continue
return None

# Evaluate a "%{photon_subrelease} <op> 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):
Expand Down Expand Up @@ -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"

Expand Down