# ios-builder: building iOS apps from Windows or Linux through CI

> ios-builder is a Go CLI that builds iOS apps without a Mac by pushing a snapshot of your working tree to a remote macOS runner on GitHub Actions, Codemagic or Bitrise and downloading the resulting IPA. It also exposes a simulator through the MobAI app, and the last push was on 2026-10-10 with release v0.11.0 from 2026-09-17.

**MobAI-App/ios-builder** — Build and develop iOS apps without a Mac using GitHub Actions, Codemagic, or Bitrise, with simulator access and hot reload via MobAI.

- Repository: https://github.com/MobAI-App/ios-builder
- Stars: 1,181 · Forks: 112
- Language: Go
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/mobai-app-ios-builder

## The snapshot trick is the whole design

ios-builder is a CLI tool for iOS development without a Mac, using GitHub Actions by default, Codemagic or Bitrise for remote builds, and MobAI for on-device development. The mechanism is worth understanding precisely, because it is what makes the tool usable on a machine that has nothing Apple-related on it.

The build command builds what is on disk, not your last commit. Uncommitted and untracked files are included, so you can try a change without committing it. The snapshot is a throwaway commit pushed to a hidden ref that is deleted when the build finishes, and no branch is created and nothing is committed on your behalf.

The flow diagram in the README makes the shape clear: your repository holds `.github/workflows/ios-build.yml`, the CLI pushes a snapshot of your working tree to GitHub Actions on a macOS runner, the workflow checks out the snapshot, builds with Xcode and uploads an IPA artifact, and the CLI downloads it.

The hidden-ref detail is what makes the uncommitted-files behaviour safe. A normal branch push would show up in your repository's history, in pull requests, in the ref list. A hidden ref that is deleted at the end leaves nothing behind.

One limit is stated precisely, and it is the right limit. `.gitignore` still applies, so ignored files such as `.env` or `GoogleService-Info.plist` are absent from the build. That is the correct behaviour for secrets, but it also means a file you forgot to unignore is silently missing from the build, and the failure will look like a runtime crash in the app rather than a packaging error.

The tool is written in Go, the module is `github.com/MobAI-App/ios-builder`, it requires Go 1.24.0, and it is MIT licensed.

## Four commands, and the one that writes to your repository

The quick start is four steps, and they are all in the README in full.

Authenticate with GitHub:

```bash
builder auth github
```

Then initialise inside your project directory:

```bash
cd your-ios-project
builder init
```

That detects your GitHub repository, creates the workflow files, and offers to commit, push and trigger your first build, all interactively. The interactivity is the point, because this is the step that writes files into your repository and pushes them.

Then build:

```bash
builder ios build
```

The CLI triggers the workflow and downloads the IPA to `./dist/`.

And then the optional one, which is the most interesting:

```bash
builder ios share
```

That builds the working tree for the iOS simulator and makes the simulator usable from the MobAI app, so you can tap through a build without a Mac. It shows up under CI Devices, stays available while you are using it, and closes when you release it there or leave it unused, 30 minutes by default with `--duration` to change it.

The dependency list in `go.mod` shows what the CLI is made of, and it is a well-chosen set. `spf13/cobra` and `spf13/viper` for the command structure and configuration, `manifoldco/promptui` for the interactive prompts, `fsnotify` for watching the working tree, `gorilla/websocket` for the connection to the MobAI service, `zalando/go-keyring` for storing provider credentials in the OS keyring, `howett.net/plist` and `software.sslmate.com/src/go-pkcs12` for handling provisioning profiles and certificates, and `skip2/go-qrcode` for the QR code you scan to install a build.

The go-keyring dependency is worth a sentence. Storing a GitHub token, a Codemagic key and a Bitrise key in the OS keyring rather than in a config file is the difference between a tool you can leave logged in on a shared machine and one you cannot.

## Six frameworks, and the runner that guesses your toolchain

The supported frameworks table is short and every row is auto-detected.

Native iOS and Swift uses the repository root as the iOS path, React Native and Expo use `ios/`, Flutter uses `ios/`, Kotlin Multiplatform uses `iosApp/`, and Cordova and Ionic use `platforms/ios/`. All are auto-detected.

The detail that matters is how the runner installs your dependencies, and for the JavaScript frameworks it reads what your project already says rather than imposing its own choice. For React Native the runner installs JavaScript dependencies with the package manager the project already uses, npm, Yarn, pnpm or Bun, taken from `packageManager` in `package.json` or from the lockfile, on the Node version from `.nvmrc`, `.node-version` or `engines.node`. Expo works the same way, with `expo prebuild` running through that same manager.

The Expo case has two wrinkles that are handled carefully and are worth knowing about. A managed Expo project has no `ios/` directory in git, so `builder init` detects it as Expo managed, still records `"ios": { "path": "ios" }`, and the runner generates the native project with `expo prebuild --platform ios --no-install` before building. Ejected projects keep the committed `ios/` they have, and the prebuild step skips a directory that already holds an Xcode project.

The unattended prebuild has a consequence the documentation spells out. `expo prebuild` has to run unattended, so the app config must set the bundle identifier, as `expo.ios.bundleIdentifier` in `app.json` or `ios.bundleIdentifier` in `app.config.js` or `app.config.ts`. Without one, prebuild would stop and ask for it, so instead the build fails immediately and names the missing setting. That is good failure behaviour, a build that tells you what is wrong rather than hanging.

There is also a configuration note for standalone builds. The default Debug configuration builds an IPA that loads its JavaScript from Metro, so you set `"ios": { "configuration": "Release" }` in `builder.json` for a standalone IPA with the bundle baked in. And an `ios/` directory left over from a local `expo prebuild` is not uploaded, because managed projects gitignore it and the working-tree snapshot skips gitignored files, so the runner prebuilds from the app config on every build and cannot drift from a stale local copy.

## Three CI providers, and the trigger that works without the API

GitHub Actions is the default, and Codemagic and Bitrise are added on top of it rather than instead of it. The feature list calls these independent provider logins, and the point is that you stay signed in to all three and choose where each build runs.

The commands are:

```bash
builder auth codemagic
builder auth bitrise
builder auth status
builder init --provider codemagic --app-id YOUR_APP_ID --branch main
builder init --provider bitrise --app-id YOUR_APP_SLUG --branch main
builder ios build --provider codemagic
builder ios build --provider bitrise
```

Note the difference in the argument, which is a small but real detail: Codemagic takes an app ID and Bitrise takes an app slug. The `init` step writes `codemagic.yaml` or `bitrise.yml` at the repository root plus the shared runner script `.builder/ci/runner.sh`. You commit them to the configured branch and connect the same repository to each provider before building.

The alternative trigger is more interesting, because it is what you use when the GitHub API is not reachable. Both workflows can be started by pushing a tag:

```bash
git tag ios-build/my-build && git push origin ios-build/my-build   # IPA build
git tag ios-share/my-build && git push origin ios-share/my-build   # simulator
```

The run is named after the tag, and the tag is deleted when the run ends. Build settings come from `builder.json` in the tagged commit, with the keys `ios.path`, `ios.scheme`, `ios.signing`, `ios.configuration`, `flutter.version` and `kmp.jdkVersion`.

The limitation of the tag path is stated exactly. A tag carries no flags, so a tagged IPA build cannot pick a profile per run, and it applies the profile named by `defaultProfile` if there is one. The simulator build takes no profile at all.

There is one more thing to understand about the whole system. The build is a snapshot push, which means the remote runner receives your source through your own repository, and the signing materials, your certificate and provisioning profile, have to reach that runner as well. The documentation mentions optional signing with your certificate and provisioning profile, and the pkcs12 and plist libraries in the dependency list are for handling them.

## Signing, distribution and the two ways to get the app onto a phone

The distribution story has three paths, and they are worth separating because they have different requirements.

The first is TestFlight and the App Store. You can upload builds and submit them for review through the App Store Connect API, from any platform. That is the path for a release, and it needs real signing credentials.

The second is the QR code install. You scan a QR code and iOS installs the build over the air, no TestFlight. That is the fast path for getting a build onto a device you own, and it is the feature that makes the tool useful for a developer without a Mac at all. The `skip2/go-qrcode` dependency in the manifest is the client side of that.

The third is the MobAI device integration, where you install and run apps via MobAI, and where a coding agent connected to MobAI, Claude Code, Codex or Cursor, can drive the simulator the same way. The simulator route requires a `MOBAI_API_KEY` repository secret, created in the MobAI app under Account then API Keys, and set with `gh secret set MOBAI_API_KEY`. It is free with any MobAI account, on MobAI 3.0 or later.

The Flutter and React Native dev tools item is the one that goes furthest. Hot reload on real iOS devices from Windows and Linux, which means the development loop, not just the build, moves off the Mac. For a Flutter team that has been paying for a Mac purely to run `flutter run`, that is the part of this tool that changes the economics.

The repository layout reflects the size of the feature set. There is `cmd/` for the CLI entry point, `internal/` and `pkg/` for the implementation, `docs/` with the provider setup guide, `assets/` with the demo GIF, an `install.sh` for installation, a `.golangci.yml` for linting, and `CLAUDE.md` and `CONTRIBUTING.md` at the root. The Makefile has a `build-all` target that cross-compiles the binary for macOS and Linux on amd64 and arm64, and for Windows on amd64, with the version, commit and build time injected into the binary through linker flags.

## Where ios-builder is the wrong tool

The first case is a team that already has macOS and Xcode. The entire value of this tool is removing a Mac from the loop. If you have one, using Xcode directly gives you a local build, a local simulator, the full debugging experience and no round trip through a CI provider. The tool's own feature list mentions simulator access and hot reload, but a real local simulator is faster than a remote one, and a local Xcode build has no snapshot, no artifact upload and no download.

The second case is a team with a build pipeline that already works. If you already have a GitHub Actions macOS runner set up with your signing credentials, a `builder init` is one file and a workflow, and you have already solved the caching, the dependency install and the artifact retention. This tool's value is in the setup it removes, and if you have already done the setup, the value is a snapshot push and a CLI wrapper.

The third case is anything where the source cannot be pushed to a third-party CI provider. This is the constraint that the tag trigger hints at. The build works by pushing your working tree to a remote runner, so a project that cannot be pushed to GitHub, Codemagic or Bitrise, or that has secrets in the tree that must not leave the machine, cannot use it. The working-tree snapshot is a genuine convenience and a genuine exposure, and a project with a strict data handling policy has to think about which of their uncommitted files travel with every build.

The fourth is signing. The signing support is described as optional, and the materials have to be transported to the runner. A project with hardware-backed signing or with a strict chain of custody for its provisioning profiles is doing something different from what this tool automates.

One more practical caveat. The MobAI simulator route and the hot reload route both depend on MobAI, a separate service, and the free allowance for the other providers is documented separately in `docs/providers.md`. The tool is a thin client for a set of hosted services, and if MobAI changes its terms or the free tier, the parts of the workflow that feel most magical are the parts most affected.

## Conclusion

ios-builder fits a team whose developers are on Windows or Linux and who need to produce an iOS build or try one on a simulator without buying a Mac, and it fits a Flutter or React Native developer who needs hot reload on a real device. It does not fit a team that already has macOS and Xcode, since the whole value is in removing that requirement. Before you adopt it, read the provider setup documentation in `docs/provider-setup.md` and plan where the build credentials live, because the tool drives third-party CI accounts on your behalf and your iOS signing certificate and provisioning profile have to reach a runner you do not control.

## FAQ

### How do I build an iOS app from Windows or Linux with ios-builder?

Run `builder auth github`, then `builder init` in your project directory to create the workflow files, then `builder ios build`. The CLI pushes a snapshot of your working tree to a macOS runner, and downloads the resulting IPA to `./dist/`.

### Does the ios-builder build include uncommitted changes?

Yes. It builds what is on disk rather than the last commit, including uncommitted and untracked files, by pushing a throwaway commit to a hidden ref that is deleted when the build finishes. `.gitignore` still applies, so ignored files are absent from the build.

### Which frameworks does ios-builder support?

Native iOS and Swift, React Native, Expo in both managed and ejected form, Flutter, Kotlin Multiplatform, and Cordova or Ionic. All of them are auto-detected, and the JavaScript frameworks are built with the package manager and Node version your project already specifies.

### Can I use Codemagic or Bitrise instead of GitHub Actions?

Yes, and you can stay signed in to all three at once. Run `builder auth codemagic` or `builder auth bitrise`, then `builder init --provider codemagic --app-id YOUR_APP_ID --branch main` or the Bitrise equivalent, and pass `--provider` on the build command.

## Sources

- [Issues](https://github.com/MobAI-App/ios-builder/issues)
- [License: MIT](https://github.com/MobAI-App/ios-builder/blob/main/LICENSE)
- [MobAI-App/ios-builder on GitHub](https://github.com/MobAI-App/ios-builder)
- [README](https://github.com/MobAI-App/ios-builder/blob/main/README.md)
- [Releases](https://github.com/MobAI-App/ios-builder/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mobai-app-ios-builder
