diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart index 2c1c63798b..dff347f7cb 100644 --- a/packages/flutter_tools/lib/src/commands/analyze.dart +++ b/packages/flutter_tools/lib/src/commands/analyze.dart @@ -16,6 +16,7 @@ import '../runner/flutter_command.dart'; import 'analyze_base.dart'; import 'analyze_continuously.dart'; import 'analyze_once.dart'; +import 'android_analyze.dart'; import 'validate_project.dart'; class AnalyzeCommand extends FlutterCommand { @@ -99,6 +100,37 @@ class AnalyzeCommand extends FlutterCommand { argParser.addFlag('fatal-warnings', help: 'Treat warning level issues as fatal.', defaultsTo: true); + + argParser.addFlag('android', + negatable: false, + help: 'Analyze Android sub-project. Used by internal tools only.', + hide: !verboseHelp, + ); + + if (verboseHelp) { + argParser.addSeparator('Usage: flutter analyze --android [arguments]'); + } + + argParser.addFlag('list-build-variants', + negatable: false, + help: 'Print out a list of available build variants for the ' + 'Android sub-project.', + hide: !verboseHelp, + ); + + argParser.addFlag('output-app-link-settings', + negatable: false, + help: 'Output a JSON with Android app link settings into a file. ' + 'The "--build-variant" must also be set.', + hide: !verboseHelp, + ); + + argParser.addOption('build-variant', + help: 'Sets the Android build variant to be analyzed.', + valueHelp: 'use "flutter analyze --android --list-build-variants" to get ' + 'all available build variants', + hide: !verboseHelp, + ); } /// The working directory for testing analysis using dartanalyzer. @@ -142,12 +174,51 @@ class AnalyzeCommand extends FlutterCommand { return false; } + // Don't run pub if asking for android analysis. + if (boolArg('android')) { + return false; + } + return super.shouldRunPub; } @override Future runCommand() async { - if (boolArg('suggestions')) { + if (boolArg('android')) { + final AndroidAnalyzeOption option; + final String? buildVariant; + if (argResults!['list-build-variants'] as bool && argResults!['output-app-link-settings'] as bool) { + throwToolExit('Only one of "--list-build-variants" or "--output-app-link-settings" can be provided'); + } + if (argResults!['list-build-variants'] as bool) { + option = AndroidAnalyzeOption.listBuildVariant; + buildVariant = null; + } else if (argResults!['output-app-link-settings'] as bool) { + option = AndroidAnalyzeOption.outputAppLinkSettings; + buildVariant = argResults!['build-variant'] as String?; + if (buildVariant == null) { + throwToolExit('"--build-variant" must be provided'); + } + } else { + throwToolExit('No argument is provided to analyze. Use -h to see available commands.'); + } + final Set items = findDirectories(argResults!, _fileSystem); + final String directoryPath; + if (items.isEmpty) { // user did not specify any path + directoryPath = _fileSystem.currentDirectory.path; + } else if (items.length > 1) { // if the user sends more than one path + throwToolExit('The Android analyze can process only one directory path'); + } else { + directoryPath = items.first; + } + await AndroidAnalyze( + fileSystem: _fileSystem, + option: option, + userPath: directoryPath, + buildVariant: buildVariant, + logger: _logger, + ).analyze(); + } else if (boolArg('suggestions')) { final String directoryPath; if (boolArg('watch')) { throwToolExit('flag --watch is not compatible with --suggestions'); diff --git a/packages/flutter_tools/lib/src/commands/android_analyze.dart b/packages/flutter_tools/lib/src/commands/android_analyze.dart new file mode 100644 index 0000000000..c4389ff41e --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/android_analyze.dart @@ -0,0 +1,56 @@ +// Copyright 2014 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 '../base/file_system.dart'; +import '../base/logger.dart'; +import '../convert.dart'; +import '../project.dart'; + +/// The type of analysis to perform. +enum AndroidAnalyzeOption { + /// Prints out available build variants of the Android sub-project. + /// + /// An example output: + /// ["debug", "profile", "release"] + listBuildVariant, + + /// Outputs app link settings of the Android sub-project into a file. + /// + /// The file path will be printed after the command is run successfully. + outputAppLinkSettings, +} + +/// Analyze the Android sub-project of a Flutter project. +/// +/// The [userPath] must be point to a flutter project. +class AndroidAnalyze { + AndroidAnalyze({ + required this.fileSystem, + required this.option, + required this.userPath, + this.buildVariant, + required this.logger, + }) : assert(option == AndroidAnalyzeOption.listBuildVariant || buildVariant != null); + + final FileSystem fileSystem; + final AndroidAnalyzeOption option; + final String? buildVariant; + final String userPath; + final Logger logger; + + Future analyze() async { + final FlutterProject project = FlutterProject.fromDirectory(fileSystem.directory(userPath)); + switch (option) { + case AndroidAnalyzeOption.listBuildVariant: + logger.printStatus(jsonEncode(await project.android.getBuildVariants())); + case AndroidAnalyzeOption.outputAppLinkSettings: + assert(buildVariant != null); + await project.android.outputsAppLinkSettings(variant: buildVariant!); + final String filePath = fileSystem.path.join(project.directory.path, 'build', 'app', 'app-link-settings-$buildVariant.json`'); + logger.printStatus('result saved in $filePath'); + } + } +} diff --git a/packages/flutter_tools/test/commands.shard/hermetic/android_analyze_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/android_analyze_test.dart new file mode 100644 index 0000000000..44c9c5fa34 --- /dev/null +++ b/packages/flutter_tools/test/commands.shard/hermetic/android_analyze_test.dart @@ -0,0 +1,129 @@ +// Copyright 2014 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 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/android/android_builder.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/analyze.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/project_validator.dart'; +import 'package:test/fake.dart'; + +import '../../src/context.dart'; +import '../../src/test_flutter_command_runner.dart'; + +void main() { + + group('Android analyze command', () { + late FileSystem fileSystem; + late Platform platform; + late BufferLogger logger; + late FakeProcessManager processManager; + late Terminal terminal; + late AnalyzeCommand command; + late CommandRunner runner; + late Directory tempDir; + late FakeAndroidBuilder builder; + + setUpAll(() { + Cache.disableLocking(); + }); + + setUp(() async { + fileSystem = MemoryFileSystem.test(); + platform = FakePlatform(); + logger = BufferLogger.test(); + processManager = FakeProcessManager.empty(); + terminal = Terminal.test(); + command = AnalyzeCommand( + artifacts: Artifacts.test(), + fileSystem: fileSystem, + logger: logger, + platform: platform, + processManager: processManager, + terminal: terminal, + allProjectValidators: [], + suppressAnalytics: true, + ); + runner = createTestCommandRunner(command); + tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_tools_packages_test.'); + tempDir.childDirectory('android').createSync(); + + // Setup repo roots + const String homePath = '/home/user/flutter'; + Cache.flutterRoot = homePath; + for (final String dir in ['dev', 'examples', 'packages']) { + fileSystem.directory(homePath).childDirectory(dir).createSync(recursive: true); + } + builder = FakeAndroidBuilder(); + + }); + + testUsingContext('can list build variants', () async { + builder.variants = ['debug', 'release']; + await runner.run(['analyze', '--android', '--list-build-variants', tempDir.path]); + expect(logger.statusText, contains('["debug","release"]')); + }, overrides: { + AndroidBuilder: () => builder, + }); + + testUsingContext('throw if provide multiple path', () async { + final Directory anotherTempDir = fileSystem.systemTempDirectory.createTempSync('another'); + await expectLater( + runner.run(['analyze', '--android', '--list-build-variants', tempDir.path, anotherTempDir.path]), + throwsA( + isA().having( + (Exception e) => e.toString(), + 'description', + contains('The Android analyze can process only one directory path'), + ), + ), + ); + }); + + testUsingContext('can output app link settings', () async { + const String buildVariant = 'release'; + await runner.run(['analyze', '--android', '--output-app-link-settings', '--build-variant=$buildVariant', tempDir.path]); + expect(builder.outputVariant, buildVariant); + }, overrides: { + AndroidBuilder: () => builder, + }); + + testUsingContext('output app link settings throws if no build variant', () async { + await expectLater( + runner.run(['analyze', '--android', '--output-app-link-settings', tempDir.path]), + throwsA( + isA().having( + (Exception e) => e.toString(), + 'description', + contains('"--build-variant" must be provided'), + ), + ), + ); + }); + }); +} + +class FakeAndroidBuilder extends Fake implements AndroidBuilder { + List variants = const []; + String? outputVariant; + + @override + Future> getBuildVariants({required FlutterProject project}) async { + return variants; + } + + @override + Future outputsAppLinkSettings(String buildVariant, {required FlutterProject project}) async { + outputVariant = buildVariant; + } +}