Skip to content

Repository files navigation

rules_flutter

Status: v0.0.1 — early/alpha. Usable today, but the public API may change before 1.0. Feedback and contributions welcome.

Bazel rules for building Flutter applications. Provides Bazel-native compilation, asset bundling, AOT compilation, and platform-specific packaging for all Flutter target platforms.

Built on top of rules_dart for Dart compilation and delegates platform packaging to mature ecosystem rulesets (rules_android, rules_apple, etc.).

Why Bazel?

If you're already using flutter build, here's what you gain by switching to Bazel:

  • Hermetic, reproducible builds — every input is tracked; the same source always produces the same output, regardless of machine state.
  • Remote caching — build artifacts are content-addressed and shared across your team. A change that only touches one package doesn't rebuild anything else.
  • Remote Build Execution (RBE) — offload compilation to cloud workers. Build macOS, Linux, and Android targets from the same bazel build invocation.
  • Monorepo interoperability — Flutter apps, backend services, Rust libraries, C++ libraries, and infrastructure code all live in one build graph with correct dependency tracking.
  • Native code composition — depend on cc_library, rust_shared_library, or swift_library targets directly via native_deps. No CMake, no Gradle, no CocoaPods.
  • No build_runner — code generators (json_serializable, freezed, etc.) run as hermetic Bazel actions via dart_codegen.

Compatibility

  • Bazel: 9+
  • Flutter SDK: 3.44.1

Prerequisites

PlatformRequirements
AllBazel 9+
macOSXcode (for rules_apple and rules_swift)
iOSXcode + valid signing identity (simulator works without signing)
AndroidAndroid SDK ($ANDROID_HOME), Android NDK ($ANDROID_NDK_HOME, pointing at a versionedndk/<version> directory), rules_android, rules_android_ndk, rules_kotlin — see Android
LinuxC++ toolchain (native or LLVM cross-toolchain from macOS)
WindowsMSVC (native builds), or C++ cross-toolchain (debug JIT only from macOS/Linux)
WebNone (Dart-to-WASM/JS compilation is fully hermetic)

Required .bazelrc

rules_flutter's transitive Java toolchain (rules_jvm_external 7+, rules_android 0.7.2+) ships internal tool jars compiled at Java 21+ and uses Java 14+ language features in its sources. Bazel's defaults for the tool exec configuration are older than that, so without setting them explicitly you will hit either UnsupportedClassVersionError at action execution time or could not locate class file for java.lang.Record at compile time.

Windows builds additionally require Bazel symlink support, which rules_python 2.0+ depends on.

Paste this block into your project's .bazelrc:

# Required for rules_flutter — bumps the tool exec JDK above Bazel's
# `remotejdk_11` default so transitive rulesets' Java 21+ tool jars run.
common --tool_java_language_version=25
common --tool_java_runtime_version=remotejdk_25
# Required on Windows for rules_python 2+.
startup --windows_enable_symlinks

Quickstart

Add the following to your MODULE.bazel:

bazel_dep(
name="rules_flutter",
version=<latestfromregistry.bazel.build/modules/rules_flutter>,
)
flutter=use_extension("@rules_flutter//flutter:extensions.bzl", "flutter")
flutter.toolchain(flutter_version="3.44.1")
use_repo(flutter, "flutter_toolchains")
register_toolchains("@flutter_toolchains//:all")

Then in your BUILD.bazel:

load("@rules_flutter//flutter:defs.bzl", "flutter_application", "flutter_library")
flutter_library(
name="my_lib",
srcs=glob(["lib/**/*.dart"]),
assets=glob(["assets/**"]),
)
flutter_application(
name="my_app",
package_name="my_app",
main="lib/main.dart",
deps= [":my_lib"],
)

flutter_application is the core compilation target shared by all platforms. It produces a FlutterApplicationInfo provider that platform-specific packaging rules consume.

package_name is required — it matches pubspec.yaml's name: field, keys the kernel under stable package: URIs for hot reload, and lets the compile reach codegen siblings.

Debug vs Release Builds

Build mode is controlled by Bazel's standard compilation mode flag:

FlagModeCompilationUse case
-c dbgDebugKernel .dill (JIT)Development, hot reload
(default)FastbuildAOT native codeCI, testing
-c optReleaseAOT native code (stripped)Production

Cross-Compilation

gen_snapshot (the AOT compiler) is a cross-compiler: it runs on the host but produces code for the target. Different binaries exist per host/target pair.

Host-to-Target Matrix

HostTargetAOT (release)JIT (debug)Notes
macOSmacOSYesYesNative build
macOSiOSYesYesVia rules_apple platform transition
macOSAndroidYesYesAutomatic platform transition in the Android rules
macOSLinuxNoYesCross-compile with LLVM CC toolchain; JIT only (no cross gen_snapshot for desktop)
macOSWindowsNoYesJIT only; requires Windows CC cross-toolchain
macOSWebYesN/AWeb uses dart2wasm/dart2js, not gen_snapshot
LinuxLinuxYesYesNative build
LinuxAndroidYesYesAutomatic platform transition in the Android rules
LinuxiOSNoNoRequires Xcode (macOS only)
LinuxWebYesN/A
WindowsWindowsYesYesNative build
WindowsAndroidYesYesAutomatic platform transition in the Android rules
WindowsWebYesN/A

Key limitation: Desktop-to-desktop AOT cross-compilation (e.g. macOS→Linux release) is not supported because Flutter does not publish cross-gen_snapshot binaries for desktop targets. Use debug/JIT mode for cross-compiled desktop bundles, or build natively on the target platform.

Platform Rules

Each platform has a Tier 1 convenience macro (recommended) and Tier 2 composable rules (advanced).

The Tier 1 macros auto-discover runner files from flutter create output and wire up all internal targets. The Tier 2 rules give full control over each component.

macOS

macOS only — requires Xcode and rules_apple.

load("@rules_flutter//flutter:defs.bzl", "flutter_application")
load("@rules_flutter//flutter:macos.bzl", "flutter_macos_app")
flutter_application(
name="my_app",
package_name="my_app",
main="lib/main.dart",
deps= [":my_lib"],
)
flutter_macos_app(
name="my_app_macos",
application=":my_app",
bundle_id="com.example.myapp",
app_name="My App",
)

Prerequisites: Run flutter create --platforms=macos . to generate macos/Runner/ with Swift sources and XIB files.

AttributeDescription
applicationA flutter_application target (required).
bundle_idmacOS bundle identifier (required).
app_nameDisplay name (menu bar, window title). Defaults to target name.
minimum_os_versionMinimum macOS version. Default: "10.14".
info_plistOverride the conventional macos/Runner/Info.plist.
versionAn apple_bundle_version target. Defaults to "1.0".
entitlementsReplace the entitlements wiring wholesale. By default the macro auto-discovers macos/Runner/{DebugProfile,Release}.entitlements and selects between them by compilation mode.
additional_entitlementsEntitlement plist files merged into the selected base in every compilation mode. See Release builds and permissions.

Produces a .app bundle with FlutterMacOS.framework, App.framework, and flutter_assets/.

Advanced: Tier 2 composable rules

For full control over the macOS bundle (custom runner, custom framework layout, etc.):

load("@rules_flutter//flutter:macos.bzl",
"flutter_entitlements_merge",
"flutter_macos_engine",
"flutter_macos_framework_gen",
"flutter_macos_info_plist_gen",
"flutter_macos_menu_xib_gen",
"flutter_macos_native_libs_gen",
"flutter_macos_registrant_gen",
"flutter_macos_runner_lib_gen")
flutter_macos_framework_gen(name="my_framework", application=":my_app")
flutter_macos_registrant_gen(name="my_registrant", application=":my_app")
flutter_macos_engine(name="my_engine")
flutter_macos_native_libs_gen(name="my_native_libs", application=":my_app")
flutter_macos_info_plist_gen(name="my_info_plist", app_name="My App")
flutter_macos_menu_xib_gen(name="my_menu_xib", app_name="My App")
flutter_macos_runner_lib_gen(
name="my_runner",
registrant=":my_registrant",
engine=":my_engine",
)
# rules_apple's `entitlements` takes one file; this merges additions into# it (add-only, with a hard error on a conflicting value). Also exported# from flutter:ios.bzl.flutter_entitlements_merge(
name="my_entitlements",
base="macos/Runner/Release.entitlements",
additions= ["entitlements/Network.entitlements"],
)
macos_application(
name="my_macos_app",
bundle_id="com.example.myapp",
additional_contents= {
":my_framework": "Frameworks",
":my_native_libs": "Frameworks",
},
infoplists= [":my_info_plist"],
resources= [":my_menu_xib"],
deps= [":my_runner"],
)

iOS

macOS only — requires Xcode, rules_apple, and rules_swift.

load("@rules_flutter//flutter:defs.bzl", "flutter_application")
load("@rules_flutter//flutter:ios.bzl", "flutter_ios_app")
flutter_application(
name="my_app",
package_name="my_app",
main="lib/main.dart",
deps= [":my_lib"],
)
flutter_ios_app(
name="my_app_ios",
application=":my_app",
bundle_id="com.example.myapp",
)

Prerequisites: Run flutter create --platforms=ios . to generate ios/Runner/ with Swift sources.

Add to your MODULE.bazel:

use_repo(flutter, "flutter_toolchains", "flutter_ios_engine")
AttributeDescription
applicationA flutter_application target (required).
bundle_idiOS bundle identifier (required).
familiesDevice families. Default: ["iphone"].
app_nameDisplay name. Defaults to target name.
minimum_os_versionMinimum iOS version. Default: "12.0".
info_plistOverride conventional ios/Runner/Info.plist.
versionAn apple_bundle_version target. Defaults to "1.0".
launch_storyboardOverride launch storyboard.
entitlementsReplace the entitlements wiring. By default the macro auto-discovers ios/Runner/Runner.entitlements if present; its absence is a valid, capability-less app.
additional_entitlementsEntitlement plist files merged into the base in every compilation mode. Works when the app ships no entitlements file at all. See Release builds and permissions.
provisioning_profileA .mobileprovision file (usually a local_provisioning_profile target) to sign a device build with. Required for device builds; unused by simulator builds. See Running an iOS example on a physical device.

The platform transition to iOS arm64 is handled automatically by rules_apple's ios_application.

Advanced: Tier 2 composable rules
load("@rules_flutter//flutter:ios.bzl",
"flutter_entitlements_merge",
"flutter_ios_engine",
"flutter_ios_framework_gen",
"flutter_ios_info_plist_gen",
"flutter_ios_native_frameworks_gen",
"flutter_ios_registrant_gen",
"flutter_ios_runner_lib_gen")
flutter_ios_framework_gen(name="my_framework", application=":my_app")
flutter_ios_registrant_gen(name="my_registrant", application=":my_app")
flutter_ios_engine(name="my_engine")
flutter_ios_info_plist_gen(name="my_info_plist", app_name="My App")
# Frameworks for the app's native assets and `native_deps` dylibs. Omitting# this from `deps` below builds and renders a perfectly normal-looking app# that fails every native-asset call at runtime.flutter_ios_native_frameworks_gen(name="my_native_frameworks", application=":my_app")
flutter_ios_runner_lib_gen(
name="my_runner",
registrant=":my_registrant",
engine=":my_engine",
)
ios_application(
name="my_ios_app",
bundle_id="com.example.myapp",
families= ["iphone"],
minimum_os_version="12.0",
deps= [":my_framework", ":my_native_frameworks", ":my_runner"],
)

Android

load("@rules_flutter//flutter:defs.bzl", "flutter_application")
load("@rules_flutter//flutter:android.bzl", "flutter_android_app")
flutter_application(
name="my_app",
package_name="my_app",
main="lib/main.dart",
deps= [":my_lib"],
)
flutter_android_app(
name="my_app_android",
application=":my_app",
package_name="com.example.myapp",
)

Prerequisites: Run flutter create --platforms=android . to generate android/app/src/main/ with manifest, resources, and Kotlin sources. The macro handles everything automatically — no edits to the flutter create output needed.

Add to your MODULE.bazel:

bazel_dep(name="rules_android_ndk", version="0.1.5")
use_repo(flutter, "flutter_toolchains", "flutter_android_engine_arm64")
# Android NDK CC toolchain (used for native/FFI deps built for Android).android_ndk_repository_extension=use_extension(
"@rules_android_ndk//:extension.bzl",
"android_ndk_repository_extension",
)
use_repo(android_ndk_repository_extension, "androidndk")
register_toolchains("@androidndk//:all")

Environment. Android builds need two variables, both read by repository rules during fetch:

VariableValueRead by
ANDROID_HOMEthe SDK root, e.g. ~/Library/Android/sdkrules_android's android_sdk_repository
ANDROID_NDK_HOMEa versioned NDK directory, e.g. $ANDROID_HOME/ndk/28.2.13676358rules_android_ndk's android_ndk_repository

ANDROID_NDK_HOME must name the versioned directory, not its ndk/ parent. Pointing at the parent fails inside the NDK repository rule with a message that mentions neither the variable nor the mistake:

Error in readdir: can't readdir(), not a directory:
.../Android/sdk/ndk/toolchains/llvm/prebuilt/darwin-x86_64

Exporting both in the environment works. Putting them in a .bazelrc requires --repo_env, not--action_env--action_env reaches build actions only, and repository rules never see it, so an --action_env line fails exactly as if nothing were set:

common --repo_env=ANDROID_HOME=/path/to/Android/sdk
common --repo_env=ANDROID_NDK_HOME=/path/to/Android/sdk/ndk/28.2.13676358

With ANDROID_NDK_HOME unset, the build stops during repository fetch, before anything Android-specific is analyzed:

ERROR: An error occurred during the fetch of repository
'rules_android_ndk++android_ndk_repository_extension+androidndk':
Error in fail: Either the ANDROID_NDK_HOME environment variable or the
path attribute of android_ndk_repository must be set.

Build — no platform flags needed. flutter_android_bundle transitions the Flutter application (AOT compile, FFI deps, and all) to the Android platform matching its android_abi:

ANDROID_HOME=~/Library/Android/sdk \
ANDROID_NDK_HOME=~/Library/Android/sdk/ndk/28.2.13676358 \
bazel build //:my_app_android
AttributeDescription
applicationA flutter_application target (required).
package_nameAndroid package name, e.g. "com.example.myapp" (required).
app_nameDisplay name. Defaults to target name.
android_abiTarget ABI — "arm64" (default) or "x64". Selects the engine and the Android platform the app is built for.
min_sdk_versionMinimum Android SDK version.
target_sdk_versionTarget Android SDK version.
manifestOverride AndroidManifest.xml (auto-discovered from flutter create output or generated). Used verbatim: ${applicationName} is not substituted, so this attribute cannot take flutter create's own android/app/src/main/AndroidManifest.xml — let the macro discover that one instead.
debug_manifestVariant manifest whose permissions merge into -c dbg APKs only. None (default) discovers android/app/src/debug/AndroidManifest.xml; a label overrides discovery; False disables variant handling.
permissionsPermission names added to the effective manifest in every compilation mode, e.g. ["android.permission.INTERNET"]. See Release builds and permissions.
multidexMultidex mode. Default: "native".
Advanced: Tier 2 composable rules

For full control over the Android build (custom manifest, custom runner activity, etc.):

load("@rules_flutter//flutter:android.bzl",
"flutter_android_bundle",
"flutter_android_engine",
"flutter_android_manifest_gen",
"flutter_android_manifest_merge",
"flutter_android_permissions_manifest",
"flutter_android_runner_lib_gen")
load("@rules_android//android:rules.bzl", "android_binary")
flutter_android_bundle(name="my_bundle", application=":my_app")
flutter_android_engine(name="my_engine")
flutter_android_manifest_gen(name="my_manifest", package_name="com.example.myapp")
flutter_android_runner_lib_gen(
name="my_runner",
package_name="com.example.myapp",
engine=":my_engine",
)
android_binary(
name="my_apk",
manifest=":my_manifest",
multidex="native",
deps= [":my_bundle_native_libs", ":my_engine", ":my_runner"],
)

flutter_android_bundle output groups:

GroupContents
native_libslibapp.so (AOT) + any native_deps shared libraries
flutter_assetsflutter_assets/ tree
mobile_installJNI-structured symlinks + assets for bazel mobile-install

Linux

load("@rules_flutter//flutter:defs.bzl", "flutter_application")
load("@rules_flutter//flutter:linux.bzl", "flutter_linux_app")
flutter_application(
name="my_app",
package_name="my_app",
main="lib/main.dart",
deps= [":my_lib"],
)
flutter_linux_app(
name="my_app_linux",
application=":my_app",
gtk_app_id="com.example.myapp",
)

Prerequisites: Run flutter create --platforms=linux . to generate linux/runner/ with C++ sources. If no runner files are found, the built-in template is used automatically.

GTK3 and your cc toolchain. rules_flutter ships its own hermetic Chromium sysroot for GTK3 headers and libraries, and links those libraries as explicit files — it adds no -L to the link line and never passes -lgtk-3-style flags. Your cc toolchain's --sysroot is untouched and remains the sole owner of libc, libm and the rest of the C runtime. (This matters: a Debian sysroot's libm.so is a GNU ld script holding absolute paths that lld rewrites only for scripts found beneath --sysroot, so a second sysroot on the -l search path would break -lm with a "no such file" error naming a file that exists.)

Cross-compile from macOS. Desktop cross-compiles are debug/JIT only — see Cross-Compilation; Flutter publishes no cross-gen_snapshot for desktop targets, so there is no -c opt equivalent of this command:

bazel build //:my_app_linux -c dbg --platforms=@rules_flutter//flutter/platforms:linux_x64
AttributeDescription
applicationA flutter_application target (required).
app_nameBinary name. Defaults to target name.
gtk_app_idGTK application identifier. Default: "com.example.flutter".

Output directory structure:

my_app/
my_app (GTK runner executable)
lib/
libapp.so (AOT-compiled Dart code)
libflutter_linux_gtk.so (Flutter engine)
*.so (native plugin libraries, if any)
data/
flutter_assets/ (fonts, images, shaders, asset manifest)
icudtl.dat (ICU internationalization data)
Advanced: Tier 2 composable rules
load("@rules_flutter//flutter:linux.bzl",
"flutter_linux_bundle",
"flutter_linux_engine",
"flutter_linux_registrant_gen",
"flutter_linux_runner_lib_gen")
flutter_linux_engine(name="flutter_engine")
flutter_linux_registrant_gen(name="app_registrant", application=":my_app")
flutter_linux_runner_lib_gen(
name="my_runner",
engine=":flutter_engine",
registrant=":app_registrant",
gtk_app_id="com.example.myapp",
)
flutter_linux_bundle(
name="my_linux_app",
application=":my_app",
runner=":my_runner",
)

Windows

load("@rules_flutter//flutter:defs.bzl", "flutter_application")
load("@rules_flutter//flutter:windows.bzl", "flutter_windows_app")
flutter_application(
name="my_app",
package_name="my_app",
main="lib/main.dart",
deps= [":my_lib"],
)
flutter_windows_app(
name="my_app_windows",
application=":my_app",
)

Prerequisites: Run flutter create --platforms=windows . to generate windows/runner/ with C++ sources. If no runner files are found, the built-in template is used automatically.

AttributeDescription
applicationA flutter_application target (required).
app_nameBinary name. Defaults to target name.

Output directory structure:

my_app/
my_app.exe (Win32 runner executable)
flutter_windows.dll (Flutter engine)
app.so (AOT-compiled Dart code as ELF)
data/
flutter_assets/ (fonts, images, shaders, asset manifest)
icudtl.dat (ICU internationalization data)
Advanced: Tier 2 composable rules
load("@rules_flutter//flutter:windows.bzl",
"flutter_windows_bundle",
"flutter_windows_engine",
"flutter_windows_registrant_gen",
"flutter_windows_runner_lib_gen")
flutter_windows_engine(name="flutter_engine")
flutter_windows_registrant_gen(name="app_registrant", application=":my_app")
flutter_windows_runner_lib_gen(
name="my_runner",
engine=":flutter_engine",
registrant=":app_registrant",
)
flutter_windows_bundle(
name="my_windows_app",
application=":my_app",
runner=":my_runner",
)

Web

load("@rules_flutter//flutter:web.bzl", "flutter_web_app")
flutter_web_app(
name="my_app_web",
package_name="my_app",
deps= ["@deps//:flutter"],
app_name="My App",
)

Prerequisites: Run flutter create --platforms=web . to generate web/ with index.html, manifest.json, and icons. If these files don't exist, the built-in templates are used automatically.

Add to your MODULE.bazel:

use_repo(flutter, "flutter_toolchains", "flutter_web_sdk")

Note: Unlike other platforms, web rules take main + deps (Dart source) directly — not a flutter_application target. Web compilation uses dart2wasm/dart2js which have a structurally different pipeline from AOT platforms.

AttributeDescription
depsdart_library or flutter_library dependencies (required).
mainThe main .dart entry point. Default: "lib/main.dart".
app_nameApplication name for HTML title and manifest. Defaults to target name.
pwaGenerate service worker for offline support. Default: True.
Advanced: Tier 2 composable rules

For full control over compiler/renderer:

load("@rules_flutter//flutter:web.bzl", "flutter_web_bundle")
# WASM (modern, default):flutter_web_bundle(
name="my_app_web",
main="lib/main.dart",
deps= ["@deps//:flutter"],
)
# JavaScript (legacy):flutter_web_bundle(
name="my_app_web_js",
main="lib/main.dart",
compiler="dart2js",
renderer="canvaskit",
deps= ["@deps//:flutter"],
)

Core Rules

Loaded from @rules_flutter//flutter:defs.bzl.

flutter_library

Collects Flutter/Dart sources and assets. Propagates DartInfo and FlutterInfo providers to downstream targets. Does not compile — serves as the dependency unit for Flutter packages.

flutter_library(
name="my_lib",
srcs=glob(["lib/**/*.dart"]),
deps= ["@pub_deps//:some_package"],
assets=glob(["assets/**"]),
package_name="my_lib", # optional, defaults to last component of Bazel package path
)
AttributeDescription
srcsDart source files (mandatory).
depsdart_library or flutter_library dependencies.
assetsFlutter asset files (images, fonts, etc.).
package_nameDart package name. Defaults to the last component of the Bazel package path.

flutter_application

Core compilation pipeline that chains sources to kernel .dill, AOT native code, and asset bundle. Mode-aware: debug (-c dbg) produces kernel .dill + assets for JIT; release (-c opt or default) produces AOT native code + assets.

flutter_application(
name="my_app",
package_name="my_app",
main="lib/main.dart",
srcs=glob(["lib/**/*.dart"]),
deps= [
":my_lib",
"@rules_flutter//flutter:material_icons", # if app uses Material widgets
],
native_deps= [":my_native_lib"], # optional, for dart:ffi
)

Apps that use Material widgets must list @rules_flutter//flutter:material_icons in deps to bundle MaterialIcons-Regular.otf into flutter_assets/. The font is shipped by the active Flutter toolchain; the dep is the explicit opt-in.

AttributeDescription
mainThe main .dart entry point (mandatory).
package_nameDart package name; same value as pubspec.yaml's name: (mandatory). Keys the kernel's libraries under stable package: URIs (hot-reload parity), anchors codegen sibling co-location, and resolves package:<self>/... imports.
srcsAdditional Dart source files.
depsdart_library or flutter_library dependencies. Add @rules_flutter//flutter:material_icons to bundle the MaterialIcons font.
assetsAsset files to include in the bundle.
native_depsShared libraries for dart:ffi bundling.
definesDart environment defines (-D flags).
profileIf True, compile in profile mode (AOT, unstripped, with service extensions for profiling). Default: False.
obfuscateIf True, obfuscate Dart symbols in the AOT output. Pair with split_debug_info. Default: False.
split_debug_infoIf True, extract debug info into a separate .symbols file. Default: False.
extra_gen_snapshot_optionsAdditional flags passed directly to gen_snapshot.
track_widget_creationIf True, track widget creation locations for the DevTools inspector. Default: False.
shadersFragment shader files (.frag) to compile with impellerc.
tree_shake_iconsIf True, tree-shake icon fonts to only include used glyphs. Default: True.
license_filesLicense/NOTICE files to include in NOTICES.Z.
min_os_versionMinimum OS deployment target for Apple platforms. Passed to gen_snapshot as --macho-min-os-version.

Dart defines from the command line

Beyond the per-target defines attr, the repeatable build flag --@rules_flutter//flutter:extra_dart_defines=KEY=VALUE appends defines to every Dart compile (native kernel, flutter_test, dart2wasm/dart2js). One define per flag occurrence, so values may contain commas. On a key collision the flag wins over the attr. The keys dart.vm.profile and dart.vm.product are reserved (the build sets them from the compilation mode) and rejected. The dev tool's flutter_bazel run --dart-define KEY=VALUE forwards to this flag and replays the defines on hot reload/restart recompiles, matching flutter run --dart-define.

flutter_test

Compiles and runs Flutter widget/unit tests using the Dart VM with Flutter's platform .dill. Tests run with assertions enabled.

flutter_test(
name="my_test",
main="my_test.dart",
deps= [":my_lib"],
)

flutter_plugin

Declares a Flutter plugin with Dart API code and per-platform native implementation dependencies.

flutter_plugin(
name="url_launcher",
srcs=glob(["lib/**/*.dart"]),
deps= ["@pub_deps//:flutter"],
platforms= ["android", "ios", "macos", "linux", "windows", "web"],
dart_plugin_class="UrlLauncherPlugin",
native_deps=select({
"@platforms//os:linux": [":url_launcher_linux_cc"],
"@platforms//os:windows": [":url_launcher_windows_cc"],
"//conditions:default": [],
}),
)

flutter_kernel_target

Compiles Flutter sources to a kernel .dill file using Flutter's patched platform kernel. This is the base compilation step shared by all platform targets.

flutter_aot_target

Compiles Flutter sources to an AOT native shared library (.so on Linux/Android, .dylib on macOS) via gen_snapshot.

flutter_asset_bundle

Generates a flutter_assets/ tree artifact containing AssetManifest.bin, FontManifest.json, NOTICES.Z, and copied asset files.

Code Generation Rules

Loaded from @rules_flutter//flutter:codegen.bzl. These replace build_runner with hermetic Bazel actions.

dart_codegen

Per-file code generation. Runs a Dart script or pre-compiled binary as a code generator, producing one output file per input file. Supports persistent Bazel workers to amortize Dart VM startup.

load("@rules_flutter//flutter:codegen.bzl", "dart_codegen")
dart_codegen(
name="models_generated",
srcs= ["lib/model.dart", "lib/order.dart"],
generator="tools/my_generator.dart",
output_suffix=".g.dart",
use_worker=True, # optional, enables persistent worker mode
)
AttributeDescription
srcsInput .dart source files to process (mandatory).
generatorA .dart script to run as the generator.
generator_binA pre-compiled generator executable (alternative to generator).
output_suffixSuffix for generated files, e.g. .g.dart, .freezed.dart. Default: .g.dart.
generator_argsAdditional arguments passed to the generator.
dataAdditional data files the generator needs as inputs.
use_workerEnable persistent Bazel worker for .dart generators. Default: False.

dart_aggregate_codegen

Package-level code generation. Takes all sources in a package and produces a single aggregate output file.

load("@rules_flutter//flutter:codegen.bzl", "dart_aggregate_codegen")
dart_aggregate_codegen(
name="routes",
srcs=glob(["lib/**/*.dart"]),
generator_script="tools/route_generator.dart",
output="lib/router.gr.dart",
)

Pub Integration

Use rules_dart's pub.from_lock() to resolve pub packages:

# In MODULE.bazel:pub=use_extension("@rules_dart//dart/pub:extensions.bzl", "pub")
pub.from_lock(name="pub_deps", lock="//:pubspec.lock")
use_repo(pub, "pub_deps")
# In BUILD.bazel:flutter_application(
name="app",
package_name="app",
main="main.dart",
deps= [
"@pub_deps//:collection", # plain Dart package":my_plugin", # local Flutter plugin
],
)
# For pub packages that are Flutter plugins, wrap them:flutter_plugin(
name="my_plugin",
deps= ["@pub_deps//:my_plugin"],
dart_plugin_class="MyPlugin",
platforms= ["android", "ios", "macos"],
)

See e2e/plugin_example/ for a complete example.

Regenerating pubspec.lock

Bazel consumespubspec.lock — it never writes one. Resolve it with the toolchain rules_flutter pins, not with a Flutter installed separately:

bazel run @rules_flutter//flutter:pub -- get # after editing pubspec.yaml
bazel run @rules_flutter//flutter:pub -- upgrade
bazel run @rules_flutter//flutter:pub -- add qr

Arguments pass through to dart pub unchanged, and the command runs in your workspace root, so pubspec.lock lands where flutter.pub() reads it.

Why not the flutter on your PATH. The version matters and the failure is silent. Pub's solver treats the running Dart SDK's version and the Flutter SDK's version as constraints, so an installation older than the pinned toolchain quietly selects older packages — and the lock it writes is still perfectly valid, so nothing downstream can tell. Resolving e2e/plugin_example with a host Flutter 3.41.6 (Dart 3.11.4) pins meta 1.17.0; the pinned 3.44.1 toolchain (Dart 3.12.1) pins meta 1.18.0.

The fetched toolchain is engine artifacts plus a Dart SDK — there is no bin/flutter in it, and no pub executable — so this target runs dart pub with FLUTTER_ROOT pointed at @flutter_dev_root, a tree assembled from the same flutter/flutter tag the toolchain pins. That repository is fetched the first time you run the target and by nothing else.

Two consequences of it being dart pub rather than flutter pub: it writes pubspec.lock and .dart_tool/package_config.json (both already covered by flutter create's .gitignore for the latter), and it does not write .flutter-plugins-dependencies — rules_flutter generates plugin registrants from the build graph, so nothing here reads that file.

Native Interop

Flutter applications can depend on native code built by other Bazel rules. This replaces Flutter's native_assets build hook system.

cc_shared_library(
name="my_native_lib",
deps= [":my_cc_lib"],
)
flutter_application(
name="my_app",
package_name="my_app",
main="lib/main.dart",
deps= [":my_lib"],
native_deps= [":my_native_lib"],
)

Works with rules_cc, rules_rust, and any ruleset that produces shared libraries.

Providers

FlutterSdkInfo

Provided by the Flutter toolchain. Carries all engine binaries and SDK files needed by custom rules. Access via:

flutter_sdk_info=ctx.toolchains["@rules_flutter//flutter:toolchain_type"].flutter_sdk_info
FieldTypeDescription
versionstrFlutter SDK version string (e.g. "3.44.1").
engine_revisionstrEngine commit hash.
dartFileThe dart executable from the Flutter-bundled Dart SDK.
dartaotruntimeFileThe dartaotruntime executable for running AOT snapshots.
gen_snapshotFileThe gen_snapshot AOT compiler binary.
frontend_serverFileThe frontend_server_aot.dart.snapshot for kernel compilation.
platform_kernel_dillFileplatform_strong.dill — debug platform kernel.
platform_kernel_dill_productFileplatform_strong_product.dill — release platform kernel.
patched_sdkTargetFlutter patched Dart SDK root directory.
icu_dataFileicudtl.dat — ICU data file required by the engine.
tool_filesdepset[File]All files needed to run Flutter build tools (for action inputs).
engine_libraryTarget or NonePlatform-specific Flutter engine runtime library. None for mobile/web.
const_finderFile or Noneconst_finder.dart.snapshot for icon tree shaking.
font_subsetFile or Nonefont-subset binary for font subsetting.
impellercFile or Noneimpellerc shader compiler binary.
shader_liblist[File]Shader include files for impellerc.
target_osstrCross-compilation target OS, or empty for native.
target_archstrCross-compilation target architecture, or empty for native.

FlutterInfo

Propagated by flutter_library and flutter_plugin. Carries transitive assets, plugins, and native libs.

FieldTypeDescription
asset_dirsdepset[File]Directories containing Flutter assets.
pluginslist[struct]Plugin metadata structs. Each has name (str) and platforms (dict).
transitive_native_libsdepset[File]Shared libraries from plugin native_deps, merged transitively.

FlutterApplicationInfo

Propagated by flutter_application. Contains the outputs of the compilation pipeline for platform bundling rules to consume.

FieldTypeDescription
aot_outputFile or NoneAOT compiled native code. None in debug mode.
kernel_dillFile or NoneKernel .dill file for JIT mode. None in release mode.
flutter_assetsFileThe flutter_assets/ tree artifact.
icu_dataFileThe icudtl.dat file.
native_libslist[File]Shared libraries from native_deps (for dart:ffi).
is_debugboolTrue if built in debug/JIT mode.
native_plugin_registrantFile or NoneGenerated native plugin registrant source file for desktop platforms.

Dev Tool

The tools/dev_tool/ directory contains flutter_bazel, a Dart program that handles the iterative development workflow: device management, app installation, hot reload, and hot restart. It speaks the --machine JSON-RPC protocol for IDE compatibility with existing Flutter IDE plugins (VS Code, IntelliJ).

App output

A running app's console output — print, debugPrint, NSLog, Java stack traces, uncaught errors — is forwarded for the whole life of the run, starting before the VM service comes up so that startup failures are visible.

Where it goes depends on the mode:

ModeDestination
terminal (default)the app's stdout → the tool's stdout, its stderr → the tool's stderr, matching flutter run
--machineapp.log events. Nothing app-related is written to raw stdout, which belongs to the JSON-RPC stream

With more than one -d, terminal output is prefixed [<device>] so an interleaved multi-device run stays readable — same convention as flutter run -d all. Machine mode never prefixes: each app.log event already carries its appId.

Each platform has exactly one log source, because a Dart print() reaches both the process's stdout and the VM service's Stdout stream, and reading both would duplicate every line:

PlatformSource
macOS / Linux / Windowsthe app process's stdout + stderr
Androidadb logcat, filtered host-side to flutter*, DartVM, AndroidRuntime, System.err and fatal records
iOS Simulatora dedicated simctl spawn log stream scoped to the app process (separate from the stream used for VM-service discovery)
iOS devicedevicectl --console, plus lldb's own output — lldb stays attached for the whole run
Chrome, DDC dev modethe DWDS VM service's Stdout/Stderr streams
Chrome, WASM / production JSCDP Runtime.consoleAPICalled
attachthe VM service's Stdout/Stderr streams — the app wasn't spawned here, so there is no process to read

Physical iOS devices. Output comes from devicectl --console — which also carries devicectl's own progress banners (Acquired tunnel connection…, Launched application with…) — together with lldb's, since lldb stays attached for the whole run holding the debugserver the JIT depends on. Upstream treats the pair the same way: on a CoreDevice with Xcode ≥ 26 flutter_tools selects a combined devicectlAndLldb log source, noting that idevicesyslog "stopped working with at least Xcode 26."

Expect the first line to take a while. Starting a debug build under the JIT breakpoint is slow: the engine traps to the debugger for every executable page it allocates, and the handler writes to device memory over the debugserver link. On a recent iPhone, resume to the engine's first log takes ~45 s over a cable and ~6–7 minutes over the network. That is the platform, not this tool — setting --auto-continue on the breakpoint changes nothing, because the cost is the memory write rather than the stop/resume handshake. Use a cable when you can. The tool prints a "still waiting" note at 45 s so a slow launch is distinguishable from a hang.

Finding the VM service

On every platform except physical iOS hardware, the URI arrives in-band: the engine prints it, and discovery reads the same log stream that carries app output. Nothing extra is involved.

A physical iOS device is the exception. A wirelessly attached device has no console channel at all, and the one a wired device has belongs to the devicectl invocation that launched the app. So the URI comes from the app's mDNS advertisement instead — the one channel both connections share. Every Flutter app built in debug or profile mode advertises _dartVmService._tcp with its port in the SRV record and its service auth code in the TXT record; the generated debug/profile Info.plist declares the matching NSBonjourServices entry (flutter/private/runners/ios/DartVmServiceMdns.plist), so this works for any app built through these rules with no extra configuration.

This is the single mechanism for iOS hardware — nothing races it — and it is what makes wireless devices work at all:

ConnectionVM service hostPort
wired127.0.0.1 through an iproxy forward, because the service binds to the device's loopbackthe advertised device-side port, forwarded
wirelessthe device's own address, resolved from the advertisementthe advertised port, dialed directly

The two halves are chosen together from what devicectl list devices reports: a wireless launch also passes --vm-service-host=0.0.0.0 so the service is reachable off-device, and a wired launch deliberately does not.

Two things are worth knowing when it fails:

  • Local Network permission. On macOS the mDNS socket needs it. Denied, the failure is a specific error naming System Settings > Privacy & Security > Local Network — not a silent timeout. The device also prompts once, on its own, the first time an app advertises.
  • mDNS queries get lost. They are UDP, and RFC 6762 §5.1 requires a querier to retransmit. Against a USB-attached iPhone a single query succeeded roughly two times in five, so discovery retransmits with the specified backoff; in practice it resolves in ~200 ms and worst-observed 3.3 s.

Hot reload and hot restart get the same allowance. A reload is quick — only the changed library is compiled and no pages are re-JITed — but a restart re-runs main() and so pays the breakpoint cost again, taking about as long as the original launch. The per-call budget is therefore five minutes wired and fifteen wireless, against thirty seconds on a host. Too short a budget does not merely wait less: it abandons the RPC and force-closes the VM-service connection, reporting a timeout for a restart that was on its way to succeeding.

devicectl list devices also lists devices that were paired once and are not attached now. Those are filtered out, so -d ios picks the device that is actually there; with two attached, it asks for -d ios:<udid> rather than guessing. Either identifier works there — a device has two, the hardware UDID Xcode shows and the CoreDevice UUID devicectl prints as identifier. Only the hardware one means anything to usbmuxd, so that is what iproxy and lldb are addressed with; getting this wrong yields a port forward that binds locally and then resets every connection, which surfaces much later as a DDS failure.

Agent / external-tool control surface

flutter_bazel run starts an HTTP control channel by default (disable with --no-http-control-channel). External tools — IDE integrations, AI coding agents, end-to-end test harnesses — drive the running app over this channel without needing a TTY.

bazel run @rules_flutter//tools/dev_tool:flutter_bazel -- \
run --target //:my_app --machine
# stdout emits a JSON line: {"event":"http_control_channel","uri":"http://[::1]:PORT","token":"..."}# stdout also emits {"event":"app.start","appId":"..."} when the app attaches

Once the channel is up:

EndpointVerbPurpose
/command?token=<token>POSTRun a machine-protocol method against a running session. Body: {"method":"app.<X>", "params":{"appId":"...", ...}}.
/sessions/{appId}/screenshot/flutter?token=<token>GETPNG of the Flutter widget tree (_flutter.screenshot via VM service). Not available on iOS or web — see below.
/sessions/{appId}/screenshot/native?token=<token>GETPNG of the app as the platform sees it (screencapture / scrot / simctl io screenshot / adb screencap / CDP). Works on every device.
/sessions/{appId}/logs?token=<token>GETThe app's console output, from a bounded ring buffer. See below.

Which screenshot.screenshot/flutter captures only the widget tree, with no OS chrome, by asking the engine — but the engine cannot encode a compressed screenshot under Impeller, and there is no engine screenshot on web at all. iOS (always Impeller) and web therefore answer 501 naming screenshot/native, rather than a 500 that reads as transient; elsewhere, an app that renders with Impeller gets the same pointer attached to the engine's own error. screenshot/native is the one that works everywhere.

Reading logs./logs is a cursor-polling endpoint rather than a stream: there is no long-lived connection, and a caller reads exactly as much as it asks for.

sincemeaning
omittedtail the last 200 lines — what you want with no prior cursor
-Ntail the last N lines
0everything still buffered, oldest first
N > 0resume at line N (feed back a previous nextCursor)

limit caps the page (default and maximum 500). A non-numeric since, or a non-positive limit, is a 400 rather than a silent fallback — a typo'd cursor would otherwise look like a working poll loop that re-reads the tail forever.

# Tail, then poll forward.
curl -s "$URI/sessions/$APP/logs?token=$T"# {"lines":[{"i":812,"t":"flutter: meter -18dB","err":false}],# "nextCursor":813,"launch":1,"missed":0,"dropped":0,"closed":false}
curl -s "$URI/sessions/$APP/logs?token=$T&since=813"

err marks lines that arrived on an error channel — the process's stderr, a VM-service Stderr event, console.error. It is a channel, not a severity: platforms that hand the whole device log over one stream (iOS via devicectl/simctl, Android via logcat) deliver engine [ERROR:…] lines with err:false, so match on the text when you care about engine errors there.

missed is non-zero when the requested cursor had already been evicted, so a poller learns it has a gap instead of reading a short page as though it were complete; dropped is the total evicted over the run. closed turns true once the app's output source has ended — no further lines can arrive, so a poll loop can stop. The buffer survives the app's exit, so a crashed app's final output is still readable.

launch is which launch of the app the page came from: 1 for the original, one more for each relaunch (see app.restart below). Each launch buffers its own output from zero, so a cursor only means anything within one launch — when launch changes, drop your cursor and re-tail.

App-driving methods (proxied to the agent extensions registered from the generated plugin registrant, which the engine invokes before main() on every launch — so they survive hot restart):

app.dumpWidgetTree, app.tap, app.longPress, app.doubleTap, app.drag, app.scrollIntoView, app.enterText, app.getText, app.getRect, app.waitFor, app.waitForAbsent, app.pageBack.

Lifecycle methods: app.hotReload, app.restart, app.stop, daemon.shutdown.

Restarts that relaunch. A hot restart swaps Dart code into the running process, which cannot replace a native library it has already dlopened. So app.restart first rebuilds the app and, when the bundle's loose native libraries (native_deps) changed, relaunches the process instead of restarting the isolate:

{"message":"Restart relaunched the app: native libraries changed (…). …",
"relaunched":true,"nativeLibsChanged":["…/libmul.dylib"],
"launch":{"<appId>":2},"ready":true}

The channel is a property of the run, not of the app process: the port, the token and the appId are unchanged, and there is no second banner because none is needed — keep using the ones you started with. The machine protocol re-emits app.debugPort and app.started for the replacement process. ready says the relaunched app rendered a first frame before the response returned, so its service extensions are registered and the next app.* call will land; a false means that wait timed out, not that the app is broken. The one thing that does not carry over is /logs: the new process buffers its output from zero, so compare launch and re-tail. Only app.stop and daemon.shutdown end a session.

Selecting a widget. Methods that target a widget (tap, longPress, doubleTap, drag, getRect, getText, enterText, scrollIntoView, waitFor, waitForAbsent) take exactly one selector — mirroring flutter_driver's finder vocabulary:

parammatches
keya widget whose ValueKey value equals the string
texta Text/EditableText whose content equals the string
tooltipa Tooltip whose message equals the string
typea widget whose runtime type name equals the string (e.g. ElevatedButton)
semanticsLabela widget whose semantics label equals the string

Passing zero or more than one selector returns a clear error. Other params: durationMs (longPress/drag/scrollIntoView), dx/dy (drag/scrollIntoView), scrollableKey (scrollIntoView, ValueKey only), timeoutMs.

A selector reaches the same distance for every method: put the Key on the widget you would point at — the Chip, the ListTile, the button — not on the Text or EditableText it happens to build.

  • getText returns the text of the first text-bearing descendant of the match in pre-order (Text, including Text.rich; RichText; EditableText), and lists every one of them in texts — so a container holding two strings is visible as two rather than silently reported as its first. {"text":"Increment (agent)","texts":["Increment (agent)"]}.
  • enterText takes text as the string to type, which is why it is the one method whose selector vocabulary excludes the text selector: key, tooltip, type and semanticsLabel apply. With a selector it focuses the first EditableText under the match and types into it — no preceding tap needed — and echoes what it typed into: {"enteredText":"hi","into":"ValueKey(emailField)"}. With no selector it types into whatever is focused (flutter_driver's model), reporting "into":"focused".

Settling and timeouts. After dispatching input, interaction methods wait until the app is idle (no animations in flight) before returning, so a follow-up getRect/getText sees post-action layout — the same model as flutter_driver. The wait is bounded by timeoutMs (default 10000); if the app can't settle within it — e.g. the window is minimized/occluded so the embedder has paused vsync — the method returns a TimeoutException error rather than blocking forever. The input is still delivered.

curl note. The endpoints speak plain HTTP/1.1; no special flags are needed — curl -s "$URI/..." works. (If your curl is configured to attempt HTTP/2, add --http1.1.)

This means an external agent can: build the app, launch it under flutter_bazel, drive an entire user flow (taps, text entry, waits, screenshots) over plain HTTP, and shut it down cleanly — no manual q keystroke needed.

Examples

End-to-end examples are in the e2e/ directory:

DirectoryDescription
e2e/smokeMinimal smoke test for toolchain setup.
e2e/hello_worldMinimal Flutter app: kernel compilation, AOT, asset bundling, macOS bundle, web build.
e2e/codegenPer-file and aggregate code generation with dart_codegen and dart_aggregate_codegen, including custom generators; doubles as the hot-reload-with-codegen example.
e2e/ffi_exampleflutter_plugin with native_deps only (FFI, no registration).
e2e/ffi_plugin_exampleflutter_plugin with both dart_plugin_class and native_deps.
e2e/plugin_exampleflutter_plugin with dart_plugin_class only (Dart-side registration).
e2e/macos_exampleFull macOS app build + bundle structure verification.
e2e/ios_exampleiOS app build (requires Xcode).
e2e/android_exampleAndroid APK build (3 approaches) + APK content verification + web build.
e2e/linux_exampleLinux desktop app (3 approaches) + bundle structure verification.
e2e/windows_exampleWindows desktop app (3 approaches) + bundle structure verification.
e2e/web_exampleWeb app builds (dart2wasm + dart2js) with web_assets.
e2e/cross_compile_exampleCross-compile Linux bundle from macOS.
e2e/multi_window_exampleMulti-window macOS + multi-scene iOS builds with FlutterEngineGroup.

Release builds and permissions

flutter create's scaffold grants network access in debug only, and these rules reproduce that faithfully. An app that networks perfectly under -c dbg can be silently offline under -c opt: there is no build error, no runtime exception, and nothing in the app's own log — just an app that never reaches anything. Every platform has the same shape, because on every platform the debug-only grant exists for the Dart VM service, not for the app.

PlatformWhat debug has that release does notWhy it is there
macOScom.apple.security.network.server, com.apple.security.cs.allow-jit in DebugProfile.entitlements; Release.entitlements declares only app-sandboxThe sandbox must let the VM service bind and the JIT engine map executable pages
Androidandroid.permission.INTERNET, from android/app/src/debug/AndroidManifest.xmlAndroid enforces INTERNET at the kernel level (AID_INET group membership) — without it the VM service cannot bind even a loopback socket
iOSNSBonjourServices, NSLocalNetworkUsageDescription, merged by these rules into non-release buildsThe engine advertises the VM service over mDNS

None of that is the app's network grant, and none of it survives into release. An app that networks for itself must say so, once, in a way that applies to every compilation mode:

flutter_macos_app(
name="my_app_macos",
application=":my_app",
bundle_id="com.example.myapp",
additional_entitlements= ["entitlements/Network.entitlements"],
)
flutter_ios_app(
name="my_app_ios",
application=":my_app",
bundle_id="com.example.myapp",
additional_entitlements= ["entitlements/Network.entitlements"],
)
flutter_android_app(
name="my_app_android",
application=":my_app",
package_name="com.example.myapp",
permissions= ["android.permission.INTERNET"],
)

where entitlements/Network.entitlements is an ordinary plist fragment:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPEplist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plistversion="1.0">
<dict>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

These attributes are additive, which is the point: they merge into whichever base the compilation mode selected, so one declaration covers debug and release both, and flutter create's files stay untouched.

  • A key the base already declares with the same value is deduped — DebugProfile.entitlements already grants network.server, so declaring it above is safe in both configurations.
  • A key the base declares with a different value is a hard error naming the key and both files, rather than a silent winner.
  • com.apple.security.network.client is absent from both scaffold files. Most Flutter apps network through NSURLSession, which the sandbox exempts; a raw socket is not exempt. If you open one, you need this key.

iOS Local Network Privacy

iOS 14+ gates LAN access behind NSLocalNetworkUsageDescription and NSBonjourServices. These rules add both to non-release builds for the Dart VM service and drop them in release, which is correct — they are the debugger's, not the app's. An app that needs LAN access for itself declares them in ios/Runner/Info.plist, where they survive into -c opt; the rules merge the VM service keys into that file, keeping your usage description and unioning your Bonjour service list with _dartVmService._tcp.

The iOS simulator does not enforce Local Network Privacy at all, so a simulator build proves nothing about these keys. Only a physical device does.

Verifying, rather than assuming

These are exactly the defects a build_test cannot see. Check the artifact:

# macOS — the entitlements codesign actually embedded
unzip -oq bazel-bin/my_app_macos.zip -d /tmp/app && \
codesign -d --entitlements - "/tmp/app/My App.app"# Android — the compiled manifest inside the APK
aapt2 dump xmltree --file AndroidManifest.xml bazel-bin/my_app_android.apk
# iOS — the processed Info.plist inside the .ipa
unzip -oq bazel-bin/my_app_ios.ipa -d /tmp/ipa && \
plutil -p "/tmp/ipa/Payload/my_app_ios.app/Info.plist"

Bazel's default fastbuild is not-c dbg, so a plain bazel build already takes the release arm of each of these selects. e2e/macos_example, e2e/android_example and e2e/ios_example each carry a test that reads the built artifact this way.

Running an iOS example on a physical device

iOS simulator builds need no code signing and run out of the box (e.g. flutter_bazel run -t //:hello_world_ios -d ios-simulator). Device builds need signing, which is per-developer and must stay out of version control.

flutter_ios_app takes the credential directly:

load("@rules_apple//apple:apple.bzl", "local_provisioning_profile")
# In a git-ignored //device package, so the credential stays local.local_provisioning_profile(
name="profile",
profile_name="iOS Team Provisioning Profile: com.example.myapp",
tags= ["manual"],
)
flutter_ios_app(
name="my_app_ios_device",
application=":my_app",
bundle_id="com.example.myapp",
provisioning_profile="//device:profile",
)

That is the whole difference from the simulator target — the device bundle is the same construction, not a second hand-assembled one. flutter_ios_app defaults to tags = ["manual"], so bazel build //... on a fresh clone does not expand the target and therefore does not load the missing //device package.

Without provisioning_profile, a device build fails at analysis with "The provisioning_profile attribute must be set for device builds on this platform (ios)".

Obtaining the profile. This is an Apple Developer account operation and these rules cannot do it for you. You need a development provisioning profile whose App ID matches your bundle_id, installed in ~/Library/Developer/Xcode/UserData/Provisioning Profiles/. Either:

  • From the Developer portal — create the App ID and a development profile, download it, and double-click it. This works for any repository layout.
  • From any Xcode project whose PRODUCT_BUNDLE_IDENTIFIER is your bundle id, using automatic signing:
    xcodebuild -project <some>.xcodeproj -scheme <scheme> -configuration Debug \
    -destination generic/platform=iOS \
    -allowProvisioningUpdates -allowProvisioningDeviceRegistration build
    Note this needs an .xcodeproj, and a flutter create --platforms=ios . tree checked into a Bazel repository has no reason to keep one — flutter_ios_app only ever reads ios/Runner/*.swift and ios/Runner/Info.plist. The project can be any scratch project with the right bundle id; it does not have to be, and usually is not, the app you are building with Bazel.

Free ("Personal Team") profiles expire after about seven days; when a build fails with "no provisioning profile was found named …", mint a fresh one the same way.

Then: flutter_bazel run -t //:my_app_ios_device -d ios. Each iOS example also ships a device.example/ template — copy it to a git-ignored device/ package and set your bundle id.

Observing an iOS release build

There is no way to run a release-configured iOS build without a signing credential, which matters because release-only defects (see Release builds and permissions) are found by running, not by reading.

  • Device, -c opt — the real thing, and it needs a provisioning profile.
  • Simulator, -c opt — builds a complete .ipa with no warning, and xcrun simctl install and launch both return 0 and print a pid. The process then stays alive and renders blank white forever. It never crashes, so nothing appears in a crash log. The simulator slice of the engine is JIT and looks for flutter_assets/kernel_blob.bin, which an AOT bundle does not contain; the only evidence is in the simulator's system log:
    (Flutter) Failed to find snapshot at .../App.framework/flutter_assets/kernel_blob.bin
    (Flutter) [ERROR:flutter/shell/common/engine.cc(219)] Engine run configuration was invalid.
    
    Read it with xcrun simctl spawn booted log show --last 5m --predicate 'eventMessage CONTAINS "kernel_blob"'.

Use -c dbg on the simulator, and a device for release.

License

See LICENSE.

About

No description, website, or topics provided.

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages