blob: 83f681bcb4928ea38958e4b2ccca993dec7cc720 [file] [log] [blame]
// 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 'core.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,
if (workspace != null) ...<String>['-workspace', workspace],
if (scheme != null) ...<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;
}
}