Run the original macOS ARM64 game code on iOS: - tools/retarget.py: header-only Mach-O retargeting to an iOS dylib - platform/native: UIKit host and macOS API shims (AppKit for SDL's Cocoa backend, CoreGraphics displays, CoreAudio device, Metal device properties, Wuffs PNG decoding, Steam and peripheral stubs) - touch, overlay, iOS keyboard and hardware keyboard input - tools/build_native.py: build, sign, install, data copy, launch - docs: build guide, controls, architecture Also includes the earlier static-translation research (ARM64 -> WASM -> native) under docs/research, runtime/, tests/ and tools/. No game files are included; the build reads them from a local install.
82 lines
4.0 KiB
Python
82 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Record exact local executable and installed data identities, without copying assets."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import plistlib
|
|
import subprocess
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from lift_rng import arm64_image
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
EXPECTED_BINARY = '00b014d78df07e05ab1cee853904c8f7b3814ca652c043461a9d49fbad46cdf6'
|
|
EXPECTED_SLICE = '2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7'
|
|
|
|
|
|
def digest(path):
|
|
result = hashlib.sha256()
|
|
with path.open('rb') as source:
|
|
for block in iter(lambda: source.read(1024 * 1024), b''):
|
|
result.update(block)
|
|
return result.hexdigest()
|
|
|
|
|
|
def command(*args):
|
|
return subprocess.check_output(args, cwd=ROOT, text=True).strip()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('app', type=Path)
|
|
args = parser.parse_args()
|
|
app = args.app.resolve(strict=True)
|
|
binary = app / 'Contents/MacOS/factorio'
|
|
source = binary.read_bytes()
|
|
image, offset = arm64_image(source)
|
|
binary_hash, slice_hash = hashlib.sha256(source).hexdigest(), hashlib.sha256(image).hexdigest()
|
|
if binary_hash != EXPECTED_BINARY or slice_hash != EXPECTED_SLICE:
|
|
raise ValueError('Target differs from the inspected Factorio 2.0.77 build')
|
|
info = plistlib.loads((app / 'Contents/Info.plist').read_bytes())
|
|
assets = app / 'Contents/data'
|
|
entries = []
|
|
for path in sorted(assets.rglob('*')):
|
|
if path.is_symlink():
|
|
raise ValueError(f'Asset symlink requires explicit inventory policy: {path}')
|
|
if path.is_file():
|
|
entries.append({'path': path.relative_to(assets).as_posix(),
|
|
'bytes': path.stat().st_size, 'sha256': digest(path)})
|
|
if not entries:
|
|
raise ValueError('No installed data files found')
|
|
packages = []
|
|
for path in sorted(assets.glob('*/info.json')):
|
|
metadata = json.loads(path.read_text())
|
|
packages.append({'directory': path.parent.name, 'name': metadata.get('name'),
|
|
'version': metadata.get('version'), 'dependencies': metadata.get('dependencies')})
|
|
asset_manifest = {'scope': 'all regular files under installed Contents/data; no user mods or saves',
|
|
'files': entries, 'file_count': len(entries),
|
|
'total_bytes': sum(e['bytes'] for e in entries), 'packages': packages,
|
|
'gameplay_configuration': 'not selected; inventory does not prove save compatibility'}
|
|
out = ROOT / 'build/target'
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
asset_path = out / 'assets.json'
|
|
asset_path.write_text(json.dumps(asset_manifest, indent=2) + '\n')
|
|
report = {'recorded_at': datetime.now(timezone.utc).isoformat(), 'app': str(app),
|
|
'version': info.get('CFBundleShortVersionString'), 'binary_sha256': binary_hash,
|
|
'arm64_sha256': slice_hash, 'arm64_slice_offset': offset, 'arm64_bytes': len(image),
|
|
'binary_uuids': command('xcrun', 'dwarfdump', '--uuid', str(binary)).splitlines(),
|
|
'xcode': command('xcodebuild', '-version'),
|
|
'iphoneos_sdk': command('xcrun', '--sdk', 'iphoneos', '--show-sdk-version'),
|
|
'wabt_revision': command('git', '-C', str(ROOT / 'third_party/wabt'), 'rev-parse', 'HEAD'),
|
|
'asset_manifest_sha256': digest(asset_path), 'asset_file_count': len(entries),
|
|
'source_files': {str(p.relative_to(ROOT)): digest(p) for base in ('tools', 'runtime', 'platform/ios', 'tests')
|
|
for p in sorted((ROOT / base).glob('*'))
|
|
if p.is_file() and p.suffix in ('.py', '.mjs', '.c', '.h', '.m', '.yml')}}
|
|
(out / 'manifest.json').write_text(json.dumps(report, indent=2) + '\n')
|
|
print(f'PASS: pinned executable and ARM64 slice; recorded {len(entries)} asset files ({asset_manifest["total_bytes"]} bytes)')
|
|
print(out / 'manifest.json')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|