xcode.dart 6.58 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7
import 'package:file/memory.dart';
8
import 'package:meta/meta.dart';
9
import 'package:process/process.dart';
10

11
import '../base/common.dart';
12
import '../base/file_system.dart';
13
import '../base/io.dart';
14
import '../base/logger.dart';
15
import '../base/platform.dart';
16
import '../base/process.dart';
17
import '../base/version.dart';
18
import '../build_info.dart';
19 20
import '../ios/xcodeproj.dart';

21
Version get xcodeRequiredVersion => Version(14, null, null);
22 23 24

/// Diverging this number from the minimum required version will provide a doctor
/// warning, not error, that users should upgrade Xcode.
25
Version get xcodeRecommendedVersion => xcodeRequiredVersion;
26

27 28 29 30 31
/// SDK name passed to `xcrun --sdk`. Corresponds to undocumented Xcode
/// SUPPORTED_PLATFORMS values.
///
/// Usage: xcrun [options] <tool name> ... arguments ...
/// ...
32
/// --sdk <sdk name>            find the tool for the given SDK name.
33 34 35 36
String getSDKNameForIOSEnvironmentType(EnvironmentType environmentType) {
  return (environmentType == EnvironmentType.simulator)
      ? 'iphonesimulator'
      : 'iphoneos';
37 38
}

39
/// A utility class for interacting with Xcode command line tools.
40
class Xcode {
41
  Xcode({
42 43 44 45 46
    required Platform platform,
    required ProcessManager processManager,
    required Logger logger,
    required FileSystem fileSystem,
    required XcodeProjectInterpreter xcodeProjectInterpreter,
47 48 49 50 51
  })  : _platform = platform,
        _fileSystem = fileSystem,
        _xcodeProjectInterpreter = xcodeProjectInterpreter,
        _processUtils =
            ProcessUtils(logger: logger, processManager: processManager);
52

53 54 55 56 57 58
  /// Create an [Xcode] for testing.
  ///
  /// Defaults to a memory file system, fake platform,
  /// buffer logger, and test [XcodeProjectInterpreter].
  @visibleForTesting
  factory Xcode.test({
59 60 61 62
    required ProcessManager processManager,
    XcodeProjectInterpreter? xcodeProjectInterpreter,
    Platform? platform,
    FileSystem? fileSystem,
63 64 65 66 67 68 69 70 71 72 73 74 75 76
  }) {
    platform ??= FakePlatform(
      operatingSystem: 'macos',
      environment: <String, String>{},
    );
    return Xcode(
      platform: platform,
      processManager: processManager,
      fileSystem: fileSystem ?? MemoryFileSystem.test(),
      logger: BufferLogger.test(),
      xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager),
    );
  }

77 78 79 80 81
  final Platform _platform;
  final ProcessUtils _processUtils;
  final FileSystem _fileSystem;
  final XcodeProjectInterpreter _xcodeProjectInterpreter;

82
  bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;
83

84 85
  String? _xcodeSelectPath;
  String? get xcodeSelectPath {
86 87
    if (_xcodeSelectPath == null) {
      try {
88
        _xcodeSelectPath = _processUtils.runSync(
89 90
          <String>['/usr/bin/xcode-select', '--print-path'],
        ).stdout.trim();
91 92
      } on ProcessException {
        // Ignored, return null below.
93 94
      } on ArgumentError {
        // Ignored, return null below.
95 96 97 98 99
      }
    }
    return _xcodeSelectPath;
  }

100
  bool get isInstalled => _xcodeProjectInterpreter.isInstalled;
101

102
  Version? get currentVersion => _xcodeProjectInterpreter.version;
103

104 105
  String? get buildVersion => _xcodeProjectInterpreter.build;

106
  String? get versionText => _xcodeProjectInterpreter.versionText;
107

108
  bool? _eulaSigned;
109 110 111 112
  /// Has the EULA been signed?
  bool get eulaSigned {
    if (_eulaSigned == null) {
      try {
113
        final RunResult result = _processUtils.runSync(
114
          <String>[...xcrunCommand(), 'clang'],
115
        );
116
        if (result.stdout.contains('license')) {
117
          _eulaSigned = false;
118
        } else if (result.stderr.contains('license')) {
119
          _eulaSigned = false;
120
        } else {
121
          _eulaSigned = true;
122
        }
123 124 125 126
      } on ProcessException {
        _eulaSigned = false;
      }
    }
127
    return _eulaSigned ?? false;
128 129
  }

130
  bool? _isSimctlInstalled;
131 132 133 134 135 136 137

  /// 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.
138
        final RunResult result = _processUtils.runSync(
139
          <String>[...xcrunCommand(), 'simctl', 'list', 'devices', 'booted'],
140
        );
141
        _isSimctlInstalled = result.exitCode == 0;
142 143 144 145
      } on ProcessException {
        _isSimctlInstalled = false;
      }
    }
146
    return _isSimctlInstalled ?? false;
147 148
  }

149
  bool get isRequiredVersionSatisfactory {
150 151
    final Version? version = currentVersion;
    if (version == null) {
152
      return false;
153
    }
154
    return version >= xcodeRequiredVersion;
155 156 157
  }

  bool get isRecommendedVersionSatisfactory {
158 159
    final Version? version = currentVersion;
    if (version == null) {
160
      return false;
161
    }
162
    return version >= xcodeRecommendedVersion;
163 164
  }

165 166
  /// See [XcodeProjectInterpreter.xcrunCommand].
  List<String> xcrunCommand() => _xcodeProjectInterpreter.xcrunCommand();
167

168 169 170 171 172 173 174
  Future<RunResult> cc(List<String> args) => _run('cc', args);

  Future<RunResult> clang(List<String> args) => _run('clang', args);

  Future<RunResult> dsymutil(List<String> args) => _run('dsymutil', args);

  Future<RunResult> strip(List<String> args) => _run('strip', args);
175

176
  Future<RunResult> _run(String command, List<String> args) {
177
    return _processUtils.run(
178
      <String>[...xcrunCommand(), command, ...args],
179 180
      throwOnError: true,
    );
181 182
  }

183
  Future<String> sdkLocation(EnvironmentType environmentType) async {
184
    final RunResult runResult = await _processUtils.run(
185
      <String>[...xcrunCommand(), '--sdk', getSDKNameForIOSEnvironmentType(environmentType), '--show-sdk-path'],
186 187
    );
    if (runResult.exitCode != 0) {
188
      throwToolExit('Could not find SDK location: ${runResult.stderr}');
189 190
    }
    return runResult.stdout.trim();
191 192
  }

193 194 195
  String? getSimulatorPath() {
    final String? selectPath = xcodeSelectPath;
    if (selectPath == null) {
196
      return null;
197
    }
198 199
    final String appPath = _fileSystem.path.join(selectPath, 'Applications', 'Simulator.app');
    return _fileSystem.directory(appPath).existsSync() ? appPath : null;
200 201
  }
}
202

203
EnvironmentType? environmentTypeFromSdkroot(String sdkroot, FileSystem fileSystem) {
204
  // iPhoneSimulator.sdk or iPhoneOS.sdk
205
  final String sdkName = fileSystem.path.basename(sdkroot).toLowerCase();
206 207 208 209 210 211
  if (sdkName.contains('iphone')) {
    return sdkName.contains('simulator') ? EnvironmentType.simulator : EnvironmentType.physical;
  }
  assert(false);
  return null;
}