[flutter_tools] Port xcode backend to dart (#86753)

diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart
new file mode 100644
index 0000000..04f86e5
--- /dev/null
+++ b/packages/flutter_tools/bin/xcode_backend.dart
@@ -0,0 +1,466 @@
+// 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 'dart:io';
+
+void main(List<String> arguments) {
+  File? scriptOutputStreamFile;
+  final String? scriptOutputStreamFileEnv = Platform.environment['SCRIPT_OUTPUT_STREAM_FILE'];
+  if (scriptOutputStreamFileEnv != null && scriptOutputStreamFileEnv.isNotEmpty) {
+    scriptOutputStreamFile = File(scriptOutputStreamFileEnv);
+  }
+  Context(
+    arguments: arguments,
+    environment: Platform.environment,
+    scriptOutputStreamFile: scriptOutputStreamFile,
+  ).run();
+}
+
+/// Container for script arguments and environment variables.
+///
+/// All interactions with the platform are broken into individual methods that
+/// can be overridden in tests.
+class Context {
+  Context({
+    required this.arguments,
+    required this.environment,
+    File? scriptOutputStreamFile,
+  }) {
+    if (scriptOutputStreamFile != null) {
+      scriptOutputStream = scriptOutputStreamFile.openSync(mode: FileMode.write);
+    }
+  }
+
+  final Map<String, String> environment;
+  final List<String> arguments;
+  RandomAccessFile? scriptOutputStream;
+
+  void run() {
+    if (arguments.isEmpty) {
+      // Named entry points were introduced in Flutter v0.0.7.
+      stderr.write(
+          'error: Your Xcode project is incompatible with this version of Flutter. '
+          'Run "rm -rf ios/Runner.xcodeproj" and "flutter create ." to regenerate.\n');
+      exit(-1);
+    }
+
+    final String subCommand = arguments.first;
+    switch (subCommand) {
+      case 'build':
+        buildApp();
+        break;
+      case 'thin':
+        // No-op, thinning is handled during the bundle asset assemble build target.
+        break;
+      case 'embed':
+        embedFlutterFrameworks();
+        break;
+      case 'embed_and_thin':
+        // Thinning is handled during the bundle asset assemble build target, so just embed.
+        embedFlutterFrameworks();
+        break;
+      case 'test_observatory_bonjour_service':
+        // Exposed for integration testing only.
+        addObservatoryBonjourService();
+    }
+  }
+
+  bool existsDir(String path) {
+    final Directory dir = Directory(path);
+    return dir.existsSync();
+  }
+
+  bool existsFile(String path) {
+    final File file = File(path);
+    return file.existsSync();
+  }
+
+  /// Run given command in a synchronous subprocess.
+  ///
+  /// Will throw [Exception] if the exit code is not 0.
+  ProcessResult runSync(
+    String bin,
+    List<String> args, {
+    bool verbose = false,
+    bool allowFail = false,
+    String? workingDirectory,
+  }) {
+    if (verbose) {
+      print('♦ $bin ${args.join(' ')}');
+    }
+    final ProcessResult result = Process.runSync(
+      bin,
+      args,
+      workingDirectory: workingDirectory,
+    );
+    if (verbose) {
+      print((result.stdout as String).trim());
+    }
+    if ((result.stderr as String).isNotEmpty) {
+      echoError((result.stderr as String).trim());
+    }
+    if (!allowFail && result.exitCode != 0) {
+      stderr.write('${result.stderr}\n');
+      throw Exception(
+        'Command "$bin ${args.join(' ')}" exited with code ${result.exitCode}',
+      );
+    }
+    return result;
+  }
+
+  /// Log message to stderr.
+  void echoError(String message) {
+    stderr.writeln(message);
+  }
+
+  /// Log message to stdout.
+  void echo(String message) {
+    stdout.write(message);
+  }
+
+  /// Exit the application with the given exit code.
+  ///
+  /// Exists to allow overriding in tests.
+  Never exitApp(int code) {
+    exit(code);
+  }
+
+  /// Return value from environment if it exists, else throw [Exception].
+  String environmentEnsure(String key) {
+    final String? value = environment[key];
+    if (value == null) {
+      throw Exception(
+        'Expected the environment variable "$key" to exist, but it was not found',
+      );
+    }
+    return value;
+  }
+
+  // When provided with a pipe by the host Flutter build process, output to the
+  // pipe goes to stdout of the Flutter build process directly.
+  void streamOutput(String output) {
+    scriptOutputStream?.writeStringSync('$output\n');
+  }
+
+  String parseFlutterBuildMode() {
+    // Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
+    // This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
+    // they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
+    final String? buildMode = (environment['FLUTTER_BUILD_MODE'] ?? environment['CONFIGURATION'])?.toLowerCase();
+
+    if (buildMode != null) {
+      if (buildMode.contains('release')) {
+        return 'release';
+      }
+      if (buildMode.contains('profile')) {
+        return 'profile';
+      }
+      if (buildMode.contains('debug')) {
+        return 'debug';
+      }
+    }
+    echoError('========================================================================');
+    echoError('ERROR: Unknown FLUTTER_BUILD_MODE: $buildMode.');
+    echoError("Valid values are 'Debug', 'Profile', or 'Release' (case insensitive).");
+    echoError('This is controlled by the FLUTTER_BUILD_MODE environment variable.');
+    echoError('If that is not set, the CONFIGURATION environment variable is used.');
+    echoError('');
+    echoError('You can fix this by either adding an appropriately named build');
+    echoError('configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the');
+    echoError('.xcconfig file for the current build configuration (${environment['CONFIGURATION']}).');
+    echoError('========================================================================');
+    exitApp(-1);
+  }
+
+  // Adds the App.framework as an embedded binary and the flutter_assets as
+  // resources.
+  void embedFlutterFrameworks() {
+    // Embed App.framework from Flutter into the app (after creating the Frameworks directory
+    // if it doesn't already exist).
+    final String xcodeFrameworksDir = '${environment['TARGET_BUILD_DIR']}/${environment['FRAMEWORKS_FOLDER_PATH']}';
+    runSync(
+      'mkdir',
+      <String>[
+        '-p',
+        '--',
+        xcodeFrameworksDir,
+      ]
+    );
+    runSync(
+      'rsync',
+      <String>[
+        '-av',
+        '--delete',
+        '--filter',
+        '- .DS_Store',
+        '${environment['BUILT_PRODUCTS_DIR']}/App.framework',
+        xcodeFrameworksDir,
+      ],
+    );
+
+    // Embed the actual Flutter.framework that the Flutter app expects to run against,
+    // which could be a local build or an arch/type specific build.
+    runSync(
+      'rsync',
+      <String>[
+        '-av',
+        '--delete',
+        '--filter',
+        '- .DS_Store',
+        '${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework',
+        '$xcodeFrameworksDir/',
+      ],
+    );
+
+    addObservatoryBonjourService();
+  }
+
+  // Add the observatory publisher Bonjour service to the produced app bundle Info.plist.
+  void addObservatoryBonjourService() {
+    final String buildMode = parseFlutterBuildMode();
+
+    // Debug and profile only.
+    if (buildMode == 'release') {
+      return;
+    }
+
+    final String builtProductsPlist = '${environment['BUILT_PRODUCTS_DIR'] ?? ''}/${environment['INFOPLIST_PATH'] ?? ''}';
+
+    if (!existsFile(builtProductsPlist)) {
+      // Very occasionally Xcode hasn't created an Info.plist when this runs.
+      // The file will be present on re-run.
+      echo(
+        '${environment['INFOPLIST_PATH'] ?? ''} does not exist. Skipping '
+        '_dartobservatory._tcp NSBonjourServices insertion. Try re-building to '
+        'enable "flutter attach".');
+      return;
+    }
+
+    // If there are already NSBonjourServices specified by the app (uncommon),
+    // insert the observatory service name to the existing list.
+    ProcessResult result = runSync(
+      'plutil',
+      <String>[
+        '-extract',
+        'NSBonjourServices',
+        'xml1',
+        '-o',
+        '-',
+        builtProductsPlist,
+      ],
+      allowFail: true,
+    );
+    if (result.exitCode == 0) {
+      runSync(
+        'plutil',
+        <String>[
+          '-insert',
+          'NSBonjourServices.0',
+          '-string',
+          '_dartobservatory._tcp',
+          builtProductsPlist
+        ],
+      );
+    } else {
+      // Otherwise, add the NSBonjourServices key and observatory service name.
+      runSync(
+        'plutil',
+        <String>[
+          '-insert',
+          'NSBonjourServices',
+          '-json',
+          '["_dartobservatory._tcp"]',
+          builtProductsPlist,
+        ],
+      );
+      //fi
+    }
+
+    // Don't override the local network description the Flutter app developer
+    // specified (uncommon). This text will appear below the "Your app would
+    // like to find and connect to devices on your local network" permissions
+    // popup.
+    result = runSync(
+      'plutil',
+      <String>[
+        '-extract',
+        'NSLocalNetworkUsageDescription',
+        'xml1',
+        '-o',
+        '-',
+        builtProductsPlist,
+      ],
+      allowFail: true,
+    );
+    if (result.exitCode != 0) {
+      runSync(
+        'plutil',
+        <String>[
+          '-insert',
+          'NSLocalNetworkUsageDescription',
+          '-string',
+          'Allow Flutter tools on your computer to connect and debug your application. This prompt will not appear on release builds.',
+          builtProductsPlist,
+        ],
+      );
+    }
+  }
+
+  void buildApp() {
+    final bool verbose = environment['VERBOSE_SCRIPT_LOGGING'] != null && environment['VERBOSE_SCRIPT_LOGGING'] != '';
+    final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
+    String projectPath = '$sourceRoot/..';
+    if (environment['FLUTTER_APPLICATION_PATH'] != null) {
+      projectPath = environment['FLUTTER_APPLICATION_PATH']!;
+    }
+
+    String targetPath = 'lib/main.dart';
+    if (environment['FLUTTER_TARGET'] != null) {
+      targetPath = environment['FLUTTER_TARGET']!;
+    }
+
+    String derivedDir = '$sourceRoot/Flutter}';
+    if (existsDir('$projectPath/.ios')) {
+      derivedDir = '$projectPath/.ios/Flutter';
+    }
+
+    // Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
+    // This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
+    // they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
+
+    final String buildMode = parseFlutterBuildMode();
+    String artifactVariant = 'unknown';
+    switch (buildMode) {
+      case 'release':
+        artifactVariant = 'ios-release';
+        break;
+      case 'profile':
+        artifactVariant = 'ios-profile';
+        break;
+      case 'debug':
+        artifactVariant = 'ios';
+        break;
+    }
+
+    // Warn the user if not archiving (ACTION=install) in release mode.
+    final String? action = environment['ACTION'];
+    if (action == 'install' && buildMode != 'release') {
+      echo(
+        'warning: Flutter archive not built in Release mode. Ensure '
+        'FLUTTER_BUILD_MODE is set to release or run "flutter build ios '
+        '--release", then re-run Archive from Xcode.',
+      );
+    }
+    final String frameworkPath = '${environmentEnsure('FLUTTER_ROOT')}/bin/cache/artifacts/engine/$artifactVariant';
+
+    String flutterFramework = '$frameworkPath/Flutter.xcframework';
+
+    final String? localEngine = environment['LOCAL_ENGINE'];
+    if (localEngine != null) {
+      if (!localEngine.toLowerCase().contains(buildMode)) {
+        echoError('========================================================================');
+        echoError("ERROR: Requested build with Flutter local engine at '$localEngine'");
+        echoError("This engine is not compatible with FLUTTER_BUILD_MODE: '$buildMode'.");
+        echoError('You can fix this by updating the LOCAL_ENGINE environment variable, or');
+        echoError('by running:');
+        echoError('  flutter build ios --local-engine=ios_$buildMode');
+        echoError('or');
+        echoError('  flutter build ios --local-engine=ios_${buildMode}_unopt');
+        echoError('========================================================================');
+        exitApp(-1);
+      }
+      flutterFramework = '${environmentEnsure('FLUTTER_ENGINE')}/out/$localEngine/Flutter.xcframework';
+    }
+    String bitcodeFlag = '';
+    if (environment['ENABLE_BITCODE'] == 'YES' && environment['ACTION'] == 'install') {
+      bitcodeFlag = 'true';
+    }
+
+    // TODO(jmagman): use assemble copied engine in add-to-app.
+    if (existsDir('$projectPath/.ios')) {
+      runSync(
+        'rsync',
+        <String>[
+          '-av',
+          '--delete',
+          '--filter',
+          '- .DS_Store',
+          flutterFramework,
+          '$derivedDir/engine',
+        ],
+        verbose: verbose,
+      );
+    }
+
+    final List<String> flutterArgs = <String>[];
+
+    if (verbose) {
+      flutterArgs.add('--verbose');
+    }
+
+    if (environment['FLUTTER_ENGINE'] != null && environment['FLUTTER_ENGINE']!.isNotEmpty) {
+      flutterArgs.add('--local-engine-src-path=${environment['FLUTTER_ENGINE']}');
+    }
+
+    if (environment['LOCAL_ENGINE'] != null && environment['LOCAL_ENGINE']!.isNotEmpty) {
+      flutterArgs.add('--local-engine=${environment['LOCAL_ENGINE']}');
+    }
+
+    flutterArgs.addAll(<String>[
+      'assemble',
+      '--no-version-check',
+      '--output=${environment['BUILT_PRODUCTS_DIR'] ?? ''}/',
+      '-dTargetPlatform=ios',
+      '-dTargetFile=$targetPath',
+      '-dBuildMode=$buildMode',
+      '-dIosArchs=${environment['ARCHS'] ?? ''}',
+      '-dSdkRoot=${environment['SDKROOT'] ?? ''}',
+      '-dSplitDebugInfo=${environment['SPLIT_DEBUG_INFO'] ?? ''}',
+      '-dTreeShakeIcons=${environment['TREE_SHAKE_ICONS'] ?? ''}',
+      '-dTrackWidgetCreation=${environment['TRACK_WIDGET_CREATION'] ?? ''}',
+      '-dDartObfuscation=${environment['DART_OBFUSCATION'] ?? ''}',
+      '-dEnableBitcode=$bitcodeFlag',
+      '--ExtraGenSnapshotOptions=${environment['EXTRA_GEN_SNAPSHOT_OPTIONS'] ?? ''}',
+      '--DartDefines=${environment['DART_DEFINES'] ?? ''}',
+      '--ExtraFrontEndOptions=${environment['EXTRA_FRONT_END_OPTIONS'] ?? ''}',
+    ]);
+
+    if (environment['PERFORMANCE_MEASUREMENT_FILE'] != null && environment['PERFORMANCE_MEASUREMENT_FILE']!.isNotEmpty) {
+      flutterArgs.add('--performance-measurement-file=${environment['PERFORMANCE_MEASUREMENT_FILE']}');
+    }
+
+    final String? expandedCodeSignIdentity = environment['EXPANDED_CODE_SIGN_IDENTITY'];
+    if (expandedCodeSignIdentity != null && expandedCodeSignIdentity.isNotEmpty && environment['CODE_SIGNING_REQUIRED'] != 'NO') {
+      flutterArgs.add('-dCodesignIdentity=$expandedCodeSignIdentity');
+    }
+
+    if (environment['BUNDLE_SKSL_PATH'] != null && environment['BUNDLE_SKSL_PATH']!.isNotEmpty) {
+      flutterArgs.add('-dBundleSkSLPath=${environment['BUNDLE_SKSL_PATH']}');
+    }
+
+    if (environment['CODE_SIZE_DIRECTORY'] != null && environment['CODE_SIZE_DIRECTORY']!.isNotEmpty) {
+      flutterArgs.add('-dCodeSizeDirectory=${environment['CODE_SIZE_DIRECTORY']}');
+    }
+
+    flutterArgs.add('${buildMode}_ios_bundle_flutter_assets');
+
+    final ProcessResult result = runSync(
+      '${environmentEnsure('FLUTTER_ROOT')}/bin/flutter',
+      flutterArgs,
+      verbose: verbose,
+      allowFail: true,
+      workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}"
+    );
+
+    if (result.exitCode != 0) {
+      echoError('Failed to package $projectPath.');
+      exitApp(-1);
+    }
+
+    streamOutput('done');
+    streamOutput(' └─Compiling, linking and signing...');
+
+    echo('Project $projectPath built and packaged successfully.');
+  }
+}
diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh
index 3c99a2f..2889d7c 100755
--- a/packages/flutter_tools/bin/xcode_backend.sh
+++ b/packages/flutter_tools/bin/xcode_backend.sh
@@ -3,266 +3,27 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-# Exit on error
-set -e
+# exit on error, or usage of unset var
+set -euo pipefail
 
-RunCommand() {
-  if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
-    echo "♦ $*"
-  fi
-  "$@"
-  return $?
-}
+# Needed because if it is set, cd may print the path it changed to.
+unset CDPATH
 
-# When provided with a pipe by the host Flutter build process, output to the
-# pipe goes to stdout of the Flutter build process directly.
-StreamOutput() {
-  if [[ -n "$SCRIPT_OUTPUT_STREAM_FILE" ]]; then
-    echo "$1" > $SCRIPT_OUTPUT_STREAM_FILE
-  fi
-}
+function follow_links() (
+  cd -P "$(dirname -- "$1")"
+  file="$PWD/$(basename -- "$1")"
+  while [[ -h "$file" ]]; do
+    cd -P "$(dirname -- "$file")"
+    file="$(readlink -- "$file")"
+    cd -P "$(dirname -- "$file")"
+    file="$PWD/$(basename -- "$file")"
+  done
+  echo "$file"
+)
 
-EchoError() {
-  echo "$@" 1>&2
-}
+PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")"
+BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
+FLUTTER_ROOT="$BIN_DIR/../../.."
+DART="$FLUTTER_ROOT/bin/dart"
 
-AssertExists() {
-  if [[ ! -e "$1" ]]; then
-    if [[ -h "$1" ]]; then
-      EchoError "The path $1 is a symlink to a path that does not exist"
-    else
-      EchoError "The path $1 does not exist"
-    fi
-    exit -1
-  fi
-  return 0
-}
-
-ParseFlutterBuildMode() {
-  # Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
-  # This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
-  # they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
-  local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
-
-  case "$build_mode" in
-    *release*) build_mode="release";;
-    *profile*) build_mode="profile";;
-    *debug*) build_mode="debug";;
-    *)
-      EchoError "========================================================================"
-      EchoError "ERROR: Unknown FLUTTER_BUILD_MODE: ${build_mode}."
-      EchoError "Valid values are 'Debug', 'Profile', or 'Release' (case insensitive)."
-      EchoError "This is controlled by the FLUTTER_BUILD_MODE environment variable."
-      EchoError "If that is not set, the CONFIGURATION environment variable is used."
-      EchoError ""
-      EchoError "You can fix this by either adding an appropriately named build"
-      EchoError "configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the"
-      EchoError ".xcconfig file for the current build configuration (${CONFIGURATION})."
-      EchoError "========================================================================"
-      exit -1;;
-  esac
-  echo "${build_mode}"
-}
-
-BuildApp() {
-  local project_path="${SOURCE_ROOT}/.."
-  if [[ -n "$FLUTTER_APPLICATION_PATH" ]]; then
-    project_path="${FLUTTER_APPLICATION_PATH}"
-  fi
-
-  local target_path="lib/main.dart"
-  if [[ -n "$FLUTTER_TARGET" ]]; then
-    target_path="${FLUTTER_TARGET}"
-  fi
-
-  local derived_dir="${SOURCE_ROOT}/Flutter"
-  if [[ -e "${project_path}/.ios" ]]; then
-    derived_dir="${project_path}/.ios/Flutter"
-  fi
-
-  # 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.
-  if FLTAssetsPath=$(/usr/libexec/PlistBuddy -c "Print :FLTAssetsPath" "${derived_dir}/AppFrameworkInfo.plist" 2>/dev/null); then
-    if [[ -n "$FLTAssetsPath" ]]; then
-      assets_path="${FLTAssetsPath}"
-    fi
-  fi
-
-  # Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
-  # This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
-  # they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
-  local build_mode="$(ParseFlutterBuildMode)"
-  local artifact_variant="unknown"
-  case "$build_mode" in
-    release ) artifact_variant="ios-release";;
-    profile ) artifact_variant="ios-profile";;
-    debug ) artifact_variant="ios";;
-  esac
-
-  # Warn the user if not archiving (ACTION=install) in release mode.
-  if [[ "$ACTION" == "install" && "$build_mode" != "release" ]]; then
-    echo "warning: Flutter archive not built in Release mode. Ensure FLUTTER_BUILD_MODE \
-is set to release or run \"flutter build ios --release\", then re-run Archive from Xcode."
-  fi
-
-  local framework_path="${FLUTTER_ROOT}/bin/cache/artifacts/engine/${artifact_variant}"
-  local flutter_framework="${framework_path}/Flutter.xcframework"
-
-  if [[ -n "$LOCAL_ENGINE" ]]; then
-    if [[ $(echo "$LOCAL_ENGINE" | tr "[:upper:]" "[:lower:]") != *"$build_mode"* ]]; then
-      EchoError "========================================================================"
-      EchoError "ERROR: Requested build with Flutter local engine at '${LOCAL_ENGINE}'"
-      EchoError "This engine is not compatible with FLUTTER_BUILD_MODE: '${build_mode}'."
-      EchoError "You can fix this by updating the LOCAL_ENGINE environment variable, or"
-      EchoError "by running:"
-      EchoError "  flutter build ios --local-engine=ios_${build_mode}"
-      EchoError "or"
-      EchoError "  flutter build ios --local-engine=ios_${build_mode}_unopt"
-      EchoError "========================================================================"
-      exit -1
-    fi
-    flutter_framework="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.xcframework"
-  fi
-  local bitcode_flag=""
-  if [[ "$ENABLE_BITCODE" == "YES" && "$ACTION" == "install" ]]; then
-    bitcode_flag="true"
-  fi
-
-  # TODO(jmagman): use assemble copied engine in add-to-app.
-  if [[ -e "${project_path}/.ios" ]]; then
-    RunCommand rsync -av --delete --filter "- .DS_Store" "${flutter_framework}" "${derived_dir}/engine"
-  fi
-
-  RunCommand pushd "${project_path}" > /dev/null
-
-  # Construct the "flutter assemble" argument array. Arguments should be added
-  # as quoted string elements of the flutter_args array, otherwise an argument
-  # (like a path) with spaces in it might be interpreted as two separate
-  # arguments.
-  local flutter_args=("${FLUTTER_ROOT}/bin/flutter")
-  if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
-    flutter_args+=('--verbose')
-  fi
-  if [[ -n "$FLUTTER_ENGINE" ]]; then
-    flutter_args+=("--local-engine-src-path=${FLUTTER_ENGINE}")
-  fi
-  if [[ -n "$LOCAL_ENGINE" ]]; then
-    flutter_args+=("--local-engine=${LOCAL_ENGINE}")
-  fi
-  flutter_args+=(
-    "assemble"
-    "--no-version-check"
-    "--output=${BUILT_PRODUCTS_DIR}/"
-    "-dTargetPlatform=ios"
-    "-dTargetFile=${target_path}"
-    "-dBuildMode=${build_mode}"
-    "-dIosArchs=${ARCHS}"
-    "-dSdkRoot=${SDKROOT}"
-    "-dSplitDebugInfo=${SPLIT_DEBUG_INFO}"
-    "-dTreeShakeIcons=${TREE_SHAKE_ICONS}"
-    "-dTrackWidgetCreation=${TRACK_WIDGET_CREATION}"
-    "-dDartObfuscation=${DART_OBFUSCATION}"
-    "-dEnableBitcode=${bitcode_flag}"
-    "--ExtraGenSnapshotOptions=${EXTRA_GEN_SNAPSHOT_OPTIONS}"
-    "--DartDefines=${DART_DEFINES}"
-    "--ExtraFrontEndOptions=${EXTRA_FRONT_END_OPTIONS}"
-  )
-  if [[ -n "$PERFORMANCE_MEASUREMENT_FILE" ]]; then
-    flutter_args+=("--performance-measurement-file=${PERFORMANCE_MEASUREMENT_FILE}")
-  fi
-  if [[ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" && "${CODE_SIGNING_REQUIRED:-}" != "NO" ]]; then
-    flutter_args+=("-dCodesignIdentity=${EXPANDED_CODE_SIGN_IDENTITY}")
-  fi
-  if [[ -n "$BUNDLE_SKSL_PATH" ]]; then
-    flutter_args+=("-dBundleSkSLPath=${BUNDLE_SKSL_PATH}")
-  fi
-  if [[ -n "$CODE_SIZE_DIRECTORY" ]]; then
-    flutter_args+=("-dCodeSizeDirectory=${CODE_SIZE_DIRECTORY}")
-  fi
-  flutter_args+=("${build_mode}_ios_bundle_flutter_assets")
-
-  RunCommand "${flutter_args[@]}"
-
-  if [[ $? -ne 0 ]]; then
-    EchoError "Failed to package ${project_path}."
-    exit -1
-  fi
-  StreamOutput "done"
-  StreamOutput " └─Compiling, linking and signing..."
-
-  RunCommand popd > /dev/null
-
-  echo "Project ${project_path} built and packaged successfully."
-  return 0
-}
-
-# Adds the App.framework as an embedded binary and the flutter_assets as
-# resources.
-EmbedFlutterFrameworks() {
-  # Embed App.framework from Flutter into the app (after creating the Frameworks directory
-  # if it doesn't already exist).
-  local xcode_frameworks_dir="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
-  RunCommand mkdir -p -- "${xcode_frameworks_dir}"
-  RunCommand rsync -av --delete --filter "- .DS_Store" "${BUILT_PRODUCTS_DIR}/App.framework" "${xcode_frameworks_dir}"
-
-  # Embed the actual Flutter.framework that the Flutter app expects to run against,
-  # which could be a local build or an arch/type specific build.
-  RunCommand rsync -av --delete --filter "- .DS_Store" "${BUILT_PRODUCTS_DIR}/Flutter.framework" "${xcode_frameworks_dir}/"
-
-  AddObservatoryBonjourService
-}
-
-# Add the observatory publisher Bonjour service to the produced app bundle Info.plist.
-AddObservatoryBonjourService() {
-  local build_mode="$(ParseFlutterBuildMode)"
-  # Debug and profile only.
-  if [[ "${build_mode}" == "release" ]]; then
-    return
-  fi
-  local built_products_plist="${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}"
-
-  if [[ ! -f "${built_products_plist}" ]]; then
-    # Very occasionally Xcode hasn't created an Info.plist when this runs.
-    # The file will be present on re-run.
-    echo "${INFOPLIST_PATH} does not exist. Skipping _dartobservatory._tcp NSBonjourServices insertion. Try re-building to enable \"flutter attach\"."
-    return
-  fi
-  # If there are already NSBonjourServices specified by the app (uncommon), insert the observatory service name to the existing list.
-  if plutil -extract NSBonjourServices xml1 -o - "${built_products_plist}"; then
-    RunCommand plutil -insert NSBonjourServices.0 -string "_dartobservatory._tcp" "${built_products_plist}"
-  else
-    # Otherwise, add the NSBonjourServices key and observatory service name.
-    RunCommand plutil -insert NSBonjourServices -json "[\"_dartobservatory._tcp\"]" "${built_products_plist}"
-  fi
-
-  # Don't override the local network description the Flutter app developer specified (uncommon).
-  # This text will appear below the "Your app would like to find and connect to devices on your local network" permissions popup.
-  if ! plutil -extract NSLocalNetworkUsageDescription xml1 -o - "${built_products_plist}"; then
-    RunCommand plutil -insert NSLocalNetworkUsageDescription -string "Allow Flutter tools on your computer to connect and debug your application. This prompt will not appear on release builds." "${built_products_plist}"
-  fi
-}
-
-# Main entry point.
-if [[ $# == 0 ]]; then
-  # Named entry points were introduced in Flutter v0.0.7.
-  EchoError "error: Your Xcode project is incompatible with this version of Flutter. Run \"rm -rf ios/Runner.xcodeproj\" and \"flutter create .\" to regenerate."
-  exit -1
-else
-  case $1 in
-    "build")
-      BuildApp ;;
-    "thin")
-      # No-op, thinning is handled during the bundle asset assemble build target.
-      ;;
-    "embed")
-      EmbedFlutterFrameworks ;;
-    "embed_and_thin")
-      # Thinning is handled during the bundle asset assemble build target, so just embed.
-      EmbedFlutterFrameworks ;;
-    "test_observatory_bonjour_service")
-      # Exposed for integration testing only.
-      AddObservatoryBonjourService ;;
-  esac
-fi
+"$DART" "$BIN_DIR/xcode_backend.dart" "$@"
diff --git a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart
new file mode 100644
index 0000000..23c7529
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart
@@ -0,0 +1,280 @@
+// 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/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/io.dart';
+
+import '../../bin/xcode_backend.dart';
+import '../src/common.dart';
+import '../src/fake_process_manager.dart';
+
+void main() {
+  late MemoryFileSystem fileSystem;
+
+  setUp(() {
+    fileSystem = MemoryFileSystem();
+  });
+
+  group('build', () {
+    test('exits with useful error message when build mode not set', () {
+      final Directory buildDir = fileSystem.directory('/path/to/builds')
+        ..createSync(recursive: true);
+      final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
+        ..createSync(recursive: true);
+      final File pipe = fileSystem.file('/tmp/pipe')
+        ..createSync(recursive: true);
+      const String buildMode = 'Debug';
+      final TestContext context = TestContext(
+        <String>['build'],
+        <String, String>{
+          'BUILT_PRODUCTS_DIR': buildDir.path,
+          'ENABLE_BITCODE': 'YES',
+          'FLUTTER_ROOT': flutterRoot.path,
+          'INFOPLIST_PATH': 'Info.plist',
+        },
+        commands: <FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              '${flutterRoot.path}/bin/flutter',
+              'assemble',
+              '--no-version-check',
+              '--output=${buildDir.path}/',
+              '-dTargetPlatform=ios',
+              '-dTargetFile=lib/main.dart',
+              '-dBuildMode=${buildMode.toLowerCase()}',
+              '-dIosArchs=',
+              '-dSdkRoot=',
+              '-dSplitDebugInfo=',
+              '-dTreeShakeIcons=',
+              '-dTrackWidgetCreation=',
+              '-dDartObfuscation=',
+              '-dEnableBitcode=',
+              '--ExtraGenSnapshotOptions=',
+              '--DartDefines=',
+              '--ExtraFrontEndOptions=',
+              'debug_ios_bundle_flutter_assets',
+            ],
+          ),
+        ],
+        fileSystem: fileSystem,
+        scriptOutputStreamFile: pipe,
+      );
+      expect(
+          () => context.run(),
+          throwsException,
+      );
+      expect(
+        context.stderr,
+        contains('ERROR: Unknown FLUTTER_BUILD_MODE: null.\n'),
+      );
+    });
+    test('calls flutter assemble', () {
+      final Directory buildDir = fileSystem.directory('/path/to/builds')
+        ..createSync(recursive: true);
+      final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
+        ..createSync(recursive: true);
+      final File pipe = fileSystem.file('/tmp/pipe')
+        ..createSync(recursive: true);
+      const String buildMode = 'Debug';
+      final TestContext context = TestContext(
+        <String>['build'],
+        <String, String>{
+          'BUILT_PRODUCTS_DIR': buildDir.path,
+          'CONFIGURATION': buildMode,
+          'ENABLE_BITCODE': 'YES',
+          'FLUTTER_ROOT': flutterRoot.path,
+          'INFOPLIST_PATH': 'Info.plist',
+        },
+        commands: <FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              '${flutterRoot.path}/bin/flutter',
+              'assemble',
+              '--no-version-check',
+              '--output=${buildDir.path}/',
+              '-dTargetPlatform=ios',
+              '-dTargetFile=lib/main.dart',
+              '-dBuildMode=${buildMode.toLowerCase()}',
+              '-dIosArchs=',
+              '-dSdkRoot=',
+              '-dSplitDebugInfo=',
+              '-dTreeShakeIcons=',
+              '-dTrackWidgetCreation=',
+              '-dDartObfuscation=',
+              '-dEnableBitcode=',
+              '--ExtraGenSnapshotOptions=',
+              '--DartDefines=',
+              '--ExtraFrontEndOptions=',
+              'debug_ios_bundle_flutter_assets',
+            ],
+          ),
+        ],
+        fileSystem: fileSystem,
+        scriptOutputStreamFile: pipe,
+      )..run();
+      final List<String> streamedLines = pipe.readAsLinesSync();
+      // Ensure after line splitting, the exact string 'done' appears
+      expect(streamedLines, contains('done'));
+      expect(streamedLines, contains(' └─Compiling, linking and signing...'));
+      expect(
+        context.stdout,
+        contains('built and packaged successfully.'),
+      );
+      expect(context.stderr, isEmpty);
+    });
+
+    test('forwards all env variables to flutter assemble', () {
+      final Directory buildDir = fileSystem.directory('/path/to/builds')
+        ..createSync(recursive: true);
+      final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
+        ..createSync(recursive: true);
+      const String archs = 'arm64 armv7';
+      const String buildMode = 'Release';
+      const String dartObfuscation = 'false';
+      const String dartDefines = 'flutter.inspector.structuredErrors%3Dtrue';
+      const String expandedCodeSignIdentity = 'F1326572E0B71C3C8442805230CB4B33B708A2E2';
+      const String extraFrontEndOptions = '--some-option';
+      const String extraGenSnapshotOptions = '--obfuscate';
+      const String sdkRoot = '/path/to/sdk';
+      const String splitDebugInfo = '/path/to/split/debug/info';
+      const String trackWidgetCreation = 'true';
+      const String treeShake = 'true';
+      final TestContext context = TestContext(
+        <String>['build'],
+        <String, String>{
+          'ACTION': 'install',
+          'ARCHS': archs,
+          'BUILT_PRODUCTS_DIR': buildDir.path,
+          'CODE_SIGNING_REQUIRED': 'YES',
+          'CONFIGURATION': buildMode,
+          'DART_DEFINES': dartDefines,
+          'DART_OBFUSCATION': dartObfuscation,
+          'ENABLE_BITCODE': 'YES',
+          'EXPANDED_CODE_SIGN_IDENTITY': expandedCodeSignIdentity,
+          'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions,
+          'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions,
+          'FLUTTER_ROOT': flutterRoot.path,
+          'INFOPLIST_PATH': 'Info.plist',
+          'SDKROOT': sdkRoot,
+          'SPLIT_DEBUG_INFO': splitDebugInfo,
+          'TRACK_WIDGET_CREATION': trackWidgetCreation,
+          'TREE_SHAKE_ICONS': treeShake,
+        },
+        commands: <FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              '${flutterRoot.path}/bin/flutter',
+              'assemble',
+              '--no-version-check',
+              '--output=${buildDir.path}/',
+              '-dTargetPlatform=ios',
+              '-dTargetFile=lib/main.dart',
+              '-dBuildMode=${buildMode.toLowerCase()}',
+              '-dIosArchs=$archs',
+              '-dSdkRoot=$sdkRoot',
+              '-dSplitDebugInfo=$splitDebugInfo',
+              '-dTreeShakeIcons=$treeShake',
+              '-dTrackWidgetCreation=$trackWidgetCreation',
+              '-dDartObfuscation=$dartObfuscation',
+              '-dEnableBitcode=true',
+              '--ExtraGenSnapshotOptions=$extraGenSnapshotOptions',
+              '--DartDefines=$dartDefines',
+              '--ExtraFrontEndOptions=$extraFrontEndOptions',
+              '-dCodesignIdentity=$expandedCodeSignIdentity',
+              'release_ios_bundle_flutter_assets',
+            ],
+          ),
+        ],
+        fileSystem: fileSystem,
+      )..run();
+      expect(
+        context.stdout,
+        contains('built and packaged successfully.'),
+      );
+      expect(context.stderr, isEmpty);
+    });
+  });
+
+  group('test_observatory_bonjour_service', () {
+    test('handles when the Info.plist is missing', () {
+      final Directory buildDir = fileSystem.directory('/path/to/builds');
+      buildDir.createSync(recursive: true);
+      final TestContext context = TestContext(
+        <String>['test_observatory_bonjour_service'],
+        <String, String>{
+          'CONFIGURATION': 'Debug',
+          'BUILT_PRODUCTS_DIR': buildDir.path,
+          'INFOPLIST_PATH': 'Info.plist',
+        },
+        commands: <FakeCommand>[],
+        fileSystem: fileSystem,
+      )..run();
+      expect(
+        context.stdout,
+        contains(
+            'Info.plist does not exist. Skipping _dartobservatory._tcp NSBonjourServices insertion.'),
+      );
+    });
+  });
+}
+
+class TestContext extends Context {
+  TestContext(
+    List<String> arguments,
+    Map<String, String> environment, {
+    required this.fileSystem,
+    required List<FakeCommand> commands,
+    File? scriptOutputStreamFile,
+  })  : processManager = FakeProcessManager.list(commands),
+        super(arguments: arguments, environment: environment, scriptOutputStreamFile: scriptOutputStreamFile);
+
+  final FileSystem fileSystem;
+  final FakeProcessManager processManager;
+
+  String stdout = '';
+  String stderr = '';
+
+  @override
+  bool existsDir(String path) {
+    return fileSystem.directory(path).existsSync();
+  }
+
+  @override
+  bool existsFile(String path) {
+    return fileSystem.file(path).existsSync();
+  }
+
+  @override
+  ProcessResult runSync(
+    String bin,
+    List<String> args, {
+    bool verbose = false,
+    bool allowFail = false,
+    String? workingDirectory,
+  }) {
+    return processManager.runSync(
+      <dynamic>[bin, ...args],
+      workingDirectory: workingDirectory,
+      environment: environment,
+    );
+  }
+
+  @override
+  void echoError(String message) {
+    stderr += '$message\n';
+  }
+
+  @override
+  void echo(String message) {
+    stdout += message;
+  }
+
+  @override
+  Never exitApp(int code) {
+    // This is an exception for the benefit of unit tests.
+    // The real implementation calls `exit(code)`.
+    throw Exception('App exited with code $code');
+  }
+}
diff --git a/packages/flutter_tools/test/integration.shard/forbidden_imports_test.dart b/packages/flutter_tools/test/integration.shard/forbidden_imports_test.dart
index 8fef03e..31e3d91 100644
--- a/packages/flutter_tools/test/integration.shard/forbidden_imports_test.dart
+++ b/packages/flutter_tools/test/integration.shard/forbidden_imports_test.dart
@@ -63,6 +63,8 @@
 
   test('no unauthorized imports of dart:io', () {
     final List<String> allowedPaths = <String>[
+      // This is a standalone script invoked by xcode, not part of the tool
+      fileSystem.path.join(flutterTools, 'bin', 'xcode_backend.dart'),
       fileSystem.path.join(flutterTools, 'lib', 'src', 'base', 'io.dart'),
       fileSystem.path.join(flutterTools, 'lib', 'src', 'base', 'platform.dart'),
       fileSystem.path.join(flutterTools, 'lib', 'src', 'base', 'error_handling_io.dart'),