| // 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(); |
| break; |
| case 'thin': |
| // No-op, thinning is handled during the bundle asset assemble build target. |
| break; |
| case 'embed': |
| embedFlutterFrameworks(); |
| break; |
| case 'embed_and_thin': |
| // Thinning is handled during the bundle asset assemble build target, so just embed. |
| embedFlutterFrameworks(); |
| break; |
| case 'test_observatory_bonjour_service': |
| // Exposed for integration testing only. |
| addObservatoryBonjourService(); |
| } |
| } |
| |
| bool existsDir(String path) { |
| final Directory dir = Directory(path); |
| return dir.existsSync(); |
| } |
| |
| 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()); |
| } |
| if ((result.stderr as String).isNotEmpty) { |
| echoError((result.stderr as String).trim()); |
| } |
| if (!allowFail && result.exitCode != 0) { |
| stderr.write('${result.stderr}\n'); |
| 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); |
| } |
| |
| // 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, |
| ] |
| ); |
| runSync( |
| 'rsync', |
| <String>[ |
| '-8', // Avoid mangling filenames with encodings that do not match the current locale. |
| '-av', |
| '--delete', |
| '--filter', |
| '- .DS_Store', |
| '${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. |
| runSync( |
| 'rsync', |
| <String>[ |
| '-av', |
| '--delete', |
| '--filter', |
| '- .DS_Store', |
| '${environment['BUILT_PRODUCTS_DIR']}/Flutter.framework', |
| '$xcodeFrameworksDir/', |
| ], |
| ); |
| |
| addObservatoryBonjourService(); |
| } |
| |
| // Add the observatory publisher Bonjour service to the produced app bundle Info.plist. |
| void addObservatoryBonjourService() { |
| 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 ' |
| '_dartobservatory._tcp NSBonjourServices insertion. Try re-building to ' |
| 'enable "flutter attach".'); |
| return; |
| } |
| |
| // If there are already NSBonjourServices specified by the app (uncommon), |
| // insert the observatory 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', |
| '_dartobservatory._tcp', |
| builtProductsPlist, |
| ], |
| ); |
| } else { |
| // Otherwise, add the NSBonjourServices key and observatory service name. |
| runSync( |
| 'plutil', |
| <String>[ |
| '-insert', |
| 'NSBonjourServices', |
| '-json', |
| '["_dartobservatory._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.', |
| ); |
| } |
| |
| String bitcodeFlag = ''; |
| if (environment['ENABLE_BITCODE'] == 'YES' && environment['ACTION'] == 'install') { |
| bitcodeFlag = 'true'; |
| } |
| |
| 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']}'); |
| } |
| |
| 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'] ?? ''}', |
| '-dEnableBitcode=$bitcodeFlag', |
| '--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.'); |
| } |
| } |