blob: 4e47e50e54cb15d1a7ae3a59d410dae0b817ed7e [file] [log] [blame]
// 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 'convert.dart';
import 'dart/language_version.dart';
import 'dart/package_map.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<void> _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<Plugin?> _pluginFromPackage(String name, Uri packageRoot, Set<String> appDependencies,
{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 ? <String>[] : <String>[...dependencies.keys.cast<String>()],
fileSystem: fs,
appDependencies: appDependencies,
);
}
Future<List<Plugin>> findPlugins(FlutterProject project, { bool throwOnError = true}) async {
final List<Plugin> plugins = <Plugin>[];
final FileSystem fs = project.directory.fileSystem;
final String packagesFile = fs.path.join(
project.directory.path,
'.packages',
);
final PackageConfig packageConfig = await loadPackageConfigWithLogging(
fs.file(packagesFile),
logger: globals.logger,
throwOnError: throwOnError,
);
for (final Package package in packageConfig.packages) {
final Uri packageRoot = package.packageUriRoot.resolve('..');
final Plugin? plugin = await _pluginFromPackage(
package.name,
packageRoot,
project.manifest.dependencies,
fileSystem: fs,
);
if (plugin != null) {
plugins.add(plugin);
}
}
return plugins;
}
// 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';
/// 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<Plugin> plugins, {
bool forceCocoaPodsOnly = false,
}) {
final File pluginsFile = project.flutterPluginsDependenciesFile;
if (plugins.isEmpty) {
return ErrorHandlingFileSystem.deleteIfExists(pluginsFile);
}
final Iterable<String> platformKeys = <String>[
project.ios.pluginConfigKey,
project.android.pluginConfigKey,
project.macos.pluginConfigKey,
project.linux.pluginConfigKey,
project.windows.pluginConfigKey,
project.web.pluginConfigKey,
];
final Map<String, Object> pluginsMap = <String, Object>{};
for (final String platformKey in platformKeys) {
pluginsMap[platformKey] = _createPluginMapOfPlatform(plugins, platformKey);
}
final Map<String, Object> result = <String, Object>{};
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'] = !forceCocoaPodsOnly && project.usesSwiftPackageManager;
// 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) {
pluginsChanged = oldPluginsFileStringContent.contains(pluginsMap.toString());
}
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].
List<Map<String, Object>> _createPluginMapOfPlatform(
List<Plugin> plugins,
String platformKey,
) {
final Iterable<Plugin> resolvedPlatformPlugins = plugins.where((Plugin p) {
return p.platforms.containsKey(platformKey);
});
final Set<String> pluginNames = resolvedPlatformPlugins.map((Plugin plugin) => plugin.name).toSet();
final List<Map<String, Object>> pluginInfo = <Map<String, Object>>[];
for (final Plugin plugin in resolvedPlatformPlugins) {
// This is guaranteed to be non-null due to the `where` filter above.
final PluginPlatform platformPlugin = plugin.platforms[platformKey]!;
pluginInfo.add(<String, Object>{
_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: <String>[...plugin.dependencies.where(pluginNames.contains)],
});
}
return pluginInfo;
}
List<Object?> _createPluginLegacyDependencyGraph(List<Plugin> plugins) {
final List<Object> directAppDependencies = <Object>[];
final Set<String> pluginNames = plugins.map((Plugin plugin) => plugin.name).toSet();
for (final Plugin plugin in plugins) {
directAppDependencies.add(<String, Object>{
'name': plugin.name,
// Extract the plugin dependencies which happen to be plugins.
'dependencies': <String>[...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<Plugin> 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<Map<String, Object?>> _extractPlatformMaps(List<Plugin> plugins, String type) {
return <Map<String, Object?>>[
for (final Plugin plugin in plugins)
if (plugin.platforms[type] case final PluginPlatform platformPlugin)
platformPlugin.toMap(),
];
}
Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Plugin> methodChannelPlugins = _filterMethodChannelPlugins(plugins, AndroidPlugin.kConfigKey);
final List<Map<String, Object?>> androidPlugins = _extractPlatformMaps(methodChannelPlugins, AndroidPlugin.kConfigKey);
final Map<String, Object> templateContext = <String, Object>{
'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<FlutterPluginRegistry>*)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<FlutterPluginRegistry>*)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 <flutter/plugin_registry.h>
// 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 <flutter_linux/flutter_linux.h>
// 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 $<TARGET_FILE:${plugin}_plugin>)
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}}/{{pluginName}}.dart';
{{/android}}
{{#ios}}
import 'package:{{pluginName}}/{{pluginName}}.dart';
{{/ios}}
{{#linux}}
import 'package:{{pluginName}}/{{pluginName}}.dart';
{{/linux}}
{{#macos}}
import 'package:{{pluginName}}/{{pluginName}}.dart';
{{/macos}}
{{#windows}}
import 'package:{{pluginName}}/{{pluginName}}.dart';
{{/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<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Plugin> methodChannelPlugins = _filterMethodChannelPlugins(plugins, IOSPlugin.kConfigKey);
final List<Map<String, Object?>> iosPlugins = _extractPlatformMaps(methodChannelPlugins, IOSPlugin.kConfigKey);
final Map<String, Object> context = <String, Object>{
'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<String> relativePathComponents = fileSystem.path.split(fileSystem.path.relative(
project.pluginSymlinkDirectory.absolute.path,
from: makefileDirPath,
));
return cmakePathContext.joinAll(relativePathComponents);
}
Future<void> _writeLinuxPluginFiles(FlutterProject project, List<Plugin> plugins) async {
final List<Plugin> methodChannelPlugins = _filterMethodChannelPlugins(plugins, LinuxPlugin.kConfigKey);
final List<Map<String, Object?>> linuxMethodChannelPlugins = _extractPlatformMaps(methodChannelPlugins, LinuxPlugin.kConfigKey);
final List<Plugin> ffiPlugins = _filterFfiPlugins(plugins, LinuxPlugin.kConfigKey)..removeWhere(methodChannelPlugins.contains);
final List<Map<String, Object?>> linuxFfiPlugins = _extractPlatformMaps(ffiPlugins, LinuxPlugin.kConfigKey);
final Map<String, Object> context = <String, Object>{
'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<void> _writeLinuxPluginRegistrant(Directory destination, Map<String, Object> 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<void> _writePluginCmakefile(File destinationFile, Map<String, Object> templateContext, TemplateRenderer templateRenderer) async {
await _renderTemplateToFile(
_pluginCmakefileTemplate,
templateContext,
destinationFile,
templateRenderer,
);
}
Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Plugin> methodChannelPlugins = _filterMethodChannelPlugins(plugins, MacOSPlugin.kConfigKey);
final List<Map<String, Object?>> macosMethodChannelPlugins = _extractPlatformMaps(methodChannelPlugins, MacOSPlugin.kConfigKey);
final Map<String, Object> context = <String, Object>{
'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<Plugin> _filterMethodChannelPlugins(List<Plugin> 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<Plugin> _filterFfiPlugins(List<Plugin> 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<Plugin> _filterPluginsByVariant(List<Plugin> 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<void> writeWindowsPluginFiles(
FlutterProject project,
List<Plugin> plugins,
TemplateRenderer templateRenderer, {
Iterable<String>? allowedPlugins,
}) async {
final List<Plugin> methodChannelPlugins = _filterMethodChannelPlugins(plugins, WindowsPlugin.kConfigKey);
if (allowedPlugins != null) {
final List<Plugin> disallowedPlugins = methodChannelPlugins
.toList()
..removeWhere((Plugin plugin) => allowedPlugins.contains(plugin.name));
if (disallowedPlugins.isNotEmpty) {
final StringBuffer buffer = StringBuffer();
buffer.writeln('The Flutter Preview device does not support the following plugins from your pubspec.yaml:');
buffer.writeln();
buffer.writeln(disallowedPlugins.map((Plugin p) => p.name).toList().toString());
buffer.writeln();
buffer.writeln('In order to build a Flutter app with plugins, you must use another target platform,');
buffer.writeln('such as Windows. Type `flutter doctor` into your terminal to see which target platforms');
buffer.writeln('are ready to be used, and how to get required dependencies for other platforms.');
throwToolExit(buffer.toString());
}
}
final List<Plugin> win32Plugins = _filterPluginsByVariant(methodChannelPlugins, WindowsPlugin.kConfigKey, PluginPlatformVariant.win32);
final List<Map<String, Object?>> windowsMethodChannelPlugins = _extractPlatformMaps(win32Plugins, WindowsPlugin.kConfigKey);
final List<Plugin> ffiPlugins = _filterFfiPlugins(plugins, WindowsPlugin.kConfigKey)..removeWhere(methodChannelPlugins.contains);
final List<Map<String, Object?>> windowsFfiPlugins = _extractPlatformMaps(ffiPlugins, WindowsPlugin.kConfigKey);
final Map<String, Object> context = <String, Object>{
'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<void> _writeCppPluginRegistrant(Directory destination, Map<String, Object> 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<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plugins, Directory destination) async {
final List<Map<String, Object?>> webPlugins = _extractPlatformMaps(plugins, WebPlugin.kConfigKey);
final Map<String, Object> context = <String, Object>{
'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<String, Object?>? platformPlugins;
final String? pluginFileContent = _readFileContent(project.flutterPluginsDependenciesFile);
if (pluginFileContent != null) {
final Map<String, Object?>? pluginInfo = json.decode(pluginFileContent) as Map<String, Object?>?;
platformPlugins = pluginInfo?[_kFlutterPluginsPluginListKey] as Map<String, Object?>?;
}
platformPlugins ??= <String, Object?>{};
if (localFeatureFlags.isWindowsEnabled && project.windows.existsSync()) {
_createPlatformPluginSymlinks(
project.windows.pluginSymlinkDirectory,
platformPlugins[project.windows.pluginConfigKey] as List<Object?>?,
force: force,
);
}
if (localFeatureFlags.isLinuxEnabled && project.linux.existsSync()) {
_createPlatformPluginSymlinks(
project.linux.pluginSymlinkDirectory,
platformPlugins[project.linux.pluginConfigKey] as List<Object?>?,
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<Object?>? platformPlugins, {bool force = false}) {
if (force && symlinkDirectory.existsSync()) {
// Start fresh to avoid stale links.
symlinkDirectory.deleteSync(recursive: true);
}
symlinkDirectory.createSync(recursive: true);
if (platformPlugins == null) {
return;
}
for (final Map<String, Object?> pluginInfo in platformPlugins.cast<Map<String, Object?>>()) {
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: 'dest',
source: 'source',
);
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`.
Future<void> refreshPluginsList(
FlutterProject project, {
bool iosPlatform = false,
bool macOSPlatform = false,
bool forceCocoaPodsOnly = false,
}) async {
final List<Plugin> plugins = await findPlugins(project);
// Sort the plugins by name to keep ordering stable in generated files.
plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name));
// TODO(franciscojma): Remove once migration is complete.
// Write the legacy plugin files to avoid breaking existing apps.
final bool legacyChanged = _writeFlutterPluginsListLegacy(project, plugins);
final bool changed = _writeFlutterPluginsList(
project,
plugins,
forceCocoaPodsOnly: forceCocoaPodsOnly,
);
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<void> injectBuildTimePluginFiles(
FlutterProject project, {
required Directory destination,
bool webPlatform = false,
}) async {
final List<Plugin> plugins = await findPlugins(project);
// Sort the plugins by name to keep ordering stable in generated files.
plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name));
if (webPlatform) {
await _writeWebPluginRegistrant(project, plugins, 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 [injectBuildTimePluginFiles] 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<void> injectPlugins(
FlutterProject project, {
bool androidPlatform = false,
bool iosPlatform = false,
bool linuxPlatform = false,
bool macOSPlatform = false,
bool windowsPlatform = false,
Iterable<String>? allowedPlugins,
DarwinDependencyManagement? darwinDependencyManagement,
}) async {
final List<Plugin> plugins = await findPlugins(project);
// Sort the plugins by name to keep ordering stable in generated files.
plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name));
if (androidPlatform) {
await _writeAndroidPluginRegistrant(project, plugins);
}
if (iosPlatform) {
await _writeIOSPluginRegistrant(project, plugins);
}
if (linuxPlatform) {
await _writeLinuxPluginFiles(project, plugins);
}
if (macOSPlatform) {
await _writeMacOSPluginRegistrant(project, plugins);
}
if (windowsPlatform) {
await writeWindowsPluginFiles(project, plugins, globals.templateRenderer, allowedPlugins: allowedPlugins);
}
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.flutterPluginsFile) != null;
}
/// Resolves the platform implementation for Dart-only plugins.
///
/// * 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.
// TODO(stuartmorgan): Expand implementation to apply to all implementations,
// not just Dart-only, per the federated plugin spec.
List<PluginInterfaceResolution> resolvePlatformImplementation(
List<Plugin> plugins,
) {
const Iterable<String> platformKeys = <String>[
AndroidPlugin.kConfigKey,
IOSPlugin.kConfigKey,
LinuxPlugin.kConfigKey,
MacOSPlugin.kConfigKey,
WindowsPlugin.kConfigKey,
];
final List<PluginInterfaceResolution> pluginResolutions = <PluginInterfaceResolution>[];
bool hasResolutionError = false;
bool hasPluginPubspecError = false;
for (final String platformKey in platformKeys) {
// Key: the plugin name, value: the list of plugin candidates for the implementation of [platformKey].
final Map<String, List<Plugin>> pluginImplCandidates = <String, List<Plugin>>{};
// Key: the plugin name, value: the plugin name of the default implementation of [platformKey].
final Map<String, String> defaultImplementations = <String, String>{};
for (final Plugin plugin in plugins) {
final String? error = _validatePlugin(plugin, platformKey);
if (error != null) {
globals.printError(error);
hasPluginPubspecError = true;
continue;
}
final String? implementsPluginName = _getImplementedPlugin(plugin, platformKey);
final String? defaultImplPluginName = _getDefaultImplPlugin(plugin, platformKey);
if (defaultImplPluginName != null) {
// Each plugin can only have one default implementation for this [platformKey].
defaultImplementations[plugin.name] = defaultImplPluginName;
}
if (implementsPluginName != null) {
pluginImplCandidates.putIfAbsent(implementsPluginName, () => <Plugin>[]);
pluginImplCandidates[implementsPluginName]!.add(plugin);
}
}
final Map<String, Plugin> pluginResolution = <String, Plugin>{};
// Now resolve all the possible resolutions to a single option for each
// plugin, or throw if that's not possible.
for (final MapEntry<String, List<Plugin>> implCandidatesEntry in pluginImplCandidates.entries) {
final (Plugin? resolution, String? error) = _resolveImplementationOfPlugin(
platformKey: platformKey,
pluginName: implCandidatesEntry.key,
candidates: implCandidatesEntry.value,
defaultPackageName: defaultImplementations[implCandidatesEntry.key],
);
if (error != null) {
globals.printError(error);
hasResolutionError = true;
} else if (resolution != null) {
pluginResolution[implCandidatesEntry.key] = resolution;
}
}
pluginResolutions.addAll(
pluginResolution.values.map((Plugin plugin) {
return PluginInterfaceResolution(plugin: plugin, platform: platformKey);
}),
);
}
if (hasPluginPubspecError) {
throwToolExit('Please resolve the plugin pubspec errors');
}
if (hasResolutionError) {
throwToolExit('Please resolve the plugin implementation selection errors');
}
return pluginResolutions;
}
/// Validates conflicting plugin parameters in pubspec, such as
/// `dartPluginClass`, `default_package` and `implements`.
///
/// Returns an error, if failing.
String? _validatePlugin(Plugin plugin, String platformKey) {
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 (_hasPluginInlineDartImpl(plugin, platformKey)) {
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: 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) {
final bool hasInlineDartImpl = _hasPluginInlineDartImpl(plugin, platformKey);
if (hasInlineDartImpl) {
final String? implementsPackage = plugin.implementsPackage;
// Only can serve, if the plugin has a dart inline implementation.
if (implementsPackage != null && implementsPackage.isNotEmpty) {
return implementsPackage;
}
if (_isEligibleDartSelfImpl(plugin, platformKey)) {
// The inline Dart 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) {
final String? defaultImplPluginName =
plugin.defaultPackagePlatforms[platformKey];
if (defaultImplPluginName != null) {
return defaultImplPluginName;
}
if (_hasPluginInlineDartImpl(plugin, platformKey) &&
_isEligibleDartSelfImpl(plugin, platformKey)) {
// The inline Dart 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 dart implementation.
bool _hasPluginInlineDartImpl(Plugin plugin, String platformKey) {
return plugin.pluginDartClassPlatforms[platformKey] != null &&
plugin.pluginDartClassPlatforms[platformKey] != '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 String pluginName,
required List<Plugin> candidates,
String? defaultPackageName,
}) {
// 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<Plugin> 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<Plugin> implementingPackage = directDependencies.where((Plugin plugin) => plugin.implementsPackage != null && plugin.implementsPackage!.isNotEmpty);
final Set<Plugin> 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 (defaultPackageName != null) {
final int defaultIndex = candidates
.indexWhere((Plugin plugin) => plugin.name == defaultPackageName);
if (defaultIndex != -1) {
return (candidates[defaultIndex], 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. /<app>/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<void> generateMainDartWithPluginRegistrant(
FlutterProject rootProject,
PackageConfig packageConfig,
String currentMainUri,
File mainFile,
) async {
final List<Plugin> plugins = await findPlugins(rootProject);
final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(
plugins,
);
final LanguageVersion entrypointVersion = determineLanguageVersion(
mainFile,
packageConfig.packageOf(mainFile.absolute.uri),
Cache.flutterRoot!,
);
final Map<String, Object> templateContext = <String, Object>{
'mainEntrypoint': currentMainUri,
'dartLanguageVersion': entrypointVersion.toString(),
AndroidPlugin.kConfigKey: <Object?>[],
IOSPlugin.kConfigKey: <Object?>[],
LinuxPlugin.kConfigKey: <Object?>[],
MacOSPlugin.kConfigKey: <Object?>[],
WindowsPlugin.kConfigKey: <Object?>[],
};
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<Object?>?)?.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;
}
}