port 2D steps 3-4: strict 40250 asset gate with a provenance manifest
tools/asset_manifest.py records path/size/sha256 of pack/Index, every .eix/.epk and Eternexus/root/*.msm (audit/assets/40250-client.json). The gate script sets MT_ASSETS_STRICT=1, verifies the manifest, runs the native pack/proto tests and four Godot tests, and fails on a missing client, a mismatch or a skip. MT_40250_CLIENT now comes from the environment for the native tests too. remote_player_test accepts both weighted WAIT variants (wait/wait_1). PORT-PLAN §5 records the 2D status. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Provenance manifest of the 40250 client assets the tests and the runtime read (docs/PORT-PLAN.md 2D step 3).
|
||||
|
||||
Records path (relative to the Client directory) + size + sha256 for pack/Index, every .eix/.epk in pack/ and the
|
||||
loose Eternexus/root/*.msm race specs. `verify` fails on a missing, changed or untracked file, so a strict
|
||||
run proves it read exactly the bytes the manifest names.
|
||||
|
||||
tools/asset_manifest.py generate <Client dir> [-o audit/assets/40250-client.json]
|
||||
tools/asset_manifest.py verify <Client dir> [-m audit/assets/40250-client.json]
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
DEFAULT = os.path.join(os.path.dirname(__file__), "..", "audit", "assets", "40250-client.json")
|
||||
|
||||
|
||||
def sha256(path):
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for block in iter(lambda: f.read(1 << 20), b""):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def tracked(client):
|
||||
files = ["pack/Index"]
|
||||
pack = os.path.join(client, "pack")
|
||||
files += sorted("pack/" + n for n in os.listdir(pack) if n.lower().endswith((".eix", ".epk")))
|
||||
root = os.path.join(client, "Eternexus", "root")
|
||||
if os.path.isdir(root):
|
||||
files += sorted("Eternexus/root/" + n for n in os.listdir(root) if n.lower().endswith(".msm"))
|
||||
return files
|
||||
|
||||
|
||||
def entry(client, rel):
|
||||
p = os.path.join(client, rel)
|
||||
return {"path": rel, "size": os.path.getsize(p), "sha256": sha256(p)}
|
||||
|
||||
|
||||
def generate(args):
|
||||
files = [entry(args.client, rel) for rel in tracked(args.client)]
|
||||
out = {
|
||||
"schema": 1,
|
||||
"source": "40250 Server Client TMP4/Client",
|
||||
"note": "path relative to the Client directory; regenerate with tools/asset_manifest.py generate",
|
||||
"files": files,
|
||||
}
|
||||
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(out, f, indent=1)
|
||||
f.write("\n")
|
||||
print("%d files, %d bytes -> %s" % (len(files), sum(e["size"] for e in files), args.output))
|
||||
return 0
|
||||
|
||||
|
||||
def verify(args):
|
||||
try:
|
||||
with open(args.manifest) as f:
|
||||
want = json.load(f)["files"]
|
||||
except (OSError, ValueError, KeyError) as e:
|
||||
print("asset_manifest: cannot read manifest %s: %s" % (args.manifest, e), file=sys.stderr)
|
||||
return 1
|
||||
if not os.path.isfile(os.path.join(args.client, "pack", "Index")):
|
||||
print("asset_manifest: no pack/Index under %s" % args.client, file=sys.stderr)
|
||||
return 1
|
||||
bad = 0
|
||||
for e in want:
|
||||
p = os.path.join(args.client, e["path"])
|
||||
if not os.path.isfile(p):
|
||||
print("missing %s" % e["path"], file=sys.stderr)
|
||||
bad += 1
|
||||
elif os.path.getsize(p) != e["size"] or sha256(p) != e["sha256"]:
|
||||
print("changed %s" % e["path"], file=sys.stderr)
|
||||
bad += 1
|
||||
extra = sorted(set(tracked(args.client)) - {e["path"] for e in want})
|
||||
for rel in extra:
|
||||
print("untracked %s" % rel, file=sys.stderr)
|
||||
bad += len(extra)
|
||||
print("%d files checked, %d mismatch(es)" % (len(want), bad))
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
g = sub.add_parser("generate")
|
||||
g.add_argument("client")
|
||||
g.add_argument("-o", "--output", default=DEFAULT)
|
||||
v = sub.add_parser("verify")
|
||||
v.add_argument("client")
|
||||
v.add_argument("-m", "--manifest", default=DEFAULT)
|
||||
args = ap.parse_args()
|
||||
return generate(args) if args.cmd == "generate" else verify(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user