Simple install guide and one-step install.sh

- README rewritten as a step-by-step guide for non-developers
- install.sh: build, install and copy data with plain error messages
- build_native.py: bundle identifier from the user's provisioning
  profile (Xcode project named factorio), version check on every
  build, friendly errors
This commit is contained in:
SashaGoncharov19 committed 2026-10-23 15:40:18 +04:00
1 parent 51d085e865
commit 30b961c923
4 files changed
+227 -133

No files matched your search

+48 -22
View File
@@ -12,7 +12,8 @@ Usage:
Options (each also readable from the environment variable shown):
--game PATH factorio.app/Contents of a local install (FX_GAME; default: Steam location)
--team ID Apple developer team ID (FX_TEAM; default: from your provisioning profiles)
--bundle-id ID app bundle identifier (FX_BUNDLE_ID; default: local.factorio.native)
--bundle-id ID app bundle identifier (FX_BUNDLE_ID; default: the App ID of a provisioning profile
containing "factorio", else local.factorio.native with a wildcard profile)
--device ID devicectl device identifier or name (FX_DEVICE; default: first paired device)
The game executable is the unmodified ARM64 slice with only Mach-O header changes (see
@@ -24,6 +25,7 @@ import json
import os
import plistlib
import subprocess
import time
import sys
import urllib.request
from pathlib import Path
@@ -36,7 +38,9 @@ SRC = ROOT / "platform/native"
MIN_IOS = "17.0"
# Set from command-line options / environment in main().
GAME = STEAM_GAME
BUNDLE_ID = "local.factorio.native"
BUNDLE_ID = None # resolved from the provisioning profile unless given
DEFAULT_BUNDLE_ID = "local.factorio.native"
SLICE_SHA256 = "2cb4de127b50d54c6c5666acfef2263fae16d11a3361ad39353ecf8a66a4d9c7" # 2.0.77 Steam, arm64
TEAM = None
SLICE = ROOT / "build/factorio.arm64"
@@ -113,12 +117,14 @@ def build_icon():
def prepare_inputs():
"""Extract the ARM64 slice from the local install and fetch the pinned Wuffs source."""
if not SLICE.exists():
binary = GAME / "MacOS/factorio"
if not binary.exists():
raise SystemExit(f"Factorio executable not found at {binary}; pass --game PATH/factorio.app/Contents")
SLICE.parent.mkdir(parents=True, exist_ok=True)
run(["lipo", "-thin", "arm64", binary, "-output", SLICE])
binary = GAME / "MacOS/factorio"
if not binary.exists():
raise SystemExit(f"Factorio not found at {GAME}.\nInstall Factorio from Steam on this Mac, or pass --game PATH/factorio.app/Contents.")
SLICE.parent.mkdir(parents=True, exist_ok=True)
# Re-extract every build so a Steam update is noticed.
run(["lipo", "-thin", "arm64", binary, "-output", SLICE])
if hashlib.sha256(SLICE.read_bytes()).hexdigest() != SLICE_SHA256:
raise SystemExit("wrong Factorio version: this port needs Factorio 2.0.77 for macOS from Steam.")
if not WUFFS.exists() or hashlib.sha256(WUFFS.read_bytes()).hexdigest() != WUFFS_SHA256:
WUFFS.parent.mkdir(parents=True, exist_ok=True)
print("+ fetch", WUFFS_URL, flush=True)
@@ -164,24 +170,42 @@ def build():
sign()
def find_profile():
"""A development profile covering BUNDLE_ID (explicit or wildcard), optionally for TEAM."""
def load_profiles():
folder = Path.home() / "Library/Developer/Xcode/UserData/Provisioning Profiles"
candidates = []
profiles = []
for path in sorted(folder.glob("*.mobileprovision")):
plist = plistlib.loads(subprocess.check_output(["security", "cms", "-D", "-i", str(path)]))
team = plist["TeamIdentifier"][0]
app_id = plist["Entitlements"]["application-identifier"]
if TEAM and team != TEAM:
continue
if app_id == f"{team}.{BUNDLE_ID}":
candidates.insert(0, (path, plist, team)) # explicit App ID wins over wildcard
elif app_id == f"{team}.*":
candidates.append((path, plist, team))
if not candidates:
raise SystemExit(f"no provisioning profile covers {BUNDLE_ID}" + (f" for team {TEAM}" if TEAM else "") +
". Create one in Xcode (any project, automatic signing, same team) or pass --team.")
return candidates[0]
if plist.get("ExpirationDate") and plist["ExpirationDate"].timestamp() < time.time():
continue
bundle = plist["Entitlements"]["application-identifier"].split(".", 1)[1]
profiles.append((path, plist, team, bundle))
return profiles
def find_profile():
"""Choose the signing profile and the bundle identifier it allows.
Preference: an explicit App ID equal to --bundle-id; otherwise an explicit App ID containing
"factorio" (what the README's one-time Xcode project creates); otherwise a wildcard profile.
"""
global BUNDLE_ID
profiles = load_profiles()
if BUNDLE_ID:
matches = [p for p in profiles if p[3] == BUNDLE_ID] + [p for p in profiles if p[3] == "*"]
else:
named = sorted((p for p in profiles if "factorio" in p[3].lower() and p[3] != "*"),
key=lambda p: p[1].get("CreationDate"), reverse=True)
matches = named + [p for p in profiles if p[3] == "*"]
if not matches:
raise SystemExit("no provisioning profile found for the app.\n"
"Do README step 4 once (an Xcode project named \"factorio\" run on your iPhone).")
path, plist, team, bundle = matches[0]
if not BUNDLE_ID:
BUNDLE_ID = bundle if bundle != "*" else DEFAULT_BUNDLE_ID
return path, plist, team
def signing_identity(profile):
@@ -222,7 +246,7 @@ def device(requested=None):
for d in data["result"]["devices"]:
if d.get("connectionProperties", {}).get("pairingState") == "paired":
return d["identifier"]
raise SystemExit("no paired device")
raise SystemExit("no iPhone found: plug it in, unlock it, tap Trust, and turn on Developer Mode.")
def main():
@@ -234,10 +258,12 @@ def main():
parser.add_argument("--no-build", action="store_true")
parser.add_argument("--game", default=os.environ.get("FX_GAME", str(STEAM_GAME)))
parser.add_argument("--team", default=os.environ.get("FX_TEAM"))
parser.add_argument("--bundle-id", default=os.environ.get("FX_BUNDLE_ID", BUNDLE_ID))
parser.add_argument("--bundle-id", default=os.environ.get("FX_BUNDLE_ID"))
parser.add_argument("--device", default=os.environ.get("FX_DEVICE"))
args = parser.parse_args()
GAME, BUNDLE_ID, TEAM = Path(args.game), args.bundle_id, args.team
find_profile()
print(f"+ bundle identifier {BUNDLE_ID}", flush=True)
if not args.no_build:
build()
if args.install or args.push_data or args.launch: