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

+115 -94
View File
@@ -1,114 +1,135 @@
# Factorio on iPhone (personal port)
# Factorio on iPhone
Run your own copy of **Factorio 2.0.77** natively on an iPhone. The original macOS ARM64 game
code from your local install runs unchanged on iOS; this repository supplies the glue: a Mach-O
retargeting step, a small set of shims for the macOS APIs iOS lacks, touch and keyboard
controls, and build/sign/install tooling.
Play your own copy of **Factorio** on your iPhone. You build the app on your Mac from the
Factorio you already own on Steam, and install it on your phone with a cable.
**This repository contains no game files.** You need a legitimately owned copy of Factorio for
macOS (the Steam build). The build reads the executable and data from your local install and
installs them only on your own device. This is an unofficial, personal project and is not
affiliated with or endorsed by Wube Software.
This project contains **no game files**. Everything comes from your own installed copy. It is an
unofficial fan project, not made or supported by Wube Software.
## Status
---
Tested on an iPhone 15 Pro (iOS 27.0.1) with Xcode 26.6 (iOS SDK 26.5), using the Steam macOS
build of Factorio 2.0.77 (base game).
## What you need
| Area | State |
| --- | --- |
| Startup to main menu | ~16 s |
| Rendering | Metal, native panel resolution (2556×1179 on iPhone 15 Pro) |
| New game, playing, autosave | Working |
| Audio | Working (plays with the silent switch on, mixes with other apps) |
| Touch controls, on-screen overlay | Working |
| Hardware keyboard, iOS keyboard with extra keys | Working |
| Loading saves, long sessions, large factories | Not yet verified |
| Mods, multiplayer, Space Age | Not tested |
- A **Mac** (tested on Apple Silicon).
- **Factorio 2.0.77 for Mac, bought on Steam** and installed.
- An **iPhone** (tested on iPhone 15 Pro) and its **cable**.
- A free or paid **Apple account**.
- About **3 GB** of free space on the phone.
No JIT and no emulation: the game's own ARM64 machine code runs natively, signed with your
developer certificate.
---
## Requirements
## Install, step by step
- A Mac (tested on Apple Silicon) with **Xcode**, including the iOS platform, and **Python 3**.
- **Factorio 2.0.77 for macOS from Steam**, installed at the default Steam location (or pass
`--game`). The build checks the exact executable hash and refuses other versions.
- An iPhone with an A-series chip that supports Metal, **Developer Mode** enabled, paired with
the Mac.
- An Apple developer account signed into Xcode, with a development provisioning profile that
covers the app's bundle identifier (a wildcard team profile works). See
[docs/building.md](docs/building.md#signing).
### Step 1: Install Xcode
## Quick start
1. Open the **App Store** on your Mac, search for **Xcode** and install it (it is large and takes
a while).
2. Open Xcode once. When it asks which platforms to install, choose **iOS**.
### Step 2: Sign in to Xcode with your Apple account
1. In Xcode open **Xcode → Settings → Accounts**.
2. Click **+**, choose **Apple ID** and sign in.
### Step 3: Connect your iPhone
1. Plug the iPhone into the Mac, unlock it, and tap **Trust**.
2. On the iPhone open **Settings → Privacy & Security → Developer Mode**, turn it **on**, and let
the phone restart. (If you don't see **Developer Mode** yet, do Step 4 first and come back.)
### Step 4: Let Xcode set up signing (one time only)
Your iPhone only runs apps signed with your Apple account. A tiny empty Xcode project sets this
up once:
1. In Xcode choose **File → New → Project… → iOS → App → Next**.
2. Fill in:
- **Product Name:** `factorio`
- **Team:** your Apple account
- **Organization Identifier:** `com.` followed by your name, for example `com.alexsmith`
3. Click **Next** and save it anywhere.
4. At the top of the Xcode window, pick your **iPhone** as the run destination and press
**▶ (Run)**. A blank white app appears on the phone; that means it worked. If the phone says
**Untrusted Developer**, open **Settings → General → VPN & Device Management**, tap your
account and choose **Trust**, then press **▶** again.
You can now close Xcode and delete that project. The real Factorio app will replace the blank one.
### Step 5: Download this project
Open the **Terminal** app on your Mac and run:
```sh
# 1. Build, sign and install the app; copy the game data (~1.5 GB) into the app once.
python3 tools/build_native.py --install --push-data
# 2. Launch with the console attached (or tap the Factorio icon on the home screen).
python3 tools/build_native.py --no-build --launch
git clone https://git.entityseven.com/entityseven/factorio-ios.git
cd factorio-ios
```
Later rebuilds only need `python3 tools/build_native.py --install`. Game data and saves live in
the app's Documents folder and survive reinstalls. Full instructions, options and
troubleshooting: [docs/building.md](docs/building.md).
### Step 6: Install Factorio on the phone
## Controls
Keep the iPhone **plugged in and unlocked**, then run:
| Input | Action |
```sh
./install.sh
```
This builds the app, installs it, and copies the game data (about 1.5 GB) to the phone. It takes
a few minutes. When it says **Done**, the **Factorio** icon is on your home screen.
---
## Playing
- **Tap** = left click. **Hold** = right click (hold on ore or trees to mine them).
- **Two-finger drag** = zoom and scroll.
- **Left stick** = walk. Buttons **E Q R F M T**, **Esc**, **Tab**, **⇧**, **Ctrl** = the same keys
as on a computer.
- **⌨︎** opens the keyboard. **◐** hides the on-screen buttons.
- A Bluetooth keyboard works too.
The text is small at first: in the game open **Settings → Interface** and set **UI scale** to
about **175–200 %**.
The game takes about 15 seconds to start. Your saves stay on the phone, also when you update the
app.
All controls: [docs/controls.md](docs/controls.md).
---
## Updating
```sh
cd factorio-ios
git pull
./install.sh
```
---
## Problems?
| What you see | What to do |
| --- | --- |
| Tap / drag | Left mouse button |
| Long press (hold) | Right mouse button held: mining, removing; drag to keep going |
| Two-finger drag | Mouse wheel (zoom, scroll lists) |
| Left stick | W A S D movement |
| E Q R F M T buttons | Inventory, pipette, rotate, pick up, map, technologies |
| ⇧ and Ctrl buttons | Sticky modifiers for the next click or key |
| Esc, Tab | Escape and Tab |
| ⌨︎ | iOS keyboard plus a bar with Esc, Tab, Ctrl, Alt, ⇧, arrows, Home/End, PgUp/PgDn, F1–F12 |
| ◐ | Hide or show the on-screen controls |
| `Factorio not found` | Install Factorio from Steam on this Mac and let it finish downloading. |
| `wrong Factorio version` | This version of the port needs Factorio **2.0.77**. In Steam, open the game's **Properties** and keep it on that version. |
| `no provisioning profile found` | Do **Step 4** again. The Xcode project must be named `factorio`. |
| `no iPhone found` | Plug in the phone, unlock it, tap **Trust**, and check **Developer Mode** (Step 3). |
| `Xcode is not installed` / `iOS platform is missing` | Do **Step 1**. |
| `device was not unlocked` | Unlock the phone and run `./install.sh` again. |
| App says **Game data missing** | Run `./install.sh` again with the phone unlocked. |
| App stops opening after 7 days | Free Apple accounts sign apps for 7 days. Run `./install.sh` again; your saves are kept. |
| The game closes while playing | Run `python3 tools/build_native.py --no-build --launch` and send the output as a bug report. |
A Bluetooth or USB keyboard works directly with Factorio's normal key bindings. Details:
[docs/controls.md](docs/controls.md).
---
Factorio's interface is small at native resolution; set **Settings → Interface → UI scale** to
around 175–200 %.
## More information
## How it works
- [docs/building.md](docs/building.md): advanced build options and troubleshooting.
- [docs/native-port.md](docs/native-port.md): how the port works.
- [docs/research/](docs/research/static-translation.md): earlier research (ARM64 → WASM
translation).
1. `tools/retarget.py` turns the unmodified macOS executable into an iOS-loadable dynamic
library by editing only Mach-O header fields (platform, file type, page-zero size,
framework paths). It verifies that no byte after the header changes.
2. `platform/native/shim/` implements the ~150 macOS symbols iOS lacks: an AppKit layer on
UIKit sized to the SDL 2.24 Cocoa backend Factorio links statically, CoreGraphics display
and CoreAudio device models, PNG decoding through Wuffs, Metal device properties, and
stubs for Steam, Carbon, ForceFeedback, OpenCL and LDAP.
3. A small host app starts UIKit, loads the game library and calls its original `main` on the
main thread with a larger stack.
The design, every shim, and the verification behind it are described in
[docs/native-port.md](docs/native-port.md).
## Repository layout
| Path | Contents |
| --- | --- |
| `tools/build_native.py` | Build, sign, install, copy data, launch |
| `tools/retarget.py` | Header-only Mach-O retargeting of the game executable |
| `tools/native_audit.py` | Lists game imports that iOS lacks (needs shims) |
| `tools/symbolize_native.py` | Symbolizes crash reports against the game's debug symbols |
| `tools/make_icon.m` | Builds the app icon from the game's own icon |
| `platform/native/` | iOS host app and macOS API shims |
| `tests/native/` | PNG decoder parity test against macOS ImageIO |
| `docs/` | Build guide, controls, architecture |
| `docs/research/`, `runtime/`, `tests/*.py`, other `tools/` | Earlier static-translation research (ARM64 → WASM → native), kept for reference |
## Third-party software
- [Wuffs](https://github.com/google/wuffs) v0.3.4 (Apache-2.0): PNG decoding. Downloaded at
build time into `third_party/` and checked against a pinned SHA-256; not vendored.
- [SDL](https://github.com/libsdl-org/SDL) 2.24 (zlib): statically linked inside the game
binary; its source was used as a reference for the AppKit shim and is not included.
- [WABT](https://github.com/WebAssembly/wabt) (Apache-2.0): used only by the research track.
Factorio is a trademark of Wube Software Ltd. All game code and assets remain theirs.
**Third-party software:** [Wuffs](https://github.com/google/wuffs) (Apache-2.0) is downloaded
during the build to decode PNG images. Factorio itself uses [SDL](https://github.com/libsdl-org/SDL)
(zlib). Factorio is a trademark of Wube Software Ltd.; all game code and assets belong to them.
+22 -17
View File
@@ -32,28 +32,33 @@ The default game location is
## Signing
The app is signed with your **Apple Development** certificate and a development provisioning
profile. The build script picks them automatically:
profile from `~/Library/Developer/Xcode/UserData/Provisioning Profiles`. The build chooses:
- it looks in `~/Library/Developer/Xcode/UserData/Provisioning Profiles` for a profile whose
App ID is exactly the app's bundle identifier, or a wildcard (`TEAMID.*`) profile;
- it signs with the keychain identity whose certificate is embedded in that profile;
- the team ID comes from the profile (override with `--team` / `FX_TEAM`).
1. with `--bundle-id` (or `FX_BUNDLE_ID`): a profile for exactly that App ID, else a wildcard
(`TEAMID.*`) profile;
2. otherwise: the newest profile whose App ID contains `factorio`, and that App ID becomes the
app's bundle identifier;
3. otherwise: a wildcard profile, with bundle identifier `local.factorio.native`.
If you have no suitable profile yet, let Xcode create one: create any new iOS App project, set
its bundle identifier to `local.factorio.native` (or your own identifier, then pass the same one
to `--bundle-id`), choose your team under *Signing & Capabilities* with automatic signing, select
your iPhone and build it once. Xcode then downloads a profile that the script can use.
It signs with the keychain identity whose certificate is embedded in the chosen profile; the team
comes from the profile (override with `--team` / `FX_TEAM`). Expired profiles are ignored.
Free Apple accounts can sign apps for their own devices, but the installed app expires after
seven days and has to be reinstalled. This has only been tested with a paid account.
The README's step 4 creates a suitable profile: an empty Xcode iOS App project named `factorio`,
with automatic signing for your team, run once on the iPhone. Its App ID
(`com.<you>.factorio`) is unique to you, which Apple requires for explicit App IDs.
Free Apple accounts work the same way, but the installed app expires after seven days and has to
be reinstalled with `./install.sh` (saves are kept). This has been tested with a paid account
only.
## 3. Build and install
```sh
python3 tools/build_native.py --install --push-data
./install.sh # same as: python3 tools/build_native.py --install --push-data
```
What happens:
`install.sh` runs the steps below with short progress messages and writes the full output to
`build/install.log`. What happens:
1. **Inputs.** The ARM64 slice of the game executable is extracted with `lipo` into
`build/factorio.arm64`. The Wuffs PNG decoder source (v0.3.4) is downloaded into
@@ -90,7 +95,7 @@ installing takes the longest; the main menu normally appears after about 16 seco
| --- | --- | --- |
| `--game PATH` | `FX_GAME` | Steam location (see above) |
| `--team ID` | `FX_TEAM` | Taken from the provisioning profile |
| `--bundle-id ID` | `FX_BUNDLE_ID` | `local.factorio.native` |
| `--bundle-id ID` | `FX_BUNDLE_ID` | From the provisioning profile (see [Signing](#signing)) |
| `--device ID` | `FX_DEVICE` | First paired device |
| `--no-build` | | Install/launch the existing bundle |
@@ -110,7 +115,7 @@ files under the phone → Files → Factorio. A save from the Mac can be copied
```sh
xcrun devicectl device copy to --device <id> --domain-type appDataContainer \
--domain-identifier local.factorio.native \
--domain-identifier <bundle identifier> \
--source ~/Library/Application\ Support/factorio/saves/my-save.zip \
--destination Documents/factorio/write/saves/my-save.zip
```
@@ -119,8 +124,8 @@ xcrun devicectl device copy to --device <id> --domain-type appDataContainer \
| Symptom | Cause and fix |
| --- | --- |
| `unexpected input …; only the inspected 2.0.77 ARM64 slice is supported` | The installed game is a different version. Install 2.0.77, or delete `build/factorio.arm64` if it was extracted from another install. |
| `no provisioning profile covers …` | See [Signing](#signing). |
| `wrong Factorio version` | The installed game is not 2.0.77. Select that version in Steam (game Properties → Betas) or pass `--game` to a 2.0.77 install. |
| `no provisioning profile found` | See [Signing](#signing). |
| `Unable to launch … because the device was not, or could not be, unlocked` | Unlock the phone and run the launch again. |
| App shows "Game data missing" | Run `python3 tools/build_native.py --no-build --push-data`. |
| App closes during play | Run with `--launch`, then `python3 tools/symbolize_native.py` to turn the `[fx-crash]` lines in `build/native/console.log` into function names. If there is no crash report, iOS may have stopped it for memory; check Settings → Privacy & Security → Analytics & Improvements → Analytics Data for a `JetsamEvent` report. |
Executable
+42
View File
@@ -0,0 +1,42 @@
#!/bin/sh
# Builds Factorio for iPhone from your local Steam copy, installs it on the connected iPhone and
# copies the game data. Safe to run again: data that is already on the phone is skipped and saves
# are kept. Extra arguments are passed to tools/build_native.py (see docs/building.md).
cd "$(dirname "$0")" || exit 1
mkdir -p build
LOG=build/install.log
: > "$LOG"
fail() {
echo
echo "Failed: $1"
echo "Details are in $LOG"
exit 1
}
step() {
# step "message" args... : run build_native.py with args, logging its output.
message=$1; shift
printf '%s... ' "$message"
if python3 tools/build_native.py "$@" >> "$LOG" 2>&1; then
echo "ok"
else
echo
if grep -q "not, or could not be, unlocked" "$LOG"; then
fail "device was not unlocked. Unlock the iPhone and run ./install.sh again."
fi
# build_native.py ends with a one-line explanation of what went wrong.
reason=$(grep -v '^+' "$LOG" | grep -v '^\s*$' | tail -n 3)
fail "$reason"
fi
}
command -v xcrun > /dev/null 2>&1 || fail "Xcode is not installed (README step 1)."
xcrun --sdk iphoneos --show-sdk-path > /dev/null 2>&1 || fail "Xcode's iOS platform is missing (README step 1)."
command -v python3 > /dev/null 2>&1 || fail "python3 not found; open Xcode once to finish installing its tools."
step "Building the app" "$@"
step "Installing on the iPhone" --no-build --install "$@"
step "Copying game data to the iPhone (first time takes a few minutes)" --no-build --push-data "$@"
echo
echo "Done. Open Factorio from the iPhone home screen."
+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: