Skip to content

flutter config --explicit-package-dependencies breaks flutter build apk #160407

Closed

Description

@matanlurey

Looking at the failures still being flagged in #160289, I see a trend where flutter build apk (which implicitly is release mode) causes the integration_test package itself to fail to be built (example); I'm guessing that when we short-circuit the build, we're still leaving artifacts that integration_test is depended on, but not in a way that compilation succeeds.

Reproduces at HEAD:

# Opt-in to the feature
flutter config --explicit-package-dependencies

# Create an Android plugin
flutter create plugin_a --template=plugin --platforms=android

# Try to build the example app for the plugin
cd plugin_a/example
flutter build apk

By opting-out again (flutter config --no-explicit-package-dependencies) the build succeeds.

Interestingly, the integration test for this feature does do a flutter build apk --release, so I'm guessing this has something to do with how integration_test imports and uses the Android embedder (i.e. import io.flutter.embedding.android.FlutterActivity and like), and the default (empty) plugin template does not - we should add a regression test as part of fixing this.


Some potentially useful bits:

With the flag enabled, .flutter-plugins-dependencies looks like this
{
  "info": "This is a generated file; do not edit or check into version control.",
  "plugins": {
    "ios": [
      {
        "name": "integration_test",
        "path": "/Users/matanl/Developer/flutter/packages/integration_test/",
        "native_build": true,
        "dependencies": [],
        "dev_dependency": true
      }
    ],
    "android": [
      {
        "name": "integration_test",
        "path": "/Users/matanl/Developer/flutter/packages/integration_test/",
        "native_build": true,
        "dependencies": [],
        "dev_dependency": true
      },
      {
        "name": "plugin_a",
        "path": "/private/tmp/flutter.tmp/plugin_a/",
        "native_build": true,
        "dependencies": [],
        "dev_dependency": false
      }
    ],
    "macos": [],
    "linux": [],
    "windows": [],
    "web": []
  },
  "dependencyGraph": [
    {
      "name": "integration_test",
      "dependencies": []
    },
    {
      "name": "plugin_a",
      "dependencies": []
    }
  ],
  "date_created": "2024-12-16 20:52:06.884223",
  "version": "3.28.0-1.0.pre.157",
  "swift_package_manager_enabled": {
    "ios": false,
    "macos": false
  }
}
Here is what plugin_a/pubspec.yaml looks like
dependencies:
  flutter:
    sdk: flutter

  plugin_a:
    # When depending on this package from a real application you should use:
    #   plugin_a: ^x.y.z
    # See https://dart.dev/tools/pub/dependencies#version-constraints
    # The example app is bundled with the plugin so we use a path dependency on
    # the parent directory to use the current plugin's version.
    path: ../

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8

dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^5.0.0

I assume it's further work required in flutter.groovy:

private void configurePluginProject(Map<String, Object> pluginObject) {
assert(pluginObject.name instanceof String)
Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
if (pluginProject == null) {
return
}
// Apply the "flutter" Gradle extension to plugins so that they can use it's vended
// compile/target/min sdk values.
pluginProject.extensions.create("flutter", FlutterExtension)
// Add plugin dependency to the app project.
project.android.buildTypes.each { buildType ->
String flutterBuildMode = buildModeFor(buildType)
if (flutterBuildMode != "release" || !pluginObject.dev_dependency) {
// Only add dependency on dev dependencies in non-release builds.
project.dependencies {
api(pluginProject)
}
}
}
Closure addEmbeddingDependencyToPlugin = { buildType ->
String flutterBuildMode = buildModeFor(buildType)
// In AGP 3.5, the embedding must be added as an API implementation,
// so java8 features are desugared against the runtime classpath.
// For more, see https://github.com/flutter/flutter/issues/40126
if (!supportsBuildMode(flutterBuildMode)) {
return
}
if (!pluginProject.hasProperty("android")) {
return
}
if (flutterBuildMode == "release" && pluginObject.dev_dependency) {
// This plugin is a dev dependency and will not be included in
// the release build, so no need to add the embedding
// dependency to it.
return
}
// Copy build types from the app to the plugin.
// This allows to build apps with plugins and custom build types or flavors.
pluginProject.android.buildTypes {
"${buildType.name}" {}
}
// The embedding is API dependency of the plugin, so the AGP is able to desugar
// default method implementations when the interface is implemented by a plugin.
//
// See https://issuetracker.google.com/139821726, and
// https://github.com/flutter/flutter/issues/72185 for more details.
addApiDependencies(
pluginProject,
buildType.name,
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion"
)
}
// Wait until the Android plugin loaded.
pluginProject.afterEvaluate {
// Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
if (pluginProject.android.compileSdkVersion > project.android.compileSdkVersion) {
project.logger.quiet("Warning: The plugin ${pluginObject.name} requires Android SDK version ${getCompileSdkFromProject(pluginProject)} or higher.")
project.logger.quiet("For more information about build configuration, see $kWebsiteDeploymentAndroidBuildConfig.")
}
project.android.buildTypes.all(addEmbeddingDependencyToPlugin)
}
}

Activity

  1. added
    platform-androidAndroid applications specifically
    f: integration_testThe flutter/packages/integration_test plugin
    P1High-priority issues at the top of the work list
    on Dec 17, 2024
  2. changed the title [-]Enabling `flutter config --explicit-package-dependencies` breaks release compilation[/-] [+]`flutter config --explicit-package-dependencies` breaks `flutter build apk`[/+] on Dec 17, 2024
  3. matanlurey commented on Dec 17, 2024

    @matanlurey
    ContributorAuthor

    Oh, you know what it is... we don't strip out the import in GeneratedPluginRegistrant.java:

    package io.flutter.plugins;
    
    import androidx.annotation.Keep;
    import androidx.annotation.NonNull;
    import io.flutter.Log;
    
    import io.flutter.embedding.engine.FlutterEngine;
    
    /**
     * Generated file. Do not edit.
     * This file is generated by the Flutter tool based on the
     * plugins that support the Android platform.
     */
    @Keep
    public final class GeneratedPluginRegistrant {
      private static final String TAG = "GeneratedPluginRegistrant";
      public static void registerWith(@NonNull FlutterEngine flutterEngine) {
        try {
          flutterEngine.getPlugins().add(new dev.flutter.plugins.integration_test.IntegrationTestPlugin());
        } catch (Exception e) {
          Log.e(TAG, "Error registering plugin integration_test, dev.flutter.plugins.integration_test.IntegrationTestPlugin", e);
        }
        try {
          flutterEngine.getPlugins().add(new com.example.plugin_a.Plugin_aPlugin());
        } catch (Exception e) {
          Log.e(TAG, "Error registering plugin plugin_a, com.example.plugin_a.Plugin_aPlugin", e);
        }
      }
    }
  4. matanlurey commented on Dec 17, 2024

    @matanlurey
    ContributorAuthor

    Hmm, that wasn't sufficient - I threw together this patch and there are still the same compilation errors:

    diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart
    index d95392c561..886477a466 100644
    --- a/packages/flutter_tools/lib/src/flutter_plugins.dart
    +++ b/packages/flutter_tools/lib/src/flutter_plugins.dart
    @@ -354,7 +354,7 @@ public final class GeneratedPluginRegistrant {
     }
     ''';
     
    -List<Map<String, Object?>> _extractPlatformMaps(List<Plugin> plugins, String type) {
    +List<Map<String, Object?>> _extractPlatformMaps(Iterable<Plugin> plugins, String type) {
       return <Map<String, Object?>>[
         for (final Plugin plugin in plugins)
           if (plugin.platforms[type] case final PluginPlatform platformPlugin)
    @@ -362,8 +362,12 @@ List<Map<String, Object?>> _extractPlatformMaps(List<Plugin> plugins, String typ
       ];
     }
     
    -Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
    -  final List<Plugin> methodChannelPlugins = _filterMethodChannelPlugins(plugins, AndroidPlugin.kConfigKey);
    +Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins, {required bool releaseMode}) async {
    +  Iterable<Plugin> methodChannelPlugins = _filterMethodChannelPlugins(plugins, AndroidPlugin.kConfigKey);
    +  if (releaseMode) {
    +    methodChannelPlugins = methodChannelPlugins.where((Plugin p) => !p.isDevDependency);
    +  }
    +
       final List<Map<String, Object?>> androidPlugins = _extractPlatformMaps(methodChannelPlugins, AndroidPlugin.kConfigKey);
     
       final Map<String, Object> templateContext = <String, Object>{
    @@ -391,6 +395,7 @@ Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin>
         globals.fs.file(registryPath),
         globals.templateRenderer,
       );
    +  print(globals.fs.file(registryPath).readAsStringSync());
     }
     
     const String _objcPluginRegistryHeaderTemplate = '''
    @@ -1129,12 +1134,13 @@ Future<void> injectPlugins(
       bool windowsPlatform = false,
       Iterable<String>? allowedPlugins,
       DarwinDependencyManagement? darwinDependencyManagement,
    +  bool? releaseMode,
     }) async {
       final List<Plugin> plugins = await findPlugins(project);
       final Map<String, List<Plugin>> pluginsByPlatform = _resolvePluginImplementations(plugins, pluginResolutionType: _PluginResolutionType.nativeOrDart);
     
       if (androidPlatform) {
    -    await _writeAndroidPluginRegistrant(project, pluginsByPlatform[AndroidPlugin.kConfigKey]!);
    +    await _writeAndroidPluginRegistrant(project, pluginsByPlatform[AndroidPlugin.kConfigKey]!, releaseMode: releaseMode ?? false);
       }
       if (iosPlatform) {
         await _writeIOSPluginRegistrant(project, pluginsByPlatform[IOSPlugin.kConfigKey]!);
    diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
    index 6d5a6aaa24..93785a6720 100644
    --- a/packages/flutter_tools/lib/src/project.dart
    +++ b/packages/flutter_tools/lib/src/project.dart
    @@ -346,6 +346,7 @@ class FlutterProject {
       Future<void> regeneratePlatformSpecificTooling({
         DeprecationBehavior deprecationBehavior = DeprecationBehavior.none,
         Iterable<String>? allowedPlugins,
    +    bool? releaseMode,
       }) async {
         return ensureReadyForPlatformSpecificTooling(
           androidPlatform: android.existsSync(),
    @@ -358,6 +359,7 @@ class FlutterProject {
           webPlatform: featureFlags.isWebEnabled && web.existsSync(),
           deprecationBehavior: deprecationBehavior,
           allowedPlugins: allowedPlugins,
    +      releaseMode: releaseMode,
         );
       }
     
    @@ -372,6 +374,7 @@ class FlutterProject {
         bool webPlatform = false,
         DeprecationBehavior deprecationBehavior = DeprecationBehavior.none,
         Iterable<String>? allowedPlugins,
    +    bool? releaseMode,
       }) async {
         if (!directory.existsSync() || isPlugin) {
           return;
    @@ -399,6 +402,7 @@ class FlutterProject {
         if (webPlatform) {
           await web.ensureReadyForPlatformSpecificTooling();
         }
    +    print('>>> releaseMode: $releaseMode');
         await injectPlugins(
           this,
           androidPlatform: androidPlatform,
    @@ -407,6 +411,7 @@ class FlutterProject {
           macOSPlatform: macOSPlatform,
           windowsPlatform: windowsPlatform,
           allowedPlugins: allowedPlugins,
    +      releaseMode: releaseMode,
         );
       }
     
    diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
    index 82b117ec6d..ff777585e2 100644
    --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
    +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
    @@ -1765,6 +1765,7 @@ Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and
           }
           await project.regeneratePlatformSpecificTooling(
             allowedPlugins: allowedPlugins,
    +        releaseMode: getBuildMode().isRelease,
           );
           if (reportNullSafety) {
             await _sendNullSafetyAnalyticsEvents(project);
  5. camsim99 commented on Dec 20, 2024

    @camsim99
    Contributor

    Started digging into this and here's what I know:

    • If I remove
      if (flutterBuildMode == "release" && pluginObject.dev_dependency) {
      // This plugin is a dev dependency and will not be included in
      // the release build, so no need to add the embedding
      // dependency to it.
      return
      }
      , all works as expected.
    • When we run flutter build apk, the Flutter Gradle plugin configures plugins and their dependencies for debug and release builds (this might just be how apks are built? still trying to determine that)

    What I've been trying to dig into now is:

    1. What happens exactly when we run flutter build apk. I want to know the exact Gradle tasks it runs because it may be possibly my integration test did work and there are some tasks that still require compiling a plugin project though it won't be included in the final APK. I think this is less likely, so I think I'll spend most of my time now looking into...
    2. If it's possible that logic in
      private void configurePluginDependencies(Map<String, Object> pluginObject) {
      or elsewhere that I'm missing is accidentally still configuring plugin projects that we wish to exclude in release mode as dependencies to the app.
  6. camsim99 commented on Dec 23, 2024

    @camsim99
    Contributor

    I now suspect that the issue was an oopsie on my part. We ideally want the app project to only depend on the dev dependency if it is a release build. I tried doing this in

    project.android.buildTypes.each { buildType ->
    String flutterBuildMode = buildModeFor(buildType)
    if (flutterBuildMode != "release" || !pluginObject.dev_dependency) {
    // Only add dependency on dev dependencies in non-release builds.
    project.dependencies {
    api(pluginProject)
    }
    }
    }

    but I don't think that actually works 😅 Need to figure out how to do something similar to api(...) but only for release builds.

    Side note: ./gradlew :app:dependencies --configuration releaseRuntimeClasspath has been helpful visualizing the dependency graph of what's include in the release APK.

  7. jmagman commented on Dec 23, 2024

    @jmagman
    Member

    Are tests not runnable in release mode?

  8. camsim99 commented on Dec 23, 2024

    @camsim99
    Contributor

    Are tests not runnable in release mode?

    Basically no when my featured is enabled and if the test depends on ‘integration_test’. Or at least that’s what @matanlurey has found so far is my understanding.

  9. camsim99 commented on Dec 23, 2024

    @camsim99
    Contributor

    Need to figure out how to do something similar to api(...) but only for release builds.

    Tried using debugApi and this did remove it from the graph printed by ./gradlew :app:dependencies --configuration releaseRuntimeClasspath but I still get the same error...

    Note to self that I had to apply Matan's patch to modify the test plugin's example app's GeneratedPluginRegistrant.java.

  10. camsim99 commented on Jan 8, 2025

    @camsim99
    Contributor

    I think I've found a solution and will follow up with a PR shortly.

    TLDR is that there were a couple of issues:

    • The integration test I wrote was not correct because (1) you need to throw TaskResults in order to make the test fail, and (2) I was always checking the debug APK instead of the APK based on the build type I was testing.
    • We need to use the <buildType>Api methods to configure dependencies per build type; the logic I wrote before was wrong, as I suspected.
  11. matanlurey commented on Jan 8, 2025

    @matanlurey
    ContributorAuthor

    Nice find! It would be really good to get this fixed before the branch cut, as otherwise it will take another quarter to flip --explicit-package-dependencies.

  12. camsim99 commented on Jan 8, 2025

    @camsim99
    Contributor

    Oop! Ok there's a tiny detail I'm not 100% sure about but I'll put up a PR right now and we can discuss there.

    Nice find! It would be really good to get this fixed before the branch cut, as otherwise it will take another quarter to flip --explicit-package-dependencies.

  13. camsim99 commented on Jan 8, 2025

    @camsim99
    Contributor

    I think I've found a solution and will follow up with a PR shortly.

    TLDR is that there were a couple of issues:

    • The integration test I wrote was not correct because (1) you need to throw TaskResults in order to make the test fail, and (2) I was always checking the debug APK instead of the APK based on the build type I was testing.
    • We need to use the <buildType>Api methods to configure dependencies per build type; the logic I wrote before was wrong, as I suspected.

    Quick notes that I forgot: also the plugin does still need the embedding dependency, that is irrelevant to it being included in a build. Also I needed Matan's patch to fix the generated plugin registrant. And the integration test needs to use the --explicit-package-dependencies flag for now :)

  14. github-actions commented on Jan 28, 2025

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  15. locked as resolved and limited conversation to collaborators on Jan 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1High-priority issues at the top of the work listf: integration_testThe flutter/packages/integration_test pluginplatform-androidAndroid applications specificallyteam-androidOwned by Android platform team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions