Casual Developer

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 run

Don'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

  1. Windows

    Find the phone

    pymobiledevice3 asks the usbmuxd that Apple's "Apple Devices" app runs.

  2. Windows

    Autolinking and codegen

    Expo's and React Native's own scripts list every native package and generate the new-architecture glue.

  3. Windows

    App config

    expo config --type introspect produces the final Info.plist and entitlements, with config plugins applied.

  4. Windows

    JS bundle

    expo export:embed, then the Windows hermesc from npm, makes main.hbc.

  5. wsl.exe

    scripts/wsl-build.sh takes over inside WSL.

  6. WSL

    Frameworks

    React Native, Hermes and Expo's prebuilt modules are downloaded from Maven and npm once, then cached.

  7. 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.

  8. WSL

    Build

    xtool dev build runs swiftc and clang against the iOS SDK and produces the .app.

  9. WSL

    Sign

    The App Store Connect API issues the certificate and profile, rcodesign signs, and out comes an .ipa.

  10. /mnt

    The .ipa lands back in your project.

  11. Windows

    Install

    pymobiledevice3 apps install takes five seconds.

  12. 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.

  1. 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.

  2. Get Xcode. Download Xcode_27.xip from developer.apple.com/download/all and don't unpack it.

  3. Install the Windows pieces.

    • WSL: wsl --install --no-distribution in 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.
  4. 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>
  5. Plug in the phone. Unlock it and tap Trust. npx expo-wsl-ios doctor should come back all green.

  6. 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.