#!/usr/bin/env python3 """Build a self-contained Windows editor bundle from explicit runtime/source inputs.""" import argparse,hashlib,json,re,shutil,struct,zipfile from pathlib import Path ROOT=Path(__file__).resolve().parents[1] NODE_VERSION='22.23.3' ZIP_SHA='2b0ff57b049cda1bbcea2240eec20467018713c1efe1f7360c2681859b90ed71' EXE_SHA='9c9245166b4a8e182e0b797da9c20136117ff24368eaff1fec8343a123c8db0e' NAME='ColorBlockEditor-Windows-x64-0.20.2-20261020-r2' def sha(data):return hashlib.sha256(data).hexdigest() def put(target,relative,data): p=target/relative;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(data) def copy_tree(source,dest): for p in sorted(source.rglob('*')): if not p.is_file() or p.is_symlink() or any(x.startswith('.') or x=='node_modules' for x in p.relative_to(source).parts):continue out=dest/p.relative_to(source);out.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(p,out) def copy_levels(source,target): files=sorted(p for p in source.iterdir() if re.fullmatch(r'level\d+\.json',p.name) and p.is_file() and not p.is_symlink()) if not files:raise ValueError('No levels found: '+str(source)) for p in files:put(target,p.name,p.read_bytes()) return len(files) def verify(archive): with zipfile.ZipFile(archive) as z: assert z.testzip() is None package_name=next(n.rsplit('/',1)[0] for n in z.namelist() if n.endswith('/PACKAGE_MANIFEST.json')) manifest=json.loads(z.read(package_name+'/PACKAGE_MANIFEST.json')) expected={package_name+'/'+r['path'] for r in manifest['files']}|{package_name+'/PACKAGE_MANIFEST.json'} assert set(z.namelist())==expected for r in manifest['files']: data=z.read(package_name+'/'+r['path']);assert len(data)==r['bytes'] and sha(data)==r['sha256'],r['path'] return {'files':len(manifest['files']),'sha256':sha(archive.read_bytes()),'bytes':archive.stat().st_size} def build(node_zip,game,out): raw=node_zip.read_bytes();assert sha(raw)==ZIP_SHA,'Node ZIP differs from official release hash' target=out/NAME;archive=out/(NAME+'.zip') if target.exists() or archive.exists():raise FileExistsError('Refusing to replace an existing package') target.mkdir(parents=True) with zipfile.ZipFile(node_zip) as z: exe=z.read('node-v'+NODE_VERSION+'-win-x64/node.exe') assert sha(exe)==EXE_SHA and exe[:2]==b'MZ' pe=struct.unpack_from('