1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:meta/meta.dart'; 8 9import '../application_package.dart'; 10import '../artifacts.dart'; 11import '../base/common.dart'; 12import '../base/context.dart'; 13import '../base/file_system.dart'; 14import '../base/io.dart'; 15import '../base/logger.dart'; 16import '../base/os.dart'; 17import '../base/platform.dart'; 18import '../base/process.dart'; 19import '../base/process_manager.dart'; 20import '../base/utils.dart'; 21import '../build_info.dart'; 22import '../convert.dart'; 23import '../globals.dart'; 24import '../macos/cocoapod_utils.dart'; 25import '../macos/xcode.dart'; 26import '../project.dart'; 27import '../reporting/reporting.dart'; 28import '../services.dart'; 29import 'code_signing.dart'; 30import 'xcodeproj.dart'; 31 32IMobileDevice get iMobileDevice => context.get<IMobileDevice>(); 33 34/// Specialized exception for expected situations where the ideviceinfo 35/// tool responds with exit code 255 / 'No device found' message 36class IOSDeviceNotFoundError implements Exception { 37 const IOSDeviceNotFoundError(this.message); 38 39 final String message; 40 41 @override 42 String toString() => message; 43} 44 45/// Exception representing an attempt to find information on an iOS device 46/// that failed because the user had not paired the device with the host yet. 47class IOSDeviceNotTrustedError implements Exception { 48 const IOSDeviceNotTrustedError(this.message, this.lockdownCode); 49 50 /// The error message to show to the user. 51 final String message; 52 53 /// The associated `lockdownd` error code. 54 final LockdownReturnCode lockdownCode; 55 56 @override 57 String toString() => '$message (lockdownd error code ${lockdownCode.code})'; 58} 59 60/// Class specifying possible return codes from `lockdownd`. 61/// 62/// This contains only a subset of the return codes that `lockdownd` can return, 63/// as we only care about a limited subset. These values should be kept in sync with 64/// https://github.com/libimobiledevice/libimobiledevice/blob/26373b3/include/libimobiledevice/lockdown.h#L37 65class LockdownReturnCode { 66 const LockdownReturnCode._(this.code); 67 68 /// Creates a new [LockdownReturnCode] from the specified OS exit code. 69 /// 70 /// If the [code] maps to one of the known codes, a `const` instance will be 71 /// returned. 72 factory LockdownReturnCode.fromCode(int code) { 73 final Map<int, LockdownReturnCode> knownCodes = <int, LockdownReturnCode>{ 74 pairingDialogResponsePending.code: pairingDialogResponsePending, 75 invalidHostId.code: invalidHostId, 76 }; 77 78 return knownCodes.containsKey(code) ? knownCodes[code] : LockdownReturnCode._(code); 79 } 80 81 /// The OS exit code. 82 final int code; 83 84 /// Error code indicating that the pairing dialog has been shown to the user, 85 /// and the user has not yet responded as to whether to trust the host. 86 static const LockdownReturnCode pairingDialogResponsePending = LockdownReturnCode._(19); 87 88 /// Error code indicating that the host is not trusted. 89 /// 90 /// This can happen if the user explicitly says "do not trust this computer" 91 /// or if they revoke all trusted computers in the device settings. 92 static const LockdownReturnCode invalidHostId = LockdownReturnCode._(21); 93} 94 95class IMobileDevice { 96 IMobileDevice() 97 : _ideviceIdPath = artifacts.getArtifactPath(Artifact.ideviceId, platform: TargetPlatform.ios) 98 ?? 'idevice_id', // TODO(fujino): remove fallback once g3 updated 99 _ideviceinfoPath = artifacts.getArtifactPath(Artifact.ideviceinfo, platform: TargetPlatform.ios) 100 ?? 'ideviceinfo', // TODO(fujino): remove fallback once g3 updated 101 _idevicenamePath = artifacts.getArtifactPath(Artifact.idevicename, platform: TargetPlatform.ios) 102 ?? 'idevicename', // TODO(fujino): remove fallback once g3 updated 103 _idevicesyslogPath = artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios) 104 ?? 'idevicesyslog', // TODO(fujino): remove fallback once g3 updated 105 _idevicescreenshotPath = artifacts.getArtifactPath(Artifact.idevicescreenshot, platform: TargetPlatform.ios) 106 ?? 'idevicescreenshot' { // TODO(fujino): remove fallback once g3 updated 107 } 108 final String _ideviceIdPath; 109 final String _ideviceinfoPath; 110 final String _idevicenamePath; 111 final String _idevicesyslogPath; 112 final String _idevicescreenshotPath; 113 114 bool get isInstalled { 115 _isInstalled ??= exitsHappy( 116 <String>[ 117 _ideviceIdPath, 118 '-h' 119 ], 120 environment: Map<String, String>.fromEntries( 121 <MapEntry<String, String>>[cache.dyLdLibEntry] 122 ), 123 ); 124 return _isInstalled; 125 } 126 bool _isInstalled; 127 128 /// Returns true if libimobiledevice is installed and working as expected. 129 /// 130 /// Older releases of libimobiledevice fail to work with iOS 10.3 and above. 131 Future<bool> get isWorking async { 132 if (_isWorking != null) { 133 return _isWorking; 134 } 135 if (!isInstalled) { 136 _isWorking = false; 137 return _isWorking; 138 } 139 // If usage info is printed in a hyphenated id, we need to update. 140 const String fakeIphoneId = '00008020-001C2D903C42002E'; 141 final Map<String, String> executionEnv = Map<String, String>.fromEntries( 142 <MapEntry<String, String>>[cache.dyLdLibEntry] 143 ); 144 final ProcessResult ideviceResult = (await runAsync( 145 <String>[ 146 _ideviceinfoPath, 147 '-u', 148 fakeIphoneId 149 ], 150 environment: executionEnv, 151 )).processResult; 152 if (ideviceResult.stdout.contains('Usage: ideviceinfo')) { 153 _isWorking = false; 154 return _isWorking; 155 } 156 157 // If no device is attached, we're unable to detect any problems. Assume all is well. 158 final ProcessResult result = (await runAsync( 159 <String>[ 160 _ideviceIdPath, 161 '-l', 162 ], 163 environment: executionEnv, 164 )).processResult; 165 if (result.exitCode == 0 && result.stdout.isEmpty) { 166 _isWorking = true; 167 } else { 168 // Check that we can look up the names of any attached devices. 169 _isWorking = await exitsHappyAsync( 170 <String>[_idevicenamePath], 171 environment: executionEnv, 172 ); 173 } 174 return _isWorking; 175 } 176 bool _isWorking; 177 178 Future<String> getAvailableDeviceIDs() async { 179 try { 180 final ProcessResult result = await processManager.run( 181 <String>[ 182 _ideviceIdPath, 183 '-l' 184 ], 185 environment: Map<String, String>.fromEntries( 186 <MapEntry<String, String>>[cache.dyLdLibEntry] 187 ), 188 ); 189 if (result.exitCode != 0) 190 throw ToolExit('idevice_id returned an error:\n${result.stderr}'); 191 return result.stdout; 192 } on ProcessException { 193 throw ToolExit('Failed to invoke idevice_id. Run flutter doctor.'); 194 } 195 } 196 197 Future<String> getInfoForDevice(String deviceID, String key) async { 198 try { 199 final ProcessResult result = await processManager.run( 200 <String>[ 201 _ideviceinfoPath, 202 '-u', 203 deviceID, 204 '-k', 205 key 206 ], 207 environment: Map<String, String>.fromEntries( 208 <MapEntry<String, String>>[cache.dyLdLibEntry] 209 ), 210 ); 211 if (result.exitCode == 255 && result.stdout != null && result.stdout.contains('No device found')) 212 throw IOSDeviceNotFoundError('ideviceinfo could not find device:\n${result.stdout}. Try unlocking attached devices.'); 213 if (result.exitCode == 255 && result.stderr != null && result.stderr.contains('Could not connect to lockdownd')) { 214 if (result.stderr.contains('error code -${LockdownReturnCode.pairingDialogResponsePending.code}')) { 215 throw const IOSDeviceNotTrustedError( 216 'Device info unavailable. Is the device asking to "Trust This Computer?"', 217 LockdownReturnCode.pairingDialogResponsePending, 218 ); 219 } 220 if (result.stderr.contains('error code -${LockdownReturnCode.invalidHostId.code}')) { 221 throw const IOSDeviceNotTrustedError( 222 'Device info unavailable. Device pairing "trust" may have been revoked.', 223 LockdownReturnCode.invalidHostId, 224 ); 225 } 226 } 227 if (result.exitCode != 0) 228 throw ToolExit('ideviceinfo returned an error:\n${result.stderr}'); 229 return result.stdout.trim(); 230 } on ProcessException { 231 throw ToolExit('Failed to invoke ideviceinfo. Run flutter doctor.'); 232 } 233 } 234 235 /// Starts `idevicesyslog` and returns the running process. 236 Future<Process> startLogger(String deviceID) { 237 return runCommand( 238 <String>[ 239 _idevicesyslogPath, 240 '-u', 241 deviceID, 242 ], 243 environment: Map<String, String>.fromEntries( 244 <MapEntry<String, String>>[cache.dyLdLibEntry] 245 ), 246 ); 247 } 248 249 /// Captures a screenshot to the specified outputFile. 250 Future<void> takeScreenshot(File outputFile) { 251 return runCheckedAsync( 252 <String>[ 253 _idevicescreenshotPath, 254 outputFile.path 255 ], 256 environment: Map<String, String>.fromEntries( 257 <MapEntry<String, String>>[cache.dyLdLibEntry] 258 ), 259 ); 260 } 261} 262 263Future<XcodeBuildResult> buildXcodeProject({ 264 BuildableIOSApp app, 265 BuildInfo buildInfo, 266 String targetOverride, 267 bool buildForDevice, 268 DarwinArch activeArch, 269 bool codesign = true, 270 bool usesTerminalUi = true, 271}) async { 272 if (!await upgradePbxProjWithFlutterAssets(app.project)) 273 return XcodeBuildResult(success: false); 274 275 if (!_checkXcodeVersion()) 276 return XcodeBuildResult(success: false); 277 278 279 final XcodeProjectInfo projectInfo = await xcodeProjectInterpreter.getInfo(app.project.hostAppRoot.path); 280 if (!projectInfo.targets.contains('Runner')) { 281 printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.'); 282 printError('Open Xcode to fix the problem:'); 283 printError(' open ios/Runner.xcworkspace'); 284 return XcodeBuildResult(success: false); 285 } 286 final String scheme = projectInfo.schemeFor(buildInfo); 287 if (scheme == null) { 288 printError(''); 289 if (projectInfo.definesCustomSchemes) { 290 printError('The Xcode project defines schemes: ${projectInfo.schemes.join(', ')}'); 291 printError('You must specify a --flavor option to select one of them.'); 292 } else { 293 printError('The Xcode project does not define custom schemes.'); 294 printError('You cannot use the --flavor option.'); 295 } 296 return XcodeBuildResult(success: false); 297 } 298 final String configuration = projectInfo.buildConfigurationFor(buildInfo, scheme); 299 if (configuration == null) { 300 printError(''); 301 printError('The Xcode project defines build configurations: ${projectInfo.buildConfigurations.join(', ')}'); 302 printError('Flutter expects a build configuration named ${XcodeProjectInfo.expectedBuildConfigurationFor(buildInfo, scheme)} or similar.'); 303 printError('Open Xcode to fix the problem:'); 304 printError(' open ios/Runner.xcworkspace'); 305 printError('1. Click on "Runner" in the project navigator.'); 306 printError('2. Ensure the Runner PROJECT is selected, not the Runner TARGET.'); 307 if (buildInfo.isDebug) { 308 printError('3. Click the Editor->Add Configuration->Duplicate "Debug" Configuration.'); 309 } else { 310 printError('3. Click the Editor->Add Configuration->Duplicate "Release" Configuration.'); 311 } 312 printError(''); 313 printError(' If this option is disabled, it is likely you have the target selected instead'); 314 printError(' of the project; see:'); 315 printError(' https://stackoverflow.com/questions/19842746/adding-a-build-configuration-in-xcode'); 316 printError(''); 317 printError(' If you have created a completely custom set of build configurations,'); 318 printError(' you can set the FLUTTER_BUILD_MODE=${buildInfo.modeName.toLowerCase()}'); 319 printError(' in the .xcconfig file for that configuration and run from Xcode.'); 320 printError(''); 321 printError('4. If you are not using completely custom build configurations, name the newly created configuration ${buildInfo.modeName}.'); 322 return XcodeBuildResult(success: false); 323 } 324 325 Map<String, String> autoSigningConfigs; 326 if (codesign && buildForDevice) 327 autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi); 328 329 // Before the build, all service definitions must be updated and the dylibs 330 // copied over to a location that is suitable for Xcodebuild to find them. 331 await _addServicesToBundle(app.project.hostAppRoot); 332 333 final FlutterProject project = FlutterProject.current(); 334 await updateGeneratedXcodeProperties( 335 project: project, 336 targetOverride: targetOverride, 337 buildInfo: buildInfo, 338 ); 339 await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode); 340 341 final List<String> buildCommands = <String>[ 342 '/usr/bin/env', 343 'xcrun', 344 'xcodebuild', 345 '-configuration', configuration, 346 ]; 347 348 if (logger.isVerbose) { 349 // An environment variable to be passed to xcode_backend.sh determining 350 // whether to echo back executed commands. 351 buildCommands.add('VERBOSE_SCRIPT_LOGGING=YES'); 352 } else { 353 // This will print warnings and errors only. 354 buildCommands.add('-quiet'); 355 } 356 357 if (autoSigningConfigs != null) { 358 for (MapEntry<String, String> signingConfig in autoSigningConfigs.entries) { 359 buildCommands.add('${signingConfig.key}=${signingConfig.value}'); 360 } 361 buildCommands.add('-allowProvisioningUpdates'); 362 buildCommands.add('-allowProvisioningDeviceRegistration'); 363 } 364 365 final List<FileSystemEntity> contents = app.project.hostAppRoot.listSync(); 366 for (FileSystemEntity entity in contents) { 367 if (fs.path.extension(entity.path) == '.xcworkspace') { 368 buildCommands.addAll(<String>[ 369 '-workspace', fs.path.basename(entity.path), 370 '-scheme', scheme, 371 'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}', 372 ]); 373 break; 374 } 375 } 376 377 if (buildForDevice) { 378 buildCommands.addAll(<String>['-sdk', 'iphoneos']); 379 } else { 380 buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']); 381 } 382 383 if (activeArch != null) { 384 final String activeArchName = getNameForDarwinArch(activeArch); 385 if (activeArchName != null) { 386 buildCommands.add('ONLY_ACTIVE_ARCH=YES'); 387 buildCommands.add('ARCHS=$activeArchName'); 388 } 389 } 390 391 if (!codesign) { 392 buildCommands.addAll( 393 <String>[ 394 'CODE_SIGNING_ALLOWED=NO', 395 'CODE_SIGNING_REQUIRED=NO', 396 'CODE_SIGNING_IDENTITY=""', 397 ] 398 ); 399 } 400 401 Status buildSubStatus; 402 Status initialBuildStatus; 403 Directory tempDir; 404 405 File scriptOutputPipeFile; 406 if (logger.hasTerminal) { 407 tempDir = fs.systemTempDirectory.createTempSync('flutter_build_log_pipe.'); 408 scriptOutputPipeFile = tempDir.childFile('pipe_to_stdout'); 409 os.makePipe(scriptOutputPipeFile.path); 410 411 Future<void> listenToScriptOutputLine() async { 412 final List<String> lines = await scriptOutputPipeFile.readAsLines(); 413 for (String line in lines) { 414 if (line == 'done' || line == 'all done') { 415 buildSubStatus?.stop(); 416 buildSubStatus = null; 417 if (line == 'all done') { 418 // Free pipe file. 419 tempDir?.deleteSync(recursive: true); 420 return; 421 } 422 } else { 423 initialBuildStatus?.cancel(); 424 initialBuildStatus = null; 425 buildSubStatus = logger.startProgress( 426 line, 427 timeout: timeoutConfiguration.slowOperation, 428 progressIndicatorPadding: kDefaultStatusPadding - 7, 429 ); 430 } 431 } 432 await listenToScriptOutputLine(); 433 } 434 435 // Trigger the start of the pipe -> stdout loop. Ignore exceptions. 436 unawaited(listenToScriptOutputLine()); 437 438 buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}'); 439 } 440 441 // Don't log analytics for downstream Flutter commands. 442 // e.g. `flutter build bundle`. 443 buildCommands.add('FLUTTER_SUPPRESS_ANALYTICS=true'); 444 buildCommands.add('COMPILER_INDEX_STORE_ENABLE=NO'); 445 446 final Stopwatch sw = Stopwatch()..start(); 447 initialBuildStatus = logger.startProgress('Running Xcode build...', timeout: timeoutConfiguration.fastOperation); 448 final RunResult buildResult = await runAsync( 449 buildCommands, 450 workingDirectory: app.project.hostAppRoot.path, 451 allowReentrantFlutter: true, 452 ); 453 // Notifies listener that no more output is coming. 454 scriptOutputPipeFile?.writeAsStringSync('all done'); 455 buildSubStatus?.stop(); 456 buildSubStatus = null; 457 initialBuildStatus?.cancel(); 458 initialBuildStatus = null; 459 printStatus( 460 'Xcode build done.'.padRight(kDefaultStatusPadding + 1) 461 + '${getElapsedAsSeconds(sw.elapsed).padLeft(5)}', 462 ); 463 flutterUsage.sendTiming('build', 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds)); 464 465 // Run -showBuildSettings again but with the exact same parameters as the build. 466 final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync( 467 (List<String> 468 .from(buildCommands) 469 ..add('-showBuildSettings')) 470 // Undocumented behavior: xcodebuild craps out if -showBuildSettings 471 // is used together with -allowProvisioningUpdates or 472 // -allowProvisioningDeviceRegistration and freezes forever. 473 .where((String buildCommand) { 474 return !const <String>[ 475 '-allowProvisioningUpdates', 476 '-allowProvisioningDeviceRegistration', 477 ].contains(buildCommand); 478 }).toList(), 479 workingDirectory: app.project.hostAppRoot.path, 480 )); 481 482 if (buildResult.exitCode != 0) { 483 printStatus('Failed to build iOS app'); 484 if (buildResult.stderr.isNotEmpty) { 485 printStatus('Error output from Xcode build:\n↳'); 486 printStatus(buildResult.stderr, indent: 4); 487 } 488 if (buildResult.stdout.isNotEmpty) { 489 printStatus('Xcode\'s output:\n↳'); 490 printStatus(buildResult.stdout, indent: 4); 491 } 492 return XcodeBuildResult( 493 success: false, 494 stdout: buildResult.stdout, 495 stderr: buildResult.stderr, 496 xcodeBuildExecution: XcodeBuildExecution( 497 buildCommands: buildCommands, 498 appDirectory: app.project.hostAppRoot.path, 499 buildForPhysicalDevice: buildForDevice, 500 buildSettings: buildSettings, 501 ), 502 ); 503 } else { 504 final String expectedOutputDirectory = fs.path.join( 505 buildSettings['TARGET_BUILD_DIR'], 506 buildSettings['WRAPPER_NAME'], 507 ); 508 509 String outputDir; 510 if (fs.isDirectorySync(expectedOutputDirectory)) { 511 // Copy app folder to a place where other tools can find it without knowing 512 // the BuildInfo. 513 outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/'); 514 if (fs.isDirectorySync(outputDir)) { 515 // Previous output directory might have incompatible artifacts 516 // (for example, kernel binary files produced from previous run). 517 fs.directory(outputDir).deleteSync(recursive: true); 518 } 519 copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir)); 520 } else { 521 printError('Build succeeded but the expected app at $expectedOutputDirectory not found'); 522 } 523 return XcodeBuildResult(success: true, output: outputDir); 524 } 525} 526 527String readGeneratedXcconfig(String appPath) { 528 final String generatedXcconfigPath = 529 fs.path.join(fs.currentDirectory.path, appPath, 'Flutter', 'Generated.xcconfig'); 530 final File generatedXcconfigFile = fs.file(generatedXcconfigPath); 531 if (!generatedXcconfigFile.existsSync()) 532 return null; 533 return generatedXcconfigFile.readAsStringSync(); 534} 535 536Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result) async { 537 if (result.xcodeBuildExecution != null && 538 result.xcodeBuildExecution.buildForPhysicalDevice && 539 result.stdout?.toUpperCase()?.contains('BITCODE') == true) { 540 BuildEvent('xcode-bitcode-failure', 541 command: result.xcodeBuildExecution.buildCommands.toString(), 542 settings: result.xcodeBuildExecution.buildSettings.toString(), 543 ).send(); 544 } 545 546 if (result.xcodeBuildExecution != null && 547 result.xcodeBuildExecution.buildForPhysicalDevice && 548 result.stdout?.contains('BCEROR') == true && 549 // May need updating if Xcode changes its outputs. 550 result.stdout?.contains('Xcode couldn\'t find a provisioning profile matching') == true) { 551 printError(noProvisioningProfileInstruction, emphasis: true); 552 return; 553 } 554 // Make sure the user has specified one of: 555 // * DEVELOPMENT_TEAM (automatic signing) 556 // * PROVISIONING_PROFILE (manual signing) 557 if (result.xcodeBuildExecution != null && 558 result.xcodeBuildExecution.buildForPhysicalDevice && 559 !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any( 560 result.xcodeBuildExecution.buildSettings.containsKey)) { 561 printError(noDevelopmentTeamInstruction, emphasis: true); 562 return; 563 } 564 if (result.xcodeBuildExecution != null && 565 result.xcodeBuildExecution.buildForPhysicalDevice && 566 result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) { 567 printError(''); 568 printError('It appears that your application still contains the default signing identifier.'); 569 printError("Try replacing 'com.example' with your signing id in Xcode:"); 570 printError(' open ios/Runner.xcworkspace'); 571 return; 572 } 573 if (result.stdout?.contains('Code Sign error') == true) { 574 printError(''); 575 printError('It appears that there was a problem signing your application prior to installation on the device.'); 576 printError(''); 577 printError('Verify that the Bundle Identifier in your project is your signing id in Xcode'); 578 printError(' open ios/Runner.xcworkspace'); 579 printError(''); 580 printError("Also try selecting 'Product > Build' to fix the problem:"); 581 return; 582 } 583} 584 585class XcodeBuildResult { 586 XcodeBuildResult({ 587 @required this.success, 588 this.output, 589 this.stdout, 590 this.stderr, 591 this.xcodeBuildExecution, 592 }); 593 594 final bool success; 595 final String output; 596 final String stdout; 597 final String stderr; 598 /// The invocation of the build that resulted in this result instance. 599 final XcodeBuildExecution xcodeBuildExecution; 600} 601 602/// Describes an invocation of a Xcode build command. 603class XcodeBuildExecution { 604 XcodeBuildExecution({ 605 @required this.buildCommands, 606 @required this.appDirectory, 607 @required this.buildForPhysicalDevice, 608 @required this.buildSettings, 609 }); 610 611 /// The original list of Xcode build commands used to produce this build result. 612 final List<String> buildCommands; 613 final String appDirectory; 614 final bool buildForPhysicalDevice; 615 /// The build settings corresponding to the [buildCommands] invocation. 616 final Map<String, String> buildSettings; 617} 618 619const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.'; 620 621bool _checkXcodeVersion() { 622 if (!platform.isMacOS) 623 return false; 624 if (!xcodeProjectInterpreter.isInstalled) { 625 printError('Cannot find "xcodebuild". $_xcodeRequirement'); 626 return false; 627 } 628 if (!xcode.isVersionSatisfactory) { 629 printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement'); 630 return false; 631 } 632 return true; 633} 634 635Future<void> _addServicesToBundle(Directory bundle) async { 636 final List<Map<String, String>> services = <Map<String, String>>[]; 637 printTrace('Trying to resolve native pub services.'); 638 639 // Step 1: Parse the service configuration yaml files present in the service 640 // pub packages. 641 await parseServiceConfigs(services); 642 printTrace('Found ${services.length} service definition(s).'); 643 644 // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up. 645 final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, 'Frameworks')); 646 await _copyServiceFrameworks(services, frameworksDirectory); 647 648 // Step 3: Copy the service definitions manifest at the correct spot for 649 // xcodebuild to pick up. 650 final File manifestFile = fs.file(fs.path.join(bundle.path, 'ServiceDefinitions.json')); 651 _copyServiceDefinitionsManifest(services, manifestFile); 652} 653 654Future<void> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async { 655 printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'."); 656 frameworksDirectory.createSync(recursive: true); 657 for (Map<String, String> service in services) { 658 final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']); 659 final File dylib = fs.file(dylibPath); 660 printTrace('Copying ${dylib.path} into bundle.'); 661 if (!dylib.existsSync()) { 662 printError("The service dylib '${dylib.path}' does not exist."); 663 continue; 664 } 665 // Shell out so permissions on the dylib are preserved. 666 await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]); 667 } 668} 669 670void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) { 671 printTrace("Creating service definitions manifest at '${manifest.path}'"); 672 final List<Map<String, String>> jsonServices = services.map<Map<String, String>>((Map<String, String> service) => <String, String>{ 673 'name': service['name'], 674 // Since we have already moved it to the Frameworks directory. Strip away 675 // the directory and basenames. 676 'framework': fs.path.basenameWithoutExtension(service['ios-framework']), 677 }).toList(); 678 final Map<String, dynamic> jsonObject = <String, dynamic>{'services': jsonServices}; 679 manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.write, flush: true); 680} 681 682Future<bool> upgradePbxProjWithFlutterAssets(IosProject project) async { 683 final File xcodeProjectFile = project.xcodeProjectInfoFile; 684 assert(await xcodeProjectFile.exists()); 685 final List<String> lines = await xcodeProjectFile.readAsLines(); 686 687 final RegExp oldAssets = RegExp(r'\/\* (flutter_assets|app\.flx)'); 688 final StringBuffer buffer = StringBuffer(); 689 final Set<String> printedStatuses = <String>{}; 690 691 for (final String line in lines) { 692 final Match match = oldAssets.firstMatch(line); 693 if (match != null) { 694 if (printedStatuses.add(match.group(1))) 695 printStatus('Removing obsolete reference to ${match.group(1)} from ${project.hostAppBundleName}'); 696 } else { 697 buffer.writeln(line); 698 } 699 } 700 await xcodeProjectFile.writeAsString(buffer.toString()); 701 return true; 702} 703