• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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