xcode.dart 6.45 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 22 23 24 25
Version get xcodeRequiredVersion => Version(12, 0, 1, text: '12.0.1');

/// Diverging this number from the minimum required version will provide a doctor
/// warning, not error, that users should upgrade Xcode.
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
  String? get versionText => _xcodeProjectInterpreter.versionText;
105

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

128
  bool? _isSimctlInstalled;
129 130 131 132 133 134 135

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

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

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

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

166
  Future<RunResult> cc(List<String> args) {
167
    return _processUtils.run(
168
      <String>[...xcrunCommand(), 'cc', ...args],
169 170
      throwOnError: true,
    );
171 172 173
  }

  Future<RunResult> clang(List<String> args) {
174
    return _processUtils.run(
175
      <String>[...xcrunCommand(), 'clang', ...args],
176 177
      throwOnError: true,
    );
178 179
  }

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

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

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