[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'),