Add split-debug and obfuscation to build aar (#56342)
diff --git a/dev/devicelab/bin/tasks/android_obfuscate_test.dart b/dev/devicelab/bin/tasks/android_obfuscate_test.dart
index f1d4bcf..b087ba1 100644
--- a/dev/devicelab/bin/tasks/android_obfuscate_test.dart
+++ b/dev/devicelab/bin/tasks/android_obfuscate_test.dart
@@ -12,7 +12,7 @@
Future<void> main() async {
await task(() async {
try {
- bool foundProjectName = false;
+ bool foundApkProjectName = false;
await runProjectTest((FlutterProject flutterProject) async {
section('APK content for task assembleRelease with --obfuscate');
await inDirectory(flutterProject.rootPath, () async {
@@ -21,13 +21,14 @@
'--target-platform=android-arm',
'--obfuscate',
'--split-debug-info=foo/',
+ '--verbose',
]);
});
- final String outputDirectory = path.join(
+ final String outputApkDirectory = path.join(
flutterProject.rootPath,
'build/app/outputs/apk/release/app-release.apk',
);
- final Iterable<String> apkFiles = await getFilesInApk(outputDirectory);
+ final Iterable<String> apkFiles = await getFilesInApk(outputApkDirectory);
checkCollectionContains<String>(<String>[
...flutterAssets,
@@ -38,20 +39,69 @@
// Verify that an identifier from the Dart project code is not present
// in the compiled binary.
await inDirectory(flutterProject.rootPath, () async {
- await exec('unzip', <String>[outputDirectory]);
+ await exec('unzip', <String>[outputApkDirectory]);
+ checkFileExists(path.join(flutterProject.rootPath, 'lib/armeabi-v7a/libapp.so'));
final String response = await eval(
'grep',
<String>[flutterProject.name, 'lib/armeabi-v7a/libapp.so'],
canFail: true,
);
if (response.trim().contains('matches')) {
- foundProjectName = true;
+ foundApkProjectName = true;
}
});
});
- if (foundProjectName) {
- return TaskResult.failure('Found project name in obfuscated dart library');
+
+ bool foundAarProjectName = false;
+ await runModuleProjectTest((FlutterModuleProject flutterProject) async {
+ section('AAR content with --obfuscate');
+
+ await inDirectory(flutterProject.rootPath, () async {
+ await flutter('build', options: <String>[
+ 'aar',
+ '--target-platform=android-arm',
+ '--obfuscate',
+ '--split-debug-info=foo/',
+ '--no-debug',
+ '--no-profile',
+ '--verbose',
+ ]);
+ });
+
+ final String outputAarDirectory = path.join(
+ flutterProject.rootPath,
+ 'build/host/outputs/repo/com/example/${flutterProject.name}/flutter_release/1.0/flutter_release-1.0.aar',
+ );
+ final Iterable<String> aarFiles = await getFilesInAar(outputAarDirectory);
+
+ checkCollectionContains<String>(<String>[
+ ...flutterAssets,
+ 'jni/armeabi-v7a/libapp.so',
+ ], aarFiles);
+
+ // Verify that an identifier from the Dart project code is not present
+ // in the compiled binary.
+ await inDirectory(flutterProject.rootPath, () async {
+ await exec('unzip', <String>[outputAarDirectory]);
+ checkFileExists(path.join(flutterProject.rootPath, 'jni/armeabi-v7a/libapp.so'));
+ final String response = await eval(
+ 'grep',
+ <String>[flutterProject.name, 'jni/armeabi-v7a/libapp.so'],
+ canFail: true,
+ );
+ if (response.trim().contains('matches')) {
+ foundAarProjectName = true;
+ }
+ });
+ });
+
+ if (foundApkProjectName) {
+ return TaskResult.failure('Found project name in obfuscated APK dart library');
}
+ if (foundAarProjectName) {
+ return TaskResult.failure('Found project name in obfuscated AAR dart library');
+ }
+
return TaskResult.success(null);
} on TaskResult catch (taskResult) {
return taskResult;
diff --git a/dev/devicelab/lib/framework/apk_utils.dart b/dev/devicelab/lib/framework/apk_utils.dart
index c6623f3..fc9b5ab 100644
--- a/dev/devicelab/lib/framework/apk_utils.dart
+++ b/dev/devicelab/lib/framework/apk_utils.dart
@@ -51,6 +51,18 @@
}
}
+/// Runs the given [testFunction] on a freshly generated Flutter module project.
+Future<void> runModuleProjectTest(Future<void> testFunction(FlutterModuleProject moduleProject)) async {
+ final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_module_test.');
+ final FlutterModuleProject moduleProject = await FlutterModuleProject.create(tempDir, 'hello_module');
+
+ try {
+ await testFunction(moduleProject);
+ } finally {
+ rmTree(tempDir);
+ }
+}
+
/// Returns the list of files inside an Android Package Kit.
Future<Iterable<String>> getFilesInApk(String apk) async {
if (!File(apk).existsSync()) {
@@ -357,6 +369,22 @@
}
}
+class FlutterModuleProject {
+ FlutterModuleProject(this.parent, this.name);
+
+ final Directory parent;
+ final String name;
+
+ static Future<FlutterModuleProject> create(Directory directory, String name) async {
+ await inDirectory(directory, () async {
+ await flutter('create', options: <String>['--template=module', name]);
+ });
+ return FlutterModuleProject(directory, name);
+ }
+
+ String get rootPath => path.join(parent.path, name);
+}
+
Future<void> _runGradleTask({String workingDirectory, String task, List<String> options}) async {
final ProcessResult result = await _resultOfGradleTask(
workingDirectory: workingDirectory,
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index b491d3a..09a517d 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -525,7 +525,8 @@
throwToolExit('AARs can only be built for plugin or module projects.');
}
- final String aarTask = getAarTaskFor(androidBuildInfo.buildInfo);
+ final BuildInfo buildInfo = androidBuildInfo.buildInfo;
+ final String aarTask = getAarTaskFor(buildInfo);
final Status status = globals.logger.startProgress(
"Running Gradle task '$aarTask'...",
timeout: timeoutConfiguration.slowOperation,
@@ -548,10 +549,28 @@
'-Pis-plugin=${manifest.isPlugin}',
'-PbuildNumber=$buildNumber'
];
+ if (globals.logger.isVerbose) {
+ command.add('-Pverbose=true');
+ } else {
+ command.add('-q');
+ }
if (target != null && target.isNotEmpty) {
command.add('-Ptarget=$target');
}
+ if (buildInfo.splitDebugInfoPath != null) {
+ command.add('-Psplit-debug-info=${buildInfo.splitDebugInfoPath}');
+ }
+ if (buildInfo.treeShakeIcons) {
+ command.add('-Pfont-subset=true');
+ }
+ if (buildInfo.dartObfuscation) {
+ if (buildInfo.mode == BuildMode.debug || buildInfo.mode == BuildMode.profile) {
+ globals.printStatus('Dart obfuscation is not supported in ${toTitleCase(buildInfo.friendlyModeName)} mode, building as unobfuscated.');
+ } else {
+ command.add('-Pdart-obfuscation=true');
+ }
+ }
if (globals.artifacts is LocalEngineArtifacts) {
final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts;
@@ -564,11 +583,8 @@
'Local Maven repo: ${localEngineRepo.path}'
);
command.add('-Plocal-engine-repo=${localEngineRepo.path}');
- command.add('-Plocal-engine-build-mode=${androidBuildInfo.buildInfo.modeName}');
+ command.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
command.add('-Plocal-engine-out=${localEngineArtifacts.engineOutPath}');
- if (androidBuildInfo.buildInfo.treeShakeIcons) {
- command.add('-Pfont-subset=true');
- }
// Copy the local engine repo in the output directory.
try {
diff --git a/packages/flutter_tools/lib/src/commands/build_aar.dart b/packages/flutter_tools/lib/src/commands/build_aar.dart
index 9c7551a..f74a2b2 100644
--- a/packages/flutter_tools/lib/src/commands/build_aar.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aar.dart
@@ -38,6 +38,8 @@
usesFlavorOption();
usesBuildNumberOption();
usesPubOption();
+ addSplitDebugInfoOption();
+ addDartObfuscationOption();
argParser
..addMultiOption(
'target-platform',
@@ -104,15 +106,18 @@
for (final String buildMode in const <String>['debug', 'profile', 'release']) {
if (boolArg(buildMode)) {
- androidBuildInfo.add(AndroidBuildInfo(
- BuildInfo(BuildMode.fromName(buildMode), stringArg('flavor'), treeShakeIcons: boolArg('tree-shake-icons')),
- targetArchs: targetArchitectures,
- ));
+ androidBuildInfo.add(
+ AndroidBuildInfo(
+ getBuildInfo(forcedBuildMode: BuildMode.fromName(buildMode)),
+ targetArchs: targetArchitectures,
+ )
+ );
}
}
if (androidBuildInfo.isEmpty) {
throwToolExit('Please specify a build mode and try again.');
}
+
await androidBuilder.buildAar(
project: _getProject(),
target: '', // Not needed because this command only builds Android's code.
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index c0d8d69..3c5c5df 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -558,10 +558,12 @@
}
/// Compute the [BuildInfo] for the current flutter command.
+ /// Commands that build multiple build modes can pass in a [forcedBuildMode]
+ /// to be used instead of parsing flags.
///
/// Throws a [ToolExit] if the current set of options is not compatible with
- /// eachother.
- BuildInfo getBuildInfo() {
+ /// each other.
+ BuildInfo getBuildInfo({ BuildMode forcedBuildMode }) {
final bool trackWidgetCreation = argParser.options.containsKey('track-widget-creation') &&
boolArg('track-widget-creation');
@@ -603,7 +605,7 @@
'combination with "--${FlutterOptions.kSplitDebugInfoOption}"',
);
}
- final BuildMode buildMode = getBuildMode();
+ final BuildMode buildMode = forcedBuildMode ?? getBuildMode();
final bool treeShakeIcons = argParser.options.containsKey('tree-shake-icons')
&& buildMode.isPrecompiled
&& boolArg('tree-shake-icons');
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
index 3656890..7d76f82 100644
--- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
@@ -761,19 +761,12 @@
group('buildPluginsAsAar', () {
FileSystem fs;
- MockProcessManager mockProcessManager;
+ FakeProcessManager fakeProcessManager;
MockAndroidSdk mockAndroidSdk;
setUp(() {
fs = MemoryFileSystem();
-
- mockProcessManager = MockProcessManager();
- when(mockProcessManager.run(
- any,
- workingDirectory: anyNamed('workingDirectory'),
- environment: anyNamed('environment'),
- )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));
-
+ fakeProcessManager = FakeProcessManager.list(<FakeCommand>[]);
mockAndroidSdk = MockAndroidSdk();
when(mockAndroidSdk.directory).thenReturn('irrelevant');
});
@@ -830,12 +823,6 @@
.childDirectory('repo')
.createSync(recursive: true);
- await buildPluginsAsAar(
- FlutterProject.fromPath(androidDirectory.path),
- const AndroidBuildInfo(BuildInfo.release),
- buildDirectory: buildDirectory,
- );
-
final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot);
final String initScript = globals.fs.path.join(
flutterRoot,
@@ -844,40 +831,55 @@
'gradle',
'aar_init_script.gradle',
);
- verify(mockProcessManager.run(
- <String>[
- 'gradlew',
- '-I=$initScript',
- '-Pflutter-root=$flutterRoot',
- '-Poutput-dir=${buildDirectory.path}',
- '-Pis-plugin=true',
- '-PbuildNumber=1.0',
- '-Ptarget-platform=android-arm,android-arm64,android-x64',
- 'assembleAarRelease',
- ],
- environment: anyNamed('environment'),
- workingDirectory: plugin1.childDirectory('android').path),
- ).called(1);
- verify(mockProcessManager.run(
- <String>[
- 'gradlew',
- '-I=$initScript',
- '-Pflutter-root=$flutterRoot',
- '-Poutput-dir=${buildDirectory.path}',
- '-Pis-plugin=true',
- '-PbuildNumber=1.0',
- '-Ptarget-platform=android-arm,android-arm64,android-x64',
- 'assembleAarRelease',
- ],
- environment: anyNamed('environment'),
- workingDirectory: plugin2.childDirectory('android').path),
- ).called(1);
+ fakeProcessManager
+ ..addCommand(FakeCommand(
+ command: <String>[
+ 'gradlew',
+ '-I=$initScript',
+ '-Pflutter-root=$flutterRoot',
+ '-Poutput-dir=${buildDirectory.path}',
+ '-Pis-plugin=true',
+ '-PbuildNumber=1.0',
+ '-q',
+ '-Pfont-subset=true',
+ '-Ptarget-platform=android-arm,android-arm64,android-x64',
+ 'assembleAarRelease',
+ ],
+ workingDirectory: plugin1.childDirectory('android').path,
+ ))
+ ..addCommand(FakeCommand(
+ command: <String>[
+ 'gradlew',
+ '-I=$initScript',
+ '-Pflutter-root=$flutterRoot',
+ '-Poutput-dir=${buildDirectory.path}',
+ '-Pis-plugin=true',
+ '-PbuildNumber=1.0',
+ '-q',
+ '-Pfont-subset=true',
+ '-Ptarget-platform=android-arm,android-arm64,android-x64',
+ 'assembleAarRelease',
+ ],
+ workingDirectory: plugin2.childDirectory('android').path,
+ ));
+ await buildPluginsAsAar(
+ FlutterProject.fromPath(androidDirectory.path),
+ const AndroidBuildInfo(BuildInfo(
+ BuildMode.release,
+ '',
+ treeShakeIcons: true,
+ dartObfuscation: true,
+ buildNumber: '2.0'
+ )),
+ buildDirectory: buildDirectory,
+ );
+ expect(fakeProcessManager.hasRemainingExpectations, isFalse);
}, overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
+ ProcessManager: () => fakeProcessManager,
GradleUtils: () => FakeGradleUtils(),
});
@@ -920,32 +922,11 @@
const AndroidBuildInfo(BuildInfo.release),
buildDirectory: buildDirectory,
);
-
- final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot);
- final String initScript = globals.fs.path.join(
- flutterRoot,
- 'packages',
- 'flutter_tools',
- 'gradle',
- 'aar_init_script.gradle',
- );
- verifyNever(mockProcessManager.run(
- <String>[
- 'gradlew',
- '-I=$initScript',
- '-Pflutter-root=$flutterRoot',
- '-Poutput-dir=${buildDirectory.path}',
- '-Pis-plugin=true',
- '-Ptarget-platform=android-arm,android-arm64,android-x64',
- 'assembleAarRelease',
- ],
- environment: anyNamed('environment'),
- workingDirectory: plugin1.childDirectory('android').path),
- );
+ expect(fakeProcessManager.hasRemainingExpectations, isFalse);
}, overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
+ ProcessManager: () => fakeProcessManager,
GradleUtils: () => FakeGradleUtils(),
});
});
diff --git a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
index eb16597f..5ec53ce 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
@@ -8,6 +8,7 @@
import 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build_aar.dart';
import 'package:flutter_tools/src/project.dart';
@@ -24,6 +25,18 @@
void main() {
Cache.disableLocking();
+ Future<BuildAarCommand> runCommandIn(String target, { List<String> arguments }) async {
+ final BuildAarCommand command = BuildAarCommand();
+ final CommandRunner<void> runner = createTestCommandRunner(command);
+ await runner.run(<String>[
+ 'aar',
+ '--no-pub',
+ ...?arguments,
+ target,
+ ]);
+ return command;
+ }
+
group('Usage', () {
Directory tempDir;
Usage mockUsage;
@@ -37,18 +50,6 @@
tryToDelete(tempDir);
});
- Future<BuildAarCommand> runCommandIn(String target, { List<String> arguments }) async {
- final BuildAarCommand command = BuildAarCommand();
- final CommandRunner<void> runner = createTestCommandRunner(command);
- await runner.run(<String>[
- 'aar',
- '--no-pub',
- ...?arguments,
- target,
- ]);
- return command;
- }
-
testUsingContext('indicate that project is a module', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=module']);
@@ -107,6 +108,93 @@
});
});
+ group('flag parsing', () {
+ Directory tempDir;
+ MockAndroidBuilder mockAndroidBuilder;
+
+ setUp(() {
+ mockAndroidBuilder = MockAndroidBuilder();
+ tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_build_aar_test.');
+ });
+
+ tearDown(() {
+ tryToDelete(tempDir);
+ });
+
+ testUsingContext('defaults', () async {
+ final String projectPath = await createProject(tempDir,
+ arguments: <String>['--no-pub']);
+ await runCommandIn(projectPath);
+
+ final Set<AndroidBuildInfo> androidBuildInfos = verify(mockAndroidBuilder.buildAar(
+ project: anyNamed('project'),
+ target: anyNamed('target'),
+ androidBuildInfo: captureAnyNamed('androidBuildInfo'),
+ outputDirectoryPath: anyNamed('outputDirectoryPath'),
+ buildNumber: '1.0',
+ )).captured[0] as Set<AndroidBuildInfo>;
+
+ expect(androidBuildInfos.length, 3);
+
+ final List<BuildMode> buildModes = <BuildMode>[];
+ for (final AndroidBuildInfo androidBuildInfo in androidBuildInfos) {
+ final BuildInfo buildInfo = androidBuildInfo.buildInfo;
+ buildModes.add(buildInfo.mode);
+ expect(buildInfo.treeShakeIcons, isFalse);
+ expect(buildInfo.flavor, isNull);
+ expect(buildInfo.splitDebugInfoPath, isNull);
+ expect(buildInfo.dartObfuscation, isFalse);
+ expect(androidBuildInfo.targetArchs, <AndroidArch>[AndroidArch.armeabi_v7a, AndroidArch.arm64_v8a, AndroidArch.x86_64]);
+ }
+ expect(buildModes.length, 3);
+ expect(buildModes, containsAll(<BuildMode>[BuildMode.debug, BuildMode.profile, BuildMode.release]));
+ }, overrides: <Type, Generator>{
+ AndroidBuilder: () => mockAndroidBuilder,
+ });
+
+ testUsingContext('parses flags', () async {
+ final String projectPath = await createProject(tempDir,
+ arguments: <String>['--no-pub']);
+ await runCommandIn(
+ projectPath,
+ arguments: <String>[
+ '--no-debug',
+ '--no-profile',
+ '--target-platform',
+ 'android-x86',
+ '--tree-shake-icons',
+ '--flavor',
+ 'free',
+ '--build-number',
+ '200',
+ '--split-debug-info',
+ '/project-name/v1.2.3/',
+ '--obfuscate',
+ ],
+ );
+
+ final Set<AndroidBuildInfo> androidBuildInfos = verify(mockAndroidBuilder.buildAar(
+ project: anyNamed('project'),
+ target: anyNamed('target'),
+ androidBuildInfo: captureAnyNamed('androidBuildInfo'),
+ outputDirectoryPath: anyNamed('outputDirectoryPath'),
+ buildNumber: '200',
+ )).captured[0] as Set<AndroidBuildInfo>;
+
+ final AndroidBuildInfo androidBuildInfo = androidBuildInfos.single;
+ expect(androidBuildInfo.targetArchs, <AndroidArch>[AndroidArch.x86]);
+
+ final BuildInfo buildInfo = androidBuildInfo.buildInfo;
+ expect(buildInfo.mode, BuildMode.release);
+ expect(buildInfo.treeShakeIcons, isTrue);
+ expect(buildInfo.flavor, 'free');
+ expect(buildInfo.splitDebugInfoPath, '/project-name/v1.2.3/');
+ expect(buildInfo.dartObfuscation, isTrue);
+ }, overrides: <Type, Generator>{
+ AndroidBuilder: () => mockAndroidBuilder,
+ });
+ });
+
group('Gradle', () {
ProcessManager mockProcessManager;
Directory tempDir;
@@ -199,6 +287,7 @@
return command;
}
+class MockAndroidBuilder extends Mock implements AndroidBuilder {}
class MockAndroidSdk extends Mock implements AndroidSdk {}
class MockProcessManager extends Mock implements ProcessManager {}
class MockProcess extends Mock implements Process {}