Revert "Revert "Allow any iOS app to be added to an existing host app (#70647)" (#70739)" (#70740)

This reverts commit aab9a76ee4cd97199e9d21118a0a3cfbda80b899.
diff --git a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart
index cbd8b6d..d35ed54 100644
--- a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart
+++ b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart
@@ -17,392 +17,439 @@
     section('Create module project');
 
     final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
-    final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
     try {
       await inDirectory(tempDir, () async {
+        section('Test module template');
+
+        final Directory moduleProjectDir =
+            Directory(path.join(tempDir.path, 'hello_module'));
         await flutter(
           'create',
-          options: <String>['--org', 'io.flutter.devicelab', '--template', 'module', 'hello'],
-        );
-      });
-
-      section('Add plugins');
-
-      final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
-      String content = pubspec.readAsStringSync();
-      content = content.replaceFirst(
-        '\ndependencies:\n',
-        '\ndependencies:\n  device_info: 0.4.1\n  package_info: 0.4.0+9\n',
-      );
-      pubspec.writeAsStringSync(content, flush: true);
-      await inDirectory(projectDir, () async {
-        await flutter(
-          'packages',
-          options: <String>['get'],
-        );
-      });
-
-      // First, build the module in Debug to copy the debug version of Flutter.framework.
-      // This proves "flutter build ios-framework" re-copies the relevant Flutter.framework,
-      // otherwise building plugins with bitcode will fail linking because the debug version
-      // of Flutter.framework does not contain bitcode.
-      await inDirectory(projectDir, () async {
-        await flutter(
-          'build',
           options: <String>[
-            'ios',
-            '--debug',
-            '--no-codesign',
+            '--org',
+            'io.flutter.devicelab',
+            '--template',
+            'module',
+            'hello_module'
           ],
         );
-      });
 
-      // This builds all build modes' frameworks by default
-      section('Build frameworks');
+        await _testBuildIosFramework(moduleProjectDir, isModule: true);
 
-      const String outputDirectoryName = 'flutter-frameworks';
+        section('Test app template');
 
-      await inDirectory(projectDir, () async {
+        final Directory projectDir =
+            Directory(path.join(tempDir.path, 'hello_project'));
         await flutter(
-          'build',
-          options: <String>[
-            'ios-framework',
-            '--universal',
-            '--output=$outputDirectoryName'
-          ],
+          'create',
+          options: <String>['--org', 'io.flutter.devicelab', 'hello_project'],
         );
+
+        await _testBuildIosFramework(projectDir);
       });
 
-      final String outputPath = path.join(projectDir.path, outputDirectoryName);
-
-      section('Check debug build has Dart snapshot as asset');
-
-      checkFileExists(path.join(
-        outputPath,
-        'Debug',
-        'App.framework',
-        'flutter_assets',
-        'vm_snapshot_data',
-      ));
-
-      section('Check debug build has no Dart AOT');
-
-      // There's still an App.framework with a dylib, but it's empty.
-      checkFileExists(path.join(
-        outputPath,
-        'Debug',
-        'App.framework',
-        'App',
-      ));
-
-      final String debugAppFrameworkPath = path.join(
-        outputPath,
-        'Debug',
-        'App.framework',
-        'App',
-      );
-      final String aotSymbols = await dylibSymbols(debugAppFrameworkPath);
-
-      if (aotSymbols.contains('architecture') ||
-          aotSymbols.contains('_kDartVmSnapshot')) {
-        throw TaskResult.failure('Debug App.framework contains AOT');
-      }
-      await _checkFrameworkArchs(debugAppFrameworkPath, 'Debug');
-
-      // Xcode changed the name of this generated directory in Xcode 12.
-      const String xcode11ArmDirectoryName = 'ios-armv7_arm64';
-      const String xcode12ArmDirectoryName = 'ios-arm64_armv7';
-
-      final String xcode11AppFrameworkDirectory = path.join(
-        outputPath,
-        'Debug',
-        'App.xcframework',
-        xcode11ArmDirectoryName,
-        'App.framework',
-        'App',
-      );
-      final String xcode12AppFrameworkDirectory = path.join(
-        outputPath,
-        'Debug',
-        'App.xcframework',
-        xcode12ArmDirectoryName,
-        'App.framework',
-        'App',
-      );
-
-      // This seemed easier than an explicit Xcode version check.
-      String xcodeArmDirectoryName;
-      if (exists(File(xcode11AppFrameworkDirectory))) {
-        xcodeArmDirectoryName = xcode11ArmDirectoryName;
-      } else if (exists(File(xcode12AppFrameworkDirectory))) {
-        xcodeArmDirectoryName = xcode12ArmDirectoryName;
-      } else {
-        throw const FileSystemException('Expected App.framework binary to exist.');
-      }
-
-      checkFileExists(path.join(
-        outputPath,
-        'Debug',
-        'App.xcframework',
-        'ios-x86_64-simulator',
-        'App.framework',
-        'App',
-      ));
-
-      section('Check profile, release builds has Dart AOT dylib');
-
-      for (final String mode in <String>['Profile', 'Release']) {
-        final String appFrameworkPath = path.join(
-          outputPath,
-          mode,
-          'App.framework',
-          'App',
-        );
-
-        await _checkFrameworkArchs(appFrameworkPath, mode);
-        await _checkBitcode(appFrameworkPath, mode);
-
-        final String aotSymbols = await dylibSymbols(appFrameworkPath);
-
-        if (!aotSymbols.contains('_kDartVmSnapshot')) {
-          throw TaskResult.failure('$mode App.framework missing Dart AOT');
-        }
-
-        checkFileNotExists(path.join(
-          outputPath,
-          mode,
-          'App.framework',
-          'flutter_assets',
-          'vm_snapshot_data',
-        ));
-
-        checkFileExists(path.join(
-          outputPath,
-          mode,
-          'App.xcframework',
-          xcodeArmDirectoryName,
-          'App.framework',
-          'App',
-        ));
-
-        checkFileNotExists(path.join(
-          outputPath,
-          mode,
-          'App.xcframework',
-          'ios-x86_64-simulator',
-          'App.framework',
-          'App',
-        ));
-      }
-
-      section("Check all modes' engine dylib");
-
-      for (final String mode in <String>['Debug', 'Profile', 'Release']) {
-        final String engineFrameworkPath = path.join(
-          outputPath,
-          mode,
-          'Flutter.framework',
-          'Flutter',
-        );
-
-        await _checkFrameworkArchs(engineFrameworkPath, mode);
-        await _checkBitcode(engineFrameworkPath, mode);
-
-        checkFileExists(path.join(
-          outputPath,
-          mode,
-          'Flutter.xcframework',
-          xcodeArmDirectoryName,
-          'Flutter.framework',
-          'Flutter',
-        ));
-        final String simulatorFrameworkPath = path.join(
-          outputPath,
-          mode,
-          'Flutter.xcframework',
-          'ios-x86_64-simulator',
-          'Flutter.framework',
-          'Flutter',
-        );
-        if (mode == 'Debug') {
-          checkFileExists(simulatorFrameworkPath);
-        } else {
-          checkFileNotExists(simulatorFrameworkPath);
-        }
-      }
-
-      section("Check all modes' engine header");
-
-      for (final String mode in <String>['Debug', 'Profile', 'Release']) {
-        checkFileExists(path.join(outputPath, mode, 'Flutter.framework', 'Headers', 'Flutter.h'));
-      }
-
-      section('Check all modes have plugins');
-
-      for (final String mode in <String>['Debug', 'Profile', 'Release']) {
-        final String pluginFrameworkPath = path.join(
-          outputPath,
-          mode,
-          'device_info.framework',
-          'device_info',
-        );
-        await _checkFrameworkArchs(pluginFrameworkPath, mode);
-        await _checkBitcode(pluginFrameworkPath, mode);
-
-        checkFileExists(path.join(
-          outputPath,
-          mode,
-          'device_info.xcframework',
-          xcodeArmDirectoryName,
-          'device_info.framework',
-          'device_info',
-        ));
-
-        checkFileExists(path.join(
-          outputPath,
-          mode,
-          'device_info.xcframework',
-          xcodeArmDirectoryName,
-          'device_info.framework',
-          'Headers',
-          'DeviceInfoPlugin.h',
-        ));
-
-        final String simulatorFrameworkPath = path.join(
-          outputPath,
-          mode,
-          'device_info.xcframework',
-          'ios-x86_64-simulator',
-          'device_info.framework',
-          'device_info',
-        );
-
-        final String simulatorFrameworkHeaderPath = path.join(
-          outputPath,
-          mode,
-          'device_info.xcframework',
-          'ios-x86_64-simulator',
-          'device_info.framework',
-          'Headers',
-          'DeviceInfoPlugin.h',
-        );
-
-        if (mode == 'Debug') {
-          checkFileExists(simulatorFrameworkPath);
-          checkFileExists(simulatorFrameworkHeaderPath);
-        } else {
-          checkFileNotExists(simulatorFrameworkPath);
-          checkFileNotExists(simulatorFrameworkHeaderPath);
-        }
-      }
-
-      section('Check all modes have generated plugin registrant');
-
-      for (final String mode in <String>['Debug', 'Profile', 'Release']) {
-        final String registrantFrameworkPath = path.join(
-          outputPath,
-          mode,
-          'FlutterPluginRegistrant.framework',
-          'FlutterPluginRegistrant'
-        );
-
-        await _checkFrameworkArchs(registrantFrameworkPath, mode);
-        await _checkBitcode(registrantFrameworkPath, mode);
-
-        checkFileExists(path.join(
-          outputPath,
-          mode,
-          'FlutterPluginRegistrant.framework',
-          'Headers',
-          'GeneratedPluginRegistrant.h',
-        ));
-        checkFileExists(path.join(
-          outputPath,
-          mode,
-          'FlutterPluginRegistrant.xcframework',
-          xcodeArmDirectoryName,
-          'FlutterPluginRegistrant.framework',
-          'Headers',
-          'GeneratedPluginRegistrant.h',
-        ));
-        final String simulatorHeaderPath = path.join(
-          outputPath,
-          mode,
-          'FlutterPluginRegistrant.xcframework',
-          'ios-x86_64-simulator',
-          'FlutterPluginRegistrant.framework',
-          'Headers',
-          'GeneratedPluginRegistrant.h',
-        );
-        if (mode == 'Debug') {
-          checkFileExists(simulatorHeaderPath);
-        } else {
-          checkFileNotExists(simulatorHeaderPath);
-        }
-      }
-
-      // This builds all build modes' frameworks by default
-      section('Build podspec');
-
-      const String cocoapodsOutputDirectoryName = 'flutter-frameworks-cocoapods';
-
-      await inDirectory(projectDir, () async {
-        await flutter(
-          'build',
-          options: <String>[
-            'ios-framework',
-            '--cocoapods',
-            '--universal',
-            '--force', // Allow podspec creation on master.
-            '--output=$cocoapodsOutputDirectoryName'
-          ],
-        );
-      });
-
-      final String cocoapodsOutputPath = path.join(projectDir.path, cocoapodsOutputDirectoryName);
-      for (final String mode in <String>['Debug', 'Profile', 'Release']) {
-        checkFileExists(path.join(
-          cocoapodsOutputPath,
-          mode,
-          'Flutter.podspec',
-        ));
-
-        checkDirectoryExists(path.join(
-          cocoapodsOutputPath,
-          mode,
-          'App.framework',
-        ));
-
-        checkDirectoryExists(path.join(
-          cocoapodsOutputPath,
-          mode,
-          'FlutterPluginRegistrant.framework',
-        ));
-
-        checkDirectoryExists(path.join(
-          cocoapodsOutputPath,
-          mode,
-          'device_info.framework',
-        ));
-
-        checkDirectoryExists(path.join(
-          cocoapodsOutputPath,
-          mode,
-          'package_info.framework',
-        ));
-      }
-
       return TaskResult.success(null);
     } on TaskResult catch (taskResult) {
       return taskResult;
     } catch (e) {
       return TaskResult.failure(e.toString());
     } finally {
-      rmTree(tempDir);
+      // rmTree(tempDir);
     }
   });
 }
 
+Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = false}) async {
+  section('Add plugins');
+
+  final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
+  String content = pubspec.readAsStringSync();
+  content = content.replaceFirst(
+    '\ndependencies:\n',
+    '\ndependencies:\n  device_info: 0.4.1\n  package_info: 0.4.0+9\n',
+  );
+  pubspec.writeAsStringSync(content, flush: true);
+  await inDirectory(projectDir, () async {
+    await flutter(
+      'packages',
+      options: <String>['get'],
+    );
+  });
+
+  // First, build the module in Debug to copy the debug version of Flutter.framework.
+  // This proves "flutter build ios-framework" re-copies the relevant Flutter.framework,
+  // otherwise building plugins with bitcode will fail linking because the debug version
+  // of Flutter.framework does not contain bitcode.
+  await inDirectory(projectDir, () async {
+    await flutter(
+      'build',
+      options: <String>[
+        'ios',
+        '--debug',
+        '--no-codesign',
+      ],
+    );
+  });
+
+  // This builds all build modes' frameworks by default
+  section('Build frameworks');
+
+  const String outputDirectoryName = 'flutter-frameworks';
+
+  await inDirectory(projectDir, () async {
+    await flutter(
+      'build',
+      options: <String>[
+        'ios-framework',
+        '--universal',
+        '--output=$outputDirectoryName'
+      ],
+    );
+  });
+
+  final String outputPath = path.join(projectDir.path, outputDirectoryName);
+
+  section('Check debug build has Dart snapshot as asset');
+
+  checkFileExists(path.join(
+    outputPath,
+    'Debug',
+    'App.framework',
+    'flutter_assets',
+    'vm_snapshot_data',
+  ));
+
+  section('Check debug build has no Dart AOT');
+
+  // There's still an App.framework with a dylib, but it's empty.
+  checkFileExists(path.join(
+    outputPath,
+    'Debug',
+    'App.framework',
+    'App',
+  ));
+
+  final String debugAppFrameworkPath = path.join(
+    outputPath,
+    'Debug',
+    'App.framework',
+    'App',
+  );
+  final String aotSymbols = await dylibSymbols(debugAppFrameworkPath);
+
+  if (aotSymbols.contains('architecture') ||
+      aotSymbols.contains('_kDartVmSnapshot')) {
+    throw TaskResult.failure('Debug App.framework contains AOT');
+  }
+  await _checkFrameworkArchs(debugAppFrameworkPath, 'Debug');
+
+  // Xcode changed the name of this generated directory in Xcode 12.
+  const String xcode11ArmDirectoryName = 'ios-armv7_arm64';
+  const String xcode12ArmDirectoryName = 'ios-arm64_armv7';
+
+  final String xcode11AppFrameworkDirectory = path.join(
+    outputPath,
+    'Debug',
+    'App.xcframework',
+    xcode11ArmDirectoryName,
+    'App.framework',
+    'App',
+  );
+  final String xcode12AppFrameworkDirectory = path.join(
+    outputPath,
+    'Debug',
+    'App.xcframework',
+    xcode12ArmDirectoryName,
+    'App.framework',
+    'App',
+  );
+
+  // This seemed easier than an explicit Xcode version check.
+  String xcodeArmDirectoryName;
+  if (exists(File(xcode11AppFrameworkDirectory))) {
+    xcodeArmDirectoryName = xcode11ArmDirectoryName;
+  } else if (exists(File(xcode12AppFrameworkDirectory))) {
+    xcodeArmDirectoryName = xcode12ArmDirectoryName;
+  } else {
+    throw const FileSystemException('Expected App.framework binary to exist.');
+  }
+
+  checkFileExists(path.join(
+    outputPath,
+    'Debug',
+    'App.xcframework',
+    'ios-x86_64-simulator',
+    'App.framework',
+    'App',
+  ));
+
+  section('Check profile, release builds has Dart AOT dylib');
+
+  for (final String mode in <String>['Profile', 'Release']) {
+    final String appFrameworkPath = path.join(
+      outputPath,
+      mode,
+      'App.framework',
+      'App',
+    );
+
+    await _checkFrameworkArchs(appFrameworkPath, mode);
+    await _checkBitcode(appFrameworkPath, mode);
+
+    final String aotSymbols = await dylibSymbols(appFrameworkPath);
+
+    if (!aotSymbols.contains('_kDartVmSnapshot')) {
+      throw TaskResult.failure('$mode App.framework missing Dart AOT');
+    }
+
+    checkFileNotExists(path.join(
+      outputPath,
+      mode,
+      'App.framework',
+      'flutter_assets',
+      'vm_snapshot_data',
+    ));
+
+    checkFileExists(path.join(
+      outputPath,
+      mode,
+      'App.xcframework',
+      xcodeArmDirectoryName,
+      'App.framework',
+      'App',
+    ));
+
+    checkFileNotExists(path.join(
+      outputPath,
+      mode,
+      'App.xcframework',
+      'ios-x86_64-simulator',
+      'App.framework',
+      'App',
+    ));
+  }
+
+  section("Check all modes' engine dylib");
+
+  for (final String mode in <String>['Debug', 'Profile', 'Release']) {
+    final String engineFrameworkPath = path.join(
+      outputPath,
+      mode,
+      'Flutter.framework',
+      'Flutter',
+    );
+
+    await _checkFrameworkArchs(engineFrameworkPath, mode);
+    await _checkBitcode(engineFrameworkPath, mode);
+
+    checkFileExists(path.join(
+      outputPath,
+      mode,
+      'Flutter.xcframework',
+      xcodeArmDirectoryName,
+      'Flutter.framework',
+      'Flutter',
+    ));
+    final String simulatorFrameworkPath = path.join(
+      outputPath,
+      mode,
+      'Flutter.xcframework',
+      'ios-x86_64-simulator',
+      'Flutter.framework',
+      'Flutter',
+    );
+    if (mode == 'Debug') {
+      checkFileExists(simulatorFrameworkPath);
+    } else {
+      checkFileNotExists(simulatorFrameworkPath);
+    }
+  }
+
+  section("Check all modes' engine header");
+
+  for (final String mode in <String>['Debug', 'Profile', 'Release']) {
+    checkFileExists(path.join(outputPath, mode, 'Flutter.framework', 'Headers', 'Flutter.h'));
+  }
+
+  section('Check all modes have plugins');
+
+  for (final String mode in <String>['Debug', 'Profile', 'Release']) {
+    final String pluginFrameworkPath = path.join(
+      outputPath,
+      mode,
+      'device_info.framework',
+      'device_info',
+    );
+    await _checkFrameworkArchs(pluginFrameworkPath, mode);
+    await _checkBitcode(pluginFrameworkPath, mode);
+
+    checkFileExists(path.join(
+      outputPath,
+      mode,
+      'device_info.xcframework',
+      xcodeArmDirectoryName,
+      'device_info.framework',
+      'device_info',
+    ));
+
+    checkFileExists(path.join(
+      outputPath,
+      mode,
+      'device_info.xcframework',
+      xcodeArmDirectoryName,
+      'device_info.framework',
+      'Headers',
+      'DeviceInfoPlugin.h',
+    ));
+
+    final String simulatorFrameworkPath = path.join(
+      outputPath,
+      mode,
+      'device_info.xcframework',
+      'ios-x86_64-simulator',
+      'device_info.framework',
+      'device_info',
+    );
+
+    final String simulatorFrameworkHeaderPath = path.join(
+      outputPath,
+      mode,
+      'device_info.xcframework',
+      'ios-x86_64-simulator',
+      'device_info.framework',
+      'Headers',
+      'DeviceInfoPlugin.h',
+    );
+
+    if (mode == 'Debug') {
+      checkFileExists(simulatorFrameworkPath);
+      checkFileExists(simulatorFrameworkHeaderPath);
+    } else {
+      checkFileNotExists(simulatorFrameworkPath);
+      checkFileNotExists(simulatorFrameworkHeaderPath);
+    }
+  }
+
+  section('Check all modes have generated plugin registrant');
+
+  for (final String mode in <String>['Debug', 'Profile', 'Release']) {
+    if (!isModule) {
+      continue;
+    }
+    final String registrantFrameworkPath = path.join(
+      outputPath,
+      mode,
+      'FlutterPluginRegistrant.framework',
+      'FlutterPluginRegistrant'
+    );
+
+    await _checkFrameworkArchs(registrantFrameworkPath, mode);
+    await _checkBitcode(registrantFrameworkPath, mode);
+
+    checkFileExists(path.join(
+      outputPath,
+      mode,
+      'FlutterPluginRegistrant.framework',
+      'Headers',
+      'GeneratedPluginRegistrant.h',
+    ));
+    checkFileExists(path.join(
+      outputPath,
+      mode,
+      'FlutterPluginRegistrant.xcframework',
+      xcodeArmDirectoryName,
+      'FlutterPluginRegistrant.framework',
+      'Headers',
+      'GeneratedPluginRegistrant.h',
+    ));
+    final String simulatorHeaderPath = path.join(
+      outputPath,
+      mode,
+      'FlutterPluginRegistrant.xcframework',
+      'ios-x86_64-simulator',
+      'FlutterPluginRegistrant.framework',
+      'Headers',
+      'GeneratedPluginRegistrant.h',
+    );
+    if (mode == 'Debug') {
+      checkFileExists(simulatorHeaderPath);
+    } else {
+      checkFileNotExists(simulatorHeaderPath);
+    }
+  }
+
+  // This builds all build modes' frameworks by default
+  section('Build podspec');
+
+  const String cocoapodsOutputDirectoryName = 'flutter-frameworks-cocoapods';
+
+  await inDirectory(projectDir, () async {
+    await flutter(
+      'build',
+      options: <String>[
+        'ios-framework',
+        '--cocoapods',
+        '--universal',
+        '--force', // Allow podspec creation on master.
+        '--output=$cocoapodsOutputDirectoryName'
+      ],
+    );
+  });
+
+  final String cocoapodsOutputPath = path.join(projectDir.path, cocoapodsOutputDirectoryName);
+  for (final String mode in <String>['Debug', 'Profile', 'Release']) {
+    checkFileExists(path.join(
+      cocoapodsOutputPath,
+      mode,
+      'Flutter.podspec',
+    ));
+
+    checkDirectoryExists(path.join(
+      cocoapodsOutputPath,
+      mode,
+      'App.framework',
+    ));
+
+    if (Directory(path.join(
+          cocoapodsOutputPath,
+          mode,
+          'FlutterPluginRegistrant.framework',
+        )).existsSync() !=
+        isModule) {
+      throw TaskResult.failure(
+          'Unexpected FlutterPluginRegistrant.framework.');
+    }
+
+    checkDirectoryExists(path.join(
+      cocoapodsOutputPath,
+      mode,
+      'device_info.framework',
+    ));
+
+    checkDirectoryExists(path.join(
+      cocoapodsOutputPath,
+      mode,
+      'package_info.framework',
+    ));
+  }
+
+  if (File(path.join(
+        outputPath,
+        'GeneratedPluginRegistrant.h',
+      )).existsSync() ==
+      isModule) {
+    throw TaskResult.failure('Unexpected GeneratedPluginRegistrant.h.');
+  }
+
+  if (File(path.join(
+        outputPath,
+        'GeneratedPluginRegistrant.m',
+      )).existsSync() ==
+      isModule) {
+    throw TaskResult.failure('Unexpected GeneratedPluginRegistrant.m.');
+  }
+}
+
 Future<void> _checkFrameworkArchs(String frameworkPath, String mode) async {
   checkFileExists(frameworkPath);
 
diff --git a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
index 48cfd18..1c73a36 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
@@ -113,7 +113,7 @@
   final String name = 'ios-framework';
 
   @override
-  final String description = 'Produces a .framework directory for a Flutter module '
+  final String description = 'Produces .frameworks for a Flutter project '
       'and its plugins for integration into existing, plain Xcode projects.\n'
       'This can only be run on macOS hosts.';
 
@@ -144,10 +144,6 @@
   Future<void> validateCommand() async {
     await super.validateCommand();
     _project = FlutterProject.current();
-    if (!_project.isModule) {
-      throwToolExit('Building frameworks for iOS is only supported from a module.');
-    }
-
     if (!_platform.isMacOS) {
       throwToolExit('Building frameworks for iOS is only supported on the Mac.');
     }
@@ -178,7 +174,7 @@
     }
 
     if (!_project.ios.existsSync()) {
-      throwToolExit('Module does not support iOS');
+      throwToolExit('Project does not support iOS');
     }
 
     final Directory outputDirectory = globals.fs.directory(globals.fs.path.absolute(globals.fs.path.normalize(outputArgument)));
@@ -233,6 +229,23 @@
     }
 
     globals.printStatus('Frameworks written to ${outputDirectory.path}.');
+
+    if (!_project.isModule && hasPlugins(_project)) {
+      // Apps do not generate a FlutterPluginRegistrant.framework. Users will need
+      // to copy the GeneratedPluginRegistrant class to their project manually.
+      final File pluginRegistrantHeader = _project.ios.pluginRegistrantHeader;
+      final File pluginRegistrantImplementation =
+          _project.ios.pluginRegistrantImplementation;
+      pluginRegistrantHeader.copySync(
+          outputDirectory.childFile(pluginRegistrantHeader.basename).path);
+      pluginRegistrantImplementation.copySync(outputDirectory
+          .childFile(pluginRegistrantImplementation.basename)
+          .path);
+      globals.printStatus(
+          '\nCopy the ${globals.fs.path.basenameWithoutExtension(pluginRegistrantHeader.path)} class into your project.\n'
+          'See https://flutter.dev/docs/development/add-to-app/ios/add-flutter-screen#create-a-flutterengine for more information.');
+    }
+
     return FlutterCommandResult.success();
   }
 
@@ -459,6 +472,7 @@
         xcodeBuildConfiguration,
         'SYMROOT=${iPhoneBuildOutput.path}',
         'BITCODE_GENERATION_MODE=$bitcodeGenerationMode',
+        'ENABLE_BITCODE=YES', // Support host apps with bitcode enabled.
         'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
         'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
       ];
@@ -483,6 +497,7 @@
           '-configuration',
           xcodeBuildConfiguration,
           'SYMROOT=${simulatorBuildOutput.path}',
+          'ENABLE_BITCODE=YES', // Support host apps with bitcode enabled.
           'ARCHS=x86_64',
           'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
           'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index 543b0fa..3883862 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -897,36 +897,24 @@
     'framework': 'Flutter',
     'plugins': iosPlugins,
   };
-  final String registryDirectory = project.ios.pluginRegistrantHost.path;
   if (project.isModule) {
-    final String registryClassesDirectory = globals.fs.path.join(registryDirectory, 'Classes');
+    final String registryDirectory = project.ios.pluginRegistrantHost.path;
     _renderTemplateToFile(
       _pluginRegistrantPodspecTemplate,
       context,
       globals.fs.path.join(registryDirectory, 'FlutterPluginRegistrant.podspec'),
     );
-    _renderTemplateToFile(
-      _objcPluginRegistryHeaderTemplate,
-      context,
-      globals.fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.h'),
-    );
-    _renderTemplateToFile(
-      _objcPluginRegistryImplementationTemplate,
-      context,
-      globals.fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.m'),
-    );
-  } else {
-    _renderTemplateToFile(
-      _objcPluginRegistryHeaderTemplate,
-      context,
-      globals.fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.h'),
-    );
-    _renderTemplateToFile(
-      _objcPluginRegistryImplementationTemplate,
-      context,
-      globals.fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.m'),
-    );
   }
+  _renderTemplateToFile(
+    _objcPluginRegistryHeaderTemplate,
+    context,
+    project.ios.pluginRegistrantHeader.path,
+  );
+  _renderTemplateToFile(
+    _objcPluginRegistryImplementationTemplate,
+    context,
+    project.ios.pluginRegistrantImplementation.path,
+  );
 }
 
 /// The relative path from a project's main CMake file to the plugin symlink
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 9e36c46..1ee6b9d 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -665,15 +665,12 @@
       )
     );
     if (framework.existsSync()) {
-      final Directory engineDest = ephemeralDirectory
-          .childDirectory('Flutter')
-          .childDirectory('engine');
       final File podspec = framework.parent.childFile('Flutter.podspec');
       globals.fsUtils.copyDirectorySync(
         framework,
-        engineDest.childDirectory('Flutter.framework'),
+        engineCopyDirectory.childDirectory('Flutter.framework'),
       );
-      podspec.copySync(engineDest.childFile('Flutter.podspec').path);
+      podspec.copySync(engineCopyDirectory.childFile('Flutter.podspec').path);
     }
   }
 
@@ -704,6 +701,22 @@
         : hostAppRoot.childDirectory(_hostAppProjectName);
   }
 
+  File get pluginRegistrantHeader {
+    final Directory registryDirectory = isModule ? pluginRegistrantHost.childDirectory('Classes') : pluginRegistrantHost;
+    return registryDirectory.childFile('GeneratedPluginRegistrant.h');
+  }
+
+  File get pluginRegistrantImplementation {
+    final Directory registryDirectory = isModule ? pluginRegistrantHost.childDirectory('Classes') : pluginRegistrantHost;
+    return registryDirectory.childFile('GeneratedPluginRegistrant.m');
+  }
+
+  Directory get engineCopyDirectory {
+    return isModule
+        ? ephemeralDirectory.childDirectory('Flutter').childDirectory('engine')
+        : hostAppRoot.childDirectory('Flutter');
+  }
+
   Future<void> _overwriteFromTemplate(String path, Directory target) async {
     final Template template = await Template.fromName(
       path,