
Support for FFI calls with `@Native external` functions through Native assets on MacOS and iOS. This enables bundling native code without any build-system boilerplate code. For more info see: * https://github.com/flutter/flutter/issues/129757 ### Implementation details for MacOS and iOS. Dylibs are bundled by (1) making them fat binaries if multiple architectures are targeted, (2) code signing these, and (3) copying them to the frameworks folder. These steps are done manual rather than via CocoaPods. CocoaPods would have done the same steps, but (a) needs the dylibs to be there before the `xcodebuild` invocation (we could trick it, by having a minimal dylib in the place and replace it during the build process, that works), and (b) can't deal with having no dylibs to be bundled (we'd have to bundle a dummy dylib or include some dummy C code in the build file). The dylibs are build as a new target inside flutter assemble, as that is the moment we know what build-mode and architecture to target. The mapping from asset id to dylib-path is passed in to every kernel compilation path. The interesting case is hot-restart where the initial kernel file is compiled by the "inner" flutter assemble, while after hot restart the "outer" flutter run compiled kernel file is pushed to the device. Both kernel files need to contain the mapping. The "inner" flutter assemble gets its mapping from the NativeAssets target which builds the native assets. The "outer" flutter run get its mapping from a dry-run invocation. Since this hot restart can be used for multiple target devices (`flutter run -d all`) it contains the mapping for all known targets. ### Example vs template The PR includes a new template that uses the new native assets in a package and has an app importing that. Separate discussion in: https://github.com/flutter/flutter/issues/131209. ### Tests This PR adds new tests to cover the various use cases. * dev/devicelab/bin/tasks/native_assets_ios.dart * Runs an example app with native assets in all build modes, doing hot reload and hot restart in debug mode. * dev/devicelab/bin/tasks/native_assets_ios_simulator.dart * Runs an example app with native assets, doing hot reload and hot restart. * packages/flutter_tools/test/integration.shard/native_assets_test.dart * Runs (incl hot reload/hot restart), builds, builds frameworks for iOS, MacOS and flutter-tester. * packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart * Unit tests the new Target in the backend. * packages/flutter_tools/test/general.shard/ios/native_assets_test.dart * packages/flutter_tools/test/general.shard/macos/native_assets_test.dart * Unit tests the native assets being packaged on a iOS/MacOS build. It also extends various existing tests: * dev/devicelab/bin/tasks/module_test_ios.dart * Exercises the add2app scenario. * packages/flutter_tools/test/general.shard/features_test.dart * Unit test the new feature flag.
1442 lines
52 KiB
Dart
1442 lines
52 KiB
Dart
// 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 'platform_plugins.dart';
|
|
import 'plugins.dart';
|
|
import 'project.dart';
|
|
|
|
void _renderTemplateToFile(String template, Object? context, File file, TemplateRenderer templateRenderer) {
|
|
final String renderedTemplate = templateRenderer
|
|
.renderString(template, context);
|
|
file.createSync(recursive: true);
|
|
file.writeAsStringSync(renderedTemplate);
|
|
}
|
|
|
|
Plugin? _pluginFromPackage(String name, Uri packageRoot, Set<String> appDependencies, {FileSystem? fileSystem}) {
|
|
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(pubspecFile.readAsStringSync());
|
|
} 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 = _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';
|
|
|
|
/// Filters [plugins] to those supported by [platformKey].
|
|
List<Map<String, Object>> _filterPluginsByPlatform(List<Plugin> plugins, String platformKey) {
|
|
final Iterable<Plugin> platformPlugins = plugins.where((Plugin p) {
|
|
return p.platforms.containsKey(platformKey);
|
|
});
|
|
|
|
final Set<String> pluginNames = platformPlugins.map((Plugin plugin) => plugin.name).toSet();
|
|
final List<Map<String, Object>> pluginInfo = <Map<String, Object>>[];
|
|
for (final Plugin plugin in platformPlugins) {
|
|
// 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;
|
|
}
|
|
|
|
/// 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) {
|
|
final File pluginsFile = project.flutterPluginsDependenciesFile;
|
|
if (plugins.isEmpty) {
|
|
return ErrorHandlingFileSystem.deleteIfExists(pluginsFile);
|
|
}
|
|
|
|
final String iosKey = project.ios.pluginConfigKey;
|
|
final String androidKey = project.android.pluginConfigKey;
|
|
final String macosKey = project.macos.pluginConfigKey;
|
|
final String linuxKey = project.linux.pluginConfigKey;
|
|
final String windowsKey = project.windows.pluginConfigKey;
|
|
final String webKey = project.web.pluginConfigKey;
|
|
|
|
final Map<String, Object> pluginsMap = <String, Object>{};
|
|
pluginsMap[iosKey] = _filterPluginsByPlatform(plugins, iosKey);
|
|
pluginsMap[androidKey] = _filterPluginsByPlatform(plugins, androidKey);
|
|
pluginsMap[macosKey] = _filterPluginsByPlatform(plugins, macosKey);
|
|
pluginsMap[linuxKey] = _filterPluginsByPlatform(plugins, linuxKey);
|
|
pluginsMap[windowsKey] = _filterPluginsByPlatform(plugins, windowsKey);
|
|
pluginsMap[webKey] = _filterPluginsByPlatform(plugins, webKey);
|
|
|
|
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;
|
|
|
|
// 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;
|
|
}
|
|
|
|
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 _androidPluginRegistryTemplateOldEmbedding = '''
|
|
package io.flutter.plugins;
|
|
|
|
import io.flutter.plugin.common.PluginRegistry;
|
|
{{#methodChannelPlugins}}
|
|
import {{package}}.{{class}};
|
|
{{/methodChannelPlugins}}
|
|
|
|
/**
|
|
* Generated file. Do not edit.
|
|
*/
|
|
public final class GeneratedPluginRegistrant {
|
|
public static void registerWith(PluginRegistry registry) {
|
|
if (alreadyRegisteredWith(registry)) {
|
|
return;
|
|
}
|
|
{{#methodChannelPlugins}}
|
|
{{class}}.registerWith(registry.registrarFor("{{package}}.{{class}}"));
|
|
{{/methodChannelPlugins}}
|
|
}
|
|
|
|
private static boolean alreadyRegisteredWith(PluginRegistry registry) {
|
|
final String key = GeneratedPluginRegistrant.class.getCanonicalName();
|
|
if (registry.hasPlugin(key)) {
|
|
return true;
|
|
}
|
|
registry.registrarFor(key);
|
|
return false;
|
|
}
|
|
}
|
|
''';
|
|
|
|
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;
|
|
{{#needsShim}}
|
|
import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry;
|
|
{{/needsShim}}
|
|
|
|
/**
|
|
* 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) {
|
|
{{#needsShim}}
|
|
ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine);
|
|
{{/needsShim}}
|
|
{{#methodChannelPlugins}}
|
|
{{#supportsEmbeddingV2}}
|
|
try {
|
|
flutterEngine.getPlugins().add(new {{package}}.{{class}}());
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error registering plugin {{name}}, {{package}}.{{class}}", e);
|
|
}
|
|
{{/supportsEmbeddingV2}}
|
|
{{^supportsEmbeddingV2}}
|
|
{{#supportsEmbeddingV1}}
|
|
try {
|
|
{{package}}.{{class}}.registerWith(shimPluginRegistry.registrarFor("{{package}}.{{class}}"));
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error registering plugin {{name}}, {{package}}.{{class}}", e);
|
|
}
|
|
{{/supportsEmbeddingV1}}
|
|
{{/supportsEmbeddingV2}}
|
|
{{/methodChannelPlugins}}
|
|
}
|
|
}
|
|
''';
|
|
|
|
List<Map<String, Object?>> _extractPlatformMaps(List<Plugin> plugins, String type) {
|
|
final List<Map<String, Object?>> pluginConfigs = <Map<String, Object?>>[];
|
|
for (final Plugin p in plugins) {
|
|
final PluginPlatform? platformPlugin = p.platforms[type];
|
|
if (platformPlugin != null) {
|
|
pluginConfigs.add(platformPlugin.toMap());
|
|
}
|
|
}
|
|
return pluginConfigs;
|
|
}
|
|
|
|
/// Returns the version of the Android embedding that the current
|
|
/// [project] is using.
|
|
AndroidEmbeddingVersion _getAndroidEmbeddingVersion(FlutterProject project) {
|
|
return project.android.getEmbeddingVersion();
|
|
}
|
|
|
|
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',
|
|
);
|
|
String templateContent;
|
|
final AndroidEmbeddingVersion appEmbeddingVersion = _getAndroidEmbeddingVersion(project);
|
|
switch (appEmbeddingVersion) {
|
|
case AndroidEmbeddingVersion.v2:
|
|
templateContext['needsShim'] = false;
|
|
// If a plugin is using an embedding version older than 2.0 and the app is using 2.0,
|
|
// then add shim for the old plugins.
|
|
|
|
final List<String> pluginsUsingV1 = <String>[];
|
|
for (final Map<String, Object?> plugin in androidPlugins) {
|
|
final bool supportsEmbeddingV1 = (plugin['supportsEmbeddingV1'] as bool?) ?? false;
|
|
final bool supportsEmbeddingV2 = (plugin['supportsEmbeddingV2'] as bool?) ?? false;
|
|
if (supportsEmbeddingV1 && !supportsEmbeddingV2) {
|
|
templateContext['needsShim'] = true;
|
|
if (plugin['name'] != null) {
|
|
pluginsUsingV1.add(plugin['name']! as String);
|
|
}
|
|
}
|
|
}
|
|
if (pluginsUsingV1.length > 1) {
|
|
globals.printWarning(
|
|
'The plugins `${pluginsUsingV1.join(', ')}` use a deprecated version of the Android embedding.\n'
|
|
'To avoid unexpected runtime failures, or future build failures, try to see if these plugins '
|
|
'support the Android V2 embedding. Otherwise, consider removing them since a future release '
|
|
'of Flutter will remove these deprecated APIs.\n'
|
|
'If you are plugin author, take a look at the docs for migrating the plugin to the V2 embedding: '
|
|
'https://flutter.dev/go/android-plugin-migration.'
|
|
);
|
|
} else if (pluginsUsingV1.isNotEmpty) {
|
|
globals.printWarning(
|
|
'The plugin `${pluginsUsingV1.first}` uses a deprecated version of the Android embedding.\n'
|
|
'To avoid unexpected runtime failures, or future build failures, try to see if this plugin '
|
|
'supports the Android V2 embedding. Otherwise, consider removing it since a future release '
|
|
'of Flutter will remove these deprecated APIs.\n'
|
|
'If you are plugin author, take a look at the docs for migrating the plugin to the V2 embedding: '
|
|
'https://flutter.dev/go/android-plugin-migration.'
|
|
);
|
|
}
|
|
templateContent = _androidPluginRegistryTemplateNewEmbedding;
|
|
case AndroidEmbeddingVersion.v1:
|
|
globals.printWarning(
|
|
'This app is using a deprecated version of the Android embedding.\n'
|
|
'To avoid unexpected runtime failures, or future build failures, try to migrate this '
|
|
'app to the V2 embedding.\n'
|
|
'Take a look at the docs for migrating an app: https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects'
|
|
);
|
|
for (final Map<String, Object?> plugin in androidPlugins) {
|
|
final bool supportsEmbeddingV1 = (plugin['supportsEmbeddingV1'] as bool?) ?? false;
|
|
final bool supportsEmbeddingV2 = (plugin['supportsEmbeddingV2'] as bool?) ?? false;
|
|
if (!supportsEmbeddingV1 && supportsEmbeddingV2) {
|
|
throwToolExit(
|
|
'The plugin `${plugin['name']}` requires your app to be migrated to '
|
|
'the Android embedding v2. Follow the steps on the migration doc above '
|
|
'and re-run this command.'
|
|
);
|
|
}
|
|
}
|
|
templateContent = _androidPluginRegistryTemplateOldEmbedding;
|
|
}
|
|
globals.printTrace('Generating $registryPath');
|
|
_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': '11.0',
|
|
'framework': 'Flutter',
|
|
'methodChannelPlugins': iosPlugins,
|
|
};
|
|
if (project.isModule) {
|
|
final Directory registryDirectory = project.ios.pluginRegistrantHost;
|
|
_renderTemplateToFile(
|
|
_pluginRegistrantPodspecTemplate,
|
|
context,
|
|
registryDirectory.childFile('FlutterPluginRegistrant.podspec'),
|
|
globals.templateRenderer,
|
|
);
|
|
}
|
|
_renderTemplateToFile(
|
|
_objcPluginRegistryHeaderTemplate,
|
|
context,
|
|
project.ios.pluginRegistrantHeader,
|
|
globals.templateRenderer,
|
|
);
|
|
_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 {
|
|
_renderTemplateToFile(
|
|
_linuxPluginRegistryHeaderTemplate,
|
|
templateContext,
|
|
destination.childFile('generated_plugin_registrant.h'),
|
|
globals.templateRenderer,
|
|
);
|
|
_renderTemplateToFile(
|
|
_linuxPluginRegistryImplementationTemplate,
|
|
templateContext,
|
|
destination.childFile('generated_plugin_registrant.cc'),
|
|
globals.templateRenderer,
|
|
);
|
|
}
|
|
|
|
Future<void> _writePluginCmakefile(File destinationFile, Map<String, Object> templateContext, TemplateRenderer templateRenderer) async {
|
|
_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,
|
|
};
|
|
_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) async {
|
|
final List<Plugin> methodChannelPlugins = _filterMethodChannelPlugins(plugins, WindowsPlugin.kConfigKey);
|
|
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 {
|
|
_renderTemplateToFile(
|
|
_cppPluginRegistryHeaderTemplate,
|
|
templateContext,
|
|
destination.childFile('generated_plugin_registrant.h'),
|
|
templateRenderer,
|
|
);
|
|
_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;
|
|
|
|
_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');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
}) 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);
|
|
if (changed || legacyChanged) {
|
|
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,
|
|
}) 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);
|
|
}
|
|
if (!project.isModule) {
|
|
final List<XcodeBasedProject> darwinProjects = <XcodeBasedProject>[
|
|
if (iosPlatform) project.ios,
|
|
if (macOSPlatform) project.macos,
|
|
];
|
|
for (final XcodeBasedProject subproject in darwinProjects) {
|
|
if (plugins.isNotEmpty) {
|
|
await globals.cocoaPods?.setupPodfile(subproject);
|
|
}
|
|
/// The user may have a custom maintained Podfile that they're running `pod install`
|
|
/// on themselves.
|
|
else if (subproject.podfile.existsSync() && subproject.podfileLock.existsSync()) {
|
|
globals.cocoaPods?.addPodsDependencyToFlutterXcconfig(subproject);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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, {
|
|
bool throwOnPluginPubspecError = true,
|
|
}) {
|
|
final List<String> platforms = <String>[
|
|
AndroidPlugin.kConfigKey,
|
|
IOSPlugin.kConfigKey,
|
|
LinuxPlugin.kConfigKey,
|
|
MacOSPlugin.kConfigKey,
|
|
WindowsPlugin.kConfigKey,
|
|
];
|
|
final Map<String, List<PluginInterfaceResolution>> possibleResolutions
|
|
= <String, List<PluginInterfaceResolution>>{};
|
|
final Map<String, String> defaultImplementations = <String, String>{};
|
|
// Generates a key for the maps above.
|
|
String getResolutionKey({required String platform, required String packageName}) {
|
|
return '$packageName:$platform';
|
|
}
|
|
|
|
bool hasPubspecError = false;
|
|
for (final Plugin plugin in plugins) {
|
|
for (final String platform in platforms) {
|
|
if (plugin.platforms[platform] == null &&
|
|
plugin.defaultPackagePlatforms[platform] == null) {
|
|
// The plugin doesn't implement this platform.
|
|
continue;
|
|
}
|
|
String? implementsPackage = plugin.implementsPackage;
|
|
if (implementsPackage == null || implementsPackage.isEmpty) {
|
|
final String? defaultImplementation = plugin.defaultPackagePlatforms[platform];
|
|
final bool hasInlineDartImplementation =
|
|
plugin.pluginDartClassPlatforms[platform] != null;
|
|
if (defaultImplementation == null && !hasInlineDartImplementation) {
|
|
if (throwOnPluginPubspecError) {
|
|
globals.printError(
|
|
"Plugin `${plugin.name}` doesn't implement a plugin interface, nor does "
|
|
'it specify an implementation in pubspec.yaml.\n\n'
|
|
'To set an inline implementation, use:\n'
|
|
'flutter:\n'
|
|
' plugin:\n'
|
|
' platforms:\n'
|
|
' $platform:\n'
|
|
' $kDartPluginClass: <plugin-class>\n'
|
|
'\n'
|
|
'To set a default implementation, use:\n'
|
|
'flutter:\n'
|
|
' plugin:\n'
|
|
' platforms:\n'
|
|
' $platform:\n'
|
|
' $kDefaultPackage: <plugin-implementation>\n'
|
|
'\n'
|
|
'To implement an interface, use:\n'
|
|
'flutter:\n'
|
|
' plugin:\n'
|
|
' implements: <plugin-interface>'
|
|
'\n'
|
|
);
|
|
}
|
|
hasPubspecError = true;
|
|
continue;
|
|
}
|
|
final String defaultImplementationKey = getResolutionKey(platform: platform, packageName: plugin.name);
|
|
if (defaultImplementation != null) {
|
|
defaultImplementations[defaultImplementationKey] = defaultImplementation;
|
|
continue;
|
|
} else {
|
|
// 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.
|
|
final bool isDesktop = platform == 'linux' || platform == 'macos' || platform == '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;
|
|
if (!isDesktop || hasMinVersionForImplementsRequirement) {
|
|
implementsPackage = plugin.name;
|
|
defaultImplementations[defaultImplementationKey] = plugin.name;
|
|
} else {
|
|
// If it doesn't meet any of the conditions, it isn't eligible for
|
|
// auto-registration.
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
// If there's no Dart implementation, there's nothing to register.
|
|
if (plugin.pluginDartClassPlatforms[platform] == null ||
|
|
plugin.pluginDartClassPlatforms[platform] == 'none') {
|
|
continue;
|
|
}
|
|
|
|
// If it hasn't been skipped, it's a candidate for auto-registration, so
|
|
// add it as a possible resolution.
|
|
final String resolutionKey = getResolutionKey(platform: platform, packageName: implementsPackage);
|
|
if (!possibleResolutions.containsKey(resolutionKey)) {
|
|
possibleResolutions[resolutionKey] = <PluginInterfaceResolution>[];
|
|
}
|
|
possibleResolutions[resolutionKey]!.add(PluginInterfaceResolution(
|
|
plugin: plugin,
|
|
platform: platform,
|
|
));
|
|
}
|
|
}
|
|
if (hasPubspecError && throwOnPluginPubspecError) {
|
|
throwToolExit('Please resolve the errors');
|
|
}
|
|
|
|
// Now resolve all the possible resolutions to a single option for each
|
|
// plugin, or throw if that's not possible.
|
|
bool hasResolutionError = false;
|
|
final List<PluginInterfaceResolution> finalResolution = <PluginInterfaceResolution>[];
|
|
for (final MapEntry<String, List<PluginInterfaceResolution>> entry in possibleResolutions.entries) {
|
|
final List<PluginInterfaceResolution> candidates = entry.value;
|
|
// If there's only one candidate, use it.
|
|
if (candidates.length == 1) {
|
|
finalResolution.add(candidates.first);
|
|
continue;
|
|
}
|
|
// Next, try direct dependencies of the resolving application.
|
|
final Iterable<PluginInterfaceResolution> directDependencies = candidates.where((PluginInterfaceResolution r) {
|
|
return r.plugin.isDirectDependency;
|
|
});
|
|
if (directDependencies.isNotEmpty) {
|
|
if (directDependencies.length > 1) {
|
|
globals.printError(
|
|
'Plugin ${entry.key} has conflicting direct dependency implementations:\n'
|
|
'${directDependencies.map((PluginInterfaceResolution r) => ' ${r.plugin.name}\n').join()}'
|
|
'To fix this issue, remove all but one of these dependencies from pubspec.yaml.\n'
|
|
);
|
|
hasResolutionError = true;
|
|
} else {
|
|
finalResolution.add(directDependencies.first);
|
|
}
|
|
continue;
|
|
}
|
|
// Next, defer to the default implementation if there is one.
|
|
final String? defaultPackageName = defaultImplementations[entry.key];
|
|
if (defaultPackageName != null) {
|
|
final int defaultIndex = candidates
|
|
.indexWhere((PluginInterfaceResolution r) => r.plugin.name == defaultPackageName);
|
|
if (defaultIndex != -1) {
|
|
finalResolution.add(candidates[defaultIndex]);
|
|
continue;
|
|
}
|
|
}
|
|
// Otherwise, require an explicit choice.
|
|
if (candidates.length > 1) {
|
|
globals.printError(
|
|
'Plugin ${entry.key} has multiple possible implementations:\n'
|
|
'${candidates.map((PluginInterfaceResolution r) => ' ${r.plugin.name}\n').join()}'
|
|
'To fix this issue, add one of these dependencies to pubspec.yaml.\n'
|
|
);
|
|
hasResolutionError = true;
|
|
continue;
|
|
}
|
|
}
|
|
if (hasResolutionError) {
|
|
throwToolExit('Please resolve the errors');
|
|
}
|
|
return finalResolution;
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// This method also validates each plugin's pubspec.yaml, but errors are only
|
|
/// reported if [throwOnPluginPubspecError] is [true].
|
|
///
|
|
/// For more details, see https://flutter.dev/go/federated-plugins.
|
|
Future<void> generateMainDartWithPluginRegistrant(
|
|
FlutterProject rootProject,
|
|
PackageConfig packageConfig,
|
|
String currentMainUri,
|
|
File mainFile, {
|
|
bool throwOnPluginPubspecError = false,
|
|
}) async {
|
|
final List<Plugin> plugins = await findPlugins(rootProject);
|
|
final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(
|
|
plugins,
|
|
throwOnPluginPubspecError: throwOnPluginPubspecError,
|
|
);
|
|
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 (newMainDart.existsSync()) {
|
|
newMainDart.deleteSync();
|
|
}
|
|
} 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 {
|
|
_renderTemplateToFile(
|
|
_dartPluginRegistryForNonWebTemplate,
|
|
templateContext,
|
|
newMainDart,
|
|
globals.templateRenderer,
|
|
);
|
|
} on FileSystemException catch (error) {
|
|
globals.printError('Unable to write ${newMainDart.path}, received error: $error');
|
|
rethrow;
|
|
}
|
|
}
|