Unverified Commit f098de1f authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Enable Proguard by default on release mode (#39986)

parent 362cde43
......@@ -132,16 +132,6 @@ class FlutterPlugin implements Plugin<Project> {
}
}
// 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<Project> {
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<Project> {
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'
}
......
# 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 <plugin-package> 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.**
......@@ -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<GradleUtils>();
/// 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<String> 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<GradleProject> 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<GradleProject> 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<File> 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<GradleProject> _gradleAppProject() async {
_cachedGradleAppProject ??= await _readGradleProject(isLibrary: false);
return _cachedGradleAppProject;
}
Future<GradleProject> _gradleLibraryProject() async {
_cachedGradleLibraryProject ??= await _readGradleProject(isLibrary: true);
return _cachedGradleLibraryProject;
}
/// Runs `gradlew dependencies`, ensuring that dependencies are resolved and
/// potentially downloaded.
Future<void> 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(
<String>[gradle, 'dependencies'],
<String>[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<GradleProject> _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<GradleProject> _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(
<String>[gradle, isLibrary ? 'properties' : 'app:properties'],
<String>[gradlew, isLibrary ? 'properties' : 'app:properties'],
workingDirectory: hostAppGradleRoot.path,
environment: _gradleEnv,
);
final RunResult tasksRunResult = await runCheckedAsync(
<String>[gradle, isLibrary ? 'tasks': 'app:tasks', '--all', '--console=auto'],
<String>[gradlew, isLibrary ? 'tasks': 'app:tasks', '--all', '--console=auto'],
workingDirectory: hostAppGradleRoot.path,
environment: _gradleEnv,
);
......@@ -274,11 +295,6 @@ String _locateGradlewExecutable(Directory directory) {
return null;
}
Future<String> _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<String> _initializeGradle(FlutterProject project) async {
......@@ -492,17 +508,15 @@ Future<void> 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<void> 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<void> 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<String> command = <String>[
gradlePath,
gradlew,
'-I=$initScript',
'-Pflutter-root=$flutterRoot',
'-Poutput-dir=${gradleProject.buildDirectory}',
......@@ -601,7 +614,8 @@ Future<void> buildGradleAar({
printStatus('Built ${fs.path.relative(repoDirectory.path)}.', color: TerminalColor.green);
}
Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async {
Future<void> _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<void> _buildGradleProjectV1(FlutterProject project, String gradle) async
);
final Stopwatch sw = Stopwatch()..start();
final int exitCode = await runCommandAndStreamOutput(
<String>[fs.file(gradle).absolute.path, 'build'],
<String>[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<void> _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<void> _buildGradleProjectV2(
timeout: timeoutConfiguration.slowOperation,
multilineOutput: true,
);
final String gradlePath = fs.file(gradle).absolute.path;
final List<String> command = <String>[gradlePath];
final List<String> command = <String>[gradlew];
if (logger.isVerbose) {
command.add('-Pverbose=true');
} else {
......@@ -712,6 +725,8 @@ Future<void> _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<void> _buildGradleProjectV2(
}
command.add(assembleTask);
bool potentialAndroidXFailure = false;
bool potentialProguardFailure = false;
final Stopwatch sw = Stopwatch()..start();
int exitCode = 1;
try {
......@@ -743,13 +759,17 @@ Future<void> _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<void> _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();
}
......
......@@ -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<AndroidArch> targetArchs;
}
......
......@@ -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: <String>['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<AndroidArch>(getAndroidArchForName)
targetArchs: argResults['target-platform'].map<AndroidArch>(getAndroidArchForName),
proguard: argResults['proguard'],
);
if (buildInfo.isRelease && !androidBuildInfo.splitPerAbi && androidBuildInfo.targetArchs.length > 1) {
......
......@@ -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: <String>['android-arm', 'android-arm64'],
......@@ -63,7 +69,8 @@ class BuildAppBundleCommand extends BuildSubCommand {
@override
Future<FlutterCommandResult> runCommand() async {
final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(getBuildInfo(),
targetArchs: argResults['target-platform'].map<AndroidArch>(getAndroidArchForName)
targetArchs: argResults['target-platform'].map<AndroidArch>(getAndroidArchForName),
proguard: argResults['proguard'],
);
await androidBuilder.buildAab(
project: FlutterProject.current(),
......
......@@ -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<T> runInContext<T>(
FuchsiaSdk: () => FuchsiaSdk(),
FuchsiaWorkflow: () => FuchsiaWorkflow(),
GenSnapshot: () => const GenSnapshot(),
GradleUtils: () => GradleUtils(),
HotRunnerConfig: () => HotRunnerConfig(),
IMobileDevice: () => IMobileDevice(),
IOSDeploy: () => const IOSDeploy(),
......
......@@ -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<List<int>> stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[
utf8.encode(stdout),
]);
final Stream<List<int>> stderrStream = Stream<List<int>>.fromIterable(<List<int>>[
utf8.encode(stderr),
]);
final Process process = MockProcess();
when(process.stdout).thenAnswer((_) => stdoutStream);
when(process.stderr).thenAnswer((_) => stderrStream);
when(process.exitCode).thenAnswer((_) => Future<int>.value(exitCode));
return process;
}
import '../src/mocks.dart';
void main() {
group('channel', () {
......
......@@ -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<BuildApkCommand> runCommandIn(String target, { List<String> arguments }) async {
final BuildApkCommand command = BuildApkCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'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: <String>['--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: <String>['--no-pub', '--template=app']);
final BuildApkCommand commandWithFlag = await runCommandIn(projectPath,
final BuildApkCommand commandWithFlag = await runBuildApkCommand(projectPath,
arguments: <String>['--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: <String>['--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: <String>['--release']);
expect(await commandInRelease.usageValues,
containsPair(CustomDimensions.commandBuildApkBuildMode, 'release'));
final BuildApkCommand commandInDebug = await runCommandIn(projectPath,
final BuildApkCommand commandInDebug = await runBuildApkCommand(projectPath,
arguments: <String>['--debug']);
expect(await commandInDebug.usageValues,
containsPair(CustomDimensions.commandBuildApkBuildMode, 'debug'));
final BuildApkCommand commandInProfile = await runCommandIn(projectPath,
final BuildApkCommand commandInProfile = await runBuildApkCommand(projectPath,
arguments: <String>['--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(<String>[gradlew, '-v'],
environment: anyNamed('environment')))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, '', '')));
when(mockProcessManager.run(<String>[gradlew, 'app:properties'],
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment')))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, 'buildDir: irrelevant', '')));
when(mockProcessManager.run(<String>[gradlew, 'app:tasks', '--all', '--console=auto'],
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment')))
.thenAnswer((_) => Future<ProcessResult>.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<Process>.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: <String>['--no-pub', '--template=app']);
await expectLater(() async {
await runBuildApkCommand(projectPath);
}, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
verify(mockProcessManager.start(
<String>[
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: <Type, Generator>{
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: <String>['--no-pub', '--template=app']);
await expectLater(() async {
await runBuildApkCommand(
projectPath,
arguments: <String>['--no-proguard'],
);
}, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
verify(mockProcessManager.start(
<String>[
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: <Type, Generator>{
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: <String>['--no-pub', '--template=app']);
when(mockProcessManager.start(
<String>[
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<Process>.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: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
},
timeout: allowForCreateFlutterProject);
});
}
Future<BuildApkCommand> runBuildApkCommand(
String target,
{ List<String> arguments }
) async {
final BuildApkCommand command = BuildApkCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'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 {}
......@@ -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<BuildAppBundleCommand> runCommandIn(String target, { List<String> arguments }) async {
final BuildAppBundleCommand command = BuildAppBundleCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'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: <String>['--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: <String>['--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: <String>['--release']);
expect(await commandInRelease.usageValues,
containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'release'));
final BuildAppBundleCommand commandInDebug = await runCommandIn(projectPath,
final BuildAppBundleCommand commandInDebug = await runBuildAppBundleCommand(projectPath,
arguments: <String>['--debug']);
expect(await commandInDebug.usageValues,
containsPair(CustomDimensions.commandBuildAppBundleBuildMode, 'debug'));
final BuildAppBundleCommand commandInProfile = await runCommandIn(projectPath,
final BuildAppBundleCommand commandInProfile = await runBuildAppBundleCommand(projectPath,
arguments: <String>['--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(<String>[gradlew, '-v'],
environment: anyNamed('environment')))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, '', '')));
when(mockProcessManager.run(<String>[gradlew, 'app:properties'],
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment')))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, 'buildDir: irrelevant', '')));
when(mockProcessManager.run(<String>[gradlew, 'app:tasks', '--all', '--console=auto'],
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment')))
.thenAnswer((_) => Future<ProcessResult>.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<Process>.value(process));
when(mockProcessManager.canRun(any)).thenReturn(false);
mockAndroidSdk = MockAndroidSdk();
when(mockAndroidSdk.validateSdkWellFormed()).thenReturn(const <String>[]);
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: <String>['--no-pub', '--template=app'],
);
await expectLater(() async {
await runBuildAppBundleCommand(projectPath);
}, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'));
verify(mockProcessManager.start(
<String>[
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: <Type, Generator>{
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: <String>['--no-pub', '--template=app'],
);
await expectLater(() async {
await runBuildAppBundleCommand(
projectPath,
arguments: <String>['--no-proguard'],
);
}, throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'));
verify(mockProcessManager.start(
<String>[
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: <Type, Generator>{
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: <String>['--no-pub', '--template=app']);
when(mockProcessManager.start(
<String>[
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<Process>.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: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
},
timeout: allowForCreateFlutterProject);
});
}
Future<BuildAppBundleCommand> runBuildAppBundleCommand(
String target,
{ List<String> arguments }
) async {
final BuildAppBundleCommand command = BuildAppBundleCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'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 {}
......@@ -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<List<int>> stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[
utf8.encode(stdout),
]);
final Stream<List<int>> stderrStream = Stream<List<int>>.fromIterable(<List<int>>[
utf8.encode(stderr),
]);
final Process process = MockProcess();
when(process.stdout).thenAnswer((_) => stdoutStream);
when(process.stderr).thenAnswer((_) => stderrStream);
when(process.exitCode).thenAnswer((_) => Future<int>.value(exitCode));
return process;
}
import '../../src/mocks.dart';
void main() {
group('UpgradeCommandRunner', () {
......
......@@ -116,6 +116,8 @@ Future<String> createProject(Directory temp, { List<String> arguments }) async {
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['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;
}
......
......@@ -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<List<int>> stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[
utf8.encode(stdout),
]);
final Stream<List<int>> stderrStream = Stream<List<int>>.fromIterable(<List<int>>[
utf8.encode(stderr),
]);
final Process process = MockBasicProcess();
when(process.stdout).thenAnswer((_) => stdoutStream);
when(process.stderr).thenAnswer((_) => stderrStream);
when(process.exitCode).thenAnswer((_) => Future<int>.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({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment