#!/bin/sh set -eu : "${PRE15_DUT:?PRE15_DUT is required}" : "${PRE15_ROOT:?PRE15_ROOT is required}" : "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}" : "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}" . "$PRE15_LIB_DIR/runner.sh" baseline=5cb420221ab0a4da7838c68db55280999474038b makefile=$PRE15_DUT/src/Makefile linux_makefile=$PRE15_ROOT/src-linux/Makefile case_file=$PRE15_DUT/tests/pre15/cases/B34-build-groups.sh artifacts=$PRE15_RUN_DIR/artifacts for tool in git python3 sha256sum; do command -v "$tool" >/dev/null 2>&1 || \ pre15_infra_blocked "missing B34 host tool: $tool" done pre15_record_fixture b34-makefile "$makefile" pre15_record_fixture b34-linux-makefile "$linux_makefile" pre15_record_fixture b34-case "$case_file" mkdir -p "$artifacts" pre15_target_reached if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \ "$artifacts/B34-object-list.json" <<'PY' from __future__ import annotations from collections import Counter from dataclasses import dataclass from fnmatch import fnmatchcase import json from pathlib import Path import re import subprocess import sys root = Path(sys.argv[1]) dut = Path(sys.argv[2]) baseline = sys.argv[3] report_path = Path(sys.argv[4]) makefile_path = dut / "src/Makefile" def committed(path: str) -> str: completed = subprocess.run( ["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if completed.returncode != 0: raise SystemExit(f"cannot read B34 baseline {path}: {completed.stderr}") return completed.stdout def changed_paths() -> list[str]: changed = subprocess.run( [ "git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15", ], check=True, text=True, stdout=subprocess.PIPE, ).stdout.splitlines() changed.extend( subprocess.run( [ "git", "-C", str(root), "ls-files", "--others", "--exclude-standard", "--", "repo-pre-15", ], check=True, text=True, stdout=subprocess.PIPE, ).stdout.splitlines() ) return changed expected_changed = [ "repo-pre-15/src/Makefile", "repo-pre-15/tests/pre15/cases/B34-build-groups.sh", ] changed = changed_paths() if sorted(changed) != expected_changed or len(changed) != len(expected_changed): raise SystemExit(f"B34 repo-pre-15 write set mismatch: {changed!r}") @dataclass class Evaluation: variables: dict[str, str] includes: list[str] src_operations: list[tuple[str, list[str]]] class MakeError(Exception): pass def logical_lines(source: str) -> list[tuple[int, str]]: result: list[tuple[int, str]] = [] parts: list[str] = [] start_line = 0 for line_number, raw_line in enumerate(source.splitlines(), 1): content = raw_line.split("#", 1)[0].rstrip() if not parts and not content.strip(): continue if not parts: start_line = line_number continued = content.endswith("\\") if continued: content = content[:-1].rstrip() parts.append(content.strip()) if not continued: result.append((start_line, " ".join(part for part in parts if part))) parts = [] if parts: raise SystemExit(f"unterminated Makefile continuation at line {start_line}") return result def expand(value: str, variables: dict[str, str]) -> str: pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_.]*)\}") previous = None while value != previous: previous = value value = pattern.sub(lambda match: variables.get(match.group(1), ""), value) return value def condition_value(expression: str, variables: dict[str, str], line: int) -> bool: terms = [term.strip() for term in expression.split("&&")] values: list[bool] = [] for term in terms: empty_match = re.fullmatch( r"empty\(([A-Za-z_][A-Za-z0-9_.]*):M([^()]*)\)", term ) if empty_match: variable, pattern = empty_match.groups() words = variables.get(variable, "").split() values.append(not any(fnmatchcase(word, pattern) for word in words)) continue compare_match = re.fullmatch( r'\$\{([A-Za-z_][A-Za-z0-9_.]*)\}\s*(==|!=)\s*"?([^" ]+)"?', term, ) if compare_match: variable, operator, expected = compare_match.groups() equal = variables.get(variable, "") == expected values.append(equal if operator == "==" else not equal) continue raise SystemExit(f"unsupported Makefile condition at line {line}: {term}") return all(values) def evaluate(source: str, initial: dict[str, str]) -> Evaluation: variables = dict(initial) includes: list[str] = [] src_operations: list[tuple[str, list[str]]] = [] active_stack = [True] for line_number, line in logical_lines(source): if line.startswith(".if "): parent_active = active_stack[-1] active_stack.append( parent_active and condition_value(line.removeprefix(".if "), variables, line_number) ) continue if line == ".endif": if len(active_stack) == 1: raise SystemExit(f"unmatched .endif at line {line_number}") active_stack.pop() continue if not active_stack[-1]: continue if line.startswith(".error "): raise MakeError(line.removeprefix(".error ")) if line.startswith(".include "): includes.append(line.removeprefix(".include ").strip()) continue if line.startswith("."): raise SystemExit(f"unsupported Makefile directive at line {line_number}: {line}") assignment = re.fullmatch( r"([A-Za-z_][A-Za-z0-9_.]*)\s*(\?=|\+=|=)\s*(.*)", line ) if assignment is None: raise SystemExit(f"unsupported Makefile statement at line {line_number}: {line}") variable, operator, raw_value = assignment.groups() value = expand(raw_value, variables) if operator == "?=" and variable in variables: continue if operator == "+=": old_value = variables.get(variable, "") variables[variable] = " ".join(part for part in (old_value, value) if part) else: variables[variable] = value if variable == "SRCS": src_operations.append((operator, value.split())) if len(active_stack) != 1: raise SystemExit("unterminated Makefile condition") return Evaluation(variables, includes, src_operations) def evaluate_error(source: str, initial: dict[str, str]) -> str: try: evaluate(source, initial) except MakeError as error: return str(error) raise SystemExit(f"Makefile unexpectedly accepted variables: {initial!r}") def multiset_list(items: Counter[str]) -> list[str]: return sorted(items.elements()) def object_multiset(sources: list[str]) -> Counter[str]: objects: Counter[str] = Counter() for source in sources: if source.endswith((".c", ".cc", ".cpp", ".S", ".s")): objects[str(Path(source).with_suffix(".o"))] += 1 return objects before = committed("src/Makefile") current = makefile_path.read_text(encoding="utf-8") linux = (root / "src-linux/Makefile").read_text(encoding="utf-8") expected_groups = [ ( "=", [ "super.c", "inode.c", "data.c", "namei.c", "dir.c", "erofs_vnops.c", "vnode_if.h", ], ), ("+=", ["xattr.c"]), ("+=", ["decompressor.c", "zmap.c", "zdata.c"]), ( "+=", [ "decompressor_lz4.c", "decompressor_lzma.c", "decompressor_deflate.c", "decompressor_zstd.c", ], ), ] scenario_reports: dict[str, object] = {} for config in ("0", "1"): initial = { "MACHINE_ARCH": "amd64", "SYSDIR": "/freebsd/sys", "WITH_ZSTDIO": config, } before_eval = evaluate(before, initial) current_eval = evaluate(current, initial) before_sources = before_eval.variables.get("SRCS", "").split() current_sources = current_eval.variables.get("SRCS", "").split() before_source_multiset = Counter(before_sources) current_source_multiset = Counter(current_sources) before_objects = object_multiset(before_sources) current_objects = object_multiset(current_sources) before_metadata = Counter(source for source in before_sources if not source.endswith(".c")) current_metadata = Counter(source for source in current_sources if not source.endswith(".c")) if current_eval.src_operations != expected_groups: raise SystemExit( f"B34 semantic SRCS groups differ for zstdio{config}: " f"{current_eval.src_operations!r}" ) if current_source_multiset != before_source_multiset: raise SystemExit(f"B34 SRCS multiset changed for zstdio{config}") if current_objects != before_objects: raise SystemExit(f"B34 object multiset changed for zstdio{config}") if current_metadata != before_metadata or current_metadata != Counter({"vnode_if.h": 1}): raise SystemExit(f"B34 vnode_if handling changed for zstdio{config}") if current_eval.includes != before_eval.includes or current_eval.includes != [""]: raise SystemExit(f"B34 bsd.kmod.mk ownership changed for zstdio{config}") if ( current_eval.variables.get("KMOD") != before_eval.variables.get("KMOD") or current_eval.variables.get("KMOD") != "erofs" ): raise SystemExit(f"B34 KMOD ownership changed for zstdio{config}") cflags_name = "CFLAGS.decompressor_zstd.c" if current_eval.variables.get(cflags_name) != before_eval.variables.get(cflags_name): raise SystemExit(f"B34 Zstd flags changed for zstdio{config}") expected_flags = "-I/freebsd/sys/contrib/zstd/lib/freebsd" if config == "1": expected_flags += " -DZSTDIO" if current_eval.variables.get(cflags_name) != expected_flags: raise SystemExit(f"B34 ZSTDIO expansion changed for zstdio{config}") scenario_reports[f"zstdio{config}"] = { "sources": multiset_list(current_source_multiset), "objects": multiset_list(current_objects), "metadata": multiset_list(current_metadata), "zstd_cflags": current_eval.variables[cflags_name].split(), "includes": current_eval.includes, } default_initial = {"MACHINE_ARCH": "amd64", "SYSDIR": "/freebsd/sys"} before_default = evaluate(before, default_initial) current_default = evaluate(current, default_initial) if before_default.variables.get("WITH_ZSTDIO") != "0": raise SystemExit("B34 baseline default is not WITH_ZSTDIO=0") if current_default.variables.get("WITH_ZSTDIO") != "0": raise SystemExit("B34 changed the WITH_ZSTDIO default") before_default_variables = dict(before_default.variables) current_default_variables = dict(current_default.variables) del before_default_variables["SRCS"] del current_default_variables["SRCS"] if current_default_variables != before_default_variables: raise SystemExit("B34 changed a default-config Makefile variable expansion") if current_default.includes != before_default.includes: raise SystemExit("B34 changed a default-config Makefile include") invalid_message = "WITH_ZSTDIO must be 0 or 1" for source, label in ((before, "baseline"), (current, "current")): message = evaluate_error( source, {"MACHINE_ARCH": "amd64", "SYSDIR": "/freebsd/sys", "WITH_ZSTDIO": "2"}, ) if message != invalid_message: raise SystemExit(f"B34 {label} invalid-value contract drifted: {message!r}") arch_message = "erofs supports only MACHINE_ARCH=amd64" for config in ("0", "1"): initial = { "MACHINE_ARCH": "arm64", "SYSDIR": "/freebsd/sys", "WITH_ZSTDIO": config, } before_message = evaluate_error(before, initial) current_message = evaluate_error(current, initial) if before_message != arch_message or current_message != arch_message: raise SystemExit(f"B34 amd64 gate changed for zstdio{config}") for forbidden in ("CONFIG_", "obj-", "erofs-objs", "stub"): if forbidden in current: raise SystemExit(f"B34 introduced forbidden Linux/config stub surface: {forbidden}") linux_categories = { "core": "erofs-objs :=" in linux, "xattr": "CONFIG_EROFS_FS_XATTR" in linux, "compression": "CONFIG_EROFS_FS_ZIP" in linux, "algorithms": all( token in linux for token in ( "CONFIG_EROFS_FS_ZIP_LZMA", "CONFIG_EROFS_FS_ZIP_DEFLATE", "CONFIG_EROFS_FS_ZIP_ZSTD", ) ), } if not all(linux_categories.values()): raise SystemExit(f"B34 Linux semantic grouping reference drifted: {linux_categories!r}") report = { "baseline": baseline, "write_set": changed, "groups": [ {"operator": operator, "sources": sources} for operator, sources in expected_groups ], "configurations": scenario_reports, "default_with_zstdio": current_default.variables["WITH_ZSTDIO"], "invalid_with_zstdio_error": invalid_message, "non_amd64_error": arch_message, "linux_reference_categories": linux_categories, } report_path.write_text( json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii" ) for config in ("zstdio0", "zstdio1"): details = scenario_reports[config] print(f"B34 {config} SRCS multiset: {' '.join(details['sources'])}") print(f"B34 {config} object multiset: {' '.join(details['objects'])}") print(f"B34 {config} Zstd flags: {' '.join(details['zstd_cflags'])}") print("B34 default/invalid/amd64 gates: PASS") print("B34 bsd.kmod.mk and vnode_if ownership: PASS") print("B34 object-list equivalence: PASS") PY then : else pre15_dut_fail 'B34 object-list or conditional expansion audit failed' fi