413 lines
14 KiB
Bash
Executable File
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
|