Enable bitcode compilation for AOT (#36471)
diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh
index cd8299a..6cc77de 100755
--- a/packages/flutter_tools/bin/xcode_backend.sh
+++ b/packages/flutter_tools/bin/xcode_backend.sh
@@ -114,6 +114,7 @@
flutter_engine_flag="--local-engine-src-path=${FLUTTER_ENGINE}"
fi
+ local bitcode_flag=""
if [[ -n "$LOCAL_ENGINE" ]]; then
if [[ $(echo "$LOCAL_ENGINE" | tr "[:upper:]" "[:lower:]") != *"$build_mode"* ]]; then
EchoError "========================================================================"
@@ -130,6 +131,9 @@
local_engine_flag="--local-engine=${LOCAL_ENGINE}"
flutter_framework="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.framework"
flutter_podspec="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.podspec"
+ if [[ $ENABLE_BITCODE == "YES" ]]; then
+ bitcode_flag="--bitcode"
+ fi
fi
if [[ -e "${project_path}/.ios" ]]; then
@@ -174,6 +178,7 @@
EchoError "========================================================================"
exit -1
fi
+
RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \
${verbose_flag} \
build aot \
@@ -183,7 +188,8 @@
--${build_mode} \
--ios-arch="${archs}" \
${flutter_engine_flag} \
- ${local_engine_flag}
+ ${local_engine_flag} \
+ ${bitcode_flag}
if [[ $? -ne 0 ]]; then
EchoError "Failed to build ${project_path}."
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index db5d625..ca09c3e 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -94,7 +94,13 @@
@required String outputPath,
IOSArch iosArch,
List<String> extraGenSnapshotOptions = const <String>[],
+ @required bool bitcode,
}) async {
+ if (bitcode && platform != TargetPlatform.ios) {
+ printError('Bitcode is only supported for iOS.');
+ return 1;
+ }
+
if (!_isValidAotPlatform(platform, buildMode)) {
printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
return 1;
@@ -172,6 +178,21 @@
return genSnapshotExitCode;
}
+ // TODO(dnfield): This should be removed when https://github.com/dart-lang/sdk/issues/37560
+ // is resolved.
+ // The DWARF section confuses Xcode tooling, so this strips it. Ideally,
+ // gen_snapshot would provide an argument to do this automatically.
+ if (platform == TargetPlatform.ios && bitcode) {
+ final IOSink sink = fs.file('$assembly.bitcode').openWrite();
+ for (String line in await fs.file(assembly).readAsLines()) {
+ if (line.startsWith('.section __DWARF')) {
+ break;
+ }
+ sink.writeln(line);
+ }
+ await sink.close();
+ }
+
// Write path to gen_snapshot, since snapshots have to be re-generated when we roll
// the Dart SDK.
final String genSnapshotPath = GenSnapshot.getSnapshotterPath(snapshotType);
@@ -180,7 +201,12 @@
// On iOS, we use Xcode to compile the snapshot into a dynamic library that the
// end-developer can link into their app.
if (platform == TargetPlatform.ios) {
- final RunResult result = await _buildIosFramework(iosArch: iosArch, assemblyPath: assembly, outputPath: outputDir.path);
+ final RunResult result = await _buildIosFramework(
+ iosArch: iosArch,
+ assemblyPath: bitcode ? '$assembly.bitcode' : assembly,
+ outputPath: outputDir.path,
+ bitcode: bitcode,
+ );
if (result.exitCode != 0)
return result.exitCode;
}
@@ -193,13 +219,21 @@
@required IOSArch iosArch,
@required String assemblyPath,
@required String outputPath,
+ @required bool bitcode,
}) async {
final String targetArch = iosArch == IOSArch.armv7 ? 'armv7' : 'arm64';
printStatus('Building App.framework for $targetArch...');
final List<String> commonBuildOptions = <String>['-arch', targetArch, '-miphoneos-version-min=8.0'];
final String assemblyO = fs.path.join(outputPath, 'snapshot_assembly.o');
- final RunResult compileResult = await xcode.cc(<String>[...commonBuildOptions, '-c', assemblyPath, '-o', assemblyO]);
+ final RunResult compileResult = await xcode.cc(<String>[
+ ...commonBuildOptions,
+ '-c',
+ assemblyPath,
+ '-o',
+ assemblyO,
+ if (bitcode) '-fembed-bitcode',
+ ]);
if (compileResult.exitCode != 0) {
printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
return compileResult;
@@ -214,14 +248,23 @@
'-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
'-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
'-install_name', '@rpath/App.framework/App',
+ if (bitcode) '-fembed-bitcode',
'-o', appLib,
assemblyO,
];
final RunResult linkResult = await xcode.clang(linkArgs);
if (linkResult.exitCode != 0) {
printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}');
+ return linkResult;
}
- return linkResult;
+ final RunResult dsymResult = await xcode.dsymutil(<String>[
+ appLib,
+ '-o', fs.path.join(outputPath, 'App.framework.dSYM'),
+ ]);
+ if (dsymResult.exitCode != 0) {
+ printError('Failed to extract dSYM out of dynamic lib');
+ }
+ return dsymResult;
}
/// Compiles a Dart file to kernel.
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index ac0489a..d53c654 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -119,8 +119,6 @@
'debug',
'profile',
'release',
- 'dynamic-profile',
- 'dynamic-release',
];
/// Return the name for the build mode, or "any" if null.
diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
index f7d17f6..d704ad3 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/dart.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart
@@ -25,6 +25,9 @@
/// The define to control what target file is used.
const String kTargetFile = 'TargetFile';
+/// The define to control whether the AOT snapshot is built with bitcode.
+const String kBitcodeFlag = 'EnableBitcode';
+
/// The define to control what iOS architectures are built for.
///
/// This is expected to be a comma-separated list of architectures. If not
@@ -74,6 +77,7 @@
if (environment.defines[kTargetPlatform] == null) {
throw MissingDefineException(kTargetPlatform, 'aot_elf');
}
+
final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
final int snapshotExitCode = await snapshotter.build(
@@ -82,6 +86,7 @@
mainPath: environment.buildDir.childFile('main.app.dill').path,
packagesPath: environment.projectDir.childFile('.packages').path,
outputPath: outputPath,
+ bitcode: false,
);
if (snapshotExitCode != 0) {
throw Exception('AOT snapshotter exited with code $snapshotExitCode');
@@ -126,6 +131,7 @@
if (targetPlatform != TargetPlatform.ios) {
throw Exception('aot_assembly is only supported for iOS applications');
}
+ final bool bitcode = environment.defines[kBitcodeFlag] == 'true';
// If we're building for a single architecture (common), then skip the lipo.
if (iosArchs.length == 1) {
@@ -136,6 +142,7 @@
packagesPath: environment.projectDir.childFile('.packages').path,
outputPath: outputPath,
iosArch: iosArchs.single,
+ bitcode: bitcode,
);
if (snapshotExitCode != 0) {
throw Exception('AOT snapshotter exited with code $snapshotExitCode');
@@ -152,6 +159,7 @@
packagesPath: environment.projectDir.childFile('.packages').path,
outputPath: fs.path.join(outputPath, getNameForIOSArch(iosArch)),
iosArch: iosArch,
+ bitcode: bitcode,
));
}
final List<int> results = await Future.wait(pending);
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index 4a86582..e22d70d 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -4,14 +4,18 @@
import 'dart:async';
+import '../artifacts.dart';
import '../base/build.dart';
import '../base/common.dart';
+import '../base/context.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../build_info.dart';
import '../dart/package_map.dart';
import '../globals.dart';
+import '../ios/ios_workflow.dart';
+import '../macos/xcode.dart';
import '../resident_runner.dart';
import '../runner/flutter_command.dart';
import 'build.dart';
@@ -52,6 +56,11 @@
..addMultiOption(FlutterOptions.kExtraGenSnapshotOptions,
splitCommas: true,
hide: true,
+ )
+ ..addFlag('bitcode',
+ defaultsTo: false,
+ help: 'Build the AOT bundle with bitcode. Requires a compatible bitcode engine.',
+ hide: true,
);
}
@@ -68,8 +77,16 @@
if (platform == null)
throwToolExit('Unknown platform: $targetPlatform');
+ final bool bitcode = argResults['bitcode'];
final BuildMode buildMode = getBuildMode();
+ if (bitcode) {
+ if (platform != TargetPlatform.ios) {
+ throwToolExit('Bitcode is only supported on iOS (TargetPlatform is $targetPlatform).');
+ }
+ await validateBitcode();
+ }
+
Status status;
if (!argResults['quiet']) {
final String typeName = artifacts.getEngineType(platform, buildMode);
@@ -118,6 +135,7 @@
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
+ bitcode: bitcode,
).then<int>((int buildExitCode) {
return buildExitCode;
});
@@ -131,8 +149,15 @@
'lipo',
...dylibs,
'-create',
- '-output',
- fs.path.join(outputPath, 'App.framework', 'App'),
+ '-output', fs.path.join(outputPath, 'App.framework', 'App'),
+ ]);
+ final Iterable<String> dSYMs = iosBuilds.values.map<String>((String outputDir) => fs.path.join(outputDir, 'App.framework.dSYM'));
+ fs.directory(fs.path.join(outputPath, 'App.framework.dSYM', 'Contents', 'Resources', 'DWARF'))..createSync(recursive: true);
+ await runCheckedAsync(<String>[
+ 'lipo',
+ '-create',
+ '-output', fs.path.join(outputPath, 'App.framework.dSYM', 'Contents', 'Resources', 'DWARF', 'App'),
+ ...dSYMs.map((String path) => fs.path.join(path, 'Contents', 'Resources', 'DWARF', 'App'))
]);
} else {
status?.cancel();
@@ -150,6 +175,7 @@
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
+ bitcode: false,
);
if (snapshotExitCode != 0) {
status?.cancel();
@@ -176,3 +202,38 @@
return null;
}
}
+
+Future<void> validateBitcode() async {
+ final Artifacts artifacts = Artifacts.instance;
+ if (artifacts is! LocalEngineArtifacts) {
+ throwToolExit('Bitcode is only supported with a local engine built with --bitcode.');
+ }
+ final String flutterFrameworkPath = artifacts.getArtifactPath(Artifact.flutterFramework);
+ if (!fs.isDirectorySync(flutterFrameworkPath)) {
+ throwToolExit('Flutter.framework not found at $flutterFrameworkPath');
+ }
+ final Xcode xcode = context.get<Xcode>();
+
+ // Check for bitcode in Flutter binary.
+ final RunResult otoolResult = await xcode.otool(<String>[
+ '-l', fs.path.join(flutterFrameworkPath, 'Flutter'),
+ ]);
+ if (!otoolResult.stdout.contains('__LLVM')) {
+ throwToolExit('The Flutter.framework at $flutterFrameworkPath does not contain bitcode.');
+ }
+ final RunResult clangResult = await xcode.clang(<String>['--version']);
+ final String clangVersion = clangResult.stdout.split('\n').first;
+ final String engineClangVersion = iosWorkflow.getPlistValueFromFile(
+ fs.path.join(flutterFrameworkPath, 'Info.plist'),
+ 'ClangVersion',
+ );
+ if (clangVersion != engineClangVersion) {
+ printStatus(
+ 'The Flutter.framework at $flutterFrameworkPath was built '
+ 'with "${engineClangVersion ?? 'unknown'}", but the current version '
+ 'of clang is "$clangVersion". This may result in failures when '
+ 'archiving your application in Xcode.',
+ emphasis: true,
+ );
+ }
+}
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 92f56eb..5b5ca1f 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -471,6 +471,18 @@
Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
if (result.xcodeBuildExecution != null &&
result.xcodeBuildExecution.buildForPhysicalDevice &&
+ result.stdout?.toUpperCase()?.contains('BITCODE') == true) {
+ flutterUsage.sendEvent(
+ 'Xcode',
+ 'bitcode-failure',
+ parameters: <String, String>{
+ 'build-commands': result.xcodeBuildExecution.buildCommands.toString(),
+ 'build-settings': result.xcodeBuildExecution.buildSettings.toString(),
+ });
+ }
+
+ if (result.xcodeBuildExecution != null &&
+ result.xcodeBuildExecution.buildForPhysicalDevice &&
result.stdout?.contains('BCEROR') == true &&
// May need updating if Xcode changes its outputs.
result.stdout?.contains('Xcode couldn\'t find a provisioning profile matching') == true) {
diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart
index 7cb7d04..3eb485e 100644
--- a/packages/flutter_tools/lib/src/macos/xcode.dart
+++ b/packages/flutter_tools/lib/src/macos/xcode.dart
@@ -97,6 +97,18 @@
return runCheckedAsync(<String>['xcrun', 'clang', ...args]);
}
+ Future<RunResult> dsymutil(List<String> args) {
+ return runCheckedAsync(<String>['xcrun', 'dsymutil', ...args]);
+ }
+
+ Future<RunResult> strip(List<String> args) {
+ return runCheckedAsync(<String>['xcrun', 'strip', ...args]);
+ }
+
+ Future<RunResult> otool(List<String> args) {
+ return runCheckedAsync(<String>['xcrun', 'otool', ...args]);
+ }
+
String getSimulatorPath() {
if (xcodeSelectPath == null)
return null;
diff --git a/packages/flutter_tools/test/general.shard/base/build_test.dart b/packages/flutter_tools/test/general.shard/base/build_test.dart
index 13f96b9..bfbda39 100644
--- a/packages/flutter_tools/test/general.shard/base/build_test.dart
+++ b/packages/flutter_tools/test/general.shard/base/build_test.dart
@@ -116,6 +116,13 @@
when(mockArtifacts.getArtifactPath(Artifact.snapshotDart,
platform: anyNamed('platform'), mode: mode)).thenReturn(kSnapshotDart);
}
+
+ when(mockXcode.dsymutil(any)).thenAnswer((_) => Future<RunResult>.value(
+ RunResult(
+ ProcessResult(1, 0, '', ''),
+ <String>['command name', 'arguments...']),
+ ),
+ );
});
final Map<Type, Generator> contextOverrides = <Type, Generator>{
@@ -135,6 +142,7 @@
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
+ bitcode: false,
), isNot(equals(0)));
}, overrides: contextOverrides);
@@ -146,6 +154,7 @@
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
+ bitcode: false,
), isNot(0));
}, overrides: contextOverrides);
@@ -157,17 +166,19 @@
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
+ bitcode: false,
), isNot(0));
}, overrides: contextOverrides);
- testUsingContext('builds iOS armv7 profile AOT snapshot', () async {
+ testUsingContext('iOS debug AOT with bitcode uses right flags', () async {
fs.file('main.dill').writeAsStringSync('binary magic');
final String outputPath = fs.path.join('build', 'foo');
fs.directory(outputPath).createSync(recursive: true);
+ final String assembly = fs.path.join(outputPath, 'snapshot_assembly.S');
genSnapshot.outputs = <String, String>{
- fs.path.join(outputPath, 'snapshot_assembly.S'): '',
+ assembly: 'blah blah\n.section __DWARF\nblah blah\n',
};
final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
@@ -181,6 +192,7 @@
packagesPath: '.packages',
outputPath: outputPath,
iosArch: IOSArch.armv7,
+ bitcode: true,
);
expect(genSnapshotExitCode, 0);
@@ -190,11 +202,70 @@
expect(genSnapshot.additionalArgs, <String>[
'--deterministic',
'--snapshot_kind=app-aot-assembly',
- '--assembly=${fs.path.join(outputPath, 'snapshot_assembly.S')}',
+ '--assembly=$assembly',
'--no-sim-use-hardfp',
'--no-use-integer-division',
'main.dill',
]);
+
+ verify(xcode.cc(argThat(contains('-fembed-bitcode')))).called(1);
+ verify(xcode.clang(argThat(contains('-fembed-bitcode')))).called(1);
+ verify(xcode.dsymutil(any)).called(1);
+
+ final File assemblyFile = fs.file(assembly);
+ final File assemblyBitcodeFile = fs.file('$assembly.bitcode');
+ expect(assemblyFile.existsSync(), true);
+ expect(assemblyBitcodeFile.existsSync(), true);
+ expect(assemblyFile.readAsStringSync().contains('.section __DWARF'), true);
+ expect(assemblyBitcodeFile.readAsStringSync().contains('.section __DWARF'), false);
+ }, overrides: contextOverrides);
+
+ testUsingContext('builds iOS armv7 profile AOT snapshot', () async {
+ fs.file('main.dill').writeAsStringSync('binary magic');
+
+ final String outputPath = fs.path.join('build', 'foo');
+ fs.directory(outputPath).createSync(recursive: true);
+
+ final String assembly = fs.path.join(outputPath, 'snapshot_assembly.S');
+ genSnapshot.outputs = <String, String>{
+ assembly: 'blah blah\n.section __DWARF\nblah blah\n',
+ };
+
+ final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+ when(xcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+ when(xcode.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,
+ iosArch: IOSArch.armv7,
+ bitcode: false,
+ );
+
+ 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',
+ '--no-sim-use-hardfp',
+ '--no-use-integer-division',
+ 'main.dill',
+ ]);
+ verifyNever(xcode.cc(argThat(contains('-fembed-bitcode'))));
+ verifyNever(xcode.clang(argThat(contains('-fembed-bitcode'))));
+ verify(xcode.dsymutil(any)).called(1);
+
+ final File assemblyFile = fs.file(assembly);
+ final File assemblyBitcodeFile = fs.file('$assembly.bitcode');
+ expect(assemblyFile.existsSync(), true);
+ expect(assemblyBitcodeFile.existsSync(), false);
+ expect(assemblyFile.readAsStringSync().contains('.section __DWARF'), true);
}, overrides: contextOverrides);
testUsingContext('builds iOS arm64 profile AOT snapshot', () async {
@@ -218,6 +289,7 @@
packagesPath: '.packages',
outputPath: outputPath,
iosArch: IOSArch.arm64,
+ bitcode: false,
);
expect(genSnapshotExitCode, 0);
@@ -253,6 +325,7 @@
packagesPath: '.packages',
outputPath: outputPath,
iosArch: IOSArch.armv7,
+ bitcode: false,
);
expect(genSnapshotExitCode, 0);
@@ -290,6 +363,7 @@
packagesPath: '.packages',
outputPath: outputPath,
iosArch: IOSArch.arm64,
+ bitcode: false,
);
expect(genSnapshotExitCode, 0);
@@ -316,6 +390,7 @@
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
+ bitcode: false,
);
expect(genSnapshotExitCode, 0);
@@ -345,6 +420,7 @@
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
+ bitcode: false,
);
expect(genSnapshotExitCode, 0);
@@ -380,6 +456,7 @@
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
+ bitcode: false,
);
expect(genSnapshotExitCode, 0);
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart
index 585ac48..74064ee 100644
--- a/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart
@@ -5,12 +5,14 @@
import 'package:flutter_tools/src/base/build.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/exceptions.dart';
import 'package:flutter_tools/src/build_system/targets/dart.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/compile.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
@@ -26,6 +28,7 @@
Environment androidEnvironment;
Environment iosEnvironment;
MockProcessManager mockProcessManager;
+ MockXcode mockXcode;
setUpAll(() {
Cache.disableLocking();
@@ -33,6 +36,7 @@
setUp(() {
mockProcessManager = MockProcessManager();
+ mockXcode = MockXcode();
testbed = Testbed(setup: () {
androidEnvironment = Environment(
projectDir: fs.currentDirectory,
@@ -153,8 +157,70 @@
expect(result.exceptions.values.single.exception, isInstanceOf<Exception>());
}));
+ test('aot_assembly_profile with bitcode sends correct argument to snapshotter (one arch)', () => testbed.run(() async {
+ iosEnvironment.defines[kIosArchs] = 'arm64';
+ iosEnvironment.defines[kBitcodeFlag] = 'true';
+
+ final FakeProcessResult fakeProcessResult = FakeProcessResult(
+ stdout: '',
+ stderr: '',
+ );
+ final RunResult fakeRunResult = RunResult(fakeProcessResult, const <String>['foo']);
+ when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
+ fs.file(fs.path.join(iosEnvironment.buildDir.path, 'App.framework', 'App'))
+ .createSync(recursive: true);
+ return fakeProcessResult;
+ });
+
+ when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(fakeRunResult));
+ when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(fakeRunResult));
+ when(mockXcode.dsymutil(any)).thenAnswer((_) => Future<RunResult>.value(fakeRunResult));
+
+ final BuildResult result = await buildSystem.build('aot_assembly_profile',
+ iosEnvironment, const BuildSystemConfig());
+
+ expect(result.success, true);
+ verify(mockXcode.cc(argThat(contains('-fembed-bitcode')))).called(1);
+ verify(mockXcode.clang(argThat(contains('-fembed-bitcode')))).called(1);
+ verify(mockXcode.dsymutil(any)).called(1);
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ Xcode: () => mockXcode,
+ }));
+
+ test('aot_assembly_profile with bitcode sends correct argument to snapshotter (mutli arch)', () => testbed.run(() async {
+ iosEnvironment.defines[kIosArchs] = 'armv7,arm64';
+ iosEnvironment.defines[kBitcodeFlag] = 'true';
+
+ final FakeProcessResult fakeProcessResult = FakeProcessResult(
+ stdout: '',
+ stderr: '',
+ );
+ final RunResult fakeRunResult = RunResult(fakeProcessResult, const <String>['foo']);
+ when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
+ fs.file(fs.path.join(iosEnvironment.buildDir.path, 'App.framework', 'App'))
+ .createSync(recursive: true);
+ return fakeProcessResult;
+ });
+
+ when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(fakeRunResult));
+ when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(fakeRunResult));
+ when(mockXcode.dsymutil(any)).thenAnswer((_) => Future<RunResult>.value(fakeRunResult));
+
+ final BuildResult result = await buildSystem.build('aot_assembly_profile',
+ iosEnvironment, const BuildSystemConfig());
+
+ expect(result.success, true);
+ verify(mockXcode.cc(argThat(contains('-fembed-bitcode')))).called(2);
+ verify(mockXcode.clang(argThat(contains('-fembed-bitcode')))).called(2);
+ verify(mockXcode.dsymutil(any)).called(2);
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ Xcode: () => mockXcode,
+ }));
+
test('aot_assembly_profile will lipo binaries together when multiple archs are requested', () => testbed.run(() async {
- iosEnvironment.defines[kIosArchs] ='armv7,arm64';
+ iosEnvironment.defines[kIosArchs] = 'armv7,arm64';
when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
fs.file(fs.path.join(iosEnvironment.buildDir.path, 'App.framework', 'App'))
.createSync(recursive: true);
@@ -175,18 +241,26 @@
class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcode extends Mock implements Xcode {}
+
class FakeGenSnapshot implements GenSnapshot {
+ List<String> lastCallAdditionalArgs;
@override
Future<int> run({SnapshotType snapshotType, IOSArch iosArch, Iterable<String> additionalArgs = const <String>[]}) async {
- final Directory out = fs.file(additionalArgs.last).parent;
+ lastCallAdditionalArgs = additionalArgs.toList();
+ final Directory out = fs.file(lastCallAdditionalArgs.last).parent;
if (iosArch == null) {
out.childFile('app.so').createSync();
out.childFile('gen_snapshot.d').createSync();
return 0;
}
out.childDirectory('App.framework').childFile('App').createSync(recursive: true);
- out.childFile('snapshot_assembly.S').createSync();
- out.childFile('snapshot_assembly.o').createSync();
+
+ final String assembly = lastCallAdditionalArgs
+ .firstWhere((String arg) => arg.startsWith('--assembly'))
+ .substring('--assembly='.length);
+ fs.file(assembly).createSync();
+ fs.file(assembly.replaceAll('.S', '.o')).createSync();
return 0;
}
}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_aot_test.dart b/packages/flutter_tools/test/general.shard/commands/build_aot_test.dart
new file mode 100644
index 0000000..f697148
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/build_aot_test.dart
@@ -0,0 +1,139 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/process.dart';
+import 'package:flutter_tools/src/commands/build_aot.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/ios/ios_workflow.dart';
+import 'package:flutter_tools/src/macos/xcode.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() {
+ MockXcode mockXcode;
+ MemoryFileSystem memoryFileSystem;
+ MockProcessManager mockProcessManager;
+ BufferLogger bufferLogger;
+ MockIOSWorkflow mockIOSWorkflow;
+
+ setUp(() {
+ mockXcode = MockXcode();
+ memoryFileSystem = MemoryFileSystem(style: FileSystemStyle.posix);
+ mockProcessManager = MockProcessManager();
+ bufferLogger = BufferLogger();
+ mockIOSWorkflow = MockIOSWorkflow();
+ });
+
+ testUsingContext('build aot validates building with bitcode requires a local engine', () async {
+ await expectToolExitLater(
+ validateBitcode(),
+ equals('Bitcode is only supported with a local engine built with --bitcode.'),
+ );
+ });
+
+ testUsingContext('build aot validates existence of Flutter.framework in engine', () async {
+ await expectToolExitLater(
+ validateBitcode(),
+ equals('Flutter.framework not found at ios_profile/Flutter.framework'),
+ );
+ }, overrides: <Type, Generator>{
+ Artifacts: () => LocalEngineArtifacts('/engine', 'ios_profile', 'host_profile'),
+ FileSystem: () => memoryFileSystem,
+ });
+
+ testUsingContext('build aot validates Flutter.framework/Flutter contains bitcode', () async {
+ final Directory flutterFramework = memoryFileSystem.directory('ios_profile/Flutter.framework')
+ ..createSync(recursive: true);
+ flutterFramework.childFile('Flutter').createSync();
+ flutterFramework.childFile('Info.plist').createSync();
+
+ final RunResult otoolResult = RunResult(
+ FakeProcessResult(stdout: '', stderr: ''),
+ const <String>['foo'],
+ );
+ when(mockXcode.otool(any)).thenAnswer((_) => Future<RunResult>.value(otoolResult));
+ await expectToolExitLater(
+ validateBitcode(),
+ equals('The Flutter.framework at ios_profile/Flutter.framework does not contain bitcode.'),
+ );
+ }, overrides: <Type, Generator>{
+ Artifacts: () => LocalEngineArtifacts('/engine', 'ios_profile', 'host_profile'),
+ FileSystem: () => memoryFileSystem,
+ ProcessManager: () => mockProcessManager,
+ Xcode: () => mockXcode,
+ });
+
+ testUsingContext('build aot validates Flutter.framework/Flutter was built with same toolchain', () async {
+ final Directory flutterFramework = memoryFileSystem.directory('ios_profile/Flutter.framework')
+ ..createSync(recursive: true);
+ flutterFramework.childFile('Flutter').createSync();
+ final File infoPlist = flutterFramework.childFile('Info.plist')..createSync();
+
+ final RunResult otoolResult = RunResult(
+ FakeProcessResult(stdout: '__LLVM', stderr: ''),
+ const <String>['foo'],
+ );
+ final RunResult clangResult = RunResult(
+ FakeProcessResult(stdout: 'BadVersion\nBlahBlah\n', stderr: ''),
+ const <String>['foo'],
+ );
+ when(mockXcode.otool(any)).thenAnswer((_) => Future<RunResult>.value(otoolResult));
+ when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(clangResult));
+ when(mockIOSWorkflow.getPlistValueFromFile(infoPlist.path, 'ClangVersion')).thenReturn('Apple LLVM Version 10.0.1');
+
+ await validateBitcode();
+
+ expect(
+ bufferLogger.statusText,
+ startsWith('The Flutter.framework at ${flutterFramework.path} was built with "Apple LLVM Version 10.0.1'),
+ );
+ }, overrides: <Type, Generator>{
+ Artifacts: () => LocalEngineArtifacts('/engine', 'ios_profile', 'host_profile'),
+ FileSystem: () => memoryFileSystem,
+ ProcessManager: () => mockProcessManager,
+ Xcode: () => mockXcode,
+ Logger: () => bufferLogger,
+ IOSWorkflow: () => mockIOSWorkflow,
+ });
+
+ testUsingContext('build aot validates and succeeds', () async {
+ final Directory flutterFramework = memoryFileSystem.directory('ios_profile/Flutter.framework')
+ ..createSync(recursive: true);
+ flutterFramework.childFile('Flutter').createSync();
+ final File infoPlist = flutterFramework.childFile('Info.plist')..createSync();
+
+ final RunResult otoolResult = RunResult(
+ FakeProcessResult(stdout: '__LLVM', stderr: ''),
+ const <String>['foo'],
+ );
+ final RunResult clangResult = RunResult(
+ FakeProcessResult(stdout: 'Apple LLVM Version 10.0.1\nBlahBlah\n', stderr: ''),
+ const <String>['foo'],
+ );
+ when(mockXcode.otool(any)).thenAnswer((_) => Future<RunResult>.value(otoolResult));
+ when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(clangResult));
+ when(mockIOSWorkflow.getPlistValueFromFile(infoPlist.path, 'ClangVersion')).thenReturn('Apple LLVM Version 10.0.1');
+
+ await validateBitcode();
+
+ expect(bufferLogger.statusText, '');
+ }, overrides: <Type, Generator>{
+ Artifacts: () => LocalEngineArtifacts('/engine', 'ios_profile', 'host_profile'),
+ FileSystem: () => memoryFileSystem,
+ ProcessManager: () => mockProcessManager,
+ Xcode: () => mockXcode,
+ Logger: () => bufferLogger,
+ IOSWorkflow: () => mockIOSWorkflow,
+ });
+}
+
+class MockXcode extends Mock implements Xcode {}
+class MockIOSWorkflow extends Mock implements IOSWorkflow {}
diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart
index 8ffc0f0..6167b8a 100644
--- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart
@@ -12,6 +12,7 @@
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/reporting/usage.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
@@ -159,11 +160,35 @@
group('Diagnose Xcode build failure', () {
Map<String, String> buildSettings;
+ MockUsage mockUsage;
setUp(() {
buildSettings = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'test.app',
};
+ mockUsage = MockUsage();
+ });
+
+ testUsingContext('Sends analytics when bitcode fails', () async {
+ const List<String> buildCommands = <String>['xcrun', 'cc', 'blah'];
+ final XcodeBuildResult buildResult = XcodeBuildResult(
+ success: false,
+ stdout: 'BITCODE_ENABLED = YES',
+ xcodeBuildExecution: XcodeBuildExecution(
+ buildCommands: buildCommands,
+ appDirectory: '/blah/blah',
+ buildForPhysicalDevice: true,
+ buildSettings: buildSettings,
+ ),
+ );
+
+ await diagnoseXcodeBuildFailure(buildResult);
+ verify(mockUsage.sendEvent('Xcode', 'bitcode-failure', parameters: <String, String>{
+ 'build-commands': buildCommands.toString(),
+ 'build-settings': buildSettings.toString(),
+ })).called(1);
+ }, overrides: <Type, Generator>{
+ Usage: () => mockUsage,
});
testUsingContext('No provisioning profile shows message', () async {
@@ -379,3 +404,5 @@
});
});
}
+
+class MockUsage extends Mock implements Usage {}
diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart
index acc91e8..fc6697c 100644
--- a/packages/flutter_tools/test/general.shard/project_test.dart
+++ b/packages/flutter_tools/test/general.shard/project_test.dart
@@ -533,17 +533,6 @@
}
}
-Future<void> expectToolExitLater(Future<dynamic> future, Matcher messageMatcher) async {
- try {
- await future;
- fail('ToolExit expected, but nothing thrown');
- } on ToolExit catch(e) {
- expect(e.message, messageMatcher);
- } catch(e, trace) {
- fail('ToolExit expected, got $e\n$trace');
- }
-}
-
void expectExists(FileSystemEntity entity) {
expect(entity.existsSync(), isTrue);
}
diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart
index 75f95c9..2b8f22a 100644
--- a/packages/flutter_tools/test/src/common.dart
+++ b/packages/flutter_tools/test/src/common.dart
@@ -141,3 +141,14 @@
/// Test case timeout for tests involving creating a Flutter project with
/// `--no-pub`. Use [allowForRemotePubInvocation] when creation involves `pub`.
const Timeout allowForCreateFlutterProject = Timeout.factor(3.0);
+
+Future<void> expectToolExitLater(Future<dynamic> future, Matcher messageMatcher) async {
+ try {
+ await future;
+ fail('ToolExit expected, but nothing thrown');
+ } on ToolExit catch(e) {
+ expect(e.message, messageMatcher);
+ } catch(e, trace) {
+ fail('ToolExit expected, got $e\n$trace');
+ }
+}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 7620d99..fa3b7a6 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -606,4 +606,7 @@
@override
final dynamic stdout;
+
+ @override
+ String toString() => stdout?.toString() ?? stderr?.toString() ?? runtimeType.toString();
}