104 lines
5.2 KiB
Python
104 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Build, package and optionally publish the Apple Silicon game with shared data."""
|
|
import argparse
|
|
import gzip
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
def hash_file(path):
|
|
with path.open('rb') as stream:
|
|
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
|
|
|
|
|
def archive_bundle(bundle, output):
|
|
"""Write deterministic USTAR regular entries only, matching the launcher's reader."""
|
|
files = []
|
|
paths = sorted(bundle.rglob('*'))
|
|
for path in paths:
|
|
if path.is_symlink() or not (path.is_dir() or path.is_file()):
|
|
raise ValueError(f'Unsupported bundle entry: {path}')
|
|
if not path.is_file():
|
|
continue
|
|
relative = path.relative_to(bundle.parent).as_posix()
|
|
if any(c in relative for c in '\\:\r\n\0'):
|
|
raise ValueError(f'Invalid bundle filename: {relative}')
|
|
files.append({'path': relative, 'size': path.stat().st_size,
|
|
'sha256': hash_file(path), 'executable': bool(path.stat().st_mode & 0o111)})
|
|
if len(files) > 10000 or sum(f['size'] for f in files) > 2 * 1024**3:
|
|
raise ValueError('Bundle exceeds launcher extraction limits')
|
|
archive = output / 'MoonWell.app.tar.gz'
|
|
with archive.open('xb') as raw:
|
|
with gzip.GzipFile(filename='', fileobj=raw, mode='wb', mtime=0) as compressed:
|
|
with tarfile.open(fileobj=compressed, mode='w', format=tarfile.USTAR_FORMAT) as tar:
|
|
for file in files:
|
|
entry = tarfile.TarInfo(file['path'])
|
|
entry.size = file['size']
|
|
entry.mode = 0o755 if file['executable'] else 0o644
|
|
with (bundle.parent / file['path']).open('rb') as stream:
|
|
tar.addfile(entry, stream)
|
|
if archive.stat().st_size > 1024**3:
|
|
raise ValueError('Bundle archive exceeds launcher download limit')
|
|
descriptor = {'path': archive.name, 'size': archive.stat().st_size,
|
|
'sha256': hash_file(archive), 'files': files}
|
|
(output / 'bundle.json').write_text(json.dumps(descriptor, indent=2) + '\n')
|
|
return descriptor
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--icon', type=Path, required=True)
|
|
parser.add_argument('--binary', type=Path, help='Use an existing arm64 player build')
|
|
parser.add_argument('--output', type=Path)
|
|
parser.add_argument('--env-file', type=Path)
|
|
parser.add_argument('--shared-manifest-key', default='manifest-1.12.1-data.json')
|
|
parser.add_argument('--manifest-key', default='manifest-1.12.1-macos-arm64.json')
|
|
parser.add_argument('--prefix', default='World of Warcraft 1.12.1')
|
|
mode = parser.add_mutually_exclusive_group()
|
|
mode.add_argument('--publish', action='store_true')
|
|
mode.add_argument('--dry-run', action='store_true')
|
|
args = parser.parse_args()
|
|
if sys.platform != 'darwin':
|
|
parser.error('Build and package on an Apple Silicon Mac')
|
|
if args.manifest_key in ('manifest.json', 'manifest-1.12.1.json', args.shared_manifest_key):
|
|
parser.error('Mac manifest key must be separate from Windows and shared data')
|
|
if (args.publish or args.dry_run) and not args.env_file:
|
|
parser.error('--env-file is required for publication and dry-run')
|
|
root = Path(__file__).resolve().parents[1]
|
|
output = (args.output or root / 'dist' / ('moonwell-macos-' + datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S'))).resolve()
|
|
output.mkdir(parents=True, exist_ok=False)
|
|
if not args.binary:
|
|
subprocess.run(['cargo', 'build', '-p', 'moonwell', '--release', '--no-default-features', '--locked',
|
|
'--target', 'aarch64-apple-darwin'], cwd=root, check=True)
|
|
binary = (args.binary or root / 'target/aarch64-apple-darwin/release/MoonWell').resolve()
|
|
bundle = output / 'build' / 'MoonWell.app'
|
|
subprocess.run([sys.executable, str(root / 'crates/moonwell/package_macos.py'),
|
|
'--binary', str(binary), '--icon', str(args.icon.resolve()), '--output', str(bundle)], check=True)
|
|
# A non-system library must be bundled and signed deliberately, not assumed present on player Macs.
|
|
dependencies = subprocess.check_output(['otool', '-L', str(bundle / 'Contents/MacOS/MoonWell')], text=True).splitlines()[1:]
|
|
for line in dependencies:
|
|
library = line.strip().split(' (', 1)[0]
|
|
if not library.startswith(('/System/Library/', '/usr/lib/')):
|
|
raise RuntimeError(f'Unbundled runtime dependency: {library}')
|
|
archive_bundle(bundle, output)
|
|
if args.publish or args.dry_run:
|
|
command = ['cargo', 'run', '-p', 'moonwell-deploy', '--release', '--locked', '--', 'publish-macos',
|
|
'--dir', str(output), '--env-file', str(args.env_file.resolve()),
|
|
'--shared-manifest-key', args.shared_manifest_key, '--manifest-key', args.manifest_key,
|
|
'--prefix', args.prefix]
|
|
if args.dry_run:
|
|
command.append('--dry-run')
|
|
subprocess.run(command, cwd=root, check=True)
|
|
print(f'Mac package: {output}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|