| // 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', | |
| '-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.'); | |
| } | |
| } |