diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart new file mode 100644 index 0000000000..04f86e5d20 --- /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 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 environment; + final List 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 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', + [ + '-p', + '--', + xcodeFrameworksDir, + ] + ); + runSync( + 'rsync', + [ + '-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', + [ + '-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', + [ + '-extract', + 'NSBonjourServices', + 'xml1', + '-o', + '-', + builtProductsPlist, + ], + allowFail: true, + ); + if (result.exitCode == 0) { + runSync( + 'plutil', + [ + '-insert', + 'NSBonjourServices.0', + '-string', + '_dartobservatory._tcp', + builtProductsPlist + ], + ); + } else { + // Otherwise, add the NSBonjourServices key and observatory service name. + runSync( + 'plutil', + [ + '-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', + [ + '-extract', + 'NSLocalNetworkUsageDescription', + 'xml1', + '-o', + '-', + builtProductsPlist, + ], + allowFail: true, + ); + if (result.exitCode != 0) { + runSync( + 'plutil', + [ + '-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', + [ + '-av', + '--delete', + '--filter', + '- .DS_Store', + flutterFramework, + '$derivedDir/engine', + ], + verbose: verbose, + ); + } + + final List flutterArgs = []; + + 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([ + '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 3c99a2f167..2889d7c8e4 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 0000000000..23c7529eb5 --- /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( + ['build'], + { + 'BUILT_PRODUCTS_DIR': buildDir.path, + 'ENABLE_BITCODE': 'YES', + 'FLUTTER_ROOT': flutterRoot.path, + 'INFOPLIST_PATH': 'Info.plist', + }, + commands: [ + FakeCommand( + command: [ + '${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( + ['build'], + { + 'BUILT_PRODUCTS_DIR': buildDir.path, + 'CONFIGURATION': buildMode, + 'ENABLE_BITCODE': 'YES', + 'FLUTTER_ROOT': flutterRoot.path, + 'INFOPLIST_PATH': 'Info.plist', + }, + commands: [ + FakeCommand( + command: [ + '${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 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( + ['build'], + { + '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( + command: [ + '${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( + ['test_observatory_bonjour_service'], + { + 'CONFIGURATION': 'Debug', + 'BUILT_PRODUCTS_DIR': buildDir.path, + 'INFOPLIST_PATH': 'Info.plist', + }, + commands: [], + fileSystem: fileSystem, + )..run(); + expect( + context.stdout, + contains( + 'Info.plist does not exist. Skipping _dartobservatory._tcp NSBonjourServices insertion.'), + ); + }); + }); +} + +class TestContext extends Context { + TestContext( + List arguments, + Map environment, { + required this.fileSystem, + required List 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 args, { + bool verbose = false, + bool allowFail = false, + String? workingDirectory, + }) { + return processManager.runSync( + [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 8fef03e765..31e3d91c1d 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 @@ void main() { test('no unauthorized imports of dart:io', () { final List allowedPaths = [ + // 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'),