Files
erofs-freebsd-out-tree/tests/pre15/cases/B34-build-groups.sh
T
2026-08-18 09:20:44 +02:00

413 lines
14 KiB
Bash
Executable File

#!/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 != ["<bsd.kmod.mk>"]:
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