update
This commit is contained in:
Executable
+412
@@ -0,0 +1,412 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user