Unverified Commit 2df7dca8 authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Fully support Dart-only mobile and macOS plugins (#96183)

parent f18d4d03
......@@ -152,14 +152,28 @@ Future<void> main() async {
await flutter('clean');
});
// Make a fake Dart-only plugin, since there are no existing examples.
section('Create local plugin');
const String dartPluginName = 'dartplugin';
await _createFakeDartPlugin(dartPluginName, tempDir);
section('Add plugins');
final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
String content = await pubspec.readAsString();
content = content.replaceFirst(
'\ndependencies:\n',
// One dynamic framework, one static framework, and one that does not support iOS.
'\ndependencies:\n device_info: 2.0.3\n google_sign_in: 4.5.1\n android_alarm_manager: 0.4.5+11\n',
// One dynamic framework, one static framework, one Dart-only,
// and one that does not support iOS.
'''
dependencies:
device_info: 2.0.3
google_sign_in: 4.5.1
android_alarm_manager: 0.4.5+11
$dartPluginName:
path: ../$dartPluginName
''',
);
await pubspec.writeAsString(content, flush: true);
await inDirectory(projectDir, () async {
......@@ -191,7 +205,8 @@ Future<void> main() async {
|| !podfileLockOutput.contains(':path: Flutter/FlutterPluginRegistrant')
|| !podfileLockOutput.contains(':path: ".symlinks/plugins/device_info/ios"')
|| !podfileLockOutput.contains(':path: ".symlinks/plugins/google_sign_in/ios"')
|| podfileLockOutput.contains('android_alarm_manager')) {
|| podfileLockOutput.contains('android_alarm_manager')
|| podfileLockOutput.contains(dartPluginName)) {
print(podfileLockOutput);
return TaskResult.failure('Building ephemeral host app Podfile.lock does not contain expected pods');
}
......@@ -205,6 +220,9 @@ Future<void> main() async {
// Android-only, no embedded framework.
checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', 'android_alarm_manager.framework'));
// Dart-only, no embedded framework.
checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$dartPluginName.framework'));
section('Clean and pub get module');
await inDirectory(projectDir, () async {
......@@ -243,7 +261,8 @@ Future<void> main() async {
|| !hostPodfileLockOutput.contains(':path: "../hello/.ios/Flutter/FlutterPluginRegistrant"')
|| !hostPodfileLockOutput.contains(':path: "../hello/.ios/.symlinks/plugins/device_info/ios"')
|| !hostPodfileLockOutput.contains(':path: "../hello/.ios/.symlinks/plugins/google_sign_in/ios"')
|| hostPodfileLockOutput.contains('android_alarm_manager')) {
|| hostPodfileLockOutput.contains('android_alarm_manager')
|| hostPodfileLockOutput.contains(dartPluginName)) {
print(hostPodfileLockOutput);
throw TaskResult.failure('Building host app Podfile.lock does not contain expected pods');
}
......@@ -501,3 +520,46 @@ Future<bool> _isAppAotBuild(Directory app) async {
return symbolTable.contains('kDartIsolateSnapshotInstructions');
}
Future<void> _createFakeDartPlugin(String name, Directory parent) async {
// Start from a standard plugin template.
await inDirectory(parent, () async {
await flutter(
'create',
options: <String>[
'--org',
'io.flutter.devicelab',
'--template=plugin',
'--platforms=ios',
name,
],
);
});
final String pluginDir = path.join(parent.path, name);
// Convert the metadata to Dart-only.
final String dartPluginClass = 'DartClassFor$name';
final File pubspec = File(path.join(pluginDir, 'pubspec.yaml'));
String content = await pubspec.readAsString();
content = content.replaceAll(
RegExp(r' pluginClass: .*?\n'),
' dartPluginClass: $dartPluginClass\n',
);
await pubspec.writeAsString(content, flush: true);
// Add the Dart registration hook that the build will generate a call to.
final File dartCode = File(path.join(pluginDir, 'lib', '$name.dart'));
content = await dartCode.readAsString();
content = '''
$content
class $dartPluginClass {
static void registerWith() {}
}
''';
await dartCode.writeAsString(content, flush: true);
// Remove the native plugin code.
await Directory(path.join(pluginDir, 'ios')).delete(recursive: true);
}
......@@ -16,5 +16,7 @@ Future<void> main() async {
<String, String>{'ENABLE_ANDROID_EMBEDDING_V2': 'true'}),
PluginTest('apk', <String>['-a', 'kotlin', '--platforms=android'], pluginCreateEnvironment:
<String, String>{'ENABLE_ANDROID_EMBEDDING_V2': 'true'}),
// Test that Dart-only plugins are supported.
PluginTest('apk', <String>['--platforms=android'], dartOnlyPlugin: true),
]));
}
......@@ -10,5 +10,8 @@ Future<void> main() async {
PluginTest('ios', <String>['-i', 'objc', '--platforms=ios']),
PluginTest('ios', <String>['-i', 'swift', '--platforms=ios']),
PluginTest('macos', <String>['--platforms=macos']),
// Test that Dart-only plugins are supported.
PluginTest('ios', <String>['--platforms=ios'], dartOnlyPlugin: true),
PluginTest('macos', <String>['--platforms=macos'], dartOnlyPlugin: true),
]));
}
......@@ -26,12 +26,19 @@ TaskFunction combine(List<TaskFunction> tasks) {
/// Defines task that creates new Flutter project, adds a local and remote
/// plugin, and then builds the specified [buildTarget].
class PluginTest {
PluginTest(this.buildTarget, this.options, { this.pluginCreateEnvironment, this.appCreateEnvironment });
PluginTest(
this.buildTarget,
this.options, {
this.pluginCreateEnvironment,
this.appCreateEnvironment,
this.dartOnlyPlugin = false,
});
final String buildTarget;
final List<String> options;
final Map<String, String>? pluginCreateEnvironment;
final Map<String, String>? appCreateEnvironment;
final bool dartOnlyPlugin;
Future<TaskResult> call() async {
final Directory tempDir =
......@@ -41,6 +48,9 @@ class PluginTest {
final _FlutterProject plugin = await _FlutterProject.create(
tempDir, options, buildTarget,
name: 'plugintest', template: 'plugin', environment: pluginCreateEnvironment);
if (dartOnlyPlugin) {
await plugin.convertDefaultPluginToDartPlugin();
}
section('Test plugin');
await plugin.test();
section('Create Flutter app');
......@@ -52,7 +62,7 @@ class PluginTest {
pluginPath: path.join('..', 'plugintest'));
await app.addPlugin('path_provider');
section('Build app');
await app.build(buildTarget);
await app.build(buildTarget, validateNativeBuildProject: !dartOnlyPlugin);
section('Test app');
await app.test();
} finally {
......@@ -76,8 +86,10 @@ class _FlutterProject {
String get rootPath => path.join(parent.path, name);
File get pubspecFile => File(path.join(rootPath, 'pubspec.yaml'));
Future<void> addPlugin(String plugin, {String? pluginPath}) async {
final File pubspec = File(path.join(rootPath, 'pubspec.yaml'));
final File pubspec = pubspecFile;
String content = await pubspec.readAsString();
final String dependency =
pluginPath != null ? '$plugin:\n path: $pluginPath' : '$plugin:';
......@@ -88,6 +100,47 @@ class _FlutterProject {
await pubspec.writeAsString(content, flush: true);
}
/// Converts a plugin created from the standard template to a Dart-only
/// plugin.
Future<void> convertDefaultPluginToDartPlugin() async {
final String dartPluginClass = 'DartClassFor$name';
// Convert the metadata.
final File pubspec = pubspecFile;
String content = await pubspec.readAsString();
content = content.replaceAll(
RegExp(r' pluginClass: .*?\n'),
' dartPluginClass: $dartPluginClass\n',
);
await pubspec.writeAsString(content, flush: true);
// Add the Dart registration hook that the build will generate a call to.
final File dartCode = File(path.join(rootPath, 'lib', '$name.dart'));
content = await dartCode.readAsString();
content = '''
$content
class $dartPluginClass {
static void registerWith() {}
}
''';
await dartCode.writeAsString(content, flush: true);
// Remove any native plugin code.
const List<String> platforms = <String>[
'android',
'ios',
'linux',
'macos',
'windows',
];
for (final String platform in platforms) {
final Directory platformDir = Directory(path.join(rootPath, platform));
if (platformDir.existsSync()) {
await platformDir.delete(recursive: true);
}
}
}
Future<void> test() async {
await inDirectory(Directory(rootPath), () async {
await flutter('test');
......@@ -147,7 +200,7 @@ class _FlutterProject {
podspec.writeAsStringSync(podspecContent, flush: true);
}
Future<void> build(String target) async {
Future<void> build(String target, {bool validateNativeBuildProject = true}) async {
await inDirectory(Directory(rootPath), () async {
final String buildOutput = await evalFlutter('build', options: <String>[
target,
......@@ -167,6 +220,7 @@ class _FlutterProject {
throw TaskResult.failure('Minimum plugin version warning present');
}
if (validateNativeBuildProject) {
final File podsProject = File(path.join(rootPath, target, 'Pods', 'Pods.xcodeproj', 'project.pbxproj'));
if (!podsProject.existsSync()) {
throw TaskResult.failure('Xcode Pods project file missing at ${podsProject.path}');
......@@ -191,6 +245,7 @@ class _FlutterProject {
throw TaskResult.failure('Plugin build setting MACOSX_DEPLOYMENT_TARGET not removed');
}
}
}
});
}
......
......@@ -255,7 +255,8 @@ def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, pl
plugin_pods.each do |plugin_hash|
plugin_name = plugin_hash['name']
plugin_path = plugin_hash['path']
if (plugin_name && plugin_path)
has_native_build = plugin_hash.fetch('native_build', true)
if (plugin_name && plugin_path && has_native_build)
symlink = File.join(symlink_plugins_dir, plugin_name)
File.symlink(plugin_path, symlink)
......
......@@ -23,6 +23,12 @@ assert object.plugins.android instanceof List
object.plugins.android.each { androidPlugin ->
assert androidPlugin.name instanceof String
assert androidPlugin.path instanceof String
// Skip plugins that have no native build (such as a Dart-only implementation
// of a federated plugin).
def needsBuild = androidPlugin.containsKey('native_build') ? androidPlugin['native_build'] : true
if (!needsBuild) {
return
}
def pluginDirectory = new File(androidPlugin.path, 'android')
assert pluginDirectory.exists()
include ":${androidPlugin.name}"
......
......@@ -20,6 +20,12 @@ if (pluginsFile.exists()) {
object.plugins.android.each { androidPlugin ->
assert androidPlugin.name instanceof String
assert androidPlugin.path instanceof String
// Skip plugins that have no native build (such as a Dart-only
// implementation of a federated plugin).
def needsBuild = androidPlugin.containsKey('native_build') ? androidPlugin['native_build'] : true
if (!needsBuild) {
return
}
def pluginDirectory = new File(androidPlugin.path, 'android')
assert pluginDirectory.exists()
include ":${androidPlugin.name}"
......
......@@ -98,6 +98,7 @@ const String _kFlutterPluginsPluginListKey = 'plugins';
const String _kFlutterPluginsNameKey = 'name';
const String _kFlutterPluginsPathKey = 'path';
const String _kFlutterPluginsDependenciesKey = 'dependencies';
const String _kFlutterPluginsHasNativeBuildKey = 'native_build';
/// Filters [plugins] to those supported by [platformKey].
List<Map<String, Object>> _filterPluginsByPlatform(List<Plugin> plugins, String platformKey) {
......@@ -108,9 +109,13 @@ List<Map<String, Object>> _filterPluginsByPlatform(List<Plugin> plugins, String
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 NativeOrDartPlugin)
_kFlutterPluginsHasNativeBuildKey: (platformPlugin as NativeOrDartPlugin).isNative(),
_kFlutterPluginsDependenciesKey: <String>[...plugin.dependencies.where(pluginNames.contains)],
});
}
......@@ -130,7 +135,8 @@ List<Map<String, Object>> _filterPluginsByPlatform(List<Plugin> plugins, String
/// "dependencies": [
/// "plugin-a",
/// "plugin-b"
/// ]
/// ],
/// "native_build": true
/// }
/// ],
/// "android": [],
......
......@@ -81,7 +81,8 @@ def install_flutter_plugin_pods(flutter_application_path)
plugin_pods.each do |plugin_hash|
plugin_name = plugin_hash['name']
plugin_path = plugin_hash['path']
if (plugin_name && plugin_path)
has_native_build = plugin_hash.fetch('native_build', true)
if (plugin_name && plugin_path && has_native_build)
symlink = File.join(symlinks_dir, plugin_name)
FileUtils.rm_f(symlink)
File.symlink(plugin_path, symlink)
......
......@@ -30,6 +30,46 @@ import '../src/context.dart';
import '../src/fakes.dart' hide FakeOperatingSystemUtils;
import '../src/pubspec_schema.dart';
/// Information for a platform entry in the 'platforms' section of a plugin's
/// pubspec.yaml.
class _PluginPlatformInfo {
const _PluginPlatformInfo({
this.pluginClass,
this.dartPluginClass,
this.androidPackage,
this.fileName
}) : assert(pluginClass != null || dartPluginClass != null),
assert(androidPackage == null || pluginClass != null);
/// The pluginClass entry, if any.
final String pluginClass;
/// The dartPluginClass entry, if any.
final String dartPluginClass;
/// The package entry for an Android plugin implementation using pluginClass.
final String androidPackage;
/// The fileName entry for a web plugin implementation.
final String fileName;
/// Returns the body of a platform section for a plugin's pubspec, properly
/// indented.
String get indentedPubspecSection {
const String indentation = ' ';
return <String>[
if (pluginClass != null)
'${indentation}pluginClass: $pluginClass',
if (dartPluginClass != null)
'${indentation}dartPluginClass: $dartPluginClass',
if (androidPackage != null)
'${indentation}package: $androidPackage',
if (fileName != null)
'${indentation}fileName: $fileName',
].join('\n');
}
}
void main() {
group('plugins', () {
FileSystem fs;
......@@ -331,7 +371,7 @@ flutter:
);
}
Directory createPluginWithDependencies({
Directory createLegacyPluginWithDependencies({
@required String name,
@required List<String> dependencies,
}) {
......@@ -347,6 +387,44 @@ flutter:
plugin:
androidPackage: plugin2
pluginClass: UseNewEmbedding
dependencies:
''');
for (final String dependency in dependencies) {
pluginDirectory
.childFile('pubspec.yaml')
.writeAsStringSync(' $dependency:\n', mode: FileMode.append);
}
flutterProject.directory
.childFile('.packages')
.writeAsStringSync(
'$name:${pluginDirectory.childDirectory('lib').uri.toString()}\n',
mode: FileMode.append,
);
return pluginDirectory;
}
Directory createPlugin({
@required String name,
@required Map<String, _PluginPlatformInfo> platforms,
List<String> dependencies = const <String>[],
}) {
assert(name != null);
assert(dependencies != null);
final Iterable<String> platformSections = platforms.entries.map((MapEntry<String, _PluginPlatformInfo> entry) => '''
${entry.key}:
${entry.value.indentedPubspecSection}
''');
final Directory pluginDirectory = fs.systemTempDirectory.createTempSync('flutter_plugin.');
pluginDirectory
.childFile('pubspec.yaml')
.writeAsStringSync('''
name: $name
flutter:
plugin:
platforms:
${platformSections.join('\n')}
dependencies:
''');
for (final String dependency in dependencies) {
......@@ -420,9 +498,9 @@ dependencies:
testUsingContext(
'Refreshing the plugin list modifies .flutter-plugins '
'and .flutter-plugins-dependencies when there are plugins', () async {
final Directory pluginA = createPluginWithDependencies(name: 'plugin-a', dependencies: const <String>['plugin-b', 'plugin-c', 'random-package']);
final Directory pluginB = createPluginWithDependencies(name: 'plugin-b', dependencies: const <String>['plugin-c']);
final Directory pluginC = createPluginWithDependencies(name: 'plugin-c', dependencies: const <String>[]);
final Directory pluginA = createLegacyPluginWithDependencies(name: 'plugin-a', dependencies: const <String>['plugin-b', 'plugin-c', 'random-package']);
final Directory pluginB = createLegacyPluginWithDependencies(name: 'plugin-b', dependencies: const <String>['plugin-c']);
final Directory pluginC = createLegacyPluginWithDependencies(name: 'plugin-c', dependencies: const <String>[]);
iosProject.testExists = true;
final DateTime dateCreated = DateTime(1970);
......@@ -449,22 +527,25 @@ dependencies:
<String, dynamic> {
'name': 'plugin-a',
'path': '${pluginA.path}/',
'native_build': true,
'dependencies': <String>[
'plugin-b',
'plugin-c'
]
],
},
<String, dynamic> {
'name': 'plugin-b',
'path': '${pluginB.path}/',
'native_build': true,
'dependencies': <String>[
'plugin-c'
]
],
},
<String, dynamic> {
'name': 'plugin-c',
'path': '${pluginC.path}/',
'dependencies': <String>[]
'native_build': true,
'dependencies': <String>[],
},
];
expect(plugins['ios'], expectedPlugins);
......@@ -514,6 +595,51 @@ dependencies:
FlutterVersion: () => flutterVersion
});
testUsingContext(
'.flutter-plugins-dependencies indicates native build inclusion', () async {
createPlugin(
name: 'plugin-a',
platforms: const <String, _PluginPlatformInfo>{
// Native-only; should include native build.
'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'),
// Hybrid native and Dart; should include native build.
'ios': _PluginPlatformInfo(pluginClass: 'Foo', dartPluginClass: 'Bar'),
// Web; should not have the native build key at all since it doesn't apply.
'web': _PluginPlatformInfo(pluginClass: 'Foo', fileName: 'lib/foo.dart'),
// Dart-only; should not include native build.
'windows': _PluginPlatformInfo(dartPluginClass: 'Foo'),
});
iosProject.testExists = true;
final DateTime dateCreated = DateTime(1970);
systemClock.currentTime = dateCreated;
await refreshPluginsList(flutterProject);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
final String pluginsString = flutterProject.flutterPluginsDependenciesFile.readAsStringSync();
final Map<String, dynamic> jsonContent = json.decode(pluginsString) as Map<String, dynamic>;
final Map<String, dynamic> plugins = jsonContent['plugins'] as Map<String, dynamic>;
// Extracts the native_build key (if any) from the first plugin for the
// given platform.
bool getNativeBuildValue(String platform) {
final List<Map<String, dynamic>> platformPlugins = (plugins[platform]
as List<dynamic>).cast<Map<String, dynamic>>();
expect(platformPlugins.length, 1);
return platformPlugins[0]['native_build'] as bool;
}
expect(getNativeBuildValue('android'), true);
expect(getNativeBuildValue('ios'), true);
expect(getNativeBuildValue('web'), null);
expect(getNativeBuildValue('windows'), false);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
SystemClock: () => systemClock,
FlutterVersion: () => flutterVersion
});
testUsingContext('Changes to the plugin list invalidates the Cocoapod lockfiles', () async {
simulatePodInstallRun(iosProject);
simulatePodInstallRun(macosProject);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment