mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
1066 lines
36 KiB
Dart
1066 lines
36 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 'dart:async';
|
|
|
|
import 'package:meta/meta.dart';
|
|
import 'package:mustache/mustache.dart' as mustache;
|
|
import 'package:yaml/yaml.dart';
|
|
|
|
import 'android/gradle.dart';
|
|
import 'base/common.dart';
|
|
import 'base/file_system.dart';
|
|
import 'base/time.dart';
|
|
import 'convert.dart';
|
|
import 'dart/package_map.dart';
|
|
import 'features.dart';
|
|
import 'globals.dart' as globals;
|
|
import 'macos/cocoapods.dart';
|
|
import 'platform_plugins.dart';
|
|
import 'project.dart';
|
|
import 'windows/property_sheet.dart';
|
|
|
|
void _renderTemplateToFile(String template, dynamic context, String filePath) {
|
|
final String renderedTemplate =
|
|
mustache.Template(template, htmlEscapeValues: false).renderString(context);
|
|
final File file = globals.fs.file(filePath);
|
|
file.createSync(recursive: true);
|
|
file.writeAsStringSync(renderedTemplate);
|
|
}
|
|
|
|
class Plugin {
|
|
Plugin({
|
|
@required this.name,
|
|
@required this.path,
|
|
@required this.platforms,
|
|
@required this.dependencies,
|
|
}) : assert(name != null),
|
|
assert(path != null),
|
|
assert(platforms != null),
|
|
assert(dependencies != null);
|
|
|
|
/// Parses [Plugin] specification from the provided pluginYaml.
|
|
///
|
|
/// This currently supports two formats. Legacy and Multi-platform.
|
|
///
|
|
/// Example of the deprecated Legacy format.
|
|
///
|
|
/// flutter:
|
|
/// plugin:
|
|
/// androidPackage: io.flutter.plugins.sample
|
|
/// iosPrefix: FLT
|
|
/// pluginClass: SamplePlugin
|
|
///
|
|
/// Example Multi-platform format.
|
|
///
|
|
/// flutter:
|
|
/// plugin:
|
|
/// platforms:
|
|
/// android:
|
|
/// package: io.flutter.plugins.sample
|
|
/// pluginClass: SamplePlugin
|
|
/// ios:
|
|
/// pluginClass: SamplePlugin
|
|
/// linux:
|
|
/// pluginClass: SamplePlugin
|
|
/// macos:
|
|
/// pluginClass: SamplePlugin
|
|
/// windows:
|
|
/// pluginClass: SamplePlugin
|
|
factory Plugin.fromYaml(
|
|
String name,
|
|
String path,
|
|
YamlMap pluginYaml,
|
|
List<String> dependencies,
|
|
) {
|
|
final List<String> errors = validatePluginYaml(pluginYaml);
|
|
if (errors.isNotEmpty) {
|
|
throwToolExit('Invalid plugin specification $name.\n${errors.join('\n')}');
|
|
}
|
|
if (pluginYaml != null && pluginYaml['platforms'] != null) {
|
|
return Plugin._fromMultiPlatformYaml(name, path, pluginYaml, dependencies);
|
|
}
|
|
return Plugin._fromLegacyYaml(name, path, pluginYaml, dependencies);
|
|
}
|
|
|
|
factory Plugin._fromMultiPlatformYaml(
|
|
String name,
|
|
String path,
|
|
dynamic pluginYaml,
|
|
List<String> dependencies,
|
|
) {
|
|
assert (pluginYaml != null && pluginYaml['platforms'] != null,
|
|
'Invalid multi-platform plugin specification $name.');
|
|
final YamlMap platformsYaml = pluginYaml['platforms'] as YamlMap;
|
|
|
|
assert (_validateMultiPlatformYaml(platformsYaml).isEmpty,
|
|
'Invalid multi-platform plugin specification $name.');
|
|
|
|
final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
|
|
|
|
if (_providesImplementationForPlatform(platformsYaml, AndroidPlugin.kConfigKey)) {
|
|
platforms[AndroidPlugin.kConfigKey] = AndroidPlugin.fromYaml(
|
|
name,
|
|
platformsYaml[AndroidPlugin.kConfigKey] as YamlMap,
|
|
path,
|
|
);
|
|
}
|
|
|
|
if (_providesImplementationForPlatform(platformsYaml, IOSPlugin.kConfigKey)) {
|
|
platforms[IOSPlugin.kConfigKey] =
|
|
IOSPlugin.fromYaml(name, platformsYaml[IOSPlugin.kConfigKey] as YamlMap);
|
|
}
|
|
|
|
if (_providesImplementationForPlatform(platformsYaml, LinuxPlugin.kConfigKey)) {
|
|
platforms[LinuxPlugin.kConfigKey] =
|
|
LinuxPlugin.fromYaml(name, platformsYaml[LinuxPlugin.kConfigKey] as YamlMap);
|
|
}
|
|
|
|
if (_providesImplementationForPlatform(platformsYaml, MacOSPlugin.kConfigKey)) {
|
|
platforms[MacOSPlugin.kConfigKey] =
|
|
MacOSPlugin.fromYaml(name, platformsYaml[MacOSPlugin.kConfigKey] as YamlMap);
|
|
}
|
|
|
|
if (_providesImplementationForPlatform(platformsYaml, WebPlugin.kConfigKey)) {
|
|
platforms[WebPlugin.kConfigKey] =
|
|
WebPlugin.fromYaml(name, platformsYaml[WebPlugin.kConfigKey] as YamlMap);
|
|
}
|
|
|
|
if (_providesImplementationForPlatform(platformsYaml, WindowsPlugin.kConfigKey)) {
|
|
platforms[WindowsPlugin.kConfigKey] =
|
|
WindowsPlugin.fromYaml(name, platformsYaml[WindowsPlugin.kConfigKey] as YamlMap);
|
|
}
|
|
|
|
return Plugin(
|
|
name: name,
|
|
path: path,
|
|
platforms: platforms,
|
|
dependencies: dependencies,
|
|
);
|
|
}
|
|
|
|
factory Plugin._fromLegacyYaml(
|
|
String name,
|
|
String path,
|
|
dynamic pluginYaml,
|
|
List<String> dependencies,
|
|
) {
|
|
final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
|
|
final String pluginClass = pluginYaml['pluginClass'] as String;
|
|
if (pluginYaml != null && pluginClass != null) {
|
|
final String androidPackage = pluginYaml['androidPackage'] as String;
|
|
if (androidPackage != null) {
|
|
platforms[AndroidPlugin.kConfigKey] = AndroidPlugin(
|
|
name: name,
|
|
package: pluginYaml['androidPackage'] as String,
|
|
pluginClass: pluginClass,
|
|
pluginPath: path,
|
|
);
|
|
}
|
|
|
|
final String iosPrefix = pluginYaml['iosPrefix'] as String ?? '';
|
|
platforms[IOSPlugin.kConfigKey] =
|
|
IOSPlugin(
|
|
name: name,
|
|
classPrefix: iosPrefix,
|
|
pluginClass: pluginClass,
|
|
);
|
|
}
|
|
return Plugin(
|
|
name: name,
|
|
path: path,
|
|
platforms: platforms,
|
|
dependencies: dependencies,
|
|
);
|
|
}
|
|
|
|
static List<String> validatePluginYaml(YamlMap yaml) {
|
|
|
|
final bool usesOldPluginFormat = const <String>{
|
|
'androidPackage',
|
|
'iosPrefix',
|
|
'pluginClass',
|
|
}.any(yaml.containsKey);
|
|
|
|
final bool usesNewPluginFormat = yaml.containsKey('platforms');
|
|
|
|
if (usesOldPluginFormat && usesNewPluginFormat) {
|
|
const String errorMessage =
|
|
'The flutter.plugin.platforms key cannot be used in combination with the old '
|
|
'flutter.plugin.{androidPackage,iosPrefix,pluginClass} keys. '
|
|
'See: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin';
|
|
return <String>[errorMessage];
|
|
}
|
|
|
|
if (usesNewPluginFormat) {
|
|
return _validateMultiPlatformYaml(yaml['platforms'] as YamlMap);
|
|
} else {
|
|
return _validateLegacyYaml(yaml);
|
|
}
|
|
}
|
|
|
|
static List<String> _validateMultiPlatformYaml(YamlMap yaml) {
|
|
bool isInvalid(String key, bool Function(YamlMap) validate) {
|
|
if (!yaml.containsKey(key)) {
|
|
return false;
|
|
}
|
|
final dynamic value = yaml[key];
|
|
if (value is! YamlMap) {
|
|
return true;
|
|
}
|
|
final YamlMap yamlValue = value as YamlMap;
|
|
if (yamlValue.containsKey('default_package')) {
|
|
return false;
|
|
}
|
|
return !validate(yamlValue);
|
|
}
|
|
|
|
if (yaml == null) {
|
|
return <String>['Invalid "platforms" specification.'];
|
|
}
|
|
final List<String> errors = <String>[];
|
|
if (isInvalid(AndroidPlugin.kConfigKey, AndroidPlugin.validate)) {
|
|
errors.add('Invalid "android" plugin specification.');
|
|
}
|
|
if (isInvalid(IOSPlugin.kConfigKey, IOSPlugin.validate)) {
|
|
errors.add('Invalid "ios" plugin specification.');
|
|
}
|
|
if (isInvalid(LinuxPlugin.kConfigKey, LinuxPlugin.validate)) {
|
|
errors.add('Invalid "linux" plugin specification.');
|
|
}
|
|
if (isInvalid(MacOSPlugin.kConfigKey, MacOSPlugin.validate)) {
|
|
errors.add('Invalid "macos" plugin specification.');
|
|
}
|
|
if (isInvalid(WindowsPlugin.kConfigKey, WindowsPlugin.validate)) {
|
|
errors.add('Invalid "windows" plugin specification.');
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
static List<String> _validateLegacyYaml(YamlMap yaml) {
|
|
final List<String> errors = <String>[];
|
|
if (yaml['androidPackage'] != null && yaml['androidPackage'] is! String) {
|
|
errors.add('The "androidPackage" must either be null or a string.');
|
|
}
|
|
if (yaml['iosPrefix'] != null && yaml['iosPrefix'] is! String) {
|
|
errors.add('The "iosPrefix" must either be null or a string.');
|
|
}
|
|
if (yaml['pluginClass'] != null && yaml['pluginClass'] is! String) {
|
|
errors.add('The "pluginClass" must either be null or a string..');
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
static bool _providesImplementationForPlatform(YamlMap platformsYaml, String platformKey) {
|
|
if (!platformsYaml.containsKey(platformKey)) {
|
|
return false;
|
|
}
|
|
if ((platformsYaml[platformKey] as YamlMap).containsKey('default_package')) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
final String name;
|
|
final String path;
|
|
|
|
/// The name of the packages this plugin depends on.
|
|
final List<String> dependencies;
|
|
|
|
/// This is a mapping from platform config key to the plugin platform spec.
|
|
final Map<String, PluginPlatform> platforms;
|
|
}
|
|
|
|
Plugin _pluginFromPackage(String name, Uri packageRoot) {
|
|
final String pubspecPath = globals.fs.path.fromUri(packageRoot.resolve('pubspec.yaml'));
|
|
if (!globals.fs.isFileSync(pubspecPath)) {
|
|
return null;
|
|
}
|
|
final dynamic pubspec = loadYaml(globals.fs.file(pubspecPath).readAsStringSync());
|
|
if (pubspec == null) {
|
|
return null;
|
|
}
|
|
final dynamic flutterConfig = pubspec['flutter'];
|
|
if (flutterConfig == null || !(flutterConfig.containsKey('plugin') as bool)) {
|
|
return null;
|
|
}
|
|
final String packageRootPath = globals.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,
|
|
dependencies == null ? <String>[] : <String>[...dependencies.keys.cast<String>()],
|
|
);
|
|
}
|
|
|
|
List<Plugin> findPlugins(FlutterProject project) {
|
|
final List<Plugin> plugins = <Plugin>[];
|
|
Map<String, Uri> packages;
|
|
try {
|
|
final String packagesFile = globals.fs.path.join(
|
|
project.directory.path,
|
|
PackageMap.globalPackagesPath,
|
|
);
|
|
packages = PackageMap(packagesFile).map;
|
|
} on FormatException catch (e) {
|
|
globals.printTrace('Invalid .packages file: $e');
|
|
return plugins;
|
|
}
|
|
packages.forEach((String name, Uri uri) {
|
|
final Uri packageRoot = uri.resolve('..');
|
|
final Plugin plugin = _pluginFromPackage(name, packageRoot);
|
|
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';
|
|
|
|
/// Filters [plugins] to those supported by [platformKey].
|
|
List<Map<String, dynamic>> _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, dynamic>> list = <Map<String, dynamic>>[];
|
|
for (final Plugin plugin in platformPlugins) {
|
|
list.add(<String, dynamic>{
|
|
_kFlutterPluginsNameKey: plugin.name,
|
|
_kFlutterPluginsPathKey: globals.fsUtils.escapePath(plugin.path),
|
|
_kFlutterPluginsDependenciesKey: <String>[...plugin.dependencies.where(pluginNames.contains)],
|
|
});
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/// 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"
|
|
/// ]
|
|
/// }
|
|
/// ],
|
|
/// "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) {
|
|
if (pluginsFile.existsSync()) {
|
|
pluginsFile.deleteSync();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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, dynamic> pluginsMap = <String, dynamic>{};
|
|
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, dynamic> result = <String, dynamic> {};
|
|
|
|
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'] = 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<dynamic> _createPluginLegacyDependencyGraph(List<Plugin> plugins) {
|
|
final List<dynamic> directAppDependencies = <dynamic>[];
|
|
|
|
final Set<String> pluginNames = plugins.map((Plugin plugin) => plugin.name).toSet();
|
|
for (final Plugin plugin in plugins) {
|
|
directAppDependencies.add(<String, dynamic>{
|
|
'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) {
|
|
if (pluginsFile.existsSync()) {
|
|
pluginsFile.deleteSync();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
{{#plugins}}
|
|
import {{package}}.{{class}};
|
|
{{/plugins}}
|
|
|
|
/**
|
|
* Generated file. Do not edit.
|
|
*/
|
|
public final class GeneratedPluginRegistrant {
|
|
public static void registerWith(PluginRegistry registry) {
|
|
if (alreadyRegisteredWith(registry)) {
|
|
return;
|
|
}
|
|
{{#plugins}}
|
|
{{class}}.registerWith(registry.registrarFor("{{package}}.{{class}}"));
|
|
{{/plugins}}
|
|
}
|
|
|
|
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;
|
|
|
|
{{#androidX}}
|
|
import androidx.annotation.Keep;
|
|
import androidx.annotation.NonNull;
|
|
{{/androidX}}
|
|
{{^androidX}}
|
|
import android.support.annotation.Keep;
|
|
import android.support.annotation.NonNull;
|
|
{{/androidX}}
|
|
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 {
|
|
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
|
{{#needsShim}}
|
|
ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine);
|
|
{{/needsShim}}
|
|
{{#plugins}}
|
|
{{#supportsEmbeddingV2}}
|
|
flutterEngine.getPlugins().add(new {{package}}.{{class}}());
|
|
{{/supportsEmbeddingV2}}
|
|
{{^supportsEmbeddingV2}}
|
|
{{#supportsEmbeddingV1}}
|
|
{{package}}.{{class}}.registerWith(shimPluginRegistry.registrarFor("{{package}}.{{class}}"));
|
|
{{/supportsEmbeddingV1}}
|
|
{{/supportsEmbeddingV2}}
|
|
{{/plugins}}
|
|
}
|
|
}
|
|
''';
|
|
|
|
List<Map<String, dynamic>> _extractPlatformMaps(List<Plugin> plugins, String type) {
|
|
final List<Map<String, dynamic>> pluginConfigs = <Map<String, dynamic>>[];
|
|
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) {
|
|
assert(project.android != null);
|
|
|
|
return project.android.getEmbeddingVersion();
|
|
}
|
|
|
|
Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
|
|
final List<Map<String, dynamic>> androidPlugins =
|
|
_extractPlatformMaps(plugins, AndroidPlugin.kConfigKey);
|
|
|
|
final Map<String, dynamic> templateContext = <String, dynamic>{
|
|
'plugins': 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.
|
|
for (final Map<String, dynamic> plugin in androidPlugins) {
|
|
if (plugin['supportsEmbeddingV1'] as bool && !(plugin['supportsEmbeddingV2'] as bool)) {
|
|
templateContext['needsShim'] = true;
|
|
if (project.isModule) {
|
|
globals.printStatus(
|
|
'The plugin `${plugin['name']}` is built using an older version '
|
|
"of the Android plugin API which assumes that it's running in a "
|
|
'full-Flutter environment. It may have undefined behaviors when '
|
|
'Flutter is integrated into an existing app as a module.\n'
|
|
'The plugin can be updated to the v2 Android Plugin APIs by '
|
|
'following https://flutter.dev/go/android-plugin-migration.'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
templateContent = _androidPluginRegistryTemplateNewEmbedding;
|
|
break;
|
|
case AndroidEmbeddingVersion.v1:
|
|
default:
|
|
for (final Map<String, dynamic> plugin in androidPlugins) {
|
|
if (!(plugin['supportsEmbeddingV1'] as bool) && plugin['supportsEmbeddingV2'] as bool) {
|
|
throwToolExit(
|
|
'The plugin `${plugin['name']}` requires your app to be migrated to '
|
|
'the Android embedding v2. Follow the steps on https://flutter.dev/go/android-project-migration '
|
|
'and re-run this command.'
|
|
);
|
|
}
|
|
}
|
|
templateContent = _androidPluginRegistryTemplateOldEmbedding;
|
|
break;
|
|
}
|
|
globals.printTrace('Generating $registryPath');
|
|
_renderTemplateToFile(
|
|
templateContent,
|
|
templateContext,
|
|
registryPath,
|
|
);
|
|
}
|
|
|
|
const String _objcPluginRegistryHeaderTemplate = '''
|
|
//
|
|
// Generated file. Do not edit.
|
|
//
|
|
|
|
#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.
|
|
//
|
|
|
|
#import "GeneratedPluginRegistrant.h"
|
|
|
|
{{#plugins}}
|
|
#if __has_include(<{{name}}/{{class}}.h>)
|
|
#import <{{name}}/{{class}}.h>
|
|
#else
|
|
@import {{name}};
|
|
#endif
|
|
|
|
{{/plugins}}
|
|
@implementation GeneratedPluginRegistrant
|
|
|
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
|
{{#plugins}}
|
|
[{{prefix}}{{class}} registerWithRegistrar:[registry registrarForPlugin:@"{{prefix}}{{class}}"]];
|
|
{{/plugins}}
|
|
}
|
|
|
|
@end
|
|
''';
|
|
|
|
const String _swiftPluginRegistryTemplate = '''
|
|
//
|
|
// Generated file. Do not edit.
|
|
//
|
|
|
|
import {{framework}}
|
|
import Foundation
|
|
|
|
{{#plugins}}
|
|
import {{name}}
|
|
{{/plugins}}
|
|
|
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|
{{#plugins}}
|
|
{{class}}.register(with: registry.registrar(forPlugin: "{{class}}"))
|
|
{{/plugins}}
|
|
}
|
|
''';
|
|
|
|
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}}'
|
|
{{#plugins}}
|
|
s.dependency '{{name}}'
|
|
{{/plugins}}
|
|
end
|
|
''';
|
|
|
|
const String _dartPluginRegistryTemplate = '''
|
|
//
|
|
// Generated file. Do not edit.
|
|
//
|
|
|
|
// ignore: unused_import
|
|
import 'dart:ui';
|
|
|
|
{{#plugins}}
|
|
import 'package:{{name}}/{{file}}';
|
|
{{/plugins}}
|
|
|
|
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
|
|
|
void registerPlugins(PluginRegistry registry) {
|
|
{{#plugins}}
|
|
{{class}}.registerWith(registry.registrarFor({{class}}));
|
|
{{/plugins}}
|
|
registry.registerMessageHandler();
|
|
}
|
|
''';
|
|
|
|
const String _cppPluginRegistryHeaderTemplate = '''
|
|
//
|
|
// Generated file. Do not edit.
|
|
//
|
|
|
|
#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.
|
|
//
|
|
|
|
#include "generated_plugin_registrant.h"
|
|
|
|
{{#plugins}}
|
|
#include <{{filename}}.h>
|
|
{{/plugins}}
|
|
|
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|
{{#plugins}}
|
|
{{class}}RegisterWithRegistrar(
|
|
registry->GetRegistrarForPlugin("{{class}}"));
|
|
{{/plugins}}
|
|
}
|
|
''';
|
|
|
|
Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
|
|
final List<Map<String, dynamic>> iosPlugins = _extractPlatformMaps(plugins, IOSPlugin.kConfigKey);
|
|
final Map<String, dynamic> context = <String, dynamic>{
|
|
'os': 'ios',
|
|
'deploymentTarget': '8.0',
|
|
'framework': 'Flutter',
|
|
'plugins': iosPlugins,
|
|
};
|
|
final String registryDirectory = project.ios.pluginRegistrantHost.path;
|
|
if (project.isModule) {
|
|
final String registryClassesDirectory = globals.fs.path.join(registryDirectory, 'Classes');
|
|
_renderTemplateToFile(
|
|
_pluginRegistrantPodspecTemplate,
|
|
context,
|
|
globals.fs.path.join(registryDirectory, 'FlutterPluginRegistrant.podspec'),
|
|
);
|
|
_renderTemplateToFile(
|
|
_objcPluginRegistryHeaderTemplate,
|
|
context,
|
|
globals.fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.h'),
|
|
);
|
|
_renderTemplateToFile(
|
|
_objcPluginRegistryImplementationTemplate,
|
|
context,
|
|
globals.fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.m'),
|
|
);
|
|
} else {
|
|
_renderTemplateToFile(
|
|
_objcPluginRegistryHeaderTemplate,
|
|
context,
|
|
globals.fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.h'),
|
|
);
|
|
_renderTemplateToFile(
|
|
_objcPluginRegistryImplementationTemplate,
|
|
context,
|
|
globals.fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.m'),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _writeLinuxPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
|
|
final List<Map<String, dynamic>> linuxPlugins = _extractPlatformMaps(plugins, LinuxPlugin.kConfigKey);
|
|
final Map<String, dynamic> context = <String, dynamic>{
|
|
'plugins': linuxPlugins,
|
|
};
|
|
await _writeCppPluginRegistrant(project.linux.managedDirectory, context);
|
|
}
|
|
|
|
Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
|
|
final List<Map<String, dynamic>> macosPlugins = _extractPlatformMaps(plugins, MacOSPlugin.kConfigKey);
|
|
final Map<String, dynamic> context = <String, dynamic>{
|
|
'os': 'macos',
|
|
'framework': 'FlutterMacOS',
|
|
'plugins': macosPlugins,
|
|
};
|
|
final String registryDirectory = project.macos.managedDirectory.path;
|
|
_renderTemplateToFile(
|
|
_swiftPluginRegistryTemplate,
|
|
context,
|
|
globals.fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.swift'),
|
|
);
|
|
}
|
|
|
|
Future<void> _writeWindowsPluginFiles(FlutterProject project, List<Plugin> plugins) async {
|
|
final List<Map<String, dynamic>> windowsPlugins = _extractPlatformMaps(plugins, WindowsPlugin.kConfigKey);
|
|
final Map<String, dynamic> context = <String, dynamic>{
|
|
'plugins': windowsPlugins,
|
|
};
|
|
await _writeCppPluginRegistrant(project.windows.managedDirectory, context);
|
|
await _writeWindowsPluginProperties(project.windows, windowsPlugins);
|
|
}
|
|
|
|
Future<void> _writeCppPluginRegistrant(Directory destination, Map<String, dynamic> templateContext) async {
|
|
final String registryDirectory = destination.path;
|
|
_renderTemplateToFile(
|
|
_cppPluginRegistryHeaderTemplate,
|
|
templateContext,
|
|
globals.fs.path.join(registryDirectory, 'generated_plugin_registrant.h'),
|
|
);
|
|
_renderTemplateToFile(
|
|
_cppPluginRegistryImplementationTemplate,
|
|
templateContext,
|
|
globals.fs.path.join(registryDirectory, 'generated_plugin_registrant.cc'),
|
|
);
|
|
}
|
|
|
|
Future<void> _writeWindowsPluginProperties(WindowsProject project, List<Map<String, dynamic>> windowsPlugins) async {
|
|
final List<String> pluginLibraryFilenames = windowsPlugins.map(
|
|
(Map<String, dynamic> plugin) => '${plugin['name']}_plugin.lib').toList();
|
|
// Use paths relative to the VS project directory.
|
|
final String projectDir = project.vcprojFile.parent.path;
|
|
final String symlinkDirPath = project.pluginSymlinkDirectory.path.substring(projectDir.length + 1);
|
|
final List<String> pluginIncludePaths = windowsPlugins.map((Map<String, dynamic> plugin) =>
|
|
globals.fs.path.join(symlinkDirPath, plugin['name'] as String, 'windows')).toList();
|
|
project.generatedPluginPropertySheetFile.writeAsStringSync(PropertySheet(
|
|
includePaths: pluginIncludePaths,
|
|
libraryDependencies: pluginLibraryFilenames,
|
|
).toString());
|
|
}
|
|
|
|
Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
|
|
final List<Map<String, dynamic>> webPlugins = _extractPlatformMaps(plugins, WebPlugin.kConfigKey);
|
|
final Map<String, dynamic> context = <String, dynamic>{
|
|
'plugins': webPlugins,
|
|
};
|
|
final String registryDirectory = project.web.libDirectory.path;
|
|
final String filePath = globals.fs.path.join(registryDirectory, 'generated_plugin_registrant.dart');
|
|
if (webPlugins.isEmpty) {
|
|
final File file = globals.fs.file(filePath);
|
|
if (file.existsSync()) {
|
|
file.deleteSync();
|
|
}
|
|
} else {
|
|
_renderTemplateToFile(
|
|
_dartPluginRegistryTemplate,
|
|
context,
|
|
filePath,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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 refreshPluginList has been run since the last plugin change.
|
|
void createPluginSymlinks(FlutterProject project, {bool force = false}) {
|
|
Map<String, dynamic> platformPlugins;
|
|
final String pluginFileContent = _readFileContent(project.flutterPluginsDependenciesFile);
|
|
if (pluginFileContent != null) {
|
|
final Map<String, dynamic> pluginInfo = json.decode(pluginFileContent) as Map<String, dynamic>;
|
|
platformPlugins = pluginInfo[_kFlutterPluginsPluginListKey] as Map<String, dynamic>;
|
|
}
|
|
platformPlugins ??= <String, dynamic>{};
|
|
|
|
if (featureFlags.isWindowsEnabled && project.windows.existsSync()) {
|
|
_createPlatformPluginSymlinks(
|
|
project.windows.pluginSymlinkDirectory,
|
|
platformPlugins[project.windows.pluginConfigKey] as List<dynamic>,
|
|
force: force,
|
|
);
|
|
}
|
|
if (featureFlags.isLinuxEnabled && project.linux.existsSync()) {
|
|
_createPlatformPluginSymlinks(
|
|
project.linux.pluginSymlinkDirectory,
|
|
platformPlugins[project.linux.pluginConfigKey] as List<dynamic>,
|
|
force: force,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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<dynamic> 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, dynamic> pluginInfo in platformPlugins.cast<Map<String, dynamic>>()) {
|
|
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) {
|
|
if (globals.platform.isWindows && (e.osError?.errorCode ?? 0) == 1314) {
|
|
throwToolExit(
|
|
'Building with plugins requires symlink support. '
|
|
'Please enable Developer Mode in your system settings.\n\n$e'
|
|
);
|
|
}
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rewrites the `.flutter-plugins` file of [project] based on the plugin
|
|
/// dependencies declared in `pubspec.yaml`.
|
|
///
|
|
/// If `checkProjects` is true, then plugins are only injected into directories
|
|
/// which already exist.
|
|
///
|
|
/// Assumes `pub get` has been executed since last change to `pubspec.yaml`.
|
|
void refreshPluginsList(FlutterProject project, {bool checkProjects = false}) {
|
|
final List<Plugin> plugins = findPlugins(project);
|
|
|
|
// 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 (!checkProjects || project.ios.existsSync()) {
|
|
cocoaPods.invalidatePodInstallOutput(project.ios);
|
|
}
|
|
// TODO(stuartmorgan): Potentially add checkProjects once a decision has
|
|
// made about how to handle macOS in existing projects.
|
|
if (project.macos.existsSync()) {
|
|
cocoaPods.invalidatePodInstallOutput(project.macos);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Injects plugins found in `pubspec.yaml` into the platform-specific projects.
|
|
///
|
|
/// If `checkProjects` is true, then plugins are only injected into directories
|
|
/// which already exist.
|
|
///
|
|
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
|
|
Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false}) async {
|
|
final List<Plugin> plugins = findPlugins(project);
|
|
if ((checkProjects && project.android.existsSync()) || !checkProjects) {
|
|
await _writeAndroidPluginRegistrant(project, plugins);
|
|
}
|
|
if ((checkProjects && project.ios.existsSync()) || !checkProjects) {
|
|
await _writeIOSPluginRegistrant(project, plugins);
|
|
}
|
|
// TODO(stuartmorgan): Revisit the conditions here once the plans for handling
|
|
// desktop in existing projects are in place. For now, ignore checkProjects
|
|
// on desktop and always treat it as true.
|
|
if (featureFlags.isLinuxEnabled && project.linux.existsSync()) {
|
|
await _writeLinuxPluginRegistrant(project, plugins);
|
|
}
|
|
if (featureFlags.isMacOSEnabled && project.macos.existsSync()) {
|
|
await _writeMacOSPluginRegistrant(project, plugins);
|
|
}
|
|
if (featureFlags.isWindowsEnabled && project.windows.existsSync()) {
|
|
await _writeWindowsPluginFiles(project, plugins);
|
|
}
|
|
for (final XcodeBasedProject subproject in <XcodeBasedProject>[project.ios, project.macos]) {
|
|
if (!project.isModule && (!checkProjects || subproject.existsSync())) {
|
|
final CocoaPods cocoaPods = CocoaPods();
|
|
if (plugins.isNotEmpty) {
|
|
await 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()) {
|
|
cocoaPods.addPodsDependencyToFlutterXcconfig(subproject);
|
|
}
|
|
}
|
|
}
|
|
if (featureFlags.isWebEnabled && project.web.existsSync()) {
|
|
await _writeWebPluginRegistrant(project, plugins);
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
}
|