mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

When iOS and macOS Flutter apps build, they use [Xcode Run Scripts](https://developer.apple.com/documentation/xcode/running-custom-scripts-during-a-build) to call do various logic. For iOS, the Run Scripts calls a bash file ([xcode_backend.sh](https://github.com/flutter/flutter/blob/master/packages/flutter_tools/bin/xcode_backend.sh)), which then calls a dart file ([xcode_backend.dart](https://github.com/flutter/flutter/blob/master/packages/flutter_tools/bin/xcode_backend.dart)). `xcode_backend.dart` then calls `flutter assemble`. For macOS, the Run Scripts calls a bash file ([macos_assemble.sh](https://github.com/flutter/flutter/blob/master/packages/flutter_tools/bin/macos_assemble.sh)), which then calls `flutter assemble` directly. This PR changes `macos_assemble.sh` to call `xcode_backend.dart` like it does for iOS so code can be shared between them and written in dart instead of bash. Fixes https://github.com/flutter/flutter/issues/168033. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
682 lines
24 KiB
Dart
682 lines
24 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 String 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 = 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 = Process.runSync(bin, args, workingDirectory: workingDirectory);
|
|
if (verbose) {
|
|
print((result.stdout as String).trim());
|
|
}
|
|
final String resultStderr = result.stderr.toString().trim();
|
|
if (resultStderr.isNotEmpty) {
|
|
final StringBuffer errorOutput = StringBuffer();
|
|
if (result.exitCode != 0) {
|
|
// "error:" prefix makes this show up as an Xcode compilation error.
|
|
errorOutput.write('error: ');
|
|
}
|
|
errorOutput.write(resultStderr);
|
|
echoError(errorOutput.toString());
|
|
}
|
|
if (!allowFail && result.exitCode != 0) {
|
|
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 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 String 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'] ?? '';
|
|
String projectPath = '$sourceRoot/..';
|
|
if (environment['FLUTTER_APPLICATION_PATH'] != null) {
|
|
projectPath = environment['FLUTTER_APPLICATION_PATH']!;
|
|
}
|
|
final String flutterBuildDir = environment['FLUTTER_BUILD_DIR']!;
|
|
final String 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 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 '
|
|
'_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();
|
|
|
|
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,
|
|
}) {
|
|
String 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 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']}');
|
|
}
|
|
|
|
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'] ?? ''}',
|
|
'-dDevDependenciesEnabled=${environment['FLUTTER_DEV_DEPENDENCIES_ENABLED'] ?? ''}',
|
|
]);
|
|
|
|
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 String ephemeralDirectory = '$sourceRoot/Flutter/ephemeral';
|
|
final String buildInputsPath = '$ephemeralDirectory/FlutterInputs.xcfilelist';
|
|
final String 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;
|
|
}
|