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();
 }