Architecture
A deep dive into how xcross reimplements Flutter's iOS build pipeline and the iOS 17+ CoreDevice launch protocol in pure Dart.
xcross does not wrap or patch flutter build ios - that command simply refuses to run
off-macOS. Instead, it re-implements the parts of Flutter’s toolchain that matter for a
debug device build, using the same engine artifacts, the same compilers, and the same
device protocols the official tooling uses.
The pipeline
xcross flutter run
├─ FlutterPacker
│ ├─ IosEngineCache download engine artifacts pinned to the SDK's engine hash
│ ├─ FlutterDebugBundler frontend_server → app.dill → App.framework (JIT)
│ ├─ SwiftPM plugins swift build (Darwin SDK) → libFlutterPluginsGenerated.dylib
│ ├─ RunnerShim clang / ld64.lld → Runner Mach-O
│ └─ assemble Flutter.framework + Info.plist + flutter_assets → .app
├─ in-process codesign → install
└─ CoreDeviceLauncher RSD tunnel → launch suspended → gdb-remote attach
└─ HotReloadController DevFS + VM Service ⇄ frontend_server
Each stage below corresponds to one box in that tree.
1. Engine artifacts, straight from Flutter’s CDN
IosEngineCache reads the engine revision from your Flutter SDK - the same hash
flutter itself pins - and downloads exactly what Flutter’s tool would cache:
- the prebuilt
Flutter.xcframework - the debug
vm_snapshot_dataandisolate_snapshot_dataJIT snapshots - the host
frontend_server - the Flutter patched Dart SDK
Your app therefore runs on the identical engine binary an Xcode build would embed. xcross never rebuilds or modifies the engine, which is why engine-level behaviour matches the official tooling exactly.
2. Kernel compilation with frontend_server
In debug mode Flutter apps are not compiled to machine code - the Dart VM runs kernel
bytecode (JIT). FlutterDebugBundler drives the same frontend_server the Flutter tool
uses, against the patched SDK, with your --dart-defines and flavor entrypoint, to
produce app.dill.
It then lays out App.framework exactly like Flutter does:
| Path | Contents |
|---|---|
flutter_assets/kernel_blob.bin | The kernel program |
flutter_assets/vm_snapshot_data | VM heap seed |
flutter_assets/isolate_snapshot_data | Isolate heap seed |
flutter_assets/AssetManifest.bin / .json | Asset index generated from pubspec.yaml |
flutter_assets/FontManifest.json, fonts, assets | Replicates Flutter’s asset bundling, in Dart |
The App.framework binary in a debug build is only a stub. xcross compiles that stub with
clang targeting arm64-apple-ios and writes the framework’s Info.plist itself.
Because the app is pure JIT, no gen_snapshot is needed - which is precisely what makes
macOS unnecessary, and why release/AOT is out of scope.
3. Native code without Xcode
Three pieces of native code have to be produced on the host.
Darwin SDK
darwin_sdk_kit unpacks Xcode.xip with pure-Dart xar, pbzx and cpio readers and
assembles a Swift SDK bundle - an iOS sysroot plus frameworks - usable by upstream Swift
and LLVM on Windows and Linux. See Darwin SDK for the user-facing
side of this step.
Runner
The Runner executable, Flutter’s AppDelegate/main shim, is compiled with clang and
linked with ld64.lld, LLVM’s Mach-O linker, against Flutter.xcframework from the SDK
above. No Xcode build system, no xcodebuild, no build phases.
Plugins
SwiftPM iOS plugins are built with swift build against the same SDK into a single
libFlutterPluginsGenerated.dylib, with a generated registrant mirroring Flutter’s
GeneratedPluginRegistrant. A Mach-O rewriter then fixes install names and rpaths so the
dylibs resolve inside the .app bundle at runtime.
NotePlugins that only ship a CocoaPods podspec are skipped with a warning. Prefer plugin
releases that include ios/<package_name>/Package.swift.
4. Signing and device install, natively
apple_developer_kit implements Apple’s GrandSlam login, with ADI machine attestation via
the Android libraries, Developer Services provisioning - certificates, device
registration, provisioning profiles - and in-process Mach-O code signing. There is no
codesign and no ldid involved; the signature is computed and written by Dart code.
Installation goes over the standard device protocols via pymobiledevice3.
5. iOS 17+ CoreDevice launch
iOS 17 replaced the old debug-launch path with CoreDevice over an encrypted RSD tunnel.
xcross tunnelmounts the Developer Disk Image and brings the tunnel up.CoreDeviceLauncherlaunches the app suspended.- A minimal gdb-remote client - the same protocol
debugserverspeaks - attaches to resume and supervise the process. - The Dart VM Service is port-forwarded from the phone to localhost.
That forwarded VM Service connection is what everything downstream - hot reload, the debugger, DevTools - talks to.
6. Hot reload: a faithful DevFS reimplementation
Hot reload is pure Flutter-internals territory, reimplemented protocol-for-protocol:
- A long-lived
frontend_serversession, fromfrontend_server_kit, holds incremental compile state; a file watcher tracks yourlib/. - On
r, changed files are recompiled to an incremental dill, which is gzip-uploaded to the device via the VM Service’s HTTP DevFS endpoint (_createDevFSplusPUT- the sameorg-dartlang-devfs://filesystem Flutter’s tool uses). - xcross calls
reloadSourceson the root isolate, then triggersext.flutter.reassembleso the widget tree rebuilds. R(hot restart) resets the compiler, uploads a full dill, and re-runs the app in eachFlutterViewvia_flutter.listViewsand run-in-view - matching Flutter’s hot restart semantics.- The
frontend_serveris also registered as the VM Service’s expression compiler, so debugger watch and evaluate work on-device.
7. IDE debugging via DAP
xcross_dap implements a Debug Adapter Protocol server that routes launch/attach,
breakpoints, stepping, and hot-reload requests to the same VM Service connection.
VS Code reaches it through a shim that intercepts launch configs marked "xcross": true;
everything else falls through to Flutter’s own adapter, so non-xcross Flutter sessions in
the same workspace keep working. JetBrains IDEs reach it through an LSP4IJ DAP run
configuration that starts xcross flutter dap over stdio.
Workspace packages
xcross is a Dart workspace. The CLI itself is thin; the implementation lives in seven packages.
| Package | Role |
|---|---|
cli_kit | Shared CLI utilities: logging and spinners, process runners, downloads, host privilege helpers. |
apple_developer_kit | GrandSlam/Anisette login, App Store Connect provisioning, in-process codesigning, ADI client. |
darwin_sdk_kit | Resolves and installs Darwin/iOS SDK artifact bundles from Xcode.xip, plus ld64.lld discovery. |
dart_mobile_device | iOS device transport: pymobiledevice3 wrappers, RSD tunnels, port forwarding, gdb-remote debug proxy. |
frontend_server_kit | Drives a persistent frontend_server for incremental kernel compile, hot reload and expression evaluation. |
xcross_flutter | Builds Flutter iOS app bundles on Linux and Windows and drives hot reload over the device tunnel. |
xcross_dap | Debug Adapter Protocol server driving xcross flutter run from IDE Run & Debug buttons. |
Why this shape
Every design decision above follows from one constraint: no macOS host is available, so anything that only exists as a macOS binary must be replaced or avoided.
gen_snapshotfor iOS AOT is macOS-only, so xcross targets debug/JIT builds only.xcodebuildis macOS-only, so compilation and linking use upstream clang andld64.llddirectly.codesignis macOS-only, so signing is implemented in Dart against the Mach-O format.- The iOS SDK ships inside Xcode, so it is extracted once from an
Xcode.xipyou supply. - CoreDevice launch is a network protocol, so it is reimplemented rather than shelled out to.