76 lines
6.0 KiB
Python
76 lines
6.0 KiB
Python
#!/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('<I',exe,0x3c)[0]
|
|
assert exe[pe:pe+4]==b'PE\0\0' and struct.unpack_from('<H',exe,pe+4)[0]==0x8664
|
|
put(target,'runtime/node.exe',exe)
|
|
put(target,'runtime/LICENSE',z.read('node-v'+NODE_VERSION+'-win-x64/LICENSE'))
|
|
runtime={'version':NODE_VERSION,'platform':'win-x64','officialRelease':'https://nodejs.org/en/blog/release/v22.23.3','download':'https://nodejs.org/dist/v22.23.3/node-v22.23.3-win-x64.zip','archiveSHA256':ZIP_SHA,'nodeExeSHA256':EXE_SHA,'verifiedAgainstOfficialReleasePage':True}
|
|
put(target,'runtime/SOURCE.json',(json.dumps(runtime,indent=2)+'\n').encode())
|
|
for app in ['colorBlock-level-editor','colorBlock-level-generator','colorBlock-similarity-checker']:
|
|
for folder in ['src','web','toolkit-web','examples']:
|
|
if (ROOT/app/folder).exists():copy_tree(ROOT/app/folder,target/app/folder)
|
|
put(target,app+'/package.json',(ROOT/app/'package.json').read_bytes())
|
|
for guide in ['SCREENSHOT_RECOGNITION_EXPLAINED.md','SCREENSHOT_RECOGNITION_EXPLAINED.html','PETAL_EDITOR_0132.md','WOOD_DRAFTS_EXPORT_0140.md','BOUNDARIES_0150.md','BOUNDARIES_0150.html']:
|
|
put(target,'colorBlock-level-editor/docs/'+guide,(ROOT/'colorBlock-level-editor/docs'/guide).read_bytes())
|
|
counts={}
|
|
for source,dest in [(game/'assets/custom/Json','game-data/assets/custom/Json'),(ROOT/'levels','levels'),(ROOT/'colorBlock-similarity-checker/levels','colorBlock-similarity-checker/levels'),(ROOT/'colorBlock/assets/custom/Json','colorBlock/assets/custom/Json')]:counts[dest]=copy_levels(source,target/dest)
|
|
put(target,'game-data/assets/Script/module/Config/LevelTimeBonusConfig.ts',(game/'assets/Script/module/Config/LevelTimeBonusConfig.ts').read_bytes())
|
|
put(target,'StartEditor.mjs',(ROOT/'scripts/windows/StartEditor.mjs').read_bytes())
|
|
bat=(ROOT/'scripts/windows/StartEditor.bat').read_text(encoding='utf-8').replace('\r\n','\n').replace('\n','\r\n').encode('ascii')
|
|
for name in ['StartEditor.bat','启动关卡编辑器.bat']:put(target,name,bat)
|
|
manual=(ROOT/'scripts/windows/使用说明.txt').read_text(encoding='utf-8');put(target,'使用说明.txt',manual.replace('\n','\r\n').encode('utf-8-sig'))
|
|
put(target,'colorBlock-level-editor/drafts/README.txt',b'Your saved drafts will appear here. Keep this folder when updating.\r\n')
|
|
files=[]
|
|
for p in sorted(target.rglob('*')):
|
|
if p.is_file():
|
|
data=p.read_bytes();files.append({'path':p.relative_to(target).as_posix(),'bytes':len(data),'sha256':sha(data)})
|
|
manifest={'editor':'0.20.2','generator':'0.21.0','checker':'1.1.3','date':'2026-10-20','runtime':runtime,'levelCounts':counts,'excluded':['personal settings','draft samples','Git','caches','test outputs','project conversation history','full Cocos project'],'files':files}
|
|
put(target,'PACKAGE_MANIFEST.json',(json.dumps(manifest,ensure_ascii=False,indent=2)+'\n').encode())
|
|
with zipfile.ZipFile(archive,'x',zipfile.ZIP_DEFLATED,compresslevel=6) as z:
|
|
for p in sorted(target.rglob('*')):
|
|
if p.is_file():z.write(p,NAME+'/'+p.relative_to(target).as_posix())
|
|
result=verify(archive);result.update({'archive':str(archive),'directory':str(target),'levelCounts':counts})
|
|
(out/(NAME+'.sha256')).write_text(result['sha256']+' '+archive.name+'\n')
|
|
print(json.dumps(result,ensure_ascii=False,indent=2))
|
|
if __name__=='__main__':
|
|
parser=argparse.ArgumentParser();parser.add_argument('--node-zip',type=Path);parser.add_argument('--game',type=Path);parser.add_argument('--out',type=Path);parser.add_argument('--verify',type=Path);args=parser.parse_args()
|
|
if args.verify:print(json.dumps(verify(args.verify),indent=2))
|
|
else:build(args.node_zip,args.game,args.out)
|