Building Expo Apps for a Real iPhone on Windows, No Mac
Claude (Opus 5.5)
Written by Claude (Opus 5.5), which also wrote the code. A human supplied a Windows PC, an iPhone and the idea.
expo-wsl-ios builds your Expo app and installs it on a real iPhone, from Windows, with no Mac and no EAS. It compiles in WSL against Apple's real iOS SDK, signs with your App Store Connect key, and installs over USB.
npm i -D github:dested/expo-wsl-ios
npx expo-wsl-ios setup --xip Xcode_27.xip --asc-key AuthKey_XXXX.p8 --issuer-id <uuid>
npx expo-wsl-ios runDon't use it for anything serious. It's a toy. It has been tested on one PC and one iPhone, builds debug only, and has no live reload. But it's a good toy if you'd rather not be forced onto a Mac every time you want your app on a phone.
Built on
- omarchy-apple-dev by Joshua Warren (@JoshuaSWarren). It provides the Linux iOS toolchain, pulls the SDK out of
Xcode.xip, and signs through App Store Connect. This project only exists because of it. - xtool by Kabir Oberai (@kabiroberai) builds iOS apps with SwiftPM on Linux.
- rcodesign by Gregory Szorc (@indygreg) does code signing without a Mac.
- pymobiledevice3 by doronz88 (@doronz88) installs over USB from Windows.
- Expo supplies the SwiftPM configs; React Native and Hermes publish prebuilt frameworks on Maven.
The new part is about 3,800 lines of glue.
How it works
- Windows
Find the phone
pymobiledevice3asks the usbmuxd that Apple's "Apple Devices" app runs. - Windows
Autolinking and codegen
Expo's and React Native's own scripts list every native package and generate the new-architecture glue.
- Windows
App config
expo config --type introspectproduces the final Info.plist and entitlements, with config plugins applied. - Windows
JS bundle
expo export:embed, then the Windowshermescfrom npm, makesmain.hbc. - wsl.exe
scripts/wsl-build.shtakes over inside WSL. - WSL
Frameworks
React Native, Hermes and Expo's prebuilt modules are downloaded from Maven and npm once, then cached.
- WSL
Generate
Each native package becomes a SwiftPM target, built from its own
spm.config.json, Expo's config for it, or its podspec converted. - WSL
Build
xtool dev buildrunsswiftcandclangagainst the iOS SDK and produces the.app. - WSL
Sign
The App Store Connect API issues the certificate and profile,
rcodesignsigns, and out comes an.ipa. - /mnt
The
.ipalands back in your project. - Windows
Install
pymobiledevice3 apps installtakes five seconds. - iPhone
Launch
You tap the icon.
Numbers
Measured on an i9-14900K, with WSL given 16 vCPUs. Builds use 6 of them by default; set EXPO_WSL_IOS_CPUS to use more.
| What | Time |
|---|---|
| Production app (29 native modules), cold native build, 16 vCPUs | 177 s |
Same app, whole run, 6 vCPUs |
480 s |
Fresh create-expo-app, whole run, cold, 6 vCPUs |
495 s |
| Blank app, native build | 15.5 s |
| Windows prep (JS bundle, codegen, config) | 8–24 s |
| Install over USB | 5 s |
| One-time setup (~12 GB of disk) | ~15 min |
What doesn't work
| Gap | What it'd take |
|---|---|
| Live reload | Something still probes Metro on the port compiled into React Native (8081). Find it and point it at the LAN address. |
| Debugger | React Native DevTools comes free once live reload works. lldb needs the iOS 17 tunnel. |
| App icon, splash screen | Generate them the way expo prebuild does, and compile them with the actool and ibtool already in the distro. |
| Plugins that edit AppDelegate or the Xcode project | Run expo prebuild on Windows in a scratch folder and use its output. |
| Push, iCloud, app groups | Turn on the capabilities through the App Store Connect API, then re-provision. |
| Release and TestFlight builds | Reuse omarchy's ship.sh, which already produces valid TestFlight builds. |
| Launching from the CLI | pymobiledevice3's tunnel, which needs admin rights on Windows. |
| Obscure native libraries | A small spm.config.json override for each one. |
| SDK 56 and older, Windows on ARM | A lot of work. |
| Simulator, wireless install | Not possible. |
Want to fix one
Paste one of these into Claude Code in a clone of the repo.
Live reload
In expo-wsl-ios (read AGENTS.md first), make live reload from Metro work. The
app reads host:port from ip.txt for its bundle URL, but something still probes
:8081, the RCT_METRO_PORT baked into the prebuilt React framework. Use
`pymobiledevice3 syslog live` to find the probe and override it from the
generated AppDelegate. Done when editing a screen hot-reloads on the phone.App icon and splash screen
In expo-wsl-ios (read AGENTS.md first), give built apps their icon and splash
screen. During prep on Windows, generate the AppIcon asset catalog and
SplashScreen.storyboard from the Expo config using @expo/prebuild-config's
mods. In WSL, compile them with the distro's actool and ibtool and set
CFBundleIcons and UILaunchStoryboardName. Done when a fresh create-expo-app
shows its icon and splash on the phone.Plugins that edit native code
In expo-wsl-ios (read AGENTS.md first), make config plugins that modify the
AppDelegate or add native files take effect. During prep, run
`expo prebuild -p ios --no-install` into a scratch directory, use its
AppDelegate.swift (expo-wsl-ios/AppDelegate.swift still wins), and add files
the plugins created to the app target. Print pbxproj changes you can't
translate. Done when a withAppDelegate plugin's code runs on the device.Entitlements
In expo-wsl-ios (read AGENTS.md and scripts/dev-sign.sh first), provision
entitlements from the app config: enable the matching bundle-id capabilities
through the App Store Connect API, regenerate the profile when they change,
and sign with the full entitlements. Done when expo-notifications gets a push
token on the phone.TestFlight
Add `npx expo-wsl-ios ship` to expo-wsl-ios (read AGENTS.md first): a release
build uploaded to TestFlight, reusing omarchy-apple-dev's ship.sh for signing
and upload. Done when App Store Connect marks the build VALID.Launch from the CLI
Add `--launch` to `npx expo-wsl-ios run` (read AGENTS.md first): start the app
after install with pymobiledevice3's tunnel and `developer dvt launch`. The
tunnel needs admin on Windows, so ask for elevation once and say why. Done when
`run --launch` opens the app on an iOS 17+ phone.Do it to your Expo app
You need Windows 11, an Expo SDK 57 app, an iPhone with a cable, and a paid Apple Developer account ($99 a year). Apple's license says the SDK is for Apple hardware, so read it and decide for yourself.
Get the API key. In App Store Connect, open Users and Access → Integrations → App Store Connect API. Make a team key with Admin access, download the
.p8(Apple only allows this once), and copy the Issuer ID.Get Xcode. Download
Xcode_27.xipfrom developer.apple.com/download/all and don't unpack it.Install the Windows pieces.
- WSL:
wsl --install --no-distributionin an admin terminal, if you don't have it already. - Apple Devices from the Microsoft Store. Open it once.
- uv:
winget install astral-sh.uv.
- WSL:
Set it up, once per PC. It takes about 15 minutes.
npm i -D github:dested/expo-wsl-ios npx expo-wsl-ios setup --xip <Xcode_27.xip> --asc-key <AuthKey.p8> --issuer-id <issuer id>Plug in the phone. Unlock it and tap Trust.
npx expo-wsl-ios doctorshould come back all green.Run it.
npx expo-wsl-ios run. The first time, turn on Developer Mode in Settings → Privacy & Security, then tap the app.
If a library won't build, run run --exclude <package> or write a small override. Or point your agent at AGENTS.md.