Build Expo iOS apps locally on Windows without EAS
This tutorial starts from a new Expo project, generates the iOS project locally on Windows, runs a development build on your iPhone, and ends with an .ipa you can upload to App Store Connect. You need an Apple Developer account, an iPhone and a USB cable, and you need to have installed KXBuild. The first run-through takes about an hour, mostly spent downloading dependencies.
Local builds or EAS Build
EAS Build builds on Expo’s cloud macOS machines, so you don’t maintain a build environment, and EAS can manage your certificates too. The trade-off is that free accounts get a limited number of builds per month, builds queue at peak times, and every change to native configuration means waiting for another cloud build. eas build --local builds on your own machine, but for iOS it still requires macOS and Xcode.
Building locally on Windows with KXBuild means no queue, no cloud build quota, and breakpoints in native code. You provide your own certificates and provisioning profiles, and the ios folder is generated on your machine. The two approaches can coexist. Some people on a team can ship production builds with EAS while others debug locally on Windows.
Create an Expo project
To use an existing project, open its root folder in VS Code with File > Open Folder… and skip to the next section.
Otherwise, create one with Expo’s official command. Use a short path for the parent folder. node_modules nests deeply, and a deep project path easily runs into path-too-long errors on Windows.
1cd D:\work
2npx create-expo-app@latest expohello
When it’s done, open D:\work\expohello in VS Code.
You can also create the project from a KXBuild template. Use either of these.
- VS Code extension. Run
KXBuild: New Projectfrom the Command Palette and pick the “Expo App” template. - Command line. Run
kxbuild project create -t expo_app -n expohello -o D:\work.
For details on the template, see Project templates.
Fill in the iOS settings in app.json
An Expo project’s ios folder is generated from app.json, so filling in the iOS fields before generating it saves you a regeneration. Only the iOS-related fields are shown below. Leave the others as they are.
1{
2 "expo": {
3 "name": "expohello",
4 "version": "1.0.0",
5 "ios": {
6 "bundleIdentifier": "com.example.expohello",
7 "buildNumber": "1",
8 "appleTeamId": "ABCDE12345",
9 "infoPlist": {
10 "NSCameraUsageDescription": "Used to scan QR codes",
11 "NSMicrophoneUsageDescription": "Used to record audio in videos"
12 }
13 }
14 }
15}
Here’s what each field does.
| Field | Purpose |
|---|---|
version |
The version shown on the App Store, maps to CFBundleShortVersionString |
ios.buildNumber |
The build number, maps to CFBundleVersion. Each upload to App Store Connect needs a higher one than the last |
ios.bundleIdentifier |
The bundle ID. It must match the App ID registered in your developer account and the provisioning profile |
ios.appleTeamId |
Your developer account’s Team ID, shown on the Membership page at developer.apple.com |
ios.infoPlist |
Keys written straight into Info.plist. Put usage descriptions here |
Apps that use the camera, microphone, photo library or location must include the matching usage description. Without it, the app crashes as soon as it makes the call, and App Review rejects it. Some Expo libraries, such as expo-camera, let you set the permission text under plugins. Follow that library’s docs; the result is the same.
Put the .p12 certificate and .mobileprovision provisioning profile for signing in the keychain folder. During development, use an Apple Development certificate and a Development provisioning profile that includes this iPhone’s UDID. For how to get and place them, see Certificates and signing.
Run doctor and install native dependencies
Run doctor first whenever you open a project for the first time. Doctor checks your Expo setup, then generates and prepares the iOS project. Afterward the project has an ios folder. Use either of these.
- VS Code extension. In the title bar of the KXBuild view, open the ··· menu and run Doctor (fix environment).
- Command line. Run the command below.
1kxbuild doctor D:\work\expohello
After doctor, go into the ios folder and install the native dependencies. The first time downloads quite a lot, so how long it takes depends on your network.
1cd D:\work\expohello\ios
2pod install
Afterward the ios folder contains expohello.xcworkspace. Build that from now on. For more on CocoaPods, see Develop iOS apps with CocoaPods on Windows.
Run a development build on your iPhone
Connect the iPhone with a cable, unlock it and tap Trust in the Trust This Computer prompt, then turn on Developer Mode in Settings > Privacy & Security > Developer Mode. The iPhone restarts once. For drivers and iPhone settings, see Set up your device.
A development build loads its JS from Metro on your computer, so first open another terminal and start Metro in the project root.
1cd D:\work\expohello
2npx expo start
Then select the iPhone in the KXBuild view and press F5. KXBuild builds, signs, installs and launches the app, then attaches the debugger. The first time you debug on a given iOS version, KXBuild downloads its device symbols. It’s a few GB, but only once.
Once the app is running, save changes to JS or TypeScript code and the app refreshes automatically, with no rebuild. Each kind of code uses its own debugger.
- Native code (Swift and Objective-C in the ios folder). Set breakpoints in VS Code and they take effect after F5. See Run and debug.
- JS code. Press
jin the terminal runningnpx expo startto open Expo’s debugger and set breakpoints there.
If the app shows a red screen as soon as it opens, the iPhone usually can’t reach Metro on your computer. Make sure the iPhone and computer are on the same local network and that Windows Firewall allows Node.js or Metro’s port 8081.
After changing native code, stop debugging before pressing F5 again. The Restart button on the debug toolbar doesn’t recompile.
Regenerate the iOS project after editing app.json
If you change the bundle ID, version, usage descriptions or icon, or install a library with native code, regenerate the ios folder, or the change won’t reach the app. Doctor only generates the ios folder when it doesn’t exist, so delete the old one first, then run doctor.
1cd D:\work\expohello
2Remove-Item -Recurse -Force .\ios
3kxbuild doctor D:\work\expohello
4cd ios
5pod install
In the extension, delete the ios folder, run Doctor (fix environment) in the KXBuild view, then run pod install in the ios folder. The result is the same.
Regenerating overwrites every manual change in the ios folder. Keep native configuration you want to last in app.json or a config plugin, which is what Expo recommends. If your project keeps the ios folder in version control and edits native files directly, don’t delete the folder. Edit it with the Property List Editor and Project Settings Editor instead.
Build a Release .ipa
Check two things before submitting. ios.buildNumber must be higher than in your last upload, and if you changed it, regenerate as in the previous section. The keychain folder must have an Apple Distribution certificate and an App Store provisioning profile, since a build signed with a Development profile is rejected on upload.
A Release build bundles the JS into the app, so it no longer depends on Metro at runtime. Use either of these.
- VS Code extension. Switch the configuration to Release in the Build configuration node of the KXBuild view, then run
KXBuild: Buildfrom the Command Palette. - Command line. Run the command below in the project root.
1kxbuild build .\ios\expohello.xcworkspace -c Release
If the project’s Release configuration doesn’t specify a distribution certificate, add -D CODE_SIGN_IDENTITY="Apple Distribution" on the command line to override it for this build.
The build produces ios\build\expohello.ipa, with the same structure as an Xcode export. Upload it to App Store Connect with Appuploader on Windows. When processing finishes, it appears on the TestFlight page. Send it to testers first, then submit it for review. See Release and App Store submission.
FAQ
Doctor didn’t generate the ios folder. Run it again with -v, as in kxbuild doctor D:\work\expohello -v. The output names the step that failed, which is usually dependencies that didn’t finish installing or a network interruption.
The build can’t find a header or module from Pods. Make sure you ran pod install in the ios folder and that you’re building the .xcworkspace, not the .xcodeproj.
File name too long or file not found. Move the project to a short path such as D:\work, delete the ios folder and regenerate it.
The app quits right after launch with “didn’t start in time”. The app most likely crashes on launch, usually because a usage description is missing or native libraries weren’t regenerated. To view device logs and crash reports, see Run and debug and Troubleshooting.