Skip to content

[jnigen] Remove the need to run flutter build apk#3303

Merged
liamappelbe merged 30 commits into
mainfrom
jnigen_no_build
Apr 22, 2026
Merged

[jnigen] Remove the need to run flutter build apk#3303
liamappelbe merged 30 commits into
mainfrom
jnigen_no_build

Conversation

@liamappelbe

@liamappelbe liamappelbe commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

This PR removes the need to run flutter build apk.

The only important changes are in pkgs/jnigen/lib/src/tools/android_sdk_tools.dart, in the gradle stubs.

JNIgen gathers dependencies by injecting a temporary stub into the build.gradle file. The goal of the stub is to find all the relevant files, and print their paths stdout. Gradle is then run in a subprocess and the paths are read from its stdout. There are 4 stubs, because we need to support loading JARs (classpath stub) vs loading source files, and we need to support both Groovy and Kotlin build.gradle files.

The stubs have been completely rewritten. All four now use the same 3 step process:

  1. Tell gradle that all the subprojects are dependencies of this project. This is necessary for multi project setups like when you have a Flutter plugin and its example app.
  2. Get all the JARs from all the dependencies.
  3. Extract the paths we care about from the JARs.

Steps 1 and 2 happen at config time, and step 3 happens at doLast time. Unfortunately this leads to some code duplication, because we need very similar looking queries to find all the JARs in steps 2 and 3.

The classpath stub now has to explicitly extract all the JAR files that are stored in AARs. That used to happen automatically during flutter build apk.

The old classpath stubs made sure to list the android.getBootClasspath first, so that JNIgen would see them first. That way, any duplicates of the core libraries would be ignored. This is particularly important for dealing with the Jetifier on Android, which produces a bunch of stub classes for core libraries. The new classpath stubs make sure to maintain the same ordering.

Examples

I added example/maven_lib and example/maven_lib_groovy to test the new stubs, because I found the existing examples were a bit too simple. They're identical except that they use Kotlin and Groovy respectively in their build.gradle files

They're plugins that depend on GSON and OkHttp as maven dependencies (rather than checking in their source code into third_party). The plugin depends on GSON, and the plugin's example app depends on OkHttp, but the bindings for both exist in the plugin. It's a weird dependency layout designed to stress test the stubs.

gradlew files

There's one other thing that flutter build apk does that we didn't anticipate: create the gradelw files if they're missing. The default Flutter .gitignore policy is to exclude these from the repo, though it's also valid to check these files into your github repo (in fact that's the recommended policy according to the gradle docs).

We work around this by automatically running flutter build apk --config-only when JNIgen is run in a flutter app. This is a lightweight command to run, and creates the gradle wrappers if they're missing. It also implicitly runs flutter pub get, which is handy.

To test the checked-in workflow, I'm also checking in the gradle wrappers for the maven_libs_groovy example.

Fixes #628
Fixes #1661
Fixes #1660
Related #1972
Related #576

@github-actions github-actions Bot added type-infra A repository infrastructure change or enhancement package:jnigen labels Apr 13, 2026
@github-actions

github-actions Bot commented Apr 14, 2026

Copy link
Copy Markdown

PR Health

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check.

Breaking changes ✔️
Package Change Current Version New Version Needed Version Looking good?
jni_flutter None 1.0.1 1.0.1 1.0.1 ✔️

This check can be disabled by tagging the PR with skip-breaking-check.

API leaks ✔️

The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.

Package Leaked API symbol Leaking sources

This check can be disabled by tagging the PR with skip-leaking-check.

@liamappelbe liamappelbe changed the title WIP [jnigen] Remove the need to run flutter build apk [jnigen] Remove the need to run flutter build apk Apr 15, 2026
@liamappelbe liamappelbe marked this pull request as ready for review April 15, 2026 06:36
@liamappelbe liamappelbe requested review from dcharkes and jwill April 15, 2026 06:36

@dcharkes dcharkes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also valid to check these files into your github repo. In fact that's the recommended policy according to the gradle docs. It has been suggested that Flutter should change this default gitignore, but there's been some push back. The push back in Flutter seems fairly mild to me, so if this becomes an issue for many users we can probably file an issue about it.

@reidbaker What are your thoughts on checking in these files in Flutter plugins that depend on JNIgen so that JNIgen can run without having to run flutter build apk?

Comment thread .github/workflows/jnigen.yaml Outdated
Comment thread pkgs/jni_flutter/.gitignore Outdated
# Allow Gradle wrapper
!gradlew
!gradlew.bat
!gradle-wrapper.jar

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are 52kb jars? Why do we need to commit them?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a part of bootstrapping Gradle and making sure the intended version of gradle is the one that is used to build the underlying project.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option would be to do flutter build apk --config-only in the workflows. This creates the gradlew files without actually doing a build.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline:

  • We can check in one project with all the generated files.
  • And for the other projects we can detect it's a flutter project and run flutter build apk --config-only inside JNIgen.

For a future where we have hooks/generate.dart, Flutter should make sure flutter build apk --config-only is run before the generate hook so that automatic detection would become a noop.

Comment thread pkgs/jnigen/example/maven_libs/LICENSE Outdated
@@ -0,0 +1 @@
TODO: Add your license here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to have the Dart license for our example projecs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other examples don't have a LICENSE file. Deleted.

Comment thread pkgs/jnigen/example/maven_libs/pubspec.yaml Outdated
Comment thread pkgs/jnigen/example/maven_libs/CHANGELOG.md Outdated
Comment thread pkgs/jnigen/example/maven_libs_groovy/CHANGELOG.md Outdated
Comment thread pkgs/jnigen/example/maven_libs/example/analysis_options.yaml Outdated
throw e
}
}
System.err.println("If you are seeing this error in `flutter build` output, it is likely that JNIgen left some stubs in the build.gradle file. Please restore that file from your version control system or manually remove the stub functions named getReleaseCompileClasspath and / or getSources.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful to have a docs markdown file where we can point from the error message so that we can add docs there. Or even a GitHub issue?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷 Not sure what needs to be expanded into a doc in this error message. Is there more we need to say here?


static void _runFlutterConfigOnly(String androidProject) {
// Only run if it looks like a flutter project.
if (!File(join(androidProject, 'pubspec.yaml')).existsSync()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, best heuristic ever 😆

jni$_.JMethodIDPtr,
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>,
)>>('globalEnv_CallObjectMethod')
jni$_.NativeFunction<

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did the formatter change? what version of the formatter are we using?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some combination of flutter create and gemini CLI initially created the examples with the SDK version set to the most recent Dart version. This caused problems on CI, so I had to change it to match the bounds of the other examples, which caused formatting changes.

@goderbauer goderbauer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also update the PR description for future archeologists (i.e. the section about "gradlew files" and how we now attempt to autogenerate them)?

Comment thread pkgs/jnigen/CHANGELOG.md Outdated

static void _runFlutterConfigOnly(String androidProject) {
// Only run if it looks like a flutter project.
if (!File(join(androidProject, 'pubspec.yaml')).existsSync()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why's the presence of a pubspec.yaml enough to assume a Flutter project? How is it garanteed that it isn't a standalone dart project?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole flow is gated by Config.androidSdkConfig being non-null, so it should only be run for Android projects. Though I guess by that argument we probably don't need the pubspec check at all. Probably should just check the flutter: key exists.

@goderbauer goderbauer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀

@liamappelbe liamappelbe merged commit 27890da into main Apr 22, 2026
37 checks passed
@liamappelbe liamappelbe deleted the jnigen_no_build branch April 22, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package:jni_flutter package:jnigen type-infra A repository infrastructure change or enhancement

Projects

None yet

4 participants