diff --git a/.ci/targets/analyze.yaml b/.ci/targets/analyze.yaml index 793ac03e26..2262e6a771 100644 --- a/.ci/targets/analyze.yaml +++ b/.ci/targets/analyze.yaml @@ -4,6 +4,10 @@ tasks: infra_step: true # Note infra steps failing prevents "always" from running. - name: analyze repo tools script: .ci/scripts/analyze_repo_tools.sh + - name: download Dart deps + script: script/tool_runner.sh + args: ["fetch-deps"] + infra_step: true - name: analyze script: script/tool_runner.sh # DO NOT change the custom-analysis argument here without changing the Dart repo. diff --git a/.ci/targets/android_platform_tests.yaml b/.ci/targets/android_platform_tests.yaml index 7ee1767785..eea5063cb7 100644 --- a/.ci/targets/android_platform_tests.yaml +++ b/.ci/targets/android_platform_tests.yaml @@ -2,10 +2,10 @@ tasks: - name: prepare tool script: .ci/scripts/prepare_tool.sh infra_step: true # Note infra steps failing prevents "always" from running. - - name: download android deps + - name: download Dart and Android deps script: script/tool_runner.sh infra_step: true - args: ["fetch-deps"] + args: ["fetch-deps", "--android", "--supporting-target-platforms-only"] - name: build examples script: script/tool_runner.sh args: ["build-examples", "--apk"] diff --git a/.ci/targets/ios_platform_tests.yaml b/.ci/targets/ios_platform_tests.yaml index 97b92d3830..212c8c95b0 100644 --- a/.ci/targets/ios_platform_tests.yaml +++ b/.ci/targets/ios_platform_tests.yaml @@ -5,6 +5,10 @@ tasks: - name: create simulator script: .ci/scripts/create_simulator.sh infra_step: true # Note infra steps failing prevents "always" from running. + - name: download Dart and iOS deps + script: script/tool_runner.sh + args: ["fetch-deps", "--ios", "--supporting-target-platforms-only"] + infra_step: true - name: build examples script: script/tool_runner.sh args: ["build-examples", "--ios"] diff --git a/.ci/targets/linux_platform_tests.yaml b/.ci/targets/linux_platform_tests.yaml index 3455958ad0..6d13b7d418 100644 --- a/.ci/targets/linux_platform_tests.yaml +++ b/.ci/targets/linux_platform_tests.yaml @@ -3,6 +3,11 @@ tasks: script: .ci/scripts/prepare_tool.sh - name: set default apps script: .ci/scripts/set_default_linux_apps.sh + infra_step: true + - name: download Dart deps + script: script/tool_runner.sh + args: ["fetch-deps", "--linux", "--supporting-target-platforms-only"] + infra_step: true - name: build examples script: script/tool_runner.sh args: ["build-examples", "--linux"] diff --git a/.ci/targets/macos_platform_tests.yaml b/.ci/targets/macos_platform_tests.yaml index 92b937e943..512e1c91f8 100644 --- a/.ci/targets/macos_platform_tests.yaml +++ b/.ci/targets/macos_platform_tests.yaml @@ -2,6 +2,10 @@ tasks: - name: prepare tool script: .ci/scripts/prepare_tool.sh infra_step: true # Note infra steps failing prevents "always" from running. + - name: download Dart and macOS deps + script: script/tool_runner.sh + args: ["fetch-deps", "--macos", "--supporting-target-platforms-only"] + infra_step: true - name: build examples script: script/tool_runner.sh args: ["build-examples", "--macos"] diff --git a/.ci/targets/web_platform_tests.yaml b/.ci/targets/web_platform_tests.yaml index acfb9cd8a6..2d28b45bd8 100644 --- a/.ci/targets/web_platform_tests.yaml +++ b/.ci/targets/web_platform_tests.yaml @@ -2,6 +2,10 @@ tasks: - name: prepare tool script: .ci/scripts/prepare_tool.sh infra_step: true # Note infra steps failing prevents "always" from running. + - name: download Dart deps + script: script/tool_runner.sh + args: ["fetch-deps", "--web", "--supporting-target-platforms-only"] + infra_step: true - name: build examples script: script/tool_runner.sh args: ["build-examples", "--web"] diff --git a/.ci/targets/windows_build_and_platform_tests.yaml b/.ci/targets/windows_build_and_platform_tests.yaml index 4bd73f9130..da1dfa85ec 100644 --- a/.ci/targets/windows_build_and_platform_tests.yaml +++ b/.ci/targets/windows_build_and_platform_tests.yaml @@ -2,6 +2,10 @@ tasks: - name: prepare tool script: .ci/scripts/prepare_tool.sh infra_step: true # Note infra steps failing prevents "always" from running. + - name: download Dart deps + script: script/tool_runner.sh + args: ["fetch-deps", "--windows", "--supporting-target-platforms-only"] + infra_step: true - name: build examples (Win32) script: .ci/scripts/build_examples_win32.sh - name: native unit tests (Win32) diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 728ac1172c..0f78d39114 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -176,9 +176,8 @@ class BuildExamplesCommand extends PackageLoopingCommand { // supported platforms. For packages, just log and skip any requested // platform that a package doesn't have set up. if (!isPlugin && - !example.directory - .childDirectory(platform.flutterPlatformDirectory) - .existsSync()) { + !example.appSupportsPlatform( + getPlatformByName(platform.pluginPlatform))) { print('Skipping ${platform.label} for $packageName; not supported.'); continue; } @@ -304,11 +303,6 @@ class _PlatformDetails { /// The `flutter build` build type. final String flutterBuildType; - /// The Flutter platform directory name. - // In practice, this is the same as the plugin platform key for all platforms. - // If that changes, this can be adjusted. - String get flutterPlatformDirectory => pluginPlatform; - /// Any extra flags to pass to `flutter build`. final List extraBuildFlags; } diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 96224f5b15..4cf62b241d 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -37,6 +37,25 @@ const String kEnableExperiment = 'enable-experiment'; // ignore: public_member_api_docs enum FlutterPlatform { android, ios, linux, macos, web, windows } +const Map _platformByName = { + platformAndroid: FlutterPlatform.android, + platformIOS: FlutterPlatform.ios, + platformLinux: FlutterPlatform.linux, + platformMacOS: FlutterPlatform.macos, + platformWeb: FlutterPlatform.web, + platformWindows: FlutterPlatform.windows, +}; + +/// Maps from a platform name (e.g., flag or platform directory) to the +/// corresponding platform enum. +FlutterPlatform getPlatformByName(String name) { + final FlutterPlatform? platform = _platformByName[name]; + if (platform == null) { + throw ArgumentError('Invalid platform: $name'); + } + return platform; +} + // Flutter->Dart SDK version mapping. Any time a command fails to look up a // corresponding version, this map should be updated. final Map _dartSdkForFlutterSdk = { diff --git a/script/tool/lib/src/common/process_runner.dart b/script/tool/lib/src/common/process_runner.dart index 7556b559c5..8ac98b8394 100644 --- a/script/tool/lib/src/common/process_runner.dart +++ b/script/tool/lib/src/common/process_runner.dart @@ -30,12 +30,13 @@ class ProcessRunner { String executable, List args, { Directory? workingDir, + Map? environment, 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); + workingDirectory: workingDir?.path, environment: environment); await Future.wait(>[ io.stdout.addStream(process.stdout), io.stderr.addStream(process.stderr), @@ -62,14 +63,19 @@ class ProcessRunner { /// 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 { + Future run( + String executable, + List args, { + Directory? workingDir, + Map? environment, + 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, + environment: environment, stdoutEncoding: stdoutEncoding, stderrEncoding: stderrEncoding); if (result.exitCode != 0) { diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index 2a824f54a2..345ace50e5 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -92,6 +92,16 @@ class RepositoryPackage { return directory.childDirectory(directoryName); } + /// Returns true if the package is an app that supports [platform]. + /// + /// The "app" prefix on this method is because this currently only works + /// for app packages (e.g., examples). + // TODO(stuartmorgan): Add support for non-app packages, by parsing the + // pubspec for `flutter:platform:` or `platform:` sections. + bool appSupportsPlatform(FlutterPlatform platform) { + return platformDirectory(platform).existsSync(); + } + late final Pubspec _parsedPubspec = Pubspec.parse(pubspecFile.readAsStringSync()); diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index ac3e6f4407..535ea1e6e1 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -254,7 +254,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { for (final MapEntry> entry in _targetDeviceFlags.entries) { final String platform = entry.key; - if (example.directory.childDirectory(platform).existsSync()) { + if (example.appSupportsPlatform(getPlatformByName(platform))) { deviceFlags.addAll(entry.value); } else { final String exampleName = diff --git a/script/tool/lib/src/fetch_deps_command.dart b/script/tool/lib/src/fetch_deps_command.dart index ce70b41524..2f763a04c2 100644 --- a/script/tool/lib/src/fetch_deps_command.dart +++ b/script/tool/lib/src/fetch_deps_command.dart @@ -2,6 +2,7 @@ // 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 'common/core.dart'; import 'common/gradle.dart'; @@ -10,12 +11,15 @@ import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/repository_package.dart'; -/// Download dependencies for the following platforms {android}. +const int _exitPrecacheFailed = 3; +const int _exitNothingRequested = 4; + +/// Download dependencies, both Dart and native. /// /// Specficially each platform runs: /// Android: 'gradlew dependencies'. -/// Dart: TBD (flutter/flutter/issues/130279) -/// iOS: TBD (flutter/flutter/issues/130280) +/// Dart: 'flutter pub get'. +/// iOS/macOS: 'pod install'. /// /// See https://docs.gradle.org/6.4/userguide/core_dependency_management.html#sec:dependency-mgmt-in-gradle. class FetchDepsCommand extends PackageLoopingCommand { @@ -24,25 +28,153 @@ class FetchDepsCommand extends PackageLoopingCommand { super.packagesDir, { super.processRunner, super.platform, - }); + }) { + argParser.addFlag(_dartFlag, defaultsTo: true, help: 'Run "pub get"'); + argParser.addFlag(_supportingTargetPlatformsOnlyFlag, + help: 'Restricts "pub get" runs to packages that have at least one ' + 'example supporting at least one of the platform flags passed.\n' + 'If no platform flags are passed, this will exclude all packages.'); + argParser.addFlag(platformAndroid, + help: 'Run "gradlew dependencies" for Android plugins.\n' + 'Include packages with Android examples when used with ' + '--$_supportingTargetPlatformsOnlyFlag'); + argParser.addFlag(platformIOS, + help: 'Run "pod install" for iOS plugins.\n' + 'Include packages with iOS examples when used with ' + '--$_supportingTargetPlatformsOnlyFlag'); + argParser.addFlag(platformLinux, + help: 'Include packages with Linux examples when used with ' + '--$_supportingTargetPlatformsOnlyFlag'); + argParser.addFlag(platformMacOS, + help: 'Run "pod install" for macOS plugins.\n' + 'Include packages with macOS examples when used with ' + '--$_supportingTargetPlatformsOnlyFlag'); + argParser.addFlag(platformWeb, + help: 'Include packages with Web examples when used with ' + '--$_supportingTargetPlatformsOnlyFlag'); + argParser.addFlag(platformWindows, + help: 'Include packages with Windows examples when used with ' + '--$_supportingTargetPlatformsOnlyFlag'); + } + + static const String _dartFlag = 'dart'; + static const String _supportingTargetPlatformsOnlyFlag = + 'supporting-target-platforms-only'; + + static const Iterable _platforms = [ + platformAndroid, + platformIOS, + platformLinux, + platformMacOS, + platformWeb, + platformWindows, + ]; @override final String name = 'fetch-deps'; @override - final String description = 'Fetches dependencies for plugins.\n' - 'Runs "gradlew dependencies" on Android plugins.\n' - 'Dart see flutter/flutter/issues/130279\n' - 'iOS plugins see flutter/flutter/issues/130280\n' - '\n' - 'Requires the examples to have been built at least once before running.'; + final String description = 'Fetches dependencies for packages'; + + @override + Future initializeRun() async { + // `pod install` requires having the platform artifacts precached. See + // https://github.com/flutter/flutter/blob/fb7a763c640d247d090cbb373e4b3a0459ac171b/packages/flutter_tools/bin/podhelper.rb#L47 + // https://github.com/flutter/flutter/blob/fb7a763c640d247d090cbb373e4b3a0459ac171b/packages/flutter_tools/bin/podhelper.rb#L130 + if (getBoolArg(platformIOS)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + ['precache', '--ios'], + ); + if (exitCode != 0) { + throw ToolExit(_exitPrecacheFailed); + } + } + if (getBoolArg(platformMacOS)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + ['precache', '--macos'], + ); + if (exitCode != 0) { + throw ToolExit(_exitPrecacheFailed); + } + } + } @override Future runForPackage(RepositoryPackage package) async { + bool fetchedDeps = false; + final List skips = []; + if (getBoolArg(_dartFlag)) { + final bool filterPlatforms = + getBoolArg(_supportingTargetPlatformsOnlyFlag); + if (!filterPlatforms || _hasExampleSupportingRequestedPlatform(package)) { + fetchedDeps = true; + if (!await _fetchDartPackages(package)) { + // If Dart-level depenendencies fail, fail immediately since the + // native dependencies won't be useful. + return PackageResult.fail(['Failed to "pub get".']); + } + } else { + skips.add('Skipping Dart dependencies; no examples support requested ' + 'platforms.'); + } + } + + final List errors = []; + for (final FlutterPlatform platform in _targetPlatforms) { + final PackageResult result; + switch (platform) { + case FlutterPlatform.android: + result = await _fetchAndroidDeps(package); + break; + case FlutterPlatform.ios: + result = await _fetchDarwinDeps(package, platformIOS); + break; + case FlutterPlatform.macos: + result = await _fetchDarwinDeps(package, platformMacOS); + break; + case FlutterPlatform.linux: + case FlutterPlatform.web: + case FlutterPlatform.windows: + // No native dependency handling yet. + result = PackageResult.skip('Nothing to do for $platform.'); + break; + } + switch (result.state) { + case RunState.succeeded: + fetchedDeps = true; + break; + case RunState.skipped: + skips.add(result.details.first); + break; + case RunState.failed: + errors.addAll(result.details); + break; + case RunState.excluded: + throw StateError('Unreachable'); + } + } + + if (errors.isNotEmpty) { + return PackageResult.fail(errors); + } + if (fetchedDeps) { + return PackageResult.success(); + } + if (skips.isNotEmpty) { + return PackageResult.skip(['', ...skips].join('\n- ')); + } + + printError('At least one type of dependency must be requested'); + throw ToolExit(_exitNothingRequested); + } + + Future _fetchAndroidDeps(RepositoryPackage package) async { if (!pluginSupportsPlatform(platformAndroid, package, requiredMode: PlatformSupport.inline)) { return PackageResult.skip( - 'Plugin does not have an Android implementation.'); + 'Package does not have native Android dependencies.'); } for (final RepositoryPackage example in package.getExamples()) { @@ -63,7 +195,8 @@ class FetchDepsCommand extends PackageLoopingCommand { final String packageName = package.directory.basename; - final int exitCode = await gradleProject.runCommand('$packageName:dependencies'); + final int exitCode = + await gradleProject.runCommand('$packageName:dependencies'); if (exitCode != 0) { return PackageResult.fail(); } @@ -71,4 +204,78 @@ class FetchDepsCommand extends PackageLoopingCommand { return PackageResult.success(); } + + Future _fetchDarwinDeps( + RepositoryPackage package, final String platform) async { + if (!pluginSupportsPlatform(platform, package, + requiredMode: PlatformSupport.inline)) { + // Convert from the flag (lower case ios/macos) to the actual name. + final String displayPlatform = platform.replaceFirst('os', 'OS'); + return PackageResult.skip( + 'Package does not have native $displayPlatform dependencies.'); + } + + for (final RepositoryPackage example in package.getExamples()) { + final Directory platformDir = + example.platformDirectory(getPlatformByName(platform)); + + // Running `pod install` requires `flutter pub get` or `flutter build` to + // have been run at some point to create the necessary native build files. + // See https://github.com/flutter/flutter/blob/fb7a763c640d247d090cbb373e4b3a0459ac171b/packages/flutter_tools/templates/cocoapods/Podfile-macos#L13-L15 + // and https://github.com/flutter/flutter/blob/fb7a763c640d247d090cbb373e4b3a0459ac171b/packages/flutter_tools/templates/cocoapods/Podfile-ios-swift#L14-L16 + final File generatedXCConfig = platform == platformMacOS + ? platformDir + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childFile('Flutter-Generated.xcconfig') + : platformDir + .childDirectory('Flutter') + .childFile('Generated.xcconfig'); + if (!generatedXCConfig.existsSync()) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + ['pub', 'get'], + workingDir: example.directory, + ); + if (exitCode != 0) { + printError('Unable to prepare native project files.'); + return PackageResult.fail(['Unable to configure project.']); + } + } + + final int exitCode = await processRunner.runAndStream( + 'pod', + ['install'], + workingDir: platformDir, + environment: { + 'LANG': 'en_US.UTF-8', + }, + ); + if (exitCode != 0) { + printError('Unable to "pod install"'); + return PackageResult.fail(['Unable to "pod install"']); + } + } + + return PackageResult.success(); + } + + Future _fetchDartPackages(RepositoryPackage package) async { + final String command = package.requiresFlutter() ? flutterCommand : 'dart'; + final int exitCode = await processRunner.runAndStream( + command, ['pub', 'get'], + workingDir: package.directory); + return exitCode == 0; + } + + bool _hasExampleSupportingRequestedPlatform(RepositoryPackage package) { + return package.getExamples().any((RepositoryPackage example) { + return _targetPlatforms.any( + (FlutterPlatform platform) => example.appSupportsPlatform(platform)); + }); + } + + Iterable get _targetPlatforms => _platforms + .where((String platform) => getBoolArg(platform)) + .map((String platformName) => getPlatformByName(platformName)); } diff --git a/script/tool/test/fetch_deps_command_test.dart b/script/tool/test/fetch_deps_command_test.dart index baff6f657d..ab4cd3fbf4 100644 --- a/script/tool/test/fetch_deps_command_test.dart +++ b/script/tool/test/fetch_deps_command_test.dart @@ -36,7 +36,142 @@ void main() { CommandRunner('fetch_deps_test', 'Test for $FetchDepsCommand'); runner.addCommand(command); }); + + group('dart', () { + test('runs pub get', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin1', packagesDir, platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); + + final List output = + await runCapturingPrint(runner, ['fetch-deps']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['pub', 'get'], + plugin.directory.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('fails if pub get fails', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); + + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + FakeProcessInfo(MockProcess(exitCode: 1)), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['fetch-deps'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Failed to "pub get"'), + ], + )); + }); + + test('skips unsupported packages when any platforms are passed', + () async { + final RepositoryPackage packageWithBoth = createFakePackage( + 'supports_both', packagesDir, extraFiles: [ + 'example/linux/placeholder', + 'example/windows/placeholder' + ]); + final RepositoryPackage packageWithOne = createFakePackage( + 'supports_one', packagesDir, + extraFiles: ['example/linux/placeholder']); + createFakePackage('supports_neither', packagesDir); + + await runCapturingPrint(runner, [ + 'fetch-deps', + '--linux', + '--windows', + '--supporting-target-platforms-only' + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'dart', + const ['pub', 'get'], + packageWithBoth.path, + ), + ProcessCall( + 'dart', + const ['pub', 'get'], + packageWithOne.path, + ), + ]), + ); + }); + }); + group('android', () { + test('runs pub get before gradlew dependencies', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/android/gradlew', + ], platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline) + }); + + final Directory androidDir = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--android']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['pub', 'get'], + plugin.directory.path, + ), + ProcessCall( + androidDir.childFile('gradlew').path, + const ['plugin1:dependencies'], + androidDir.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + test('runs gradlew dependencies', () async { final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, extraFiles: [ @@ -50,8 +185,8 @@ void main() { .first .platformDirectory(FlutterPlatform.android); - final List output = - await runCapturingPrint(runner, ['fetch-deps']); + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--android']); expect( processRunner.recordedCalls, @@ -89,8 +224,8 @@ void main() { (RepositoryPackage example) => example.platformDirectory(FlutterPlatform.android)); - final List output = - await runCapturingPrint(runner, ['fetch-deps']); + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--android']); expect( processRunner.recordedCalls, @@ -123,8 +258,8 @@ void main() { .first .platformDirectory(FlutterPlatform.android); - final List output = - await runCapturingPrint(runner, ['fetch-deps']); + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--android']); expect( processRunner.recordedCalls, @@ -164,7 +299,8 @@ void main() { Error? commandError; final List output = await runCapturingPrint( - runner, ['fetch-deps'], errorHandler: (Error e) { + runner, ['fetch-deps', '--no-dart', '--android'], + errorHandler: (Error e) { commandError = e; }); @@ -199,7 +335,8 @@ void main() { Error? commandError; final List output = await runCapturingPrint( - runner, ['fetch-deps'], errorHandler: (Error e) { + runner, ['fetch-deps', '--no-dart', '--android'], + errorHandler: (Error e) { commandError = e; }); @@ -212,41 +349,479 @@ void main() { ], )); }); + + test('skips non-Android plugins', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--android']); + + expect( + output, + containsAllInOrder( + [ + contains('Package does not have native Android dependencies.') + ], + )); + }); + + test('skips non-inline plugins', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.federated) + }); + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--android']); + + expect( + output, + containsAllInOrder( + [ + contains('Package does not have native Android dependencies.') + ], + )); + }); }); - test('skips non-Android plugins', () async { - createFakePlugin('plugin1', packagesDir); + group('ios', () { + test('runs pub get before pod install', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/ios/Flutter/Generated.xcconfig', + ], platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); - final List output = - await runCapturingPrint(runner, ['fetch-deps']); + final Directory iOSDir = + plugin.getExamples().first.platformDirectory(FlutterPlatform.ios); - expect( - output, - containsAllInOrder( - [ - contains( - 'SKIPPING: Plugin does not have an Android implementation.') + final List output = + await runCapturingPrint(runner, ['fetch-deps', '--ios']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'flutter', + ['precache', '--ios'], + null, + ), + ProcessCall( + 'flutter', + const ['pub', 'get'], + plugin.directory.path, + ), + ProcessCall( + 'pod', + const ['install'], + iOSDir.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('runs on all examples', () async { + final List examples = ['example1', 'example2']; + final RepositoryPackage plugin = createFakePlugin( + 'plugin1', packagesDir, + examples: examples, + extraFiles: [ + 'example/example1/ios/Flutter/Generated.xcconfig', + 'example/example2/ios/Flutter/Generated.xcconfig', ], - )); + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); + + final Iterable exampleIOSDirs = plugin.getExamples().map( + (RepositoryPackage example) => + example.platformDirectory(FlutterPlatform.ios)); + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--ios']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'flutter', + ['precache', '--ios'], + null, + ), + for (final Directory directory in exampleIOSDirs) + ProcessCall( + 'pod', + const ['install'], + directory.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('runs pub get if example is not configured', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin1', packagesDir, platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); + + final RepositoryPackage example = plugin.getExamples().first; + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--ios']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'flutter', + ['precache', '--ios'], + null, + ), + ProcessCall( + 'flutter', + const ['pub', 'get'], + example.directory.path, + ), + ProcessCall( + 'pod', + const ['install'], + example.platformDirectory(FlutterPlatform.ios).path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('fails if pre-pod pub get fails', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); + + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + FakeProcessInfo(MockProcess(), ['precache']), + FakeProcessInfo(MockProcess(exitCode: 1), ['pub', 'get']), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--ios'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Unable to configure project'), + ], + )); + }); + + test('fails if pod install fails', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); + + processRunner.mockProcessesForExecutable['pod'] = [ + FakeProcessInfo(MockProcess(exitCode: 1)), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--ios'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('The following packages had errors:'), + ], + )); + }); + + test('skips non-iOS plugins', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--ios']); + + expect( + output, + containsAllInOrder( + [ + contains('Package does not have native iOS dependencies.') + ], + )); + }); + + test('skips non-inline plugins', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.federated) + }); + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--ios']); + + expect( + output, + containsAllInOrder( + [ + contains('Package does not have native iOS dependencies.') + ], + )); + }); }); - test('skips non-inline plugins', () async { - createFakePlugin('plugin1', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.federated) - }); + group('macos', () { + test('runs pub get before pod install', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig', + ], platformSupport: { + platformMacOS: const PlatformDetails(PlatformSupport.inline) + }); - final List output = - await runCapturingPrint(runner, ['fetch-deps']); + final Directory macOSDir = + plugin.getExamples().first.platformDirectory(FlutterPlatform.macos); - expect( - output, - containsAllInOrder( - [ - contains( - 'SKIPPING: Plugin does not have an Android implementation.') + final List output = + await runCapturingPrint(runner, ['fetch-deps', '--macos']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'flutter', + ['precache', '--macos'], + null, + ), + ProcessCall( + 'flutter', + const ['pub', 'get'], + plugin.directory.path, + ), + ProcessCall( + 'pod', + const ['install'], + macOSDir.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('runs on all examples', () async { + final List examples = ['example1', 'example2']; + final RepositoryPackage plugin = createFakePlugin( + 'plugin1', packagesDir, + examples: examples, + extraFiles: [ + 'example/example1/macos/Flutter/ephemeral/Flutter-Generated.xcconfig', + 'example/example2/macos/Flutter/ephemeral/Flutter-Generated.xcconfig', ], - )); + platformSupport: { + platformMacOS: const PlatformDetails(PlatformSupport.inline) + }); + + final Iterable examplemacOSDirs = plugin.getExamples().map( + (RepositoryPackage example) => + example.platformDirectory(FlutterPlatform.macos)); + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--macos']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'flutter', + ['precache', '--macos'], + null, + ), + for (final Directory directory in examplemacOSDirs) + ProcessCall( + 'pod', + const ['install'], + directory.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('runs pub get if example is not configured', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin1', packagesDir, platformSupport: { + platformMacOS: const PlatformDetails(PlatformSupport.inline) + }); + + final RepositoryPackage example = plugin.getExamples().first; + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--macos']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'flutter', + ['precache', '--macos'], + null, + ), + ProcessCall( + 'flutter', + const ['pub', 'get'], + example.directory.path, + ), + ProcessCall( + 'pod', + const ['install'], + example.platformDirectory(FlutterPlatform.macos).path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('fails if pre-pod pub get fails', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + platformMacOS: const PlatformDetails(PlatformSupport.inline) + }); + + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + FakeProcessInfo(MockProcess(), ['precache']), + FakeProcessInfo(MockProcess(exitCode: 1), ['pub', 'get']), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Unable to configure project'), + ], + )); + }); + + test('fails if pod install fails', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + platformMacOS: const PlatformDetails(PlatformSupport.inline) + }); + + processRunner.mockProcessesForExecutable['pod'] = [ + FakeProcessInfo(MockProcess(exitCode: 1)), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('The following packages had errors:'), + ], + )); + }); + + test('skips non-macOS plugins', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--macos']); + + expect( + output, + containsAllInOrder( + [ + contains('Package does not have native macOS dependencies.') + ], + )); + }); + + test('skips non-inline plugins', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + platformMacOS: const PlatformDetails(PlatformSupport.federated) + }); + + final List output = await runCapturingPrint( + runner, ['fetch-deps', '--no-dart', '--macos']); + + expect( + output, + containsAllInOrder( + [ + contains('Package does not have native macOS dependencies.') + ], + )); + }); }); }); } diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 4f38dd416a..1fd6abd80b 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -394,6 +394,7 @@ class RecordingProcessRunner extends ProcessRunner { String executable, List args, { Directory? workingDir, + Map? environment, bool exitOnError = false, }) async { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); @@ -412,6 +413,7 @@ class RecordingProcessRunner extends ProcessRunner { String executable, List args, { Directory? workingDir, + Map? environment, bool exitOnError = false, bool logOnError = false, Encoding stdoutEncoding = io.systemEncoding,