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.
103 lines
4.7 KiB
Python
103 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Resolve every import of the macOS ARM64 slice against the iOS SDK's text stubs.
|
|
|
|
Usage: python3 tools/native_audit.py build/factorio.arm64 [--json out.json]
|
|
|
|
For each LC_LOAD_DYLIB the import is looked up in the iOS library with the same
|
|
framework name, following re-exported libraries. Imports that do not resolve
|
|
must be supplied by a shim. Nothing on the device is consulted, so this is a
|
|
link-level audit only; Objective-C selectors are not checked.
|
|
"""
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
SDK = Path(subprocess.check_output(["xcrun", "--sdk", "iphoneos", "--show-sdk-path"], text=True).strip())
|
|
|
|
|
|
def tbd_documents():
|
|
"""Yield (install_name, symbols, reexports) for every tbd document in the SDK."""
|
|
files = list((SDK / "usr/lib").rglob("*.tbd")) + list((SDK / "System/Library").rglob("*.tbd"))
|
|
for path in files:
|
|
text = path.read_text(errors="ignore")
|
|
for doc in re.split(r"^--- !tapi-tbd", text, flags=re.M):
|
|
name = re.search(r"^install-name:\s*'?([^'\n]+)'?", doc, re.M)
|
|
if not name:
|
|
continue
|
|
exports = doc.split("\nreexports:")[0].split("\nundefineds:")[0]
|
|
symbols = set()
|
|
for kind, prefix in (("symbols", ""), ("weak-symbols", ""), ("thread-local-symbols", ""),
|
|
("objc-classes", "class"), ("objc-eh-types", "eh"), ("objc-ivars", "ivar")):
|
|
for block in re.findall(rf"\b{kind}:\s*\[([^\]]*)\]", exports):
|
|
for item in block.split(","):
|
|
item = item.strip().strip("'\"")
|
|
if not item:
|
|
continue
|
|
if prefix == "class":
|
|
symbols.update({f"_OBJC_CLASS_$_{item}", f"_OBJC_METACLASS_$_{item}"})
|
|
elif prefix == "eh":
|
|
symbols.add(f"_OBJC_EHTYPE_$_{item}")
|
|
elif prefix == "ivar":
|
|
symbols.add(f"_OBJC_IVAR_$_{item}")
|
|
else:
|
|
symbols.add(item)
|
|
reexports = set()
|
|
for block in re.findall(r"reexported-libraries:.*?libraries:\s*\[([^\]]*)\]", doc, re.S):
|
|
reexports.update(x.strip().strip("'\"") for x in block.split(",") if x.strip())
|
|
yield name.group(1), symbols, reexports
|
|
|
|
|
|
def main():
|
|
binary = sys.argv[1]
|
|
libs = {}
|
|
for name, symbols, reexports in tbd_documents():
|
|
entry = libs.setdefault(name, [set(), set()])
|
|
entry[0] |= symbols
|
|
entry[1] |= reexports
|
|
|
|
def resolves(install, symbol, seen=()):
|
|
if install not in libs or install in seen:
|
|
return False
|
|
symbols, reexports = libs[install]
|
|
return symbol in symbols or any(resolves(r, symbol, seen + (install,)) for r in reexports)
|
|
|
|
def ios_path(mac_path):
|
|
if mac_path in libs: # e.g. IOKit keeps its Versions/A install name on iOS
|
|
return mac_path
|
|
m = re.match(r"/System/Library/Frameworks/(\w+)\.framework/", mac_path)
|
|
if m:
|
|
return f"/System/Library/Frameworks/{m.group(1)}.framework/{m.group(1)}"
|
|
return mac_path
|
|
|
|
loads = re.findall(r"cmd LC_LOAD(?:_WEAK)?_DYLIB\n\s+cmdsize \d+\n\s+name (\S+)",
|
|
subprocess.check_output(["otool", "-l", binary], text=True))
|
|
short = {Path(p).name.replace(".dylib", "").split(".")[0]: p for p in loads}
|
|
undef = subprocess.check_output(["nm", "-m", "-u", binary], text=True)
|
|
report = {}
|
|
for line in undef.splitlines():
|
|
m = re.search(r"external (\S+) \(from (\S+)\)", line)
|
|
if not m:
|
|
continue
|
|
symbol, lib = m.groups()
|
|
mac = short.get(lib)
|
|
target = ios_path(mac) if mac else None
|
|
status = "resolved" if target and resolves(target, symbol) else "missing"
|
|
lib_report = report.setdefault(lib, {"mac_path": mac, "ios_path": target,
|
|
"ios_library_exists": target in libs,
|
|
"resolved": 0, "missing": []})
|
|
if status == "resolved":
|
|
lib_report["resolved"] += 1
|
|
else:
|
|
lib_report["missing"].append(symbol)
|
|
for lib, r in sorted(report.items(), key=lambda kv: -len(kv[1]["missing"])):
|
|
print(f"{lib:20} exists={r['ios_library_exists']!s:5} resolved={r['resolved']:4} missing={len(r['missing']):3} "
|
|
+ " ".join(r["missing"][:12]) + (" ..." if len(r["missing"]) > 12 else ""))
|
|
if "--json" in sys.argv:
|
|
Path(sys.argv[sys.argv.index("--json") + 1]).write_text(json.dumps(report, indent=2) + "\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|