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

This commit is contained in:
Christopher Fujino 2021-07-27 14:39:05 -07:00 committed by GitHub
parent 93de096e64
commit 2d07436dbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 768 additions and 259 deletions

View file

@ -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.');
}
}

View file

@ -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" "$@"

View file

@ -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');
}
}

View file

@ -63,6 +63,8 @@ void main() {
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'),