From 369ee7e1a1cba9cd49421dd12bd1fd6fa69d6b68 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Fri, 14 Jul 2023 09:04:20 -0700 Subject: [PATCH] [Tool] New tool to download android dependencies (#4408) This pr is pushed for high level feedback/conversation. I will add tests before serious review. should be read in conjuction with https://flutter-review.googlesource.com/c/recipes/+/46980 - Create new top level command to run flutter dependencies on changed packages - when running android tests download dependencies before running tests https://github.com/flutter/flutter/issues/120119 --- .ci/targets/android_platform_tests.yaml | 5 + script/tool/lib/src/fetch_deps_command.dart | 74 +++++ script/tool/lib/src/main.dart | 2 + script/tool/test/fetch_deps_command_test.dart | 252 ++++++++++++++++++ 4 files changed, 333 insertions(+) create mode 100644 script/tool/lib/src/fetch_deps_command.dart create mode 100644 script/tool/test/fetch_deps_command_test.dart diff --git a/.ci/targets/android_platform_tests.yaml b/.ci/targets/android_platform_tests.yaml index ee5de6e56b1ef..40dbc7d2d0173 100644 --- a/.ci/targets/android_platform_tests.yaml +++ b/.ci/targets/android_platform_tests.yaml @@ -1,6 +1,11 @@ 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 + script: script/tool_runner.sh + infra_step: true + args: ["fetch-deps"] - name: build examples script: script/tool_runner.sh args: ["build-examples", "--apk"] diff --git a/script/tool/lib/src/fetch_deps_command.dart b/script/tool/lib/src/fetch_deps_command.dart new file mode 100644 index 0000000000000..ce70b41524c5b --- /dev/null +++ b/script/tool/lib/src/fetch_deps_command.dart @@ -0,0 +1,74 @@ +// 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 'common/core.dart'; +import 'common/gradle.dart'; +import 'common/output_utils.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/repository_package.dart'; + +/// Download dependencies for the following platforms {android}. +/// +/// Specficially each platform runs: +/// Android: 'gradlew dependencies'. +/// Dart: TBD (flutter/flutter/issues/130279) +/// iOS: TBD (flutter/flutter/issues/130280) +/// +/// See https://docs.gradle.org/6.4/userguide/core_dependency_management.html#sec:dependency-mgmt-in-gradle. +class FetchDepsCommand extends PackageLoopingCommand { + /// Creates an instance of the fetch-deps command. + FetchDepsCommand( + super.packagesDir, { + super.processRunner, + super.platform, + }); + + @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.'; + + @override + Future runForPackage(RepositoryPackage package) async { + if (!pluginSupportsPlatform(platformAndroid, package, + requiredMode: PlatformSupport.inline)) { + return PackageResult.skip( + 'Plugin does not have an Android implementation.'); + } + + for (final RepositoryPackage example in package.getExamples()) { + final GradleProject gradleProject = GradleProject(example, + processRunner: processRunner, platform: platform); + + if (!gradleProject.isConfigured()) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + ['build', 'apk', '--config-only'], + workingDir: example.directory, + ); + if (exitCode != 0) { + printError('Unable to configure Gradle project.'); + return PackageResult.fail(['Unable to configure Gradle.']); + } + } + + final String packageName = package.directory.basename; + + final int exitCode = await gradleProject.runCommand('$packageName:dependencies'); + if (exitCode != 0) { + return PackageResult.fail(); + } + } + + return PackageResult.success(); + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 78fa5a3f0c7ba..89ffae268276b 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -17,6 +17,7 @@ import 'dart_test_command.dart'; import 'dependabot_check_command.dart'; import 'drive_examples_command.dart'; import 'federation_safety_check_command.dart'; +import 'fetch_deps_command.dart'; import 'firebase_test_lab_command.dart'; import 'fix_command.dart'; import 'format_command.dart'; @@ -65,6 +66,7 @@ void main(List args) { ..addCommand(DependabotCheckCommand(packagesDir)) ..addCommand(DriveExamplesCommand(packagesDir)) ..addCommand(FederationSafetyCheckCommand(packagesDir)) + ..addCommand(FetchDepsCommand(packagesDir)) ..addCommand(FirebaseTestLabCommand(packagesDir)) ..addCommand(FixCommand(packagesDir)) ..addCommand(FormatCommand(packagesDir)) diff --git a/script/tool/test/fetch_deps_command_test.dart b/script/tool/test/fetch_deps_command_test.dart new file mode 100644 index 0000000000000..baff6f657d199 --- /dev/null +++ b/script/tool/test/fetch_deps_command_test.dart @@ -0,0 +1,252 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.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/fetch_deps_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('FetchDepsCommand', () { + FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late MockPlatform mockPlatform; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + mockPlatform = MockPlatform(); + processRunner = RecordingProcessRunner(); + final FetchDepsCommand command = FetchDepsCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = + CommandRunner('fetch_deps_test', 'Test for $FetchDepsCommand'); + runner.addCommand(command); + }); + group('android', () { + test('runs 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']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidDir.childFile('gradlew').path, + const ['plugin1:dependencies'], + androidDir.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/android/gradlew', + 'example/example2/android/gradlew', + ], + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline) + }); + + final Iterable exampleAndroidDirs = plugin.getExamples().map( + (RepositoryPackage example) => + example.platformDirectory(FlutterPlatform.android)); + + final List output = + await runCapturingPrint(runner, ['fetch-deps']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + for (final Directory directory in exampleAndroidDirs) + ProcessCall( + directory.childFile('gradlew').path, + const ['plugin1:dependencies'], + directory.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('runs --config-only build if gradlew is missing', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin1', packagesDir, platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline) + }); + + final Directory androidDir = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); + + final List output = + await runCapturingPrint(runner, ['fetch-deps']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'apk', '--config-only'], + plugin.getExamples().first.directory.path, + ), + ProcessCall( + androidDir.childFile('gradlew').path, + const ['plugin1:dependencies'], + androidDir.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('fails if gradlew generation fails', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + platformAndroid: 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('Unable to configure Gradle project'), + ], + )); + }); + + test('fails if dependency download finds issues', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/android/gradlew', + ], platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline) + }); + + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = + [ + 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('The following packages had errors:'), + ], + )); + }); + }); + + test('skips non-Android plugins', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = + await runCapturingPrint(runner, ['fetch-deps']); + + expect( + output, + containsAllInOrder( + [ + contains( + 'SKIPPING: Plugin does not have an Android implementation.') + ], + )); + }); + + test('skips non-inline plugins', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.federated) + }); + + final List output = + await runCapturingPrint(runner, ['fetch-deps']); + + expect( + output, + containsAllInOrder( + [ + contains( + 'SKIPPING: Plugin does not have an Android implementation.') + ], + )); + }); + }); +}