#!/usr/bin/env python3 """Package a clean, committed source tree and optional Linux executables. No user worlds, tokens, Cargo caches, Minecraft JAR, or untracked files enter an archive. Every extracted file is checked against the in-archive manifest. """ import argparse import gzip import hashlib import io import json import subprocess import tarfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def git(*args): return subprocess.check_output(['git', *args], cwd=ROOT) def main(): parser = argparse.ArgumentParser() parser.add_argument('--binaries', action='store_true') args = parser.parse_args() if git('status', '--porcelain').strip(): raise SystemExit('Commit the reviewed tree before packaging.') revision = git('rev-parse', 'HEAD').decode().strip() epoch = int(git('show', '-s', '--format=%ct', 'HEAD')) paths = [Path(p.decode()) for p in git('ls-files', '-z').split(b'\0') if p] if args.binaries: paths += [Path('target/release') / name for name in ['shacraft-server', 'shacraft-mcp', 'shacraft-compat', 'shacraft-tools']] files = [] for relative in sorted(paths): path = ROOT / relative if path.is_symlink() or not path.is_file(): raise SystemExit(f'Only regular files are distributable: {relative}') if relative.name.endswith('.token') or relative.name == '.env': raise SystemExit(f'Secret filename cannot be distributed: {relative}') data = path.read_bytes() files.append((relative.as_posix(), data, 0o755 if path.stat().st_mode & 0o111 else 0o644)) manifest = {'project':'Shacraft Core','git_revision':revision,'source_epoch':epoch, 'kind':'linux-x86_64' if args.binaries else 'source', 'license':'MIT OR Apache-2.0', 'files':{name:{'sha256':hashlib.sha256(data).hexdigest(),'size':len(data)} for name,data,_ in files}} files.append(('RELEASE.json', (json.dumps(manifest,indent=2)+'\n').encode(), 0o644)) output = ROOT / 'artifacts' / f"shacraft-core-mvp-{revision[:8]}-{manifest['kind']}.tar.gz" output.parent.mkdir(exist_ok=True) with output.open('wb') as raw, gzip.GzipFile(fileobj=raw,mode='wb',mtime=0) as zipped: with tarfile.open(fileobj=zipped,mode='w') as archive: for name,data,mode in files: info=tarfile.TarInfo('shacraft-core/'+name) info.size=len(data);info.mode=mode;info.mtime=epoch archive.addfile(info,io.BytesIO(data)) digest=hashlib.sha256(output.read_bytes()).hexdigest() output.with_name(output.name+'.sha256').write_text(f'{digest} {output.name}\n') with tarfile.open(output,'r:gz') as archive: for name,data,_ in files: actual=archive.extractfile('shacraft-core/'+name).read() if actual!=data: raise SystemExit(f'Archive verification failed for {name}') print(json.dumps({'archive':str(output),'sha256':digest,'bytes':output.stat().st_size, 'git_revision':revision,'verified_files':len(files)},indent=2)) if __name__=='__main__': main()