[flutter_tools] support --split-debug-info option in android builds (#49650)

This commit is contained in:
Jonah Williams 2020-02-05 17:45:24 -08:00 committed by GitHub
parent de7908f9e9
commit da4b5d68c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 231 additions and 2 deletions

3
.gitignore vendored
View file

@ -102,6 +102,9 @@ unlinked_spec.ds
# Coverage
coverage/
# Symbols
app.*.symbols
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3

View file

@ -37,6 +37,12 @@ if [[ -n "$FLUTTER_ENGINE" ]]; then
flutter_engine_flag="--local-engine-src-path=${FLUTTER_ENGINE}"
fi
# Provide location to split debug info
split_debug_info_option=""
if [[ -n "$SPLIT_DEBUG_INFO" ]]; then
split_debug_info_option="-dSplitDebugInfo=${SPLIT_DEBUG_INFO}"
fi
# Set the build mode
build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
@ -75,7 +81,8 @@ RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \
-dTargetPlatform=darwin-x64 \
-dTargetFile="${target_path}" \
-dBuildMode="${build_mode}" \
-dFontSubset="${icon_tree_shaker_flag}" \
"${split_debug_info_option}" \
-dFontSubset="${icon_tree_shaker_flag}" \
--build-inputs="${build_inputs_path}" \
--build-outputs="${build_outputs_path}" \
--output="${ephemeral_dir}" \

View file

@ -603,6 +603,10 @@ class FlutterPlugin implements Plugin<Project> {
if (project.hasProperty('extra-gen-snapshot-options')) {
extraGenSnapshotOptionsValue = project.property('extra-gen-snapshot-options')
}
String splitDebugInfoValue = null
if (project.hasProperty('split-debug-info')) {
splitDebugInfoValue = project.property('split-debug-info')
}
Boolean treeShakeIconsOptionsValue = false
if (project.hasProperty('tree-shake-icons')) {
treeShakeIconsOptionsValue = project.property('tree-shake-icons').toBoolean()
@ -641,6 +645,7 @@ class FlutterPlugin implements Plugin<Project> {
intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/")
extraFrontEndOptions extraFrontEndOptionsValue
extraGenSnapshotOptions extraGenSnapshotOptionsValue
splitDebugInfo splitDebugInfoValue
treeShakeIcons treeShakeIconsOptionsValue
}
File libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
@ -775,6 +780,8 @@ abstract class BaseFlutterTask extends DefaultTask {
@Optional @Input
String extraGenSnapshotOptions
@Optional @Input
String splitDebugInfo
@Optional @Input
Boolean treeShakeIcons
@OutputFiles
@ -832,6 +839,9 @@ abstract class BaseFlutterTask extends DefaultTask {
if (extraFrontEndOptions != null) {
args "-dExtraFrontEndOptions=${extraFrontEndOptions}"
}
if (splitDebugInfo != null) {
args "-dSplitDebugInfo=${splitDebugInfo}"
}
if (treeShakeIcons == true) {
args "-dTreeShakeIcons=true"
}

View file

@ -338,6 +338,9 @@ Future<void> buildGradleApp({
if (androidBuildInfo.fastStart) {
command.add('-Pfast-start=true');
}
if (androidBuildInfo.buildInfo.splitDebugInfoPath != null) {
command.add('-Psplit-debug-info=${androidBuildInfo.buildInfo.splitDebugInfoPath}');
}
if (androidBuildInfo.buildInfo.treeShakeIcons) {
command.add('-Ptree-shake-icons=true');
}

View file

@ -92,6 +92,7 @@ class AotBuilder {
extraGenSnapshotOptions: extraGenSnapshotOptions,
bitcode: bitcode,
quiet: quiet,
splitDebugInfo: null,
).then<int>((int buildExitCode) {
return buildExitCode;
});
@ -128,6 +129,7 @@ class AotBuilder {
outputPath: outputPath,
extraGenSnapshotOptions: extraGenSnapshotOptions,
bitcode: false,
splitDebugInfo: null,
);
if (snapshotExitCode != 0) {
status?.cancel();

View file

@ -94,6 +94,7 @@ class AOTSnapshotter {
DarwinArch darwinArch,
List<String> extraGenSnapshotOptions = const <String>[],
@required bool bitcode,
@required String splitDebugInfo,
bool quiet = false,
}) async {
if (bitcode && platform != TargetPlatform.ios) {
@ -157,11 +158,25 @@ class AOTSnapshotter {
genSnapshotArgs.add('--no-use-integer-division');
}
// The name of the debug file must contain additonal information about
// the architecture, since a single build command may produce
// multiple debug files.
final String archName = getNameForTargetPlatform(platform, darwinArch: darwinArch);
final String debugFilename = 'app.$archName.symbols';
if (splitDebugInfo != null) {
globals.fs.directory(splitDebugInfo)
.createSync(recursive: true);
}
// Optimization arguments.
genSnapshotArgs.addAll(<String>[
// Faster async/await
'--no-causal-async-stacks',
'--lazy-async-stacks',
if (splitDebugInfo != null) ...<String>[
'--dwarf-stack-traces',
'--save-debugging-info=${globals.fs.path.join(splitDebugInfo, debugFilename)}'
]
]);
genSnapshotArgs.add(mainPath);

View file

@ -21,6 +21,7 @@ class BuildInfo {
this.fileSystemScheme,
this.buildNumber,
this.buildName,
this.splitDebugInfoPath,
@required this.treeShakeIcons,
});
@ -62,6 +63,11 @@ class BuildInfo {
/// On Xcode builds it is used as CFBundleShortVersionString,
final String buildName;
/// An optional directory path to save debugging information from dwarf stack
/// traces. If null, stack trace information is not stripped from the
/// executable.
final String splitDebugInfoPath;
static const BuildInfo debug = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
static const BuildInfo profile = BuildInfo(BuildMode.profile, null, treeShakeIcons: kIconTreeShakerEnabledDefault);
static const BuildInfo jitRelease = BuildInfo(BuildMode.jitRelease, null, treeShakeIcons: kIconTreeShakerEnabledDefault);
@ -392,7 +398,7 @@ DarwinArch getIOSArchForName(String arch) {
return null;
}
String getNameForTargetPlatform(TargetPlatform platform) {
String getNameForTargetPlatform(TargetPlatform platform, {DarwinArch darwinArch}) {
switch (platform) {
case TargetPlatform.android_arm:
return 'android-arm';
@ -403,6 +409,9 @@ String getNameForTargetPlatform(TargetPlatform platform) {
case TargetPlatform.android_x86:
return 'android-x86';
case TargetPlatform.ios:
if (darwinArch != null) {
return 'ios-${getNameForDarwinArch(darwinArch)}';
}
return 'ios';
case TargetPlatform.darwin_x64:
return 'darwin-x64';

View file

@ -204,6 +204,7 @@ class AndroidAot extends AotElfBase {
Future<void> build(Environment environment) async {
final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false);
final Directory output = environment.buildDir.childDirectory(_androidAbiName);
final String splitDebugInfo = environment.defines[kSplitDebugInfo];
if (environment.defines[kBuildMode] == null) {
throw MissingDefineException(kBuildMode, 'aot_elf');
}
@ -221,6 +222,7 @@ class AndroidAot extends AotElfBase {
outputPath: output.path,
bitcode: false,
extraGenSnapshotOptions: extraGenSnapshotOptions,
splitDebugInfo: splitDebugInfo,
);
if (snapshotExitCode != 0) {
throw Exception('AOT snapshotter exited with code $snapshotExitCode');

View file

@ -41,6 +41,9 @@ const String kExtraFrontEndOptions = 'ExtraFrontEndOptions';
/// This is expected to be a comma separated list of strings.
const String kExtraGenSnapshotOptions = 'ExtraGenSnapshotOptions';
/// Whether to strip source code information out of release builds and where to save it.
const String kSplitDebugInfo = 'SplitDebugInfo';
/// Alternative scheme for file URIs.
///
/// May be used along with [kFileSystemRoots] to support a multi-root
@ -259,6 +262,7 @@ abstract class AotElfBase extends Target {
?? const <String>[];
final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
final String saveDebuggingInformation = environment.defines[kSplitDebugInfo];
final int snapshotExitCode = await snapshotter.build(
platform: targetPlatform,
buildMode: buildMode,
@ -267,6 +271,7 @@ abstract class AotElfBase extends Target {
outputPath: outputPath,
bitcode: false,
extraGenSnapshotOptions: extraGenSnapshotOptions,
splitDebugInfo: saveDebuggingInformation
);
if (snapshotExitCode != 0) {
throw Exception('AOT snapshotter exited with code $snapshotExitCode');

View file

@ -38,6 +38,7 @@ abstract class AotAssemblyBase extends Target {
final bool bitcode = environment.defines[kBitcodeFlag] == 'true';
final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
final String splitDebugInfo = environment.defines[kSplitDebugInfo];
final List<DarwinArch> iosArchs = environment.defines[kIosArchs]
?.split(' ')
?.map(getIOSArchForName)
@ -60,6 +61,7 @@ abstract class AotAssemblyBase extends Target {
darwinArch: iosArch,
bitcode: bitcode,
quiet: true,
splitDebugInfo: splitDebugInfo,
));
}
final List<int> results = await Future.wait(pending);

View file

@ -200,6 +200,7 @@ class CompileMacOSFramework extends Target {
if (buildMode == BuildMode.debug) {
throw Exception('precompiled macOS framework only supported in release/profile builds.');
}
final String splitDebugInfo = environment.defines[kSplitDebugInfo];
final int result = await AOTSnapshotter(reportTimings: false).build(
bitcode: false,
buildMode: buildMode,
@ -208,6 +209,7 @@ class CompileMacOSFramework extends Target {
platform: TargetPlatform.darwin_x64,
darwinArch: DarwinArch.x86_64,
packagesPath: environment.projectDir.childFile('.packages').path,
splitDebugInfo: splitDebugInfo,
);
if (result != 0) {
throw Exception('gen shapshot failed.');

View file

@ -26,6 +26,7 @@ class BuildApkCommand extends BuildSubCommand {
usesBuildNumberOption();
usesBuildNameOption();
addShrinkingFlag();
addSplitDebugInfoOption();
argParser
..addFlag('split-per-abi',
negatable: false,

View file

@ -167,6 +167,11 @@ List<String> _xcodeBuildSettingsLines({
xcodeBuildSettings.add('FLUTTER_TARGET=$targetOverride');
}
// This is an optional path to split debug info
if (buildInfo.splitDebugInfoPath != null) {
xcodeBuildSettings.add('SPLIT_DEBUG_INFO=${buildInfo.splitDebugInfoPath}');
}
// The build outputs directory, relative to FLUTTER_APPLICATION_PATH.
xcodeBuildSettings.add('FLUTTER_BUILD_DIR=${buildDirOverride ?? getBuildDirectory()}');

View file

@ -107,6 +107,7 @@ class FlutterOptions {
static const String kEnableExperiment = 'enable-experiment';
static const String kFileSystemRoot = 'filesystem-root';
static const String kFileSystemScheme = 'filesystem-scheme';
static const String kSplitDebugInfoOption = 'split-debug-info';
}
abstract class FlutterCommand extends Command<void> {
@ -366,6 +367,20 @@ abstract class FlutterCommand extends Command<void> {
help: 'Build a JIT release version of your app${defaultToRelease ? ' (default mode)' : ''}.');
}
void addSplitDebugInfoOption() {
argParser.addOption(FlutterOptions.kSplitDebugInfoOption,
help: 'In a release build, this flag reduces application size by storing '
'Dart program symbols in a separate file on the host rather than in the '
'application. The value of the flag should be a directory where program '
'symbol files can be stored for later use. These symbol files contain '
'the information needed to symbolize Dart stack traces. For an app built '
'with this flag, the \'flutter symbolize\' command with the right program '
'symbol file is required to obtain a human readable stack trace. This '
'command is tracked by https://github.com/flutter/flutter/issues/50206',
valueHelp: '/project-name/v1.2.3/',
);
}
void addTreeShakeIconsFlag() {
argParser.addFlag('tree-shake-icons',
negatable: true,
@ -499,6 +514,9 @@ abstract class FlutterCommand extends Command<void> {
buildName: argParser.options.containsKey('build-name')
? stringArg('build-name')
: null,
splitDebugInfoPath: argParser.options.containsKey(FlutterOptions.kSplitDebugInfoOption)
? stringArg(FlutterOptions.kSplitDebugInfoOption)
: null,
treeShakeIcons: argParser.options.containsKey('tree-shake-icons')
? boolArg('tree-shake-icons')
: kIconTreeShakerEnabledDefault,

View file

@ -33,5 +33,8 @@
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

View file

@ -40,3 +40,6 @@ build/
.ios/
.flutter-plugins
.flutter-plugins-dependencies
# Symbolication related
app.*.symbols

View file

@ -266,6 +266,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
splitDebugInfo: null,
), isNot(equals(0)));
}, overrides: contextOverrides);
@ -278,6 +279,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
splitDebugInfo: null,
), isNot(0));
}, overrides: contextOverrides);
@ -290,6 +292,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
splitDebugInfo: null,
), isNot(0));
}, overrides: contextOverrides);
@ -316,6 +319,7 @@ void main() {
outputPath: outputPath,
darwinArch: DarwinArch.armv7,
bitcode: true,
splitDebugInfo: null,
);
expect(genSnapshotExitCode, 0);
@ -376,6 +380,7 @@ void main() {
outputPath: outputPath,
darwinArch: DarwinArch.armv7,
bitcode: true,
splitDebugInfo: null,
);
expect(genSnapshotExitCode, 0);
@ -435,6 +440,7 @@ void main() {
outputPath: outputPath,
darwinArch: DarwinArch.armv7,
bitcode: false,
splitDebugInfo: null,
);
expect(genSnapshotExitCode, 0);
@ -463,6 +469,61 @@ void main() {
expect(assemblyFile.readAsStringSync().contains('.section __DWARF'), true);
}, overrides: contextOverrides);
testUsingContext('builds iOS armv7 profile AOT snapshot with dwarf stack traces', () async {
globals.fs.file('main.dill').writeAsStringSync('binary magic');
final String outputPath = globals.fs.path.join('build', 'foo');
globals.fs.directory(outputPath).createSync(recursive: true);
final String assembly = globals.fs.path.join(outputPath, 'snapshot_assembly.S');
genSnapshot.outputs = <String, String>{
assembly: 'blah blah\n.section __DWARF\nblah blah\n',
};
final String debugPath = globals.fs.path.join('foo', 'app.ios-armv7.symbols');
final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.ios,
buildMode: BuildMode.profile,
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
darwinArch: DarwinArch.armv7,
bitcode: false,
splitDebugInfo: 'foo',
);
expect(genSnapshotExitCode, 0);
expect(genSnapshot.callCount, 1);
expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
expect(genSnapshot.snapshotType.mode, BuildMode.profile);
expect(genSnapshot.additionalArgs, <String>[
'--deterministic',
'--snapshot_kind=app-aot-assembly',
'--assembly=$assembly',
'--strip',
'--no-sim-use-hardfp',
'--no-use-integer-division',
'--no-causal-async-stacks',
'--lazy-async-stacks',
'--dwarf-stack-traces',
'--save-debugging-info=$debugPath',
'main.dill',
]);
verifyNever(mockXcode.cc(argThat(contains('-fembed-bitcode'))));
verifyNever(mockXcode.clang(argThat(contains('-fembed-bitcode'))));
verify(mockXcode.cc(argThat(contains('-isysroot')))).called(1);
verify(mockXcode.clang(argThat(contains('-isysroot')))).called(1);
final File assemblyFile = globals.fs.file(assembly);
expect(assemblyFile.existsSync(), true);
expect(assemblyFile.readAsStringSync().contains('.section __DWARF'), true);
}, overrides: contextOverrides);
testUsingContext('builds iOS arm64 profile AOT snapshot', () async {
globals.fs.file('main.dill').writeAsStringSync('binary magic');
@ -485,6 +546,7 @@ void main() {
outputPath: outputPath,
darwinArch: DarwinArch.arm64,
bitcode: false,
splitDebugInfo: null,
);
expect(genSnapshotExitCode, 0);
@ -524,6 +586,7 @@ void main() {
outputPath: outputPath,
darwinArch: DarwinArch.armv7,
bitcode: false,
splitDebugInfo: null,
);
expect(genSnapshotExitCode, 0);
@ -565,6 +628,7 @@ void main() {
outputPath: outputPath,
darwinArch: DarwinArch.arm64,
bitcode: false,
splitDebugInfo: null,
);
expect(genSnapshotExitCode, 0);
@ -595,6 +659,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
splitDebugInfo: null,
);
expect(genSnapshotExitCode, 0);
@ -614,6 +679,42 @@ void main() {
]);
}, overrides: contextOverrides);
testUsingContext('builds shared library for android-arm with dwarf stack traces', () async {
globals.fs.file('main.dill').writeAsStringSync('binary magic');
final String outputPath = globals.fs.path.join('build', 'foo');
final String debugPath = globals.fs.path.join('foo', 'app.android-arm.symbols');
globals.fs.directory(outputPath).createSync(recursive: true);
final int genSnapshotExitCode = await snapshotter.build(
platform: TargetPlatform.android_arm,
buildMode: BuildMode.release,
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
splitDebugInfo: 'foo',
);
expect(genSnapshotExitCode, 0);
expect(genSnapshot.callCount, 1);
expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
expect(genSnapshot.snapshotType.mode, BuildMode.release);
expect(genSnapshot.additionalArgs, <String>[
'--deterministic',
'--snapshot_kind=app-aot-elf',
'--elf=build/foo/app.so',
'--strip',
'--no-sim-use-hardfp',
'--no-use-integer-division',
'--no-causal-async-stacks',
'--lazy-async-stacks',
'--dwarf-stack-traces',
'--save-debugging-info=$debugPath',
'main.dill',
]);
}, overrides: contextOverrides);
testUsingContext('builds shared library for android-arm64', () async {
globals.fs.file('main.dill').writeAsStringSync('binary magic');
@ -627,6 +728,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
splitDebugInfo: null,
);
expect(genSnapshotExitCode, 0);
@ -665,6 +767,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
splitDebugInfo: null,
);
expect(genSnapshotExitCode, 0);

View file

@ -78,4 +78,11 @@ void main() {
expect(() => BuildMode.fromName('foo'), throwsArgumentError);
});
});
test('getNameForTargetPlatform on Darwin arches', () {
expect(getNameForTargetPlatform(TargetPlatform.ios, darwinArch: DarwinArch.arm64), 'ios-arm64');
expect(getNameForTargetPlatform(TargetPlatform.ios, darwinArch: DarwinArch.armv7), 'ios-armv7');
expect(getNameForTargetPlatform(TargetPlatform.ios, darwinArch: DarwinArch.x86_64), 'ios-x86_64');
expect(getNameForTargetPlatform(TargetPlatform.android), isNot(contains('ios')));
});
}

View file

@ -230,6 +230,35 @@ void main() {
ProcessManager: () => mockProcessManager,
});
testUsingContext('--split-debug-info is enabled when an output directory is provided', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=app']);
await expectLater(() async {
await runBuildApkCommand(projectPath, arguments: <String>['--split-debug-info=${tempDir.path}']);
}, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
verify(mockProcessManager.start(
<String>[
gradlew,
'-q',
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
'-Ptrack-widget-creation=true',
'-Pshrink=true',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'-Psplit-debug-info=${tempDir.path}',
'assembleRelease',
],
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment'),
)).called(1);
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
});
testUsingContext('shrinking is disabled when --no-shrink is passed', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=app']);