From f098de1fdedec2232aa740a6413f318166762795 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Tue, 10 Sep 2019 17:22:55 -0700 Subject: [PATCH] Enable Proguard by default on release mode (#39986) --- packages/flutter_tools/gradle/flutter.gradle | 42 +++- .../gradle/flutter_proguard_rules.pro | 11 + .../flutter_tools/lib/src/android/gradle.dart | 110 +++++---- .../flutter_tools/lib/src/build_info.dart | 4 + .../lib/src/commands/build_apk.dart | 11 +- .../lib/src/commands/build_appbundle.dart | 9 +- .../flutter_tools/lib/src/context_runner.dart | 2 + .../test/general.shard/channel_test.dart | 17 +- .../commands/build_apk_test.dart | 231 +++++++++++++++-- .../commands/build_appbundle_test.dart | 233 ++++++++++++++++-- .../general.shard/commands/upgrade_test.dart | 18 +- packages/flutter_tools/test/src/common.dart | 2 + packages/flutter_tools/test/src/mocks.dart | 18 ++ 13 files changed, 586 insertions(+), 122 deletions(-) create mode 100644 packages/flutter_tools/gradle/flutter_proguard_rules.pro diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index 349aa8e6095c..53b1c8ecc8cd 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle @@ -132,16 +132,6 @@ class FlutterPlugin implements Plugin { } } - // Add custom build types - project.android.buildTypes { - profile { - initWith debug - if (it.hasProperty('matchingFallbacks')) { - matchingFallbacks = ['debug', 'release'] - } - } - } - String flutterRootPath = resolveProperty(project, "flutter.sdk", System.env.FLUTTER_ROOT) if (flutterRootPath == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.") @@ -154,6 +144,30 @@ class FlutterPlugin implements Plugin { String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter" flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile(); + // Add custom build types. + project.android.buildTypes { + profile { + initWith debug + if (it.hasProperty("matchingFallbacks")) { + matchingFallbacks = ["debug", "release"] + } + } + } + + if (useProguard(project)) { + String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", + "gradle", "flutter_proguard_rules.pro") + project.android.buildTypes { + release { + minifyEnabled true + useProguard true + // Fallback to `android/app/proguard-rules.pro`. + // This way, custom Proguard rules can be configured as needed. + proguardFiles project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro" + } + } + } + if (useLocalEngine(project)) { String engineOutPath = project.property('localEngineOut') File engineOut = project.file(engineOutPath) @@ -375,6 +389,14 @@ class FlutterPlugin implements Plugin { return false } + + private static Boolean useProguard(Project project) { + if (project.hasProperty('proguard')) { + return project.property('proguard').toBoolean() + } + return false + } + private static Boolean buildPluginAsAar() { return System.getProperty('build-plugins-as-aars') == 'true' } diff --git a/packages/flutter_tools/gradle/flutter_proguard_rules.pro b/packages/flutter_tools/gradle/flutter_proguard_rules.pro new file mode 100644 index 000000000000..8fc13e7bb35e --- /dev/null +++ b/packages/flutter_tools/gradle/flutter_proguard_rules.pro @@ -0,0 +1,11 @@ +# Prevents `Fragment and FragmentActivity not found`. +# TODO(blasten): Remove once we bring the Maven dependencies. +-dontwarn io.flutter.embedding.** + +# Build the ephemeral app in a module project. +# Prevents: Warning: library class depends on program class io.flutter.plugin.** +# This is due to plugins (libraries) depending on the embedding (the program jar) +-dontwarn io.flutter.plugin.** + +# The android.** package is provided by the OS at runtime. +-dontwarn android.** diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 2b14a1afbc1f..aaf0979f0a45 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -10,6 +10,7 @@ import 'package:meta/meta.dart'; import '../android/android_sdk.dart'; import '../artifacts.dart'; import '../base/common.dart'; +import '../base/context.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/os.dart'; @@ -28,11 +29,39 @@ import '../reporting/reporting.dart'; import 'android_sdk.dart'; import 'android_studio.dart'; -final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)'); +/// Gradle utils in the current [AppContext]. +GradleUtils get gradleUtils => context.get(); + +/// Provides utilities to run a Gradle task, +/// such as finding the Gradle executable or constructing a Gradle project. +class GradleUtils { + /// Empty constructor. + GradleUtils(); + + String _cachedExecutable; + /// Gets the Gradle executable path. + /// This is the `gradlew` or `gradlew.bat` script in the `android/` directory. + Future getExecutable(FlutterProject project) async { + _cachedExecutable ??= await _initializeGradle(project); + return _cachedExecutable; + } + + GradleProject _cachedAppProject; + /// Gets the [GradleProject] for the current [FlutterProject] if built as an app. + Future get appProject async { + _cachedAppProject ??= await _readGradleProject(isLibrary: false); + return _cachedAppProject; + } -GradleProject _cachedGradleAppProject; -GradleProject _cachedGradleLibraryProject; -String _cachedGradleExecutable; + GradleProject _cachedLibraryProject; + /// Gets the [GradleProject] for the current [FlutterProject] if built as a library. + Future get libraryProject async { + _cachedLibraryProject ??= await _readGradleProject(isLibrary: true); + return _cachedLibraryProject; + } +} + +final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)'); enum FlutterPluginVersion { none, @@ -103,29 +132,20 @@ Future getGradleAppOut(AndroidProject androidProject) async { case FlutterPluginVersion.managed: // Fall through. The managed plugin matches plugin v2 for now. case FlutterPluginVersion.v2: - return fs.file((await _gradleAppProject()).apkDirectory.childFile('app.apk')); + final GradleProject gradleProject = await gradleUtils.appProject; + return fs.file(gradleProject.apkDirectory.childFile('app.apk')); } return null; } -Future _gradleAppProject() async { - _cachedGradleAppProject ??= await _readGradleProject(isLibrary: false); - return _cachedGradleAppProject; -} - -Future _gradleLibraryProject() async { - _cachedGradleLibraryProject ??= await _readGradleProject(isLibrary: true); - return _cachedGradleLibraryProject; -} - /// Runs `gradlew dependencies`, ensuring that dependencies are resolved and /// potentially downloaded. Future checkGradleDependencies() async { final Status progress = logger.startProgress('Ensuring gradle dependencies are up to date...', timeout: timeoutConfiguration.slowOperation); final FlutterProject flutterProject = FlutterProject.current(); - final String gradle = await _ensureGradle(flutterProject); + final String gradlew = await gradleUtils.getExecutable(flutterProject); await runCheckedAsync( - [gradle, 'dependencies'], + [gradlew, 'dependencies'], workingDirectory: flutterProject.android.hostAppGradleRoot.path, environment: _gradleEnv, ); @@ -189,7 +209,8 @@ void createSettingsAarGradle(Directory androidDirectory) { // of calculating the app properties using Gradle. This may take minutes. Future _readGradleProject({bool isLibrary = false}) async { final FlutterProject flutterProject = FlutterProject.current(); - final String gradle = await _ensureGradle(flutterProject); + final String gradlew = await gradleUtils.getExecutable(flutterProject); + updateLocalProperties(project: flutterProject); final FlutterManifest manifest = flutterProject.manifest; @@ -213,12 +234,12 @@ Future _readGradleProject({bool isLibrary = false}) async { // flavors and build types defined in the project. If gradle fails, then check if the failure is due to t try { final RunResult propertiesRunResult = await runCheckedAsync( - [gradle, isLibrary ? 'properties' : 'app:properties'], + [gradlew, isLibrary ? 'properties' : 'app:properties'], workingDirectory: hostAppGradleRoot.path, environment: _gradleEnv, ); final RunResult tasksRunResult = await runCheckedAsync( - [gradle, isLibrary ? 'tasks': 'app:tasks', '--all', '--console=auto'], + [gradlew, isLibrary ? 'tasks': 'app:tasks', '--all', '--console=auto'], workingDirectory: hostAppGradleRoot.path, environment: _gradleEnv, ); @@ -274,11 +295,6 @@ String _locateGradlewExecutable(Directory directory) { return null; } -Future _ensureGradle(FlutterProject project) async { - _cachedGradleExecutable ??= await _initializeGradle(project); - return _cachedGradleExecutable; -} - // Note: Gradle may be bootstrapped and possibly downloaded as a side-effect // of validating the Gradle executable. This may take several seconds. Future _initializeGradle(FlutterProject project) async { @@ -492,17 +508,15 @@ Future buildGradleProject({ // from the local.properties file. updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo); - final String gradle = await _ensureGradle(project); - switch (getFlutterPluginVersion(project.android)) { case FlutterPluginVersion.none: // Fall through. Pretend it's v1, and just go for it. case FlutterPluginVersion.v1: - return _buildGradleProjectV1(project, gradle); + return _buildGradleProjectV1(project); case FlutterPluginVersion.managed: // Fall through. Managed plugin builds the same way as plugin v2. case FlutterPluginVersion.v2: - return _buildGradleProjectV2(project, gradle, androidBuildInfo, target, isBuildingBundle); + return _buildGradleProjectV2(project, androidBuildInfo, target, isBuildingBundle); } } @@ -516,9 +530,9 @@ Future buildGradleAar({ GradleProject gradleProject; if (manifest.isModule) { - gradleProject = await _gradleAppProject(); + gradleProject = await gradleUtils.appProject; } else if (manifest.isPlugin) { - gradleProject = await _gradleLibraryProject(); + gradleProject = await gradleUtils.libraryProject; } else { throwToolExit('AARs can only be built for plugin or module projects.'); } @@ -538,12 +552,11 @@ Future buildGradleAar({ multilineOutput: true, ); - final String gradle = await _ensureGradle(project); - final String gradlePath = fs.file(gradle).absolute.path; + final String gradlew = await gradleUtils.getExecutable(project); final String flutterRoot = fs.path.absolute(Cache.flutterRoot); final String initScript = fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'aar_init_script.gradle'); final List command = [ - gradlePath, + gradlew, '-I=$initScript', '-Pflutter-root=$flutterRoot', '-Poutput-dir=${gradleProject.buildDirectory}', @@ -601,7 +614,8 @@ Future buildGradleAar({ printStatus('Built ${fs.path.relative(repoDirectory.path)}.', color: TerminalColor.green); } -Future _buildGradleProjectV1(FlutterProject project, String gradle) async { +Future _buildGradleProjectV1(FlutterProject project) async { + final String gradlew = await gradleUtils.getExecutable(project); // Run 'gradlew build'. final Status status = logger.startProgress( 'Running \'gradlew build\'...', @@ -610,7 +624,7 @@ Future _buildGradleProjectV1(FlutterProject project, String gradle) async ); final Stopwatch sw = Stopwatch()..start(); final int exitCode = await runCommandAndStreamOutput( - [fs.file(gradle).absolute.path, 'build'], + [fs.file(gradlew).absolute.path, 'build'], workingDirectory: project.android.hostAppGradleRoot.path, allowReentrantFlutter: true, environment: _gradleEnv, @@ -661,12 +675,12 @@ void printUndefinedTask(GradleProject project, BuildInfo buildInfo) { Future _buildGradleProjectV2( FlutterProject flutterProject, - String gradle, AndroidBuildInfo androidBuildInfo, String target, bool isBuildingBundle, ) async { - final GradleProject project = await _gradleAppProject(); + final String gradlew = await gradleUtils.getExecutable(flutterProject); + final GradleProject project = await gradleUtils.appProject; final BuildInfo buildInfo = androidBuildInfo.buildInfo; String assembleTask; @@ -685,8 +699,7 @@ Future _buildGradleProjectV2( timeout: timeoutConfiguration.slowOperation, multilineOutput: true, ); - final String gradlePath = fs.file(gradle).absolute.path; - final List command = [gradlePath]; + final List command = [gradlew]; if (logger.isVerbose) { command.add('-Pverbose=true'); } else { @@ -712,6 +725,8 @@ Future _buildGradleProjectV2( command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}'); if (androidBuildInfo.splitPerAbi) command.add('-Psplit-per-abi=true'); + if (androidBuildInfo.proguard) + command.add('-Pproguard=true'); if (androidBuildInfo.targetArchs.isNotEmpty) { final String targetPlatforms = androidBuildInfo.targetArchs .map(getPlatformNameForAndroidArch).join(','); @@ -727,6 +742,7 @@ Future _buildGradleProjectV2( } command.add(assembleTask); bool potentialAndroidXFailure = false; + bool potentialProguardFailure = false; final Stopwatch sw = Stopwatch()..start(); int exitCode = 1; try { @@ -743,13 +759,17 @@ Future _buildGradleProjectV2( if (!isAndroidXPluginWarning && androidXFailureRegex.hasMatch(line)) { potentialAndroidXFailure = true; } + // Proguard errors include this url. + if (!potentialProguardFailure && androidBuildInfo.proguard && + line.contains('http://proguard.sourceforge.net')) { + potentialProguardFailure = true; + } // Always print the full line in verbose mode. if (logger.isVerbose) { return line; } else if (isAndroidXPluginWarning || !ndkMessageFilter.hasMatch(line)) { return null; } - return line; }, ); @@ -758,7 +778,13 @@ Future _buildGradleProjectV2( } if (exitCode != 0) { - if (potentialAndroidXFailure) { + if (potentialProguardFailure) { + final String exclamationMark = terminal.color('[!]', TerminalColor.red); + printStatus('$exclamationMark Proguard may have failed to optimize the Java bytecode.', emphasis: true); + printStatus('To disable proguard, pass the `--no-proguard` flag to this command.', indent: 4); + printStatus('To learn more about Proguard, see: https://flutter.dev/docs/deployment/android#enabling-proguard', indent: 4); + BuildEvent('proguard-failure').send(); + } else if (potentialAndroidXFailure) { printStatus('AndroidX incompatibilities may have caused this build to fail. See https://goo.gl/CP92wY.'); BuildEvent('android-x-failure').send(); } diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 5331465e3853..cb7afb8ae575 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -92,6 +92,7 @@ class AndroidBuildInfo { AndroidArch.arm64_v8a, ], this.splitPerAbi = false, + this.proguard = false, }); // The build info containing the mode and flavor. @@ -104,6 +105,9 @@ class AndroidBuildInfo { /// will be produced. final bool splitPerAbi; + /// Whether to enable Proguard on release mode. + final bool proguard; + /// The target platforms for the build. final Iterable targetArchs; } diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart index 95b230ce09fc..6ed6b418f56e 100644 --- a/packages/flutter_tools/lib/src/commands/build_apk.dart +++ b/packages/flutter_tools/lib/src/commands/build_apk.dart @@ -25,9 +25,15 @@ class BuildApkCommand extends BuildSubCommand { argParser ..addFlag('split-per-abi', negatable: false, - help: 'Whether to split the APKs per ABIs.' + help: 'Whether to split the APKs per ABIs. ' 'To learn more, see: https://developer.android.com/studio/build/configure-apk-splits#configure-abi-split', ) + ..addFlag('proguard', + negatable: true, + defaultsTo: true, + help: 'Whether to enable Proguard on release mode. ' + 'To learn more, see: https://flutter.dev/docs/deployment/android#enabling-proguard', + ) ..addMultiOption('target-platform', splitCommas: true, defaultsTo: ['android-arm', 'android-arm64'], @@ -79,7 +85,8 @@ class BuildApkCommand extends BuildSubCommand { final BuildInfo buildInfo = getBuildInfo(); final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(buildInfo, splitPerAbi: argResults['split-per-abi'], - targetArchs: argResults['target-platform'].map(getAndroidArchForName) + targetArchs: argResults['target-platform'].map(getAndroidArchForName), + proguard: argResults['proguard'], ); if (buildInfo.isRelease && !androidBuildInfo.splitPerAbi && androidBuildInfo.targetArchs.length > 1) { diff --git a/packages/flutter_tools/lib/src/commands/build_appbundle.dart b/packages/flutter_tools/lib/src/commands/build_appbundle.dart index 7496708b0ff6..48f9eebb9239 100644 --- a/packages/flutter_tools/lib/src/commands/build_appbundle.dart +++ b/packages/flutter_tools/lib/src/commands/build_appbundle.dart @@ -22,6 +22,12 @@ class BuildAppBundleCommand extends BuildSubCommand { argParser ..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp) + ..addFlag('proguard', + negatable: true, + defaultsTo: true, + help: 'Whether to enable Proguard on release mode. ' + 'To learn more, see: https://flutter.dev/docs/deployment/android#enabling-proguard', + ) ..addMultiOption('target-platform', splitCommas: true, defaultsTo: ['android-arm', 'android-arm64'], @@ -63,7 +69,8 @@ class BuildAppBundleCommand extends BuildSubCommand { @override Future runCommand() async { final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(getBuildInfo(), - targetArchs: argResults['target-platform'].map(getAndroidArchForName) + targetArchs: argResults['target-platform'].map(getAndroidArchForName), + proguard: argResults['proguard'], ); await androidBuilder.buildAab( project: FlutterProject.current(), diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 4bb99fd39010..c761abda4c95 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'android/android_sdk.dart'; import 'android/android_studio.dart'; import 'android/android_workflow.dart'; +import 'android/gradle.dart'; import 'application_package.dart'; import 'artifacts.dart'; import 'asset.dart'; @@ -89,6 +90,7 @@ Future runInContext( FuchsiaSdk: () => FuchsiaSdk(), FuchsiaWorkflow: () => FuchsiaWorkflow(), GenSnapshot: () => const GenSnapshot(), + GradleUtils: () => GradleUtils(), HotRunnerConfig: () => HotRunnerConfig(), IMobileDevice: () => IMobileDevice(), IOSDeploy: () => const IOSDeploy(), diff --git a/packages/flutter_tools/test/general.shard/channel_test.dart b/packages/flutter_tools/test/general.shard/channel_test.dart index 64dd56097988..3e42ece363a1 100644 --- a/packages/flutter_tools/test/general.shard/channel_test.dart +++ b/packages/flutter_tools/test/general.shard/channel_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:convert'; import 'dart:io' hide File; import 'package:args/command_runner.dart'; @@ -17,21 +16,7 @@ import 'package:process/process.dart'; import '../src/common.dart'; import '../src/context.dart'; - -Process createMockProcess({ int exitCode = 0, String stdout = '', String stderr = '' }) { - final Stream> stdoutStream = Stream>.fromIterable(>[ - utf8.encode(stdout), - ]); - final Stream> stderrStream = Stream>.fromIterable(>[ - utf8.encode(stderr), - ]); - final Process process = MockProcess(); - - when(process.stdout).thenAnswer((_) => stdoutStream); - when(process.stderr).thenAnswer((_) => stderrStream); - when(process.exitCode).thenAnswer((_) => Future.value(exitCode)); - return process; -} +import '../src/mocks.dart'; void main() { group('channel', () { diff --git a/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart b/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart index b8b35726c40e..68b9bc2c552a 100644 --- a/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart @@ -2,15 +2,24 @@ // 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:args/command_runner.dart'; import 'package:flutter_tools/src/android/android_builder.dart'; +import 'package:flutter_tools/src/android/android_sdk.dart'; +import 'package:flutter_tools/src/android/gradle.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/build_apk.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/mocks.dart'; void main() { Cache.disableLocking(); @@ -26,21 +35,10 @@ void main() { tryToDelete(tempDir); }); - Future runCommandIn(String target, { List arguments }) async { - final BuildApkCommand command = BuildApkCommand(); - final CommandRunner runner = createTestCommandRunner(command); - await runner.run([ - 'apk', - ...?arguments, - fs.path.join(target, 'lib', 'main.dart'), - ]); - return command; - } - testUsingContext('indicate the default target platforms', () async { final String projectPath = await createProject(tempDir, arguments: ['--no-pub', '--template=app']); - final BuildApkCommand command = await runCommandIn(projectPath); + final BuildApkCommand command = await runBuildApkCommand(projectPath); expect(await command.usageValues, containsPair(CustomDimensions.commandBuildApkTargetPlatform, 'android-arm,android-arm64')); @@ -53,12 +51,12 @@ void main() { final String projectPath = await createProject(tempDir, arguments: ['--no-pub', '--template=app']); - final BuildApkCommand commandWithFlag = await runCommandIn(projectPath, + final BuildApkCommand commandWithFlag = await runBuildApkCommand(projectPath, arguments: ['--split-per-abi']); expect(await commandWithFlag.usageValues, containsPair(CustomDimensions.commandBuildApkSplitPerAbi, 'true')); - final BuildApkCommand commandWithoutFlag = await runCommandIn(projectPath); + final BuildApkCommand commandWithoutFlag = await runBuildApkCommand(projectPath); expect(await commandWithoutFlag.usageValues, containsPair(CustomDimensions.commandBuildApkSplitPerAbi, 'false')); @@ -70,21 +68,21 @@ void main() { final String projectPath = await createProject(tempDir, arguments: ['--no-pub', '--template=app']); - final BuildApkCommand commandDefault = await runCommandIn(projectPath); + final BuildApkCommand commandDefault = await runBuildApkCommand(projectPath); expect(await commandDefault.usageValues, containsPair(CustomDimensions.commandBuildApkBuildMode, 'release')); - final BuildApkCommand commandInRelease = await runCommandIn(projectPath, + final BuildApkCommand commandInRelease = await runBuildApkCommand(projectPath, arguments: ['--release']); expect(await commandInRelease.usageValues, containsPair(CustomDimensions.commandBuildApkBuildMode, 'release')); - final BuildApkCommand commandInDebug = await runCommandIn(projectPath, + final BuildApkCommand commandInDebug = await runBuildApkCommand(projectPath, arguments: ['--debug']); expect(await commandInDebug.usageValues, containsPair(CustomDimensions.commandBuildApkBuildMode, 'debug')); - final BuildApkCommand commandInProfile = await runCommandIn(projectPath, + final BuildApkCommand commandInProfile = await runBuildApkCommand(projectPath, arguments: ['--profile']); expect(await commandInProfile.usageValues, containsPair(CustomDimensions.commandBuildApkBuildMode, 'profile')); @@ -93,4 +91,201 @@ void main() { AndroidBuilder: () => FakeAndroidBuilder(), }, timeout: allowForCreateFlutterProject); }); + + group('Gradle', () { + Directory tempDir; + ProcessManager mockProcessManager; + String gradlew; + AndroidSdk mockAndroidSdk; + Usage mockUsage; + + setUp(() { + mockUsage = MockUsage(); + when(mockUsage.isFirstRun).thenReturn(true); + + tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.'); + gradlew = fs.path.join(tempDir.path, 'flutter_project', 'android', + platform.isWindows ? 'gradlew.bat' : 'gradlew'); + + mockProcessManager = MockProcessManager(); + when(mockProcessManager.run([gradlew, '-v'], + environment: anyNamed('environment'))) + .thenAnswer((_) => Future.value(ProcessResult(0, 0, '', ''))); + + when(mockProcessManager.run([gradlew, 'app:properties'], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'))) + .thenAnswer((_) => Future.value(ProcessResult(0, 0, 'buildDir: irrelevant', ''))); + + when(mockProcessManager.run([gradlew, 'app:tasks', '--all', '--console=auto'], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'))) + .thenAnswer((_) => Future.value(ProcessResult(0, 0, 'assembleRelease', ''))); + // Fallback with error. + final Process process = createMockProcess(exitCode: 1); + when(mockProcessManager.start(any, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'))) + .thenAnswer((_) => Future.value(process)); + when(mockProcessManager.canRun(any)).thenReturn(false); + + mockAndroidSdk = MockAndroidSdk(); + when(mockAndroidSdk.directory).thenReturn('irrelevant'); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + testUsingContext('proguard is enabled by default on release mode', () async { + final String projectPath = await createProject(tempDir, + arguments: ['--no-pub', '--template=app']); + + await expectLater(() async { + await runBuildApkCommand(projectPath); + }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); + + verify(mockProcessManager.start( + [ + gradlew, + '-q', + '-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}', + '-Ptrack-widget-creation=false', + '-Pproguard=true', + '-Ptarget-platform=android-arm,android-arm64', + 'assembleRelease', + ], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).called(1); + }, + overrides: { + AndroidSdk: () => mockAndroidSdk, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + GradleUtils: () => GradleUtils(), + ProcessManager: () => mockProcessManager, + }, + timeout: allowForCreateFlutterProject); + + testUsingContext('proguard is disabled when --no-proguard is passed', () async { + final String projectPath = await createProject(tempDir, + arguments: ['--no-pub', '--template=app']); + + await expectLater(() async { + await runBuildApkCommand( + projectPath, + arguments: ['--no-proguard'], + ); + }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); + + verify(mockProcessManager.start( + [ + gradlew, + '-q', + '-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}', + '-Ptrack-widget-creation=false', + '-Ptarget-platform=android-arm,android-arm64', + 'assembleRelease', + ], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).called(1); + }, + overrides: { + AndroidSdk: () => mockAndroidSdk, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + GradleUtils: () => GradleUtils(), + ProcessManager: () => mockProcessManager, + }, + timeout: allowForCreateFlutterProject); + + testUsingContext('guides the user when proguard fails', () async { + final String projectPath = await createProject(tempDir, + arguments: ['--no-pub', '--template=app']); + + when(mockProcessManager.start( + [ + gradlew, + '-q', + '-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}', + '-Ptrack-widget-creation=false', + '-Pproguard=true', + '-Ptarget-platform=android-arm,android-arm64', + 'assembleRelease', + ], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).thenAnswer((_) { + const String proguardStdoutWarning = + 'Warning: there were 6 unresolved references to program class members.' + 'Your input classes appear to be inconsistent.' + 'You may need to recompile the code.' + '(http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedprogramclassmember)'; + return Future.value( + createMockProcess( + exitCode: 1, + stdout: proguardStdoutWarning, + ) + ); + }); + + await expectLater(() async { + await runBuildApkCommand( + projectPath, + ); + }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); + + expect(testLogger.statusText, + contains('Proguard may have failed to optimize the Java bytecode.')); + expect(testLogger.statusText, + contains('To disable proguard, pass the `--no-proguard` flag to this command.')); + expect(testLogger.statusText, + contains('To learn more about Proguard, see: https://flutter.dev/docs/deployment/android#enabling-proguard')); + + verify(mockUsage.sendEvent( + 'build-apk', + 'proguard-failure', + parameters: anyNamed('parameters'), + )).called(1); + }, + overrides: { + AndroidSdk: () => mockAndroidSdk, + GradleUtils: () => GradleUtils(), + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + ProcessManager: () => mockProcessManager, + Usage: () => mockUsage, + }, + timeout: allowForCreateFlutterProject); + }); +} + +Future runBuildApkCommand( + String target, + { List arguments } +) async { + final BuildApkCommand command = BuildApkCommand(); + final CommandRunner runner = createTestCommandRunner(command); + await runner.run([ + 'apk', + ...?arguments, + fs.path.join(target, 'lib', 'main.dart'), + ]); + return command; } + +class FakeFlutterProjectFactory extends FlutterProjectFactory { + FakeFlutterProjectFactory(this.directoryOverride) : + assert(directoryOverride != null); + + final Directory directoryOverride; + + @override + FlutterProject fromDirectory(Directory _) { + return super.fromDirectory(directoryOverride.childDirectory('flutter_project')); + } +} + +class MockAndroidSdk extends Mock implements AndroidSdk {} +class MockProcessManager extends Mock implements ProcessManager {} +class MockProcess extends Mock implements Process {} +class MockUsage extends Mock implements Usage {} diff --git a/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart b/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart index 31fb604ffc78..7a0f02ae8e8a 100644 --- a/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart @@ -2,15 +2,24 @@ // 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:args/command_runner.dart'; import 'package:flutter_tools/src/android/android_builder.dart'; +import 'package:flutter_tools/src/android/android_sdk.dart'; +import 'package:flutter_tools/src/android/gradle.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/build_appbundle.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/mocks.dart'; void main() { Cache.disableLocking(); @@ -26,21 +35,10 @@ void main() { tryToDelete(tempDir); }); - Future runCommandIn(String target, { List arguments }) async { - final BuildAppBundleCommand command = BuildAppBundleCommand(); - final CommandRunner runner = createTestCommandRunner(command); - await runner.run([ - 'appbundle', - ...?arguments, - fs.path.join(target, 'lib', 'main.dart'), - ]); - return command; - } - testUsingContext('indicate the default target platforms', () async { final String projectPath = await createProject(tempDir, arguments: ['--no-pub', '--template=app']); - final BuildAppBundleCommand command = await runCommandIn(projectPath); + final BuildAppBundleCommand command = await runBuildAppBundleCommand(projectPath); expect(await command.usageValues, containsPair(CustomDimensions.commandBuildAppBundleTargetPlatform, 'android-arm,android-arm64')); @@ -53,21 +51,21 @@ void main() { final String projectPath = await createProject(tempDir, arguments: ['--no-pub', '--template=app']); - final BuildAppBundleCommand commandDefault = await runCommandIn(projectPath); + final BuildAppBundleCommand commandDefault = await runBuildAppBundleCommand(projectPath); expect(await commandDefault.usageValues, containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'release')); - final BuildAppBundleCommand commandInRelease = await runCommandIn(projectPath, + final BuildAppBundleCommand commandInRelease = await runBuildAppBundleCommand(projectPath, arguments: ['--release']); expect(await commandInRelease.usageValues, containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'release')); - final BuildAppBundleCommand commandInDebug = await runCommandIn(projectPath, + final BuildAppBundleCommand commandInDebug = await runBuildAppBundleCommand(projectPath, arguments: ['--debug']); expect(await commandInDebug.usageValues, containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'debug')); - final BuildAppBundleCommand commandInProfile = await runCommandIn(projectPath, + final BuildAppBundleCommand commandInProfile = await runBuildAppBundleCommand(projectPath, arguments: ['--profile']); expect(await commandInProfile.usageValues, containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'profile')); @@ -76,4 +74,207 @@ void main() { AndroidBuilder: () => FakeAndroidBuilder(), }, timeout: allowForCreateFlutterProject); }); + + group('Flags', () { + Directory tempDir; + ProcessManager mockProcessManager; + MockAndroidSdk mockAndroidSdk; + String gradlew; + Usage mockUsage; + + + setUp(() { + mockUsage = MockUsage(); + when(mockUsage.isFirstRun).thenReturn(true); + + tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.'); + gradlew = fs.path.join(tempDir.path, 'flutter_project', 'android', + platform.isWindows ? 'gradlew.bat' : 'gradlew'); + + mockProcessManager = MockProcessManager(); + when(mockProcessManager.run([gradlew, '-v'], + environment: anyNamed('environment'))) + .thenAnswer((_) => Future.value(ProcessResult(0, 0, '', ''))); + + when(mockProcessManager.run([gradlew, 'app:properties'], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'))) + .thenAnswer((_) => Future.value(ProcessResult(0, 0, 'buildDir: irrelevant', ''))); + + when(mockProcessManager.run([gradlew, 'app:tasks', '--all', '--console=auto'], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'))) + .thenAnswer((_) => Future.value(ProcessResult(0, 0, 'assembleRelease', ''))); + // Fallback with error. + final Process process = createMockProcess(exitCode: 1); + when(mockProcessManager.start(any, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'))) + .thenAnswer((_) => Future.value(process)); + when(mockProcessManager.canRun(any)).thenReturn(false); + + mockAndroidSdk = MockAndroidSdk(); + when(mockAndroidSdk.validateSdkWellFormed()).thenReturn(const []); + when(mockAndroidSdk.directory).thenReturn('irrelevant'); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + testUsingContext('proguard is enabled by default on release mode', () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=app'], + ); + + await expectLater(() async { + await runBuildAppBundleCommand(projectPath); + }, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1')); + + verify(mockProcessManager.start( + [ + gradlew, + '-q', + '-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}', + '-Ptrack-widget-creation=false', + '-Pproguard=true', + '-Ptarget-platform=android-arm,android-arm64', + 'bundleRelease', + ], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).called(1); + }, + overrides: { + AndroidSdk: () => mockAndroidSdk, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + GradleUtils: () => GradleUtils(), + ProcessManager: () => mockProcessManager, + }, + timeout: allowForCreateFlutterProject); + + testUsingContext('proguard is disabled when --no-proguard is passed', () async { + final String projectPath = await createProject( + tempDir, + arguments: ['--no-pub', '--template=app'], + ); + + await expectLater(() async { + await runBuildAppBundleCommand( + projectPath, + arguments: ['--no-proguard'], + ); + }, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1')); + + verify(mockProcessManager.start( + [ + gradlew, + '-q', + '-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}', + '-Ptrack-widget-creation=false', + '-Ptarget-platform=android-arm,android-arm64', + 'bundleRelease', + ], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).called(1); + }, + overrides: { + AndroidSdk: () => mockAndroidSdk, + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + GradleUtils: () => GradleUtils(), + ProcessManager: () => mockProcessManager, + }, + timeout: allowForCreateFlutterProject); + + testUsingContext('guides the user when proguard fails', () async { + final String projectPath = await createProject(tempDir, + arguments: ['--no-pub', '--template=app']); + + when(mockProcessManager.start( + [ + gradlew, + '-q', + '-Ptarget=${fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}', + '-Ptrack-widget-creation=false', + '-Pproguard=true', + '-Ptarget-platform=android-arm,android-arm64', + 'bundleRelease', + ], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).thenAnswer((_) { + const String proguardStdoutWarning = + 'Warning: there were 6 unresolved references to program class members.' + 'Your input classes appear to be inconsistent.' + 'You may need to recompile the code.' + '(http://proguard.sourceforge.net/manual/troubleshooting.html#unresolvedprogramclassmember)'; + return Future.value( + createMockProcess( + exitCode: 1, + stdout: proguardStdoutWarning, + ) + ); + }); + + await expectLater(() async { + await runBuildAppBundleCommand( + projectPath, + ); + }, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1')); + + expect(testLogger.statusText, + contains('Proguard may have failed to optimize the Java bytecode.')); + expect(testLogger.statusText, + contains('To disable proguard, pass the `--no-proguard` flag to this command.')); + expect(testLogger.statusText, + contains('To learn more about Proguard, see: https://flutter.dev/docs/deployment/android#enabling-proguard')); + + verify(mockUsage.sendEvent( + 'build-appbundle', + 'proguard-failure', + parameters: anyNamed('parameters'), + )).called(1); + }, + overrides: { + AndroidSdk: () => mockAndroidSdk, + GradleUtils: () => GradleUtils(), + FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), + ProcessManager: () => mockProcessManager, + Usage: () => mockUsage, + }, + timeout: allowForCreateFlutterProject); + }); } + +Future runBuildAppBundleCommand( + String target, + { List arguments } +) async { + final BuildAppBundleCommand command = BuildAppBundleCommand(); + final CommandRunner runner = createTestCommandRunner(command); + await runner.run([ + 'appbundle', + ...?arguments, + fs.path.join(target, 'lib', 'main.dart'), + ]); + return command; +} + +class FakeFlutterProjectFactory extends FlutterProjectFactory { + FakeFlutterProjectFactory(this._directoryOverride) : + assert(_directoryOverride != null); + + final Directory _directoryOverride; + + @override + FlutterProject fromDirectory(Directory _) { + return super.fromDirectory(_directoryOverride.childDirectory('flutter_project')); + } +} + +class MockAndroidSdk extends Mock implements AndroidSdk {} +class MockProcessManager extends Mock implements ProcessManager {} +class MockProcess extends Mock implements Process {} +class MockUsage extends Mock implements Usage {} diff --git a/packages/flutter_tools/test/general.shard/commands/upgrade_test.dart b/packages/flutter_tools/test/general.shard/commands/upgrade_test.dart index 499697dc7bc2..94cefa6b1300 100644 --- a/packages/flutter_tools/test/general.shard/commands/upgrade_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/upgrade_test.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:convert'; - import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; @@ -17,21 +15,7 @@ import 'package:process/process.dart'; import '../../src/common.dart'; import '../../src/context.dart'; - -Process createMockProcess({ int exitCode = 0, String stdout = '', String stderr = '' }) { - final Stream> stdoutStream = Stream>.fromIterable(>[ - utf8.encode(stdout), - ]); - final Stream> stderrStream = Stream>.fromIterable(>[ - utf8.encode(stderr), - ]); - final Process process = MockProcess(); - - when(process.stdout).thenAnswer((_) => stdoutStream); - when(process.stderr).thenAnswer((_) => stderrStream); - when(process.exitCode).thenAnswer((_) => Future.value(exitCode)); - return process; -} +import '../../src/mocks.dart'; void main() { group('UpgradeCommandRunner', () { diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart index ff4e549cd4ae..686015427158 100644 --- a/packages/flutter_tools/test/src/common.dart +++ b/packages/flutter_tools/test/src/common.dart @@ -116,6 +116,8 @@ Future createProject(Directory temp, { List arguments }) async { final CreateCommand command = CreateCommand(); final CommandRunner runner = createTestCommandRunner(command); await runner.run(['create', ...arguments, projectPath]); + // Created `.packages` since it's not created when the flag `--no-pub` is passed. + fs.file(fs.path.join(projectPath, '.packages')).createSync(); return projectPath; } diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index d7564efd8435..adea05c14468 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -221,6 +221,24 @@ ProcessFactory flakyProcessFactory({ }; } +/// Creates a mock process that returns with the given [exitCode], [stdout] and [stderr]. +Process createMockProcess({ int exitCode = 0, String stdout = '', String stderr = '' }) { + final Stream> stdoutStream = Stream>.fromIterable(>[ + utf8.encode(stdout), + ]); + final Stream> stderrStream = Stream>.fromIterable(>[ + utf8.encode(stderr), + ]); + final Process process = MockBasicProcess(); + + when(process.stdout).thenAnswer((_) => stdoutStream); + when(process.stderr).thenAnswer((_) => stderrStream); + when(process.exitCode).thenAnswer((_) => Future.value(exitCode)); + return process; +} + +class MockBasicProcess extends Mock implements Process {} + /// A process that exits successfully with no output and ignores all input. class MockProcess extends Mock implements Process { MockProcess({