Skip to content
xcross
Documentation menu

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_data and isolate_snapshot_data JIT 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:

PathContents
flutter_assets/kernel_blob.binThe kernel program
flutter_assets/vm_snapshot_dataVM heap seed
flutter_assets/isolate_snapshot_dataIsolate heap seed
flutter_assets/AssetManifest.bin / .jsonAsset index generated from pubspec.yaml
flutter_assets/FontManifest.json, fonts, assetsReplicates 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.

  1. xcross tunnel mounts the Developer Disk Image and brings the tunnel up.
  2. CoreDeviceLauncher launches the app suspended.
  3. A minimal gdb-remote client - the same protocol debugserver speaks - attaches to resume and supervise the process.
  4. 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:

  1. A long-lived frontend_server session, from frontend_server_kit, holds incremental compile state; a file watcher tracks your lib/.
  2. 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 (_createDevFS plus PUT - the same org-dartlang-devfs:// filesystem Flutter’s tool uses).
  3. xcross calls reloadSources on the root isolate, then triggers ext.flutter.reassemble so the widget tree rebuilds.
  4. R (hot restart) resets the compiler, uploads a full dill, and re-runs the app in each FlutterView via _flutter.listViews and run-in-view - matching Flutter’s hot restart semantics.
  5. The frontend_server is 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.

PackageRole
cli_kitShared CLI utilities: logging and spinners, process runners, downloads, host privilege helpers.
apple_developer_kitGrandSlam/Anisette login, App Store Connect provisioning, in-process codesigning, ADI client.
darwin_sdk_kitResolves and installs Darwin/iOS SDK artifact bundles from Xcode.xip, plus ld64.lld discovery.
dart_mobile_deviceiOS device transport: pymobiledevice3 wrappers, RSD tunnels, port forwarding, gdb-remote debug proxy.
frontend_server_kitDrives a persistent frontend_server for incremental kernel compile, hot reload and expression evaluation.
xcross_flutterBuilds Flutter iOS app bundles on Linux and Windows and drives hot reload over the device tunnel.
xcross_dapDebug 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_snapshot for iOS AOT is macOS-only, so xcross targets debug/JIT builds only.
  • xcodebuild is macOS-only, so compilation and linking use upstream clang and ld64.lld directly.
  • codesign is 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.xip you supply.
  • CoreDevice launch is a network protocol, so it is reimplemented rather than shelled out to.

See also

ESC