| // Copyright 2016 The Chromium 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:async'; |
| import 'dart:convert' show json; |
| |
| import 'package:meta/meta.dart'; |
| |
| import '../application_package.dart'; |
| import '../base/common.dart'; |
| import '../base/context.dart'; |
| import '../base/file_system.dart'; |
| import '../base/fingerprint.dart'; |
| import '../base/io.dart'; |
| import '../base/logger.dart'; |
| import '../base/os.dart'; |
| import '../base/platform.dart'; |
| import '../base/process.dart'; |
| import '../base/process_manager.dart'; |
| import '../base/utils.dart'; |
| import '../build_info.dart'; |
| import '../bundle.dart' as bundle; |
| import '../globals.dart'; |
| import '../plugins.dart'; |
| import '../services.dart'; |
| import 'cocoapods.dart'; |
| import 'code_signing.dart'; |
| import 'xcodeproj.dart'; |
| |
| const int kXcodeRequiredVersionMajor = 9; |
| const int kXcodeRequiredVersionMinor = 0; |
| |
| // The Python `six` module is a dependency for Xcode builds, and installed by |
| // default, but may not be present in custom Python installs; e.g., via |
| // Homebrew. |
| const PythonModule kPythonSix = const PythonModule('six'); |
| |
| IMobileDevice get iMobileDevice => context[IMobileDevice]; |
| |
| Xcode get xcode => context[Xcode]; |
| |
| class PythonModule { |
| const PythonModule(this.name); |
| |
| final String name; |
| |
| bool get isInstalled => exitsHappy(<String>['python', '-c', 'import $name']); |
| |
| String get errorMessage => |
| 'Missing Xcode dependency: Python module "$name".\n' |
| 'Install via \'pip install $name\' or \'sudo easy_install $name\'.'; |
| } |
| |
| class IMobileDevice { |
| const IMobileDevice(); |
| |
| bool get isInstalled => exitsHappy(<String>['idevice_id', '-h']); |
| |
| /// Returns true if libimobiledevice is installed and working as expected. |
| /// |
| /// Older releases of libimobiledevice fail to work with iOS 10.3 and above. |
| Future<bool> get isWorking async { |
| if (!isInstalled) |
| return false; |
| |
| // If no device is attached, we're unable to detect any problems. Assume all is well. |
| final ProcessResult result = (await runAsync(<String>['idevice_id', '-l'])).processResult; |
| if (result.exitCode != 0 || result.stdout.isEmpty) |
| return true; |
| |
| // Check that we can look up the names of any attached devices. |
| return await exitsHappyAsync(<String>['idevicename']); |
| } |
| |
| Future<String> getAvailableDeviceIDs() async { |
| try { |
| final ProcessResult result = await processManager.run(<String>['idevice_id', '-l']); |
| if (result.exitCode != 0) |
| throw new ToolExit('idevice_id returned an error:\n${result.stderr}'); |
| return result.stdout; |
| } on ProcessException { |
| throw new ToolExit('Failed to invoke idevice_id. Run flutter doctor.'); |
| } |
| } |
| |
| Future<String> getInfoForDevice(String deviceID, String key) async { |
| try { |
| final ProcessResult result = await processManager.run(<String>['ideviceinfo', '-u', deviceID, '-k', key, '--simple']); |
| if (result.exitCode != 0) |
| throw new ToolExit('idevice_id returned an error:\n${result.stderr}'); |
| return result.stdout.trim(); |
| } on ProcessException { |
| throw new ToolExit('Failed to invoke idevice_id. Run flutter doctor.'); |
| } |
| } |
| |
| /// Starts `idevicesyslog` and returns the running process. |
| Future<Process> startLogger() => runCommand(<String>['idevicesyslog']); |
| |
| /// Captures a screenshot to the specified outputFile. |
| Future<Null> takeScreenshot(File outputFile) { |
| return runCheckedAsync(<String>['idevicescreenshot', outputFile.path]); |
| } |
| } |
| |
| class Xcode { |
| bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory; |
| |
| String _xcodeSelectPath; |
| String get xcodeSelectPath { |
| if (_xcodeSelectPath == null) { |
| try { |
| _xcodeSelectPath = processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']).stdout.trim(); |
| } on ProcessException { |
| // Ignore: return null below. |
| } |
| } |
| return _xcodeSelectPath; |
| } |
| |
| bool get isInstalled { |
| if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) |
| return false; |
| return xcodeProjectInterpreter.isInstalled; |
| } |
| |
| int get majorVersion => xcodeProjectInterpreter.majorVersion; |
| |
| int get minorVersion => xcodeProjectInterpreter.minorVersion; |
| |
| String get versionText => xcodeProjectInterpreter.versionText; |
| |
| bool _eulaSigned; |
| /// Has the EULA been signed? |
| bool get eulaSigned { |
| if (_eulaSigned == null) { |
| try { |
| final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'clang']); |
| if (result.stdout != null && result.stdout.contains('license')) |
| _eulaSigned = false; |
| else if (result.stderr != null && result.stderr.contains('license')) |
| _eulaSigned = false; |
| else |
| _eulaSigned = true; |
| } on ProcessException { |
| _eulaSigned = false; |
| } |
| } |
| return _eulaSigned; |
| } |
| |
| bool _isSimctlInstalled; |
| |
| /// Verifies that simctl is installed by trying to run it. |
| bool get isSimctlInstalled { |
| if (_isSimctlInstalled == null) { |
| try { |
| // This command will error if additional components need to be installed in |
| // xcode 9.2 and above. |
| final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'simctl', 'list']); |
| _isSimctlInstalled = result.stderr == null || result.stderr == ''; |
| } on ProcessException { |
| _isSimctlInstalled = false; |
| } |
| } |
| return _isSimctlInstalled; |
| } |
| |
| bool get isVersionSatisfactory { |
| if (!xcodeProjectInterpreter.isInstalled) |
| return false; |
| if (majorVersion > kXcodeRequiredVersionMajor) |
| return true; |
| if (majorVersion == kXcodeRequiredVersionMajor) |
| return minorVersion >= kXcodeRequiredVersionMinor; |
| return false; |
| } |
| |
| Future<RunResult> cc(List<String> args) { |
| return runCheckedAsync(<String>['xcrun', 'cc']..addAll(args)); |
| } |
| |
| Future<RunResult> clang(List<String> args) { |
| return runCheckedAsync(<String>['xcrun', 'clang']..addAll(args)); |
| } |
| } |
| |
| Future<XcodeBuildResult> buildXcodeProject({ |
| BuildableIOSApp app, |
| BuildInfo buildInfo, |
| String target: bundle.defaultMainPath, |
| bool buildForDevice, |
| bool codesign: true, |
| bool usesTerminalUi: true, |
| }) async { |
| if (!await upgradePbxProjWithFlutterAssets(app.name, app.appDirectory)) |
| return new XcodeBuildResult(success: false); |
| |
| if (!_checkXcodeVersion()) |
| return new XcodeBuildResult(success: false); |
| |
| if (!kPythonSix.isInstalled) { |
| printError(kPythonSix.errorMessage); |
| return new XcodeBuildResult(success: false); |
| } |
| |
| final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.appDirectory); |
| if (!projectInfo.targets.contains('Runner')) { |
| printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.'); |
| printError('Open Xcode to fix the problem:'); |
| printError(' open ios/Runner.xcworkspace'); |
| return new XcodeBuildResult(success: false); |
| } |
| final String scheme = projectInfo.schemeFor(buildInfo); |
| if (scheme == null) { |
| printError(''); |
| if (projectInfo.definesCustomSchemes) { |
| printError('The Xcode project defines schemes: ${projectInfo.schemes.join(', ')}'); |
| printError('You must specify a --flavor option to select one of them.'); |
| } else { |
| printError('The Xcode project does not define custom schemes.'); |
| printError('You cannot use the --flavor option.'); |
| } |
| return new XcodeBuildResult(success: false); |
| } |
| final String configuration = projectInfo.buildConfigurationFor(buildInfo, scheme); |
| if (configuration == null) { |
| printError(''); |
| printError('The Xcode project defines build configurations: ${projectInfo.buildConfigurations.join(', ')}'); |
| printError('Flutter expects a build configuration named ${XcodeProjectInfo.expectedBuildConfigurationFor(buildInfo, scheme)} or similar.'); |
| printError('Open Xcode to fix the problem:'); |
| printError(' open ios/Runner.xcworkspace'); |
| return new XcodeBuildResult(success: false); |
| } |
| |
| Map<String, String> autoSigningConfigs; |
| if (codesign && buildForDevice) |
| autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi); |
| |
| // Before the build, all service definitions must be updated and the dylibs |
| // copied over to a location that is suitable for Xcodebuild to find them. |
| final Directory appDirectory = fs.directory(app.appDirectory); |
| await _addServicesToBundle(appDirectory); |
| |
| updateGeneratedXcodeProperties( |
| projectPath: fs.currentDirectory.path, |
| buildInfo: buildInfo, |
| target: target, |
| previewDart2: buildInfo.previewDart2, |
| ); |
| |
| if (hasPlugins()) { |
| final String iosPath = fs.path.join(fs.currentDirectory.path, app.appDirectory); |
| // If the Xcode project, Podfile, or Generated.xcconfig have changed since |
| // last run, pods should be updated. |
| final Fingerprinter fingerprinter = new Fingerprinter( |
| fingerprintPath: fs.path.join(getIosBuildDirectory(), 'pod_inputs.fingerprint'), |
| paths: <String>[ |
| _getPbxProjPath(app.appDirectory), |
| fs.path.join(iosPath, 'Podfile'), |
| fs.path.join(iosPath, 'Flutter', 'Generated.xcconfig'), |
| ], |
| properties: <String, String>{}, |
| ); |
| final bool didPodInstall = await cocoaPods.processPods( |
| appIosDirectory: appDirectory, |
| iosEngineDir: flutterFrameworkDir(buildInfo.mode), |
| isSwift: app.isSwift, |
| dependenciesChanged: !await fingerprinter.doesFingerprintMatch() |
| ); |
| if (didPodInstall) |
| await fingerprinter.writeFingerprint(); |
| } |
| |
| // If buildNumber is not specified, keep the project untouched. |
| if (buildInfo.buildNumber != null) { |
| final Status buildNumberStatus = |
| logger.startProgress('Setting CFBundleVersion...', expectSlowOperation: true); |
| try { |
| final RunResult buildNumberResult = await runAsync( |
| <String>[ |
| '/usr/bin/env', |
| 'xcrun', |
| 'agvtool', |
| 'new-version', |
| '-all', |
| buildInfo.buildNumber.toString(), |
| ], |
| workingDirectory: app.appDirectory, |
| ); |
| if (buildNumberResult.exitCode != 0) { |
| throwToolExit('Xcode failed to set new version\n${buildNumberResult.stderr}'); |
| } |
| } finally { |
| buildNumberStatus.stop(); |
| } |
| } |
| |
| // If buildName is not specified, keep the project untouched. |
| if (buildInfo.buildName != null) { |
| final Status buildNameStatus = |
| logger.startProgress('Setting CFBundleShortVersionString...', expectSlowOperation: true); |
| try { |
| final RunResult buildNameResult = await runAsync( |
| <String>[ |
| '/usr/bin/env', |
| 'xcrun', |
| 'agvtool', |
| 'new-marketing-version', |
| buildInfo.buildName, |
| ], |
| workingDirectory: app.appDirectory, |
| ); |
| if (buildNameResult.exitCode != 0) { |
| throwToolExit('Xcode failed to set new marketing version\n${buildNameResult.stderr}'); |
| } |
| } finally { |
| buildNameStatus.stop(); |
| } |
| } |
| |
| final List<String> buildCommands = <String>[ |
| '/usr/bin/env', |
| 'xcrun', |
| 'xcodebuild', |
| '-configuration', configuration, |
| 'ONLY_ACTIVE_ARCH=YES', |
| ]; |
| |
| if (logger.isVerbose) { |
| // An environment variable to be passed to xcode_backend.sh determining |
| // whether to echo back executed commands. |
| buildCommands.add('VERBOSE_SCRIPT_LOGGING=YES'); |
| } else { |
| // This will print warnings and errors only. |
| buildCommands.add('-quiet'); |
| } |
| |
| if (autoSigningConfigs != null) { |
| for (MapEntry<String, String> signingConfig in autoSigningConfigs.entries) { |
| buildCommands.add('${signingConfig.key}=${signingConfig.value}'); |
| } |
| buildCommands.add('-allowProvisioningUpdates'); |
| buildCommands.add('-allowProvisioningDeviceRegistration'); |
| } |
| |
| final List<FileSystemEntity> contents = fs.directory(app.appDirectory).listSync(); |
| for (FileSystemEntity entity in contents) { |
| if (fs.path.extension(entity.path) == '.xcworkspace') { |
| buildCommands.addAll(<String>[ |
| '-workspace', fs.path.basename(entity.path), |
| '-scheme', scheme, |
| 'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}', |
| ]); |
| break; |
| } |
| } |
| |
| if (buildForDevice) { |
| buildCommands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']); |
| } else { |
| buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']); |
| } |
| |
| if (!codesign) { |
| buildCommands.addAll( |
| <String>[ |
| 'CODE_SIGNING_ALLOWED=NO', |
| 'CODE_SIGNING_REQUIRED=NO', |
| 'CODE_SIGNING_IDENTITY=""' |
| ] |
| ); |
| } |
| |
| Status buildSubStatus; |
| Status initialBuildStatus; |
| Directory scriptOutputPipeTempDirectory; |
| |
| if (logger.supportsColor) { |
| scriptOutputPipeTempDirectory = fs.systemTempDirectory |
| .createTempSync('flutter_build_log_pipe'); |
| final File scriptOutputPipeFile = |
| scriptOutputPipeTempDirectory.childFile('pipe_to_stdout'); |
| os.makePipe(scriptOutputPipeFile.path); |
| |
| Future<void> listenToScriptOutputLine() async { |
| final List<String> lines = await scriptOutputPipeFile.readAsLines(); |
| for (String line in lines) { |
| if (line == 'done') { |
| buildSubStatus?.stop(); |
| buildSubStatus = null; |
| } else { |
| initialBuildStatus.cancel(); |
| buildSubStatus = logger.startProgress( |
| line, |
| expectSlowOperation: true, |
| progressIndicatorPadding: kDefaultStatusPadding - 7, |
| ); |
| } |
| } |
| return listenToScriptOutputLine(); |
| } |
| |
| // Trigger the start of the pipe -> stdout loop. Ignore exceptions. |
| listenToScriptOutputLine(); // ignore: unawaited_futures |
| |
| buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}'); |
| } |
| |
| final Stopwatch buildStopwatch = new Stopwatch()..start(); |
| initialBuildStatus = logger.startProgress('Starting Xcode build...'); |
| final RunResult buildResult = await runAsync( |
| buildCommands, |
| workingDirectory: app.appDirectory, |
| allowReentrantFlutter: true |
| ); |
| buildSubStatus?.stop(); |
| initialBuildStatus?.cancel(); |
| buildStopwatch.stop(); |
| // Free pipe file. |
| scriptOutputPipeTempDirectory?.deleteSync(recursive: true); |
| printStatus( |
| 'Xcode build done.', |
| ansiAlternative: 'Xcode build done.'.padRight(kDefaultStatusPadding + 1) |
| + '${getElapsedAsSeconds(buildStopwatch.elapsed).padLeft(5)}', |
| ); |
| |
| // Run -showBuildSettings again but with the exact same parameters as the build. |
| final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync( |
| (new List<String> |
| .from(buildCommands) |
| ..add('-showBuildSettings')) |
| // Undocumented behaviour: xcodebuild craps out if -showBuildSettings |
| // is used together with -allowProvisioningUpdates or |
| // -allowProvisioningDeviceRegistration and freezes forever. |
| .where((String buildCommand) { |
| return !const <String>[ |
| '-allowProvisioningUpdates', |
| '-allowProvisioningDeviceRegistration', |
| ].contains(buildCommand); |
| }), |
| workingDirectory: app.appDirectory, |
| )); |
| |
| if (buildResult.exitCode != 0) { |
| printStatus('Failed to build iOS app'); |
| if (buildResult.stderr.isNotEmpty) { |
| printStatus('Error output from Xcode build:\n↳'); |
| printStatus(buildResult.stderr, indent: 4); |
| } |
| if (buildResult.stdout.isNotEmpty) { |
| printStatus('Xcode\'s output:\n↳'); |
| printStatus(buildResult.stdout, indent: 4); |
| } |
| return new XcodeBuildResult( |
| success: false, |
| stdout: buildResult.stdout, |
| stderr: buildResult.stderr, |
| xcodeBuildExecution: new XcodeBuildExecution( |
| buildCommands: buildCommands, |
| appDirectory: app.appDirectory, |
| buildForPhysicalDevice: buildForDevice, |
| buildSettings: buildSettings, |
| ), |
| ); |
| } else { |
| final String expectedOutputDirectory = fs.path.join( |
| buildSettings['TARGET_BUILD_DIR'], |
| buildSettings['WRAPPER_NAME'], |
| ); |
| |
| String outputDir; |
| if (fs.isDirectorySync(expectedOutputDirectory)) { |
| // Copy app folder to a place where other tools can find it without knowing |
| // the BuildInfo. |
| outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/'); |
| if (fs.isDirectorySync(outputDir)) { |
| // Previous output directory might have incompatible artifacts |
| // (for example, kernel binary files produced from previous `--preview-dart-2` run). |
| fs.directory(outputDir).deleteSync(recursive: true); |
| } |
| copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir)); |
| } else { |
| printError('Build succeeded but the expected app at $expectedOutputDirectory not found'); |
| } |
| return new XcodeBuildResult(success: true, output: outputDir); |
| } |
| } |
| |
| String readGeneratedXcconfig(String appPath) { |
| final String generatedXcconfigPath = |
| fs.path.join(fs.currentDirectory.path, appPath, 'Flutter', 'Generated.xcconfig'); |
| final File generatedXcconfigFile = fs.file(generatedXcconfigPath); |
| if (!generatedXcconfigFile.existsSync()) |
| return null; |
| return generatedXcconfigFile.readAsStringSync(); |
| } |
| |
| Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result) async { |
| if (result.xcodeBuildExecution != null && |
| result.xcodeBuildExecution.buildForPhysicalDevice && |
| result.stdout?.contains('BCEROR') == true && |
| // May need updating if Xcode changes its outputs. |
| result.stdout?.contains('Xcode couldn\'t find a provisioning profile matching') == true) { |
| printError(noProvisioningProfileInstruction, emphasis: true); |
| return; |
| } |
| // Make sure the user has specified one of: |
| // * DEVELOPMENT_TEAM (automatic signing) |
| // * PROVISIONING_PROFILE (manual signing) |
| if (result.xcodeBuildExecution != null && |
| result.xcodeBuildExecution.buildForPhysicalDevice && |
| !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any( |
| result.xcodeBuildExecution.buildSettings.containsKey) |
| ) { |
| printError(noDevelopmentTeamInstruction, emphasis: true); |
| return; |
| } |
| if (result.xcodeBuildExecution != null && |
| result.xcodeBuildExecution.buildForPhysicalDevice && |
| result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) { |
| printError(''); |
| printError('It appears that your application still contains the default signing identifier.'); |
| printError("Try replacing 'com.example' with your signing id in Xcode:"); |
| printError(' open ios/Runner.xcworkspace'); |
| return; |
| } |
| if (result.stdout?.contains('Code Sign error') == true) { |
| printError(''); |
| printError('It appears that there was a problem signing your application prior to installation on the device.'); |
| printError(''); |
| printError('Verify that the Bundle Identifier in your project is your signing id in Xcode'); |
| printError(' open ios/Runner.xcworkspace'); |
| printError(''); |
| printError("Also try selecting 'Product > Build' to fix the problem:"); |
| return; |
| } |
| } |
| |
| class XcodeBuildResult { |
| XcodeBuildResult( |
| { |
| @required this.success, |
| this.output, |
| this.stdout, |
| this.stderr, |
| this.xcodeBuildExecution, |
| } |
| ); |
| |
| final bool success; |
| final String output; |
| final String stdout; |
| final String stderr; |
| /// The invocation of the build that resulted in this result instance. |
| final XcodeBuildExecution xcodeBuildExecution; |
| } |
| |
| /// Describes an invocation of a Xcode build command. |
| class XcodeBuildExecution { |
| XcodeBuildExecution( |
| { |
| @required this.buildCommands, |
| @required this.appDirectory, |
| @required this.buildForPhysicalDevice, |
| @required this.buildSettings, |
| } |
| ); |
| |
| /// The original list of Xcode build commands used to produce this build result. |
| final List<String> buildCommands; |
| final String appDirectory; |
| final bool buildForPhysicalDevice; |
| /// The build settings corresponding to the [buildCommands] invocation. |
| final Map<String, String> buildSettings; |
| } |
| |
| const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.'; |
| |
| bool _checkXcodeVersion() { |
| if (!platform.isMacOS) |
| return false; |
| if (!xcodeProjectInterpreter.isInstalled) { |
| printError('Cannot find "xcodebuild". $_xcodeRequirement'); |
| return false; |
| } |
| if (!xcode.isVersionSatisfactory) { |
| printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement'); |
| return false; |
| } |
| return true; |
| } |
| |
| Future<Null> _addServicesToBundle(Directory bundle) async { |
| final List<Map<String, String>> services = <Map<String, String>>[]; |
| printTrace('Trying to resolve native pub services.'); |
| |
| // Step 1: Parse the service configuration yaml files present in the service |
| // pub packages. |
| await parseServiceConfigs(services); |
| printTrace('Found ${services.length} service definition(s).'); |
| |
| // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up. |
| final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, 'Frameworks')); |
| await _copyServiceFrameworks(services, frameworksDirectory); |
| |
| // Step 3: Copy the service definitions manifest at the correct spot for |
| // xcodebuild to pick up. |
| final File manifestFile = fs.file(fs.path.join(bundle.path, 'ServiceDefinitions.json')); |
| _copyServiceDefinitionsManifest(services, manifestFile); |
| } |
| |
| Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async { |
| printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'."); |
| frameworksDirectory.createSync(recursive: true); |
| for (Map<String, String> service in services) { |
| final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']); |
| final File dylib = fs.file(dylibPath); |
| printTrace('Copying ${dylib.path} into bundle.'); |
| if (!dylib.existsSync()) { |
| printError("The service dylib '${dylib.path}' does not exist."); |
| continue; |
| } |
| // Shell out so permissions on the dylib are preserved. |
| await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]); |
| } |
| } |
| |
| /// The path of the Xcode project file. |
| String _getPbxProjPath(String appPath) => fs.path.join(fs.currentDirectory.path, appPath, 'Runner.xcodeproj', 'project.pbxproj'); |
| |
| void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) { |
| printTrace("Creating service definitions manifest at '${manifest.path}'"); |
| final List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => <String, String>{ |
| 'name': service['name'], |
| // Since we have already moved it to the Frameworks directory. Strip away |
| // the directory and basenames. |
| 'framework': fs.path.basenameWithoutExtension(service['ios-framework']) |
| }).toList(); |
| final Map<String, dynamic> jsonObject = <String, dynamic>{ 'services' : jsonServices }; |
| manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.WRITE, flush: true); // ignore: deprecated_member_use |
| } |
| |
| Future<bool> upgradePbxProjWithFlutterAssets(String app, String appPath) async { |
| final File xcodeProjectFile = fs.file(_getPbxProjPath(appPath)); |
| assert(await xcodeProjectFile.exists()); |
| final List<String> lines = await xcodeProjectFile.readAsLines(); |
| |
| if (lines.any((String line) => line.contains('path = Flutter/flutter_assets'))) |
| return true; |
| |
| const String l1 = ' 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };'; |
| const String l2 = ' 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };'; |
| const String l3 = ' 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };'; |
| const String l4 = ' 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };'; |
| const String l5 = ' 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,'; |
| const String l6 = ' 2D5378251FAA1A9400D5DBA9 /* flutter_assets */,'; |
| const String l7 = ' 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,'; |
| const String l8 = ' 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,'; |
| |
| |
| printStatus("Upgrading project.pbxproj of $app' to include the " |
| "'flutter_assets' directory"); |
| |
| if (!lines.contains(l1) || !lines.contains(l3) || |
| !lines.contains(l5) || !lines.contains(l7)) { |
| printError('Automatic upgrade of project.pbxproj failed.'); |
| printError(' To manually upgrade, open ${xcodeProjectFile.path}:'); |
| printError(' Add the following line in the "PBXBuildFile" section'); |
| printError(l2); |
| printError(' Add the following line in the "PBXFileReference" section'); |
| printError(l4); |
| printError(' Add the following line in the "children" list of the "Flutter" group in the "PBXGroup" section'); |
| printError(l6); |
| printError(' Add the following line in the "files" list of "Resources" in the "PBXResourcesBuildPhase" section'); |
| printError(l8); |
| return false; |
| } |
| |
| lines.insert(lines.indexOf(l1) + 1, l2); |
| lines.insert(lines.indexOf(l3) + 1, l4); |
| lines.insert(lines.indexOf(l5) + 1, l6); |
| lines.insert(lines.indexOf(l7) + 1, l8); |
| |
| const String l9 = ' 9740EEBB1CF902C7004384FC /* app.flx in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB71CF902C7004384FC /* app.flx */; };'; |
| const String l10 = ' 9740EEB71CF902C7004384FC /* app.flx */ = {isa = PBXFileReference; lastKnownFileType = file; name = app.flx; path = Flutter/app.flx; sourceTree = "<group>"; };'; |
| const String l11 = ' 9740EEB71CF902C7004384FC /* app.flx */,'; |
| const String l12 = ' 9740EEBB1CF902C7004384FC /* app.flx in Resources */,'; |
| |
| if (lines.contains(l9)) { |
| printStatus('Removing app.flx from project.pbxproj since it has been ' |
| 'replaced with flutter_assets.'); |
| lines.remove(l9); |
| lines.remove(l10); |
| lines.remove(l11); |
| lines.remove(l12); |
| } |
| |
| final StringBuffer buffer = new StringBuffer(); |
| lines.forEach(buffer.writeln); |
| await xcodeProjectFile.writeAsString(buffer.toString()); |
| return true; |
| } |