Volver al índice
docs/publicacion/verify-final-jar.py
"""Compare a final JAR with its preserved baseline and current native source.
This is a packaging audit, not a substitute for the server's functional tests.
Only private build artifacts are written; production sources are never edited.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
import zipfile
from pathlib import Path
# INPUTS: the final JAR must be supplied explicitly after its build completes.
ROOT = Path(__file__).resolve().parents[2]
HERE = ROOT / 'build/jar-native-audit'
HARNESS = Path(__file__).resolve().parent / 'JarRigExportAudit.java'
SOURCE = ROOT / "src/main/java/ar/com/companeros/horror/client/ParasiteRig.java"
BASELINE = ROOT / "respaldos/antes-contactos-20261007-140143/Raps.jar"
JAVA_BIN = Path("C:/Program Files/Eclipse Adoptium/jdk-17.0.20.8-hotspot/bin")
CLIENT_PREFIX = "ar/com/companeros/horror/client/"
# Cambios visuales permitidos en esta revisión: el compositor de piel vive en
# identity, y las dos texturas son los atlas regenerados del huésped.
ALLOWED_CLIENT_ENTRIES = {"ar/com/companeros/identity/HostSkinAtlas.class"}
ALLOWED_RESOURCE_ENTRIES = {
"assets/companeros/textures/entity/parasite.png",
"assets/companeros/textures/entity/parasite_initial.png",
}
def sha(path):
result = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
result.update(chunk)
return result.hexdigest()
# ZIP: compare uncompressed entry bytes, independent of timestamps/compression.
def entries(path):
with zipfile.ZipFile(path) as archive:
return {info.filename: hashlib.sha256(archive.read(info)).hexdigest()
for info in archive.infolist() if not info.is_dir()}
def compare_zip(old, new):
before, after = entries(old), entries(new)
changed = [{"name": name, "before_sha256": before.get(name), "after_sha256": after.get(name)}
for name in sorted(set(before) | set(after)) if before.get(name) != after.get(name)]
backend = [item["name"] for item in changed
if item["name"].endswith(".class")
and not item["name"].startswith(CLIENT_PREFIX)
and item["name"] not in ALLOWED_CLIENT_ENTRIES]
resources = [item["name"] for item in changed
if not item["name"].endswith(".class")
and not item["name"].startswith("META-INF/")
and item["name"] not in ALLOWED_RESOURCE_ENTRIES]
return {"baseline_entries": len(before), "final_entries": len(after), "changed_entries": changed,
"backend_classes_changed": backend, "resources_changed": resources,
"backend_and_resources_identical": not backend and not resources}
# NATIVE: compile the actual source once; run identical public CLI calls in each.
def export(classpath, destination):
with destination.open("wb") as stream:
subprocess.run([str(JAVA_BIN / "java.exe"), "-cp", classpath, "JarRigExportAudit"],
stdout=stream, stderr=subprocess.PIPE, check=True, timeout=120)
def compare_exports(left, right):
rows = []
with left.open(encoding="utf-8") as original, right.open(encoding="utf-8") as packaged:
for line_a, line_b in zip(original, packaged, strict=True):
a, b = json.loads(line_a), json.loads(line_b)
rows.append({"mode": a["mode"], "stage": a["stage"], "sample_count": len(a["samples"]),
"identical_json": a == b, "identical_bytes": line_a == line_b})
if len(rows) != 70:
raise AssertionError(f"Expected70 mode/stage comparisons, received{len(rows)}")
return {"comparisons": len(rows), "native_frames_per_target": sum(row["sample_count"] for row in rows),
"all_json_identical": all(row["identical_json"] for row in rows),
"all_bytes_identical": all(row["identical_bytes"] for row in rows),
"source_export_sha256": sha(left), "jar_export_sha256": sha(right), "results": rows}
# REPORT: hash the final JAR and reject files that change during inspection.
def main():
HERE.mkdir(parents=True, exist_ok=True)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--jar", type=Path, required=True)
parser.add_argument("--baseline", type=Path, default=BASELINE)
parser.add_argument("--source-only", action="store_true",
help="Verificar equivalencia fuente/JAR; registrar diferencias de backend sin evaluarlas")
args = parser.parse_args()
jar, baseline = args.jar.resolve(), args.baseline.resolve()
stable = {path: sha(path) for path in (SOURCE, baseline, jar)}
harness, source_classes = HERE / "harness-classes", HERE / "source-classes"
harness.mkdir(exist_ok=True)
source_classes.mkdir(exist_ok=True)
subprocess.run([str(JAVA_BIN / "javac.exe"), "-encoding", "UTF-8", "-d", str(harness),
str(HARNESS)], check=True, capture_output=True, timeout=45)
subprocess.run([str(JAVA_BIN / "javac.exe"), "-encoding", "UTF-8", "-d", str(source_classes), str(SOURCE)],
check=True, capture_output=True, timeout=45)
source_output, jar_output = HERE / "source.jsonl", HERE / "packaged.jsonl"
export(str(source_classes) + ";" + str(harness), source_output)
export(str(jar) + ";" + str(harness), jar_output)
native = compare_exports(source_output, jar_output)
zip_audit = compare_zip(baseline, jar)
unchanged = all(sha(path) == expected for path, expected in stable.items())
passed = native["all_json_identical"] and unchanged and (args.source_only or zip_audit["backend_and_resources_identical"])
report = {"passed": passed, "jar": str(jar), "jar_sha256": stable[jar],
"baseline": str(baseline), "baseline_sha256": stable[baseline],
"native_source_sha256": stable[SOURCE], "inputs_stable": unchanged,
"native_cli": native, "zip_comparison": zip_audit,
"scope": "Packaging/source equivalence only; no server/GameTests executed by this harness",
"backend_comparison_enforced": not args.source_only}
destination = HERE / "result.json"
destination.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"passed": passed, "jar_sha256": stable[jar], "report_sha256": sha(destination),
"native_comparisons": native["comparisons"], "native_frames": native["native_frames_per_target"],
"all_json_identical": native["all_json_identical"], "changed_entries": [x["name"] for x in zip_audit["changed_entries"]],
"backend_classes_changed": zip_audit["backend_classes_changed"],
"resources_changed": zip_audit["resources_changed"]}, indent=2), flush=True)
if not passed:
raise SystemExit(1)
if __name__ == "__main__":
main()