// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:io'; void main(List<String> arguments) { File? scriptOutputStreamFile; final String? scriptOutputStreamFileEnv = Platform.environment['SCRIPT_OUTPUT_STREAM_FILE']; if (scriptOutputStreamFileEnv != null && scriptOutputStreamFileEnv.isNotEmpty) { scriptOutputStreamFile = File(scriptOutputStreamFileEnv); } Context( arguments: arguments, environment: Platform.environment, scriptOutputStreamFile: scriptOutputStreamFile, ).run(); } /// Container for script arguments and environment variables. /// /// All interactions with the platform are broken into individual methods that /// can be overridden in tests. class Context { Context({ required this.arguments, required this.environment, File? scriptOutputStreamFile, }) { if (scriptOutputStreamFile != null) { scriptOutputStream = scriptOutputStreamFile.openSync(mode: FileMode.write); } } final Map<String, String> environment; final List<String> arguments; RandomAccessFile? scriptOutputStream; void run() { if (arguments.isEmpty) { // Named entry points were introduced in Flutter v0.0.7. stderr.write( 'error: Your Xcode project is incompatible with this version of Flutter. ' 'Run "rm -rf ios/Runner.xcodeproj" and "flutter create ." to regenerate.\n'); exit(-1); } final String subCommand = arguments.first; switch (subCommand) { case 'build': buildApp(); case 'thin': // No-op, thinning is handled during the bundle asset assemble build target. break; case 'embed': embedFlutterFrameworks(); case 'embed_and_thin': // Thinning is handled during the bundle asset assemble build target, so just embed. embedFlutterFrameworks(); case 'test_vm_service_bonjour_service': // Exposed for integration testing only. addVmServiceBonjourService(); } } bool existsFile(String path) { final File file = File(path); return file.existsSync(); } /// Run given command in a synchronous subprocess. /// /// Will throw [Exception] if the exit code is not 0. ProcessResult runSync( String bin, List<String> args, { bool verbose = false, bool allowFail = false, String? workingDirectory, }) { if (verbose) { print('♦ $bin ${args.join(' ')}'); } final ProcessResult result = Process.runSync( bin, args, workingDirectory: workingDirectory, ); if (verbose) { print((result.stdout as String).trim()); } 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 stdout. void echo(String message) { stdout.write(message); } /// Exit the application with the given exit code. /// /// Exists to allow overriding in tests. Never exitApp(int code) { exit(code); } /// Return value from environment if it exists, else throw [Exception]. String environmentEnsure(String key) { final String? value = environment[key]; if (value == null) { throw Exception( 'Expected the environment variable "$key" to exist, but it was not found', ); } return value; } // When provided with a pipe by the host Flutter build process, output to the // pipe goes to stdout of the Flutter build process directly. void streamOutput(String output) { scriptOutputStream?.writeStringSync('$output\n'); } String parseFlutterBuildMode() { // Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name // This means that if someone wants to use an Xcode build config other than Debug/Profile/Release, // they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build. final String? buildMode = (environment['FLUTTER_BUILD_MODE'] ?? environment['CONFIGURATION'])?.toLowerCase(); if (buildMode != null) { if (buildMode.contains('release')) { return 'release'; } if (buildMode.contains('profile')) { return 'profile'; } if (buildMode.contains('debug')) { return 'debug'; } } echoError('========================================================================'); echoError('ERROR: Unknown FLUTTER_BUILD_MODE: $buildMode.'); echoError("Valid values are 'Debug', 'Profile', or 'Release' (case insensitive)."); echoError('This is controlled by the FLUTTER_BUILD_MODE environment variable.'); echoError('If that is not set, the CONFIGURATION environment variable is used.'); echoError(''); echoError('You can fix this by either adding an appropriately named build'); echoError('configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the'); echoError('.xcconfig file for the current build configuration (${environment['CONFIGURATION']}).'); echoError('========================================================================'); exitApp(-1); } /// Copies all files from [source] to [destination]. /// /// Does not copy `.DS_Store`. /// /// If [delete], delete extraneous files from [destination]. void runRsync( String source, String destination, { List<String> extraArgs = const <String>[], bool delete = false, }) { runSync( 'rsync', <String>[ '-8', // Avoid mangling filenames with encodings that do not match the current locale. '-av', if (delete) '--delete', '--filter', '- .DS_Store', ...extraArgs, source, destination, ], ); } // Adds the App.framework as an embedded binary and the flutter_assets as // resources. void embedFlutterFrameworks() { // Embed App.framework from Flutter into the app (after creating the Frameworks directory // if it doesn't already exist). final String xcodeFrameworksDir = '${environment['TARGET_BUILD_DIR']}/${environment['FRAMEWORKS_FOLDER_PATH']}'; runSync( 'mkdir', <String>[ '-p', '--', xcodeFrameworksDir, ] ); runRsync( delete: true, '${environment['BUILT_PRODUCTS_DIR']}/App.framework', xcodeFrameworksDir, ); // Embed the actual Flutter.framework that the Flutter app expects to run against, // which could be a local build or an arch/type specific build. runRsync( delete: true, '${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework', '$xcodeFrameworksDir/', ); // Copy the native assets. These do not have to be codesigned here because, // they are already codesigned in buildNativeAssetsMacOS. 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/ios/'; final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty; if (Directory(nativeAssetsPath).existsSync()) { if (verbose) { print('♦ Copying native assets from $nativeAssetsPath.'); } runRsync( extraArgs: <String>[ '--filter', '- native_assets.yaml', ], nativeAssetsPath, xcodeFrameworksDir, ); } else if (verbose) { print("♦ No native assets to bundle. $nativeAssetsPath doesn't exist."); } addVmServiceBonjourService(); } // 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, ], ); } } void buildApp() { final bool verbose = environment['VERBOSE_SCRIPT_LOGGING'] != null && environment['VERBOSE_SCRIPT_LOGGING'] != ''; final String sourceRoot = environment['SOURCE_ROOT'] ?? ''; String projectPath = '$sourceRoot/..'; if (environment['FLUTTER_APPLICATION_PATH'] != null) { projectPath = environment['FLUTTER_APPLICATION_PATH']!; } String targetPath = 'lib/main.dart'; if (environment['FLUTTER_TARGET'] != null) { targetPath = environment['FLUTTER_TARGET']!; } final String buildMode = parseFlutterBuildMode(); // Warn the user if not archiving (ACTION=install) in release mode. final String? action = environment['ACTION']; if (action == 'install' && buildMode != 'release') { echo( 'warning: Flutter archive not built in Release mode. Ensure ' 'FLUTTER_BUILD_MODE is set to release or run "flutter build ios ' '--release", then re-run Archive from Xcode.', ); } final 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']}'); } flutterArgs.addAll(<String>[ 'assemble', '--no-version-check', '--output=${environment['BUILT_PRODUCTS_DIR'] ?? ''}/', '-dTargetPlatform=ios', '-dTargetFile=$targetPath', '-dBuildMode=$buildMode', if (environment['FLAVOR'] != null) '-dFlavor=${environment['FLAVOR']}', '-dIosArchs=${environment['ARCHS'] ?? ''}', '-dSdkRoot=${environment['SDKROOT'] ?? ''}', '-dSplitDebugInfo=${environment['SPLIT_DEBUG_INFO'] ?? ''}', '-dTreeShakeIcons=${environment['TREE_SHAKE_ICONS'] ?? ''}', '-dTrackWidgetCreation=${environment['TRACK_WIDGET_CREATION'] ?? ''}', '-dDartObfuscation=${environment['DART_OBFUSCATION'] ?? ''}', '-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'] ?? ''}', ]); if (environment['PERFORMANCE_MEASUREMENT_FILE'] != null && environment['PERFORMANCE_MEASUREMENT_FILE']!.isNotEmpty) { flutterArgs.add('--performance-measurement-file=${environment['PERFORMANCE_MEASUREMENT_FILE']}'); } final String? expandedCodeSignIdentity = environment['EXPANDED_CODE_SIGN_IDENTITY']; if (expandedCodeSignIdentity != null && expandedCodeSignIdentity.isNotEmpty && environment['CODE_SIGNING_REQUIRED'] != 'NO') { flutterArgs.add('-dCodesignIdentity=$expandedCodeSignIdentity'); } if (environment['BUNDLE_SKSL_PATH'] != null && environment['BUNDLE_SKSL_PATH']!.isNotEmpty) { flutterArgs.add('-dBundleSkSLPath=${environment['BUNDLE_SKSL_PATH']}'); } if (environment['CODE_SIZE_DIRECTORY'] != null && environment['CODE_SIZE_DIRECTORY']!.isNotEmpty) { flutterArgs.add('-dCodeSizeDirectory=${environment['CODE_SIZE_DIRECTORY']}'); } flutterArgs.add('${buildMode}_ios_bundle_flutter_assets'); final ProcessResult result = runSync( '${environmentEnsure('FLUTTER_ROOT')}/bin/flutter', flutterArgs, verbose: verbose, allowFail: true, workingDirectory: projectPath, // equivalent of RunCommand pushd "${project_path}" ); if (result.exitCode != 0) { echoError('Failed to package $projectPath.'); exitApp(-1); } streamOutput('done'); streamOutput(' └─Compiling, linking and signing...'); echo('Project $projectPath built and packaged successfully.'); } }