Native iPhone port of Factorio 2.0.77
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.
This commit is contained in:
commit
51d085e865
89 files changed
+7999
No files matched your search
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Retarget the unmodified macOS ARM64 slice into an iOS-loadable dylib.
|
||||
|
||||
Usage: python3 tools/retarget.py build/factorio.arm64 build/native/Factorio.dylib
|
||||
|
||||
Only Mach-O metadata changes; every instruction and data byte stays the same:
|
||||
* MH_EXECUTE -> MH_DYLIB, plus an LC_ID_DYLIB in the header padding.
|
||||
* __PAGEZERO shrinks to one page just below __TEXT, so dyld can slide the image.
|
||||
Segment indices stay unchanged, so rebase/bind opcodes remain valid.
|
||||
* LC_BUILD_VERSION platform macOS -> iOS.
|
||||
* macOS framework paths -> iOS paths, or to @loader_path shims for frameworks
|
||||
(or individual symbols) that iOS lacks. Shims re-export the real framework
|
||||
when one exists.
|
||||
The host app reads LC_MAIN at runtime to find the original entry point.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
EXPECTED_SHA256 = "2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7"
|
||||
MH_MAGIC_64, MH_EXECUTE, MH_DYLIB = 0xFEEDFACF, 2, 6
|
||||
MH_NO_REEXPORTED_DYLIBS = 0x100000
|
||||
LC_SEGMENT_64, LC_ID_DYLIB, LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB = 0x19, 0xD, 0xC, 0x80000018
|
||||
LC_BUILD_VERSION, LC_MAIN = 0x32, 0x80000028
|
||||
PLATFORM_IOS = 2
|
||||
MIN_IOS = (17 << 16)
|
||||
SDK_IOS = (26 << 16) | (5 << 8)
|
||||
INSTALL_NAME = "@rpath/Factorio.dylib"
|
||||
|
||||
# Framework name -> replacement install name. Absent: use the iOS path of the same framework.
|
||||
SHIMS = {
|
||||
"AppKit": "@loader_path/fx_AppKit.dylib",
|
||||
"Cocoa": "@loader_path/fx_Cocoa.dylib",
|
||||
"Carbon": "@loader_path/fx_Carbon.dylib",
|
||||
"OpenGL": "@loader_path/fx_OpenGL.dylib",
|
||||
"OpenCL": "@loader_path/fx_OpenCL.dylib",
|
||||
"ForceFeedback": "@loader_path/fx_ForceFeedback.dylib",
|
||||
"LDAP": "@loader_path/fx_LDAP.dylib",
|
||||
"ApplicationServices": "@loader_path/fx_ApplicationServices.dylib",
|
||||
# Present on iOS but missing some imported symbols: shim re-exports the real framework.
|
||||
"CoreGraphics": "@loader_path/fx_CoreGraphics.dylib",
|
||||
"CoreVideo": "@loader_path/fx_CoreVideo.dylib",
|
||||
"CoreServices": "@loader_path/fx_CoreServices.dylib",
|
||||
"Security": "@loader_path/fx_Security.dylib",
|
||||
"Metal": "@loader_path/fx_Metal.dylib",
|
||||
"Foundation": "@loader_path/fx_Foundation.dylib",
|
||||
"IOKit": "@loader_path/fx_IOKit.dylib",
|
||||
"CoreAudio": "@loader_path/fx_CoreAudio.dylib",
|
||||
"AudioToolbox": "@loader_path/fx_AudioToolbox.dylib",
|
||||
}
|
||||
|
||||
|
||||
def cstr(data, offset):
|
||||
return data[offset:data.index(b"\0", offset)].decode()
|
||||
|
||||
|
||||
def main():
|
||||
source, output = Path(sys.argv[1]), Path(sys.argv[2])
|
||||
data = bytearray(source.read_bytes())
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
if digest != EXPECTED_SHA256:
|
||||
raise SystemExit(f"unexpected input {digest}; only the inspected 2.0.77 ARM64 slice is supported")
|
||||
magic, _cpu, _sub, filetype, ncmds, sizeofcmds, flags, _ = struct.unpack_from("<IiiIIIII", data, 0)
|
||||
if magic != MH_MAGIC_64 or filetype != MH_EXECUTE:
|
||||
raise SystemExit("input is not a 64-bit Mach-O executable")
|
||||
|
||||
report = {"input_sha256": digest, "dylibs": [], "changes": []}
|
||||
offset, first_section = 32, None
|
||||
text_vmaddr = None
|
||||
for _ in range(ncmds):
|
||||
cmd, cmdsize = struct.unpack_from("<II", data, offset)
|
||||
if cmd == LC_SEGMENT_64:
|
||||
segname = data[offset + 8:offset + 24].rstrip(b"\0").decode()
|
||||
vmaddr, vmsize, fileoff, filesize = struct.unpack_from("<QQQQ", data, offset + 24)
|
||||
nsects = struct.unpack_from("<I", data, offset + 64)[0]
|
||||
if segname == "__TEXT":
|
||||
text_vmaddr = vmaddr
|
||||
for s in range(nsects):
|
||||
sect_offset = struct.unpack_from("<I", data, offset + 72 + s * 80 + 48)[0]
|
||||
if sect_offset and (first_section is None or sect_offset < first_section):
|
||||
first_section = sect_offset
|
||||
if segname == "__PAGEZERO":
|
||||
pagezero = offset
|
||||
elif cmd == LC_BUILD_VERSION:
|
||||
platform, minos, sdk = struct.unpack_from("<III", data, offset + 8)
|
||||
struct.pack_into("<III", data, offset + 8, PLATFORM_IOS, MIN_IOS, SDK_IOS)
|
||||
report["changes"].append({"build_version": [platform, hex(minos), hex(sdk)],
|
||||
"to": [PLATFORM_IOS, hex(MIN_IOS), hex(SDK_IOS)]})
|
||||
elif cmd == LC_MAIN:
|
||||
report["entryoff"] = struct.unpack_from("<Q", data, offset + 8)[0]
|
||||
elif cmd in (LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB):
|
||||
name_offset = struct.unpack_from("<I", data, offset + 8)[0]
|
||||
old = cstr(data, offset + name_offset)
|
||||
framework = None
|
||||
if old.startswith("/System/Library/Frameworks/"):
|
||||
framework = old.split("/")[4].removesuffix(".framework")
|
||||
if framework in SHIMS:
|
||||
new = SHIMS[framework]
|
||||
elif framework == "IOKit":
|
||||
new = old
|
||||
elif framework:
|
||||
new = f"/System/Library/Frameworks/{framework}.framework/{framework}"
|
||||
else:
|
||||
new = old # libSystem, libobjc, libresolv, @loader_path/libsteam_api.dylib
|
||||
room = cmdsize - name_offset
|
||||
encoded = new.encode() + b"\0"
|
||||
if len(encoded) > room:
|
||||
raise SystemExit(f"{new} does not fit in {room} bytes")
|
||||
data[offset + name_offset:offset + cmdsize] = encoded.ljust(room, b"\0")
|
||||
report["dylibs"].append({"from": old, "to": new, "weak": cmd == LC_LOAD_WEAK_DYLIB})
|
||||
offset += cmdsize
|
||||
|
||||
if text_vmaddr is None or first_section is None:
|
||||
raise SystemExit("__TEXT segment not found")
|
||||
# Shrink __PAGEZERO to a single 16 KiB page directly below __TEXT.
|
||||
struct.pack_into("<QQ", data, pagezero + 24, text_vmaddr - 0x4000, 0x4000)
|
||||
report["changes"].append({"pagezero": [hex(text_vmaddr - 0x4000), "0x4000"]})
|
||||
|
||||
# Append LC_ID_DYLIB in the padding before the first section.
|
||||
name = INSTALL_NAME.encode() + b"\0"
|
||||
id_size = (24 + len(name) + 7) & ~7
|
||||
end = 32 + sizeofcmds
|
||||
if end + id_size > first_section:
|
||||
raise SystemExit("no header padding for LC_ID_DYLIB")
|
||||
if any(data[end:end + id_size]):
|
||||
raise SystemExit("header padding is not empty")
|
||||
struct.pack_into("<IIIIII", data, end, LC_ID_DYLIB, id_size, 24, 2, 0x10000, 0x10000)
|
||||
data[end + 24:end + 24 + len(name)] = name
|
||||
struct.pack_into("<IIIII", data, 12, MH_DYLIB, ncmds + 1, sizeofcmds + id_size,
|
||||
flags | MH_NO_REEXPORTED_DYLIBS, 0)
|
||||
report["changes"].append({"filetype": "MH_EXECUTE->MH_DYLIB", "install_name": INSTALL_NAME})
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_bytes(data)
|
||||
report["output_sha256"] = hashlib.sha256(data).hexdigest()
|
||||
changed = sum(1 for a, b in zip(data, source.read_bytes()) if a != b)
|
||||
report["changed_bytes"] = changed
|
||||
report["changed_bytes_outside_header"] = sum(
|
||||
1 for i, (a, b) in enumerate(zip(data[first_section:], source.read_bytes()[first_section:])) if a != b)
|
||||
if report["changed_bytes_outside_header"]:
|
||||
raise SystemExit("retarget changed bytes outside the Mach-O header")
|
||||
(output.parent / "retarget.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(json.dumps({k: v for k, v in report.items() if k != "dylibs"}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user