diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index 86c2124efcc..758a0a1bf94 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -1161,6 +1161,8 @@ Future refreshPluginsList(FlutterProject project, {bool checkProjects = fa /// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`. Future injectPlugins(FlutterProject project, {bool checkProjects = false}) async { final List 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 ((checkProjects && project.android.existsSync()) || !checkProjects) { await _writeAndroidPluginRegistrant(project, plugins); } diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index 9b09e85d601..a589c51bc34 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -8,6 +8,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/time.dart'; +import 'package:flutter_tools/src/base/utils.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/ios/xcodeproj.dart'; @@ -122,37 +123,57 @@ void main() { ); }); - const String _pluginYaml = ''' + // Makes fake plugin packages for each plugin, adds them to flutterProject, + // and returns their directories. + // + // If an entry contains a path separator, it will be treated as a path for + // the location of the package, with the name being the last component. + // Otherwise it will be treated as a name, and put in a default location + // (a fake pub cache). + List createFakePlugins(FileSystem fileSystem, List pluginNamesOrPaths) { + const String pluginYamlTemplate = ''' flutter: plugin: platforms: ios: - pluginClass: SomePlugin + pluginClass: PLUGIN_CLASS macos: - pluginClass: SomePlugin + pluginClass: PLUGIN_CLASS windows: - pluginClass: SomePlugin + pluginClass: PLUGIN_CLASS linux: - pluginClass: SomePlugin + pluginClass: PLUGIN_CLASS web: - pluginClass: SomePlugin - fileName: lib/SomeFile.dart + pluginClass: PLUGIN_CLASS + fileName: lib/PLUGIN_CLASS.dart android: - pluginClass: SomePlugin + pluginClass: PLUGIN_CLASS package: AndroidPackage '''; + final List directories = []; + final Directory fakePubCache = fileSystem.systemTempDirectory.childDirectory('cache'); + final File packagesFile = flutterProject.directory.childFile('.packages') + ..createSync(recursive: true); + for (final String nameOrPath in pluginNamesOrPaths) { + final String name = fileSystem.path.basename(nameOrPath); + final Directory pluginDirectory = (nameOrPath == name) + ? fakePubCache.childDirectory(name) + : fileSystem.directory(nameOrPath); + packagesFile.writeAsStringSync( + '$name:file://${pluginDirectory.childFile('lib').uri}\n', + mode: FileMode.writeOnlyAppend); + pluginDirectory.childFile('pubspec.yaml') + ..createSync(recursive: true) + ..writeAsStringSync(pluginYamlTemplate.replaceAll('PLUGIN_CLASS', toTitleCase(camelCase(name)))); + directories.add(pluginDirectory); + } + return directories; + } + // Makes a fake plugin package, adds it to flutterProject, and returns its directory. Directory createFakePlugin(FileSystem fileSystem) { - const String name = 'apackage'; - final Directory packageDirectory = fileSystem.systemTempDirectory.childDirectory('cache').childDirectory(name); - flutterProject.directory.childFile('.packages') - ..createSync(recursive: true) - ..writeAsStringSync('$name:file://${packageDirectory.childFile('lib').uri}\n'); - packageDirectory.childFile('pubspec.yaml') - ..createSync(recursive: true) - ..writeAsStringSync(_pluginYaml); - return packageDirectory; + return createFakePlugins(fileSystem, ['some_plugin'])[0]; } void createNewJavaPlugin1() { @@ -1034,7 +1055,7 @@ flutter: expect(pluginMakefile.existsSync(), isTrue); final String contents = pluginMakefile.readAsStringSync(); - expect(contents, contains('apackage')); + expect(contents, contains('some_plugin')); expect(contents, contains('target_link_libraries(\${BINARY_NAME} PRIVATE \${plugin}_plugin)')); expect(contents, contains('list(APPEND PLUGIN_BUNDLED_LIBRARIES \$)')); expect(contents, contains('list(APPEND PLUGIN_BUNDLED_LIBRARIES \${\${plugin}_bundled_libraries})')); @@ -1044,6 +1065,32 @@ flutter: FeatureFlags: () => featureFlags, }); + testUsingContext('Generated Linux plugin files sorts by plugin name', () async { + when(linuxProject.existsSync()).thenReturn(true); + when(featureFlags.isLinuxEnabled).thenReturn(true); + when(flutterProject.isModule).thenReturn(false); + createFakePlugins(fs, [ + 'plugin_d', + 'plugin_a', + '/local_plugins/plugin_c', + '/local_plugins/plugin_b' + ]); + + await injectPlugins(flutterProject, checkProjects: true); + + final File pluginCmakeFile = linuxProject.generatedPluginCmakeFile; + final File pluginRegistrant = linuxProject.managedDirectory.childFile('generated_plugin_registrant.cc'); + for (final File file in [pluginCmakeFile, pluginRegistrant]) { + final String contents = file.readAsStringSync(); + expect(contents.indexOf('plugin_a'), lessThan(contents.indexOf('plugin_b'))); + expect(contents.indexOf('plugin_b'), lessThan(contents.indexOf('plugin_c'))); + expect(contents.indexOf('plugin_c'), lessThan(contents.indexOf('plugin_d'))); + } + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + FeatureFlags: () => featureFlags, + }); testUsingContext('Injecting creates generated Windows registrant', () async { when(windowsProject.existsSync()).thenReturn(true); @@ -1119,22 +1166,27 @@ flutter: FeatureFlags: () => featureFlags, }); - testUsingContext('Injecting creates generated Windows plugin CMake file', () async { + testUsingContext('Generated Windows plugin files sorts by plugin name', () async { when(windowsProject.existsSync()).thenReturn(true); when(featureFlags.isWindowsEnabled).thenReturn(true); when(flutterProject.isModule).thenReturn(false); - createFakePlugin(fs); + createFakePlugins(fs, [ + 'plugin_d', + 'plugin_a', + '/local_plugins/plugin_c', + '/local_plugins/plugin_b' + ]); await injectPlugins(flutterProject, checkProjects: true); - final File pluginMakefile = windowsProject.generatedPluginCmakeFile; - - expect(pluginMakefile.existsSync(), isTrue); - final String contents = pluginMakefile.readAsStringSync(); - expect(contents, contains('apackage')); - expect(contents, contains('target_link_libraries(\${BINARY_NAME} PRIVATE \${plugin}_plugin)')); - expect(contents, contains('list(APPEND PLUGIN_BUNDLED_LIBRARIES \$)')); - expect(contents, contains('list(APPEND PLUGIN_BUNDLED_LIBRARIES \${\${plugin}_bundled_libraries})')); + final File pluginCmakeFile = windowsProject.generatedPluginCmakeFile; + final File pluginRegistrant = windowsProject.managedDirectory.childFile('generated_plugin_registrant.cc'); + for (final File file in [pluginCmakeFile, pluginRegistrant]) { + final String contents = file.readAsStringSync(); + expect(contents.indexOf('plugin_a'), lessThan(contents.indexOf('plugin_b'))); + expect(contents.indexOf('plugin_b'), lessThan(contents.indexOf('plugin_c'))); + expect(contents.indexOf('plugin_c'), lessThan(contents.indexOf('plugin_d'))); + } }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), @@ -1183,7 +1235,7 @@ flutter: // refreshPluginsList should call createPluginSymlinks. await refreshPluginsList(flutterProject); - expect(linuxProject.pluginSymlinkDirectory.childLink('apackage').existsSync(), true); + expect(linuxProject.pluginSymlinkDirectory.childLink('some_plugin').existsSync(), true); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), @@ -1196,7 +1248,7 @@ flutter: // refreshPluginsList should call createPluginSymlinks. await refreshPluginsList(flutterProject); - expect(windowsProject.pluginSymlinkDirectory.childLink('apackage').existsSync(), true); + expect(windowsProject.pluginSymlinkDirectory.childLink('some_plugin').existsSync(), true); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), @@ -1282,8 +1334,8 @@ flutter: await refreshPluginsList(flutterProject); final List links = [ - linuxProject.pluginSymlinkDirectory.childLink('apackage'), - windowsProject.pluginSymlinkDirectory.childLink('apackage'), + linuxProject.pluginSymlinkDirectory.childLink('some_plugin'), + windowsProject.pluginSymlinkDirectory.childLink('some_plugin'), ]; for (final Link link in links) { link.deleteSync(); @@ -1318,7 +1370,26 @@ flutter: } test('validatePubspecForPlugin works', () async { - _createPubspecFile(_pluginYaml); + const String pluginYaml = ''' + flutter: + plugin: + platforms: + ios: + pluginClass: SomePlugin + macos: + pluginClass: SomePlugin + windows: + pluginClass: SomePlugin + linux: + pluginClass: SomePlugin + web: + pluginClass: SomePlugin + fileName: lib/SomeFile.dart + android: + pluginClass: SomePlugin + package: AndroidPackage + '''; + _createPubspecFile(pluginYaml); validatePubspecForPlugin(projectDir: projectDir.absolute.path, pluginClass: 'SomePlugin', expectedPlatforms: [ 'ios', 'macos', 'windows', 'linux', 'android', 'web' ], androidIdentifier: 'AndroidPackage', webFileName: 'lib/SomeFile.dart');