flutteractionsbot 40aecd76fc
[CP-beta][ios26]Do not report error for Info.plist key not found (#173438)
This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request)
Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request.

### Issue Link:
What is the link to the issue this cherry-pick is addressing?

https://github.com/flutter/flutter/issues/172627

### Changelog Description:
Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples

Prevents a non-fatal error from causing Xcode compilation failures on macOS 26.

### Impact Description:
What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch)

Building iOS flutter apps may crash with Xcode 26.

### Workaround:
Is there a workaround for this issue?

Add NSBonjourServices and NSLocalNetworkUsageDescription settings to your iOS app's Info.plist.

### Risk:
What is the risk level of this cherry-pick?

### Test Coverage:
Are you confident that your fix is well-tested by automated tests?

### Validation Steps:
What are the steps to validate that this fix works?

1. `flutter create` a new app on macOS 26 beta
2. Install Xcode 26 beta 5
3. `flutter run`
2025-08-07 23:16:47 +00:00

749 lines
27 KiB
Dart

// 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;
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':
prepare(platform);
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);
/// 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 = 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, do not fail Xcode build. An example is on macOS 26,
// plutil reports NSBonjourServices key not found via stderr (rather than
// stdout on older macOS), and it should not cause compile failure.
if (!allowFail && result.exitCode != 0) {
// "error:" prefix makes this show up as an Xcode compilation error.
errorOutput.write('error: ');
}
errorOutput.write(resultStderr);
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.
if (!verbose && exitCode == 0) {
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';
// 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();
}
}
void _embedNativeAssets(
TargetPlatform platform, {
required String xcodeFrameworksDir,
required bool codesign,
String? expandedCodeSignIdentity,
}) {
// Copy the native assets.
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 Directory nativeAssetsDir = directoryFromPath(nativeAssetsPath);
if (!nativeAssetsDir.existsSync()) {
if (verbose) {
print("♦ No native assets to bundle. $nativeAssetsPath doesn't exist.");
}
return;
}
if (verbose) {
print('♦ Copying native assets from $nativeAssetsPath.');
}
for (final FileSystemEntity entity in nativeAssetsDir.listSync()) {
if (entity is Directory) {
final String? frameworkName = parseFrameworkNameFromDirectory(entity);
if (frameworkName != null) {
runRsync(
extraArgs: <String>[
'--filter',
'- native_assets.yaml',
'--filter',
'- native_assets.json',
],
entity.path,
xcodeFrameworksDir,
);
if (codesign && expandedCodeSignIdentity != null) {
_codesignFramework(
expandedCodeSignIdentity,
'$xcodeFrameworksDir/$frameworkName.framework/$frameworkName',
);
}
}
}
}
}
void _codesignFramework(String expandedCodeSignIdentity, String frameworkPath) {
runSync('codesign', <String>[
'--force',
'--verbose',
'--sign',
expandedCodeSignIdentity,
'--',
frameworkPath,
]);
}
/// Parse the [dir]'s path to get the framework name. For example,
/// `/path/to/framework_name.framework/` would parse to `framework_name`.
///
/// Returns null if [dir] is not a `.framework`.
static String? parseFrameworkNameFromDirectory(Directory dir) {
final List<String> pathSegments = dir.uri.pathSegments;
if (pathSegments.isEmpty) {
return null;
}
final String basename;
if (pathSegments.last.isEmpty && pathSegments.length > 1) {
basename = pathSegments[pathSegments.length - 2];
} else {
basename = pathSegments.last;
}
final int extensionIndex = basename.indexOf('.framework');
if (extensionIndex == -1) {
return null;
}
return basename.substring(0, extensionIndex);
}
/// 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;
}
// 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,
], allowFail: 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,
], 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,
]);
}
}
/// Calls `flutter assemble [buildMode]_unpack_[platform]` (e.g. `debug_unpack_ios`, `debug_unpack_macos`)
void prepare(TargetPlatform platform) {
// 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: 'prepare',
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();
_validateBuildMode(platform, buildMode);
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.');
}
/// Validate that the build mode targeted matches the build mode set by the
/// Flutter CLI.
/// If it doesn't match, print a warning unless the Xcode action is `install`,
/// which means the app is being archived for distribution. In that case, print
/// an error and fail the build.
///
/// The targeted build mode might not match the one set by Flutter CLI when it
/// is changed and ran directly through Xcode.
///
/// Flutter may change settings or files depending on the build mode. For
/// example, dev dependencies are excluded from release builds and requires
/// the Flutter CLI to update certain files.
void _validateBuildMode(TargetPlatform platform, String currentBuildMode) {
final String? buildModeCLILastUsed = environment['FLUTTER_CLI_BUILD_MODE'];
// Also fail the build if ACTION=install, which indicates the app is being
// built for distribution.
final String? action = environment['ACTION'];
final fatal = action == 'install';
if (buildModeCLILastUsed == null) {
final message =
'Your Flutter build settings are outdated. Please run '
'"flutter build ${platform.name} --config-only --$currentBuildMode" in your Flutter '
'project and try again.\n';
if (fatal) {
echoXcodeError(message);
exitApp(-1);
} else {
echoXcodeWarning(message);
return;
}
}
if (currentBuildMode != buildModeCLILastUsed) {
final message =
'Your Flutter project is currently configured for $buildModeCLILastUsed mode. '
'Please run `flutter build ${platform.name} --config-only --$currentBuildMode` '
'in your Flutter project to update your settings.\n';
if (fatal) {
echoXcodeError(message);
exitApp(-1);
} else {
echoXcodeWarning(message);
}
}
}
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'] ?? ''}',
]);
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 (command == 'prepare') {
// Use the PreBuildAction define flag to force the tool to use a different
// filecache file for the "prepare" command. This will make the environment
// buildPrefix for the "prepare" command unique from the "build" command.
// This will improve caching since the "build" command has more target dependencies.
flutterArgs.add('-dPreBuildAction=PrepareFramework');
}
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('ios'),
macos('macos');
const TargetPlatform(this.name);
final String name;
}