mirror of
https://github.com/flutter/flutter
synced 2024-09-12 21:01:59 +00:00
[flutter_tools] Port xcode backend to dart (#86753)
This commit is contained in:
parent
93de096e64
commit
2d07436dbd
466
packages/flutter_tools/bin/xcode_backend.dart
Normal file
466
packages/flutter_tools/bin/xcode_backend.dart
Normal 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.');
|
||||
}
|
||||
}
|
|
@ -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" "$@"
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
|
|
Loading…
Reference in a new issue