70 lines
5.8 KiB
Python
70 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Build an offline Mac ZIP with two hash-pinned official runtimes and executable modes."""
|
|
import argparse,hashlib,importlib.util,json,stat,struct,tarfile,zipfile
|
|
from pathlib import Path
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
spec=importlib.util.spec_from_file_location('windows_packager',ROOT/'scripts/package-editor-windows.py')
|
|
common=importlib.util.module_from_spec(spec);spec.loader.exec_module(common)
|
|
VERSION='22.23.3'
|
|
HASHES={'arm64':'23b25245dcfb9af7262f8ff142e9e2e0af025368117329e7a7458a51e5922f53','x64':'8a677b0219178efd6eb0e475457c4afb452b521a92f6e67845a73bd85727f2a8'}
|
|
NAME='ColorBlockEditor-Mac-0.19.3-20261019'
|
|
def sha(data):return hashlib.sha256(data).hexdigest()
|
|
def put(root,rel,data,mode=0o644):
|
|
p=root/rel;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(data);p.chmod(mode)
|
|
def verify(archive):
|
|
result=common.verify(archive)
|
|
with zipfile.ZipFile(archive) as z:
|
|
m=json.loads(z.read(NAME+'/PACKAGE_MANIFEST.json'))
|
|
for r in m['files']:
|
|
assert stat.S_IMODE(z.getinfo(NAME+'/'+r['path']).external_attr>>16)==r['mode'],r['path']
|
|
assert all(r['mode']==0o755 for r in m['files'] if r['path'].endswith('.command') or r['path'].endswith('/node'))
|
|
result['executableModesVerified']=True
|
|
return result
|
|
def build(args):
|
|
runtimes={}
|
|
for arch in HASHES:
|
|
archive=getattr(args,arch);assert sha(archive.read_bytes())==HASHES[arch],f'{arch} official SHA mismatch'
|
|
prefix=f'node-v{VERSION}-darwin-{arch}'
|
|
with tarfile.open(archive,'r:gz') as t:
|
|
binary=t.extractfile(prefix+'/bin/node').read();license=t.extractfile(prefix+'/LICENSE').read()
|
|
assert binary[:4]==b'\xcf\xfa\xed\xfe'
|
|
assert struct.unpack_from('<I',binary,4)[0]=={'arm64':0x100000c,'x64':0x1000007}[arch]
|
|
runtimes[arch]=(binary,license)
|
|
target=args.out/NAME;archive=args.out/(NAME+'.zip')
|
|
if target.exists() or archive.exists():raise FileExistsError('Refusing to replace existing release')
|
|
target.mkdir(parents=True)
|
|
for arch,(binary,license) in runtimes.items():
|
|
put(target,f'runtime/darwin-{arch}/node',binary,0o755);put(target,f'runtime/darwin-{arch}/LICENSE',license)
|
|
meta={'version':VERSION,'arch':arch,'download':f'https://nodejs.org/dist/v{VERSION}/node-v{VERSION}-darwin-{arch}.tar.gz','officialRelease':'https://nodejs.org/en/blog/release/v22.23.3','archiveSHA256':HASHES[arch],'binarySHA256':sha(binary),'verifiedAgainstOfficialReleasePage':True}
|
|
put(target,f'runtime/darwin-{arch}/SOURCE.json',(json.dumps(meta,indent=2)+'\n').encode())
|
|
versions={}
|
|
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():common.copy_tree(ROOT/app/folder,target/app/folder)
|
|
put(target,app+'/package.json',(ROOT/app/'package.json').read_bytes());versions[app]=json.loads((ROOT/app/'package.json').read_text())['version']
|
|
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 [(args.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]=common.copy_levels(source,target/dest)
|
|
put(target,'game-data/assets/Script/module/Config/LevelTimeBonusConfig.ts',(args.game/'assets/Script/module/Config/LevelTimeBonusConfig.ts').read_bytes())
|
|
for name in ['StartEditor.command','启动关卡编辑器.command']:put(target,name,(ROOT/'scripts/macos/StartEditor.command').read_bytes(),0o755)
|
|
for name in ['StartEditor.mjs','使用说明.txt']:put(target,name,(ROOT/'scripts/macos'/name).read_bytes())
|
|
put(target,'colorBlock-level-editor/drafts/README.txt',b'Your saved drafts appear here. Keep this folder when updating.\n')
|
|
files=[]
|
|
for p in sorted(target.rglob('*')):
|
|
if p.is_file():
|
|
mode=0o755 if p.name.endswith('.command') or p.name=='node' else 0o644;p.chmod(mode)
|
|
files.append({'path':p.relative_to(target).as_posix(),'bytes':p.stat().st_size,'sha256':sha(p.read_bytes()),'mode':mode})
|
|
manifest={'platform':'macOS','architectures':['arm64','x64'],'minimumOS':'11.0','editor':versions['colorBlock-level-editor'],'generator':versions['colorBlock-level-generator'],'checker':versions['colorBlock-similarity-checker'],'date':'2026-10-19','runtime':VERSION,'levelCounts':counts,'excluded':['personal settings','draft samples','Git','caches','test outputs','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),'versions':versions,'levelCounts':counts})
|
|
(args.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('--arm64',type=Path);parser.add_argument('--x64',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)
|