Unverified Commit c953cd19 authored by Dan Field's avatar Dan Field Committed by GitHub

Enable bitcode compilation for AOT (#36471)

parent e6128a0c
......@@ -114,6 +114,7 @@ BuildApp() {
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 @@ BuildApp() {
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 @@ BuildApp() {
EchoError "========================================================================"
exit -1
fi
RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \
${verbose_flag} \
build aot \
......@@ -183,7 +188,8 @@ BuildApp() {
--${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}."
......
......@@ -94,7 +94,13 @@ class AOTSnapshotter {
@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 @@ class AOTSnapshotter {
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 @@ class AOTSnapshotter {
// 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 @@ class AOTSnapshotter {
@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 @@ class AOTSnapshotter {
'-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;
}
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 linkResult;
return dsymResult;
}
/// Compiles a Dart file to kernel.
......
......@@ -119,8 +119,6 @@ const List<String> _kBuildModes = <String>[
'debug',
'profile',
'release',
'dynamic-profile',
'dynamic-release',
];
/// Return the name for the build mode, or "any" if null.
......
......@@ -25,6 +25,9 @@ const String kTargetPlatform = 'TargetPlatform';
/// 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 @@ Future<void> compileAotElf(Map<String, ChangeType> updates, Environment environm
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 @@ Future<void> compileAotElf(Map<String, ChangeType> updates, Environment environm
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 @@ Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment env
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 @@ Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment env
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 @@ Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment env
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);
......
......@@ -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 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
..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 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
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 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
bitcode: bitcode,
).then<int>((int buildExitCode) {
return buildExitCode;
});
......@@ -131,8 +149,15 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
'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 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
bitcode: false,
);
if (snapshotExitCode != 0) {
status?.cancel();
......@@ -176,3 +202,38 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
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,
);
}
}
......@@ -469,6 +469,18 @@ String readGeneratedXcconfig(String appPath) {
}
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 &&
......
......@@ -97,6 +97,18 @@ class Xcode {
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;
......
......@@ -116,6 +116,13 @@ void main() {
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 @@ void main() {
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
), isNot(equals(0)));
}, overrides: contextOverrides);
......@@ -146,6 +154,7 @@ void main() {
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
), isNot(0));
}, overrides: contextOverrides);
......@@ -157,17 +166,69 @@ void main() {
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
), isNot(0));
}, overrides: contextOverrides);
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>{
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: true,
);
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',
]);
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>{
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 +242,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
iosArch: IOSArch.armv7,
bitcode: false,
);
expect(genSnapshotExitCode, 0);
......@@ -190,11 +252,20 @@ void main() {
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',
]);
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 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
iosArch: IOSArch.arm64,
bitcode: false,
);
expect(genSnapshotExitCode, 0);
......@@ -253,6 +325,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
iosArch: IOSArch.armv7,
bitcode: false,
);
expect(genSnapshotExitCode, 0);
......@@ -290,6 +363,7 @@ void main() {
packagesPath: '.packages',
outputPath: outputPath,
iosArch: IOSArch.arm64,
bitcode: false,
);
expect(genSnapshotExitCode, 0);
......@@ -316,6 +390,7 @@ void main() {
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
);
expect(genSnapshotExitCode, 0);
......@@ -345,6 +420,7 @@ void main() {
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
);
expect(genSnapshotExitCode, 0);
......@@ -380,6 +456,7 @@ void main() {
mainPath: 'main.dill',
packagesPath: '.packages',
outputPath: outputPath,
bitcode: false,
);
expect(genSnapshotExitCode, 0);
......
......@@ -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 @@ void main() {
Environment androidEnvironment;
Environment iosEnvironment;
MockProcessManager mockProcessManager;
MockXcode mockXcode;
setUpAll(() {
Cache.disableLocking();
......@@ -33,6 +36,7 @@ void main() {
setUp(() {
mockProcessManager = MockProcessManager();
mockXcode = MockXcode();
testbed = Testbed(setup: () {
androidEnvironment = Environment(
projectDir: fs.currentDirectory,
......@@ -153,8 +157,70 @@ flutter_tools:lib/''');
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 @@ flutter_tools:lib/''');
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;
}
}
......
// 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 {}
......@@ -12,6 +12,7 @@ import 'package:flutter_tools/src/ios/xcodeproj.dart';
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 @@ void main() {
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 @@ Could not build the precompiled application for the device.''',
});
});
}
class MockUsage extends Mock implements Usage {}
......@@ -533,17 +533,6 @@ void transfer(FileSystemEntity entity, FileSystem target) {
}
}
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);
}
......
......@@ -141,3 +141,14 @@ const Timeout allowForRemotePubInvocation = Timeout.factor(10.0);
/// 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');
}
}
......@@ -606,4 +606,7 @@ class FakeProcessResult implements ProcessResult {
@override
final dynamic stdout;
@override
String toString() => stdout?.toString() ?? stderr?.toString() ?? runtimeType.toString();
}
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