// 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:meta/meta.dart'; import 'package:package_config/package_config.dart'; import 'package:path/path.dart' as path; // flutter_ignore: package_path_import import 'package:pub_semver/pub_semver.dart' as semver; import 'package:yaml/yaml.dart'; import 'android/gradle.dart'; import 'base/common.dart'; import 'base/error_handling_io.dart'; import 'base/file_system.dart'; import 'base/os.dart'; import 'base/platform.dart'; import 'base/template.dart'; import 'base/version.dart'; import 'cache.dart'; import 'compute_dev_dependencies.dart'; import 'convert.dart'; import 'dart/language_version.dart'; import 'dart/package_map.dart'; import 'dart/pub.dart'; import 'features.dart'; import 'globals.dart' as globals; import 'macos/darwin_dependency_management.dart'; import 'macos/swift_package_manager.dart'; import 'platform_plugins.dart'; import 'plugins.dart'; import 'project.dart'; Future _renderTemplateToFile( String template, Object? context, File file, TemplateRenderer templateRenderer, ) async { final String renderedTemplate = templateRenderer.renderString(template, context); await file.create(recursive: true); await file.writeAsString(renderedTemplate); } Future _pluginFromPackage( String name, Uri packageRoot, Set appDependencies, { required Set devDependencies, FileSystem? fileSystem, }) async { final FileSystem fs = fileSystem ?? globals.fs; final File pubspecFile = fs.file(packageRoot.resolve('pubspec.yaml')); if (!pubspecFile.existsSync()) { return null; } Object? pubspec; try { pubspec = loadYaml(await pubspecFile.readAsString()); } on YamlException catch (err) { globals.printTrace('Failed to parse plugin manifest for $name: $err'); // Do nothing, potentially not a plugin. } if (pubspec == null || pubspec is! YamlMap) { return null; } final Object? flutterConfig = pubspec['flutter']; if (flutterConfig == null || flutterConfig is! YamlMap || !flutterConfig.containsKey('plugin')) { return null; } final String? flutterConstraintText = (pubspec['environment'] as YamlMap?)?['flutter'] as String?; final semver.VersionConstraint? flutterConstraint = flutterConstraintText == null ? null : semver.VersionConstraint.parse(flutterConstraintText); final String packageRootPath = fs.path.fromUri(packageRoot); final YamlMap? dependencies = pubspec['dependencies'] as YamlMap?; globals.printTrace('Found plugin $name at $packageRootPath'); return Plugin.fromYaml( name, packageRootPath, flutterConfig['plugin'] as YamlMap?, flutterConstraint, dependencies == null ? [] : [...dependencies.keys.cast()], fileSystem: fs, appDependencies: appDependencies, isDevDependency: devDependencies.contains(name), ); } /// Returns a list of all plugins to be registered with the provided [project]. /// /// If [throwOnError] is `true`, an empty package configuration is an error. Future> findPlugins( FlutterProject project, { bool throwOnError = true, bool? determineDevDependencies, }) async { determineDevDependencies ??= featureFlags.isExplicitPackageDependenciesEnabled; final List plugins = []; final FileSystem fs = project.directory.fileSystem; final File packageConfigFile = findPackageConfigFileOrDefault(project.directory); final PackageConfig packageConfig = await loadPackageConfigWithLogging( packageConfigFile, logger: globals.logger, throwOnError: throwOnError, ); final Set devDependencies; if (!determineDevDependencies) { devDependencies = {}; } else { devDependencies = await computeExclusiveDevDependencies( pub, logger: globals.logger, project: project, ); } for (final Package package in packageConfig.packages) { final Uri packageRoot = package.packageUriRoot.resolve('..'); final Plugin? plugin = await _pluginFromPackage( package.name, packageRoot, project.manifest.dependencies, devDependencies: devDependencies, fileSystem: fs, ); if (plugin != null) { plugins.add(plugin); } } return plugins; } /// Plugin resolution type to determine the injection mechanism. enum _PluginResolutionType { dart, nativeOrDart } // Key strings for the .flutter-plugins-dependencies file. const String _kFlutterPluginsPluginListKey = 'plugins'; const String _kFlutterPluginsNameKey = 'name'; const String _kFlutterPluginsPathKey = 'path'; const String _kFlutterPluginsDependenciesKey = 'dependencies'; const String _kFlutterPluginsHasNativeBuildKey = 'native_build'; const String _kFlutterPluginsSharedDarwinSource = 'shared_darwin_source'; const String _kFlutterPluginsDevDependencyKey = 'dev_dependency'; /// Writes the .flutter-plugins-dependencies file based on the list of plugins. /// If there aren't any plugins, then the files aren't written to disk. The resulting /// file looks something like this (order of keys is not guaranteed): /// { /// "info": "This is a generated file; do not edit or check into version control.", /// "plugins": { /// "ios": [ /// { /// "name": "test", /// "path": "test_path", /// "dependencies": [ /// "plugin-a", /// "plugin-b" /// ], /// "native_build": true /// } /// ], /// "android": [], /// "macos": [], /// "linux": [], /// "windows": [], /// "web": [] /// }, /// "dependencyGraph": [ /// { /// "name": "plugin-a", /// "dependencies": [ /// "plugin-b", /// "plugin-c" /// ] /// }, /// { /// "name": "plugin-b", /// "dependencies": [ /// "plugin-c" /// ] /// }, /// { /// "name": "plugin-c", /// "dependencies": [] /// } /// ], /// "date_created": "1970-01-01 00:00:00.000", /// "version": "0.0.0-unknown" /// } /// /// /// Finally, returns [true] if the plugins list has changed, otherwise returns [false]. bool _writeFlutterPluginsList( FlutterProject project, List plugins, { required bool swiftPackageManagerEnabledIos, required bool swiftPackageManagerEnabledMacos, }) { final File pluginsFile = project.flutterPluginsDependenciesFile; if (plugins.isEmpty) { return ErrorHandlingFileSystem.deleteIfExists(pluginsFile); } final Iterable platformKeys = [ project.ios.pluginConfigKey, project.android.pluginConfigKey, project.macos.pluginConfigKey, project.linux.pluginConfigKey, project.windows.pluginConfigKey, project.web.pluginConfigKey, ]; final Map> resolvedPlatformPlugins = _resolvePluginImplementations( plugins, pluginResolutionType: _PluginResolutionType.nativeOrDart, ); final Map pluginsMap = {}; for (final String platformKey in platformKeys) { pluginsMap[platformKey] = _createPluginMapOfPlatform( resolvedPlatformPlugins[platformKey] ?? [], platformKey, ); } final Map result = {}; result['info'] = 'This is a generated file; do not edit or check into version control.'; result[_kFlutterPluginsPluginListKey] = pluginsMap; /// The dependencyGraph object is kept for backwards compatibility, but /// should be removed once migration is complete. /// https://github.com/flutter/flutter/issues/48918 result['dependencyGraph'] = _createPluginLegacyDependencyGraph(plugins); result['date_created'] = globals.systemClock.now().toString(); result['version'] = globals.flutterVersion.frameworkVersion; result['swift_package_manager_enabled'] = { 'ios': swiftPackageManagerEnabledIos, 'macos': swiftPackageManagerEnabledMacos, }; // Only notify if the plugins list has changed. [date_created] will always be different, // [version] is not relevant for this check. final String? oldPluginsFileStringContent = _readFileContent(pluginsFile); bool pluginsChanged = true; if (oldPluginsFileStringContent != null) { try { final Object? decodedJson = jsonDecode(oldPluginsFileStringContent); if (decodedJson is Map) { final String jsonOfNewPluginsMap = jsonEncode(pluginsMap); final String jsonOfOldPluginsMap = jsonEncode(decodedJson[_kFlutterPluginsPluginListKey]); pluginsChanged = jsonOfNewPluginsMap != jsonOfOldPluginsMap; } } on FormatException catch (_) {} } final String pluginFileContent = json.encode(result); pluginsFile.writeAsStringSync(pluginFileContent, flush: true); return pluginsChanged; } /// Creates a map representation of the [plugins] for those supported by [platformKey]. /// All given [plugins] must provide an implementation for the [platformKey]. List> _createPluginMapOfPlatform(List plugins, String platformKey) { final Set pluginNames = plugins.map((Plugin plugin) => plugin.name).toSet(); final List> pluginInfo = >[]; for (final Plugin plugin in plugins) { assert( plugin.platforms[platformKey] != null, 'Plugin ${plugin.name} does not provide an implementation for $platformKey.', ); final PluginPlatform platformPlugin = plugin.platforms[platformKey]!; pluginInfo.add({ _kFlutterPluginsNameKey: plugin.name, _kFlutterPluginsPathKey: globals.fsUtils.escapePath(plugin.path), if (platformPlugin is DarwinPlugin && (platformPlugin as DarwinPlugin).sharedDarwinSource) _kFlutterPluginsSharedDarwinSource: (platformPlugin as DarwinPlugin).sharedDarwinSource, if (platformPlugin is NativeOrDartPlugin) _kFlutterPluginsHasNativeBuildKey: (platformPlugin as NativeOrDartPlugin).hasMethodChannel() || (platformPlugin as NativeOrDartPlugin).hasFfi(), _kFlutterPluginsDependenciesKey: [...plugin.dependencies.where(pluginNames.contains)], _kFlutterPluginsDevDependencyKey: plugin.isDevDependency, }); } return pluginInfo; } List _createPluginLegacyDependencyGraph(List plugins) { final List directAppDependencies = []; final Set pluginNames = plugins.map((Plugin plugin) => plugin.name).toSet(); for (final Plugin plugin in plugins) { directAppDependencies.add({ 'name': plugin.name, // Extract the plugin dependencies which happen to be plugins. 'dependencies': [...plugin.dependencies.where(pluginNames.contains)], }); } return directAppDependencies; } // The .flutter-plugins file will be DEPRECATED in favor of .flutter-plugins-dependencies. // TODO(franciscojma): Remove this method once deprecated. // https://github.com/flutter/flutter/issues/48918 // /// Writes the .flutter-plugins files based on the list of plugins. /// If there aren't any plugins, then the files aren't written to disk. /// /// Finally, returns [true] if .flutter-plugins has changed, otherwise returns [false]. bool _writeFlutterPluginsListLegacy(FlutterProject project, List plugins) { final File pluginsFile = project.flutterPluginsFile; if (plugins.isEmpty) { return ErrorHandlingFileSystem.deleteIfExists(pluginsFile); } const String info = 'This is a generated file; do not edit or check into version control.'; final StringBuffer flutterPluginsBuffer = StringBuffer('# $info\n'); for (final Plugin plugin in plugins) { flutterPluginsBuffer.write('${plugin.name}=${globals.fsUtils.escapePath(plugin.path)}\n'); } final String? oldPluginFileContent = _readFileContent(pluginsFile); final String pluginFileContent = flutterPluginsBuffer.toString(); pluginsFile.writeAsStringSync(pluginFileContent, flush: true); return oldPluginFileContent != _readFileContent(pluginsFile); } /// Returns the contents of [File] or [null] if that file does not exist. String? _readFileContent(File file) { return file.existsSync() ? file.readAsStringSync() : null; } const String _androidPluginRegistryTemplateNewEmbedding = ''' package io.flutter.plugins; import androidx.annotation.Keep; import androidx.annotation.NonNull; import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; /** * Generated file. Do not edit. * This file is generated by the Flutter tool based on the * plugins that support the Android platform. */ @Keep public final class GeneratedPluginRegistrant { private static final String TAG = "GeneratedPluginRegistrant"; public static void registerWith(@NonNull FlutterEngine flutterEngine) { {{#methodChannelPlugins}} {{#supportsEmbeddingV2}} try { flutterEngine.getPlugins().add(new {{package}}.{{class}}()); } catch (Exception e) { Log.e(TAG, "Error registering plugin {{name}}, {{package}}.{{class}}", e); } {{/supportsEmbeddingV2}} {{/methodChannelPlugins}} } } '''; List> _extractPlatformMaps(Iterable plugins, String type) { return >[ for (final Plugin plugin in plugins) if (plugin.platforms[type] case final PluginPlatform platformPlugin) platformPlugin.toMap(), ]; } Future _writeAndroidPluginRegistrant(FlutterProject project, List plugins) async { final List methodChannelPlugins = _filterMethodChannelPlugins( plugins, AndroidPlugin.kConfigKey, ); final List> androidPlugins = _extractPlatformMaps( methodChannelPlugins, AndroidPlugin.kConfigKey, ); final Map templateContext = { 'methodChannelPlugins': androidPlugins, 'androidX': isAppUsingAndroidX(project.android.hostAppGradleRoot), }; final String javaSourcePath = globals.fs.path.join( project.android.pluginRegistrantHost.path, 'src', 'main', 'java', ); final String registryPath = globals.fs.path.join( javaSourcePath, 'io', 'flutter', 'plugins', 'GeneratedPluginRegistrant.java', ); const String templateContent = _androidPluginRegistryTemplateNewEmbedding; globals.printTrace('Generating $registryPath'); await _renderTemplateToFile( templateContent, templateContext, globals.fs.file(registryPath), globals.templateRenderer, ); } const String _objcPluginRegistryHeaderTemplate = ''' // // Generated file. Do not edit. // // clang-format off #ifndef GeneratedPluginRegistrant_h #define GeneratedPluginRegistrant_h #import <{{framework}}/{{framework}}.h> NS_ASSUME_NONNULL_BEGIN @interface GeneratedPluginRegistrant : NSObject + (void)registerWithRegistry:(NSObject*)registry; @end NS_ASSUME_NONNULL_END #endif /* GeneratedPluginRegistrant_h */ '''; const String _objcPluginRegistryImplementationTemplate = ''' // // Generated file. Do not edit. // // clang-format off #import "GeneratedPluginRegistrant.h" {{#methodChannelPlugins}} #if __has_include(<{{name}}/{{class}}.h>) #import <{{name}}/{{class}}.h> #else @import {{name}}; #endif {{/methodChannelPlugins}} @implementation GeneratedPluginRegistrant + (void)registerWithRegistry:(NSObject*)registry { {{#methodChannelPlugins}} [{{prefix}}{{class}} registerWithRegistrar:[registry registrarForPlugin:@"{{prefix}}{{class}}"]]; {{/methodChannelPlugins}} } @end '''; const String _swiftPluginRegistryTemplate = ''' // // Generated file. Do not edit. // import {{framework}} import Foundation {{#methodChannelPlugins}} import {{name}} {{/methodChannelPlugins}} func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { {{#methodChannelPlugins}} {{class}}.register(with: registry.registrar(forPlugin: "{{class}}")) {{/methodChannelPlugins}} } '''; const String _pluginRegistrantPodspecTemplate = ''' # # Generated file, do not edit. # Pod::Spec.new do |s| s.name = 'FlutterPluginRegistrant' s.version = '0.0.1' s.summary = 'Registers plugins with your Flutter app' s.description = <<-DESC Depends on all your plugins, and provides a function to register them. DESC s.homepage = 'https://flutter.dev' s.license = { :type => 'BSD' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.{{os}}.deployment_target = '{{deploymentTarget}}' s.source_files = "Classes", "Classes/**/*.{h,m}" s.source = { :path => '.' } s.public_header_files = './Classes/**/*.h' s.static_framework = true s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.dependency '{{framework}}' {{#methodChannelPlugins}} s.dependency '{{name}}' {{/methodChannelPlugins}} end '''; const String _noopDartPluginRegistryTemplate = ''' // Flutter web plugin registrant file. // // Generated file. Do not edit. // // ignore_for_file: type=lint void registerPlugins() {} '''; const String _dartPluginRegistryTemplate = ''' // Flutter web plugin registrant file. // // Generated file. Do not edit. // // @dart = 2.13 // ignore_for_file: type=lint {{#methodChannelPlugins}} import 'package:{{name}}/{{file}}'; {{/methodChannelPlugins}} import 'package:flutter_web_plugins/flutter_web_plugins.dart'; void registerPlugins([final Registrar? pluginRegistrar]) { final Registrar registrar = pluginRegistrar ?? webPluginRegistrar; {{#methodChannelPlugins}} {{class}}.registerWith(registrar); {{/methodChannelPlugins}} registrar.registerMessageHandler(); } '''; const String _cppPluginRegistryHeaderTemplate = ''' // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ '''; const String _cppPluginRegistryImplementationTemplate = ''' // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" {{#methodChannelPlugins}} #include <{{name}}/{{filename}}.h> {{/methodChannelPlugins}} void RegisterPlugins(flutter::PluginRegistry* registry) { {{#methodChannelPlugins}} {{class}}RegisterWithRegistrar( registry->GetRegistrarForPlugin("{{class}}")); {{/methodChannelPlugins}} } '''; const String _linuxPluginRegistryHeaderTemplate = ''' // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void fl_register_plugins(FlPluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ '''; const String _linuxPluginRegistryImplementationTemplate = ''' // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" {{#methodChannelPlugins}} #include <{{name}}/{{filename}}.h> {{/methodChannelPlugins}} void fl_register_plugins(FlPluginRegistry* registry) { {{#methodChannelPlugins}} g_autoptr(FlPluginRegistrar) {{name}}_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "{{class}}"); {{filename}}_register_with_registrar({{name}}_registrar); {{/methodChannelPlugins}} } '''; const String _pluginCmakefileTemplate = r''' # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST {{#methodChannelPlugins}} {{name}} {{/methodChannelPlugins}} ) list(APPEND FLUTTER_FFI_PLUGIN_LIST {{#ffiPlugins}} {{name}} {{/ffiPlugins}} ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory({{pluginsDir}}/${plugin}/{{os}} plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 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''' try { {{dartClass}}.registerWith(); } catch (err) { print( '`{{pluginName}}` threw an error: $err. ' 'The app may not function as expected until you remove this plugin from pubspec.yaml' ); } '''; // TODO(egarciad): Evaluate merging the web and non-web plugin registry templates. // https://github.com/flutter/flutter/issues/80406 const String _dartPluginRegistryForNonWebTemplate = ''' // // Generated file. Do not edit. // This file is generated from template in file `flutter_tools/lib/src/flutter_plugins.dart`. // // @dart = {{dartLanguageVersion}} import 'dart:io'; // flutter_ignore: dart_io_import. {{#android}} import 'package:{{pluginName}}/{{dartFileName}}'; {{/android}} {{#ios}} import 'package:{{pluginName}}/{{dartFileName}}'; {{/ios}} {{#linux}} import 'package:{{pluginName}}/{{dartFileName}}'; {{/linux}} {{#macos}} import 'package:{{pluginName}}/{{dartFileName}}'; {{/macos}} {{#windows}} import 'package:{{pluginName}}/{{dartFileName}}'; {{/windows}} @pragma('vm:entry-point') class _PluginRegistrant { @pragma('vm:entry-point') static void register() { if (Platform.isAndroid) { {{#android}} $_dartPluginRegisterWith {{/android}} } else if (Platform.isIOS) { {{#ios}} $_dartPluginRegisterWith {{/ios}} } else if (Platform.isLinux) { {{#linux}} $_dartPluginRegisterWith {{/linux}} } else if (Platform.isMacOS) { {{#macos}} $_dartPluginRegisterWith {{/macos}} } else if (Platform.isWindows) { {{#windows}} $_dartPluginRegisterWith {{/windows}} } } } '''; Future _writeIOSPluginRegistrant(FlutterProject project, List plugins) async { final List methodChannelPlugins = _filterMethodChannelPlugins( plugins, IOSPlugin.kConfigKey, ); final List> iosPlugins = _extractPlatformMaps( methodChannelPlugins, IOSPlugin.kConfigKey, ); final Map context = { 'os': 'ios', 'deploymentTarget': '12.0', 'framework': 'Flutter', 'methodChannelPlugins': iosPlugins, }; if (project.isModule) { final Directory registryDirectory = project.ios.pluginRegistrantHost; await _renderTemplateToFile( _pluginRegistrantPodspecTemplate, context, registryDirectory.childFile('FlutterPluginRegistrant.podspec'), globals.templateRenderer, ); } await _renderTemplateToFile( _objcPluginRegistryHeaderTemplate, context, project.ios.pluginRegistrantHeader, globals.templateRenderer, ); await _renderTemplateToFile( _objcPluginRegistryImplementationTemplate, context, project.ios.pluginRegistrantImplementation, globals.templateRenderer, ); } /// The relative path from a project's main CMake file to the plugin symlink /// directory to use in the generated plugin CMake file. /// /// Because the generated file is checked in, it can't use absolute paths. It is /// designed to be included by the main CMakeLists.txt, so it relative to /// that file, rather than the generated file. String _cmakeRelativePluginSymlinkDirectoryPath(CmakeBasedProject project) { final FileSystem fileSystem = project.pluginSymlinkDirectory.fileSystem; final String makefileDirPath = project.cmakeFile.parent.absolute.path; // CMake always uses posix-style path separators, regardless of the platform. final path.Context cmakePathContext = path.Context(style: path.Style.posix); final List relativePathComponents = fileSystem.path.split( fileSystem.path.relative(project.pluginSymlinkDirectory.absolute.path, from: makefileDirPath), ); return cmakePathContext.joinAll(relativePathComponents); } Future _writeLinuxPluginFiles(FlutterProject project, List plugins) async { 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', 'methodChannelPlugins': linuxMethodChannelPlugins, 'ffiPlugins': linuxFfiPlugins, 'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.linux), }; await _writeLinuxPluginRegistrant(project.linux.managedDirectory, context); await _writePluginCmakefile( project.linux.generatedPluginCmakeFile, context, globals.templateRenderer, ); } Future _writeLinuxPluginRegistrant( Directory destination, Map templateContext, ) async { await _renderTemplateToFile( _linuxPluginRegistryHeaderTemplate, templateContext, destination.childFile('generated_plugin_registrant.h'), globals.templateRenderer, ); await _renderTemplateToFile( _linuxPluginRegistryImplementationTemplate, templateContext, destination.childFile('generated_plugin_registrant.cc'), globals.templateRenderer, ); } Future _writePluginCmakefile( File destinationFile, Map templateContext, TemplateRenderer templateRenderer, ) async { await _renderTemplateToFile( _pluginCmakefileTemplate, templateContext, destinationFile, templateRenderer, ); } Future _writeMacOSPluginRegistrant(FlutterProject project, List plugins) async { final List methodChannelPlugins = _filterMethodChannelPlugins( plugins, MacOSPlugin.kConfigKey, ); final List> macosMethodChannelPlugins = _extractPlatformMaps( methodChannelPlugins, MacOSPlugin.kConfigKey, ); final Map context = { 'os': 'macos', 'framework': 'FlutterMacOS', 'methodChannelPlugins': macosMethodChannelPlugins, }; await _renderTemplateToFile( _swiftPluginRegistryTemplate, context, project.macos.managedDirectory.childFile('GeneratedPluginRegistrant.swift'), globals.templateRenderer, ); } /// 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).hasMethodChannel(); } // Not all platforms have the ability to create Dart-only plugins. Therefore, any plugin that doesn't // implement NativeOrDartPlugin is always native. return true; }).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) { final PluginPlatform? platformPlugin = element.platforms[platformKey]; if (platformPlugin == null) { return false; } assert(platformPlugin is VariantPlatformPlugin); return (platformPlugin as VariantPlatformPlugin).supportedVariants.contains(variant); }).toList(); } @visibleForTesting Future writeWindowsPluginFiles( FlutterProject project, List plugins, TemplateRenderer templateRenderer, ) async { 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', 'methodChannelPlugins': windowsMethodChannelPlugins, 'ffiPlugins': windowsFfiPlugins, 'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.windows), }; await _writeCppPluginRegistrant(project.windows.managedDirectory, context, templateRenderer); await _writePluginCmakefile(project.windows.generatedPluginCmakeFile, context, templateRenderer); } Future _writeCppPluginRegistrant( Directory destination, Map templateContext, TemplateRenderer templateRenderer, ) async { await _renderTemplateToFile( _cppPluginRegistryHeaderTemplate, templateContext, destination.childFile('generated_plugin_registrant.h'), templateRenderer, ); await _renderTemplateToFile( _cppPluginRegistryImplementationTemplate, templateContext, destination.childFile('generated_plugin_registrant.cc'), templateRenderer, ); } Future _writeWebPluginRegistrant( FlutterProject project, List plugins, Directory destination, ) async { final List> webPlugins = _extractPlatformMaps(plugins, WebPlugin.kConfigKey); final Map context = {'methodChannelPlugins': webPlugins}; final File pluginFile = destination.childFile('web_plugin_registrant.dart'); final String template = webPlugins.isEmpty ? _noopDartPluginRegistryTemplate : _dartPluginRegistryTemplate; await _renderTemplateToFile(template, context, pluginFile, globals.templateRenderer); } /// For each platform that uses them, creates symlinks within the platform /// directory to each plugin used on that platform. /// /// If |force| is true, the symlinks will be recreated, otherwise they will /// be created only if missing. /// /// This uses [project.flutterPluginsDependenciesFile], so it should only be /// run after [refreshPluginsList] has been run since the last plugin change. void createPluginSymlinks( FlutterProject project, { bool force = false, @visibleForTesting FeatureFlags? featureFlagsOverride, }) { final FeatureFlags localFeatureFlags = featureFlagsOverride ?? featureFlags; Map? platformPlugins; final String? pluginFileContent = _readFileContent(project.flutterPluginsDependenciesFile); if (pluginFileContent != null) { final Map? pluginInfo = json.decode(pluginFileContent) as Map?; platformPlugins = pluginInfo?[_kFlutterPluginsPluginListKey] as Map?; } platformPlugins ??= {}; if (localFeatureFlags.isWindowsEnabled && project.windows.existsSync()) { _createPlatformPluginSymlinks( project.windows.pluginSymlinkDirectory, platformPlugins[project.windows.pluginConfigKey] as List?, force: force, ); } if (localFeatureFlags.isLinuxEnabled && project.linux.existsSync()) { _createPlatformPluginSymlinks( project.linux.pluginSymlinkDirectory, platformPlugins[project.linux.pluginConfigKey] as List?, force: force, ); } } /// Handler for symlink failures which provides specific instructions for known /// failure cases. @visibleForTesting void handleSymlinkException( FileSystemException e, { required Platform platform, required OperatingSystemUtils os, required String destination, required String source, }) { if (platform.isWindows) { // ERROR_ACCESS_DENIED if (e.osError?.errorCode == 5) { throwToolExit( 'ERROR_ACCESS_DENIED file system exception thrown while trying to ' 'create a symlink from $source to $destination', ); } // ERROR_PRIVILEGE_NOT_HELD, user cannot symlink if (e.osError?.errorCode == 1314) { final String? versionString = RegExp(r'[\d.]+').firstMatch(os.name)?.group(0); final Version? version = Version.parse(versionString); // Windows 10 14972 is the oldest version that allows creating symlinks // just by enabling developer mode; before that it requires running the // terminal as Administrator. // https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10/ final String instructions = (version != null && version >= Version(10, 0, 14972)) ? 'Please enable Developer Mode in your system settings. Run\n' ' start ms-settings:developers\n' 'to open settings.' : 'You must build from a terminal run as administrator.'; throwToolExit('Building with plugins requires symlink support.\n\n$instructions'); } // ERROR_INVALID_FUNCTION, trying to link across drives, which is not supported if (e.osError?.errorCode == 1) { throwToolExit( 'Creating symlink from $source to $destination failed with ' 'ERROR_INVALID_FUNCTION. Try moving your Flutter project to the same ' 'drive as your Flutter SDK.', ); } } } /// Creates [symlinkDirectory] containing symlinks to each plugin listed in [platformPlugins]. /// /// If [force] is true, the directory will be created only if missing. void _createPlatformPluginSymlinks( Directory symlinkDirectory, List? platformPlugins, { bool force = false, }) { if (force) { // Start fresh to avoid stale links. ErrorHandlingFileSystem.deleteIfExists(symlinkDirectory, recursive: true); } symlinkDirectory.createSync(recursive: true); if (platformPlugins == null) { return; } for (final Map pluginInfo in platformPlugins.cast>()) { final String name = pluginInfo[_kFlutterPluginsNameKey]! as String; final String path = pluginInfo[_kFlutterPluginsPathKey]! as String; final Link link = symlinkDirectory.childLink(name); if (link.existsSync()) { continue; } try { link.createSync(path); } on FileSystemException catch (e) { handleSymlinkException( e, platform: globals.platform, os: globals.os, destination: link.path, source: path, ); rethrow; } } } /// Rewrites the `.flutter-plugins` file of [project] based on the plugin /// dependencies declared in `pubspec.yaml`. /// /// Assumes `pub get` has been executed since last change to `pubspec.yaml`. /// /// Unless explicitly specified, [determineDevDependencies] is disabled by /// default; if set to `true`, plugins that are development-only dependencies /// may be labeled or, depending on the platform, omitted from metadata or /// platform-specific artifacts. Future refreshPluginsList( FlutterProject project, { bool iosPlatform = false, bool macOSPlatform = false, bool forceCocoaPodsOnly = false, bool determineDevDependencies = false, bool? generateLegacyPlugins, }) async { final List plugins = await findPlugins( project, determineDevDependencies: determineDevDependencies, ); // Sort the plugins by name to keep ordering stable in generated files. plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name)); // TODO(matanlurey): Remove once migration is complete. // Write the legacy plugin files to avoid breaking existing apps. generateLegacyPlugins ??= !featureFlags.isExplicitPackageDependenciesEnabled; final bool legacyChanged = generateLegacyPlugins && _writeFlutterPluginsListLegacy(project, plugins); bool swiftPackageManagerEnabledIos = false; bool swiftPackageManagerEnabledMacos = false; if (!forceCocoaPodsOnly) { if (iosPlatform) { swiftPackageManagerEnabledIos = project.ios.usesSwiftPackageManager; } if (macOSPlatform) { swiftPackageManagerEnabledMacos = project.macos.usesSwiftPackageManager; } } final bool changed = _writeFlutterPluginsList( project, plugins, swiftPackageManagerEnabledIos: swiftPackageManagerEnabledIos, swiftPackageManagerEnabledMacos: swiftPackageManagerEnabledMacos, ); if (changed || legacyChanged || forceCocoaPodsOnly) { createPluginSymlinks(project, force: true); if (iosPlatform) { globals.cocoaPods?.invalidatePodInstallOutput(project.ios); } if (macOSPlatform) { globals.cocoaPods?.invalidatePodInstallOutput(project.macos); } } } /// Injects plugins found in `pubspec.yaml` into the platform-specific projects /// only at build-time. /// /// This method is similar to [injectPlugins], but used only for platforms where /// the plugin files are not required when the app is created (currently: Web). /// /// This method will create files in the temporary flutter build directory /// specified by `destination`. /// /// In the Web platform, `destination` can point to a real filesystem (`flutter build`) /// or an in-memory filesystem (`flutter run`). /// /// This method is also used by [WebProject.ensureReadyForPlatformSpecificTooling] /// to inject a copy of the plugin registrant for web into .dart_tool/dartpad so /// dartpad can get the plugin registrant without needing to build the complete /// project. See: https://github.com/dart-lang/dart-services/pull/874 Future injectBuildTimePluginFilesForWebPlatform( FlutterProject project, { required Directory destination, }) async { final List plugins = await findPlugins(project); final Map> pluginsByPlatform = _resolvePluginImplementations( plugins, pluginResolutionType: _PluginResolutionType.nativeOrDart, ); await _writeWebPluginRegistrant(project, pluginsByPlatform[WebPlugin.kConfigKey]!, destination); } /// Injects plugins found in `pubspec.yaml` into the platform-specific projects. /// /// The injected files are required by the Flutter app as soon as possible, so /// it can be built. /// /// Files written by this method end up in platform-specific locations that are /// configured by each [FlutterProject] subclass (except for the Web). /// /// Web tooling uses [injectBuildTimePluginFilesForWebPlatform] instead, which places files in the /// current build (temp) directory, and doesn't modify the users' working copy. /// /// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`. Future injectPlugins( FlutterProject project, { bool androidPlatform = false, bool iosPlatform = false, bool linuxPlatform = false, bool macOSPlatform = false, bool windowsPlatform = false, DarwinDependencyManagement? darwinDependencyManagement, bool? releaseMode, }) async { List plugins = await findPlugins(project); if (releaseMode ?? false) { plugins = plugins.where((Plugin p) => !p.isDevDependency).toList(); } final Map> pluginsByPlatform = _resolvePluginImplementations( plugins, pluginResolutionType: _PluginResolutionType.nativeOrDart, ); if (androidPlatform) { await _writeAndroidPluginRegistrant(project, pluginsByPlatform[AndroidPlugin.kConfigKey]!); } if (iosPlatform) { await _writeIOSPluginRegistrant(project, pluginsByPlatform[IOSPlugin.kConfigKey]!); } if (linuxPlatform) { await _writeLinuxPluginFiles(project, pluginsByPlatform[LinuxPlugin.kConfigKey]!); } if (macOSPlatform) { await _writeMacOSPluginRegistrant(project, pluginsByPlatform[MacOSPlugin.kConfigKey]!); } if (windowsPlatform) { await writeWindowsPluginFiles( project, pluginsByPlatform[WindowsPlugin.kConfigKey]!, globals.templateRenderer, ); } if (iosPlatform || macOSPlatform) { final DarwinDependencyManagement darwinDependencyManagerSetup = darwinDependencyManagement ?? DarwinDependencyManagement( project: project, plugins: plugins, cocoapods: globals.cocoaPods!, swiftPackageManager: SwiftPackageManager( fileSystem: globals.fs, templateRenderer: globals.templateRenderer, ), fileSystem: globals.fs, logger: globals.logger, ); if (iosPlatform) { await darwinDependencyManagerSetup.setUp(platform: SupportedPlatform.ios); } if (macOSPlatform) { await darwinDependencyManagerSetup.setUp(platform: SupportedPlatform.macos); } } } /// Returns whether the specified Flutter [project] has any plugin dependencies. /// /// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`. bool hasPlugins(FlutterProject project) { return _readFileContent(project.flutterPluginsDependenciesFile) != null; } /// Resolves the plugin implementations for all platforms. /// /// * If there is only one dependency on a package that implements the /// frontend plugin for the current platform, use that. /// * If there is a single direct dependency on a package that implements the /// frontend plugin for the current platform, use that. /// * If there is no direct dependency on a package that implements the /// frontend plugin, but there is a default for the current platform, /// use that. /// * Else fail. /// /// For more details, https://flutter.dev/go/federated-plugins. /// /// If [selectDartPluginsOnly] is enabled, only Dart plugin implementations are /// considered. Else, native and Dart plugin implementations are considered. List resolvePlatformImplementation( List plugins, { required bool selectDartPluginsOnly, }) { final Map> resolution = _resolvePluginImplementations( plugins, pluginResolutionType: selectDartPluginsOnly ? _PluginResolutionType.dart : _PluginResolutionType.nativeOrDart, ); return resolution.entries.expand((MapEntry> entry) { return entry.value.map((Plugin plugin) { return PluginInterfaceResolution(plugin: plugin, platform: entry.key); }); }).toList(); } /// Resolves the plugin implementations for all platforms, /// see [resolvePlatformImplementation]. /// /// Only plugins which provide the according platform implementation are returned. Map> _resolvePluginImplementations( List plugins, { required _PluginResolutionType pluginResolutionType, }) { final Map> pluginsByPlatform = >{ AndroidPlugin.kConfigKey: [], IOSPlugin.kConfigKey: [], LinuxPlugin.kConfigKey: [], MacOSPlugin.kConfigKey: [], WindowsPlugin.kConfigKey: [], WebPlugin.kConfigKey: [], }; bool hasPluginPubspecError = false; bool hasResolutionError = false; for (final String platformKey in pluginsByPlatform.keys) { final ( List platformPluginResolutions, bool hasPlatformPluginPubspecError, bool hasPlatformResolutionError, ) = _resolvePluginImplementationsByPlatform( plugins, platformKey, pluginResolutionType: pluginResolutionType, ); if (hasPlatformPluginPubspecError) { hasPluginPubspecError = true; } else if (hasPlatformResolutionError) { hasResolutionError = true; } else { pluginsByPlatform[platformKey] = platformPluginResolutions; } } if (hasPluginPubspecError) { throwToolExit('Please resolve the plugin pubspec errors'); } if (hasResolutionError) { throwToolExit('Please resolve the plugin implementation selection errors'); } return pluginsByPlatform; } /// Resolves the plugins for the given [platformKey] (Dart-only or native /// implementations). (List pluginImplementations, bool hasPluginPubspecError, bool hasResolutionError) _resolvePluginImplementationsByPlatform( Iterable plugins, String platformKey, { _PluginResolutionType pluginResolutionType = _PluginResolutionType.nativeOrDart, }) { bool hasPluginPubspecError = false; bool hasResolutionError = false; // Key: the plugin name, value: the list of plugin candidates for the implementation of [platformKey]. final Map> pluginImplCandidates = >{}; // Key: the plugin name, value: the plugin of the default implementation of [platformKey]. final Map defaultImplementations = {}; for (final Plugin plugin in plugins) { final String? error = _validatePlugin( plugin, platformKey, pluginResolutionType: pluginResolutionType, ); if (error != null) { globals.printError(error); hasPluginPubspecError = true; continue; } final String? implementsPluginName = _getImplementedPlugin( plugin, platformKey, pluginResolutionType: pluginResolutionType, ); final String? defaultImplPluginName = _getDefaultImplPlugin( plugin, platformKey, pluginResolutionType: pluginResolutionType, ); if (defaultImplPluginName != null) { final Plugin? defaultPackage = plugins.where((Plugin plugin) => plugin.name == defaultImplPluginName).firstOrNull; if (defaultPackage != null) { if (_hasPluginInlineImpl( defaultPackage, platformKey, pluginResolutionType: _PluginResolutionType.nativeOrDart, )) { if (pluginResolutionType == _PluginResolutionType.nativeOrDart || _hasPluginInlineImpl( defaultPackage, platformKey, pluginResolutionType: pluginResolutionType, )) { // Each plugin can only have one default implementation for this [platformKey]. defaultImplementations[plugin.name] = defaultPackage; // No need to add the default plugin to `pluginImplCandidates`, // as if the plugin is present and provides an implementation // it is added via `_getImplementedPlugin`. } } else { // Only warn, if neither an implementation for native nor for Dart is given. globals.printWarning( 'Package ${plugin.name}:$platformKey references $defaultImplPluginName:$platformKey as the default plugin, but it does not provide an inline implementation.\n' 'Ask the maintainers of ${plugin.name} to either avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName` ' 'or add an inline implementation to $defaultImplPluginName via `platforms: $platformKey:` `pluginClass` or `dartPluginClass`.\n', ); } } else { globals.printWarning( 'Package ${plugin.name}:$platformKey references $defaultImplPluginName:$platformKey as the default plugin, but the package does not exist, or is not a plugin package.\n' 'Ask the maintainers of ${plugin.name} to either avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName` ' 'or create a plugin named $defaultImplPluginName.\n', ); } } if (implementsPluginName != null) { pluginImplCandidates.putIfAbsent(implementsPluginName, () => []); pluginImplCandidates[implementsPluginName]!.add(plugin); } } // Key: the plugin name, value: the plugin which provides an implementation for [platformKey]. final Map pluginResolution = {}; // Now resolve all the possible resolutions to a single option for each // plugin, or throw if that's not possible. for (final MapEntry> implCandidatesEntry in pluginImplCandidates.entries) { final (Plugin? resolution, String? error) = _resolveImplementationOfPlugin( platformKey: platformKey, pluginResolutionType: pluginResolutionType, pluginName: implCandidatesEntry.key, candidates: implCandidatesEntry.value, defaultPackage: defaultImplementations[implCandidatesEntry.key], ); if (error != null) { globals.printError(error); hasResolutionError = true; } else if (resolution != null) { pluginResolution[implCandidatesEntry.key] = resolution; } } // Sort the plugins by name to keep ordering stable in generated files. final List pluginImplementations = pluginResolution.values.toList() ..sort((Plugin left, Plugin right) => left.name.compareTo(right.name)); return (pluginImplementations, hasPluginPubspecError, hasResolutionError); } /// Validates conflicting plugin parameters in pubspec, such as /// `dartPluginClass`, `default_package` and `implements`. /// /// Returns an error, if failing. String? _validatePlugin( Plugin plugin, String platformKey, { required _PluginResolutionType pluginResolutionType, }) { final String? implementsPackage = plugin.implementsPackage; final String? defaultImplPluginName = plugin.defaultPackagePlatforms[platformKey]; if (plugin.name == implementsPackage && plugin.name == defaultImplPluginName) { // Allow self implementing and self as platform default. return null; } if (defaultImplPluginName != null) { if (implementsPackage != null && implementsPackage.isNotEmpty) { return 'Plugin ${plugin.name}:$platformKey provides an implementation for $implementsPackage ' 'and also references a default implementation for $defaultImplPluginName, which is currently not supported. ' 'Ask the maintainers of ${plugin.name} to either remove the implementation via `implements: $implementsPackage` ' 'or avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName`.\n'; } if (_hasPluginInlineImpl(plugin, platformKey, pluginResolutionType: pluginResolutionType)) { return 'Plugin ${plugin.name}:$platformKey which provides an inline implementation ' 'cannot also reference a default implementation for $defaultImplPluginName. ' 'Ask the maintainers of ${plugin.name} to either remove the implementation via `platforms: $platformKey:${pluginResolutionType == _PluginResolutionType.dart ? ' dartPluginClass' : '` `pluginClass` or `dartPluginClass'}` ' 'or avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName`.\n'; } } return null; } /// Determine if this [plugin] serves as implementation for an app-facing /// package for the given platform [platformKey]. /// /// If so, return the package name, which the [plugin] implements. /// /// Options: /// * The [plugin] (e.g. 'url_launcher_linux') serves as implementation for /// an app-facing package (e.g. 'url_launcher'). /// * The [plugin] (e.g. 'url_launcher') implements itself and then also /// serves as its own default implementation. /// * The [plugin] does not provide an implementation. String? _getImplementedPlugin( Plugin plugin, String platformKey, { _PluginResolutionType pluginResolutionType = _PluginResolutionType.nativeOrDart, }) { if (_hasPluginInlineImpl(plugin, platformKey, pluginResolutionType: pluginResolutionType)) { // Only can serve, if the plugin has an inline implementation. final String? implementsPackage = plugin.implementsPackage; if (implementsPackage != null && implementsPackage.isNotEmpty) { // The inline plugin implements another package. return implementsPackage; } if (pluginResolutionType == _PluginResolutionType.nativeOrDart || _isEligibleDartSelfImpl(plugin, platformKey)) { // The inline plugin implements itself. return plugin.name; } } return null; } /// Determine if this [plugin] (or package) references a default plugin with an /// implementation for the given platform [platformKey]. /// /// If so, return the plugin name, which provides the default implementation. /// /// Options: /// * The [plugin] (e.g. 'url_launcher') references a default implementation /// (e.g. 'url_launcher_linux'). /// * The [plugin] (e.g. 'url_launcher') implements itself and then also /// serves as its own default implementation. /// * The [plugin] does not reference a default implementation. String? _getDefaultImplPlugin( Plugin plugin, String platformKey, { _PluginResolutionType pluginResolutionType = _PluginResolutionType.nativeOrDart, }) { final String? defaultImplPluginName = plugin.defaultPackagePlatforms[platformKey]; if (defaultImplPluginName != null) { return defaultImplPluginName; } if (_hasPluginInlineImpl(plugin, platformKey, pluginResolutionType: pluginResolutionType) && (pluginResolutionType == _PluginResolutionType.nativeOrDart || _isEligibleDartSelfImpl(plugin, platformKey))) { // The inline plugin serves as its own default implementation. return plugin.name; } return null; } /// Determine if the [plugin]'s inline dart implementation for the /// [platformKey] is eligible to serve as its own default. /// /// An app-facing package (i.e., one with no 'implements') with an /// inline implementation should be its own default implementation. /// Desktop platforms originally did not work that way, and enabling /// it unconditionally would break existing published plugins, so /// only treat it as such if either: /// - the platform is not desktop, or /// - the plugin requires at least Flutter 2.11 (when this opt-in logic /// was added), so that existing plugins continue to work. /// See https://github.com/flutter/flutter/issues/87862 for details. bool _isEligibleDartSelfImpl(Plugin plugin, String platformKey) { final bool isDesktop = platformKey == 'linux' || platformKey == 'macos' || platformKey == 'windows'; final semver.VersionConstraint? flutterConstraint = plugin.flutterConstraint; final semver.Version? minFlutterVersion = flutterConstraint != null && flutterConstraint is semver.VersionRange ? flutterConstraint.min : null; final bool hasMinVersionForImplementsRequirement = minFlutterVersion != null && minFlutterVersion.compareTo(semver.Version(2, 11, 0)) >= 0; return !isDesktop || hasMinVersionForImplementsRequirement; } /// Determine if the plugin provides an inline implementation. bool _hasPluginInlineImpl( Plugin plugin, String platformKey, { required _PluginResolutionType pluginResolutionType, }) { return pluginResolutionType == _PluginResolutionType.nativeOrDart && plugin.platforms[platformKey] != null || pluginResolutionType == _PluginResolutionType.dart && _hasPluginInlineDartImpl(plugin, platformKey); } /// Determine if the plugin provides an inline Dart implementation. bool _hasPluginInlineDartImpl(Plugin plugin, String platformKey) { final DartPluginClassAndFilePair? platformInfo = plugin.pluginDartClassPlatforms[platformKey]; return platformInfo != null && platformInfo.dartClass != 'none'; } /// Get the resolved plugin [resolution] from the [candidates] serving as implementation for /// [pluginName]. /// /// Returns an [error] string, if failing. (Plugin? resolution, String? error) _resolveImplementationOfPlugin({ required String platformKey, required _PluginResolutionType pluginResolutionType, required String pluginName, required List candidates, Plugin? defaultPackage, }) { // If there's only one candidate, use it. if (candidates.length == 1) { return (candidates.first, null); } // Next, try direct dependencies of the resolving application. final Iterable directDependencies = candidates.where((Plugin plugin) { return plugin.isDirectDependency; }); if (directDependencies.isNotEmpty) { if (directDependencies.length > 1) { // Allow overriding an app-facing package with an inline implementation (which is a direct dependency) // with another direct dependency which implements the app-facing package. final Iterable implementingPackage = directDependencies.where( (Plugin plugin) => plugin.implementsPackage != null && plugin.implementsPackage!.isNotEmpty, ); final Set appFacingPackage = directDependencies.toSet()..removeAll(implementingPackage); if (implementingPackage.length == 1 && appFacingPackage.length == 1) { return (implementingPackage.first, null); } return ( null, 'Plugin $pluginName:$platformKey has conflicting direct dependency implementations:\n' '${directDependencies.map((Plugin plugin) => ' ${plugin.name}\n').join()}' 'To fix this issue, remove all but one of these dependencies from pubspec.yaml.\n', ); } else { return (directDependencies.first, null); } } // Next, defer to the default implementation if there is one. if (defaultPackage != null && candidates.contains(defaultPackage)) { // By definition every candidate has an inline implementation assert( _hasPluginInlineImpl(defaultPackage, platformKey, pluginResolutionType: pluginResolutionType), ); return (defaultPackage, null); } // Otherwise, require an explicit choice. if (candidates.length > 1) { return ( null, 'Plugin $pluginName:$platformKey has multiple possible implementations:\n' '${candidates.map((Plugin plugin) => ' ${plugin.name}\n').join()}' 'To fix this issue, add one of these dependencies to pubspec.yaml.\n', ); } // No implementation provided return (null, null); } /// Generates the Dart plugin registrant, which allows to bind a platform /// implementation of a Dart only plugin to its interface. /// The new entrypoint wraps [currentMainUri], adds the [_PluginRegistrant] class, /// and writes the file to [newMainDart]. /// /// [mainFile] is the main entrypoint file. e.g. //lib/main.dart. /// /// A successful run will create a new generate_main.dart file or update the existing file. /// Throws [ToolExit] if unable to generate the file. /// /// For more details, see https://flutter.dev/go/federated-plugins. Future generateMainDartWithPluginRegistrant( FlutterProject rootProject, PackageConfig packageConfig, String currentMainUri, File mainFile, ) async { final List plugins = await findPlugins(rootProject); final List resolutions = resolvePlatformImplementation( plugins, selectDartPluginsOnly: true, ); final LanguageVersion entrypointVersion = determineLanguageVersion( mainFile, packageConfig.packageOf(mainFile.absolute.uri), Cache.flutterRoot!, ); final Map templateContext = { 'mainEntrypoint': currentMainUri, 'dartLanguageVersion': entrypointVersion.toString(), AndroidPlugin.kConfigKey: [], IOSPlugin.kConfigKey: [], LinuxPlugin.kConfigKey: [], MacOSPlugin.kConfigKey: [], WindowsPlugin.kConfigKey: [], }; final File newMainDart = rootProject.dartPluginRegistrant; if (resolutions.isEmpty) { try { if (await newMainDart.exists()) { await newMainDart.delete(); } } on FileSystemException catch (error) { globals.printWarning( 'Unable to remove ${newMainDart.path}, received error: $error.\n' 'You might need to run flutter clean.', ); rethrow; } return; } for (final PluginInterfaceResolution resolution in resolutions) { assert(templateContext.containsKey(resolution.platform)); (templateContext[resolution.platform] as List?)?.add(resolution.toMap()); } try { await _renderTemplateToFile( _dartPluginRegistryForNonWebTemplate, templateContext, newMainDart, globals.templateRenderer, ); } on FileSystemException catch (error) { globals.printError('Unable to write ${newMainDart.path}, received error: $error'); rethrow; } }