| // Copyright 2013 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:convert'; |
| import 'dart:io' as io; |
| |
| import 'package:file/file.dart'; |
| |
| import 'output_utils.dart'; |
| import 'process_runner.dart'; |
| |
| const String _xcodeBuildCommand = 'xcodebuild'; |
| const String _xcRunCommand = 'xcrun'; |
| |
| /// A utility class for interacting with the installed version of Xcode. |
| class Xcode { |
| /// Creates an instance that runs commands with the given [processRunner]. |
| /// |
| /// If [log] is true, commands run by this instance will long various status |
| /// messages. |
| Xcode({ |
| this.processRunner = const ProcessRunner(), |
| this.log = false, |
| }); |
| |
| /// The [ProcessRunner] used to run commands. Overridable for testing. |
| final ProcessRunner processRunner; |
| |
| /// Whether or not to log when running commands. |
| final bool log; |
| |
| /// Runs an `xcodebuild` in [directory] with the given parameters. |
| Future<int> runXcodeBuild( |
| Directory directory, { |
| List<String> actions = const <String>['build'], |
| required String workspace, |
| required String scheme, |
| String? configuration, |
| List<String> extraFlags = const <String>[], |
| }) { |
| final List<String> args = <String>[ |
| _xcodeBuildCommand, |
| ...actions, |
| ...<String>['-workspace', workspace], |
| ...<String>['-scheme', scheme], |
| if (configuration != null) ...<String>['-configuration', configuration], |
| ...extraFlags, |
| ]; |
| final String completeTestCommand = '$_xcRunCommand ${args.join(' ')}'; |
| if (log) { |
| print(completeTestCommand); |
| } |
| return processRunner.runAndStream(_xcRunCommand, args, |
| workingDir: directory); |
| } |
| |
| /// Returns true if [project], which should be an .xcodeproj directory, |
| /// contains a target called [target], false if it does not, and null if the |
| /// check fails (e.g., if [project] is not an Xcode project). |
| Future<bool?> projectHasTarget(Directory project, String target) async { |
| final io.ProcessResult result = |
| await processRunner.run(_xcRunCommand, <String>[ |
| _xcodeBuildCommand, |
| '-list', |
| '-json', |
| '-project', |
| project.path, |
| ]); |
| if (result.exitCode != 0) { |
| return null; |
| } |
| Map<String, dynamic>? projectInfo; |
| try { |
| projectInfo = (jsonDecode(result.stdout as String) |
| as Map<String, dynamic>)['project'] as Map<String, dynamic>?; |
| } on FormatException { |
| return null; |
| } |
| if (projectInfo == null) { |
| return null; |
| } |
| final List<String>? targets = |
| (projectInfo['targets'] as List<dynamic>?)?.cast<String>(); |
| return targets?.contains(target) ?? false; |
| } |
| |
| /// Returns the newest available simulator (highest OS version, with ties |
| /// broken in favor of newest device), if any. |
| Future<String?> findBestAvailableIphoneSimulator() async { |
| final List<String> findSimulatorsArguments = <String>[ |
| 'simctl', |
| 'list', |
| 'devices', |
| 'runtimes', |
| 'available', |
| '--json', |
| ]; |
| final String findSimulatorCompleteCommand = |
| '$_xcRunCommand ${findSimulatorsArguments.join(' ')}'; |
| if (log) { |
| print('Looking for available simulators...'); |
| print(findSimulatorCompleteCommand); |
| } |
| final io.ProcessResult findSimulatorsResult = |
| await processRunner.run(_xcRunCommand, findSimulatorsArguments); |
| if (findSimulatorsResult.exitCode != 0) { |
| if (log) { |
| printError( |
| 'Error occurred while running "$findSimulatorCompleteCommand":\n' |
| '${findSimulatorsResult.stderr}'); |
| } |
| return null; |
| } |
| final Map<String, dynamic> simulatorListJson = |
| jsonDecode(findSimulatorsResult.stdout as String) |
| as Map<String, dynamic>; |
| final List<Map<String, dynamic>> runtimes = |
| (simulatorListJson['runtimes'] as List<dynamic>) |
| .cast<Map<String, dynamic>>(); |
| final Map<String, Object> devices = |
| (simulatorListJson['devices'] as Map<String, dynamic>) |
| .cast<String, Object>(); |
| if (runtimes.isEmpty || devices.isEmpty) { |
| return null; |
| } |
| String? id; |
| // Looking for runtimes, trying to find one with highest OS version. |
| for (final Map<String, dynamic> rawRuntimeMap in runtimes.reversed) { |
| final Map<String, Object> runtimeMap = |
| rawRuntimeMap.cast<String, Object>(); |
| if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { |
| continue; |
| } |
| final String? runtimeID = runtimeMap['identifier'] as String?; |
| if (runtimeID == null) { |
| continue; |
| } |
| final List<Map<String, dynamic>>? devicesForRuntime = |
| (devices[runtimeID] as List<dynamic>?)?.cast<Map<String, dynamic>>(); |
| if (devicesForRuntime == null || devicesForRuntime.isEmpty) { |
| continue; |
| } |
| // Looking for runtimes, trying to find latest version of device. |
| for (final Map<String, dynamic> rawDevice in devicesForRuntime.reversed) { |
| final Map<String, Object> device = rawDevice.cast<String, Object>(); |
| id = device['udid'] as String?; |
| if (id == null) { |
| continue; |
| } |
| if (log) { |
| print('device selected: $device'); |
| } |
| return id; |
| } |
| } |
| return null; |
| } |
| } |