#!/usr/bin/env python3 """Compare resolved dependency TSVs and inspect both WARs; exits nonzero on regression.""" import argparse import hashlib import io import json from pathlib import Path import re import zipfile EXPECTED_CHANGES = { "org.thymeleaf:thymeleaf": ("3.1.4.RELEASE", "3.1.5.RELEASE"), "org.thymeleaf:thymeleaf-spring5": ("3.1.4.RELEASE", "3.1.5.RELEASE"), "ognl:ognl": ("3.3.4", "3.3.5"), } EXPECTED_CLASSES = { "org/thymeleaf/TemplateEngine.class": "thymeleaf-3.1.5.RELEASE.jar", "org/thymeleaf/spring5/SpringTemplateEngine.class": "thymeleaf-spring5-3.1.5.RELEASE.jar", "ognl/Ognl.class": "ognl-3.3.5.jar", } LOCAL_PREFIXES = ( "spring-boot-devtools", "spring-boot-starter-actuator", "spring-boot-actuator", "micrometer-", "spring-boot-admin-", "tomcat-embed-websocket-", ) def require(condition, message): if not condition: raise ValueError(message) def dependencies(path): result = {} for line in path.read_text().splitlines(): module, version, filename = line.split("\t") entries = result.setdefault(module, []) require((version, filename) not in entries, "Duplicate artifact: " + filename) require(not entries or entries[0][0] == version, "Multiple versions: " + module) entries.append((version, filename)) for entries in result.values(): entries.sort() return result def compare(before, after): old, new = dependencies(before), dependencies(after) require(old.keys() == new.keys(), "Added/removed external modules: " + str(old.keys() ^ new.keys())) changed = {key: (old[key][0][0], new[key][0][0]) for key in old if old[key] != new[key]} require(changed == EXPECTED_CHANGES, "Unexpected dependency changes: " + str(changed)) return {"external_modules": len(new), "changes": changed} def inspect_war(path): owners = {name: [] for name in EXPECTED_CLASSES} with zipfile.ZipFile(path) as war: names = war.namelist() require(len(names) == len(set(names)), "Duplicate ZIP entries: " + str(path)) jars = sorted(name for name in names if name.endswith(".jar")) basenames = [Path(name).name for name in jars] require(len(basenames) == len(set(basenames)), "Duplicate JAR names: " + str(path)) require(not any(Path(name).name.startswith("application-local") and name.endswith(".yml") for name in names), "Local profile packaged: " + str(path)) for name in jars: basename = Path(name).name require(not basename.startswith(LOCAL_PREFIXES), "Excluded library packaged: " + name) require(not re.match(r"spring-[\w-]+-6\.", basename), "Spring 6 packaged: " + name) with zipfile.ZipFile(io.BytesIO(war.read(name))) as jar: classes = set(jar.namelist()) require(not any(c.startswith("jakarta/servlet/") for c in classes), "Jakarta Servlet classes packaged: " + name) for target in owners: if target in classes: owners[target].append(name) # Java 8 compatibility of each upgraded library's entry class. require(int.from_bytes(jar.read(target)[6:8], "big") <= 52, "Java >8 class: " + name + "!" + target) for target, expected in EXPECTED_CLASSES.items(): require(owners[target] == ["WEB-INF/lib/" + expected], "Wrong/duplicate class provider: " + target + " " + str(owners[target])) for pattern, expected in [ (r"thymeleaf-\d", "thymeleaf-3.1.5.RELEASE.jar"), (r"thymeleaf-spring\d-", "thymeleaf-spring5-3.1.5.RELEASE.jar"), (r"ognl-", "ognl-3.3.5.jar"), ]: require([n for n in basenames if re.match(pattern, n)] == [expected], "Wrong/duplicate library version for " + expected) return {"file": path.name, "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), "jar_count": len(jars), "class_providers": owners, "jars": jars} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--baseline", type=Path, required=True, help="Directory of baseline dependency TSVs") parser.add_argument("--current", type=Path, required=True, help="Directory of current dependency TSVs") parser.add_argument("--war", type=Path, action="append", required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() require(len(args.war) == 2 and len(set(args.war)) == 2, "Provide the standard WAR and bootWar") result = { "dependencies": {name: compare(args.baseline / (name + ".tsv"), args.current / (name + ".tsv")) for name in ["runtimeClasspath", "testRuntimeClasspath"]}, "wars": [inspect_war(path) for path in args.war], } args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n") print("PASS: dependency changes limited to three modules; both WARs verified") for war in result["wars"]: print(war["file"], war["sha256"]) if __name__ == "__main__": main()