From f73fe8f58ce90f8d941514273e73422e6db5e5f3 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 16 Jun 2021 11:53:46 -0700 Subject: [PATCH 1/3] [url_launcher] Add a workaround for Uri encoding (#3817) `Uri`'s constructor doesn't handle query parameters correctly for non-http(s) schemes, so the `mailto` example in the README is misleading. This updates the README to show using a simple method to work around that bug, and a warning about the need to use it. Fixes https://github.com/flutter/flutter/issues/75552 Fixes https://github.com/flutter/flutter/issues/73717 --- .../url_launcher/url_launcher/CHANGELOG.md | 5 +++ packages/url_launcher/url_launcher/README.md | 36 ++++++++++++------- .../url_launcher/url_launcher/pubspec.yaml | 2 +- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 03a3f6fb0b76..697b7c7816dd 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,8 @@ +## 6.0.7 + +* Update the README to describe a workaround to the `Uri` query + encoding bug. + ## 6.0.6 * Require `url_launcher_platform_interface` 2.0.3. This fixes an issue diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index 31fed9a833f1..20ee0a59caa8 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -10,10 +10,10 @@ To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml fil ## Installation -### iOS +### iOS Add any URL schemes passed to `canLaunch` as `LSApplicationQueriesSchemes` entries in your Info.plist file. -Example: +Example: ``` LSApplicationQueriesSchemes @@ -73,25 +73,35 @@ apps installed, so can't open `tel:` or `mailto:` links. ### Encoding URLs -URLs must be properly encoded, especially when including spaces or other special characters. This can be done using the [`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html): +URLs must be properly encoded, especially when including spaces or other special +characters. This can be done using the +[`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html). +For example: ```dart -import 'dart:core'; -import 'package:url_launcher/url_launcher.dart'; +String? encodeQueryParameters(Map params) { + return params.entries + .map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') + .join('&'); +} -final Uri _emailLaunchUri = Uri( +final Uri emailLaunchUri = Uri( scheme: 'mailto', path: 'smith@example.com', - queryParameters: { + query: encodeQueryParameters({ 'subject': 'Example Subject & Symbols are allowed!' - } + }), ); -// ... - -// mailto:smith@example.com?subject=Example+Subject+%26+Symbols+are+allowed%21 -launch(_emailLaunchUri.toString()); +launch(emailLaunchUri.toString()); ``` +**Warning**: For any scheme other than `http` or `https`, you should use the +`query` parameter and the `encodeQueryParameters` function shown above rather +than `Uri`'s `queryParameters` constructor argument, due to +[a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri` +encodes query parameters. Using `queryParameters` will result in spaces being +converted to `+` in many cases. + ## Handling missing URL receivers A particular mobile device may not be able to receive all supported URL schemes. @@ -113,4 +123,4 @@ By default, Android opens up a browser when handling URLs. You can pass If you do this for a URL of a page containing JavaScript, make sure to pass in `enableJavaScript: true`, or else the launch method will not work properly. On iOS, the default behavior is to open all web URLs within the app. Everything -else is redirected to the app handler. \ No newline at end of file +else is redirected to the app handler. diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 00cfc218ce9e..a2facbd3adf2 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.6 +version: 6.0.7 environment: sdk: ">=2.12.0 <3.0.0" From d1ddb68ebe6b7c1faafa0e32af1275f6c3f3a8b0 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 16 Jun 2021 12:37:30 -0700 Subject: [PATCH 2/3] [flutter_plugin_tools] Split common.dart (#4057) common.dart is a large-and-growing file containing all shared code, which makes it hard to navigate. To make maintenance easier, this splits the file (and its test file) into separate files for each major component or category. --- script/tool/lib/src/analyze_command.dart | 4 +- .../tool/lib/src/build_examples_command.dart | 5 +- script/tool/lib/src/common.dart | 781 ------------------ script/tool/lib/src/common/core.dart | 69 ++ .../lib/src/common/git_version_finder.dart | 81 ++ .../tool/lib/src/common/plugin_command.dart | 353 ++++++++ script/tool/lib/src/common/plugin_utils.dart | 110 +++ .../tool/lib/src/common/process_runner.dart | 104 +++ .../lib/src/common/pub_version_finder.dart | 103 +++ .../src/create_all_plugins_app_command.dart | 3 +- .../tool/lib/src/drive_examples_command.dart | 7 +- .../lib/src/firebase_test_lab_command.dart | 4 +- script/tool/lib/src/format_command.dart | 5 +- script/tool/lib/src/java_test_command.dart | 6 +- .../tool/lib/src/license_check_command.dart | 5 +- .../tool/lib/src/lint_podspecs_command.dart | 4 +- script/tool/lib/src/list_command.dart | 4 +- script/tool/lib/src/main.dart | 2 +- .../tool/lib/src/publish_check_command.dart | 5 +- .../tool/lib/src/publish_plugin_command.dart | 5 +- .../tool/lib/src/pubspec_check_command.dart | 6 +- script/tool/lib/src/test_command.dart | 7 +- .../tool/lib/src/version_check_command.dart | 8 +- script/tool/lib/src/xctest_command.dart | 6 +- script/tool/test/analyze_command_test.dart | 2 +- .../test/common/git_version_finder_test.dart | 93 +++ .../plugin_command_test.dart} | 361 +------- .../plugin_command_test.mocks.dart} | 0 .../tool/test/common/plugin_utils_test.dart | 210 +++++ .../test/common/pub_version_finder_test.dart | 89 ++ .../test/drive_examples_command_test.dart | 2 +- script/tool/test/firebase_test_lab_test.dart | 2 +- .../tool/test/license_check_command_test.dart | 2 +- .../tool/test/publish_check_command_test.dart | 2 +- .../test/publish_plugin_command_test.dart | 3 +- .../tool/test/pubspec_check_command_test.dart | 2 +- script/tool/test/util.dart | 3 +- script/tool/test/version_check_test.dart | 4 +- script/tool/test/xctest_command_test.dart | 3 +- 39 files changed, 1284 insertions(+), 1181 deletions(-) delete mode 100644 script/tool/lib/src/common.dart create mode 100644 script/tool/lib/src/common/core.dart create mode 100644 script/tool/lib/src/common/git_version_finder.dart create mode 100644 script/tool/lib/src/common/plugin_command.dart create mode 100644 script/tool/lib/src/common/plugin_utils.dart create mode 100644 script/tool/lib/src/common/process_runner.dart create mode 100644 script/tool/lib/src/common/pub_version_finder.dart create mode 100644 script/tool/test/common/git_version_finder_test.dart rename script/tool/test/{common_test.dart => common/plugin_command_test.dart} (51%) rename script/tool/test/{common_test.mocks.dart => common/plugin_command_test.mocks.dart} (100%) create mode 100644 script/tool/test/common/plugin_utils_test.dart create mode 100644 script/tool/test/common/pub_version_finder_test.dart diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 076c8f69885d..003f0bcda82d 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -7,7 +7,9 @@ import 'dart:async'; import 'package:file/file.dart'; import 'package:path/path.dart' as p; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; /// A command to run Dart analysis on packages. class AnalyzeCommand extends PluginCommand { diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 9590aecef98e..61d291d87c68 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -9,7 +9,10 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; /// Key for IPA. const String kIpa = 'ipa'; diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart deleted file mode 100644 index 5d653ad0ed20..000000000000 --- a/script/tool/lib/src/common.dart +++ /dev/null @@ -1,781 +0,0 @@ -// Copyright 2013 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 'dart:convert'; -import 'dart:io' as io; -import 'dart:math'; - -import 'package:args/command_runner.dart'; -import 'package:colorize/colorize.dart'; -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; -import 'package:yaml/yaml.dart'; - -/// The signature for a print handler for commands that allow overriding the -/// print destination. -typedef Print = void Function(Object? object); - -/// Key for windows platform. -const String kPlatformFlagWindows = 'windows'; - -/// Key for macos platform. -const String kPlatformFlagMacos = 'macos'; - -/// Key for linux platform. -const String kPlatformFlagLinux = 'linux'; - -/// Key for IPA (iOS) platform. -const String kPlatformFlagIos = 'ios'; - -/// Key for APK (Android) platform. -const String kPlatformFlagAndroid = 'android'; - -/// Key for Web platform. -const String kPlatformFlagWeb = 'web'; - -/// Key for enable experiment. -const String kEnableExperiment = 'enable-experiment'; - -/// Returns whether the given directory contains a Flutter package. -bool isFlutterPackage(FileSystemEntity entity) { - if (entity is! Directory) { - return false; - } - - try { - final File pubspecFile = entity.childFile('pubspec.yaml'); - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? dependencies = pubspecYaml['dependencies'] as YamlMap?; - if (dependencies == null) { - return false; - } - return dependencies.containsKey('flutter'); - } on FileSystemException { - return false; - } on YamlException { - return false; - } -} - -/// Possible plugin support options for a platform. -enum PlatformSupport { - /// The platform has an implementation in the package. - inline, - - /// The platform has an endorsed federated implementation in another package. - federated, -} - -/// Returns whether the given directory contains a Flutter [platform] plugin. -/// -/// It checks this by looking for the following pattern in the pubspec: -/// -/// flutter: -/// plugin: -/// platforms: -/// [platform]: -/// -/// If [requiredMode] is provided, the plugin must have the given type of -/// implementation in order to return true. -bool pluginSupportsPlatform(String platform, FileSystemEntity entity, - {PlatformSupport? requiredMode}) { - assert(platform == kPlatformFlagIos || - platform == kPlatformFlagAndroid || - platform == kPlatformFlagWeb || - platform == kPlatformFlagMacos || - platform == kPlatformFlagWindows || - platform == kPlatformFlagLinux); - if (entity is! Directory) { - return false; - } - - try { - final File pubspecFile = entity.childFile('pubspec.yaml'); - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; - if (flutterSection == null) { - return false; - } - final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; - if (pluginSection == null) { - return false; - } - final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; - if (platforms == null) { - // Legacy plugin specs are assumed to support iOS and Android. They are - // never federated. - if (requiredMode == PlatformSupport.federated) { - return false; - } - if (!pluginSection.containsKey('platforms')) { - return platform == kPlatformFlagIos || platform == kPlatformFlagAndroid; - } - return false; - } - final YamlMap? platformEntry = platforms[platform] as YamlMap?; - if (platformEntry == null) { - return false; - } - // If the platform entry is present, then it supports the platform. Check - // for required mode if specified. - final bool federated = platformEntry.containsKey('default_package'); - return requiredMode == null || - federated == (requiredMode == PlatformSupport.federated); - } on FileSystemException { - return false; - } on YamlException { - return false; - } -} - -/// Returns whether the given directory contains a Flutter Android plugin. -bool isAndroidPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagAndroid, entity); -} - -/// Returns whether the given directory contains a Flutter iOS plugin. -bool isIosPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagIos, entity); -} - -/// Returns whether the given directory contains a Flutter web plugin. -bool isWebPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagWeb, entity); -} - -/// Returns whether the given directory contains a Flutter Windows plugin. -bool isWindowsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagWindows, entity); -} - -/// Returns whether the given directory contains a Flutter macOS plugin. -bool isMacOsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagMacos, entity); -} - -/// Returns whether the given directory contains a Flutter linux plugin. -bool isLinuxPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagLinux, entity); -} - -/// Prints `errorMessage` in red. -void printError(String errorMessage) { - final Colorize redError = Colorize(errorMessage)..red(); - print(redError); -} - -/// Error thrown when a command needs to exit with a non-zero exit code. -class ToolExit extends Error { - /// Creates a tool exit with the given [exitCode]. - ToolExit(this.exitCode); - - /// The code that the process should exit with. - final int exitCode; -} - -/// Interface definition for all commands in this tool. -abstract class PluginCommand extends Command { - /// Creates a command to operate on [packagesDir] with the given environment. - PluginCommand( - this.packagesDir, { - this.processRunner = const ProcessRunner(), - this.gitDir, - }) { - argParser.addMultiOption( - _pluginsArg, - splitCommas: true, - help: - 'Specifies which plugins the command should run on (before sharding).', - valueHelp: 'plugin1,plugin2,...', - ); - argParser.addOption( - _shardIndexArg, - help: 'Specifies the zero-based index of the shard to ' - 'which the command applies.', - valueHelp: 'i', - defaultsTo: '0', - ); - argParser.addOption( - _shardCountArg, - help: 'Specifies the number of shards into which plugins are divided.', - valueHelp: 'n', - defaultsTo: '1', - ); - argParser.addMultiOption( - _excludeArg, - abbr: 'e', - help: 'Exclude packages from this command.', - defaultsTo: [], - ); - argParser.addFlag(_runOnChangedPackagesArg, - help: 'Run the command on changed packages/plugins.\n' - 'If the $_pluginsArg is specified, this flag is ignored.\n' - 'If no packages have changed, or if there have been changes that may\n' - 'affect all packages, the command runs on all packages.\n' - 'The packages excluded with $_excludeArg is also excluded even if changed.\n' - 'See $_kBaseSha if a custom base is needed to determine the diff.'); - argParser.addOption(_kBaseSha, - help: 'The base sha used to determine git diff. \n' - 'This is useful when $_runOnChangedPackagesArg is specified.\n' - 'If not specified, merge-base is used as base sha.'); - } - - static const String _pluginsArg = 'plugins'; - static const String _shardIndexArg = 'shardIndex'; - static const String _shardCountArg = 'shardCount'; - static const String _excludeArg = 'exclude'; - static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; - static const String _kBaseSha = 'base-sha'; - - /// The directory containing the plugin packages. - final Directory packagesDir; - - /// The process runner. - /// - /// This can be overridden for testing. - final ProcessRunner processRunner; - - /// The git directory to use. By default it uses the parent directory. - /// - /// This can be mocked for testing. - final GitDir? gitDir; - - int? _shardIndex; - int? _shardCount; - - /// The shard of the overall command execution that this instance should run. - int get shardIndex { - if (_shardIndex == null) { - _checkSharding(); - } - return _shardIndex!; - } - - /// The number of shards this command is divided into. - int get shardCount { - if (_shardCount == null) { - _checkSharding(); - } - return _shardCount!; - } - - /// Convenience accessor for boolean arguments. - bool getBoolArg(String key) { - return (argResults![key] as bool?) ?? false; - } - - /// Convenience accessor for String arguments. - String getStringArg(String key) { - return (argResults![key] as String?) ?? ''; - } - - /// Convenience accessor for List arguments. - List getStringListArg(String key) { - return (argResults![key] as List?) ?? []; - } - - void _checkSharding() { - final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg)); - final int? shardCount = int.tryParse(getStringArg(_shardCountArg)); - if (shardIndex == null) { - usageException('$_shardIndexArg must be an integer'); - } - if (shardCount == null) { - usageException('$_shardCountArg must be an integer'); - } - if (shardCount < 1) { - usageException('$_shardCountArg must be positive'); - } - if (shardIndex < 0 || shardCount <= shardIndex) { - usageException( - '$_shardIndexArg must be in the half-open range [0..$shardCount['); - } - _shardIndex = shardIndex; - _shardCount = shardCount; - } - - /// Returns the root Dart package folders of the plugins involved in this - /// command execution. - Stream getPlugins() async* { - // To avoid assuming consistency of `Directory.list` across command - // invocations, we collect and sort the plugin folders before sharding. - // This is considered an implementation detail which is why the API still - // uses streams. - final List allPlugins = await _getAllPlugins().toList(); - allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); - // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. - // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. - // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. - final int shardSize = allPlugins.length ~/ shardCount + - (allPlugins.length % shardCount == 0 ? 0 : 1); - final int start = min(shardIndex * shardSize, allPlugins.length); - final int end = min(start + shardSize, allPlugins.length); - - for (final Directory plugin in allPlugins.sublist(start, end)) { - yield plugin; - } - } - - /// Returns the root Dart package folders of the plugins involved in this - /// command execution, assuming there is only one shard. - /// - /// Plugin packages can exist in the following places relative to the packages - /// directory: - /// - /// 1. As a Dart package in a directory which is a direct child of the - /// packages directory. This is a plugin where all of the implementations - /// exist in a single Dart package. - /// 2. Several plugin packages may live in a directory which is a direct - /// child of the packages directory. This directory groups several Dart - /// packages which implement a single plugin. This directory contains a - /// "client library" package, which declares the API for the plugin, as - /// well as one or more platform-specific implementations. - /// 3./4. Either of the above, but in a third_party/packages/ directory that - /// is a sibling of the packages directory. This is used for a small number - /// of packages in the flutter/packages repository. - Stream _getAllPlugins() async* { - Set plugins = Set.from(getStringListArg(_pluginsArg)); - final Set excludedPlugins = - Set.from(getStringListArg(_excludeArg)); - final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); - if (plugins.isEmpty && - runOnChangedPackages && - !(await _changesRequireFullTest())) { - plugins = await _getChangedPackages(); - } - - final Directory thirdPartyPackagesDirectory = packagesDir.parent - .childDirectory('third_party') - .childDirectory('packages'); - - for (final Directory dir in [ - packagesDir, - if (thirdPartyPackagesDirectory.existsSync()) thirdPartyPackagesDirectory, - ]) { - await for (final FileSystemEntity entity - in dir.list(followLinks: false)) { - // A top-level Dart package is a plugin package. - if (_isDartPackage(entity)) { - if (!excludedPlugins.contains(entity.basename) && - (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { - yield entity as Directory; - } - } else if (entity is Directory) { - // Look for Dart packages under this top-level directory. - await for (final FileSystemEntity subdir - in entity.list(followLinks: false)) { - if (_isDartPackage(subdir)) { - // If --plugin=my_plugin is passed, then match all federated - // plugins under 'my_plugin'. Also match if the exact plugin is - // passed. - final String relativePath = - p.relative(subdir.path, from: dir.path); - final String packageName = p.basename(subdir.path); - final String basenamePath = p.basename(entity.path); - if (!excludedPlugins.contains(basenamePath) && - !excludedPlugins.contains(packageName) && - !excludedPlugins.contains(relativePath) && - (plugins.isEmpty || - plugins.contains(relativePath) || - plugins.contains(basenamePath))) { - yield subdir as Directory; - } - } - } - } - } - } - } - - /// Returns the example Dart package folders of the plugins involved in this - /// command execution. - Stream getExamples() => - getPlugins().expand(getExamplesForPlugin); - - /// Returns all Dart package folders (typically, plugin + example) of the - /// plugins involved in this command execution. - Stream getPackages() async* { - await for (final Directory plugin in getPlugins()) { - yield plugin; - yield* plugin - .list(recursive: true, followLinks: false) - .where(_isDartPackage) - .cast(); - } - } - - /// Returns the files contained, recursively, within the plugins - /// involved in this command execution. - Stream getFiles() { - return getPlugins().asyncExpand((Directory folder) => folder - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => entity is File) - .cast()); - } - - /// Returns whether the specified entity is a directory containing a - /// `pubspec.yaml` file. - bool _isDartPackage(FileSystemEntity entity) { - return entity is Directory && entity.childFile('pubspec.yaml').existsSync(); - } - - /// Returns the example Dart packages contained in the specified plugin, or - /// an empty List, if the plugin has no examples. - Iterable getExamplesForPlugin(Directory plugin) { - final Directory exampleFolder = plugin.childDirectory('example'); - if (!exampleFolder.existsSync()) { - return []; - } - if (isFlutterPackage(exampleFolder)) { - return [exampleFolder]; - } - // Only look at the subdirectories of the example directory if the example - // directory itself is not a Dart package, and only look one level below the - // example directory for other dart packages. - return exampleFolder - .listSync() - .where((FileSystemEntity entity) => isFlutterPackage(entity)) - .cast(); - } - - /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir]. - /// - /// Throws tool exit if [gitDir] nor root directory is a git directory. - Future retrieveVersionFinder() async { - final String rootDir = packagesDir.parent.absolute.path; - final String baseSha = getStringArg(_kBaseSha); - - GitDir? baseGitDir = gitDir; - if (baseGitDir == null) { - if (!await GitDir.isGitDir(rootDir)) { - printError( - '$rootDir is not a valid Git repository.', - ); - throw ToolExit(2); - } - baseGitDir = await GitDir.fromExisting(rootDir); - } - - final GitVersionFinder gitVersionFinder = - GitVersionFinder(baseGitDir, baseSha); - return gitVersionFinder; - } - - // Returns packages that have been changed relative to the git base. - Future> _getChangedPackages() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - - final List allChangedFiles = - await gitVersionFinder.getChangedFiles(); - final Set packages = {}; - for (final String path in allChangedFiles) { - final List pathComponents = path.split('/'); - final int packagesIndex = - pathComponents.indexWhere((String element) => element == 'packages'); - if (packagesIndex != -1) { - packages.add(pathComponents[packagesIndex + 1]); - } - } - if (packages.isEmpty) { - print('No changed packages.'); - } else { - final String changedPackages = packages.join(','); - print('Changed packages: $changedPackages'); - } - return packages; - } - - // Returns true if one or more files changed that have the potential to affect - // any plugin (e.g., CI script changes). - Future _changesRequireFullTest() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - - const List specialFiles = [ - '.ci.yaml', // LUCI config. - '.cirrus.yml', // Cirrus config. - '.clang-format', // ObjC and C/C++ formatting options. - 'analysis_options.yaml', // Dart analysis settings. - ]; - const List specialDirectories = [ - '.ci/', // Support files for CI. - 'script/', // This tool, and its wrapper scripts. - ]; - // Directory entries must end with / to avoid over-matching, since the - // check below is done via string prefixing. - assert(specialDirectories.every((String dir) => dir.endsWith('/'))); - - final List allChangedFiles = - await gitVersionFinder.getChangedFiles(); - return allChangedFiles.any((String path) => - specialFiles.contains(path) || - specialDirectories.any((String dir) => path.startsWith(dir))); - } -} - -/// A class used to run processes. -/// -/// We use this instead of directly running the process so it can be overridden -/// in tests. -class ProcessRunner { - /// Creates a new process runner. - const ProcessRunner(); - - /// Run the [executable] with [args] and stream output to stderr and stdout. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// If [exitOnError] is set to `true`, then this will throw an error if - /// the [executable] terminates with a non-zero exit code. - /// - /// Returns the exit code of the [executable]. - Future runAndStream( - String executable, - List args, { - Directory? workingDir, - bool exitOnError = false, - }) async { - print( - 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); - final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDir?.path); - await io.stdout.addStream(process.stdout); - await io.stderr.addStream(process.stderr); - if (exitOnError && await process.exitCode != 0) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error See above for details.'); - throw ToolExit(await process.exitCode); - } - return process.exitCode; - } - - /// Run the [executable] with [args]. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// If [exitOnError] is set to `true`, then this will throw an error if - /// the [executable] terminates with a non-zero exit code. - /// Defaults to `false`. - /// - /// If [logOnError] is set to `true`, it will print a formatted message about the error. - /// Defaults to `false` - /// - /// Returns the [io.ProcessResult] of the [executable]. - Future run(String executable, List args, - {Directory? workingDir, - bool exitOnError = false, - bool logOnError = false, - Encoding stdoutEncoding = io.systemEncoding, - Encoding stderrEncoding = io.systemEncoding}) async { - final io.ProcessResult result = await io.Process.run(executable, args, - workingDirectory: workingDir?.path, - stdoutEncoding: stdoutEncoding, - stderrEncoding: stderrEncoding); - if (result.exitCode != 0) { - if (logOnError) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error Stderr:\n${result.stdout}'); - } - if (exitOnError) { - throw ToolExit(result.exitCode); - } - } - return result; - } - - /// Starts the [executable] with [args]. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// Returns the started [io.Process]. - Future start(String executable, List args, - {Directory? workingDirectory}) async { - final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDirectory?.path); - return process; - } - - String _getErrorString(String executable, List args, - {Directory? workingDir}) { - final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; - return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; - } -} - -/// Finding version of [package] that is published on pub. -class PubVersionFinder { - /// Constructor. - /// - /// Note: you should manually close the [httpClient] when done using the finder. - PubVersionFinder({this.pubHost = defaultPubHost, required this.httpClient}); - - /// The default pub host to use. - static const String defaultPubHost = 'https://pub.dev'; - - /// The pub host url, defaults to `https://pub.dev`. - final String pubHost; - - /// The http client. - /// - /// You should manually close this client when done using this finder. - final http.Client httpClient; - - /// Get the package version on pub. - Future getPackageVersion( - {required String package}) async { - assert(package.isNotEmpty); - final Uri pubHostUri = Uri.parse(pubHost); - final Uri url = pubHostUri.replace(path: '/packages/$package.json'); - final http.Response response = await httpClient.get(url); - - if (response.statusCode == 404) { - return PubVersionFinderResponse( - versions: [], - result: PubVersionFinderResult.noPackageFound, - httpResponse: response); - } else if (response.statusCode != 200) { - return PubVersionFinderResponse( - versions: [], - result: PubVersionFinderResult.fail, - httpResponse: response); - } - final List versions = - (json.decode(response.body)['versions'] as List) - .map((final dynamic versionString) => - Version.parse(versionString as String)) - .toList(); - - return PubVersionFinderResponse( - versions: versions, - result: PubVersionFinderResult.success, - httpResponse: response); - } -} - -/// Represents a response for [PubVersionFinder]. -class PubVersionFinderResponse { - /// Constructor. - PubVersionFinderResponse( - {required this.versions, - required this.result, - required this.httpResponse}) { - if (versions.isNotEmpty) { - versions.sort((Version a, Version b) { - // TODO(cyanglaz): Think about how to handle pre-release version with [Version.prioritize]. - // https://github.com/flutter/flutter/issues/82222 - return b.compareTo(a); - }); - } - } - - /// The versions found in [PubVersionFinder]. - /// - /// This is sorted by largest to smallest, so the first element in the list is the largest version. - /// Might be `null` if the [result] is not [PubVersionFinderResult.success]. - final List versions; - - /// The result of the version finder. - final PubVersionFinderResult result; - - /// The response object of the http request. - final http.Response httpResponse; -} - -/// An enum representing the result of [PubVersionFinder]. -enum PubVersionFinderResult { - /// The version finder successfully found a version. - success, - - /// The version finder failed to find a valid version. - /// - /// This might due to http connection errors or user errors. - fail, - - /// The version finder failed to locate the package. - /// - /// This indicates the package is new. - noPackageFound, -} - -/// Finding diffs based on `baseGitDir` and `baseSha`. -class GitVersionFinder { - /// Constructor - GitVersionFinder(this.baseGitDir, this.baseSha); - - /// The top level directory of the git repo. - /// - /// That is where the .git/ folder exists. - final GitDir baseGitDir; - - /// The base sha used to get diff. - final String? baseSha; - - static bool _isPubspec(String file) { - return file.trim().endsWith('pubspec.yaml'); - } - - /// Get a list of all the pubspec.yaml file that is changed. - Future> getChangedPubSpecs() async { - return (await getChangedFiles()).where(_isPubspec).toList(); - } - - /// Get a list of all the changed files. - Future> getChangedFiles() async { - final String baseSha = await _getBaseSha(); - final io.ProcessResult changedFilesCommand = await baseGitDir - .runCommand(['diff', '--name-only', baseSha, 'HEAD']); - print('Determine diff with base sha: $baseSha'); - final String changedFilesStdout = changedFilesCommand.stdout.toString(); - if (changedFilesStdout.isEmpty) { - return []; - } - final List changedFiles = changedFilesStdout.split('\n') - ..removeWhere((String element) => element.isEmpty); - return changedFiles.toList(); - } - - /// Get the package version specified in the pubspec file in `pubspecPath` and - /// at the revision of `gitRef` (defaulting to the base if not provided). - Future getPackageVersion(String pubspecPath, - {String? gitRef}) async { - final String ref = gitRef ?? (await _getBaseSha()); - - io.ProcessResult gitShow; - try { - gitShow = - await baseGitDir.runCommand(['show', '$ref:$pubspecPath']); - } on io.ProcessException { - return null; - } - final String fileContent = gitShow.stdout as String; - final String? versionString = loadYaml(fileContent)['version'] as String?; - return versionString == null ? null : Version.parse(versionString); - } - - Future _getBaseSha() async { - if (baseSha != null && baseSha!.isNotEmpty) { - return baseSha!; - } - - io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( - ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], - throwOnError: false); - if (baseShaFromMergeBase.stderr != null || - baseShaFromMergeBase.stdout == null) { - baseShaFromMergeBase = await baseGitDir - .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); - } - return (baseShaFromMergeBase.stdout as String).trim(); - } -} diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart new file mode 100644 index 000000000000..4788b9fa9143 --- /dev/null +++ b/script/tool/lib/src/common/core.dart @@ -0,0 +1,69 @@ +// Copyright 2013 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:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:yaml/yaml.dart'; + +/// The signature for a print handler for commands that allow overriding the +/// print destination. +typedef Print = void Function(Object? object); + +/// Key for windows platform. +const String kPlatformFlagWindows = 'windows'; + +/// Key for macos platform. +const String kPlatformFlagMacos = 'macos'; + +/// Key for linux platform. +const String kPlatformFlagLinux = 'linux'; + +/// Key for IPA (iOS) platform. +const String kPlatformFlagIos = 'ios'; + +/// Key for APK (Android) platform. +const String kPlatformFlagAndroid = 'android'; + +/// Key for Web platform. +const String kPlatformFlagWeb = 'web'; + +/// Key for enable experiment. +const String kEnableExperiment = 'enable-experiment'; + +/// Returns whether the given directory contains a Flutter package. +bool isFlutterPackage(FileSystemEntity entity) { + if (entity is! Directory) { + return false; + } + + try { + final File pubspecFile = entity.childFile('pubspec.yaml'); + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap? dependencies = pubspecYaml['dependencies'] as YamlMap?; + if (dependencies == null) { + return false; + } + return dependencies.containsKey('flutter'); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Prints `errorMessage` in red. +void printError(String errorMessage) { + final Colorize redError = Colorize(errorMessage)..red(); + print(redError); +} + +/// Error thrown when a command needs to exit with a non-zero exit code. +class ToolExit extends Error { + /// Creates a tool exit with the given [exitCode]. + ToolExit(this.exitCode); + + /// The code that the process should exit with. + final int exitCode; +} diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart new file mode 100644 index 000000000000..2c9519e7a856 --- /dev/null +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -0,0 +1,81 @@ +// Copyright 2013 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:io' as io; + +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:yaml/yaml.dart'; + +/// Finding diffs based on `baseGitDir` and `baseSha`. +class GitVersionFinder { + /// Constructor + GitVersionFinder(this.baseGitDir, this.baseSha); + + /// The top level directory of the git repo. + /// + /// That is where the .git/ folder exists. + final GitDir baseGitDir; + + /// The base sha used to get diff. + final String? baseSha; + + static bool _isPubspec(String file) { + return file.trim().endsWith('pubspec.yaml'); + } + + /// Get a list of all the pubspec.yaml file that is changed. + Future> getChangedPubSpecs() async { + return (await getChangedFiles()).where(_isPubspec).toList(); + } + + /// Get a list of all the changed files. + Future> getChangedFiles() async { + final String baseSha = await _getBaseSha(); + final io.ProcessResult changedFilesCommand = await baseGitDir + .runCommand(['diff', '--name-only', baseSha, 'HEAD']); + print('Determine diff with base sha: $baseSha'); + final String changedFilesStdout = changedFilesCommand.stdout.toString(); + if (changedFilesStdout.isEmpty) { + return []; + } + final List changedFiles = changedFilesStdout.split('\n') + ..removeWhere((String element) => element.isEmpty); + return changedFiles.toList(); + } + + /// Get the package version specified in the pubspec file in `pubspecPath` and + /// at the revision of `gitRef` (defaulting to the base if not provided). + Future getPackageVersion(String pubspecPath, + {String? gitRef}) async { + final String ref = gitRef ?? (await _getBaseSha()); + + io.ProcessResult gitShow; + try { + gitShow = + await baseGitDir.runCommand(['show', '$ref:$pubspecPath']); + } on io.ProcessException { + return null; + } + final String fileContent = gitShow.stdout as String; + final String? versionString = loadYaml(fileContent)['version'] as String?; + return versionString == null ? null : Version.parse(versionString); + } + + Future _getBaseSha() async { + if (baseSha != null && baseSha!.isNotEmpty) { + return baseSha!; + } + + io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( + ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], + throwOnError: false); + if (baseShaFromMergeBase.stderr != null || + baseShaFromMergeBase.stdout == null) { + baseShaFromMergeBase = await baseGitDir + .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); + } + return (baseShaFromMergeBase.stdout as String).trim(); + } +} diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart new file mode 100644 index 000000000000..1ab9d8dcc6e0 --- /dev/null +++ b/script/tool/lib/src/common/plugin_command.dart @@ -0,0 +1,353 @@ +// Copyright 2013 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:math'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:path/path.dart' as p; + +import 'core.dart'; +import 'git_version_finder.dart'; +import 'process_runner.dart'; + +/// Interface definition for all commands in this tool. +abstract class PluginCommand extends Command { + /// Creates a command to operate on [packagesDir] with the given environment. + PluginCommand( + this.packagesDir, { + this.processRunner = const ProcessRunner(), + this.gitDir, + }) { + argParser.addMultiOption( + _pluginsArg, + splitCommas: true, + help: + 'Specifies which plugins the command should run on (before sharding).', + valueHelp: 'plugin1,plugin2,...', + ); + argParser.addOption( + _shardIndexArg, + help: 'Specifies the zero-based index of the shard to ' + 'which the command applies.', + valueHelp: 'i', + defaultsTo: '0', + ); + argParser.addOption( + _shardCountArg, + help: 'Specifies the number of shards into which plugins are divided.', + valueHelp: 'n', + defaultsTo: '1', + ); + argParser.addMultiOption( + _excludeArg, + abbr: 'e', + help: 'Exclude packages from this command.', + defaultsTo: [], + ); + argParser.addFlag(_runOnChangedPackagesArg, + help: 'Run the command on changed packages/plugins.\n' + 'If the $_pluginsArg is specified, this flag is ignored.\n' + 'If no packages have changed, or if there have been changes that may\n' + 'affect all packages, the command runs on all packages.\n' + 'The packages excluded with $_excludeArg is also excluded even if changed.\n' + 'See $_kBaseSha if a custom base is needed to determine the diff.'); + argParser.addOption(_kBaseSha, + help: 'The base sha used to determine git diff. \n' + 'This is useful when $_runOnChangedPackagesArg is specified.\n' + 'If not specified, merge-base is used as base sha.'); + } + + static const String _pluginsArg = 'plugins'; + static const String _shardIndexArg = 'shardIndex'; + static const String _shardCountArg = 'shardCount'; + static const String _excludeArg = 'exclude'; + static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _kBaseSha = 'base-sha'; + + /// The directory containing the plugin packages. + final Directory packagesDir; + + /// The process runner. + /// + /// This can be overridden for testing. + final ProcessRunner processRunner; + + /// The git directory to use. By default it uses the parent directory. + /// + /// This can be mocked for testing. + final GitDir? gitDir; + + int? _shardIndex; + int? _shardCount; + + /// The shard of the overall command execution that this instance should run. + int get shardIndex { + if (_shardIndex == null) { + _checkSharding(); + } + return _shardIndex!; + } + + /// The number of shards this command is divided into. + int get shardCount { + if (_shardCount == null) { + _checkSharding(); + } + return _shardCount!; + } + + /// Convenience accessor for boolean arguments. + bool getBoolArg(String key) { + return (argResults![key] as bool?) ?? false; + } + + /// Convenience accessor for String arguments. + String getStringArg(String key) { + return (argResults![key] as String?) ?? ''; + } + + /// Convenience accessor for List arguments. + List getStringListArg(String key) { + return (argResults![key] as List?) ?? []; + } + + void _checkSharding() { + final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg)); + final int? shardCount = int.tryParse(getStringArg(_shardCountArg)); + if (shardIndex == null) { + usageException('$_shardIndexArg must be an integer'); + } + if (shardCount == null) { + usageException('$_shardCountArg must be an integer'); + } + if (shardCount < 1) { + usageException('$_shardCountArg must be positive'); + } + if (shardIndex < 0 || shardCount <= shardIndex) { + usageException( + '$_shardIndexArg must be in the half-open range [0..$shardCount['); + } + _shardIndex = shardIndex; + _shardCount = shardCount; + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution. + Stream getPlugins() async* { + // To avoid assuming consistency of `Directory.list` across command + // invocations, we collect and sort the plugin folders before sharding. + // This is considered an implementation detail which is why the API still + // uses streams. + final List allPlugins = await _getAllPlugins().toList(); + allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); + // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. + // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. + // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. + final int shardSize = allPlugins.length ~/ shardCount + + (allPlugins.length % shardCount == 0 ? 0 : 1); + final int start = min(shardIndex * shardSize, allPlugins.length); + final int end = min(start + shardSize, allPlugins.length); + + for (final Directory plugin in allPlugins.sublist(start, end)) { + yield plugin; + } + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution, assuming there is only one shard. + /// + /// Plugin packages can exist in the following places relative to the packages + /// directory: + /// + /// 1. As a Dart package in a directory which is a direct child of the + /// packages directory. This is a plugin where all of the implementations + /// exist in a single Dart package. + /// 2. Several plugin packages may live in a directory which is a direct + /// child of the packages directory. This directory groups several Dart + /// packages which implement a single plugin. This directory contains a + /// "client library" package, which declares the API for the plugin, as + /// well as one or more platform-specific implementations. + /// 3./4. Either of the above, but in a third_party/packages/ directory that + /// is a sibling of the packages directory. This is used for a small number + /// of packages in the flutter/packages repository. + Stream _getAllPlugins() async* { + Set plugins = Set.from(getStringListArg(_pluginsArg)); + final Set excludedPlugins = + Set.from(getStringListArg(_excludeArg)); + final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); + if (plugins.isEmpty && + runOnChangedPackages && + !(await _changesRequireFullTest())) { + plugins = await _getChangedPackages(); + } + + final Directory thirdPartyPackagesDirectory = packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages'); + + for (final Directory dir in [ + packagesDir, + if (thirdPartyPackagesDirectory.existsSync()) thirdPartyPackagesDirectory, + ]) { + await for (final FileSystemEntity entity + in dir.list(followLinks: false)) { + // A top-level Dart package is a plugin package. + if (_isDartPackage(entity)) { + if (!excludedPlugins.contains(entity.basename) && + (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { + yield entity as Directory; + } + } else if (entity is Directory) { + // Look for Dart packages under this top-level directory. + await for (final FileSystemEntity subdir + in entity.list(followLinks: false)) { + if (_isDartPackage(subdir)) { + // If --plugin=my_plugin is passed, then match all federated + // plugins under 'my_plugin'. Also match if the exact plugin is + // passed. + final String relativePath = + p.relative(subdir.path, from: dir.path); + final String packageName = p.basename(subdir.path); + final String basenamePath = p.basename(entity.path); + if (!excludedPlugins.contains(basenamePath) && + !excludedPlugins.contains(packageName) && + !excludedPlugins.contains(relativePath) && + (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath))) { + yield subdir as Directory; + } + } + } + } + } + } + } + + /// Returns the example Dart package folders of the plugins involved in this + /// command execution. + Stream getExamples() => + getPlugins().expand(getExamplesForPlugin); + + /// Returns all Dart package folders (typically, plugin + example) of the + /// plugins involved in this command execution. + Stream getPackages() async* { + await for (final Directory plugin in getPlugins()) { + yield plugin; + yield* plugin + .list(recursive: true, followLinks: false) + .where(_isDartPackage) + .cast(); + } + } + + /// Returns the files contained, recursively, within the plugins + /// involved in this command execution. + Stream getFiles() { + return getPlugins().asyncExpand((Directory folder) => folder + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .cast()); + } + + /// Returns whether the specified entity is a directory containing a + /// `pubspec.yaml` file. + bool _isDartPackage(FileSystemEntity entity) { + return entity is Directory && entity.childFile('pubspec.yaml').existsSync(); + } + + /// Returns the example Dart packages contained in the specified plugin, or + /// an empty List, if the plugin has no examples. + Iterable getExamplesForPlugin(Directory plugin) { + final Directory exampleFolder = plugin.childDirectory('example'); + if (!exampleFolder.existsSync()) { + return []; + } + if (isFlutterPackage(exampleFolder)) { + return [exampleFolder]; + } + // Only look at the subdirectories of the example directory if the example + // directory itself is not a Dart package, and only look one level below the + // example directory for other dart packages. + return exampleFolder + .listSync() + .where((FileSystemEntity entity) => isFlutterPackage(entity)) + .cast(); + } + + /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir]. + /// + /// Throws tool exit if [gitDir] nor root directory is a git directory. + Future retrieveVersionFinder() async { + final String rootDir = packagesDir.parent.absolute.path; + final String baseSha = getStringArg(_kBaseSha); + + GitDir? baseGitDir = gitDir; + if (baseGitDir == null) { + if (!await GitDir.isGitDir(rootDir)) { + printError( + '$rootDir is not a valid Git repository.', + ); + throw ToolExit(2); + } + baseGitDir = await GitDir.fromExisting(rootDir); + } + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(baseGitDir, baseSha); + return gitVersionFinder; + } + + // Returns packages that have been changed relative to the git base. + Future> _getChangedPackages() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + + final List allChangedFiles = + await gitVersionFinder.getChangedFiles(); + final Set packages = {}; + for (final String path in allChangedFiles) { + final List pathComponents = path.split('/'); + final int packagesIndex = + pathComponents.indexWhere((String element) => element == 'packages'); + if (packagesIndex != -1) { + packages.add(pathComponents[packagesIndex + 1]); + } + } + if (packages.isEmpty) { + print('No changed packages.'); + } else { + final String changedPackages = packages.join(','); + print('Changed packages: $changedPackages'); + } + return packages; + } + + // Returns true if one or more files changed that have the potential to affect + // any plugin (e.g., CI script changes). + Future _changesRequireFullTest() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + + const List specialFiles = [ + '.ci.yaml', // LUCI config. + '.cirrus.yml', // Cirrus config. + '.clang-format', // ObjC and C/C++ formatting options. + 'analysis_options.yaml', // Dart analysis settings. + ]; + const List specialDirectories = [ + '.ci/', // Support files for CI. + 'script/', // This tool, and its wrapper scripts. + ]; + // Directory entries must end with / to avoid over-matching, since the + // check below is done via string prefixing. + assert(specialDirectories.every((String dir) => dir.endsWith('/'))); + + final List allChangedFiles = + await gitVersionFinder.getChangedFiles(); + return allChangedFiles.any((String path) => + specialFiles.contains(path) || + specialDirectories.any((String dir) => path.startsWith(dir))); + } +} diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart new file mode 100644 index 000000000000..b6ac433db2e2 --- /dev/null +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -0,0 +1,110 @@ +// Copyright 2013 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:file/file.dart'; +import 'package:yaml/yaml.dart'; + +import 'core.dart'; + +/// Possible plugin support options for a platform. +enum PlatformSupport { + /// The platform has an implementation in the package. + inline, + + /// The platform has an endorsed federated implementation in another package. + federated, +} + +/// Returns whether the given directory contains a Flutter [platform] plugin. +/// +/// It checks this by looking for the following pattern in the pubspec: +/// +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +/// +/// If [requiredMode] is provided, the plugin must have the given type of +/// implementation in order to return true. +bool pluginSupportsPlatform(String platform, FileSystemEntity entity, + {PlatformSupport? requiredMode}) { + assert(platform == kPlatformFlagIos || + platform == kPlatformFlagAndroid || + platform == kPlatformFlagWeb || + platform == kPlatformFlagMacos || + platform == kPlatformFlagWindows || + platform == kPlatformFlagLinux); + if (entity is! Directory) { + return false; + } + + try { + final File pubspecFile = entity.childFile('pubspec.yaml'); + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + if (flutterSection == null) { + return false; + } + final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; + if (pluginSection == null) { + return false; + } + final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; + if (platforms == null) { + // Legacy plugin specs are assumed to support iOS and Android. They are + // never federated. + if (requiredMode == PlatformSupport.federated) { + return false; + } + if (!pluginSection.containsKey('platforms')) { + return platform == kPlatformFlagIos || platform == kPlatformFlagAndroid; + } + return false; + } + final YamlMap? platformEntry = platforms[platform] as YamlMap?; + if (platformEntry == null) { + return false; + } + // If the platform entry is present, then it supports the platform. Check + // for required mode if specified. + final bool federated = platformEntry.containsKey('default_package'); + return requiredMode == null || + federated == (requiredMode == PlatformSupport.federated); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter Android plugin. +bool isAndroidPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagAndroid, entity); +} + +/// Returns whether the given directory contains a Flutter iOS plugin. +bool isIosPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagIos, entity); +} + +/// Returns whether the given directory contains a Flutter web plugin. +bool isWebPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagWeb, entity); +} + +/// Returns whether the given directory contains a Flutter Windows plugin. +bool isWindowsPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagWindows, entity); +} + +/// Returns whether the given directory contains a Flutter macOS plugin. +bool isMacOsPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagMacos, entity); +} + +/// Returns whether the given directory contains a Flutter linux plugin. +bool isLinuxPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagLinux, entity); +} diff --git a/script/tool/lib/src/common/process_runner.dart b/script/tool/lib/src/common/process_runner.dart new file mode 100644 index 000000000000..429761ead3b8 --- /dev/null +++ b/script/tool/lib/src/common/process_runner.dart @@ -0,0 +1,104 @@ +// Copyright 2013 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:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; + +import 'core.dart'; + +/// A class used to run processes. +/// +/// We use this instead of directly running the process so it can be overridden +/// in tests. +class ProcessRunner { + /// Creates a new process runner. + const ProcessRunner(); + + /// Run the [executable] with [args] and stream output to stderr and stdout. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the exit code of the [executable]. + Future runAndStream( + String executable, + List args, { + Directory? workingDir, + bool exitOnError = false, + }) async { + print( + 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDir?.path); + await io.stdout.addStream(process.stdout); + await io.stderr.addStream(process.stderr); + if (exitOnError && await process.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error See above for details.'); + throw ToolExit(await process.exitCode); + } + return process.exitCode; + } + + /// Run the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// Defaults to `false`. + /// + /// If [logOnError] is set to `true`, it will print a formatted message about the error. + /// Defaults to `false` + /// + /// Returns the [io.ProcessResult] of the [executable]. + Future run(String executable, List args, + {Directory? workingDir, + bool exitOnError = false, + bool logOnError = false, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding}) async { + final io.ProcessResult result = await io.Process.run(executable, args, + workingDirectory: workingDir?.path, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding); + if (result.exitCode != 0) { + if (logOnError) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error Stderr:\n${result.stdout}'); + } + if (exitOnError) { + throw ToolExit(result.exitCode); + } + } + return result; + } + + /// Starts the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the started [io.Process]. + Future start(String executable, List args, + {Directory? workingDirectory}) async { + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDirectory?.path); + return process; + } + + String _getErrorString(String executable, List args, + {Directory? workingDir}) { + final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; + return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; + } +} diff --git a/script/tool/lib/src/common/pub_version_finder.dart b/script/tool/lib/src/common/pub_version_finder.dart new file mode 100644 index 000000000000..ebac473de7ac --- /dev/null +++ b/script/tool/lib/src/common/pub_version_finder.dart @@ -0,0 +1,103 @@ +// Copyright 2013 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:convert'; + +import 'package:http/http.dart' as http; +import 'package:pub_semver/pub_semver.dart'; + +/// Finding version of [package] that is published on pub. +class PubVersionFinder { + /// Constructor. + /// + /// Note: you should manually close the [httpClient] when done using the finder. + PubVersionFinder({this.pubHost = defaultPubHost, required this.httpClient}); + + /// The default pub host to use. + static const String defaultPubHost = 'https://pub.dev'; + + /// The pub host url, defaults to `https://pub.dev`. + final String pubHost; + + /// The http client. + /// + /// You should manually close this client when done using this finder. + final http.Client httpClient; + + /// Get the package version on pub. + Future getPackageVersion( + {required String package}) async { + assert(package.isNotEmpty); + final Uri pubHostUri = Uri.parse(pubHost); + final Uri url = pubHostUri.replace(path: '/packages/$package.json'); + final http.Response response = await httpClient.get(url); + + if (response.statusCode == 404) { + return PubVersionFinderResponse( + versions: [], + result: PubVersionFinderResult.noPackageFound, + httpResponse: response); + } else if (response.statusCode != 200) { + return PubVersionFinderResponse( + versions: [], + result: PubVersionFinderResult.fail, + httpResponse: response); + } + final List versions = + (json.decode(response.body)['versions'] as List) + .map((final dynamic versionString) => + Version.parse(versionString as String)) + .toList(); + + return PubVersionFinderResponse( + versions: versions, + result: PubVersionFinderResult.success, + httpResponse: response); + } +} + +/// Represents a response for [PubVersionFinder]. +class PubVersionFinderResponse { + /// Constructor. + PubVersionFinderResponse( + {required this.versions, + required this.result, + required this.httpResponse}) { + if (versions.isNotEmpty) { + versions.sort((Version a, Version b) { + // TODO(cyanglaz): Think about how to handle pre-release version with [Version.prioritize]. + // https://github.com/flutter/flutter/issues/82222 + return b.compareTo(a); + }); + } + } + + /// The versions found in [PubVersionFinder]. + /// + /// This is sorted by largest to smallest, so the first element in the list is the largest version. + /// Might be `null` if the [result] is not [PubVersionFinderResult.success]. + final List versions; + + /// The result of the version finder. + final PubVersionFinderResult result; + + /// The response object of the http request. + final http.Response httpResponse; +} + +/// An enum representing the result of [PubVersionFinder]. +enum PubVersionFinderResult { + /// The version finder successfully found a version. + success, + + /// The version finder failed to find a valid version. + /// + /// This might due to http connection errors or user errors. + fail, + + /// The version finder failed to locate the package. + /// + /// This indicates the package is new. + noPackageFound, +} diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart index cd5b85e45ac0..fab41bcf4ec4 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -8,7 +8,8 @@ import 'package:file/file.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; /// A command to create an application that builds all in a single application. class CreateAllPluginsAppCommand extends PluginCommand { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 14dfede5b2f1..b6576cd13ba8 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -2,11 +2,14 @@ // 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:file/file.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; -import 'common.dart'; + +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; /// A command to run the example applications for packages via Flutter driver. class DriveExamplesCommand extends PluginCommand { diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 741d8569322b..b4f5e92933c6 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -9,7 +9,9 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'package:uuid/uuid.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; /// A command to run tests via Firebase test lab. class FirebaseTestLabCommand extends PluginCommand { diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 1ef41f82bb2c..5f060d715bfd 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -11,7 +10,9 @@ import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; import 'package:quiver/iterables.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; final Uri _googleFormatterUrl = Uri.https('github.com', '/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'); diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index d1366ea7636a..d7e453b6ad74 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -2,12 +2,12 @@ // 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:file/file.dart'; import 'package:path/path.dart' as p; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; /// A command to run the Java tests of Android plugins. class JavaTestCommand extends PluginCommand { diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 805c3ab9f900..4ea8a1e09392 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -2,12 +2,11 @@ // 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:file/file.dart'; import 'package:path/path.dart' as p; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; const Set _codeFileExtensions = { '.c', diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 364653bd13ba..5e86d2be40b8 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -9,7 +9,9 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; /// Lint the CocoaPod podspecs and run unit tests. /// diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index f6b186e7ba2f..39515cf686b0 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -2,11 +2,9 @@ // 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:file/file.dart'; -import 'common.dart'; +import 'common/plugin_command.dart'; /// A command to list different types of repository content. class ListCommand extends PluginCommand { diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index a7603122186a..f397a04aa663 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -10,7 +10,7 @@ import 'package:file/local.dart'; import 'analyze_command.dart'; import 'build_examples_command.dart'; -import 'common.dart'; +import 'common/core.dart'; import 'create_all_plugins_app_command.dart'; import 'drive_examples_command.dart'; import 'firebase_test_lab_command.dart'; diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index b77eceecbf41..82a76609e98b 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -12,7 +12,10 @@ import 'package:http/http.dart' as http; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; +import 'common/pub_version_finder.dart'; /// A command to check that packages are publishable via 'dart publish'. class PublishCheckCommand extends PluginCommand { diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 1e7c15029846..70ec75bc7b76 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -14,7 +14,10 @@ import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/git_version_finder.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; @immutable class _RemoteInfo { diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 878b683dbbb8..480d3a4c1190 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -2,14 +2,14 @@ // 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:file/file.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; /// A command to enforce pubspec conventions across the repository. /// diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 0174b986eb63..b7bf261caa8a 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -2,12 +2,13 @@ // 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:file/file.dart'; import 'package:path/path.dart' as p; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; /// A command to run Dart unit tests for packages. class TestCommand extends PluginCommand { diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 6baa38e465a2..5e9f55333f8e 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -2,8 +2,6 @@ // 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:file/file.dart'; import 'package:git/git.dart'; import 'package:http/http.dart' as http; @@ -11,7 +9,11 @@ import 'package:meta/meta.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/git_version_finder.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; +import 'common/pub_version_finder.dart'; /// Categories of version change types. enum NextVersionType { diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 288851ca7edf..77e5659df3f6 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -2,14 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; import 'package:path/path.dart' as p; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; const String _kiOSDestination = 'ios-destination'; const String _kXcodeBuildCommand = 'xcodebuild'; diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index ec627f25864c..1ef4fdc44b42 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -6,7 +6,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/analyze_command.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:test/test.dart'; import 'mocks.dart'; diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart new file mode 100644 index 000000000000..f1f40b5e0035 --- /dev/null +++ b/script/tool/test/common/git_version_finder_test.dart @@ -0,0 +1,93 @@ +// Copyright 2013 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:io'; + +import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'plugin_command_test.mocks.dart'; + +void main() { + late List?> gitDirCommands; + late String gitDiffResponse; + late MockGitDir gitDir; + String? mergeBaseResponse; + + setUp(() { + gitDirCommands = ?>[]; + gitDiffResponse = ''; + gitDir = MockGitDir(); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0] as List?); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout as String?) + .thenReturn(gitDiffResponse); + } else if (invocation.positionalArguments[0][0] == 'merge-base') { + when(mockProcessResult.stdout as String?) + .thenReturn(mergeBaseResponse); + } + return Future.value(mockProcessResult); + }); + }); + + test('No git diff should result no files changed', () async { + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + final List changedFiles = await finder.getChangedFiles(); + + expect(changedFiles, isEmpty); + }); + + test('get correct files changed based on git diff', () async { + gitDiffResponse = ''' +file1/file1.cc +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + final List changedFiles = await finder.getChangedFiles(); + + expect(changedFiles, equals(['file1/file1.cc', 'file2/file2.cc'])); + }); + + test('get correct pubspec change based on git diff', () async { + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + final List changedFiles = await finder.getChangedPubSpecs(); + + expect(changedFiles, equals(['file1/pubspec.yaml'])); + }); + + test('use correct base sha if not specified', () async { + mergeBaseResponse = 'shaqwiueroaaidf12312jnadf123nd'; + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + + final GitVersionFinder finder = GitVersionFinder(gitDir, null); + await finder.getChangedFiles(); + verify(gitDir.runCommand( + ['diff', '--name-only', mergeBaseResponse!, 'HEAD'])); + }); + + test('use correct base sha if specified', () async { + const String customBaseSha = 'aklsjdcaskf12312'; + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); + await finder.getChangedFiles(); + verify(gitDir + .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); + }); +} + +class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common_test.dart b/script/tool/test/common/plugin_command_test.dart similarity index 51% rename from script/tool/test/common_test.dart rename to script/tool/test/common/plugin_command_test.dart index a51182d91ff8..58d202e19920 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -2,24 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_command.dart'; +import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:git/git.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; -import 'common_test.mocks.dart'; -import 'util.dart'; +import '../util.dart'; +import 'plugin_command_test.mocks.dart'; @GenerateMocks([GitDir]) void main() { @@ -362,355 +358,6 @@ packages/plugin3/plugin3.dart }); }); }); - - group('$GitVersionFinder', () { - late FileSystem fileSystem; - late List?> gitDirCommands; - late String gitDiffResponse; - String? mergeBaseResponse; - late MockGitDir gitDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - createPackagesDirectory(fileSystem: fileSystem); - gitDirCommands = ?>[]; - gitDiffResponse = ''; - gitDir = MockGitDir(); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0] as List?); - final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout as String?) - .thenReturn(gitDiffResponse); - } else if (invocation.positionalArguments[0][0] == 'merge-base') { - when(mockProcessResult.stdout as String?) - .thenReturn(mergeBaseResponse); - } - return Future.value(mockProcessResult); - }); - processRunner = RecordingProcessRunner(); - }); - - test('No git diff should result no files changed', () async { - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedFiles(); - - expect(changedFiles, isEmpty); - }); - - test('get correct files changed based on git diff', () async { - gitDiffResponse = ''' -file1/file1.cc -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedFiles(); - - expect( - changedFiles, equals(['file1/file1.cc', 'file2/file2.cc'])); - }); - - test('get correct pubspec change based on git diff', () async { - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedPubSpecs(); - - expect(changedFiles, equals(['file1/pubspec.yaml'])); - }); - - test('use correct base sha if not specified', () async { - mergeBaseResponse = 'shaqwiueroaaidf12312jnadf123nd'; - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - - final GitVersionFinder finder = GitVersionFinder(gitDir, null); - await finder.getChangedFiles(); - verify(gitDir.runCommand( - ['diff', '--name-only', mergeBaseResponse!, 'HEAD'])); - }); - - test('use correct base sha if specified', () async { - const String customBaseSha = 'aklsjdcaskf12312'; - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); - await finder.getChangedFiles(); - verify(gitDir - .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); - }); - }); - - group('$PubVersionFinder', () { - test('Package does not exist.', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('', 404); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); - - expect(response.versions, isEmpty); - expect(response.result, PubVersionFinderResult.noPackageFound); - expect(response.httpResponse.statusCode, 404); - expect(response.httpResponse.body, ''); - }); - - test('HTTP error when getting versions from pub', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('', 400); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); - - expect(response.versions, isEmpty); - expect(response.result, PubVersionFinderResult.fail); - expect(response.httpResponse.statusCode, 400); - expect(response.httpResponse.body, ''); - }); - - test('Get a correct list of versions when http response is OK.', () async { - const Map httpResponse = { - 'name': 'some_package', - 'versions': [ - '0.0.1', - '0.0.2', - '0.0.2+2', - '0.1.1', - '0.0.1+1', - '0.1.0', - '0.2.0', - '0.1.0+1', - '0.0.2+1', - '2.0.0', - '1.2.0', - '1.0.0', - ], - }; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(httpResponse), 200); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); - - expect(response.versions, [ - Version.parse('2.0.0'), - Version.parse('1.2.0'), - Version.parse('1.0.0'), - Version.parse('0.2.0'), - Version.parse('0.1.1'), - Version.parse('0.1.0+1'), - Version.parse('0.1.0'), - Version.parse('0.0.2+2'), - Version.parse('0.0.2+1'), - Version.parse('0.0.2'), - Version.parse('0.0.1+1'), - Version.parse('0.0.1'), - ]); - expect(response.result, PubVersionFinderResult.success); - expect(response.httpResponse.statusCode, 200); - expect(response.httpResponse.body, json.encode(httpResponse)); - }); - }); - - group('pluginSupportsPlatform', () { - test('no platforms', () async { - final Directory plugin = createFakePlugin('plugin', packagesDir); - - expect(pluginSupportsPlatform('android', plugin), isFalse); - expect(pluginSupportsPlatform('ios', plugin), isFalse); - expect(pluginSupportsPlatform('linux', plugin), isFalse); - expect(pluginSupportsPlatform('macos', plugin), isFalse); - expect(pluginSupportsPlatform('web', plugin), isFalse); - expect(pluginSupportsPlatform('windows', plugin), isFalse); - }); - - test('all platforms', () async { - final Directory plugin = createFakePlugin( - 'plugin', - packagesDir, - isAndroidPlugin: true, - isIosPlugin: true, - isLinuxPlugin: true, - isMacOsPlugin: true, - isWebPlugin: true, - isWindowsPlugin: true, - ); - - expect(pluginSupportsPlatform('android', plugin), isTrue); - expect(pluginSupportsPlatform('ios', plugin), isTrue); - expect(pluginSupportsPlatform('linux', plugin), isTrue); - expect(pluginSupportsPlatform('macos', plugin), isTrue); - expect(pluginSupportsPlatform('web', plugin), isTrue); - expect(pluginSupportsPlatform('windows', plugin), isTrue); - }); - - test('some platforms', () async { - final Directory plugin = createFakePlugin( - 'plugin', - packagesDir, - isAndroidPlugin: true, - isIosPlugin: false, - isLinuxPlugin: true, - isMacOsPlugin: false, - isWebPlugin: true, - isWindowsPlugin: false, - ); - - expect(pluginSupportsPlatform('android', plugin), isTrue); - expect(pluginSupportsPlatform('ios', plugin), isFalse); - expect(pluginSupportsPlatform('linux', plugin), isTrue); - expect(pluginSupportsPlatform('macos', plugin), isFalse); - expect(pluginSupportsPlatform('web', plugin), isTrue); - expect(pluginSupportsPlatform('windows', plugin), isFalse); - }); - - test('inline plugins are only detected as inline', () async { - // createFakePlugin makes non-federated pubspec entries. - final Directory plugin = createFakePlugin( - 'plugin', - packagesDir, - isAndroidPlugin: true, - isIosPlugin: true, - isLinuxPlugin: true, - isMacOsPlugin: true, - isWebPlugin: true, - isWindowsPlugin: true, - ); - - expect( - pluginSupportsPlatform('android', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('android', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform('ios', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('ios', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform('linux', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('linux', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform('macos', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('macos', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform('web', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('web', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform('windows', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('windows', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - }); - - test('federated plugins are only detected as federated', () async { - const String pluginName = 'plugin'; - final Directory plugin = createFakePlugin( - pluginName, - packagesDir, - isAndroidPlugin: true, - isIosPlugin: true, - isLinuxPlugin: true, - isMacOsPlugin: true, - isWebPlugin: true, - isWindowsPlugin: true, - ); - - createFakePubspec( - plugin, - name: pluginName, - androidSupport: PlatformSupport.federated, - iosSupport: PlatformSupport.federated, - linuxSupport: PlatformSupport.federated, - macosSupport: PlatformSupport.federated, - webSupport: PlatformSupport.federated, - windowsSupport: PlatformSupport.federated, - ); - - expect( - pluginSupportsPlatform('android', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('android', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform('ios', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('ios', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform('linux', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('linux', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform('macos', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('macos', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform('web', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('web', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform('windows', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('windows', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - }); - }); } class SamplePluginCommand extends PluginCommand { diff --git a/script/tool/test/common_test.mocks.dart b/script/tool/test/common/plugin_command_test.mocks.dart similarity index 100% rename from script/tool/test/common_test.mocks.dart rename to script/tool/test/common/plugin_command_test.mocks.dart diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart new file mode 100644 index 000000000000..aaa850155da4 --- /dev/null +++ b/script/tool/test/common/plugin_utils_test.dart @@ -0,0 +1,210 @@ +// Copyright 2013 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:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:test/test.dart'; + +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + }); + + group('pluginSupportsPlatform', () { + test('no platforms', () async { + final Directory plugin = createFakePlugin('plugin', packagesDir); + + expect(pluginSupportsPlatform('android', plugin), isFalse); + expect(pluginSupportsPlatform('ios', plugin), isFalse); + expect(pluginSupportsPlatform('linux', plugin), isFalse); + expect(pluginSupportsPlatform('macos', plugin), isFalse); + expect(pluginSupportsPlatform('web', plugin), isFalse); + expect(pluginSupportsPlatform('windows', plugin), isFalse); + }); + + test('all platforms', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + isAndroidPlugin: true, + isIosPlugin: true, + isLinuxPlugin: true, + isMacOsPlugin: true, + isWebPlugin: true, + isWindowsPlugin: true, + ); + + expect(pluginSupportsPlatform('android', plugin), isTrue); + expect(pluginSupportsPlatform('ios', plugin), isTrue); + expect(pluginSupportsPlatform('linux', plugin), isTrue); + expect(pluginSupportsPlatform('macos', plugin), isTrue); + expect(pluginSupportsPlatform('web', plugin), isTrue); + expect(pluginSupportsPlatform('windows', plugin), isTrue); + }); + + test('some platforms', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + isAndroidPlugin: true, + isIosPlugin: false, + isLinuxPlugin: true, + isMacOsPlugin: false, + isWebPlugin: true, + isWindowsPlugin: false, + ); + + expect(pluginSupportsPlatform('android', plugin), isTrue); + expect(pluginSupportsPlatform('ios', plugin), isFalse); + expect(pluginSupportsPlatform('linux', plugin), isTrue); + expect(pluginSupportsPlatform('macos', plugin), isFalse); + expect(pluginSupportsPlatform('web', plugin), isTrue); + expect(pluginSupportsPlatform('windows', plugin), isFalse); + }); + + test('inline plugins are only detected as inline', () async { + // createFakePlugin makes non-federated pubspec entries. + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + isAndroidPlugin: true, + isIosPlugin: true, + isLinuxPlugin: true, + isMacOsPlugin: true, + isWebPlugin: true, + isWindowsPlugin: true, + ); + + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + }); + + test('federated plugins are only detected as federated', () async { + const String pluginName = 'plugin'; + final Directory plugin = createFakePlugin( + pluginName, + packagesDir, + isAndroidPlugin: true, + isIosPlugin: true, + isLinuxPlugin: true, + isMacOsPlugin: true, + isWebPlugin: true, + isWindowsPlugin: true, + ); + + createFakePubspec( + plugin, + name: pluginName, + androidSupport: PlatformSupport.federated, + iosSupport: PlatformSupport.federated, + linuxSupport: PlatformSupport.federated, + macosSupport: PlatformSupport.federated, + webSupport: PlatformSupport.federated, + windowsSupport: PlatformSupport.federated, + ); + + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + }); + }); +} diff --git a/script/tool/test/common/pub_version_finder_test.dart b/script/tool/test/common/pub_version_finder_test.dart new file mode 100644 index 000000000000..7d8658a907ee --- /dev/null +++ b/script/tool/test/common/pub_version_finder_test.dart @@ -0,0 +1,89 @@ +// Copyright 2013 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:convert'; +import 'dart:io'; + +import 'package:flutter_plugin_tools/src/common/pub_version_finder.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:mockito/mockito.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +void main() { + test('Package does not exist.', () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('', 404); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(package: 'some_package'); + + expect(response.versions, isEmpty); + expect(response.result, PubVersionFinderResult.noPackageFound); + expect(response.httpResponse.statusCode, 404); + expect(response.httpResponse.body, ''); + }); + + test('HTTP error when getting versions from pub', () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('', 400); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(package: 'some_package'); + + expect(response.versions, isEmpty); + expect(response.result, PubVersionFinderResult.fail); + expect(response.httpResponse.statusCode, 400); + expect(response.httpResponse.body, ''); + }); + + test('Get a correct list of versions when http response is OK.', () async { + const Map httpResponse = { + 'name': 'some_package', + 'versions': [ + '0.0.1', + '0.0.2', + '0.0.2+2', + '0.1.1', + '0.0.1+1', + '0.1.0', + '0.2.0', + '0.1.0+1', + '0.0.2+1', + '2.0.0', + '1.2.0', + '1.0.0', + ], + }; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(httpResponse), 200); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(package: 'some_package'); + + expect(response.versions, [ + Version.parse('2.0.0'), + Version.parse('1.2.0'), + Version.parse('1.0.0'), + Version.parse('0.2.0'), + Version.parse('0.1.1'), + Version.parse('0.1.0+1'), + Version.parse('0.1.0'), + Version.parse('0.0.2+2'), + Version.parse('0.0.2+1'), + Version.parse('0.0.2'), + Version.parse('0.0.1+1'), + Version.parse('0.0.1'), + ]); + expect(response.result, PubVersionFinderResult.success); + expect(response.httpResponse.statusCode, 200); + expect(response.httpResponse.body, json.encode(httpResponse)); + }); +} + +class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index c9a8b9d90a83..9c5bd18cfb11 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -5,7 +5,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index aa8be17d6794..0bc8f1e197c6 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.dart @@ -7,7 +7,7 @@ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; import 'package:test/test.dart'; diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index a874d7db17b7..dfe8d25197ab 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -5,7 +5,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/license_check_command.dart'; import 'package:test/test.dart'; diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index e5722567f20c..c0ccd2989cf9 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -9,7 +9,7 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/publish_check_command.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 1cb4245fdb73..ef682bfe61f6 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -9,7 +9,8 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 576060d23a9d..f5fe6aef849a 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -5,7 +5,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/pubspec_check_command.dart'; import 'package:test/test.dart'; diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index c590d8a4bb04..79c46fcc50e5 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -9,7 +9,8 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:meta/meta.dart'; import 'package:quiver/collection.dart'; diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index 1199c270642f..a8e7e20bad24 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -9,7 +9,7 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/version_check_command.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; @@ -17,7 +17,7 @@ import 'package:mockito/mockito.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; -import 'common_test.mocks.dart'; +import 'common/plugin_command_test.mocks.dart'; import 'util.dart'; void testAllowedVersion( diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 8ed8144562c9..c0bd6b5dee50 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -7,7 +7,8 @@ import 'dart:convert'; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/xctest_command.dart'; import 'package:test/test.dart'; From da401ba248dfe9eee93444d5bf7f28be72a9b042 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Wed, 16 Jun 2021 12:44:03 -0700 Subject: [PATCH 3/3] Support Hybrid Composition on Android (#4017) --- .../test/google_map_test.dart | 30 ++++++ .../CHANGELOG.md | 5 + ...oogle_maps_flutter_platform_interface.dart | 2 + .../method_channel_google_maps_flutter.dart | 93 +++++++++++++++++++ .../pubspec.yaml | 2 +- 5 files changed, 131 insertions(+), 1 deletion(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart index 2b754afbd359..d1ec87a4730d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart @@ -587,4 +587,34 @@ void main() { expect(platformGoogleMap.buildingsEnabled, true); }); + + testWidgets( + 'Default Android widget is AndroidView', + (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + + expect(find.byType(AndroidView), findsOneWidget); + }, + ); + + // TODO(bparrishMines): Uncomment once https://github.com/flutter/plugins/pull/4017 has landed. + // testWidgets('Use AndroidViewSurface on Android', (WidgetTester tester) async { + // await tester.pumpWidget( + // const Directionality( + // textDirection: TextDirection.ltr, + // child: GoogleMap( + // initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + // ), + // ), + // ); + // + // expect(find.byType(AndroidViewSurface), findsOneWidget); + // }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index b6603d66fa89..2dc533fe1dfa 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.0 + +* Add support for Hybrid Composition when building the Google Maps widget on Android. Set + `MethodChannelGoogleMapsFlutter.useAndroidViewSurface` to `true` to build with Hybrid Composition. + ## 2.0.4 * Preserve the `TileProvider` when copying `TileOverlay`, fixing a diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart index 650a839cb676..300700071102 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/method_channel/method_channel_google_maps_flutter.dart' + show MethodChannelGoogleMapsFlutter; export 'src/platform_interface/google_maps_flutter_platform.dart'; export 'src/types/types.dart'; export 'src/events/map_event.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 49029cc3d22d..41aedc759b15 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -8,6 +8,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -441,6 +442,98 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { return channel(mapId).invokeMethod('map#takeSnapshot'); } + /// Set [GoogleMapsFlutterPlatform] to use [AndroidViewSurface] to build the Google Maps widget. + /// + /// This implementation uses hybrid composition to render the Google Maps + /// Widget on Android. This comes at the cost of some performance on Android + /// versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more + /// information. + /// + /// If set to true, the google map widget should be built with + /// [buildViewWithTextDirection] instead of [buildView]. + /// + /// Defaults to false. + bool useAndroidViewSurface = false; + + /// Returns a widget displaying the map view. + /// + /// This method includes a parameter for platforms that require a text + /// direction. For example, this should be used when using hybrid composition + /// on Android. + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + required TextDirection textDirection, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + if (defaultTargetPlatform == TargetPlatform.android && + useAndroidViewSurface) { + final Map creationParams = { + 'initialCameraPosition': initialCameraPosition.toMap(), + 'options': mapOptions, + 'markersToAdd': serializeMarkerSet(markers), + 'polygonsToAdd': serializePolygonSet(polygons), + 'polylinesToAdd': serializePolylineSet(polylines), + 'circlesToAdd': serializeCircleSet(circles), + 'tileOverlaysToAdd': serializeTileOverlaySet(tileOverlays), + }; + return PlatformViewLink( + viewType: 'plugins.flutter.io/google_maps', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + final SurfaceAndroidViewController controller = + PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/google_maps', + layoutDirection: textDirection, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () => params.onFocusChanged(true), + ); + controller.addOnPlatformViewCreatedListener( + params.onPlatformViewCreated, + ); + controller.addOnPlatformViewCreatedListener( + onPlatformViewCreated, + ); + + controller.create(); + return controller; + }, + ); + } + return buildView( + creationId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays, + gestureRecognizers: gestureRecognizers, + mapOptions: mapOptions, + ); + } + @override Widget buildView( int creationId, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index 5b278a812a8e..1ea425ea0273 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.4 +version: 2.1.0 environment: sdk: '>=2.12.0 <3.0.0'