Daco Harkes 6e459a3860
[native_assets] Fix flutter build ios-framework (#181507)
This PR fixes two issues. Accidental reuse of code assets between build
modes and SDKs (https://github.com/flutter/flutter/issues/181724), and
the bundling in ios-frameworks
(https://github.com/flutter/flutter/issues/181382).

To fix the accidental caching, the `Target`s related to build hooks and
code assets now output their files to `environment.outputDir` instead of
`$projectDir/$buildDir/native_assets`.

* `xcode_backend` is updated to deal with this.
* `Flutter.kt` has been updated to deal with this.
* Because the `Target`s are responsible for caching, the code has been
refactored to provide the target directories from there. The
"global-ish" function `nativeAssetsBuildUri` that was calculating the
directory before has been removed.
* `runFlutterSpecificHooks` has nothing to do with that directory, it's
access to it has been removed.
* To avoid another cmakefile migration, the Linux and Windows
implementation use the same directory. (Note that output dir and build
dir overlap for Linux and Windows, while they do not for MacOS, iOS, and
Android.)
* This also means that we don't have to read `NativeAssetsManifest.json`
in `xcode_backend` anymore. Instead the `Target` clears the output
directory, so we should not have any stale frameworks.
* Refactored `installCodeAssets` and its platform-specific
implementations to return a list of all produced files. These are now
added to the `Target`'s depfile. This fixes an issue where the build
system would skip re-installing native assets after an Xcode "Clean
Build Folder because it wasn't tracking the frameworks/dylibs as
outputs.

Closes: https://github.com/flutter/flutter/issues/181724

Other `Target`s related tweaks:

* Added proper
`Source.pattern('{BUILD_DIR}/${DartBuild.dartHookResultFilename}'),` for
all `Target`s that depend on that file. These were missing. (The build
system uses `dependencies` for ordering of `Target`s, but relies on
`inputs` and `outputs` for caching.)
* Removed code assets from `CopyAssets`. That target is supposed to make
an asset-bundle that is OS-independent if I understand correctly.

This PR changes the way code assets are bundled in `flutter build
ios-framework`.

* This PR now packages in an `.xcframework`, which is necessary to be
able to package both device and simulator.
* Run through the frameworks of both device and simulator and give
errors on inconsistencies.

Closes: https://github.com/flutter/flutter/issues/181382

Other iOS related tweaks:

* Use `xcrun` for invoking all the commands. (This is used for producing
the app framework, but was not for code assets frameworks.)
* Make sure all commands are added to the traces when running verbose.
(Also to bring it in line for with the other `xcrun` commands.)

Testing:

* The integration test is updated to inspect the `xcframework`s.
* Added a test that simulates Xcode "Product > Clean Build Folder...",
to check that it now correctly triggers a rebuild.
* We do _not_ have an integration test that _runs_ the frameworks output
from `flutter build ios-framework` inside a host app at all as far as
I'm aware.
* dev/devicelab/bin/tasks/build_ios_framework_module_test.dart builds a
framework, but doesn't run it in a host app
* dev/integration_tests/ios_add2app_life_cycle/build_and_test.sh runs,
but does so via `flutter build ios` not as a framework.
* Does not add an integration test for caching behavior between
switching build modes. However, the proper functioning of `flutter build
ios-framework` depends on the `Target`s for different not using
overlapping directories.

Architectural approaches tried but didn't work:

* Subclass `InstallCodeAssets` per OS to be more precise in the
`output`s on what files are output. This doesn't work because other
OS-independent targets on the `InstallCodeAssets` target.
2026-02-12 15:33:43 +00:00

704 lines
25 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':
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);
/// 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]);
final String? expandedCodeSignIdentity = environment['EXPANDED_CODE_SIGN_IDENTITY'];
final bool codesign =
platform == TargetPlatform.macos &&
expandedCodeSignIdentity != null &&
expandedCodeSignIdentity.isNotEmpty &&
environment['CODE_SIGNING_REQUIRED'] != 'NO';
_embedAppFramework(xcodeFrameworksDir, codesign ? expandedCodeSignIdentity : null);
// 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/FlutterMacOS.framework/FlutterMacOS',
);
}
}
_embedNativeAssets(
platform,
xcodeFrameworksDir: xcodeFrameworksDir,
codesign: codesign,
expandedCodeSignIdentity: expandedCodeSignIdentity,
);
if (platform == TargetPlatform.ios) {
addVmServiceBonjourService();
}
}
void _embedAppFramework(String xcodeFrameworksDir, String? expandedCodeSignIdentity) {
runRsync('${environment['BUILT_PRODUCTS_DIR']}/App.framework', xcodeFrameworksDir);
if (expandedCodeSignIdentity != null) {
_codesignFramework(expandedCodeSignIdentity, '$xcodeFrameworksDir/App.framework/App');
}
}
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 builtProductsDir = environment['BUILT_PRODUCTS_DIR']!;
final nativeAssetsPath = '$builtProductsDir/native_assets/';
final Directory nativeAssetsDir = directoryFromPath(nativeAssetsPath);
final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty;
if (!nativeAssetsDir.existsSync()) {
if (verbose) {
print("♦ No native assets to bundle. $nativeAssetsPath doesn't exist.");
}
return;
}
final Iterable<String> frameworks = nativeAssetsDir
.listSync()
.whereType<Directory>()
.where((Directory d) => !d.path.endsWith('.dSYM'))
.map(_parseFrameworkNameFromDirectory)
.whereType<String>();
if (verbose) {
print('♦ Copying native assets ${frameworks.join(', ')} from $nativeAssetsPath.');
}
for (final framework in frameworks) {
final Directory frameworkDirectory = directoryFromPath(
'$nativeAssetsPath$framework.framework',
);
runRsync(frameworkDirectory.path, xcodeFrameworksDir);
if (codesign && expandedCodeSignIdentity != null) {
_codesignFramework(
expandedCodeSignIdentity,
'$xcodeFrameworksDir/$framework.framework/$framework',
);
}
final Directory dsymDirectory = directoryFromPath(
'$nativeAssetsPath$framework.framework.dSYM',
);
if (dsymDirectory.existsSync()) {
runRsync(dsymDirectory.path, '${environment['BUILT_PRODUCTS_DIR']}/');
}
}
}
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, macos }
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);
}