From b8c3628f31da517b9380d5ba8cb973f8d50d24f2 Mon Sep 17 00:00:00 2001 From: Daco Harkes Date: Wed, 26 Jan 2022 23:44:45 +0100 Subject: [PATCH] FFI plugins (#96225) --- dev/devicelab/bin/tasks/plugin_test.dart | 2 + dev/devicelab/bin/tasks/plugin_test_ios.dart | 3 + dev/devicelab/lib/tasks/plugin_tests.dart | 18 +- .../windows/flutter/generated_plugins.cmake | 8 + packages/flutter_tools/gradle/flutter.gradle | 54 +++++- .../lib/src/commands/create.dart | 154 +++++++++++++++--- .../lib/src/commands/create_base.dart | 21 ++- .../lib/src/flutter_plugins.dart | 148 ++++++++++------- .../lib/src/flutter_project_metadata.dart | 5 + .../lib/src/platform_plugins.dart | 128 +++++++++++---- packages/flutter_tools/lib/src/plugins.dart | 9 +- packages/flutter_tools/templates/README.md | 9 +- .../templates/app/lib/main.dart.tmpl | 80 ++++++++- .../templates/app/pubspec.yaml.tmpl | 4 +- .../android-java.tmpl/app/build.gradle.tmpl | 1 + .../android-kotlin.tmpl/app/build.gradle.tmpl | 1 + .../app_shared/linux.tmpl/CMakeLists.txt.tmpl | 6 +- .../test/widget_test.dart.tmpl | 4 +- .../Flutter.tmpl/build.gradle.tmpl | 1 + .../module/common/lib/main.dart.tmpl | 12 +- .../module/common/test/widget_test.dart.tmpl | 6 +- .../plugin/.idea/libraries/Dart_SDK.xml.tmpl | 19 --- .../templates/plugin/.idea/modules.xml.tmpl | 10 -- .../example_lib_main_dart.xml.tmpl | 6 - .../templates/plugin/.idea/workspace.xml.tmpl | 45 ----- .../templates/plugin/README.md.tmpl | 4 +- .../templates/plugin_ffi/README.md.tmpl | 97 +++++++++++ .../plugin_ffi/android.tmpl/build.gradle.tmpl | 59 +++++++ .../android.tmpl/projectName_android.iml.tmpl | 27 +++ .../templates/plugin_ffi/ffigen.yaml.tmpl | 19 +++ .../ios.tmpl/Classes/projectName.c.tmpl | 3 + .../ios.tmpl/projectName.podspec.tmpl | 28 ++++ .../plugin_ffi/lib/projectName.dart.tmpl | 139 ++++++++++++++++ .../projectName_bindings_generated.dart.tmpl | 69 ++++++++ .../plugin_ffi/linux.tmpl/CMakeLists.txt.tmpl | 22 +++ .../macos.tmpl/Classes/projectName.c.tmpl | 3 + .../plugin_ffi/src.tmpl/CMakeLists.txt.tmpl | 17 ++ .../plugin_ffi/src.tmpl/projectName.c.tmpl | 23 +++ .../plugin_ffi/src.tmpl/projectName.h.tmpl | 30 ++++ .../windows.tmpl/CMakeLists.txt.tmpl | 23 +++ .../{plugin => plugin_shared}/.gitignore.tmpl | 0 .../{plugin => plugin_shared}/.metadata.tmpl | 5 + .../CHANGELOG.md.tmpl | 0 .../{plugin => plugin_shared}/LICENSE.tmpl | 0 .../analysis_options.yaml.tmpl | 0 .../plugin_shared/android.tmpl/.gitignore | 9 + .../android.tmpl/settings.gradle.tmpl | 1 + .../src/main/AndroidManifest.xml.tmpl | 3 + .../macos.tmpl/projectName.podspec.tmpl | 7 + .../projectName.iml.tmpl | 2 +- .../pubspec.yaml.tmpl | 56 ++++++- .../windows.tmpl/.gitignore | 0 .../templates/template_manifest.json | 50 ++++-- .../hermetic/create_usage_test.dart | 6 + .../commands.shard/permeable/create_test.dart | 99 +++++++++++ ...android_plugin_example_app_build_test.dart | 45 ++++- ...droid_plugin_ndkversion_mismatch_test.dart | 86 ++++++++++ .../deferred_components_project.dart | 1 + .../test/src/pubspec_schema.dart | 8 +- 59 files changed, 1441 insertions(+), 254 deletions(-) rename packages/flutter_tools/templates/{app => app_test_widget}/test/widget_test.dart.tmpl (95%) delete mode 100644 packages/flutter_tools/templates/plugin/.idea/libraries/Dart_SDK.xml.tmpl delete mode 100644 packages/flutter_tools/templates/plugin/.idea/modules.xml.tmpl delete mode 100644 packages/flutter_tools/templates/plugin/.idea/runConfigurations/example_lib_main_dart.xml.tmpl delete mode 100644 packages/flutter_tools/templates/plugin/.idea/workspace.xml.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/README.md.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/android.tmpl/build.gradle.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/android.tmpl/projectName_android.iml.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/ffigen.yaml.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/ios.tmpl/Classes/projectName.c.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/ios.tmpl/projectName.podspec.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/lib/projectName.dart.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/linux.tmpl/CMakeLists.txt.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/macos.tmpl/Classes/projectName.c.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/src.tmpl/CMakeLists.txt.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/src.tmpl/projectName.c.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/src.tmpl/projectName.h.tmpl create mode 100644 packages/flutter_tools/templates/plugin_ffi/windows.tmpl/CMakeLists.txt.tmpl rename packages/flutter_tools/templates/{plugin => plugin_shared}/.gitignore.tmpl (100%) rename packages/flutter_tools/templates/{plugin => plugin_shared}/.metadata.tmpl (67%) rename packages/flutter_tools/templates/{plugin => plugin_shared}/CHANGELOG.md.tmpl (100%) rename packages/flutter_tools/templates/{plugin => plugin_shared}/LICENSE.tmpl (100%) rename packages/flutter_tools/templates/{plugin => plugin_shared}/analysis_options.yaml.tmpl (100%) create mode 100644 packages/flutter_tools/templates/plugin_shared/android.tmpl/.gitignore create mode 100644 packages/flutter_tools/templates/plugin_shared/android.tmpl/settings.gradle.tmpl create mode 100644 packages/flutter_tools/templates/plugin_shared/android.tmpl/src/main/AndroidManifest.xml.tmpl rename packages/flutter_tools/templates/{plugin => plugin_shared}/macos.tmpl/projectName.podspec.tmpl (68%) rename packages/flutter_tools/templates/{plugin => plugin_shared}/projectName.iml.tmpl (98%) rename packages/flutter_tools/templates/{plugin => plugin_shared}/pubspec.yaml.tmpl (61%) rename packages/flutter_tools/templates/{plugin => plugin_shared}/windows.tmpl/.gitignore (100%) create mode 100644 packages/flutter_tools/test/integration.shard/android_plugin_ndkversion_mismatch_test.dart diff --git a/dev/devicelab/bin/tasks/plugin_test.dart b/dev/devicelab/bin/tasks/plugin_test.dart index 9b556cded7000..422c683c47233 100644 --- a/dev/devicelab/bin/tasks/plugin_test.dart +++ b/dev/devicelab/bin/tasks/plugin_test.dart @@ -18,5 +18,7 @@ Future main() async { {'ENABLE_ANDROID_EMBEDDING_V2': 'true'}), // Test that Dart-only plugins are supported. PluginTest('apk', ['--platforms=android'], dartOnlyPlugin: true), + // Test that FFI plugins are supported. + PluginTest('apk', ['--platforms=android'], template: 'plugin_ffi'), ])); } diff --git a/dev/devicelab/bin/tasks/plugin_test_ios.dart b/dev/devicelab/bin/tasks/plugin_test_ios.dart index 408cf1b87be82..6862d453c6c88 100644 --- a/dev/devicelab/bin/tasks/plugin_test_ios.dart +++ b/dev/devicelab/bin/tasks/plugin_test_ios.dart @@ -13,5 +13,8 @@ Future main() async { // Test that Dart-only plugins are supported. PluginTest('ios', ['--platforms=ios'], dartOnlyPlugin: true), PluginTest('macos', ['--platforms=macos'], dartOnlyPlugin: true), + // Test that FFI plugins are supported. + PluginTest('ios', ['--platforms=ios'], template: 'plugin_ffi'), + PluginTest('macos', ['--platforms=macos'], template: 'plugin_ffi'), ])); } diff --git a/dev/devicelab/lib/tasks/plugin_tests.dart b/dev/devicelab/lib/tasks/plugin_tests.dart index 4de88a284a4bd..f04de84698294 100644 --- a/dev/devicelab/lib/tasks/plugin_tests.dart +++ b/dev/devicelab/lib/tasks/plugin_tests.dart @@ -32,6 +32,7 @@ class PluginTest { this.pluginCreateEnvironment, this.appCreateEnvironment, this.dartOnlyPlugin = false, + this.template = 'plugin', }); final String buildTarget; @@ -39,20 +40,27 @@ class PluginTest { final Map? pluginCreateEnvironment; final Map? appCreateEnvironment; final bool dartOnlyPlugin; + final String template; Future call() async { final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_plugin_test.'); + // FFI plugins do not have support for `flutter test`. + // `flutter test` does not do a native build. + // Supporting `flutter test` would require invoking a native build. + final bool runFlutterTest = template != 'plugin_ffi'; try { section('Create plugin'); final _FlutterProject plugin = await _FlutterProject.create( tempDir, options, buildTarget, - name: 'plugintest', template: 'plugin', environment: pluginCreateEnvironment); + name: 'plugintest', template: template, environment: pluginCreateEnvironment); if (dartOnlyPlugin) { await plugin.convertDefaultPluginToDartPlugin(); } section('Test plugin'); - await plugin.test(); + if (runFlutterTest) { + await plugin.test(); + } section('Create Flutter app'); final _FlutterProject app = await _FlutterProject.create(tempDir, options, buildTarget, name: 'plugintestapp', template: 'app', environment: appCreateEnvironment); @@ -63,8 +71,10 @@ class PluginTest { await app.addPlugin('path_provider'); section('Build app'); await app.build(buildTarget, validateNativeBuildProject: !dartOnlyPlugin); - section('Test app'); - await app.test(); + if (runFlutterTest) { + section('Test app'); + await app.test(); + } } finally { await plugin.delete(); await app.delete(); diff --git a/dev/integration_tests/flutter_gallery/windows/flutter/generated_plugins.cmake b/dev/integration_tests/flutter_gallery/windows/flutter/generated_plugins.cmake index 411af46dd7212..88b22e5c775e5 100644 --- a/dev/integration_tests/flutter_gallery/windows/flutter/generated_plugins.cmake +++ b/dev/integration_tests/flutter_gallery/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_windows ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index aaaa8e6bed4a0..744e89c9cffd7 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle @@ -38,6 +38,12 @@ class FlutterExtension { /** Sets the targetSdkVersion used by default in Flutter app projects. */ static int targetSdkVersion = 31 + /** + * Sets the ndkVersion used by default in Flutter app projects. + * Chosen as default version of the AGP version below. + */ + static String ndkVersion = "21.1.6352462" + /** * Specifies the relative directory to the Flutter project directory. * In an app project, this is ../.. since the app's build.gradle is under android/app. @@ -54,6 +60,7 @@ buildscript { mavenCentral() } dependencies { + /* When bumping, also update ndkVersion above. */ classpath 'com.android.tools.build:gradle:4.1.0' } } @@ -409,11 +416,45 @@ class FlutterPlugin implements Plugin { } } - /** Prints error message and fix for any plugin compileSdkVersion that are higher than the project. */ - private void detectLowCompileSdkVersion() { + /** + * Compares semantic versions ignoring labels. + * + * If the versions are equal (ignoring labels), returns one of the two strings arbitrarily. + * + * If minor or patch are omitted (non-conformant to semantic versioning), they are considered zero. + * If the provided versions in both are equal, the longest version string is returned. + * For example, "2.8.0" vs "2.8" will always consider "2.8.0" to be the most recent version. + */ + static String mostRecentSemanticVersion(String version1, String version2) { + List version1Tokenized = version1.tokenize('.') + List version2Tokenized = version2.tokenize('.') + def version1numTokens = version1Tokenized.size() + def version2numTokens = version2Tokenized.size() + def minNumTokens = Math.min(version1numTokens, version2numTokens) + for (int i = 0; i < minNumTokens; i++) { + def num1 = version1Tokenized[i].toInteger() + def num2 = version2Tokenized[i].toInteger() + if (num1 > num2) { + return version1 + } + if (num2 > num1) { + return version2 + } + } + if (version1numTokens > version2numTokens) { + return version1 + } + return version2 + } + + /** Prints error message and fix for any plugin compileSdkVersion or ndkVersion that are higher than the project. */ + private void detectLowCompileSdkVersionOrNdkVersion() { project.afterEvaluate { int projectCompileSdkVersion = project.android.compileSdkVersion.substring(8) as int int maxPluginCompileSdkVersion = projectCompileSdkVersion + String ndkVersionIfUnspecified = "21.1.6352462" /* The default for AGP 4.1.0 used in old templates. */ + String projectNdkVersion = project.android.ndkVersion ?: ndkVersionIfUnspecified + String maxPluginNdkVersion = projectNdkVersion int numProcessedPlugins = getPluginList().size() getPluginList().each { plugin -> @@ -421,12 +462,17 @@ class FlutterPlugin implements Plugin { pluginProject.afterEvaluate { int pluginCompileSdkVersion = pluginProject.android.compileSdkVersion.substring(8) as int maxPluginCompileSdkVersion = Math.max(pluginCompileSdkVersion, maxPluginCompileSdkVersion) + String pluginNdkVersion = pluginProject.android.ndkVersion ?: ndkVersionIfUnspecified + maxPluginNdkVersion = mostRecentSemanticVersion(pluginNdkVersion, maxPluginNdkVersion) numProcessedPlugins-- if (numProcessedPlugins == 0) { if (maxPluginCompileSdkVersion > projectCompileSdkVersion) { project.logger.error("One or more plugins require a higher Android SDK version.\nFix this issue by adding the following to ${project.projectDir}${File.separator}build.gradle:\nandroid {\n compileSdkVersion ${maxPluginCompileSdkVersion}\n ...\n}\n") } + if (maxPluginNdkVersion != projectNdkVersion) { + project.logger.error("One or more plugins require a higher Android NDK version.\nFix this issue by adding the following to ${project.projectDir}${File.separator}build.gradle:\nandroid {\n ndkVersion ${maxPluginNdkVersion}\n ...\n}\n") + } } } } @@ -963,7 +1009,7 @@ class FlutterPlugin implements Plugin { } } configurePlugins() - detectLowCompileSdkVersion() + detectLowCompileSdkVersionOrNdkVersion() return } // Flutter host module project (Add-to-app). @@ -1015,7 +1061,7 @@ class FlutterPlugin implements Plugin { } } configurePlugins() - detectLowCompileSdkVersion() + detectLowCompileSdkVersionOrNdkVersion() } } diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index d7f82a710809a..43a25134a5b79 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -48,8 +48,12 @@ class CreateCommand extends CreateBase { flutterProjectTypeToString(FlutterProjectType.package): 'Generate a shareable Flutter project containing modular ' 'Dart code.', flutterProjectTypeToString(FlutterProjectType.plugin): 'Generate a shareable Flutter project containing an API ' - 'in Dart code with a platform-specific implementation for Android, for iOS code, or ' - 'for both.', + 'in Dart code with a platform-specific implementation through method channels for Android, iOS, ' + 'Linux, macOS, Windows, web, or any combination of these.', + flutterProjectTypeToString(FlutterProjectType.ffiPlugin): + 'Generate a shareable Flutter project containing an API ' + 'in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, ' + 'Linux, macOS, Windows, or any combination of these.', flutterProjectTypeToString(FlutterProjectType.module): 'Generate a project to add a Flutter module to an ' 'existing Android or iOS application.', }, @@ -159,18 +163,17 @@ class CreateCommand extends CreateBase { final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync(); if (argResults['template'] != null) { template = stringToProjectType(stringArg('template')); - } else { - // If the project directory exists and isn't empty, then try to determine the template - // type from the project directory. - if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) { - detectedProjectType = determineTemplateType(); - if (detectedProjectType == null && metadataExists) { - // We can only be definitive that this is the wrong type if the .metadata file - // exists and contains a type that we don't understand, or doesn't contain a type. - throwToolExit('Sorry, unable to detect the type of project to recreate. ' - 'Try creating a fresh project and migrating your existing code to ' - 'the new project manually.'); - } + } + // If the project directory exists and isn't empty, then try to determine the template + // type from the project directory. + if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) { + detectedProjectType = determineTemplateType(); + if (detectedProjectType == null && metadataExists) { + // We can only be definitive that this is the wrong type if the .metadata file + // exists and contains a type that we don't understand, or doesn't contain a type. + throwToolExit('Sorry, unable to detect the type of project to recreate. ' + 'Try creating a fresh project and migrating your existing code to ' + 'the new project manually.'); } } template ??= detectedProjectType ?? FlutterProjectType.app; @@ -206,7 +209,8 @@ class CreateCommand extends CreateBase { final FlutterProjectType template = _getProjectType(projectDir); final bool generateModule = template == FlutterProjectType.module; - final bool generatePlugin = template == FlutterProjectType.plugin; + final bool generateMethodChannelsPlugin = template == FlutterProjectType.plugin; + final bool generateFfiPlugin = template == FlutterProjectType.ffiPlugin; final bool generatePackage = template == FlutterProjectType.package; final List platforms = stringsArg('platforms'); @@ -220,6 +224,23 @@ class CreateCommand extends CreateBase { } else if (platforms == null || platforms.isEmpty) { throwToolExit('Must specify at least one platform using --platforms', exitCode: 2); + } else if (generateFfiPlugin && argResults.wasParsed('platforms') && platforms.contains('web')) { + throwToolExit( + 'The web platform is not supported in plugin_ffi template.', + exitCode: 2, + ); + } else if (generateFfiPlugin && argResults.wasParsed('ios-language')) { + throwToolExit( + 'The "ios-language" option is not supported with the plugin_ffi ' + 'template: the language will always be C or C++.', + exitCode: 2, + ); + } else if (generateFfiPlugin && argResults.wasParsed('android-language')) { + throwToolExit( + 'The "android-language" option is not supported with the plugin_ffi ' + 'template: the language will always be C or C++.', + exitCode: 2, + ); } final String organization = await getOrganization(); @@ -257,7 +278,8 @@ class CreateCommand extends CreateBase { titleCaseProjectName: titleCaseProjectName, projectDescription: stringArg('description'), flutterRoot: flutterRoot, - withPluginHook: generatePlugin, + withPlatformChannelPluginHook: generateMethodChannelsPlugin, + withFfiPluginHook: generateFfiPlugin, androidLanguage: stringArg('android-language'), iosLanguage: stringArg('ios-language'), iosDevelopmentTeam: developmentTeam, @@ -293,7 +315,7 @@ class CreateCommand extends CreateBase { switch (template) { case FlutterProjectType.app: generatedFileCount += await generateApp( - 'app', + ['app', 'app_test_widget'], relativeDir, templateContext, overwrite: overwrite, @@ -302,7 +324,7 @@ class CreateCommand extends CreateBase { break; case FlutterProjectType.skeleton: generatedFileCount += await generateApp( - 'skeleton', + ['skeleton'], relativeDir, templateContext, overwrite: overwrite, @@ -326,7 +348,15 @@ class CreateCommand extends CreateBase { ); break; case FlutterProjectType.plugin: - generatedFileCount += await _generatePlugin( + generatedFileCount += await _generateMethodChannelPlugin( + relativeDir, + templateContext, + overwrite: overwrite, + printStatusWhenWriting: !creatingNewProject, + ); + break; + case FlutterProjectType.ffiPlugin: + generatedFileCount += await _generateFfiPlugin( relativeDir, templateContext, overwrite: overwrite, @@ -354,7 +384,7 @@ class CreateCommand extends CreateBase { 'main.dart', )); globals.printStatus('Your module code is in $relativeMainPath.'); - } else if (generatePlugin) { + } else if (generateMethodChannelsPlugin) { final String relativePluginPath = globals.fs.path.normalize(globals.fs.path.relative(projectDirPath)); final List requestedPlatforms = _getUserRequestedPlatforms(); final String platformsString = requestedPlatforms.join(', '); @@ -460,7 +490,7 @@ Your $application code is in $relativeAppMain. return generatedCount; } - Future _generatePlugin( + Future _generateMethodChannelPlugin( Directory directory, Map templateContext, { bool overwrite = false, @@ -487,8 +517,8 @@ Your $application code is in $relativeAppMain. ? stringArg('description') : 'A new flutter plugin project.'; templateContext['description'] = description; - generatedCount += await renderTemplate( - 'plugin', + generatedCount += await renderMerged( + ['plugin', 'plugin_shared'], directory, templateContext, overwrite: overwrite, @@ -525,7 +555,83 @@ Your $application code is in $relativeAppMain. templateContext['androidPluginIdentifier'] = androidPluginIdentifier; generatedCount += await generateApp( - 'app', + ['app', 'app_test_widget'], + project.example.directory, + templateContext, + overwrite: overwrite, + pluginExampleApp: true, + printStatusWhenWriting: printStatusWhenWriting, + ); + return generatedCount; + } + + Future _generateFfiPlugin( + Directory directory, + Map templateContext, { + bool overwrite = false, + bool printStatusWhenWriting = true, + }) async { + // Plugins only add a platform if it was requested explicitly by the user. + if (!argResults.wasParsed('platforms')) { + for (final String platform in kAllCreatePlatforms) { + templateContext[platform] = false; + } + } + final List platformsToAdd = + _getSupportedPlatformsFromTemplateContext(templateContext); + + final List existingPlatforms = + _getSupportedPlatformsInPlugin(directory); + for (final String existingPlatform in existingPlatforms) { + // re-generate files for existing platforms + templateContext[existingPlatform] = true; + } + + final bool willAddPlatforms = platformsToAdd.isNotEmpty; + templateContext['no_platforms'] = !willAddPlatforms; + int generatedCount = 0; + final String description = argResults.wasParsed('description') + ? stringArg('description') + : 'A new Flutter FFI plugin project.'; + templateContext['description'] = description; + generatedCount += await renderMerged( + ['plugin_ffi', 'plugin_shared'], + directory, + templateContext, + overwrite: overwrite, + printStatusWhenWriting: printStatusWhenWriting, + ); + + if (boolArg('pub')) { + await pub.get( + context: PubContext.createPlugin, + directory: directory.path, + offline: boolArg('offline'), + generateSyntheticPackage: false, + ); + } + + final FlutterProject project = FlutterProject.fromDirectory(directory); + final bool generateAndroid = templateContext['android'] == true; + if (generateAndroid) { + gradle.updateLocalProperties(project: project, requireAndroidSdk: false); + } + + final String projectName = templateContext['projectName'] as String; + final String organization = templateContext['organization'] as String; + final String androidPluginIdentifier = templateContext['androidIdentifier'] as String; + final String exampleProjectName = '${projectName}_example'; + templateContext['projectName'] = exampleProjectName; + templateContext['androidIdentifier'] = CreateBase.createAndroidIdentifier(organization, exampleProjectName); + templateContext['iosIdentifier'] = CreateBase.createUTIIdentifier(organization, exampleProjectName); + templateContext['macosIdentifier'] = CreateBase.createUTIIdentifier(organization, exampleProjectName); + templateContext['windowsIdentifier'] = CreateBase.createWindowsIdentifier(organization, exampleProjectName); + templateContext['description'] = 'Demonstrates how to use the $projectName plugin.'; + templateContext['pluginProjectName'] = projectName; + templateContext['androidPluginIdentifier'] = androidPluginIdentifier; + + generatedCount += await generateApp( + ['app'], project.example.directory, templateContext, overwrite: overwrite, diff --git a/packages/flutter_tools/lib/src/commands/create_base.dart b/packages/flutter_tools/lib/src/commands/create_base.dart index 7ea49d03788fa..25e490fb528ca 100644 --- a/packages/flutter_tools/lib/src/commands/create_base.dart +++ b/packages/flutter_tools/lib/src/commands/create_base.dart @@ -5,6 +5,7 @@ // @dart = 2.8 import 'package:meta/meta.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:uuid/uuid.dart'; import '../android/android.dart' as android_common; @@ -110,7 +111,7 @@ abstract class CreateBase extends FlutterCommand { abbr: 'i', defaultsTo: 'swift', allowed: ['objc', 'swift'], - help: 'The language to use for iOS-specific code, either ObjectiveC (legacy) or Swift (recommended).' + help: 'The language to use for iOS-specific code, either Objective-C (legacy) or Swift (recommended).' ); argParser.addOption( 'android-language', @@ -339,7 +340,8 @@ abstract class CreateBase extends FlutterCommand { String agpVersion, String kotlinVersion, String gradleVersion, - bool withPluginHook = false, + bool withPlatformChannelPluginHook = false, + bool withFfiPluginHook = false, bool ios = false, bool android = false, bool web = false, @@ -366,6 +368,12 @@ abstract class CreateBase extends FlutterCommand { // https://developer.gnome.org/gio/stable/GApplication.html#g-application-id-is-valid final String linuxIdentifier = androidIdentifier; + // TODO(dacoharkes): Replace with hardcoded version in template when Flutter 2.11 is released. + final Version ffiPluginStableRelease = Version(2, 11, 0); + final String minFrameworkVersionFfiPlugin = Version.parse(globals.flutterVersion.frameworkVersion) < ffiPluginStableRelease + ? globals.flutterVersion.frameworkVersion + : ffiPluginStableRelease.toString(); + return { 'organization': organization, 'projectName': projectName, @@ -384,13 +392,16 @@ abstract class CreateBase extends FlutterCommand { 'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase, 'pluginDartClass': pluginDartClass, 'pluginProjectUUID': const Uuid().v4().toUpperCase(), - 'withPluginHook': withPluginHook, + 'withFfiPluginHook': withFfiPluginHook, + 'withPlatformChannelPluginHook': withPlatformChannelPluginHook, + 'withPluginHook': withFfiPluginHook || withPlatformChannelPluginHook, 'androidLanguage': androidLanguage, 'iosLanguage': iosLanguage, 'hasIosDevelopmentTeam': iosDevelopmentTeam != null && iosDevelopmentTeam.isNotEmpty, 'iosDevelopmentTeam': iosDevelopmentTeam ?? '', 'flutterRevision': globals.flutterVersion.frameworkRevision, 'flutterChannel': globals.flutterVersion.channel, + 'minFrameworkVersionFfiPlugin': minFrameworkVersionFfiPlugin, 'ios': ios, 'android': android, 'web': web, @@ -468,7 +479,7 @@ abstract class CreateBase extends FlutterCommand { /// If `overwrite` is true, overwrites existing files, `overwrite` defaults to `false`. @protected Future generateApp( - String templateName, + List templateNames, Directory directory, Map templateContext, { bool overwrite = false, @@ -477,7 +488,7 @@ abstract class CreateBase extends FlutterCommand { }) async { int generatedCount = 0; generatedCount += await renderMerged( - [templateName, 'app_shared'], + [...templateNames, 'app_shared'], directory, templateContext, overwrite: overwrite, diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart index f948feb13aac5..af965ede48141 100644 --- a/packages/flutter_tools/lib/src/flutter_plugins.dart +++ b/packages/flutter_tools/lib/src/flutter_plugins.dart @@ -120,7 +120,7 @@ List> _filterPluginsByPlatform(List plugins, String _kFlutterPluginsNameKey: plugin.name, _kFlutterPluginsPathKey: globals.fsUtils.escapePath(plugin.path), if (platformPlugin is NativeOrDartPlugin) - _kFlutterPluginsHasNativeBuildKey: (platformPlugin as NativeOrDartPlugin).isNative(), + _kFlutterPluginsHasNativeBuildKey: (platformPlugin as NativeOrDartPlugin).hasMethodChannel() || (platformPlugin as NativeOrDartPlugin).hasFfi(), _kFlutterPluginsDependenciesKey: [...plugin.dependencies.where(pluginNames.contains)], }); } @@ -270,9 +270,9 @@ const String _androidPluginRegistryTemplateOldEmbedding = ''' package io.flutter.plugins; import io.flutter.plugin.common.PluginRegistry; -{{#plugins}} +{{#methodChannelPlugins}} import {{package}}.{{class}}; -{{/plugins}} +{{/methodChannelPlugins}} /** * Generated file. Do not edit. @@ -282,9 +282,9 @@ public final class GeneratedPluginRegistrant { if (alreadyRegisteredWith(registry)) { return; } -{{#plugins}} +{{#methodChannelPlugins}} {{class}}.registerWith(registry.registrarFor("{{package}}.{{class}}")); -{{/plugins}} +{{/methodChannelPlugins}} } private static boolean alreadyRegisteredWith(PluginRegistry registry) { @@ -322,7 +322,7 @@ public final class GeneratedPluginRegistrant { {{#needsShim}} ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine); {{/needsShim}} -{{#plugins}} +{{#methodChannelPlugins}} {{#supportsEmbeddingV2}} try { flutterEngine.getPlugins().add(new {{package}}.{{class}}()); @@ -339,7 +339,7 @@ public final class GeneratedPluginRegistrant { } {{/supportsEmbeddingV1}} {{/supportsEmbeddingV2}} -{{/plugins}} +{{/methodChannelPlugins}} } } '''; @@ -363,12 +363,11 @@ AndroidEmbeddingVersion _getAndroidEmbeddingVersion(FlutterProject project) { } Future _writeAndroidPluginRegistrant(FlutterProject project, List plugins) async { - final List nativePlugins = _filterNativePlugins(plugins, AndroidPlugin.kConfigKey); - final List> androidPlugins = - _extractPlatformMaps(nativePlugins, AndroidPlugin.kConfigKey); + final List methodChannelPlugins = _filterMethodChannelPlugins(plugins, AndroidPlugin.kConfigKey); + final List> androidPlugins = _extractPlatformMaps(methodChannelPlugins, AndroidPlugin.kConfigKey); final Map templateContext = { - 'plugins': androidPlugins, + 'methodChannelPlugins': androidPlugins, 'androidX': isAppUsingAndroidX(project.android.hostAppGradleRoot), }; final String javaSourcePath = globals.fs.path.join( @@ -485,20 +484,20 @@ const String _objcPluginRegistryImplementationTemplate = ''' #import "GeneratedPluginRegistrant.h" -{{#plugins}} +{{#methodChannelPlugins}} #if __has_include(<{{name}}/{{class}}.h>) #import <{{name}}/{{class}}.h> #else @import {{name}}; #endif -{{/plugins}} +{{/methodChannelPlugins}} @implementation GeneratedPluginRegistrant + (void)registerWithRegistry:(NSObject*)registry { -{{#plugins}} +{{#methodChannelPlugins}} [{{prefix}}{{class}} registerWithRegistrar:[registry registrarForPlugin:@"{{prefix}}{{class}}"]]; -{{/plugins}} +{{/methodChannelPlugins}} } @end @@ -512,14 +511,14 @@ const String _swiftPluginRegistryTemplate = ''' import {{framework}} import Foundation -{{#plugins}} +{{#methodChannelPlugins}} import {{name}} -{{/plugins}} +{{/methodChannelPlugins}} func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - {{#plugins}} + {{#methodChannelPlugins}} {{class}}.register(with: registry.registrar(forPlugin: "{{class}}")) -{{/plugins}} +{{/methodChannelPlugins}} } '''; @@ -545,9 +544,9 @@ Depends on all your plugins, and provides a function to register them. s.static_framework = true s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.dependency '{{framework}}' - {{#plugins}} + {{#methodChannelPlugins}} s.dependency '{{name}}' - {{/plugins}} + {{/methodChannelPlugins}} end '''; @@ -559,17 +558,17 @@ const String _dartPluginRegistryTemplate = ''' // ignore_for_file: directives_ordering // ignore_for_file: lines_longer_than_80_chars -{{#plugins}} +{{#methodChannelPlugins}} import 'package:{{name}}/{{file}}'; -{{/plugins}} +{{/methodChannelPlugins}} import 'package:flutter_web_plugins/flutter_web_plugins.dart'; // ignore: public_member_api_docs void registerPlugins(Registrar registrar) { -{{#plugins}} +{{#methodChannelPlugins}} {{class}}.registerWith(registrar); -{{/plugins}} +{{/methodChannelPlugins}} registrar.registerMessageHandler(); } '''; @@ -601,15 +600,15 @@ const String _cppPluginRegistryImplementationTemplate = ''' #include "generated_plugin_registrant.h" -{{#plugins}} +{{#methodChannelPlugins}} #include <{{name}}/{{filename}}.h> -{{/plugins}} +{{/methodChannelPlugins}} void RegisterPlugins(flutter::PluginRegistry* registry) { -{{#plugins}} +{{#methodChannelPlugins}} {{class}}RegisterWithRegistrar( registry->GetRegistrarForPlugin("{{class}}")); -{{/plugins}} +{{/methodChannelPlugins}} } '''; @@ -640,16 +639,16 @@ const String _linuxPluginRegistryImplementationTemplate = ''' #include "generated_plugin_registrant.h" -{{#plugins}} +{{#methodChannelPlugins}} #include <{{name}}/{{filename}}.h> -{{/plugins}} +{{/methodChannelPlugins}} void fl_register_plugins(FlPluginRegistry* registry) { -{{#plugins}} +{{#methodChannelPlugins}} g_autoptr(FlPluginRegistrar) {{name}}_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "{{class}}"); {{filename}}_register_with_registrar({{name}}_registrar); -{{/plugins}} +{{/methodChannelPlugins}} } '''; @@ -659,9 +658,15 @@ const String _pluginCmakefileTemplate = r''' # list(APPEND FLUTTER_PLUGIN_LIST -{{#plugins}} +{{#methodChannelPlugins}} {{name}} -{{/plugins}} +{{/methodChannelPlugins}} +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +{{#ffiPlugins}} + {{name}} +{{/ffiPlugins}} ) set(PLUGIN_BUNDLED_LIBRARIES) @@ -672,6 +677,11 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory({{pluginsDir}}/${ffi_plugin}/{{os}} plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) '''; const String _dartPluginRegisterWith = r''' @@ -760,13 +770,13 @@ void main(List args) { '''; Future _writeIOSPluginRegistrant(FlutterProject project, List plugins) async { - final List nativePlugins = _filterNativePlugins(plugins, IOSPlugin.kConfigKey); - final List> iosPlugins = _extractPlatformMaps(nativePlugins, IOSPlugin.kConfigKey); + final List methodChannelPlugins = _filterMethodChannelPlugins(plugins, IOSPlugin.kConfigKey); + final List> iosPlugins = _extractPlatformMaps(methodChannelPlugins, IOSPlugin.kConfigKey); final Map context = { 'os': 'ios', 'deploymentTarget': '9.0', 'framework': 'Flutter', - 'plugins': iosPlugins, + 'methodChannelPlugins': iosPlugins, }; if (project.isModule) { final Directory registryDirectory = project.ios.pluginRegistrantHost; @@ -810,11 +820,14 @@ String _cmakeRelativePluginSymlinkDirectoryPath(CmakeBasedProject project) { } Future _writeLinuxPluginFiles(FlutterProject project, List plugins) async { - final ListnativePlugins = _filterNativePlugins(plugins, LinuxPlugin.kConfigKey); - final List> linuxPlugins = _extractPlatformMaps(nativePlugins, LinuxPlugin.kConfigKey); + final List methodChannelPlugins = _filterMethodChannelPlugins(plugins, LinuxPlugin.kConfigKey); + final List> linuxMethodChannelPlugins = _extractPlatformMaps(methodChannelPlugins, LinuxPlugin.kConfigKey); + final List ffiPlugins = _filterFfiPlugins(plugins, LinuxPlugin.kConfigKey)..removeWhere(methodChannelPlugins.contains); + final List> linuxFfiPlugins = _extractPlatformMaps(ffiPlugins, LinuxPlugin.kConfigKey); final Map context = { 'os': 'linux', - 'plugins': linuxPlugins, + 'methodChannelPlugins': linuxMethodChannelPlugins, + 'ffiPlugins': linuxFfiPlugins, 'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.linux), }; await _writeLinuxPluginRegistrant(project.linux.managedDirectory, context); @@ -846,12 +859,12 @@ Future _writePluginCmakefile(File destinationFile, Map tem } Future _writeMacOSPluginRegistrant(FlutterProject project, List plugins) async { - final ListnativePlugins = _filterNativePlugins(plugins, MacOSPlugin.kConfigKey); - final List> macosPlugins = _extractPlatformMaps(nativePlugins, MacOSPlugin.kConfigKey); + final List methodChannelPlugins = _filterMethodChannelPlugins(plugins, MacOSPlugin.kConfigKey); + final List> macosMethodChannelPlugins = _extractPlatformMaps(methodChannelPlugins, MacOSPlugin.kConfigKey); final Map context = { 'os': 'macos', 'framework': 'FlutterMacOS', - 'plugins': macosPlugins, + 'methodChannelPlugins': macosMethodChannelPlugins, }; _renderTemplateToFile( _swiftPluginRegistryTemplate, @@ -861,15 +874,15 @@ Future _writeMacOSPluginRegistrant(FlutterProject project, List pl ); } -/// Filters out Dart-only plugins, which shouldn't be added to the native generated registrants. -List _filterNativePlugins(List plugins, String platformKey) { +/// Filters out any plugins that don't use method channels, and thus shouldn't be added to the native generated registrants. +List _filterMethodChannelPlugins(List plugins, String platformKey) { return plugins.where((Plugin element) { final PluginPlatform? plugin = element.platforms[platformKey]; if (plugin == null) { return false; } if (plugin is NativeOrDartPlugin) { - return (plugin as NativeOrDartPlugin).isNative(); + return (plugin as NativeOrDartPlugin).hasMethodChannel(); } // Not all platforms have the ability to create Dart-only plugins. Therefore, any plugin that doesn't // implement NativeOrDartPlugin is always native. @@ -877,6 +890,23 @@ List _filterNativePlugins(List plugins, String platformKey) { }).toList(); } +/// Filters out Dart-only and method channel plugins. +/// +/// FFI plugins do not need native code registration, but their binaries need to be bundled. +List _filterFfiPlugins(List plugins, String platformKey) { + return plugins.where((Plugin element) { + final PluginPlatform? plugin = element.platforms[platformKey]; + if (plugin == null) { + return false; + } + if (plugin is NativeOrDartPlugin) { + final NativeOrDartPlugin plugin_ = plugin as NativeOrDartPlugin; + return plugin_.hasFfi(); + } + return false; + }).toList(); +} + /// Returns only the plugins with the given platform variant. List _filterPluginsByVariant(List plugins, String platformKey, PluginPlatformVariant variant) { return plugins.where((Plugin element) { @@ -892,12 +922,15 @@ List _filterPluginsByVariant(List plugins, String platformKey, P @visibleForTesting Future writeWindowsPluginFiles(FlutterProject project, List plugins, TemplateRenderer templateRenderer) async { - final List nativePlugins = _filterNativePlugins(plugins, WindowsPlugin.kConfigKey); - final List win32Plugins = _filterPluginsByVariant(nativePlugins, WindowsPlugin.kConfigKey, PluginPlatformVariant.win32); - final List> pluginInfo = _extractPlatformMaps(win32Plugins, WindowsPlugin.kConfigKey); + final List methodChannelPlugins = _filterMethodChannelPlugins(plugins, WindowsPlugin.kConfigKey); + final List win32Plugins = _filterPluginsByVariant(methodChannelPlugins, WindowsPlugin.kConfigKey, PluginPlatformVariant.win32); + final List> windowsMethodChannelPlugins = _extractPlatformMaps(win32Plugins, WindowsPlugin.kConfigKey); + final List ffiPlugins = _filterFfiPlugins(plugins, WindowsPlugin.kConfigKey)..removeWhere(methodChannelPlugins.contains); + final List> windowsFfiPlugins = _extractPlatformMaps(ffiPlugins, WindowsPlugin.kConfigKey); final Map context = { 'os': 'windows', - 'plugins': pluginInfo, + 'methodChannelPlugins': windowsMethodChannelPlugins, + 'ffiPlugins': windowsFfiPlugins, 'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.windows), }; await _writeCppPluginRegistrant(project.windows.managedDirectory, context, templateRenderer); @@ -908,12 +941,15 @@ Future writeWindowsPluginFiles(FlutterProject project, List plugin /// filtering, for the purposes of tooling support and initial UWP bootstrap. @visibleForTesting Future writeWindowsUwpPluginFiles(FlutterProject project, List plugins, TemplateRenderer templateRenderer) async { - final List nativePlugins = _filterNativePlugins(plugins, WindowsPlugin.kConfigKey); - final List uwpPlugins = _filterPluginsByVariant(nativePlugins, WindowsPlugin.kConfigKey, PluginPlatformVariant.winuwp); - final List> pluginInfo = _extractPlatformMaps(uwpPlugins, WindowsPlugin.kConfigKey); + final List methodChannelPlugins = _filterMethodChannelPlugins(plugins, WindowsPlugin.kConfigKey); + final List uwpPlugins = _filterPluginsByVariant(methodChannelPlugins, WindowsPlugin.kConfigKey, PluginPlatformVariant.winuwp); + final List> windowsMethodChannelPlugins = _extractPlatformMaps(uwpPlugins, WindowsPlugin.kConfigKey); + final List ffiPlugins = _filterFfiPlugins(plugins, WindowsPlugin.kConfigKey)..removeWhere(methodChannelPlugins.contains); + final List> windowsFfiPlugins = _extractPlatformMaps(ffiPlugins, WindowsPlugin.kConfigKey); final Map context = { 'os': 'windows', - 'plugins': pluginInfo, + 'methodChannelPlugins': windowsMethodChannelPlugins, + 'ffiPlugins': windowsFfiPlugins, 'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.windowsUwp), }; await _writeCppPluginRegistrant(project.windowsUwp.managedDirectory, context, templateRenderer); @@ -938,7 +974,7 @@ Future _writeCppPluginRegistrant(Directory destination, Map _writeWebPluginRegistrant(FlutterProject project, List plugins) async { final List> webPlugins = _extractPlatformMaps(plugins, WebPlugin.kConfigKey); final Map context = { - 'plugins': webPlugins, + 'methodChannelPlugins': webPlugins, }; final File pluginFile = project.web.libDirectory.childFile('generated_plugin_registrant.dart'); if (webPlugins.isEmpty) { diff --git a/packages/flutter_tools/lib/src/flutter_project_metadata.dart b/packages/flutter_tools/lib/src/flutter_project_metadata.dart index b23fdc7f2aee7..da2800fbf0111 100644 --- a/packages/flutter_tools/lib/src/flutter_project_metadata.dart +++ b/packages/flutter_tools/lib/src/flutter_project_metadata.dart @@ -23,9 +23,14 @@ enum FlutterProjectType { package, /// This is a native plugin project. plugin, + /// This is an FFI native plugin project. + ffiPlugin, } String flutterProjectTypeToString(FlutterProjectType type) { + if (type == FlutterProjectType.ffiPlugin) { + return 'plugin_ffi'; + } return getEnumName(type); } diff --git a/packages/flutter_tools/lib/src/platform_plugins.dart b/packages/flutter_tools/lib/src/platform_plugins.dart index 1de3576bbf6a9..6aef966640245 100644 --- a/packages/flutter_tools/lib/src/platform_plugins.dart +++ b/packages/flutter_tools/lib/src/platform_plugins.dart @@ -10,9 +10,12 @@ import 'base/file_system.dart'; /// Constant for 'pluginClass' key in plugin maps. const String kPluginClass = 'pluginClass'; -/// Constant for 'pluginClass' key in plugin maps. +/// Constant for 'dartPluginClass' key in plugin maps. const String kDartPluginClass = 'dartPluginClass'; +/// Constant for 'ffiPlugin' key in plugin maps. +const String kFfiPlugin = 'ffiPlugin'; + // Constant for 'defaultPackage' key in plugin maps. const String kDefaultPackage = 'default_package'; @@ -42,9 +45,14 @@ abstract class VariantPlatformPlugin { } abstract class NativeOrDartPlugin { - /// Determines whether the plugin has a native implementation or if it's a - /// Dart-only plugin. - bool isNative(); + /// Determines whether the plugin has a Dart implementation. + bool hasDart(); + + /// Determines whether the plugin has a FFI implementation. + bool hasFfi(); + + /// Determines whether the plugin has a method channel implementation. + bool hasMethodChannel(); } /// Contains parameters to template an Android plugin. @@ -64,9 +72,11 @@ class AndroidPlugin extends PluginPlatform implements NativeOrDartPlugin { this.package, this.pluginClass, this.dartPluginClass, + bool? ffiPlugin, this.defaultPackage, required FileSystem fileSystem, - }) : _fileSystem = fileSystem; + }) : _fileSystem = fileSystem, + ffiPlugin = ffiPlugin ?? false; factory AndroidPlugin.fromYaml(String name, YamlMap yaml, String pluginPath, FileSystem fileSystem) { assert(validate(yaml)); @@ -75,6 +85,7 @@ class AndroidPlugin extends PluginPlatform implements NativeOrDartPlugin { package: yaml['package'] as String?, pluginClass: yaml[kPluginClass] as String?, dartPluginClass: yaml[kDartPluginClass] as String?, + ffiPlugin: yaml[kFfiPlugin] as bool?, defaultPackage: yaml[kDefaultPackage] as String?, pluginPath: pluginPath, fileSystem: fileSystem, @@ -84,15 +95,22 @@ class AndroidPlugin extends PluginPlatform implements NativeOrDartPlugin { final FileSystem _fileSystem; @override - bool isNative() => pluginClass != null; + bool hasMethodChannel() => pluginClass != null; + + @override + bool hasFfi() => ffiPlugin; + + @override + bool hasDart() => dartPluginClass != null; static bool validate(YamlMap yaml) { if (yaml == null) { return false; } - return (yaml['package'] is String && yaml['pluginClass'] is String)|| - yaml[kDartPluginClass] is String || - yaml[kDefaultPackage] is String; + return (yaml['package'] is String && yaml[kPluginClass] is String) || + yaml[kDartPluginClass] is String || + yaml[kFfiPlugin] == true || + yaml[kDefaultPackage] is String; } static const String kConfigKey = 'android'; @@ -109,6 +127,9 @@ class AndroidPlugin extends PluginPlatform implements NativeOrDartPlugin { /// The Dart plugin main class defined in pubspec.yaml, if any. final String? dartPluginClass; + /// Is FFI plugin defined in pubspec.yaml. + final bool ffiPlugin; + /// The default implementation package defined in pubspec.yaml, if any. final String? defaultPackage; @@ -122,6 +143,7 @@ class AndroidPlugin extends PluginPlatform implements NativeOrDartPlugin { if (package != null) 'package': package, if (pluginClass != null) 'class': pluginClass, if (dartPluginClass != null) kDartPluginClass : dartPluginClass, + if (ffiPlugin) kFfiPlugin: true, if (defaultPackage != null) kDefaultPackage : defaultPackage, // Mustache doesn't support complex types. 'supportsEmbeddingV1': _supportedEmbeddings.contains('1'), @@ -214,8 +236,9 @@ class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin { required this.classPrefix, this.pluginClass, this.dartPluginClass, + bool? ffiPlugin, this.defaultPackage, - }); + }) : ffiPlugin = ffiPlugin ?? false; factory IOSPlugin.fromYaml(String name, YamlMap yaml) { assert(validate(yaml)); // TODO(zanderso): https://github.com/flutter/flutter/issues/67241 @@ -224,6 +247,7 @@ class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin { classPrefix: '', pluginClass: yaml[kPluginClass] as String?, dartPluginClass: yaml[kDartPluginClass] as String?, + ffiPlugin: yaml[kFfiPlugin] as bool?, defaultPackage: yaml[kDefaultPackage] as String?, ); } @@ -233,8 +257,9 @@ class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin { return false; } return yaml[kPluginClass] is String || - yaml[kDartPluginClass] is String || - yaml[kDefaultPackage] is String; + yaml[kDartPluginClass] is String || + yaml[kFfiPlugin] == true || + yaml[kDefaultPackage] is String; } static const String kConfigKey = 'ios'; @@ -246,10 +271,17 @@ class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin { final String classPrefix; final String? pluginClass; final String? dartPluginClass; + final bool ffiPlugin; final String? defaultPackage; @override - bool isNative() => pluginClass != null; + bool hasMethodChannel() => pluginClass != null; + + @override + bool hasFfi() => ffiPlugin; + + @override + bool hasDart() => dartPluginClass != null; @override Map toMap() { @@ -258,6 +290,7 @@ class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin { 'prefix': classPrefix, if (pluginClass != null) 'class': pluginClass, if (dartPluginClass != null) kDartPluginClass : dartPluginClass, + if (ffiPlugin) kFfiPlugin: true, if (defaultPackage != null) kDefaultPackage : defaultPackage, }; } @@ -265,15 +298,17 @@ class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin { /// Contains the parameters to template a macOS plugin. /// -/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required. +/// The [name] of the plugin is required. Either [dartPluginClass] or +/// [pluginClass] or [ffiPlugin] are required. /// [pluginClass] will be the entry point to the plugin's native code. class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin { const MacOSPlugin({ required this.name, this.pluginClass, this.dartPluginClass, + bool? ffiPlugin, this.defaultPackage, - }); + }) : ffiPlugin = ffiPlugin ?? false; factory MacOSPlugin.fromYaml(String name, YamlMap yaml) { assert(validate(yaml)); @@ -286,6 +321,7 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin { name: name, pluginClass: pluginClass, dartPluginClass: yaml[kDartPluginClass] as String?, + ffiPlugin: yaml[kFfiPlugin] as bool?, defaultPackage: yaml[kDefaultPackage] as String?, ); } @@ -295,8 +331,9 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin { return false; } return yaml[kPluginClass] is String || - yaml[kDartPluginClass] is String || - yaml[kDefaultPackage] is String; + yaml[kDartPluginClass] is String || + yaml[kFfiPlugin] == true || + yaml[kDefaultPackage] is String; } static const String kConfigKey = 'macos'; @@ -304,18 +341,26 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin { final String name; final String? pluginClass; final String? dartPluginClass; + final bool ffiPlugin; final String? defaultPackage; @override - bool isNative() => pluginClass != null; + bool hasMethodChannel() => pluginClass != null; + + @override + bool hasFfi() => ffiPlugin; + + @override + bool hasDart() => dartPluginClass != null; @override Map toMap() { return { 'name': name, if (pluginClass != null) 'class': pluginClass, - if (dartPluginClass != null) kDartPluginClass : dartPluginClass, - if (defaultPackage != null) kDefaultPackage : defaultPackage, + if (dartPluginClass != null) kDartPluginClass: dartPluginClass, + if (ffiPlugin) kFfiPlugin: true, + if (defaultPackage != null) kDefaultPackage: defaultPackage, }; } } @@ -324,14 +369,17 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin { /// /// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required. /// [pluginClass] will be the entry point to the plugin's native code. -class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, VariantPlatformPlugin { +class WindowsPlugin extends PluginPlatform + implements NativeOrDartPlugin, VariantPlatformPlugin { const WindowsPlugin({ required this.name, this.pluginClass, this.dartPluginClass, + bool? ffiPlugin, this.defaultPackage, this.variants = const {}, - }) : assert(pluginClass != null || dartPluginClass != null || defaultPackage != null); + }) : ffiPlugin = ffiPlugin ?? false, + assert(pluginClass != null || dartPluginClass != null || defaultPackage != null); factory WindowsPlugin.fromYaml(String name, YamlMap yaml) { assert(validate(yaml)); @@ -363,6 +411,7 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, Varian name: name, pluginClass: pluginClass, dartPluginClass: yaml[kDartPluginClass] as String?, + ffiPlugin: yaml[kFfiPlugin] as bool?, defaultPackage: yaml[kDefaultPackage] as String?, variants: variants, ); @@ -374,8 +423,9 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, Varian } return yaml[kPluginClass] is String || - yaml[kDartPluginClass] is String || - yaml[kDefaultPackage] is String; + yaml[kDartPluginClass] is String || + yaml[kFfiPlugin] == true || + yaml[kDefaultPackage] is String; } static const String kConfigKey = 'windows'; @@ -383,6 +433,7 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, Varian final String name; final String? pluginClass; final String? dartPluginClass; + final bool ffiPlugin; final String? defaultPackage; final Set variants; @@ -390,7 +441,13 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, Varian Set get supportedVariants => variants; @override - bool isNative() => pluginClass != null; + bool hasMethodChannel() => pluginClass != null; + + @override + bool hasFfi() => ffiPlugin; + + @override + bool hasDart() => dartPluginClass != null; @override Map toMap() { @@ -399,6 +456,7 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, Varian if (pluginClass != null) 'class': pluginClass!, if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!), if (dartPluginClass != null) kDartPluginClass: dartPluginClass!, + if (ffiPlugin) kFfiPlugin: true, if (defaultPackage != null) kDefaultPackage: defaultPackage!, }; } @@ -413,8 +471,10 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin { required this.name, this.pluginClass, this.dartPluginClass, + bool? ffiPlugin, this.defaultPackage, - }) : assert(pluginClass != null || dartPluginClass != null || defaultPackage != null); + }) : ffiPlugin = ffiPlugin ?? false, + assert(pluginClass != null || dartPluginClass != null || ffiPlugin == true || defaultPackage != null); factory LinuxPlugin.fromYaml(String name, YamlMap yaml) { assert(validate(yaml)); @@ -427,6 +487,7 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin { name: name, pluginClass: pluginClass, dartPluginClass: yaml[kDartPluginClass] as String?, + ffiPlugin: yaml[kFfiPlugin] as bool?, defaultPackage: yaml[kDefaultPackage] as String?, ); } @@ -436,8 +497,9 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin { return false; } return yaml[kPluginClass] is String || - yaml[kDartPluginClass] is String || - yaml[kDefaultPackage] is String; + yaml[kDartPluginClass] is String || + yaml[kFfiPlugin] == true || + yaml[kDefaultPackage] is String; } static const String kConfigKey = 'linux'; @@ -445,10 +507,17 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin { final String name; final String? pluginClass; final String? dartPluginClass; + final bool ffiPlugin; final String? defaultPackage; @override - bool isNative() => pluginClass != null; + bool hasMethodChannel() => pluginClass != null; + + @override + bool hasFfi() => ffiPlugin; + + @override + bool hasDart() => dartPluginClass != null; @override Map toMap() { @@ -457,6 +526,7 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin { if (pluginClass != null) 'class': pluginClass!, if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!), if (dartPluginClass != null) kDartPluginClass: dartPluginClass!, + if (ffiPlugin) kFfiPlugin: true, if (defaultPackage != null) kDefaultPackage: defaultPackage!, }; } diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index ff4e2cfd13648..8dc9923c98f15 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -49,12 +49,17 @@ class Plugin { /// package: io.flutter.plugins.sample /// pluginClass: SamplePlugin /// ios: + /// # A plugin implemented through method channels. /// pluginClass: SamplePlugin /// linux: - /// pluginClass: SamplePlugin + /// # A plugin implemented purely in Dart code. + /// dartPluginClass: SamplePlugin /// macos: - /// pluginClass: SamplePlugin + /// # A plugin implemented with `dart:ffi`. + /// ffiPlugin: true /// windows: + /// # A plugin using platform-specific Dart and method channels. + /// dartPluginClass: SamplePlugin /// pluginClass: SamplePlugin factory Plugin.fromYaml( String name, diff --git a/packages/flutter_tools/templates/README.md b/packages/flutter_tools/templates/README.md index 53646ea3b716c..f3e5f6d98b563 100644 --- a/packages/flutter_tools/templates/README.md +++ b/packages/flutter_tools/templates/README.md @@ -1,8 +1,11 @@ This directory contains templates for `flutter create`. -The `app_shared` subdirectory is special. It provides files for all app -templates (as opposed to plugin or module templates). -As of May 2021, there are two app templates: `app` (the counter app) +The `*_shared` subdirectories provide files for multiple templates. + +* `app_shared` for `app` and `skeleton`. +* `plugin_shared` for (method channel) `plugin` and `plugin_ffi`. + +For example, there are two app templates: `app` (the counter app) and `skeleton` (the more advanced list view/detail view app). ```plain diff --git a/packages/flutter_tools/templates/app/lib/main.dart.tmpl b/packages/flutter_tools/templates/app/lib/main.dart.tmpl index a18822bd1be74..b8b0bf7faf9f6 100644 --- a/packages/flutter_tools/templates/app/lib/main.dart.tmpl +++ b/packages/flutter_tools/templates/app/lib/main.dart.tmpl @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; -{{#withPluginHook}} +{{#withPlatformChannelPluginHook}} import 'dart:async'; import 'package:flutter/services.dart'; import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart'; -{{/withPluginHook}} +{{/withPlatformChannelPluginHook}} +{{#withFfiPluginHook}} +import 'dart:async'; + +import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart' as {{pluginProjectName}}; +{{/withFfiPluginHook}} void main() { runApp(const MyApp()); @@ -121,7 +126,7 @@ class _MyHomePageState extends State { } } {{/withPluginHook}} -{{#withPluginHook}} +{{#withPlatformChannelPluginHook}} class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @@ -174,4 +179,71 @@ class _MyAppState extends State { ); } } -{{/withPluginHook}} +{{/withPlatformChannelPluginHook}} +{{#withFfiPluginHook}} +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + late int sumResult; + late Future sumAsyncResult; + + @override + void initState() { + super.initState(); + sumResult = {{pluginProjectName}}.sum(1, 2); + sumAsyncResult = {{pluginProjectName}}.sumAsync(3, 4); + } + + @override + Widget build(BuildContext context) { + const textStyle = TextStyle(fontSize: 25); + const spacerSmall = SizedBox(height: 10); + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Native Packages'), + ), + body: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + const Text( + 'This calls a native function through FFI that is shipped as source in the package. ' + 'The native code is built as part of the Flutter Runner build.', + style: textStyle, + textAlign: TextAlign.center, + ), + spacerSmall, + Text( + 'sum(1, 2) = $sumResult', + style: textStyle, + textAlign: TextAlign.center, + ), + spacerSmall, + FutureBuilder( + future: sumAsyncResult, + builder: (BuildContext context, AsyncSnapshot value) { + final displayValue = + (value.hasData) ? value.data : 'loading'; + return Text( + 'await sumAsync(3, 4) = $displayValue', + style: textStyle, + textAlign: TextAlign.center, + ); + }, + ), + ], + ), + ), + ), + ), + ); + } +} +{{/withFfiPluginHook}} diff --git a/packages/flutter_tools/templates/app/pubspec.yaml.tmpl b/packages/flutter_tools/templates/app/pubspec.yaml.tmpl index b8c0d2d2eebdf..6bc1a4a1d6b83 100644 --- a/packages/flutter_tools/templates/app/pubspec.yaml.tmpl +++ b/packages/flutter_tools/templates/app/pubspec.yaml.tmpl @@ -4,7 +4,7 @@ description: {{description}} # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -{{^withPluginHook}} +{{^withPlatformChannelPluginHook}} # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.0+1 -{{/withPluginHook}} +{{/withPlatformChannelPluginHook}} environment: sdk: {{dartSdkVersionBounds}} diff --git a/packages/flutter_tools/templates/app_shared/android-java.tmpl/app/build.gradle.tmpl b/packages/flutter_tools/templates/app_shared/android-java.tmpl/app/build.gradle.tmpl index 23c06f0c1de96..2ebd98494445b 100644 --- a/packages/flutter_tools/templates/app_shared/android-java.tmpl/app/build.gradle.tmpl +++ b/packages/flutter_tools/templates/app_shared/android-java.tmpl/app/build.gradle.tmpl @@ -27,6 +27,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/packages/flutter_tools/templates/app_shared/android-kotlin.tmpl/app/build.gradle.tmpl b/packages/flutter_tools/templates/app_shared/android-kotlin.tmpl/app/build.gradle.tmpl index c0691cb9f3db2..c69744f37259c 100644 --- a/packages/flutter_tools/templates/app_shared/android-kotlin.tmpl/app/build.gradle.tmpl +++ b/packages/flutter_tools/templates/app_shared/android-kotlin.tmpl/app/build.gradle.tmpl @@ -27,6 +27,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl index ef572e52fcd39..f2269110c9998 100644 --- a/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl +++ b/packages/flutter_tools/templates/app_shared/linux.tmpl/CMakeLists.txt.tmpl @@ -116,11 +116,11 @@ install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR} install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) -endif() +endforeach(bundled_library) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. diff --git a/packages/flutter_tools/templates/app/test/widget_test.dart.tmpl b/packages/flutter_tools/templates/app_test_widget/test/widget_test.dart.tmpl similarity index 95% rename from packages/flutter_tools/templates/app/test/widget_test.dart.tmpl rename to packages/flutter_tools/templates/app_test_widget/test/widget_test.dart.tmpl index fd5e4e4011a42..e915079b8e4f7 100644 --- a/packages/flutter_tools/templates/app/test/widget_test.dart.tmpl +++ b/packages/flutter_tools/templates/app_test_widget/test/widget_test.dart.tmpl @@ -30,7 +30,7 @@ void main() { }); } {{/withPluginHook}} -{{#withPluginHook}} +{{#withPlatformChannelPluginHook}} void main() { testWidgets('Verify Platform version', (WidgetTester tester) async { // Build our app and trigger a frame. @@ -46,4 +46,4 @@ void main() { ); }); } -{{/withPluginHook}} +{{/withPlatformChannelPluginHook}} diff --git a/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/build.gradle.tmpl index c4df7869206f9..0ca0966be8fe0 100644 --- a/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/build.gradle.tmpl +++ b/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/build.gradle.tmpl @@ -31,6 +31,7 @@ version '1.0' android { compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/packages/flutter_tools/templates/module/common/lib/main.dart.tmpl b/packages/flutter_tools/templates/module/common/lib/main.dart.tmpl index d8ef114c40906..ec9fada2d07d4 100644 --- a/packages/flutter_tools/templates/module/common/lib/main.dart.tmpl +++ b/packages/flutter_tools/templates/module/common/lib/main.dart.tmpl @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -{{#withPluginHook}} +{{#withPlatformChannelPluginHook}} import 'dart:async'; import 'package:flutter/services.dart'; import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart'; -{{/withPluginHook}} +{{/withPlatformChannelPluginHook}} void main() => runApp(const MyApp()); -{{^withPluginHook}} +{{^withPlatformChannelPluginHook}} class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @@ -117,8 +117,8 @@ class _MyHomePageState extends State { ); } } -{{/withPluginHook}} -{{#withPluginHook}} +{{/withPlatformChannelPluginHook}} +{{#withPlatformChannelPluginHook}} class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @@ -171,4 +171,4 @@ class _MyAppState extends State { ); } } -{{/withPluginHook}} +{{/withPlatformChannelPluginHook}} diff --git a/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl b/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl index fd5e4e4011a42..72a734a5c1e3e 100644 --- a/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl +++ b/packages/flutter_tools/templates/module/common/test/widget_test.dart.tmpl @@ -8,7 +8,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +{{^withFfiPluginHook}} import 'package:{{projectName}}/main.dart'; +{{/withFfiPluginHook}} {{^withPluginHook}} void main() { @@ -30,7 +32,7 @@ void main() { }); } {{/withPluginHook}} -{{#withPluginHook}} +{{#withPlatformChannelPluginHook}} void main() { testWidgets('Verify Platform version', (WidgetTester tester) async { // Build our app and trigger a frame. @@ -46,4 +48,4 @@ void main() { ); }); } -{{/withPluginHook}} +{{/withPlatformChannelPluginHook}} diff --git a/packages/flutter_tools/templates/plugin/.idea/libraries/Dart_SDK.xml.tmpl b/packages/flutter_tools/templates/plugin/.idea/libraries/Dart_SDK.xml.tmpl deleted file mode 100644 index 20ef202703297..0000000000000 --- a/packages/flutter_tools/templates/plugin/.idea/libraries/Dart_SDK.xml.tmpl +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/flutter_tools/templates/plugin/.idea/modules.xml.tmpl b/packages/flutter_tools/templates/plugin/.idea/modules.xml.tmpl deleted file mode 100644 index fa2d48f2220e9..0000000000000 --- a/packages/flutter_tools/templates/plugin/.idea/modules.xml.tmpl +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/packages/flutter_tools/templates/plugin/.idea/runConfigurations/example_lib_main_dart.xml.tmpl b/packages/flutter_tools/templates/plugin/.idea/runConfigurations/example_lib_main_dart.xml.tmpl deleted file mode 100644 index 5fd9159d1c5ba..0000000000000 --- a/packages/flutter_tools/templates/plugin/.idea/runConfigurations/example_lib_main_dart.xml.tmpl +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/flutter_tools/templates/plugin/.idea/workspace.xml.tmpl b/packages/flutter_tools/templates/plugin/.idea/workspace.xml.tmpl deleted file mode 100644 index 3b4f99b555d54..0000000000000 --- a/packages/flutter_tools/templates/plugin/.idea/workspace.xml.tmpl +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/flutter_tools/templates/plugin/README.md.tmpl b/packages/flutter_tools/templates/plugin/README.md.tmpl index fc3fa8f317545..a007a8cc9b2c7 100644 --- a/packages/flutter_tools/templates/plugin/README.md.tmpl +++ b/packages/flutter_tools/templates/plugin/README.md.tmpl @@ -15,6 +15,6 @@ samples, guidance on mobile development, and a full API reference. {{#no_platforms}} The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported. -To add platforms, run `flutter create -t plugin --platforms .` under the same -directory. You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. +To add platforms, run `flutter create -t plugin --platforms .` in this directory. +You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. {{/no_platforms}} diff --git a/packages/flutter_tools/templates/plugin_ffi/README.md.tmpl b/packages/flutter_tools/templates/plugin_ffi/README.md.tmpl new file mode 100644 index 0000000000000..7f1dfb82a9e51 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/README.md.tmpl @@ -0,0 +1,97 @@ +# {{projectName}} + +{{description}} + +## Getting Started + +This project is a starting point for a Flutter +[FFI plugin](https://docs.flutter.dev/development/platform-integration/c-interop), +a specialized package that includes native code directly invoked with Dart FFI. + +## Project stucture + +This template uses the following structure: + +* `src`: Contains the native source code, and a CmakeFile.txt file for building + that source code into a dynamic library. + +* `lib`: Contains the Dart code that defines the API of the plugin, and which + calls into the native code using `dart:ffi`. + +* platform folders (`android`, `ios`, `windows`, etc.): Contains the build files + for building and bundling the native code library with the platform application. + +## Buidling and bundling native code + +The `pubspec.yaml` specifies FFI plugins as follows: + +```yaml + plugin: + platforms: + some_platform: + ffiPlugin: true +``` + +This configuration invokes the native build for the various target platforms +and bundles the binaries in Flutter applications using these FFI plugins. + +This can be combined with dartPluginClass, such as when FFI is used for the +implementation of one platform in a federated plugin: + +```yaml + plugin: + implements: some_other_plugin + platforms: + some_platform: + dartPluginClass: SomeClass + ffiPlugin: true +``` + +A plugin can have both FFI and method channels: + +```yaml + plugin: + platforms: + some_platform: + pluginClass: SomeName + ffiPlugin: true +``` + +The native build systems that are invoked by FFI (and method channel) plugins are: + +* For Android: Gradle, which invokes the Android NDK for native builds. + * See the documentation in android/build.gradle. +* For iOS and MacOS: Xcode, via CocoaPods. + * See the documentation in ios/{{projectName}}.podspec. + * See the documentation in macos/{{projectName}}.podspec. +* For Linux and Windows: CMake. + * See the documentation in linux/CMakeLists.txt. + * See the documentation in windows/CMakeLists.txt. + +## Binding to native code + +To use the native code, bindings in Dart are needed. +To avoid writing these by hand, they are generated from the header file +(`src/{{projectName}}.h`) by `package:ffigen`. +Regenerate the bindings by running `flutter pub run ffigen --config ffigen.yaml`. + +## Invoking native code + +Very short-running native functions can be directly invoked from any isolate. +For example, see `sum` in `lib/{{projectName}}.dart`. + +Longer-running functions should be invoked on a helper isolate to avoid +dropping frames in Flutter applications. +For example, see `sumAsync` in `lib/{{projectName}}.dart`. + +## Flutter help + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + +{{#no_platforms}} +The plugin project was generated without specifying the `--platforms` flag, so no platforms are currently supported. +To add platforms, run `flutter create -t plugin_ffi --platforms .` in this directory. +You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. +{{/no_platforms}} diff --git a/packages/flutter_tools/templates/plugin_ffi/android.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/plugin_ffi/android.tmpl/build.gradle.tmpl new file mode 100644 index 0000000000000..9f73fadb889dc --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/android.tmpl/build.gradle.tmpl @@ -0,0 +1,59 @@ +// The Android Gradle Plugin builds the native code with the Android NDK. + +group '{{androidIdentifier}}' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + // The Android Gradle Plugin knows how to build native code with the NDK. + classpath 'com.android.tools.build:gradle:4.1.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + // Bumping the plugin compileSdkVersion requires all clients of this plugin + // to bump the version in their app. + compileSdkVersion 31 + + // Bumping the plugin ndkVersion requires all clients of this plugin to bump + // the version in their app and to download a newer version of the NDK. + ndkVersion "21.1.6352462" + + // Invoke the shared CMake build with the Android Gradle Plugin. + externalNativeBuild { + cmake { + path "../src/CMakeLists.txt" + + // The default CMake version for the Android Gradle Plugin is 3.10.2. + // https://developer.android.com/studio/projects/install-ndk#vanilla_cmake + // + // The Flutter tooling requires that developers have CMake 3.10 or later + // installed. You should not increase this version, as doing so will cause + // the plugin to fail to compile for some customers of the plugin. + // version "3.10.2" + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 16 + } +} diff --git a/packages/flutter_tools/templates/plugin_ffi/android.tmpl/projectName_android.iml.tmpl b/packages/flutter_tools/templates/plugin_ffi/android.tmpl/projectName_android.iml.tmpl new file mode 100644 index 0000000000000..a56f0d75d3c03 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/android.tmpl/projectName_android.iml.tmpl @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_tools/templates/plugin_ffi/ffigen.yaml.tmpl b/packages/flutter_tools/templates/plugin_ffi/ffigen.yaml.tmpl new file mode 100644 index 0000000000000..5762da10e4aa8 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/ffigen.yaml.tmpl @@ -0,0 +1,19 @@ +# Run with `dart run ffigen --config ffigen.yaml`. +name: {{pluginDartClass}}Bindings +description: | + Bindings for `src/{{projectName}}.h`. + + Regenerate bindings with `dart run ffigen --config ffigen.yaml`. +output: 'lib/{{projectName}}_bindings_generated.dart' +headers: + entry-points: + - 'src/{{projectName}}.h' + include-directives: + - 'src/{{projectName}}.h' +preamble: | + // ignore_for_file: always_specify_types + // ignore_for_file: camel_case_types + // ignore_for_file: non_constant_identifier_names +comments: + style: any + length: full diff --git a/packages/flutter_tools/templates/plugin_ffi/ios.tmpl/Classes/projectName.c.tmpl b/packages/flutter_tools/templates/plugin_ffi/ios.tmpl/Classes/projectName.c.tmpl new file mode 100644 index 0000000000000..3982502596059 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/ios.tmpl/Classes/projectName.c.tmpl @@ -0,0 +1,3 @@ +// Relative import to be able to reuse the C sources. +// See the comment in ../{projectName}}.podspec for more information. +#include "../../src/{{projectName}}.c" diff --git a/packages/flutter_tools/templates/plugin_ffi/ios.tmpl/projectName.podspec.tmpl b/packages/flutter_tools/templates/plugin_ffi/ios.tmpl/projectName.podspec.tmpl new file mode 100644 index 0000000000000..b914d852bcbe9 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/ios.tmpl/projectName.podspec.tmpl @@ -0,0 +1,28 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint {{projectName}}.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = '{{projectName}}' + s.version = '0.0.1' + s.summary = '{{description}}' + s.description = <<-DESC +{{description}} + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '9.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/packages/flutter_tools/templates/plugin_ffi/lib/projectName.dart.tmpl b/packages/flutter_tools/templates/plugin_ffi/lib/projectName.dart.tmpl new file mode 100644 index 0000000000000..43e49f988bd87 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/lib/projectName.dart.tmpl @@ -0,0 +1,139 @@ +{{#no_platforms}} +// You have generated a new plugin project without specifying the `--platforms` +// flag. An FFI plugin project that supports no platforms is generated. +// To add platforms, run `flutter create -t plugin_ffi --platforms .` +// in this directory. You can also find a detailed instruction on how to +// add platforms in the `pubspec.yaml` at +// https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. +{{/no_platforms}} + +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + +import '{{projectName}}_bindings_generated.dart'; + +/// A very short-lived native function. +/// +/// For very short-lived functions, it is fine to call them on the main isolate. +/// They will block the Dart execution while running the native function, so +/// only do this for native functions which are guaranteed to be short-lived. +int sum(int a, int b) => _bindings.sum(a, b); + +/// A longer lived native function, which occupies the thread calling it. +/// +/// Do not call these kind of native functions in the main isolate. They will +/// block Dart execution. This will cause dropped frames in Flutter applications. +/// Instead, call these native functions on a separate isolate. +/// +/// Modify this to suit your own use case. Example use cases: +/// +/// 1. Reuse a single isolate for various different kinds of requests. +/// 2. Use multiple helper isolates for parallel execution. +Future sumAsync(int a, int b) async { + final SendPort helperIsolateSendPort = await _helperIsolateSendPort; + final int requestId = _nextSumRequestId++; + final _SumRequest request = _SumRequest(requestId, a, b); + final Completer completer = Completer(); + _sumRequests[requestId] = completer; + helperIsolateSendPort.send(request); + return completer.future; +} + +const String _libName = '{{projectName}}'; + +/// The dynamic library in which the symbols for [{{pluginDartClass}}Bindings] can be found. +final DynamicLibrary _dylib = () { + if (Platform.isMacOS || Platform.isIOS) { + return DynamicLibrary.open('$_libName.framework/$_libName'); + } + if (Platform.isAndroid || Platform.isLinux) { + return DynamicLibrary.open('lib$_libName.so'); + } + if (Platform.isWindows) { + return DynamicLibrary.open('$_libName.dll'); + } + throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}'); +}(); + +/// The bindings to the native functions in [_dylib]. +final {{pluginDartClass}}Bindings _bindings = {{pluginDartClass}}Bindings(_dylib); + + +/// A request to compute `sum`. +/// +/// Typically sent from one isolate to another. +class _SumRequest { + final int id; + final int a; + final int b; + + const _SumRequest(this.id, this.a, this.b); +} + +/// A response with the result of `sum`. +/// +/// Typically sent from one isolate to another. +class _SumResponse { + final int id; + final int result; + + const _SumResponse(this.id, this.result); +} + +/// Counter to identify [_SumRequest]s and [_SumResponse]s. +int _nextSumRequestId = 0; + +/// Mapping from [_SumRequest] `id`s to the completers corresponding to the correct future of the pending request. +final Map> _sumRequests = >{}; + +/// The SendPort belonging to the helper isolate. +Future _helperIsolateSendPort = () async { + // The helper isolate is going to send us back a SendPort, which we want to + // wait for. + final Completer completer = Completer(); + + // Receive port on the main isolate to receive messages from the helper. + // We receive two types of messages: + // 1. A port to send messages on. + // 2. Responses to requests we sent. + final ReceivePort receivePort = ReceivePort() + ..listen((dynamic data) { + if (data is SendPort) { + // The helper isolate sent us the port on which we can sent it requests. + completer.complete(data); + return; + } + if (data is _SumResponse) { + // The helper isolate sent us a response to a request we sent. + final Completer completer = _sumRequests[data.id]!; + _sumRequests.remove(data.id); + completer.complete(data.result); + return; + } + throw UnsupportedError('Unsupported message type: ${data.runtimeType}'); + }); + + // Start the helper isolate. + await Isolate.spawn((SendPort sendPort) async { + final ReceivePort helperReceivePort = ReceivePort() + ..listen((dynamic data) { + // On the helper isolate listen to requests and respond to them. + if (data is _SumRequest) { + final int result = _bindings.sum_long_running(data.a, data.b); + final _SumResponse response = _SumResponse(data.id, result); + sendPort.send(response); + return; + } + throw UnsupportedError('Unsupported message type: ${data.runtimeType}'); + }); + + // Send the the port to the main isolate on which we can receive requests. + sendPort.send(helperReceivePort.sendPort); + }, receivePort.sendPort); + + // Wait until the helper isolate has sent us back the SendPort on which we + // can start sending requests. + return completer.future; +}(); diff --git a/packages/flutter_tools/templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl b/packages/flutter_tools/templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl new file mode 100644 index 0000000000000..0c5f74045e323 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl @@ -0,0 +1,69 @@ +// ignore_for_file: always_specify_types +// ignore_for_file: camel_case_types +// ignore_for_file: non_constant_identifier_names + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +import 'dart:ffi' as ffi; + +/// Bindings for `src/{{projectName}}.h`. +/// +/// Regenerate bindings with `dart run ffigen --config ffigen.yaml`. +/// +class {{pluginDartClass}}Bindings { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + {{pluginDartClass}}Bindings(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + {{pluginDartClass}}Bindings.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + /// A very short-lived native function. + /// + /// For very short-lived functions, it is fine to call them on the main isolate. + /// They will block the Dart execution while running the native function, so + /// only do this for native functions which are guaranteed to be short-lived. + int sum( + int a, + int b, + ) { + return _sum( + a, + b, + ); + } + + late final _sumPtr = + _lookup>( + 'sum'); + late final _sum = _sumPtr.asFunction(); + + /// A longer lived native function, which occupies the thread calling it. + /// + /// Calling these kind of native functions in the main isolate will + /// block Dart execution and cause dropped frames in Flutter applications. + /// Consider calling such native functions from a separate isolate. + int sum_long_running( + int a, + int b, + ) { + return _sum_long_running( + a, + b, + ); + } + + late final _sum_long_runningPtr = + _lookup>( + 'sum_long_running'); + late final _sum_long_running = + _sum_long_runningPtr.asFunction(); +} diff --git a/packages/flutter_tools/templates/plugin_ffi/linux.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/plugin_ffi/linux.tmpl/CMakeLists.txt.tmpl new file mode 100644 index 0000000000000..a328c52e7c82b --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/linux.tmpl/CMakeLists.txt.tmpl @@ -0,0 +1,22 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "{{projectName}}") +project(${PROJECT_NAME} LANGUAGES CXX) + +# Invoke the build for native code shared with the other target platforms. +# This can be changed to accomodate different builds. +add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set({{projectName}}_bundled_libraries + # Defined in ../src/CMakeLists.txt. + # This can be changed to accomodate different builds. + $ + PARENT_SCOPE +) diff --git a/packages/flutter_tools/templates/plugin_ffi/macos.tmpl/Classes/projectName.c.tmpl b/packages/flutter_tools/templates/plugin_ffi/macos.tmpl/Classes/projectName.c.tmpl new file mode 100644 index 0000000000000..3982502596059 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/macos.tmpl/Classes/projectName.c.tmpl @@ -0,0 +1,3 @@ +// Relative import to be able to reuse the C sources. +// See the comment in ../{projectName}}.podspec for more information. +#include "../../src/{{projectName}}.c" diff --git a/packages/flutter_tools/templates/plugin_ffi/src.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/plugin_ffi/src.tmpl/CMakeLists.txt.tmpl new file mode 100644 index 0000000000000..2a9ed9b9815ec --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/src.tmpl/CMakeLists.txt.tmpl @@ -0,0 +1,17 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +project({{projectName}}_library VERSION 0.0.1 LANGUAGES C) + +add_library({{projectName}} SHARED + "{{projectName}}.c" +) + +set_target_properties({{projectName}} PROPERTIES + PUBLIC_HEADER {{projectName}}.h + OUTPUT_NAME "{{projectName}}" +) + +target_compile_definitions({{projectName}} PUBLIC DART_SHARED_LIB) diff --git a/packages/flutter_tools/templates/plugin_ffi/src.tmpl/projectName.c.tmpl b/packages/flutter_tools/templates/plugin_ffi/src.tmpl/projectName.c.tmpl new file mode 100644 index 0000000000000..a4bb788df989b --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/src.tmpl/projectName.c.tmpl @@ -0,0 +1,23 @@ +#include "{{projectName}}.h" + +// A very short-lived native function. +// +// For very short-lived functions, it is fine to call them on the main isolate. +// They will block the Dart execution while running the native function, so +// only do this for native functions which are guaranteed to be short-lived. +FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b) { return a + b; } + +// A longer-lived native function, which occupies the thread calling it. +// +// Do not call these kind of native functions in the main isolate. They will +// block Dart execution. This will cause dropped frames in Flutter applications. +// Instead, call these native functions on a separate isolate. +FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b) { + // Simulate work. +#if _WIN32 + Sleep(5000); +#else + usleep(5000 * 1000); +#endif + return a + b; +} diff --git a/packages/flutter_tools/templates/plugin_ffi/src.tmpl/projectName.h.tmpl b/packages/flutter_tools/templates/plugin_ffi/src.tmpl/projectName.h.tmpl new file mode 100644 index 0000000000000..084c64228f465 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/src.tmpl/projectName.h.tmpl @@ -0,0 +1,30 @@ +#include +#include +#include + +#if _WIN32 +#include +#else +#include +#include +#endif + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +// A very short-lived native function. +// +// For very short-lived functions, it is fine to call them on the main isolate. +// They will block the Dart execution while running the native function, so +// only do this for native functions which are guaranteed to be short-lived. +FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b); + +// A longer lived native function, which occupies the thread calling it. +// +// Do not call these kind of native functions in the main isolate. They will +// block Dart execution. This will cause dropped frames in Flutter applications. +// Instead, call these native functions on a separate isolate. +FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b); diff --git a/packages/flutter_tools/templates/plugin_ffi/windows.tmpl/CMakeLists.txt.tmpl b/packages/flutter_tools/templates/plugin_ffi/windows.tmpl/CMakeLists.txt.tmpl new file mode 100644 index 0000000000000..cfd21c3676cac --- /dev/null +++ b/packages/flutter_tools/templates/plugin_ffi/windows.tmpl/CMakeLists.txt.tmpl @@ -0,0 +1,23 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "{{projectName}}") +project(${PROJECT_NAME} LANGUAGES CXX) + +# Invoke the build for native code shared with the other target platforms. +# This can be changed to accomodate different builds. +add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set({{projectName}}_bundled_libraries + # Defined in ../src/CMakeLists.txt. + # This can be changed to accomodate different builds. + $ + PARENT_SCOPE +) diff --git a/packages/flutter_tools/templates/plugin/.gitignore.tmpl b/packages/flutter_tools/templates/plugin_shared/.gitignore.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/.gitignore.tmpl rename to packages/flutter_tools/templates/plugin_shared/.gitignore.tmpl diff --git a/packages/flutter_tools/templates/plugin/.metadata.tmpl b/packages/flutter_tools/templates/plugin_shared/.metadata.tmpl similarity index 67% rename from packages/flutter_tools/templates/plugin/.metadata.tmpl rename to packages/flutter_tools/templates/plugin_shared/.metadata.tmpl index 91ede56d6681c..89546c72e7604 100644 --- a/packages/flutter_tools/templates/plugin/.metadata.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/.metadata.tmpl @@ -7,4 +7,9 @@ version: revision: {{flutterRevision}} channel: {{flutterChannel}} +{{#withFfiPluginHook}} +project_type: plugin_ffi +{{/withFfiPluginHook}} +{{#withPlatformChannelPluginHook}} project_type: plugin +{{/withPlatformChannelPluginHook}} diff --git a/packages/flutter_tools/templates/plugin/CHANGELOG.md.tmpl b/packages/flutter_tools/templates/plugin_shared/CHANGELOG.md.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/CHANGELOG.md.tmpl rename to packages/flutter_tools/templates/plugin_shared/CHANGELOG.md.tmpl diff --git a/packages/flutter_tools/templates/plugin/LICENSE.tmpl b/packages/flutter_tools/templates/plugin_shared/LICENSE.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/LICENSE.tmpl rename to packages/flutter_tools/templates/plugin_shared/LICENSE.tmpl diff --git a/packages/flutter_tools/templates/plugin/analysis_options.yaml.tmpl b/packages/flutter_tools/templates/plugin_shared/analysis_options.yaml.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/analysis_options.yaml.tmpl rename to packages/flutter_tools/templates/plugin_shared/analysis_options.yaml.tmpl diff --git a/packages/flutter_tools/templates/plugin_shared/android.tmpl/.gitignore b/packages/flutter_tools/templates/plugin_shared/android.tmpl/.gitignore new file mode 100644 index 0000000000000..161bdcdaf88c7 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_shared/android.tmpl/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/flutter_tools/templates/plugin_shared/android.tmpl/settings.gradle.tmpl b/packages/flutter_tools/templates/plugin_shared/android.tmpl/settings.gradle.tmpl new file mode 100644 index 0000000000000..ad71a66e66b97 --- /dev/null +++ b/packages/flutter_tools/templates/plugin_shared/android.tmpl/settings.gradle.tmpl @@ -0,0 +1 @@ +rootProject.name = '{{projectName}}' diff --git a/packages/flutter_tools/templates/plugin_shared/android.tmpl/src/main/AndroidManifest.xml.tmpl b/packages/flutter_tools/templates/plugin_shared/android.tmpl/src/main/AndroidManifest.xml.tmpl new file mode 100644 index 0000000000000..9d86092fc124e --- /dev/null +++ b/packages/flutter_tools/templates/plugin_shared/android.tmpl/src/main/AndroidManifest.xml.tmpl @@ -0,0 +1,3 @@ + + diff --git a/packages/flutter_tools/templates/plugin/macos.tmpl/projectName.podspec.tmpl b/packages/flutter_tools/templates/plugin_shared/macos.tmpl/projectName.podspec.tmpl similarity index 68% rename from packages/flutter_tools/templates/plugin/macos.tmpl/projectName.podspec.tmpl rename to packages/flutter_tools/templates/plugin_shared/macos.tmpl/projectName.podspec.tmpl index 8f73170cd0f88..72bae61e8a73b 100644 --- a/packages/flutter_tools/templates/plugin/macos.tmpl/projectName.podspec.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/macos.tmpl/projectName.podspec.tmpl @@ -12,6 +12,13 @@ Pod::Spec.new do |s| s.homepage = 'http://example.com' s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } + +{{#withFfiPluginHook}} + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. +{{/withFfiPluginHook}} s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' diff --git a/packages/flutter_tools/templates/plugin/projectName.iml.tmpl b/packages/flutter_tools/templates/plugin_shared/projectName.iml.tmpl similarity index 98% rename from packages/flutter_tools/templates/plugin/projectName.iml.tmpl rename to packages/flutter_tools/templates/plugin_shared/projectName.iml.tmpl index 429df7daf76a2..39cce21274d79 100644 --- a/packages/flutter_tools/templates/plugin/projectName.iml.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/projectName.iml.tmpl @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/packages/flutter_tools/templates/plugin/pubspec.yaml.tmpl b/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl similarity index 61% rename from packages/flutter_tools/templates/plugin/pubspec.yaml.tmpl rename to packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl index 3c3d150c0c1f9..b2fbb42ae7892 100644 --- a/packages/flutter_tools/templates/plugin/pubspec.yaml.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/pubspec.yaml.tmpl @@ -5,7 +5,12 @@ homepage: environment: sdk: {{dartSdkVersionBounds}} +{{#withPlatformChannelPluginHook}} flutter: ">=2.5.0" +{{/withPlatformChannelPluginHook}} +{{#withFfiPluginHook}} + flutter: ">={{minFrameworkVersionFfiPlugin}}" +{{/withFfiPluginHook}} dependencies: flutter: @@ -16,6 +21,9 @@ dependencies: {{/web}} dev_dependencies: +{{#withFfiPluginHook}} + ffigen: ^4.1.2 +{{/withFfiPluginHook}} flutter_test: sdk: flutter flutter_lints: ^1.0.0 @@ -26,9 +34,52 @@ dev_dependencies: # The following section is specific to Flutter. flutter: # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' and Android 'package' identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when # adding or updating assets for this project. +{{#withFfiPluginHook}} + # + # Please refer to README.md for a detailed explanation. + plugin: + platforms: +{{#no_platforms}} + # This FFI plugin project was generated without specifying any + # platforms with the `--platform` argument. If you see the `some_platform` map below, remove it and + # then add platforms following the instruction here: + # https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms + # ------------------- + some_platform: + ffiPlugin: true + # ------------------- +{{/no_platforms}} +{{#android}} + android: + ffiPlugin: true +{{/android}} +{{#ios}} + ios: + ffiPlugin: true +{{/ios}} +{{#linux}} + linux: + ffiPlugin: true +{{/linux}} +{{#macos}} + macos: + ffiPlugin: true +{{/macos}} +{{#windows}} + windows: + ffiPlugin: true +{{/windows}} +{{/withFfiPluginHook}} +{{#withPlatformChannelPluginHook}} plugin: platforms: {{#no_platforms}} @@ -67,6 +118,7 @@ flutter: pluginClass: {{pluginDartClass}}Web fileName: {{projectName}}_web.dart {{/web}} +{{/withPlatformChannelPluginHook}} # To add assets to your plugin package, add an assets section, like this: # assets: diff --git a/packages/flutter_tools/templates/plugin/windows.tmpl/.gitignore b/packages/flutter_tools/templates/plugin_shared/windows.tmpl/.gitignore similarity index 100% rename from packages/flutter_tools/templates/plugin/windows.tmpl/.gitignore rename to packages/flutter_tools/templates/plugin_shared/windows.tmpl/.gitignore diff --git a/packages/flutter_tools/templates/template_manifest.json b/packages/flutter_tools/templates/template_manifest.json index 56a022085da6a..95f07951be64d 100644 --- a/packages/flutter_tools/templates/template_manifest.json +++ b/packages/flutter_tools/templates/template_manifest.json @@ -5,7 +5,6 @@ "templates/app/lib/main.dart.tmpl", "templates/app/pubspec.yaml.tmpl", "templates/app/README.md.tmpl", - "templates/app/test/widget_test.dart.tmpl", "templates/app/winuwp.tmpl/.gitignore", "templates/app_shared/.gitignore.tmpl", @@ -201,6 +200,8 @@ "templates/app_shared/winuwp.tmpl/runner_uwp/resources.pri.img.tmpl", "templates/app_shared/winuwp.tmpl/runner_uwp/Windows_TemporaryKey.pfx.img.tmpl", + "templates/app_test_widget/test/widget_test.dart.tmpl", + "templates/cocoapods/Podfile-ios-objc", "templates/cocoapods/Podfile-ios-swift", "templates/cocoapods/Podfile-macos", @@ -304,13 +305,6 @@ "templates/package/README.md.tmpl", "templates/package/test/projectName_test.dart.tmpl", - "templates/plugin/.gitignore.tmpl", - "templates/plugin/.idea/libraries/Dart_SDK.xml.tmpl", - "templates/plugin/.idea/modules.xml.tmpl", - "templates/plugin/.idea/runConfigurations/example_lib_main_dart.xml.tmpl", - "templates/plugin/.idea/workspace.xml.tmpl", - "templates/plugin/.metadata.tmpl", - "templates/plugin/analysis_options.yaml.tmpl", "templates/plugin/android-java.tmpl/build.gradle.tmpl", "templates/plugin/android-java.tmpl/projectName_android.iml.tmpl", "templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl", @@ -322,7 +316,6 @@ "templates/plugin/android.tmpl/gradle.properties.tmpl", "templates/plugin/android.tmpl/settings.gradle.tmpl", "templates/plugin/android.tmpl/src/main/AndroidManifest.xml.tmpl", - "templates/plugin/CHANGELOG.md.tmpl", "templates/plugin/ios-objc.tmpl/Classes/pluginClass.h.tmpl", "templates/plugin/ios-objc.tmpl/Classes/pluginClass.m.tmpl", "templates/plugin/ios-objc.tmpl/projectName.podspec.tmpl", @@ -333,22 +326,51 @@ "templates/plugin/ios.tmpl/.gitignore", "templates/plugin/ios.tmpl/Assets/.gitkeep", "templates/plugin/lib/projectName.dart.tmpl", - "templates/plugin/LICENSE.tmpl", "templates/plugin/linux.tmpl/CMakeLists.txt.tmpl", "templates/plugin/linux.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl", "templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl", "templates/plugin/macos.tmpl/Classes/pluginClass.swift.tmpl", - "templates/plugin/macos.tmpl/projectName.podspec.tmpl", - "templates/plugin/projectName.iml.tmpl", - "templates/plugin/pubspec.yaml.tmpl", "templates/plugin/README.md.tmpl", "templates/plugin/test/projectName_test.dart.tmpl", - "templates/plugin/windows.tmpl/.gitignore", "templates/plugin/windows.tmpl/CMakeLists.txt.tmpl", "templates/plugin/windows.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl", "templates/plugin/windows.tmpl/pluginClassSnakeCase.cpp.tmpl", "templates/plugin/lib/projectName_web.dart.tmpl", + "templates/plugin_ffi/android.tmpl/build.gradle.tmpl", + "templates/plugin_ffi/android.tmpl/projectName_android.iml.tmpl", + "templates/plugin_ffi/ffigen.yaml.tmpl", + "templates/plugin_ffi/ios.tmpl/.gitignore", + "templates/plugin_ffi/ios.tmpl/Classes/projectName.c.tmpl", + "templates/plugin_ffi/ios.tmpl/projectName.podspec.tmpl", + "templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl", + "templates/plugin_ffi/lib/projectName.dart.tmpl", + "templates/plugin_ffi/linux.tmpl/CMakeLists.txt.tmpl", + "templates/plugin_ffi/linux.tmpl/include/projectName.tmpl/plugin_ffiClassSnakeCase.h.tmpl", + "templates/plugin_ffi/macos.tmpl/Classes/projectName.c.tmpl", + "templates/plugin_ffi/README.md.tmpl", + "templates/plugin_ffi/src.tmpl/CMakeLists.txt.tmpl", + "templates/plugin_ffi/src.tmpl/projectName.c.tmpl", + "templates/plugin_ffi/src.tmpl/projectName.h.tmpl", + "templates/plugin_ffi/windows.tmpl/CMakeLists.txt.tmpl", + + "templates/plugin_shared/.gitignore.tmpl", + "templates/plugin_shared/.idea/libraries/Dart_SDK.xml.tmpl", + "templates/plugin_shared/.idea/modules.xml.tmpl", + "templates/plugin_shared/.idea/runConfigurations/example_lib_main_dart.xml.tmpl", + "templates/plugin_shared/.idea/workspace.xml.tmpl", + "templates/plugin_shared/.metadata.tmpl", + "templates/plugin_shared/analysis_options.yaml.tmpl", + "templates/plugin_shared/android.tmpl/.gitignore", + "templates/plugin_shared/android.tmpl/settings.gradle.tmpl", + "templates/plugin_shared/android.tmpl/src/main/AndroidManifest.xml.tmpl", + "templates/plugin_shared/CHANGELOG.md.tmpl", + "templates/plugin_shared/LICENSE.tmpl", + "templates/plugin_shared/macos.tmpl/projectName.podspec.tmpl", + "templates/plugin_shared/projectName.iml.tmpl", + "templates/plugin_shared/pubspec.yaml.tmpl", + "templates/plugin_shared/windows.tmpl/.gitignore", + "templates/skeleton/assets/images/2.0x/flutter_logo.png.img.tmpl", "templates/skeleton/assets/images/3.0x/flutter_logo.png.img.tmpl", "templates/skeleton/assets/images/flutter_logo.png.img.tmpl", diff --git a/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart index 6b45a6dd69762..bb283a3198989 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart @@ -43,11 +43,14 @@ void main() { final List templatePaths = [ globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'app'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'app_shared'), + globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'app_test_widget'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'cocoapods'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'skeleton'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'module', 'common'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'package'), globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin'), + globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_ffi'), + globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_shared'), ]; for (final String templatePath in templatePaths) { globals.fs.directory(templatePath).createSync(recursive: true); @@ -96,6 +99,9 @@ void main() { await runner.run(['create', '--no-pub', '--template=plugin', 'testy']); expect((await command.usageValues).commandCreateProjectType, 'plugin'); + + await runner.run(['create', '--no-pub', '--template=plugin_ffi', 'testy']); + expect((await command.usageValues).commandCreateProjectType, 'plugin_ffi'); })); testUsingContext('set iOS host language type as usage value', () => testbed.run(() async { diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart index 3a1e9fb3beeaa..b8ce108031131 100755 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -2492,6 +2492,7 @@ void main() { final String buildContent = await globals.fs.file('${projectDir.path}/android/app/build.gradle').readAsString(); expect(buildContent.contains('compileSdkVersion flutter.compileSdkVersion'), true); + expect(buildContent.contains('ndkVersion flutter.ndkVersion'), true); expect(buildContent.contains('targetSdkVersion flutter.targetSdkVersion'), true); }); @@ -2761,6 +2762,104 @@ void main() { platform: globals.platform, ), }); + + testUsingContext('create an FFI plugin with ios, then add macos', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + await runner.run(['create', '--no-pub', '--template=plugin_ffi', '--platform=ios', projectDir.path]); + expect(projectDir.childDirectory('src'), exists); + expect(projectDir.childDirectory('ios'), exists); + expect(projectDir.childDirectory('example').childDirectory('ios'), exists); + validatePubspecForPlugin( + projectDir: projectDir.absolute.path, + expectedPlatforms: const [ + 'ios', + ], + ffiPlugin: true, + unexpectedPlatforms: ['some_platform'], + ); + + await runner.run(['create', '--no-pub', '--template=plugin_ffi', '--platform=macos', projectDir.path]); + expect(projectDir.childDirectory('macos'), exists); + expect( + projectDir.childDirectory('example').childDirectory('macos'), exists); + expect(projectDir.childDirectory('ios'), exists); + expect(projectDir.childDirectory('example').childDirectory('ios'), exists); + }, overrides: { + FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), + }); + + testUsingContext('FFI plugins error android language', () async { + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + final List args = [ + 'create', + '--no-pub', + '--template=plugin_ffi', + '-a', + 'kotlin', + '--platforms=android', + projectDir.path, + ]; + + await expectLater( + runner.run(args), + throwsToolExit(message: 'The "android-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'), + ); + }); + + testUsingContext('FFI plugins error ios language', () async { + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + final List args = [ + 'create', + '--no-pub', + '--template=plugin_ffi', + '--ios-language', + 'swift', + '--platforms=ios', + projectDir.path, + ]; + + await expectLater( + runner.run(args), + throwsToolExit(message: 'The "ios-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'), + ); + }); + + testUsingContext('FFI plugins error web platform', () async { + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + final List args = [ + 'create', + '--no-pub', + '--template=plugin_ffi', + '--platforms=web', + projectDir.path, + ]; + + await expectLater( + runner.run(args), + throwsToolExit(message: 'The web platform is not supported in plugin_ffi template.'), + ); + }); + + testUsingContext('should show warning when disabled platforms are selected while creating an FFI plugin', () async { + Cache.flutterRoot = '../..'; + + final CreateCommand command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); + + await runner.run(['create', '--no-pub', '--template=plugin_ffi', '--platforms=android,ios,windows,macos,linux', projectDir.path]); + await runner.run(['create', '--no-pub', '--template=plugin_ffi', projectDir.path]); + expect(logger.statusText, contains(_kDisabledPlatformRequestedMessage)); + + }, overrides: { + FeatureFlags: () => TestFeatureFlags(), + Logger: () => logger, + }); } Future _createProject( diff --git a/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart b/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart index 86ffbd3b8a04b..9ed260c2a875c 100644 --- a/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart +++ b/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart @@ -9,33 +9,43 @@ import '../src/common.dart'; import 'test_utils.dart'; void main() { - late Directory tempDir; + late Directory tempDirPluginMethodChannels; + late Directory tempDirPluginFfi; setUp(() async { - tempDir = createResolvedTempDirectorySync('flutter_plugin_test.'); + tempDirPluginMethodChannels = createResolvedTempDirectorySync('flutter_plugin_test.'); + tempDirPluginFfi = + createResolvedTempDirectorySync('flutter_ffi_plugin_test.'); }); tearDown(() async { - tryToDelete(tempDir); + tryToDelete(tempDirPluginMethodChannels); + tryToDelete(tempDirPluginFfi); }); - test('plugin example can be built using current Flutter Gradle plugin', () async { + Future testPlugin({ + required String template, + required Directory tempDir, + }) async { final String flutterBin = fileSystem.path.join( getFlutterRoot(), 'bin', 'flutter', ); + final String testName = '${template}_test'; + processManager.runSync([ flutterBin, ...getLocalEngineArguments(), 'create', - '--template=plugin', + '--template=$template', '--platforms=android', - 'plugin_test', + testName, ], workingDirectory: tempDir.path); - final Directory exampleAppDir = tempDir.childDirectory('plugin_test').childDirectory('example'); + final Directory exampleAppDir = + tempDir.childDirectory(testName).childDirectory('example'); final File buildGradleFile = exampleAppDir.childDirectory('android').childFile('build.gradle'); expect(buildGradleFile, exists); @@ -68,6 +78,11 @@ void main() { )); expect(exampleApk, exists); + if (template == 'plugin_ffi') { + // Does not support AGP 3.3.0. + return; + } + // Clean processManager.runSync([ flutterBin, @@ -101,5 +116,21 @@ android.enableR8=true'''); '--target-platform=android-arm', ], workingDirectory: exampleAppDir.path); expect(exampleApk, exists); + } + + test('plugin example can be built using current Flutter Gradle plugin', + () async { + await testPlugin( + template: 'plugin', + tempDir: tempDirPluginMethodChannels, + ); + }); + + test('FFI plugin example can be built using current Flutter Gradle plugin', + () async { + await testPlugin( + template: 'plugin_ffi', + tempDir: tempDirPluginFfi, + ); }); } diff --git a/packages/flutter_tools/test/integration.shard/android_plugin_ndkversion_mismatch_test.dart b/packages/flutter_tools/test/integration.shard/android_plugin_ndkversion_mismatch_test.dart new file mode 100644 index 0000000000000..306ead3714884 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/android_plugin_ndkversion_mismatch_test.dart @@ -0,0 +1,86 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/cache.dart'; + +import '../src/common.dart'; +import 'test_utils.dart'; + +void main() { + late Directory tempDir; + + setUp(() { + Cache.flutterRoot = getFlutterRoot(); + tempDir = createResolvedTempDirectorySync('flutter_plugin_test.'); + }); + + tearDown(() async { + tryToDelete(tempDir); + }); + + test('error logged when plugin Android ndkVersion higher than project', () async { + final String flutterBin = fileSystem.path.join( + getFlutterRoot(), + 'bin', + 'flutter', + ); + + // Create dummy plugin + processManager.runSync([ + flutterBin, + ...getLocalEngineArguments(), + 'create', + '--template=plugin_ffi', + '--platforms=android', + 'test_plugin', + ], workingDirectory: tempDir.path); + + final Directory pluginAppDir = tempDir.childDirectory('test_plugin'); + final File pluginGradleFile = pluginAppDir.childDirectory('android').childFile('build.gradle'); + expect(pluginGradleFile, exists); + + final String pluginBuildGradle = pluginGradleFile.readAsStringSync(); + + // Bump up plugin ndkVersion to 21.4.7075529. + final RegExp androidNdkVersionRegExp = RegExp(r'ndkVersion (\"[0-9\.]+\"|flutter.ndkVersion)'); + final String newPluginGradleFile = pluginBuildGradle.replaceAll(androidNdkVersionRegExp, 'ndkVersion "21.4.7075529"'); + expect(newPluginGradleFile, contains('21.4.7075529')); + pluginGradleFile.writeAsStringSync(newPluginGradleFile); + + final Directory pluginExampleAppDir = pluginAppDir.childDirectory('example'); + + final File projectGradleFile = pluginExampleAppDir.childDirectory('android').childDirectory('app').childFile('build.gradle'); + expect(projectGradleFile, exists); + + final String projectBuildGradle = projectGradleFile.readAsStringSync(); + + // Bump down plugin example app ndkVersion to 21.1.6352462. + final String newProjectGradleFile = projectBuildGradle.replaceAll(androidNdkVersionRegExp, 'ndkVersion "21.1.6352462"'); + expect(newProjectGradleFile, contains('21.1.6352462')); + projectGradleFile.writeAsStringSync(newProjectGradleFile); + + // Run flutter build apk to build plugin example project + final ProcessResult result = processManager.runSync([ + flutterBin, + ...getLocalEngineArguments(), + 'build', + 'apk', + '--target-platform=android-arm', + ], workingDirectory: pluginExampleAppDir.path); + + // Check that an error message is thrown. + expect(result.stderr, contains(''' +One or more plugins require a higher Android NDK version. +Fix this issue by adding the following to ${projectGradleFile.path}: +android { + ndkVersion 21.4.7075529 + ... +} + +''')); + }); +} diff --git a/packages/flutter_tools/test/integration.shard/test_data/deferred_components_project.dart b/packages/flutter_tools/test/integration.shard/test_data/deferred_components_project.dart index a51b332cca16c..715e1dd40cc58 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/deferred_components_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/deferred_components_project.dart @@ -176,6 +176,7 @@ class BasicDeferredComponentsConfig extends DeferredComponentsConfig { android { compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/packages/flutter_tools/test/src/pubspec_schema.dart b/packages/flutter_tools/test/src/pubspec_schema.dart index be2aef5e5cdc8..c5f45f89d8a7a 100644 --- a/packages/flutter_tools/test/src/pubspec_schema.dart +++ b/packages/flutter_tools/test/src/pubspec_schema.dart @@ -11,19 +11,23 @@ import 'common.dart'; /// Check if the pubspec.yaml file under the `projectDir` is valid for a plugin project. void validatePubspecForPlugin({ required String projectDir, - required String pluginClass, + String? pluginClass, + bool ffiPlugin = false, required List expectedPlatforms, List unexpectedPlatforms = const [], String? androidIdentifier, String? webFileName, }) { + assert(pluginClass != null || ffiPlugin); final FlutterManifest manifest = FlutterManifest.createFromPath('$projectDir/pubspec.yaml', fileSystem: globals.fs, logger: globals.logger)!; final YamlMap platformMaps = YamlMap.wrap(manifest.supportedPlatforms!); for (final String platform in expectedPlatforms) { expect(platformMaps[platform], isNotNull); final YamlMap platformMap = platformMaps[platform]! as YamlMap; - expect(platformMap['pluginClass'], pluginClass); + if (pluginClass != null) { + expect(platformMap['pluginClass'], pluginClass); + } if (platform == 'android') { expect(platformMap['package'], androidIdentifier); }