Support multi-arch iOS binaries (#17312)

This change adds support for armv7, arm64, and universal iOS apps.

This change eliminates iOS target architecture hardcoding (previously
arm64 only) and uses the target architecture(s) specified in Xcode's
ARCHS setting ('Architectures' in Xcode Build Settings).

For universal binaries, set ARCHS to its default value, $(ARCHS_STANDARD).

Note that after changing the architecture in Xcode, developers should
run 'pod install' from the ios subdirectory of their project. A separate
change (that will land before this one) will add support for
automatically detecting project file and Podfile changes and re-running
pod install if necessary.

This change also adds an --ios-arch option to flutter build aot. In iOS
AOT builds (in profile and release mode), this dictates which
architectures are built into App.framework. This flag should generally
be unnecessary to set manually since flutter build aot is typically only
invoked internally by flutter itself.
diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh
index a42537c..3d6e955 100755
--- a/packages/flutter_tools/bin/xcode_backend.sh
+++ b/packages/flutter_tools/bin/xcode_backend.sh
@@ -111,6 +111,8 @@
 
   if [[ "${build_mode}" != "debug" ]]; then
     StreamOutput " ├─Building Dart code..."
+    # Transform ARCHS to comma-separated list of target architectures.
+    local archs="${ARCHS// /,}"
     RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics           \
       ${verbose_flag}                                                       \
       build aot                                                             \
@@ -118,6 +120,7 @@
       --target-platform=ios                                                 \
       --target="${target_path}"                                             \
       --${build_mode}                                                       \
+      --ios-arch="${archs}"                                                 \
       ${local_engine_flag}                                                  \
       ${preview_dart_2_flag}
 
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index be6f94c..d14cfa6 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -40,6 +40,7 @@
     @required SnapshotType snapshotType,
     @required String packagesPath,
     @required String depfilePath,
+    IOSArch iosArch,
     Iterable<String> additionalArgs: const <String>[],
   }) {
     final List<String> args = <String>[
@@ -56,8 +57,8 @@
     // code. /usr/bin/arch can be used to run binaries with the specified
     // architecture.
     if (snapshotType.platform == TargetPlatform.ios) {
-      // TODO(cbracken): for the moment, always generate only arm64 code.
-      return runCommandAndStreamOutput(<String>['/usr/bin/arch', '-x86_64', snapshotterPath]..addAll(args));
+      final String hostArch = iosArch == IOSArch.armv7 ? '-i386' : '-x86_64';
+      return runCommandAndStreamOutput(<String>['/usr/bin/arch', hostArch, snapshotterPath]..addAll(args));
     }
     return runCommandAndStreamOutput(<String>[snapshotterPath]..addAll(args));
   }
@@ -137,12 +138,15 @@
     @required String outputPath,
     @required bool previewDart2,
     @required bool preferSharedLibrary,
+    IOSArch iosArch,
     List<String> extraGenSnapshotOptions: const <String>[],
   }) async {
     if (!_isValidAotPlatform(platform, buildMode)) {
       printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
       return -1;
     }
+    // TODO(cbracken): replace IOSArch with TargetPlatform.ios_{armv7,arm64}.
+    assert(platform != TargetPlatform.ios || iosArch != null);
 
     final bool compileToSharedLibrary = preferSharedLibrary && androidSdk.ndkCompiler != null;
     if (preferSharedLibrary && !compileToSharedLibrary) {
@@ -215,7 +219,7 @@
       ]);
     }
 
-    if (platform == TargetPlatform.android_arm) {
+    if (platform == TargetPlatform.android_arm || iosArch == IOSArch.armv7) {
       // Not supported by the Pixel in 32-bit mode.
       genSnapshotArgs.add('--no-use-integer-division');
     }
@@ -253,6 +257,7 @@
       packagesPath: packageMap.packagesPath,
       depfilePath: depfilePath,
       additionalArgs: genSnapshotArgs,
+      iosArch: iosArch,
     );
     if (genSnapshotExitCode != 0) {
       printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
@@ -266,7 +271,7 @@
     // 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(assemblyPath: assembly, outputPath: outputDir.path);
+      final RunResult result = await _buildIosFramework(iosArch: iosArch, assemblyPath: assembly, outputPath: outputDir.path);
       if (result.exitCode != 0)
         return result.exitCode;
     } else if (compileToSharedLibrary) {
@@ -283,11 +288,13 @@
   /// Builds an iOS framework at [outputPath]/App.framework from the assembly
   /// source at [assemblyPath].
   Future<RunResult> _buildIosFramework({
+    @required IOSArch iosArch,
     @required String assemblyPath,
     @required String outputPath,
   }) async {
-    printStatus('Building App.framework...');
-    const List<String> commonBuildOptions = const <String>['-arch', 'arm64', '-miphoneos-version-min=8.0'];
+    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(commonBuildOptions.toList()..addAll(<String>['-c', assemblyPath, '-o', assemblyO]));
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 82e7dea..2472b9d 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -160,6 +160,41 @@
   tester,
 }
 
+/// iOS target device architecture.
+//
+// TODO(cbracken): split TargetPlatform.ios into ios_armv7, ios_arm64.
+enum IOSArch {
+  armv7,
+  arm64,
+}
+
+/// The default set of iOS device architectures to build for.
+const List<IOSArch> defaultIOSArchs = const <IOSArch>[
+  IOSArch.arm64,
+];
+
+String getNameForIOSArch(IOSArch arch) {
+  switch (arch) {
+    case IOSArch.armv7:
+      return 'armv7';
+    case IOSArch.arm64:
+      return 'arm64';
+  }
+  assert(false);
+  return null;
+}
+
+IOSArch getIOSArchForName(String arch) {
+  switch (arch) {
+    case 'armv7':
+      return IOSArch.armv7;
+    case 'arm64':
+      return IOSArch.arm64;
+  }
+  assert(false);
+  return null;
+}
+
 String getNameForTargetPlatform(TargetPlatform platform) {
   switch (platform) {
     case TargetPlatform.android_arm:
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index ee36b69..d67c2a6 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -8,6 +8,7 @@
 import '../base/common.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';
@@ -32,6 +33,12 @@
         hide: !verboseHelp,
         help: 'Preview Dart 2.0 functionality.',
       )
+      ..addMultiOption('ios-arch',
+        splitCommas: true,
+        defaultsTo: defaultIOSArchs.map(getNameForIOSArch),
+        allowed: IOSArch.values.map(getNameForIOSArch),
+        help: 'iOS architectures to build',
+      )
       ..addMultiOption(FlutterOptions.kExtraFrontEndOptions,
         splitCommas: true,
         hide: true,
@@ -90,18 +97,57 @@
       }
 
       // Build AOT snapshot.
-      final int snapshotExitCode = await snapshotter.build(
-        platform: platform,
-        buildMode: buildMode,
-        mainPath: mainPath,
-        packagesPath: PackageMap.globalPackagesPath,
-        outputPath: outputPath,
-        previewDart2: previewDart2,
-        preferSharedLibrary: argResults['prefer-shared-library'],
-        extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
-      );
-      if (snapshotExitCode != 0) {
-        printError('Snapshotting exited with non-zero exit code: $snapshotExitCode');
+      if (platform == TargetPlatform.ios) {
+        // Determine which iOS architectures to build for.
+        final Iterable<IOSArch> buildArchs = argResults['ios-arch'].map(getIOSArchForName);
+        final Map<IOSArch, String> iosBuilds = <IOSArch, String>{};
+        for (IOSArch arch in buildArchs)
+          iosBuilds[arch] = fs.path.join(outputPath, getNameForIOSArch(arch));
+
+        // Generate AOT snapshot and compile to arch-specific App.framework.
+        final Map<IOSArch, Future<int>> exitCodes = <IOSArch, Future<int>>{};
+        iosBuilds.forEach((IOSArch iosArch, String outputPath) {
+          exitCodes[iosArch] = snapshotter.build(
+            platform: platform,
+            iosArch: iosArch,
+            buildMode: buildMode,
+            mainPath: mainPath,
+            packagesPath: PackageMap.globalPackagesPath,
+            outputPath: outputPath,
+            previewDart2: previewDart2,
+            preferSharedLibrary: false,
+            extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
+          ).then((int buildExitCode) {
+            if (buildExitCode != 0)
+              printError('Snapshotting ($iosArch) exited with non-zero exit code: $buildExitCode');
+            return buildExitCode;
+          });
+        });
+
+        // Merge arch-specific App.frameworks into a multi-arch App.framework.
+        if ((await Future.wait(exitCodes.values)).every((int buildExitCode) => buildExitCode == 0)) {
+          final Iterable<String> dylibs = iosBuilds.values.map((String outputDir) => fs.path.join(outputDir, 'App.framework', 'App'));
+          fs.directory(fs.path.join(outputPath, 'App.framework'))..createSync();
+          await runCheckedAsync(<String>['lipo']
+            ..addAll(dylibs)
+            ..addAll(<String>['-create', '-output', fs.path.join(outputPath, 'App.framework', 'App')]),
+          );
+        }
+      } else {
+        // Android AOT snapshot.
+        final int snapshotExitCode = await snapshotter.build(
+          platform: platform,
+          buildMode: buildMode,
+          mainPath: mainPath,
+          packagesPath: PackageMap.globalPackagesPath,
+          outputPath: outputPath,
+          previewDart2: previewDart2,
+          preferSharedLibrary: argResults['prefer-shared-library'],
+          extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
+        );
+        if (snapshotExitCode != 0) {
+          printError('Snapshotting exited with non-zero exit code: $snapshotExitCode');
+        }
       }
     } on String catch (error) {
       // Catch the String exceptions thrown from the `runCheckedSync` methods below.
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 08333f1..2248932 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -325,7 +325,6 @@
     'xcrun',
     'xcodebuild',
     '-configuration', configuration,
-    'ONLY_ACTIVE_ARCH=YES',
   ];
 
   if (logger.isVerbose) {
@@ -358,7 +357,7 @@
   }
 
   if (buildForDevice) {
-    buildCommands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']);
+    buildCommands.addAll(<String>['-sdk', 'iphoneos']);
   } else {
     buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
   }
diff --git a/packages/flutter_tools/test/base/build_test.dart b/packages/flutter_tools/test/base/build_test.dart
index a3a9a0e..39244a2 100644
--- a/packages/flutter_tools/test/base/build_test.dart
+++ b/packages/flutter_tools/test/base/build_test.dart
@@ -52,6 +52,7 @@
     SnapshotType snapshotType,
     String packagesPath,
     String depfilePath,
+    IOSArch iosArch,
     Iterable<String> additionalArgs,
   }) async {
     _callCount += 1;
@@ -361,6 +362,7 @@
         outputPath: outputPath,
         preferSharedLibrary: false,
         previewDart2: true,
+        iosArch: IOSArch.arm64,
       );
 
       expect(genSnapshotExitCode, 0);
@@ -407,6 +409,7 @@
         outputPath: outputPath,
         preferSharedLibrary: false,
         previewDart2: true,
+        iosArch: IOSArch.arm64,
       );
 
       expect(genSnapshotExitCode, 0);