blob: 25358e0cd5ea13befa2856448c2beea0f280fb70 [file] [log] [blame]
// 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:convert';
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;
static const incompatibleErrorMessage =
'Your Xcode project is incompatible with this version of Flutter. '
'Run "rm -rf ios/Runner.xcodeproj" and "flutter create ." to regenerate.\n';
void run() {
if (arguments.isEmpty) {
// Named entry points were introduced in Flutter v0.0.7.
echoXcodeError(incompatibleErrorMessage);
exit(-1);
}
final String subCommand = validateCommand(arguments[0]);
final String? platformName = arguments.length < 2 ? null : arguments[1];
final TargetPlatform platform = parsePlatform(platformName);
switch (subCommand) {
case 'build':
buildApp(platform);
case 'prepare':
unpackFor(platform, 'prepare');
case 'thin':
// No-op, thinning is handled during the bundle asset assemble build target.
break;
case 'embed':
case 'embed_and_thin':
// Thinning is handled during the bundle asset assemble build target, so just embed.
embedFlutterFrameworks(platform);
case 'test_vm_service_bonjour_service':
// Exposed for integration testing only.
addVmServiceBonjourService();
}
}
/// Validates the command argument matches one of the possible commands.
/// Returns null if not.
String validateCommand(String command) {
switch (command) {
case 'build':
case 'prepare':
case 'thin':
case 'embed':
case 'embed_and_thin':
case 'test_vm_service_bonjour_service':
return command;
default:
echoXcodeError(incompatibleErrorMessage);
exit(-1);
}
}
/// Converts the [platformName] argument to a [TargetPlatform]. If there is
/// not a match, prints a warning and defaults to [TargetPlatform.ios].
TargetPlatform parsePlatform(String? platformName) {
switch (platformName) {
case 'macos':
return TargetPlatform.macos;
case 'ios':
return TargetPlatform.ios;
default:
echoXcodeWarning('Unrecognized platform: $platformName. Defaulting to iOS.');
return TargetPlatform.ios;
}
}
bool existsFile(String path) {
final file = File(path);
return file.existsSync();
}
Directory directoryFromPath(String path) => Directory(path);
File fileFromPath(String path) => File(path);
/// Run given command ([bin]) in a synchronous subprocess.
///
/// If [allowFail] is true, an exception will not be thrown even if the process returns a
/// non-zero exit code. Also, `error:` will not be prefixed to the output to prevent Xcode
/// complication failures.
///
/// If [skipErrorLog] is true, `stderr` from the process will not be output unless in [verbose]
/// mode. If in [verbose], pipes `stderr` to `stdout`.
///
/// Will throw [Exception] if the exit code is not 0.
ProcessResult runSync(
String bin,
List<String> args, {
bool verbose = false,
bool allowFail = false,
bool skipErrorLog = false,
String? workingDirectory,
}) {
if (verbose) {
print('♦ $bin ${args.join(' ')}');
}
final ProcessResult result = runSyncProcess(bin, args, workingDirectory: workingDirectory);
if (verbose) {
print((result.stdout as String).trim());
}
final String resultStderr = result.stderr.toString().trim();
if (resultStderr.isNotEmpty) {
final errorOutput = StringBuffer();
if (!allowFail && result.exitCode != 0) {
// "error:" prefix makes this show up as an Xcode compilation error.
errorOutput.write('error: ');
}
errorOutput.write(resultStderr);
if (skipErrorLog) {
// Even if skipErrorLog, we still want to write to stdout if verbose.
if (verbose) {
echo(errorOutput.toString());
}
} else {
echoError(errorOutput.toString());
}
// Stream stderr to the Flutter build process.
// When in verbose mode, `echoError` above will show the logs. So only
// stream if not in verbose mode to avoid duplicate logs.
// Also, only stream if exitCode is 0 since errors are handled separately
// by the tool on failure.
// Also check for `skipErrorLog`, because some errors should not be printed
// out. For example, on macOS 26, plutil reports NSBonjourServices key not
// found as an error. However, logging it in non-verbose mode would be
// confusing, since not having the key is one of the expected states.
if (!verbose && exitCode == 0 && !skipErrorLog) {
streamOutput(errorOutput.toString());
}
}
if (!allowFail && result.exitCode != 0) {
throw Exception('Command "$bin ${args.join(' ')}" exited with code ${result.exitCode}');
}
return result;
}
// TODO(hellohuanlin): Instead of using inheritance to stub the function in
// the subclass, we should favor composition by injecting the dependencies.
// See: https://github.com/flutter/flutter/issues/173133
ProcessResult runSyncProcess(String bin, List<String> args, {String? workingDirectory}) {
return Process.runSync(bin, args, workingDirectory: workingDirectory);
}
/// Log message to stderr.
void echoError(String message) {
stderr.writeln(message);
}
/// Log message to stderr.
void echoXcodeError(String message) {
stderr.writeln('error: $message');
}
/// Log message appended with `warning:` to stderr.
/// This will display with a yellow warning icon in Xcode.
void echoXcodeWarning(String message) {
stderr.writeln('warning: $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');
}
/// Parses and normalizes the build mode (debug, profile, release).
///
/// Uses `FLUTTER_BUILD_MODE` (uncommon) if set, otherwise uses `CONFIGURATION`.
/// The `CONFIGURATION` may not match exactly since it can be named by the developer.
/// If the `FLUTTER_BUILD_MODE` and `CONFIGURATION` do not contain either
/// debug, profile, or release, prints an error and exits the build.
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);
}
/// Copies all files from [source] to [destination].
///
/// Does not copy `.DS_Store`.
///
/// Deletes extraneous files from [destination].
void runRsync(String source, String destination, {List<String> extraArgs = const <String>[]}) {
runSync('rsync', <String>[
'-8', // Avoid mangling filenames with encodings that do not match the current locale.
'-av',
'--delete',
'--filter',
'- .DS_Store',
...extraArgs,
source,
destination,
]);
}
/// Embeds the App.framework, Flutter/FlutterMacOS.framework, and any native
/// asset frameworks into the app.
///
/// On macOS, also codesigns the framework binaries. Codesigning occurs here rather
/// than during the Run Script `build` phase because the `EXPANDED_CODE_SIGN_IDENTITY`
/// is not passed in the build settings during the `build` phase for macOS.
///
/// On iOS, also injects local network permissions into the app's Info.plist.
void embedFlutterFrameworks(TargetPlatform platform) {
// Embed App.framework from Flutter into the app (after creating the Frameworks directory
// if it doesn't already exist).
final xcodeFrameworksDir =
'${environment['TARGET_BUILD_DIR']}/${environment['FRAMEWORKS_FOLDER_PATH']}';
runSync('mkdir', <String>['-p', '--', xcodeFrameworksDir]);
runRsync('${environment['BUILT_PRODUCTS_DIR']}/App.framework', xcodeFrameworksDir);
final String? expandedCodeSignIdentity = environment['EXPANDED_CODE_SIGN_IDENTITY'];
final bool codesign =
platform == TargetPlatform.macos &&
expandedCodeSignIdentity != null &&
expandedCodeSignIdentity.isNotEmpty &&
environment['CODE_SIGNING_REQUIRED'] != 'NO';
var shouldEmbedFlutterFramework = true;
if (_usingFlutterFrameworkSwiftPackage()) {
final bool isFrameworkCorrect = _validateFlutterFramework(
buildMode: parseFlutterBuildMode(),
platform: platform,
builtProductsDir: environment['BUILT_PRODUCTS_DIR'],
targetBuildDir: xcodeFrameworksDir,
);
if (isFrameworkCorrect) {
// If the engine is correct, skip embedding.
shouldEmbedFlutterFramework = false;
} else {
// If the engine is wrong in either BUILT_PRODUCTS_DIR or TARGET_BUILD_DIR, call unpack
// again so the correct framework is copied into BUILT_PRODUCTS_DIR to then be embedded
// into TARGET_BUILD_DIR below.
unpackFor(platform, 'embed');
}
}
if (shouldEmbedFlutterFramework) {
// 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.
switch (platform) {
case TargetPlatform.ios:
runRsync(
'${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework',
'$xcodeFrameworksDir/',
);
case TargetPlatform.macos:
runRsync(
extraArgs: <String>['--filter', '- Headers', '--filter', '- Modules'],
'${environment['BUILT_PRODUCTS_DIR']}/FlutterMacOS.framework',
'$xcodeFrameworksDir/',
);
if (codesign) {
_codesignFramework(expandedCodeSignIdentity, '$xcodeFrameworksDir/App.framework/App');
_codesignFramework(
expandedCodeSignIdentity,
'$xcodeFrameworksDir/FlutterMacOS.framework/FlutterMacOS',
);
}
}
}
_embedNativeAssets(
platform,
xcodeFrameworksDir: xcodeFrameworksDir,
codesign: codesign,
expandedCodeSignIdentity: expandedCodeSignIdentity,
);
if (platform == TargetPlatform.ios) {
addVmServiceBonjourService();
}
}
/// Returns `true` if a directory exists at `FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH`.
bool _usingFlutterFrameworkSwiftPackage() {
final String? swiftPackagePath = environment['FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH'];
if (swiftPackagePath == null) {
return false;
}
final swiftPackage = Directory(swiftPackagePath);
return swiftPackage.existsSync();
}
/// Returns `true` if the Flutter/FlutterMacOS framework Info.plist in [builtProductsDir] and
/// [targetBuildDir] matches the framework Info.plist from the engine cache for the
/// corresponding [platform] and [buildMode].
///
/// The Info.plist contains the build mode and engine version.
///
/// This validation will always fail when using a local engine since the engine version will not match.
bool _validateFlutterFramework({
required String buildMode,
required TargetPlatform platform,
required String? builtProductsDir,
required String? targetBuildDir,
}) {
if (builtProductsDir == null) {
echo('Unable to locate $builtProductsDir; falling back to direct embedding.');
return false;
}
if (targetBuildDir == null) {
echo('Unable to locate $targetBuildDir; falling back to direct embedding.');
return false;
}
try {
final infoPlistFromBuild = File('$builtProductsDir/${platform.infoPlistPath}');
if (!infoPlistFromBuild.existsSync()) {
echo('Unable to locate $infoPlistFromBuild; falling back to direct embedding.');
return false;
}
final infoPlistFromEmbedded = File('$targetBuildDir/${platform.infoPlistPath}');
if (!infoPlistFromEmbedded.existsSync()) {
echo('Unable to locate $infoPlistFromEmbedded; falling back to direct embedding.');
return false;
}
final File? infoPlistFromEngineCache = _infoPlistFromEngineCache(buildMode, platform);
if (infoPlistFromEngineCache == null || !infoPlistFromEngineCache.existsSync()) {
echo('Unable to locate $infoPlistFromEngineCache; falling back to direct embedding.');
return false;
}
final String expectedInfoPlist = infoPlistFromEngineCache.readAsStringSync();
if (infoPlistFromBuild.readAsStringSync() != expectedInfoPlist) {
echo(
'Initially processed Flutter framework did not match expectations; falling back to direct embedding.',
);
return false;
}
if (infoPlistFromEmbedded.readAsStringSync() != expectedInfoPlist) {
echo(
'Initially embedded Flutter framework did not match expectations; falling back to direct embedding.',
);
return false;
}
return infoPlistFromBuild.readAsStringSync() == expectedInfoPlist &&
infoPlistFromEmbedded.readAsStringSync() == expectedInfoPlist;
} on Exception catch (e) {
// Use `echo` instead of `echoError` so it does not cause the build to fail.
echo('$e\n');
echo(
'An error occured while validating the Flutter framework; falling back to direct embedding.\n',
);
}
return false;
}
/// Find the Info.plist of the Flutter/FlutterMacOS framework for the corresponding [buildMode]
/// and [platform].
File? _infoPlistFromEngineCache(String buildMode, TargetPlatform platform) {
final String artifactMode = buildMode == 'debug'
? platform.artifactName
: '${platform.artifactName}-$buildMode';
final xcframework = Directory(
'${environment['FLUTTER_ROOT'] ?? ''}/bin/cache/artifacts/engine/$artifactMode/${platform.frameworkName}.xcframework',
);
switch (platform) {
case TargetPlatform.ios:
final String? sdkRoot = environment['SDKROOT']?.toLowerCase();
if (sdkRoot == null || !sdkRoot.contains('iphone')) {
return null;
}
final bool simulatorSDK = sdkRoot.contains('simulator');
for (final FileSystemEntity entity in xcframework.listSync()) {
final String platformBaseName = Uri.parse(entity.path).pathSegments.last;
if (entity is Directory && platformBaseName.startsWith('ios-')) {
final bool isSimulatorDirectory = platformBaseName.endsWith('-simulator');
if (simulatorSDK == isSimulatorDirectory) {
return File('${entity.path}/${platform.infoPlistPath}');
}
}
}
case TargetPlatform.macos:
for (final FileSystemEntity entity in xcframework.listSync()) {
final String platformBaseName = Uri.parse(entity.path).pathSegments.last;
if (entity is Directory && platformBaseName.startsWith('macos-')) {
return File('${entity.path}/${platform.infoPlistPath}');
}
}
}
return null;
}
void _embedNativeAssets(
TargetPlatform platform, {
required String xcodeFrameworksDir,
required bool codesign,
String? expandedCodeSignIdentity,
}) {
// Copy native assets referenced in the native_assets.json file for the
// current build.
final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
var projectPath = '$sourceRoot/..';
if (environment['FLUTTER_APPLICATION_PATH'] != null) {
projectPath = environment['FLUTTER_APPLICATION_PATH']!;
}
final String flutterBuildDir = environment['FLUTTER_BUILD_DIR']!;
final nativeAssetsPath = '$projectPath/$flutterBuildDir/native_assets/${platform.name}/';
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
final Set<String> referencedFrameworks = {};
final appResourcesDir = platform == TargetPlatform.macos ? 'Resources/' : '';
final File nativeAssetsJson = fileFromPath(
'$xcodeFrameworksDir/App.framework/${appResourcesDir}flutter_assets/NativeAssetsManifest.json',
);
if (!nativeAssetsJson.existsSync()) {
if (verbose) {
print("♦ No native assets to bundle. ${nativeAssetsJson.path} doesn't exist.");
}
return;
}
// NativeAssetsManifest.json looks like this: {
// "format-version":[1,0,0],
// "native-assets":{
// "ios_arm64":{
// "package:sqlite3/src/ffi/libsqlite3.g.dart":[
// "absolute",
// "sqlite3arm64ios.framework/sqlite3arm64ios"
// ]
// }
// }
// }
//
// Note that this format is also parsed and expected in
// engine/src/flutter/assets/native_assets.cc
try {
final nativeAssetsSpec = json.decode(nativeAssetsJson.readAsStringSync()) as Map;
for (final Object? perPlatform
in (nativeAssetsSpec['native-assets'] as Map<String, Object?>).values) {
for (final Object? asset in (perPlatform! as Map<String, Object?>).values) {
if (asset case ['absolute', final String frameworkPath]) {
// frameworkPath is usually something like sqlite3arm64ios.framework/sqlite3arm64ios
final [String directory, String name] = frameworkPath.split('/');
if (directory != '$name.framework') {
throw Exception(
'Unexpected framework path: $frameworkPath. Should be $name.framework/$name',
);
}
referencedFrameworks.add(name);
}
}
}
} on Object catch (e, stackTrace) {
echo(e.toString());
echo(stackTrace.toString());
echoXcodeError('Failed to embed native assets: $e');
exitApp(-1);
}
if (verbose) {
print('♦ Copying native assets ${referencedFrameworks.join(', ')} from $nativeAssetsPath.');
}
for (final framework in referencedFrameworks) {
final Directory frameworkDirectory = directoryFromPath(
'$nativeAssetsPath$framework.framework',
);
if (!frameworkDirectory.existsSync()) {
throw Exception(
'The native assets specification at ${nativeAssetsJson.path} references $framework, '
'which was not found in $nativeAssetsPath.',
);
}
runRsync(
extraArgs: <String>['--filter', '- native_assets.yaml', '--filter', '- native_assets.json'],
frameworkDirectory.path,
xcodeFrameworksDir,
);
if (codesign && expandedCodeSignIdentity != null) {
_codesignFramework(
expandedCodeSignIdentity,
'$xcodeFrameworksDir/$framework.framework/$framework',
);
}
}
}
void _codesignFramework(String expandedCodeSignIdentity, String frameworkPath) {
runSync('codesign', <String>[
'--force',
'--verbose',
'--sign',
expandedCodeSignIdentity,
'--',
frameworkPath,
]);
}
/// Add the vmService publisher Bonjour service to the produced app bundle Info.plist.
void addVmServiceBonjourService() {
// Skip adding Bonjour service settings when DISABLE_PORT_PUBLICATION is YES.
// These settings are not needed if port publication is disabled.
if (environment['DISABLE_PORT_PUBLICATION'] == 'YES') {
return;
}
final String buildMode = parseFlutterBuildMode();
// Debug and profile only.
if (buildMode == 'release') {
return;
}
final 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 '
'_dartVmService._tcp NSBonjourServices insertion. Try re-building to '
'enable "flutter attach".',
);
return;
}
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
// If there are already NSBonjourServices specified by the app (uncommon),
// insert the vmService service name to the existing list.
ProcessResult result = runSync(
'plutil',
<String>['-extract', 'NSBonjourServices', 'xml1', '-o', '-', builtProductsPlist],
verbose: verbose,
allowFail: true,
skipErrorLog: true,
);
if (result.exitCode == 0) {
runSync('plutil', <String>[
'-insert',
'NSBonjourServices.0',
'-string',
'_dartVmService._tcp',
builtProductsPlist,
]);
} else {
// Otherwise, add the NSBonjourServices key and vmService service name.
runSync('plutil', <String>[
'-insert',
'NSBonjourServices',
'-json',
'["_dartVmService._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],
verbose: verbose,
allowFail: true,
skipErrorLog: 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,
]);
}
}
/// Calls `flutter assemble [buildMode]_unpack_[platform]` (e.g. `debug_unpack_ios`, `debug_unpack_macos`)
void unpackFor(TargetPlatform platform, String command) {
// The "prepare" command runs in a pre-action script, which also runs when
// using the Xcode/xcodebuild clean command. Skip if cleaning.
if (environment['ACTION'] == 'clean') {
return;
}
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
final String projectPath = environment['FLUTTER_APPLICATION_PATH'] ?? '$sourceRoot/..';
final String buildMode = parseFlutterBuildMode();
final List<String> flutterArgs = _generateFlutterArgsForAssemble(
command: command,
buildMode: buildMode,
sourceRoot: sourceRoot,
platform: platform,
verbose: verbose,
);
// The "prepare" command only targets the UnpackIOS/UnpackMacOS target, which copies the
// Flutter framework to the BUILT_PRODUCTS_DIR.
flutterArgs.add('${buildMode}_unpack_${platform.name}');
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 copy Flutter framework.');
exitApp(-1);
}
}
/// Calls `flutter assemble [buildMode]_[platform]_bundle_flutter_assets`
/// (e.g. `debug_ios_bundle_flutter_assets`, `debug_macos_bundle_flutter_assets`)
void buildApp(TargetPlatform platform) {
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
final String sourceRoot = environment['SOURCE_ROOT'] ?? '';
final String projectPath = environment['FLUTTER_APPLICATION_PATH'] ?? '$sourceRoot/..';
final String buildMode = parseFlutterBuildMode();
final List<String> flutterArgs = _generateFlutterArgsForAssemble(
command: 'build',
buildMode: buildMode,
sourceRoot: sourceRoot,
platform: platform,
verbose: verbose,
);
flutterArgs.add('${buildMode}_${platform.name}_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.');
}
List<String> _generateFlutterArgsForAssemble({
required String command,
required String buildMode,
required String sourceRoot,
required TargetPlatform platform,
required bool verbose,
}) {
var targetPath = 'lib/main.dart';
if (environment['FLUTTER_TARGET'] != null) {
targetPath = environment['FLUTTER_TARGET']!;
}
// Warn the user if not archiving (ACTION=install) in release mode.
final String? action = environment['ACTION'];
if (action == 'install' && buildMode != 'release') {
echoXcodeWarning(
'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 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']}');
}
if (environment['LOCAL_ENGINE_HOST'] != null && environment['LOCAL_ENGINE_HOST']!.isNotEmpty) {
flutterArgs.add('--local-engine-host=${environment['LOCAL_ENGINE_HOST']}');
}
// The "prepare" command runs in a pre-action script, which doesn't always
// filter the "ARCHS" build setting. Attempt to filter the architecture
// to improve caching. If this filter is incorrect, it will later be
// corrected by the "build" command.
String archs = environment['ARCHS'] ?? '';
if (command == 'prepare' && archs.contains(' ')) {
// If "ONLY_ACTIVE_ARCH" is "YES", the product includes only code for the
// native architecture ("NATIVE_ARCH").
final String? nativeArch = environment['NATIVE_ARCH'];
if (environment['ONLY_ACTIVE_ARCH'] == 'YES' && nativeArch != null) {
if (nativeArch.contains('arm64') && archs.contains('arm64')) {
archs = 'arm64';
} else if (nativeArch.contains('x86_64') && archs.contains('x86_64')) {
archs = 'x86_64';
}
}
}
final String targetPlatform;
final String platformArches;
switch (platform) {
case TargetPlatform.ios:
targetPlatform = '-dTargetPlatform=ios';
platformArches = '-dIosArchs=$archs';
case TargetPlatform.macos:
targetPlatform = '-dTargetPlatform=darwin';
platformArches = '-dDarwinArchs=$archs';
}
flutterArgs.addAll(<String>[
'assemble',
'--no-version-check',
'--output=${environment['BUILT_PRODUCTS_DIR'] ?? ''}/',
targetPlatform,
'-dTargetFile=$targetPath',
'-dBuildMode=$buildMode',
// FLAVOR is set by the Flutter CLI in the Flutter/Generated.xcconfig file
// when the --flavor flag is used, so it may not always be present.
if (environment['FLAVOR'] != null) '-dFlavor=${environment['FLAVOR']}',
'-dConfiguration=${environment['CONFIGURATION']}',
platformArches,
'-dSdkRoot=${environment['SDKROOT'] ?? ''}',
'-dSplitDebugInfo=${environment['SPLIT_DEBUG_INFO'] ?? ''}',
'-dTreeShakeIcons=${environment['TREE_SHAKE_ICONS'] ?? ''}',
'-dTrackWidgetCreation=${environment['TRACK_WIDGET_CREATION'] ?? ''}',
'-dDartObfuscation=${environment['DART_OBFUSCATION'] ?? ''}',
'-dAction=${environment['ACTION'] ?? ''}',
'-dFrontendServerStarterPath=${environment['FRONTEND_SERVER_STARTER_PATH'] ?? ''}',
'--ExtraGenSnapshotOptions=${environment['EXTRA_GEN_SNAPSHOT_OPTIONS'] ?? ''}',
'--DartDefines=${environment['DART_DEFINES'] ?? ''}',
'--ExtraFrontEndOptions=${environment['EXTRA_FRONT_END_OPTIONS'] ?? ''}',
'-dSrcRoot=${environment['SRCROOT'] ?? ''}',
'-dXcodeBuildScript=$command',
]);
if (platform == TargetPlatform.ios) {
flutterArgs.add('-dTargetDeviceOSVersion=${environment['TARGET_DEVICE_OS_VERSION'] ?? ''}');
final String? expandedCodeSignIdentity = environment['EXPANDED_CODE_SIGN_IDENTITY'];
if (expandedCodeSignIdentity != null &&
expandedCodeSignIdentity.isNotEmpty &&
environment['CODE_SIGNING_REQUIRED'] != 'NO') {
flutterArgs.add('-dCodesignIdentity=$expandedCodeSignIdentity');
}
}
if (platform == TargetPlatform.macos && command == 'build') {
final ephemeralDirectory = '$sourceRoot/Flutter/ephemeral';
final buildInputsPath = '$ephemeralDirectory/FlutterInputs.xcfilelist';
final buildOutputsPath = '$ephemeralDirectory/FlutterOutputs.xcfilelist';
flutterArgs.addAll(<String>[
'--build-inputs=$buildInputsPath',
'--build-outputs=$buildOutputsPath',
]);
}
if (environment['PERFORMANCE_MEASUREMENT_FILE'] != null &&
environment['PERFORMANCE_MEASUREMENT_FILE']!.isNotEmpty) {
flutterArgs.add(
'--performance-measurement-file=${environment['PERFORMANCE_MEASUREMENT_FILE']}',
);
}
if (environment['CODE_SIZE_DIRECTORY'] != null &&
environment['CODE_SIZE_DIRECTORY']!.isNotEmpty) {
flutterArgs.add('-dCodeSizeDirectory=${environment['CODE_SIZE_DIRECTORY']}');
}
return flutterArgs;
}
}
enum TargetPlatform {
ios(artifactName: 'ios', frameworkName: 'Flutter', infoPlistPath: 'Flutter.framework/Info.plist'),
macos(
artifactName: 'darwin-x64',
frameworkName: 'FlutterMacOS',
infoPlistPath: 'FlutterMacOS.framework/Resources/Info.plist',
);
const TargetPlatform({
required this.artifactName,
required this.frameworkName,
required this.infoPlistPath,
});
final String artifactName;
final String frameworkName;
final String infoPlistPath;
}