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() { ...@@ -114,6 +114,7 @@ BuildApp() {
flutter_engine_flag="--local-engine-src-path=${FLUTTER_ENGINE}" flutter_engine_flag="--local-engine-src-path=${FLUTTER_ENGINE}"
fi fi
local bitcode_flag=""
if [[ -n "$LOCAL_ENGINE" ]]; then if [[ -n "$LOCAL_ENGINE" ]]; then
if [[ $(echo "$LOCAL_ENGINE" | tr "[:upper:]" "[:lower:]") != *"$build_mode"* ]]; then if [[ $(echo "$LOCAL_ENGINE" | tr "[:upper:]" "[:lower:]") != *"$build_mode"* ]]; then
EchoError "========================================================================" EchoError "========================================================================"
...@@ -130,6 +131,9 @@ BuildApp() { ...@@ -130,6 +131,9 @@ BuildApp() {
local_engine_flag="--local-engine=${LOCAL_ENGINE}" local_engine_flag="--local-engine=${LOCAL_ENGINE}"
flutter_framework="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.framework" flutter_framework="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.framework"
flutter_podspec="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.podspec" flutter_podspec="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.podspec"
if [[ $ENABLE_BITCODE == "YES" ]]; then
bitcode_flag="--bitcode"
fi
fi fi
if [[ -e "${project_path}/.ios" ]]; then if [[ -e "${project_path}/.ios" ]]; then
...@@ -174,6 +178,7 @@ BuildApp() { ...@@ -174,6 +178,7 @@ BuildApp() {
EchoError "========================================================================" EchoError "========================================================================"
exit -1 exit -1
fi fi
RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \ RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \
${verbose_flag} \ ${verbose_flag} \
build aot \ build aot \
...@@ -183,7 +188,8 @@ BuildApp() { ...@@ -183,7 +188,8 @@ BuildApp() {
--${build_mode} \ --${build_mode} \
--ios-arch="${archs}" \ --ios-arch="${archs}" \
${flutter_engine_flag} \ ${flutter_engine_flag} \
${local_engine_flag} ${local_engine_flag} \
${bitcode_flag}
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
EchoError "Failed to build ${project_path}." EchoError "Failed to build ${project_path}."
......
...@@ -94,7 +94,13 @@ class AOTSnapshotter { ...@@ -94,7 +94,13 @@ class AOTSnapshotter {
@required String outputPath, @required String outputPath,
IOSArch iosArch, IOSArch iosArch,
List<String> extraGenSnapshotOptions = const <String>[], List<String> extraGenSnapshotOptions = const <String>[],
@required bool bitcode,
}) async { }) async {
if (bitcode && platform != TargetPlatform.ios) {
printError('Bitcode is only supported for iOS.');
return 1;
}
if (!_isValidAotPlatform(platform, buildMode)) { if (!_isValidAotPlatform(platform, buildMode)) {
printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.'); printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
return 1; return 1;
...@@ -172,6 +178,21 @@ class AOTSnapshotter { ...@@ -172,6 +178,21 @@ class AOTSnapshotter {
return genSnapshotExitCode; 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 // Write path to gen_snapshot, since snapshots have to be re-generated when we roll
// the Dart SDK. // the Dart SDK.
final String genSnapshotPath = GenSnapshot.getSnapshotterPath(snapshotType); final String genSnapshotPath = GenSnapshot.getSnapshotterPath(snapshotType);
...@@ -180,7 +201,12 @@ class AOTSnapshotter { ...@@ -180,7 +201,12 @@ class AOTSnapshotter {
// On iOS, we use Xcode to compile the snapshot into a dynamic library that the // On iOS, we use Xcode to compile the snapshot into a dynamic library that the
// end-developer can link into their app. // end-developer can link into their app.
if (platform == TargetPlatform.ios) { 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) if (result.exitCode != 0)
return result.exitCode; return result.exitCode;
} }
...@@ -193,13 +219,21 @@ class AOTSnapshotter { ...@@ -193,13 +219,21 @@ class AOTSnapshotter {
@required IOSArch iosArch, @required IOSArch iosArch,
@required String assemblyPath, @required String assemblyPath,
@required String outputPath, @required String outputPath,
@required bool bitcode,
}) async { }) async {
final String targetArch = iosArch == IOSArch.armv7 ? 'armv7' : 'arm64'; final String targetArch = iosArch == IOSArch.armv7 ? 'armv7' : 'arm64';
printStatus('Building App.framework for $targetArch...'); printStatus('Building App.framework for $targetArch...');
final List<String> commonBuildOptions = <String>['-arch', targetArch, '-miphoneos-version-min=8.0']; final List<String> commonBuildOptions = <String>['-arch', targetArch, '-miphoneos-version-min=8.0'];
final String assemblyO = fs.path.join(outputPath, 'snapshot_assembly.o'); 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) { if (compileResult.exitCode != 0) {
printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}'); printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
return compileResult; return compileResult;
...@@ -214,14 +248,23 @@ class AOTSnapshotter { ...@@ -214,14 +248,23 @@ class AOTSnapshotter {
'-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks', '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
'-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks', '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
'-install_name', '@rpath/App.framework/App', '-install_name', '@rpath/App.framework/App',
if (bitcode) '-fembed-bitcode',
'-o', appLib, '-o', appLib,
assemblyO, assemblyO,
]; ];
final RunResult linkResult = await xcode.clang(linkArgs); final RunResult linkResult = await xcode.clang(linkArgs);
if (linkResult.exitCode != 0) { if (linkResult.exitCode != 0) {
printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}'); 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. /// Compiles a Dart file to kernel.
......
...@@ -119,8 +119,6 @@ const List<String> _kBuildModes = <String>[ ...@@ -119,8 +119,6 @@ const List<String> _kBuildModes = <String>[
'debug', 'debug',
'profile', 'profile',
'release', 'release',
'dynamic-profile',
'dynamic-release',
]; ];
/// Return the name for the build mode, or "any" if null. /// Return the name for the build mode, or "any" if null.
......
...@@ -25,6 +25,9 @@ const String kTargetPlatform = 'TargetPlatform'; ...@@ -25,6 +25,9 @@ const String kTargetPlatform = 'TargetPlatform';
/// The define to control what target file is used. /// The define to control what target file is used.
const String kTargetFile = 'TargetFile'; 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. /// The define to control what iOS architectures are built for.
/// ///
/// This is expected to be a comma-separated list of architectures. If not /// 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 ...@@ -74,6 +77,7 @@ Future<void> compileAotElf(Map<String, ChangeType> updates, Environment environm
if (environment.defines[kTargetPlatform] == null) { if (environment.defines[kTargetPlatform] == null) {
throw MissingDefineException(kTargetPlatform, 'aot_elf'); throw MissingDefineException(kTargetPlatform, 'aot_elf');
} }
final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]); final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
final int snapshotExitCode = await snapshotter.build( final int snapshotExitCode = await snapshotter.build(
...@@ -82,6 +86,7 @@ Future<void> compileAotElf(Map<String, ChangeType> updates, Environment environm ...@@ -82,6 +86,7 @@ Future<void> compileAotElf(Map<String, ChangeType> updates, Environment environm
mainPath: environment.buildDir.childFile('main.app.dill').path, mainPath: environment.buildDir.childFile('main.app.dill').path,
packagesPath: environment.projectDir.childFile('.packages').path, packagesPath: environment.projectDir.childFile('.packages').path,
outputPath: outputPath, outputPath: outputPath,
bitcode: false,
); );
if (snapshotExitCode != 0) { if (snapshotExitCode != 0) {
throw Exception('AOT snapshotter exited with code $snapshotExitCode'); throw Exception('AOT snapshotter exited with code $snapshotExitCode');
...@@ -126,6 +131,7 @@ Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment env ...@@ -126,6 +131,7 @@ Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment env
if (targetPlatform != TargetPlatform.ios) { if (targetPlatform != TargetPlatform.ios) {
throw Exception('aot_assembly is only supported for iOS applications'); 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 we're building for a single architecture (common), then skip the lipo.
if (iosArchs.length == 1) { if (iosArchs.length == 1) {
...@@ -136,6 +142,7 @@ Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment env ...@@ -136,6 +142,7 @@ Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment env
packagesPath: environment.projectDir.childFile('.packages').path, packagesPath: environment.projectDir.childFile('.packages').path,
outputPath: outputPath, outputPath: outputPath,
iosArch: iosArchs.single, iosArch: iosArchs.single,
bitcode: bitcode,
); );
if (snapshotExitCode != 0) { if (snapshotExitCode != 0) {
throw Exception('AOT snapshotter exited with code $snapshotExitCode'); throw Exception('AOT snapshotter exited with code $snapshotExitCode');
...@@ -152,6 +159,7 @@ Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment env ...@@ -152,6 +159,7 @@ Future<void> compileAotAssembly(Map<String, ChangeType> updates, Environment env
packagesPath: environment.projectDir.childFile('.packages').path, packagesPath: environment.projectDir.childFile('.packages').path,
outputPath: fs.path.join(outputPath, getNameForIOSArch(iosArch)), outputPath: fs.path.join(outputPath, getNameForIOSArch(iosArch)),
iosArch: iosArch, iosArch: iosArch,
bitcode: bitcode,
)); ));
} }
final List<int> results = await Future.wait(pending); final List<int> results = await Future.wait(pending);
......
...@@ -4,14 +4,18 @@ ...@@ -4,14 +4,18 @@
import 'dart:async'; import 'dart:async';
import '../artifacts.dart';
import '../base/build.dart'; import '../base/build.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../dart/package_map.dart'; import '../dart/package_map.dart';
import '../globals.dart'; import '../globals.dart';
import '../ios/ios_workflow.dart';
import '../macos/xcode.dart';
import '../resident_runner.dart'; import '../resident_runner.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import 'build.dart'; import 'build.dart';
...@@ -52,6 +56,11 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen ...@@ -52,6 +56,11 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
..addMultiOption(FlutterOptions.kExtraGenSnapshotOptions, ..addMultiOption(FlutterOptions.kExtraGenSnapshotOptions,
splitCommas: true, splitCommas: true,
hide: 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 ...@@ -68,8 +77,16 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
if (platform == null) if (platform == null)
throwToolExit('Unknown platform: $targetPlatform'); throwToolExit('Unknown platform: $targetPlatform');
final bool bitcode = argResults['bitcode'];
final BuildMode buildMode = getBuildMode(); final BuildMode buildMode = getBuildMode();
if (bitcode) {
if (platform != TargetPlatform.ios) {
throwToolExit('Bitcode is only supported on iOS (TargetPlatform is $targetPlatform).');
}
await validateBitcode();
}
Status status; Status status;
if (!argResults['quiet']) { if (!argResults['quiet']) {
final String typeName = artifacts.getEngineType(platform, buildMode); final String typeName = artifacts.getEngineType(platform, buildMode);
...@@ -118,6 +135,7 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen ...@@ -118,6 +135,7 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
packagesPath: PackageMap.globalPackagesPath, packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath, outputPath: outputPath,
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
bitcode: bitcode,
).then<int>((int buildExitCode) { ).then<int>((int buildExitCode) {
return buildExitCode; return buildExitCode;
}); });
...@@ -131,8 +149,15 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen ...@@ -131,8 +149,15 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
'lipo', 'lipo',
...dylibs, ...dylibs,
'-create', '-create',
'-output', '-output', fs.path.join(outputPath, 'App.framework', 'App'),
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 { } else {
status?.cancel(); status?.cancel();
...@@ -150,6 +175,7 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen ...@@ -150,6 +175,7 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
packagesPath: PackageMap.globalPackagesPath, packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath, outputPath: outputPath,
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
bitcode: false,
); );
if (snapshotExitCode != 0) { if (snapshotExitCode != 0) {
status?.cancel(); status?.cancel();
...@@ -176,3 +202,38 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen ...@@ -176,3 +202,38 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
return null; 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) { ...@@ -469,6 +469,18 @@ String readGeneratedXcconfig(String appPath) {
} }
Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result) async { 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 && if (result.xcodeBuildExecution != null &&
result.xcodeBuildExecution.buildForPhysicalDevice && result.xcodeBuildExecution.buildForPhysicalDevice &&
result.stdout?.contains('BCEROR') == true && result.stdout?.contains('BCEROR') == true &&
......
...@@ -97,6 +97,18 @@ class Xcode { ...@@ -97,6 +97,18 @@ class Xcode {
return runCheckedAsync(<String>['xcrun', 'clang', ...args]); 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() { String getSimulatorPath() {
if (xcodeSelectPath == null) if (xcodeSelectPath == null)
return null; return null;
......
...@@ -116,6 +116,13 @@ void main() { ...@@ -116,6 +116,13 @@ void main() {
when(mockArtifacts.getArtifactPath(Artifact.snapshotDart, when(mockArtifacts.getArtifactPath(Artifact.snapshotDart,
platform: anyNamed('platform'), mode: mode)).thenReturn(kSnapshotDart); 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>{ final Map<Type, Generator> contextOverrides = <Type, Generator>{
...@@ -135,6 +142,7 @@ void main() { ...@@ -135,6 +142,7 @@ void main() {
mainPath: 'main.dill', mainPath: 'main.dill',
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
bitcode: false,
), isNot(equals(0))); ), isNot(equals(0)));
}, overrides: contextOverrides); }, overrides: contextOverrides);
...@@ -146,6 +154,7 @@ void main() { ...@@ -146,6 +154,7 @@ void main() {
mainPath: 'main.dill', mainPath: 'main.dill',
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
bitcode: false,
), isNot(0)); ), isNot(0));
}, overrides: contextOverrides); }, overrides: contextOverrides);
...@@ -157,17 +166,69 @@ void main() { ...@@ -157,17 +166,69 @@ void main() {
mainPath: 'main.dill', mainPath: 'main.dill',
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
bitcode: false,
), isNot(0)); ), isNot(0));
}, overrides: contextOverrides); }, 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 { testUsingContext('builds iOS armv7 profile AOT snapshot', () async {
fs.file('main.dill').writeAsStringSync('binary magic'); fs.file('main.dill').writeAsStringSync('binary magic');
final String outputPath = fs.path.join('build', 'foo'); final String outputPath = fs.path.join('build', 'foo');
fs.directory(outputPath).createSync(recursive: true); fs.directory(outputPath).createSync(recursive: true);
final String assembly = fs.path.join(outputPath, 'snapshot_assembly.S');
genSnapshot.outputs = <String, String>{ 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...']); final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
...@@ -181,6 +242,7 @@ void main() { ...@@ -181,6 +242,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
iosArch: IOSArch.armv7, iosArch: IOSArch.armv7,
bitcode: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
...@@ -190,11 +252,20 @@ void main() { ...@@ -190,11 +252,20 @@ void main() {
expect(genSnapshot.additionalArgs, <String>[ expect(genSnapshot.additionalArgs, <String>[
'--deterministic', '--deterministic',
'--snapshot_kind=app-aot-assembly', '--snapshot_kind=app-aot-assembly',
'--assembly=${fs.path.join(outputPath, 'snapshot_assembly.S')}', '--assembly=$assembly',
'--no-sim-use-hardfp', '--no-sim-use-hardfp',
'--no-use-integer-division', '--no-use-integer-division',
'main.dill', '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); }, overrides: contextOverrides);
testUsingContext('builds iOS arm64 profile AOT snapshot', () async { testUsingContext('builds iOS arm64 profile AOT snapshot', () async {
...@@ -218,6 +289,7 @@ void main() { ...@@ -218,6 +289,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
iosArch: IOSArch.arm64, iosArch: IOSArch.arm64,
bitcode: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
...@@ -253,6 +325,7 @@ void main() { ...@@ -253,6 +325,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
iosArch: IOSArch.armv7, iosArch: IOSArch.armv7,
bitcode: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
...@@ -290,6 +363,7 @@ void main() { ...@@ -290,6 +363,7 @@ void main() {
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
iosArch: IOSArch.arm64, iosArch: IOSArch.arm64,
bitcode: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
...@@ -316,6 +390,7 @@ void main() { ...@@ -316,6 +390,7 @@ void main() {
mainPath: 'main.dill', mainPath: 'main.dill',
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
bitcode: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
...@@ -345,6 +420,7 @@ void main() { ...@@ -345,6 +420,7 @@ void main() {
mainPath: 'main.dill', mainPath: 'main.dill',
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
bitcode: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
...@@ -380,6 +456,7 @@ void main() { ...@@ -380,6 +456,7 @@ void main() {
mainPath: 'main.dill', mainPath: 'main.dill',
packagesPath: '.packages', packagesPath: '.packages',
outputPath: outputPath, outputPath: outputPath,
bitcode: false,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
......
...@@ -5,12 +5,14 @@ ...@@ -5,12 +5,14 @@
import 'package:flutter_tools/src/base/build.dart'; import 'package:flutter_tools/src/base/build.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.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_info.dart';
import 'package:flutter_tools/src/build_system/build_system.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/exceptions.dart';
import 'package:flutter_tools/src/build_system/targets/dart.dart'; import 'package:flutter_tools/src/build_system/targets/dart.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/compile.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:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
...@@ -26,6 +28,7 @@ void main() { ...@@ -26,6 +28,7 @@ void main() {
Environment androidEnvironment; Environment androidEnvironment;
Environment iosEnvironment; Environment iosEnvironment;
MockProcessManager mockProcessManager; MockProcessManager mockProcessManager;
MockXcode mockXcode;
setUpAll(() { setUpAll(() {
Cache.disableLocking(); Cache.disableLocking();
...@@ -33,6 +36,7 @@ void main() { ...@@ -33,6 +36,7 @@ void main() {
setUp(() { setUp(() {
mockProcessManager = MockProcessManager(); mockProcessManager = MockProcessManager();
mockXcode = MockXcode();
testbed = Testbed(setup: () { testbed = Testbed(setup: () {
androidEnvironment = Environment( androidEnvironment = Environment(
projectDir: fs.currentDirectory, projectDir: fs.currentDirectory,
...@@ -153,8 +157,70 @@ flutter_tools:lib/'''); ...@@ -153,8 +157,70 @@ flutter_tools:lib/''');
expect(result.exceptions.values.single.exception, isInstanceOf<Exception>()); 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 { 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 { when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
fs.file(fs.path.join(iosEnvironment.buildDir.path, 'App.framework', 'App')) fs.file(fs.path.join(iosEnvironment.buildDir.path, 'App.framework', 'App'))
.createSync(recursive: true); .createSync(recursive: true);
...@@ -175,18 +241,26 @@ flutter_tools:lib/'''); ...@@ -175,18 +241,26 @@ flutter_tools:lib/''');
class MockProcessManager extends Mock implements ProcessManager {} class MockProcessManager extends Mock implements ProcessManager {}
class MockXcode extends Mock implements Xcode {}
class FakeGenSnapshot implements GenSnapshot { class FakeGenSnapshot implements GenSnapshot {
List<String> lastCallAdditionalArgs;
@override @override
Future<int> run({SnapshotType snapshotType, IOSArch iosArch, Iterable<String> additionalArgs = const <String>[]}) async { 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) { if (iosArch == null) {
out.childFile('app.so').createSync(); out.childFile('app.so').createSync();
out.childFile('gen_snapshot.d').createSync(); out.childFile('gen_snapshot.d').createSync();
return 0; return 0;
} }
out.childDirectory('App.framework').childFile('App').createSync(recursive: true); 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; 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'; ...@@ -12,6 +12,7 @@ import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/usage.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
...@@ -159,11 +160,35 @@ void main() { ...@@ -159,11 +160,35 @@ void main() {
group('Diagnose Xcode build failure', () { group('Diagnose Xcode build failure', () {
Map<String, String> buildSettings; Map<String, String> buildSettings;
MockUsage mockUsage;
setUp(() { setUp(() {
buildSettings = <String, String>{ buildSettings = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'test.app', '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 { testUsingContext('No provisioning profile shows message', () async {
...@@ -379,3 +404,5 @@ Could not build the precompiled application for the device.''', ...@@ -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) { ...@@ -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) { void expectExists(FileSystemEntity entity) {
expect(entity.existsSync(), isTrue); expect(entity.existsSync(), isTrue);
} }
......
...@@ -141,3 +141,14 @@ const Timeout allowForRemotePubInvocation = Timeout.factor(10.0); ...@@ -141,3 +141,14 @@ const Timeout allowForRemotePubInvocation = Timeout.factor(10.0);
/// Test case timeout for tests involving creating a Flutter project with /// Test case timeout for tests involving creating a Flutter project with
/// `--no-pub`. Use [allowForRemotePubInvocation] when creation involves `pub`. /// `--no-pub`. Use [allowForRemotePubInvocation] when creation involves `pub`.
const Timeout allowForCreateFlutterProject = Timeout.factor(3.0); 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 { ...@@ -606,4 +606,7 @@ class FakeProcessResult implements ProcessResult {
@override @override
final dynamic stdout; 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