[flutter_tools] Reland flutter assemble for iOS (#50229)

diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart
index 04503ef..53413cc 100644
--- a/dev/devicelab/bin/tasks/module_test_ios.dart
+++ b/dev/devicelab/bin/tasks/module_test_ios.dart
@@ -270,10 +270,10 @@
       final String objectiveCAnalyticsOutput = objectiveCAnalyticsOutputFile.readAsStringSync();
       if (!objectiveCAnalyticsOutput.contains('cd24: ios')
           || !objectiveCAnalyticsOutput.contains('cd25: true')
-          || !objectiveCAnalyticsOutput.contains('viewName: build/bundle')) {
+          || !objectiveCAnalyticsOutput.contains('viewName: assemble')) {
         return TaskResult.failure(
           'Building outer Objective-C app produced the following analytics: "$objectiveCAnalyticsOutput" '
-          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: build/bundle"'
+          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: assemble"'
         );
       }
 
@@ -359,10 +359,10 @@
       final String swiftAnalyticsOutput = swiftAnalyticsOutputFile.readAsStringSync();
       if (!swiftAnalyticsOutput.contains('cd24: ios')
           || !swiftAnalyticsOutput.contains('cd25: true')
-          || !swiftAnalyticsOutput.contains('viewName: build/bundle')) {
+          || !swiftAnalyticsOutput.contains('viewName: assemble')) {
         return TaskResult.failure(
           'Building outer Swift app produced the following analytics: "$swiftAnalyticsOutput" '
-          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: build/bundle"'
+          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: assemble"'
         );
       }
 
diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh
index 0022992..aa5c224 100755
--- a/packages/flutter_tools/bin/xcode_backend.sh
+++ b/packages/flutter_tools/bin/xcode_backend.sh
@@ -51,6 +51,10 @@
     derived_dir="${project_path}/.ios/Flutter"
   fi
 
+  RunCommand mkdir -p -- "$derived_dir"
+  AssertExists "$derived_dir"
+  RunCommand rm -rf -- "${derived_dir}/App.framework"
+
   # Default value of assets_path is flutter_assets
   local assets_path="flutter_assets"
   # The value of assets_path can set by add FLTAssetsPath to AppFrameworkInfo.plist
@@ -96,15 +100,6 @@
   fi
 
   local framework_path="${FLUTTER_ROOT}/bin/cache/artifacts/engine/${artifact_variant}"
-
-  AssertExists "${framework_path}"
-  AssertExists "${project_path}"
-
-  RunCommand mkdir -p -- "$derived_dir"
-  AssertExists "$derived_dir"
-
-  RunCommand rm -rf -- "${derived_dir}/App.framework"
-
   local flutter_engine_flag=""
   local local_engine_flag=""
   local flutter_framework="${framework_path}/Flutter.framework"
@@ -134,9 +129,10 @@
 
   local bitcode_flag=""
   if [[ $ENABLE_BITCODE == "YES" ]]; then
-    bitcode_flag="--bitcode"
+    bitcode_flag="true"
   fi
 
+  # TODO(jonahwilliams): move engine copying to build system.
   if [[ -e "${project_path}/.ios" ]]; then
     RunCommand rm -rf -- "${derived_dir}/engine"
     mkdir "${derived_dir}/engine"
@@ -150,102 +146,29 @@
 
   RunCommand pushd "${project_path}" > /dev/null
 
-  AssertExists "${target_path}"
-
   local verbose_flag=""
   if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
     verbose_flag="--verbose"
   fi
 
-  local build_dir="${FLUTTER_BUILD_DIR:-build}"
-
   local track_widget_creation_flag=""
   if [[ -n "$TRACK_WIDGET_CREATION" ]]; then
-    track_widget_creation_flag="--track-widget-creation"
+    track_widget_creation_flag="true"
   fi
 
-  if [[ "${build_mode}" != "debug" ]]; then
-    StreamOutput " ├─Building Dart code..."
-    # Transform ARCHS to comma-separated list of target architectures.
-    local archs="${ARCHS// /,}"
-    if [[ $archs =~ .*i386.* || $archs =~ .*x86_64.* ]]; then
-      EchoError "========================================================================"
-      EchoError "ERROR: Flutter does not support running in profile or release mode on"
-      EchoError "the Simulator (this build was: '$build_mode')."
-      EchoError "You can ensure Flutter runs in Debug mode with your host app in release"
-      EchoError "mode by setting FLUTTER_BUILD_MODE=debug in the .xcconfig associated"
-      EchoError "with the ${CONFIGURATION} build configuration."
-      EchoError "========================================================================"
-      exit -1
-    fi
-
-    RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics           \
-      ${verbose_flag}                                                       \
-      build aot                                                             \
-      --output-dir="${build_dir}/aot"                                       \
-      --target-platform=ios                                                 \
-      --target="${target_path}"                                             \
-      --${build_mode}                                                       \
-      --ios-arch="${archs}"                                                 \
-      ${flutter_engine_flag}                                                \
-      ${local_engine_flag}                                                  \
-      ${bitcode_flag}
-
-    if [[ $? -ne 0 ]]; then
-      EchoError "Failed to build ${project_path}."
-      exit -1
-    fi
-    StreamOutput "done"
-
-    local app_framework="${build_dir}/aot/App.framework"
-
-    RunCommand cp -r -- "${app_framework}" "${derived_dir}"
-
-  else
-    RunCommand mkdir -p -- "${derived_dir}/App.framework"
-
-    # Build stub for all requested architectures.
-    local arch_flags=""
-    read -r -a archs <<< "$ARCHS"
-    for arch in "${archs[@]}"; do
-      arch_flags="${arch_flags}-arch $arch "
-    done
-
-    RunCommand eval "$(echo "static const int Moo = 88;" | xcrun clang -x c \
-        ${arch_flags} \
-        -fembed-bitcode-marker \
-        -dynamiclib \
-        -Xlinker -rpath -Xlinker '@executable_path/Frameworks' \
-        -Xlinker -rpath -Xlinker '@loader_path/Frameworks' \
-        -install_name '@rpath/App.framework/App' \
-        -o "${derived_dir}/App.framework/App" -)"
-  fi
-
-  local plistPath="${project_path}/ios/Flutter/AppFrameworkInfo.plist"
-  if [[ -e "${project_path}/.ios" ]]; then
-    plistPath="${project_path}/.ios/Flutter/AppFrameworkInfo.plist"
-  fi
-
-  RunCommand cp -- "$plistPath" "${derived_dir}/App.framework/Info.plist"
-
-  local precompilation_flag=""
-  if [[ "$CURRENT_ARCH" != "x86_64" ]] && [[ "$build_mode" != "debug" ]]; then
-    precompilation_flag="--precompiled"
-  fi
-
-  StreamOutput " ├─Assembling Flutter resources..."
-  RunCommand "${FLUTTER_ROOT}/bin/flutter"     \
-    ${verbose_flag}                                                         \
-    build bundle                                                            \
-    --target-platform=ios                                                   \
-    --target="${target_path}"                                               \
-    --${build_mode}                                                         \
-    --depfile="${build_dir}/snapshot_blob.bin.d"                            \
-    --asset-dir="${derived_dir}/App.framework/${assets_path}"               \
-    ${precompilation_flag}                                                  \
-    ${flutter_engine_flag}                                                  \
-    ${local_engine_flag}                                                    \
-    ${track_widget_creation_flag}
+  RunCommand "${FLUTTER_ROOT}/bin/flutter"                                \
+    ${verbose_flag}                                                       \
+    ${flutter_engine_flag}                                                \
+    ${local_engine_flag}                                                  \
+    assemble                                                              \
+    --output="${derived_dir}/"                                            \
+    -dTargetPlatform=ios                                                  \
+    -dTargetFile="${target_path}"                                         \
+    -dBuildMode=${build_mode}                                             \
+    -dIosArchs="${ARCHS}"                                                 \
+    -dTrackWidgetCreation="${track_widget_creation_flag}"                 \
+    -dEnableBitcode="${bitcode_flag}"                                     \
+    "${build_mode}_ios_bundle_flutter_assets"
 
   if [[ $? -ne 0 ]]; then
     EchoError "Failed to package ${project_path}."
diff --git a/packages/flutter_tools/lib/src/aot.dart b/packages/flutter_tools/lib/src/aot.dart
index 7465654..24d3b84 100644
--- a/packages/flutter_tools/lib/src/aot.dart
+++ b/packages/flutter_tools/lib/src/aot.dart
@@ -12,14 +12,9 @@
 import 'base/logger.dart';
 import 'base/process.dart';
 import 'build_info.dart';
-import 'build_system/build_system.dart';
-import 'build_system/targets/dart.dart';
-import 'build_system/targets/ios.dart';
-import 'cache.dart';
 import 'dart/package_map.dart';
 import 'globals.dart' as globals;
 import 'ios/bitcode.dart';
-import 'project.dart';
 
 /// Builds AOT snapshots given a platform, build mode and a path to a Dart
 /// library.
@@ -42,21 +37,6 @@
       throwToolExit('No AOT build platform specified');
     }
 
-    if (_canUseAssemble(platform)
-        && extraGenSnapshotOptions?.isEmpty != false
-        && extraFrontEndOptions?.isEmpty != false) {
-      await _buildWithAssemble(
-        targetFile: mainDartFile,
-        outputDir: outputPath,
-        targetPlatform: platform,
-        buildMode: buildMode,
-        quiet: quiet,
-        iosArchs: iosBuildArchs ?? defaultIOSArchs,
-        bitcode: bitcode ?? kBitcodeEnabledDefault,
-      );
-      return;
-    }
-
     if (bitcode) {
       if (platform != TargetPlatform.ios) {
         throwToolExit('Bitcode is only supported on iOS (TargetPlatform is $platform).');
@@ -174,81 +154,4 @@
     }
     return;
   }
-
-  bool _canUseAssemble(TargetPlatform targetPlatform) {
-    switch (targetPlatform) {
-      case TargetPlatform.ios:
-        return true;
-      case TargetPlatform.android_arm:
-      case TargetPlatform.android_arm64:
-      case TargetPlatform.android_x86:
-      case TargetPlatform.darwin_x64:
-      case TargetPlatform.android_x64:
-      case TargetPlatform.linux_x64:
-      case TargetPlatform.windows_x64:
-      case TargetPlatform.fuchsia_arm64:
-      case TargetPlatform.fuchsia_x64:
-      case TargetPlatform.tester:
-      case TargetPlatform.web_javascript:
-      default:
-        return false;
-    }
-  }
-
-  Future<void> _buildWithAssemble({
-    TargetPlatform targetPlatform,
-    BuildMode buildMode,
-    String targetFile,
-    String outputDir,
-    bool quiet,
-    Iterable<DarwinArch> iosArchs,
-    bool bitcode,
-  }) async {
-    Status status;
-    if (!quiet) {
-      final String typeName = globals.artifacts.getEngineType(targetPlatform, buildMode);
-      status = globals.logger.startProgress(
-        'Building AOT snapshot in ${getFriendlyModeName(buildMode)} mode ($typeName)...',
-        timeout: timeoutConfiguration.slowOperation,
-      );
-    }
-    final FlutterProject flutterProject = FlutterProject.current();
-    final Target target = buildMode == BuildMode.profile
-      ? const AotAssemblyProfile()
-      : const AotAssemblyRelease();
-
-    final BuildResult result = await buildSystem.build(target, Environment(
-      projectDir: flutterProject.directory,
-      cacheDir: globals.cache.getRoot(),
-      flutterRootDir: globals.fs.directory(Cache.flutterRoot),
-      outputDir: globals.fs.directory(outputDir),
-      buildDir: flutterProject.directory
-        .childDirectory('.dart_tool')
-        .childDirectory('flutter_build'),
-      defines: <String, String>{
-        kBuildMode: getNameForBuildMode(buildMode),
-        kTargetPlatform: getNameForTargetPlatform(targetPlatform),
-        kTargetFile: targetFile,
-        kIosArchs: iosArchs.map(getNameForDarwinArch).join(','),
-        kBitcodeFlag: bitcode.toString()
-      }
-    ));
-    status?.stop();
-    if (!result.success) {
-      for (final ExceptionMeasurement measurement in result.exceptions.values) {
-        globals.printError('Target ${measurement.target} failed: ${measurement.exception}',
-          stackTrace: measurement.fatal
-            ? measurement.stackTrace
-            : null,
-        );
-      }
-      throwToolExit('Failed to build aot.');
-    }
-    final String builtMessage = 'Built to $outputDir${globals.fs.path.separator}.';
-    if (quiet) {
-      globals.printTrace(builtMessage);
-    } else {
-      globals.printStatus(builtMessage);
-    }
-  }
 }
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 545a869..3888954 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -385,6 +385,8 @@
       return DarwinArch.armv7;
     case 'arm64':
       return DarwinArch.arm64;
+    case 'x86_64':
+      return DarwinArch.x86_64;
   }
   assert(false);
   return null;
diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart
index 9b1ede5..a25b1ef 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart
@@ -11,8 +11,11 @@
 import '../../build_info.dart';
 import '../../globals.dart' as globals;
 import '../../macos/xcode.dart';
+import '../../project.dart';
 import '../build_system.dart';
+import '../depfile.dart';
 import '../exceptions.dart';
+import 'assets.dart';
 import 'dart.dart';
 
 /// Supports compiling a dart kernel file to an assembly file.
@@ -25,7 +28,7 @@
   @override
   Future<void> build(Environment environment) async {
     final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false);
-    final String buildOutputPath = environment.outputDir.path;
+    final String buildOutputPath = environment.buildDir.path;
     if (environment.defines[kBuildMode] == null) {
       throw MissingDefineException(kBuildMode, 'aot_assembly');
     }
@@ -35,8 +38,11 @@
     final bool bitcode = environment.defines[kBitcodeFlag] == 'true';
     final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
     final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
-    final List<DarwinArch> iosArchs = environment.defines[kIosArchs]?.split(',')?.map(getIOSArchForName)?.toList()
-        ?? <DarwinArch>[DarwinArch.arm64];
+    final List<DarwinArch> iosArchs = environment.defines[kIosArchs]
+      ?.split(' ')
+      ?.map(getIOSArchForName)
+      ?.toList()
+      ?? <DarwinArch>[DarwinArch.arm64];
     if (targetPlatform != TargetPlatform.ios) {
       throw Exception('aot_assembly is only supported for iOS applications');
     }
@@ -60,7 +66,7 @@
     if (results.any((int result) => result != 0)) {
       throw Exception('AOT snapshotter exited with code ${results.join()}');
     }
-    final String resultPath = globals.fs.path.join(environment.outputDir.path, 'App.framework', 'App');
+    final String resultPath = globals.fs.path.join(environment.buildDir.path, 'App.framework', 'App');
     globals.fs.directory(resultPath).parent.createSync(recursive: true);
     final ProcessResult result = await globals.processManager.run(<String>[
       'lipo',
@@ -145,12 +151,215 @@
   ];
 }
 
+/// Create a trivial App.framework file for debug iOS builds.
+class DebugUniveralFramework extends Target {
+  const DebugUniveralFramework();
+
+  @override
+  String get name => 'debug_universal_framework';
+
+  @override
+  List<Target> get dependencies => const <Target>[
+    KernelSnapshot(),
+  ];
+
+  @override
+  List<Source> get inputs => const <Source>[
+     Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
+  ];
+
+  @override
+  List<Source> get outputs => const <Source>[
+    Source.pattern('{BUILD_DIR}/App')
+  ];
+
+  @override
+  Future<void> build(Environment environment) async {
+    // Generate a trivial App.framework.
+    final Set<DarwinArch> iosArchs = environment.defines[kIosArchs]
+      ?.split(' ')
+      ?.map(getIOSArchForName)
+      ?.toSet()
+      ?? <DarwinArch>{DarwinArch.arm64};
+    final File iphoneFile = environment.buildDir.childFile('iphone_framework');
+    final File simulatorFile = environment.buildDir.childFile('simulator_framework');
+    final File lipoOutputFile = environment.buildDir.childFile('App');
+    final RunResult iphoneResult = await createStubAppFramework(
+      iphoneFile,
+      SdkType.iPhone,
+      // Only include 32bit if it is contained in the active architectures.
+      include32Bit: iosArchs.contains(DarwinArch.armv7)
+    );
+    final RunResult simulatorResult = await createStubAppFramework(
+      simulatorFile,
+      SdkType.iPhoneSimulator,
+    );
+    if (iphoneResult.exitCode != 0 || simulatorResult.exitCode != 0) {
+      throw Exception('Failed to create App.framework.');
+    }
+    final List<String> lipoCommand = <String>[
+      'xcrun',
+      'lipo',
+      '-create',
+      iphoneFile.path,
+      simulatorFile.path,
+      '-output',
+      lipoOutputFile.path
+    ];
+    final RunResult lipoResult = await processUtils.run(
+      lipoCommand,
+    );
+
+    if (lipoResult.exitCode != 0) {
+      throw Exception('Failed to create App.framework.');
+    }
+  }
+}
+
+/// The base class for all iOS bundle targets.
+///
+/// This is responsible for setting up the basic App.framework structure, including:
+/// * Copying the app.dill/kernel_blob.bin from the build directory to assets (debug)
+/// * Copying the precompiled isolate/vm data from the engine (debug)
+/// * Copying the flutter assets to App.framework/flutter_assets
+/// * Copying either the stub or real App assembly file to App.framework/App
+abstract class IosAssetBundle extends Target {
+  const IosAssetBundle();
+
+  @override
+  List<Target> get dependencies => const <Target>[
+    KernelSnapshot(),
+  ];
+
+  @override
+  List<Source> get inputs => const <Source>[
+    Source.pattern('{BUILD_DIR}/App'),
+    Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
+  ];
+
+  @override
+  List<Source> get outputs => const <Source>[
+    Source.pattern('{OUTPUT_DIR}/App.framework/App'),
+    Source.pattern('{OUTPUT_DIR}/App.framework/Info.plist')
+  ];
+
+  @override
+  List<String> get depfiles => <String>[
+    'flutter_assets.d',
+  ];
+
+  @override
+  Future<void> build(Environment environment) async {
+    if (environment.defines[kBuildMode] == null) {
+      throw MissingDefineException(kBuildMode, name);
+    }
+    final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
+    final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework');
+    final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets');
+    frameworkDirectory.createSync(recursive: true);
+    assetDirectory.createSync();
+
+    // Only copy the prebuilt runtimes and kernel blob in debug mode.
+    if (buildMode == BuildMode.debug) {
+      // Copy the App.framework to the output directory.
+      environment.buildDir.childFile('App')
+        .copySync(frameworkDirectory.childFile('App').path);
+
+      final String vmSnapshotData = globals.artifacts.getArtifactPath(Artifact.vmSnapshotData, mode: BuildMode.debug);
+      final String isolateSnapshotData = globals.artifacts.getArtifactPath(Artifact.isolateSnapshotData, mode: BuildMode.debug);
+      environment.buildDir.childFile('app.dill')
+          .copySync(assetDirectory.childFile('kernel_blob.bin').path);
+      globals.fs.file(vmSnapshotData)
+          .copySync(assetDirectory.childFile('vm_snapshot_data').path);
+      globals.fs.file(isolateSnapshotData)
+          .copySync(assetDirectory.childFile('isolate_snapshot_data').path);
+    } else {
+      environment.buildDir.childDirectory('App.framework').childFile('App')
+        .copySync(frameworkDirectory.childFile('App').path);
+    }
+
+    // Copy the assets.
+    final Depfile assetDepfile = await copyAssets(environment, assetDirectory);
+    assetDepfile.writeToFile(environment.buildDir.childFile('flutter_assets.d'));
+
+
+    // Copy the plist from either the project or module.
+    // TODO(jonahwilliams): add plist to inputs
+    final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
+    final Directory plistRoot = flutterProject.isModule
+      ? flutterProject.ios.ephemeralDirectory
+      : environment.projectDir.childDirectory('ios');
+    plistRoot
+      .childDirectory('Flutter')
+      .childFile('AppFrameworkInfo.plist')
+      .copySync(environment.outputDir
+      .childDirectory('App.framework')
+      .childFile('Info.plist').path);
+  }
+}
+
+/// Build a debug iOS application bundle.
+class DebugIosApplicationBundle extends IosAssetBundle {
+  const DebugIosApplicationBundle();
+
+  @override
+  String get name => 'debug_ios_bundle_flutter_assets';
+
+  @override
+  List<Source> get inputs => <Source>[
+    const Source.artifact(Artifact.vmSnapshotData, mode: BuildMode.debug),
+    const Source.artifact(Artifact.isolateSnapshotData, mode: BuildMode.debug),
+    const Source.pattern('{BUILD_DIR}/app.dill'),
+    ...super.inputs,
+  ];
+
+  @override
+  List<Source> get outputs => <Source>[
+    const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/vm_snapshot_data'),
+    const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/isolate_snapshot_data'),
+    const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/kernel_blob.bin'),
+    ...super.outputs,
+  ];
+
+  @override
+  List<Target> get dependencies => <Target>[
+    const DebugUniveralFramework(),
+    ...super.dependencies,
+  ];
+}
+
+/// Build a profile iOS application bundle.
+class ProfileIosApplicationBundle extends IosAssetBundle {
+  const ProfileIosApplicationBundle();
+
+  @override
+  String get name => 'profile_ios_bundle_flutter_assets';
+
+  @override
+  List<Target> get dependencies => const <Target>[
+    AotAssemblyProfile(),
+  ];
+}
+
+/// Build a release iOS application bundle.
+class ReleaseIosApplicationBundle extends IosAssetBundle {
+  const ReleaseIosApplicationBundle();
+
+  @override
+  String get name => 'release_ios_bundle_flutter_assets';
+
+  @override
+  List<Target> get dependencies => const <Target>[
+    AotAssemblyRelease(),
+  ];
+}
+
 /// Create an App.framework for debug iOS targets.
 ///
 /// This framework needs to exist for the Xcode project to link/bundle,
 /// but it isn't actually executed. To generate something valid, we compile a trivial
 /// constant.
-Future<RunResult> createStubAppFramework(File outputFile, SdkType sdk) async {
+Future<RunResult> createStubAppFramework(File outputFile, SdkType sdk, { bool include32Bit = true }) async {
   try {
     outputFile.createSync(recursive: true);
   } catch (e) {
@@ -167,8 +376,8 @@
     List<String> archFlags;
     if (sdk == SdkType.iPhone) {
       archFlags = <String>[
-        '-arch',
-        getNameForDarwinArch(DarwinArch.armv7),
+        if (include32Bit)
+          ...<String>['-arch', getNameForDarwinArch(DarwinArch.armv7)],
         '-arch',
         getNameForDarwinArch(DarwinArch.arm64),
       ];
diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart
index 4d89483..6a8ca21 100644
--- a/packages/flutter_tools/lib/src/commands/assemble.dart
+++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -50,6 +50,9 @@
   androidArmReleaseBundle,
   androidArm64ReleaseBundle,
   androidx64ReleaseBundle,
+  DebugIosApplicationBundle(),
+  ProfileIosApplicationBundle(),
+  ReleaseIosApplicationBundle(),
 ];
 
 /// Assemble provides a low level API to interact with the flutter tool build
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 baa0699..590887d 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
@@ -315,7 +315,7 @@
   }));
 
   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 {
       globals.fs.file(globals.fs.path.join(iosEnvironment.buildDir.path, 'App.framework', 'App'))
           .createSync(recursive: true);
@@ -360,7 +360,7 @@
   }));
 
   test('aot_assembly_profile with bitcode sends correct argument to snapshotter (mutli arch)', () => testbed.run(() async {
-    iosEnvironment.defines[kIosArchs] = 'armv7,arm64';
+    iosEnvironment.defines[kIosArchs] = 'armv7 arm64';
     iosEnvironment.defines[kBitcodeFlag] = 'true';
 
     final FakeProcessResult fakeProcessResult = FakeProcessResult(
@@ -377,9 +377,8 @@
     when(mockXcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(fakeRunResult));
     when(mockXcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(fakeRunResult));
 
-    final BuildResult result = await buildSystem.build(const AotAssemblyProfile(), iosEnvironment);
+    await const AotAssemblyProfile().build(iosEnvironment);
 
-    expect(result.success, true);
     verify(mockXcode.cc(argThat(contains('-fembed-bitcode')))).called(2);
     verify(mockXcode.clang(argThat(contains('-fembed-bitcode')))).called(2);
   }, overrides: <Type, Generator>{
@@ -388,7 +387,7 @@
   }));
 
   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 {
       globals.fs.file(globals.fs.path.join(iosEnvironment.buildDir.path, 'App.framework', 'App'))
           .createSync(recursive: true);
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart
new file mode 100644
index 0000000..3a610c2
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart
@@ -0,0 +1,165 @@
+// Copyright 2014 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_testing/file_testing.dart';
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/file_system.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/targets/dart.dart';
+import 'package:flutter_tools/src/build_system/targets/ios.dart';
+import 'package:flutter_tools/src/globals.dart' as globals;
+import 'package:mockito/mockito.dart';
+
+import '../../../src/common.dart';
+import '../../../src/fake_process_manager.dart';
+import '../../../src/testbed.dart';
+
+const List<String> _kSharedConfig = <String>[
+  '-dynamiclib',
+  '-fembed-bitcode-marker',
+  '-Xlinker',
+  '-rpath',
+  '-Xlinker',
+  '@executable_path/Frameworks',
+  '-Xlinker',
+  '-rpath',
+  '-Xlinker',
+  '@loader_path/Frameworks',
+  '-install_name',
+  '@rpath/App.framework/App',
+  '-isysroot',
+];
+
+void main() {
+  Testbed testbed;
+  Environment environment;
+  ProcessManager processManager;
+
+  setUp(() {
+    testbed = Testbed(setup: () {
+      environment = Environment.test(globals.fs.currentDirectory, defines: <String, String>{
+        kTargetPlatform: 'ios',
+      });
+    });
+  });
+
+  test('DebugUniveralFramework creates expected binary with arm64 only arch', () => testbed.run(() async {
+    environment.defines[kIosArchs] = 'arm64';
+    processManager = FakeProcessManager.list(<FakeCommand>[
+      // Create iphone stub.
+      const FakeCommand(command: <String>['xcrun', '--sdk', 'iphoneos', '--show-sdk-path']),
+      FakeCommand(command: <String>[
+        'xcrun',
+        'clang',
+        '-x',
+        'c',
+         // iphone only gets 64 bit arch based on kIosArchs
+        '-arch',
+        'arm64',
+        globals.fs.path.absolute(globals.fs.path.join('.tmp_rand0', 'flutter_tools_stub_source.rand0', 'debug_app.cc')),
+        ..._kSharedConfig,
+        '',
+        '-o',
+        environment.buildDir.childFile('iphone_framework').path
+      ]),
+      // Create simulator stub.
+      const FakeCommand(command: <String>['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-path']),
+      FakeCommand(command: <String>[
+        'xcrun',
+        'clang',
+        '-x',
+        'c',
+        // Simulator only as x86_64 arch
+        '-arch',
+        'x86_64',
+        globals.fs.path.absolute(globals.fs.path.join('.tmp_rand0', 'flutter_tools_stub_source.rand0', 'debug_app.cc')),
+        ..._kSharedConfig,
+        '',
+        '-o',
+        environment.buildDir.childFile('simulator_framework').path
+      ]),
+      // Lipo stubs together.
+      FakeCommand(command: <String>[
+        'xcrun',
+        'lipo',
+        '-create',
+        environment.buildDir.childFile('iphone_framework').path,
+        environment.buildDir.childFile('simulator_framework').path,
+        '-output',
+        environment.buildDir.childFile('App').path,
+      ]),
+    ]);
+
+    await const DebugUniveralFramework().build(environment);
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => processManager,
+  }));
+
+  test('DebugIosApplicationBundle', () => testbed.run(() async {
+    environment.defines[kBuildMode] = 'debug';
+    // Precompiled dart data
+    when(globals.artifacts.getArtifactPath(Artifact.vmSnapshotData, mode: BuildMode.debug))
+      .thenReturn('vm_snapshot_data');
+    when(globals.artifacts.getArtifactPath(Artifact.isolateSnapshotData, mode: BuildMode.debug))
+      .thenReturn('isolate_snapshot_data');
+    globals.fs.file('vm_snapshot_data').createSync();
+    globals.fs.file('isolate_snapshot_data').createSync();
+    // Project info
+    globals.fs.file('pubspec.yaml').writeAsStringSync('name: hello');
+    globals.fs.file('.packages').writeAsStringSync('\n');
+    // Plist file
+    globals.fs.file(globals.fs.path.join('ios', 'Flutter', 'AppFrameworkInfo.plist'))
+      ..createSync(recursive: true);
+    // App kernel
+    environment.buildDir.childFile('app.dill').createSync(recursive: true);
+    // Stub framework
+    environment.buildDir.childFile('App').createSync();
+
+    await const DebugIosApplicationBundle().build(environment);
+
+    final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework');
+    expect(frameworkDirectory.childFile('App'), exists);
+    expect(frameworkDirectory.childFile('Info.plist'), exists);
+
+    final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets');
+    expect(assetDirectory.childFile('kernel_blob.bin'), exists);
+    expect(assetDirectory.childFile('AssetManifest.json'), exists);
+    expect(assetDirectory.childFile('vm_snapshot_data'), exists);
+    expect(assetDirectory.childFile('isolate_snapshot_data'), exists);
+  }, overrides: <Type, Generator>{
+    Artifacts: () => MockArtifacts(),
+  }));
+
+  test('ReleaseIosApplicationBundle', () => testbed.run(() async {
+    environment.defines[kBuildMode] = 'release';
+
+    // Project info
+    globals.fs.file('pubspec.yaml').writeAsStringSync('name: hello');
+    globals.fs.file('.packages').writeAsStringSync('\n');
+    // Plist file
+    globals.fs.file(globals.fs.path.join('ios', 'Flutter', 'AppFrameworkInfo.plist'))
+      ..createSync(recursive: true);
+
+    // Real framework
+    environment.buildDir
+      .childDirectory('App.framework')
+      .childFile('App')
+      .createSync(recursive: true);
+
+    await const ReleaseIosApplicationBundle().build(environment);
+
+    final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework');
+    expect(frameworkDirectory.childFile('App'), exists);
+    expect(frameworkDirectory.childFile('Info.plist'), exists);
+
+    final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets');
+    expect(assetDirectory.childFile('kernel_blob.bin'), isNot(exists));
+    expect(assetDirectory.childFile('AssetManifest.json'), exists);
+    expect(assetDirectory.childFile('vm_snapshot_data'), isNot(exists));
+    expect(assetDirectory.childFile('isolate_snapshot_data'), isNot(exists));
+  }));
+}
+
+class MockArtifacts extends Mock implements Artifacts {}