Skip to content

Commit a76becb

Browse files
committed
feat: bootstrap Flutter plugins through Bazel
1 parent 780e8ae commit a76becb

16 files changed

Lines changed: 322 additions & 236 deletions

File tree

QUICKSTART.md

Lines changed: 75 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,15 @@ Pin Bazel with `.bazelversion`:
6868
9.2.0
6969
```
7070

71-
Use this complete `.bazelrc` in an outside consumer (the examples import the
71+
Use this complete `.bazelrc` in an outside consumer; the examples import
72+
repository-shared cache settings instead:
7273
```
7374
common --enable_bzlmod
7475
7576
build:android --merge_android_manifest_permissions
7677
build:android --tool_java_language_version=17 --tool_java_runtime_version=remotejdk_17
7778
build:android --java_language_version=17 --java_runtime_version=remotejdk_17
79+
common:android --repo_env=ANDROID_NDK_HOME
7880
common --config=android
7981
```
8082

@@ -376,27 +378,24 @@ Future<void> _checkDocumentsDirectory() async {
376378
This is the runtime assertion: after installation it must resolve a directory,
377379
not report `MissingPluginException`.
378380

379-
### Seed generated state before changing the module graph
381+
### Create generated state and wire the plugin graph
380382

381-
The ordering is required. First create the Dart registrant placeholder, but do
382-
**not** create an empty `plugin_deps.MODULE.bazel`:
383+
After `flutter pub get`, create both generated-state files as zero-byte
384+
placeholders. The plugin guards require their committed files to exist, and
385+
`include()` requires its target file to exist while the module is evaluated:
383386

384387
```sh
385-
touch lib/dart_plugin_registrant.dart
386-
# NOT `touch plugin_deps.MODULE.bazel`.
388+
touch plugin_deps.MODULE.bazel lib/dart_plugin_registrant.dart
387389
```
388390

389-
Move the complete `maven = use_extension(...)` through
390-
`use_repo(maven, "flutter_maven")` block from the plugin-free `MODULE.bazel`
391-
above into `plugin_deps.MODULE.bazel` unchanged. It is a valid seed for the
392-
module include; an empty included file does not create `@flutter_maven`, so
393-
`plugins.project(maven_repo = "@flutter_maven//:pin")` cannot resolve.
394-
395-
Then remove that Maven block from `MODULE.bazel` and replace it with the real
396-
plugin graph. Keep the module declaration, `rules_flutter` override, and three
397-
direct Bazel dependencies unchanged.
391+
In the same editing pass, replace the hand-written Maven install in
392+
`android/config.MODULE.bazel` with the Consumer Module's permanent extension
393+
proxy and import, then add the plugin extension and generated segment:
398394

399395
```python
396+
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
397+
use_repo(maven, "flutter_maven")
398+
400399
plugins = use_extension("@rules_flutter//tools/flutter:plugins.bzl", "flutter_plugins_ext")
401400
plugins.project(
402401
abis = ["arm64-v8a", "x86_64"],
@@ -409,9 +408,16 @@ use_repo(plugins, "flutter_plugins")
409408
include("//:plugin_deps.MODULE.bazel")
410409
```
411410

412-
`use_repo` makes the generated per-package targets and
413-
`@flutter_plugins//:all` visible. `include` creates the one Maven repository in
414-
the consumer module. They are different, and both are required.
411+
The Consumer Module owns the Maven proxy and `use_repo(maven, "flutter_maven")`;
412+
the generated segment owns the single `maven.install`. The position of
413+
`include()` relative to `plugins.project()` does not matter, provided the
414+
segment declaring the repository name has been evaluated. Do not leave a
415+
second `use_repo` in the generated segment: declaring the name in both places
416+
fails with:
417+
418+
```text
419+
Error in use_repo: The repo name 'flutter_maven' cannot be defined by a use_repo() call at ... as it is already defined by a use_repo() call at ...
420+
```
415421

416422
### Turn on the Dart and Android plugin graph
417423

@@ -425,9 +431,9 @@ flutter_app(
425431
)
426432
```
427433

428-
Apply that change in the same edit as the module extension above: enabling the
429-
Dart guard without creating `@flutter_plugins` leaves its expected file
430-
unresolvable.
434+
This BUILD edit and the module wiring above are one transition; apply both
435+
before running a guard. With a graph, `flutter_android_binary` derives the
436+
conventional registrant target.
431437

432438
In `android/app/BUILD.bazel`, retain the existing imports, embedding, and
433439
`main_activity`, then replace the opt-out Android setup with the plugin-aware
@@ -467,21 +473,24 @@ registrant target.
467473

468474
### Generate, commit, build, and prove the transition
469475

470-
Use the guards' generated outputs rather than hand-editing either committed
471-
file. The first guard fails against the seed by design; copy its expected file
472-
from the generated repository:
476+
The placeholders make the plugin guard fail once, as expected. Its failure
477+
output names the updater command; run the updater rather than copying from
478+
Bazel's output tree:
473479

474480
```sh
475481
bazel build //:plugins_check
476-
cp "$(bazel info output_base)/$(bazel cquery --output=files @flutter_plugins//:plugin_deps.MODULE.bazel)" plugin_deps.MODULE.bazel
477-
bazel build //:plugins_check
482+
bazel run //:plugins_update
478483

479484
bazel build //:dart_registrant_check
480-
cp "$(bazel info output_base)/$(bazel cquery --output=files @flutter_plugins//:dart_plugin_registrant.dart)" lib/dart_plugin_registrant.dart
481-
bazel build //:dart_registrant_check
485+
# Run the `bazel run //:dart_registrant_update` command printed above.
486+
bazel run //:dart_registrant_update
487+
488+
bazel build //:plugins_check //:dart_registrant_check
482489
bazel test //:guards_test
483490
```
484491

492+
The guard prints this command because it owns the generated artifact's identity.
493+
485494
Commit `plugin_deps.MODULE.bazel` and `lib/dart_plugin_registrant.dart`. Then
486495
build and install the APK:
487496

@@ -495,15 +504,16 @@ adb install -r bazel-bin/android/app/hello_bazel.apk
495504
Launch the app and make the `path_provider` call. It should return the
496505
application documents directory without throwing `MissingPluginException`.
497506
If the app opens but the call throws that exception, the APK build and install
498-
succeeded but plugin registration is stale or missing; refresh the generated
499-
registrants and rebuild.
507+
succeeded but plugin registration is stale or missing; rerun the registrant
508+
guard and its printed updater, then rebuild.
500509

501510
For every later pub plugin addition, removal, or upgrade, run `flutter pub get`.
502-
Flutter refreshes `GeneratedPluginRegistrant.java`; keep its BUILD target dependent
503-
on `@flutter_plugins//:all`, rather than maintaining a per-plugin Bazel list.
504-
Rerun the two guards, copy their generated outputs when they drift, commit them,
505-
rebuild, install, and exercise the changed plugin. Standard CMake-backed plugins
506-
are generated automatically. If a plugin's Maven coordinate cannot be read statically,
511+
Flutter refreshes `GeneratedPluginRegistrant.java`; keep its BUILD target
512+
dependent on `@flutter_plugins//:all`, rather than maintaining a per-plugin
513+
list. Run both guards; when a file drifts, use the updater command printed
514+
by its guard and commit the resulting files. Rebuild, install, and
515+
exercise the changed plugin. Standard CMake-backed plugins are generated
516+
automatically. If a plugin's Maven coordinate cannot be read statically,
507517
declare it on that package instead of adding a second Maven install:
508518

509519
```python
@@ -552,19 +562,17 @@ flutter pub get
552562
cd ../..
553563
```
554564

555-
Before enabling `plugins.project()`, bootstrap the two committed generated-state
556-
files. From the module root, create the Dart placeholder but do **not** create an
557-
empty Maven segment:
565+
Create both committed generated-state files as zero-byte placeholders from the
566+
module root:
558567

559568
```sh
560-
touch packages/host_app/lib/dart_plugin_registrant.dart
561-
# NOT `touch plugin_deps.MODULE.bazel`.
569+
touch plugin_deps.MODULE.bazel packages/host_app/lib/dart_plugin_registrant.dart
562570
```
563571

564-
Seed the root `plugin_deps.MODULE.bazel` by moving the complete
565-
`maven = use_extension(...)` through `use_repo(maven, "flutter_maven")` block
566-
from the plugin-free module above into that file, unchanged. This valid seed
567-
makes `@flutter_maven//:pin` available while `plugins.project()` is evaluated.
572+
The root `include()` requires the Maven segment to exist, and the plugin
573+
guards' committed-file attributes are mandatory. The placeholders carry no
574+
Maven content; the Consumer Module's wiring below makes `@flutter_maven`
575+
visible before the generated segment is populated.
568576

569577
Keep the root module as a composition point and delegate Android configuration:
570578

@@ -614,6 +622,9 @@ android_ndk = use_extension("@rules_flutter//tools/flutter:ndk.bzl", "android_nd
614622
use_repo(android_ndk, "androidndk", "androidndk_cmake")
615623
register_toolchains("@androidndk//:all")
616624

625+
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
626+
use_repo(maven, "flutter_maven")
627+
617628
plugins = use_extension("@rules_flutter//tools/flutter:plugins.bzl", "flutter_plugins_ext")
618629
plugins.project(
619630
abis = ["arm64-v8a"],
@@ -626,6 +637,12 @@ use_repo(plugins, "flutter_plugins")
626637
include("//:plugin_deps.MODULE.bazel")
627638
```
628639

640+
The Maven proxy and import are owned by this Consumer Module; the generated
641+
segment contributes the single install. `include()` may appear before or after
642+
`plugins.project()` because its position does not matter once the segment
643+
declaring the repository name has been evaluated. Do not duplicate the
644+
`use_repo` declaration in the generated segment.
645+
629646
The root needs a Bazel package solely so it can export the included generated
630647
segment:
631648

@@ -700,21 +717,24 @@ the generated Android app; the canonical full file is
700717
[`packages/host_app/android/app/BUILD.bazel`](examples/local_plugin/packages/host_app/android/app/BUILD.bazel).
701718

702719
Generate the root Maven segment and the app's Dart registrant through their
703-
guards; the first build is expected to fail against the seed and print the
704-
expected generated content:
720+
updater targets. The empty placeholders make the plugin guard fail once, and
721+
its failure output names the Maven updater:
705722

706723
```sh
707724
bazel build //packages/host_app:plugins_check
708-
cp "$(bazel info output_base)/$(bazel cquery --output=files @flutter_plugins//:plugin_deps.MODULE.bazel)" plugin_deps.MODULE.bazel
709-
bazel build //packages/host_app:plugins_check
725+
bazel run //packages/host_app:plugins_update
710726

711727
bazel build //packages/host_app:dart_registrant_check
712-
cp "$(bazel info output_base)/$(bazel cquery --output=files @flutter_plugins//:dart_plugin_registrant.dart)" packages/host_app/lib/dart_plugin_registrant.dart
713-
bazel build //packages/host_app:dart_registrant_check
728+
# Run the `bazel run //packages/host_app:dart_registrant_update` command printed above.
729+
bazel run //packages/host_app:dart_registrant_update
730+
731+
bazel build //packages/host_app:plugins_check //packages/host_app:dart_registrant_check
714732
bazel test //packages/host_app:guards_test
715733
bazel build //packages/host_app/android/app:host_app
716734
```
717735

736+
The guard prints this command because it owns the generated artifact's identity.
737+
718738
Adapt all three label families together when your layout differs:
719739
`metadata = "//<app-package>:.flutter-plugins-dependencies"`,
720740
`embedding = "//<app-package>/android/app:flutter_embedding"`,
@@ -999,10 +1019,11 @@ at the monorepo root for the subpackage shape), and map the real
9991019
the explicit root-vs-named `app` label rule.
10001020

10011021
After every pub resolution change, run `flutter pub get`. For any plugin graph,
1002-
then run `:plugins_check` and `:dart_registrant_check`, copy the generator's
1003-
expected files with the `bazel cquery --output=files` commands above, and commit
1004-
them with the lockfile changes. For `path:` dependencies, keep the local Dart
1005-
filegroup in `path_deps`; its content is not represented by a pub lock hash.
1022+
run `:plugins_check` and `:dart_registrant_check`; when either guard reports
1023+
drift, run the updater command it prints and commit the generated files with
1024+
the lockfile changes. For `path:` dependencies,
1025+
keep the local Dart filegroup in `path_deps`; its content is not represented by
1026+
a pub lock hash.
10061027

10071028
Finally, treat building as artifact production, not runtime proof. Discover the
10081029
APK, install it on a device or emulator for a shipped ABI, launch it, and

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ Android release and debug packaging across the supported ABIs, including fat and
251251
- The local Flutter, Android SDK, and NDK installations are not hermetic.
252252
- Most Dart and asset actions are unsandboxed and do not support remote execution; source tracking is imperfect and Dart compilation is not incremental.
253253
- A cold analysis with an empty `HOME` may leave the `Analyzing` count unchanged for minutes while Maven/Coursier, JDK, Flutter engine, Kotlin, NDK, and tool repositories are fetched; continued download or process activity indicates network-bound setup, not proof of a deadlock. When intentionally perturbing `HOME`, pin `BAZELISK_HOME` and Bazel's startup `--output_user_root` to isolate launcher and download caches from rules behavior.
254-
- Native assets require manual consumer recipes, and the first plugin graph requires manual bootstrap and committed generated state.
254+
- Native assets require manual consumer recipes. A first plugin graph still needs two irreducible zero-byte placeholders: `plugin_deps.MODULE.bazel`, because `include()` requires its target file to exist, and `lib/dart_plugin_registrant.dart`, because the plugin guard's committed-file attribute is mandatory. The bootstrap path has no automated regression gate, so follow the [public plugin-graph walkthrough](QUICKSTART.md#create-generated-state-and-wire-the-plugin-graph) by hand.
255255
- Custom release signing is not supported, and `ndk-build` plugins are not supported.
256256
- Plugin Maven coordinates that cannot be read statically require `plugins.package(artifacts = ...)`.
257257
- iOS and other platform packaging are not implemented.

examples/demo_app/MODULE.bazel.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/demo_app/android/config.MODULE.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ bazel_dep(name = "rules_android", version = "0.7.3")
22
bazel_dep(name = "rules_kotlin", version = "2.4.0")
33
bazel_dep(name = "rules_jvm_external", version = "7.1")
44

5+
# The proxy and import stay in this Consumer Module so the mandatory
6+
# `plugins.project(maven_repo = ...)` label resolves before the generated
7+
# segment is included.
8+
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
9+
use_repo(maven, "flutter_maven")
10+
511
# The compile SDK, pinned. Left unpinned, rules_android selects the *highest
612
# installed* platform, which is a machine-dependent input: every APK this
713
# example builds was byte-identical between a developer machine and a CI runner
Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
# Generated by @rules_flutter//tools/flutter:plugins.bzl -- do not edit by hand.
22
#
3-
# The complete Maven artifact list: the Flutter embedding's own dependencies
4-
# (from @rules_flutter//tools/flutter:embedding.bzl) merged with the coordinates
5-
# extracted from every plugin's android/build.gradle. Regenerate with:
3+
# Maven artifacts from the Flutter embedding and plugin build files.
4+
# Run the generated updater target when `pub_plugins_check` reports drift.
65
#
7-
# bazel build //:plugins_check
8-
#
9-
# which prints this file's expected contents when it drifts.
10-
#
11-
# This is the *only* maven.install for @flutter_maven, deliberately. Two install
12-
# tags sharing a repository name merge into one resolution and the later tag
13-
# wins outright -- not highest-version, not declaration order -- so splitting
14-
# the list let a plugin silently downgrade an artifact the embedding declared.
15-
# One list, resolved once, with highest-wins applied across all of it.
6+
# The Consumer Module must import `maven_repo` before this segment; it declares
7+
# the sole `flutter_maven` install, avoiding version skew from split installs.
168
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
179
maven.install(
1810
name = "flutter_maven",
@@ -35,9 +27,6 @@ maven.install(
3527
"https://maven.google.com",
3628
"https://repo1.maven.org/maven2",
3729
],
38-
# Every artifact above is already reduced to one version, so "pinned" makes
39-
# those chosen versions authoritative over transitive suggestions rather
40-
# than failing resolution when a pom asks for something older.
30+
# Keep the selected artifact versions authoritative.
4131
version_conflict_policy = "pinned",
4232
)
43-
use_repo(maven, "flutter_maven")

examples/local_plugin/MODULE.bazel.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/local_plugin/packages/host_app/android/config.MODULE.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ bazel_dep(name = "rules_android", version = "0.7.3")
22
bazel_dep(name = "rules_kotlin", version = "2.4.0")
33
bazel_dep(name = "rules_jvm_external", version = "7.1")
44

5+
# The proxy and import stay in this Consumer Module so the mandatory
6+
# `plugins.project(maven_repo = ...)` label resolves before the generated
7+
# segment is included.
8+
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
9+
use_repo(maven, "flutter_maven")
10+
511
android_sdk = use_extension(
612
"@rules_android//rules/android_sdk_repository:rule.bzl",
713
"android_sdk_repository_extension",
Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
# Generated by @rules_flutter//tools/flutter:plugins.bzl -- do not edit by hand.
22
#
3-
# The complete Maven artifact list: the Flutter embedding's own dependencies
4-
# (from @rules_flutter//tools/flutter:embedding.bzl) merged with the coordinates
5-
# extracted from every plugin's android/build.gradle. Regenerate with:
3+
# Maven artifacts from the Flutter embedding and plugin build files.
4+
# Run the generated updater target when `pub_plugins_check` reports drift.
65
#
7-
# bazel build //packages/host_app:plugins_check
8-
#
9-
# which prints this file's expected contents when it drifts.
10-
#
11-
# This is the *only* maven.install for @flutter_maven, deliberately. Two install
12-
# tags sharing a repository name merge into one resolution and the later tag
13-
# wins outright -- not highest-version, not declaration order -- so splitting
14-
# the list let a plugin silently downgrade an artifact the embedding declared.
15-
# One list, resolved once, with highest-wins applied across all of it.
6+
# The Consumer Module must import `maven_repo` before this segment; it declares
7+
# the sole `flutter_maven` install, avoiding version skew from split installs.
168
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
179
maven.install(
1810
name = "flutter_maven",
@@ -34,9 +26,6 @@ maven.install(
3426
"https://maven.google.com",
3527
"https://repo1.maven.org/maven2",
3628
],
37-
# Every artifact above is already reduced to one version, so "pinned" makes
38-
# those chosen versions authoritative over transitive suggestions rather
39-
# than failing resolution when a pom asks for something older.
29+
# Keep the selected artifact versions authoritative.
4030
version_conflict_policy = "pinned",
4131
)
42-
use_repo(maven, "flutter_maven")

examples/pub_plugins/MODULE.bazel.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/pub_plugins/android/config.MODULE.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ bazel_dep(name = "rules_android", version = "0.7.3")
22
bazel_dep(name = "rules_kotlin", version = "2.4.0")
33
bazel_dep(name = "rules_jvm_external", version = "7.1")
44

5+
# The proxy and import stay in this Consumer Module so the mandatory
6+
# `plugins.project(maven_repo = ...)` label resolves before the generated
7+
# segment is included.
8+
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
9+
use_repo(maven, "flutter_maven")
10+
511
android_sdk = use_extension(
612
"@rules_android//rules/android_sdk_repository:rule.bzl",
713
"android_sdk_repository_extension",

0 commit comments

Comments
 (0)