Implement macOS support in `flutter doctor` (#33277)
Splits Xcode validation out of the iOS validator and into a stand-alone
validator, and groups the CocoaPods validator with that top-level
validator instead of the iOS validator. iOS now validates only the
iOS-specific tools (e.g., ideviceinstaller).
Reorganizes many of the associated clases so that those that are used by
both macOS and iOS live in macos/ rather than ios/. Moves some
validators to their own files as part of the restructuring.
This is the macOS portion of #31368
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index d1bc978..0b35c07 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -14,7 +14,7 @@
import '../compile.dart';
import '../dart/package_map.dart';
import '../globals.dart';
-import '../ios/mac.dart';
+import '../macos/xcode.dart';
import '../project.dart';
import 'context.dart';
import 'file_system.dart';
diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart
index 58b83e8..032377f 100644
--- a/packages/flutter_tools/lib/src/base/user_messages.dart
+++ b/packages/flutter_tools/lib/src/base/user_messages.dart
@@ -126,24 +126,26 @@
'Android Studio not found; download from https://developer.android.com/studio/index.html\n'
'(or visit https://flutter.dev/setup/#android-setup for detailed instructions).';
- // Messages used in IOSValidator
- String iOSXcodeLocation(String location) => 'Xcode at $location';
- String iOSXcodeOutdated(int versionMajor, int versionMinor) =>
+ // Messages used in XcodeValidator
+ String xcodeLocation(String location) => 'Xcode at $location';
+ String xcodeOutdated(int versionMajor, int versionMinor) =>
'Flutter requires a minimum Xcode version of $versionMajor.$versionMinor.0.\n'
'Download the latest version or update via the Mac App Store.';
- String get iOSXcodeEula => 'Xcode end user license agreement not signed; open Xcode or run the command \'sudo xcodebuild -license\'.';
- String get iOSXcodeMissingSimct =>
+ String get xcodeEula => 'Xcode end user license agreement not signed; open Xcode or run the command \'sudo xcodebuild -license\'.';
+ String get xcodeMissingSimct =>
'Xcode requires additional components to be installed in order to run.\n'
'Launch Xcode and install additional required components when prompted.';
- String get iOSXcodeMissing =>
+ String get xcodeMissing =>
'Xcode not installed; this is necessary for iOS development.\n'
'Download at https://developer.apple.com/xcode/download/.';
- String get iOSXcodeIncomplete =>
+ String get xcodeIncomplete =>
'Xcode installation is incomplete; a full installation is necessary for iOS development.\n'
'Download at: https://developer.apple.com/xcode/download/\n'
'Or install Xcode via the App Store.\n'
'Once installed, run:\n'
' sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer';
+
+ // Messages used in IOSValidator
String get iOSIMobileDeviceMissing =>
'libimobiledevice and ideviceinstaller are not installed. To install with Brew, run:\n'
' brew update\n'
@@ -204,6 +206,9 @@
'$consequence\n'
'To upgrade:\n'
'$upgradeInstructions';
+ String get cocoaPodsBrewMissing =>
+ 'Brew can be used to install CocoaPods.\n'
+ 'Download brew at https://brew.sh/.';
// Messages used in VsCodeValidator
String vsCodeVersion(String version) => 'version $version';
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 41cff48..01adc7b 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -12,7 +12,7 @@
import '../cache.dart';
import '../device.dart';
import '../globals.dart';
-import '../ios/mac.dart';
+import '../macos/xcode.dart';
import '../project.dart';
import '../resident_runner.dart';
import '../run_cold.dart';
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index 7b6bbaf..182130a 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -30,13 +30,16 @@
import 'fuchsia/fuchsia_device.dart' show FuchsiaDeviceTools;
import 'fuchsia/fuchsia_sdk.dart' show FuchsiaSdk, FuchsiaArtifacts;
import 'fuchsia/fuchsia_workflow.dart' show FuchsiaWorkflow;
-import 'ios/cocoapods.dart';
import 'ios/ios_workflow.dart';
import 'ios/mac.dart';
import 'ios/simulators.dart';
import 'ios/xcodeproj.dart';
import 'linux/linux_workflow.dart';
+import 'macos/cocoapods.dart';
+import 'macos/cocoapods_validator.dart';
import 'macos/macos_workflow.dart';
+import 'macos/xcode.dart';
+import 'macos/xcode_validator.dart';
import 'run_hot.dart';
import 'usage.dart';
import 'version.dart';
@@ -99,6 +102,7 @@
WebCompiler: () => const WebCompiler(),
WindowsWorkflow: () => const WindowsWorkflow(),
Xcode: () => Xcode(),
+ XcodeValidator: () => const XcodeValidator(),
XcodeProjectInterpreter: () => XcodeProjectInterpreter(),
},
);
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index 60089f0..1fff8d7 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -26,7 +26,9 @@
import 'ios/ios_workflow.dart';
import 'ios/plist_utils.dart';
import 'linux/linux_workflow.dart';
+import 'macos/cocoapods_validator.dart';
import 'macos/macos_workflow.dart';
+import 'macos/xcode_validator.dart';
import 'proxy_validator.dart';
import 'tester/flutter_tester.dart';
import 'version.dart';
@@ -58,8 +60,11 @@
if (androidWorkflow.appliesToHostPlatform)
_validators.add(GroupedValidator(<DoctorValidator>[androidValidator, androidLicenseValidator]));
+ if (iosWorkflow.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
+ _validators.add(GroupedValidator(<DoctorValidator>[xcodeValidator, cocoapodsValidator]));
+
if (iosWorkflow.appliesToHostPlatform)
- _validators.add(GroupedValidator(<DoctorValidator>[iosValidator, cocoapodsValidator]));
+ _validators.add(iosValidator);
final List<DoctorValidator> ideValidators = <DoctorValidator>[];
ideValidators.addAll(AndroidStudioValidator.allValidators);
diff --git a/packages/flutter_tools/lib/src/ios/ios_emulators.dart b/packages/flutter_tools/lib/src/ios/ios_emulators.dart
index a6d197d..df34c23 100644
--- a/packages/flutter_tools/lib/src/ios/ios_emulators.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_emulators.dart
@@ -8,7 +8,7 @@
import '../base/process.dart';
import '../emulator.dart';
import '../globals.dart';
-import '../ios/mac.dart';
+import '../macos/xcode.dart';
import 'ios_workflow.dart';
class IOSEmulators extends EmulatorDiscovery {
diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
index c0ced36..cf95c30 100644
--- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
@@ -11,13 +11,12 @@
import '../base/user_messages.dart';
import '../base/version.dart';
import '../doctor.dart';
-import 'cocoapods.dart';
+import '../macos/xcode.dart';
import 'mac.dart';
import 'plist_utils.dart' as plist;
IOSWorkflow get iosWorkflow => context.get<IOSWorkflow>();
IOSValidator get iosValidator => context.get<IOSValidator>();
-CocoaPodsValidator get cocoapodsValidator => context.get<CocoaPodsValidator>();
class IOSWorkflow implements Workflow {
const IOSWorkflow();
@@ -44,7 +43,7 @@
class IOSValidator extends DoctorValidator {
- const IOSValidator() : super('iOS toolchain - develop for iOS devices');
+ const IOSValidator() : super('iOS tools - develop for iOS devices');
Future<bool> get hasIDeviceInstaller => exitsHappyAsync(<String>['ideviceinstaller', '-h']);
@@ -79,44 +78,7 @@
@override
Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[];
- ValidationType xcodeStatus = ValidationType.missing;
ValidationType packageManagerStatus = ValidationType.installed;
- String xcodeVersionInfo;
-
- if (xcode.isInstalled) {
- xcodeStatus = ValidationType.installed;
-
- messages.add(ValidationMessage(userMessages.iOSXcodeLocation(xcode.xcodeSelectPath)));
-
- xcodeVersionInfo = xcode.versionText;
- if (xcodeVersionInfo.contains(','))
- xcodeVersionInfo = xcodeVersionInfo.substring(0, xcodeVersionInfo.indexOf(','));
- messages.add(ValidationMessage(xcode.versionText));
-
- if (!xcode.isInstalledAndMeetsVersionCheck) {
- xcodeStatus = ValidationType.partial;
- messages.add(ValidationMessage.error(
- userMessages.iOSXcodeOutdated(kXcodeRequiredVersionMajor, kXcodeRequiredVersionMinor)
- ));
- }
-
- if (!xcode.eulaSigned) {
- xcodeStatus = ValidationType.partial;
- messages.add(ValidationMessage.error(userMessages.iOSXcodeEula));
- }
- if (!xcode.isSimctlInstalled) {
- xcodeStatus = ValidationType.partial;
- messages.add(ValidationMessage.error(userMessages.iOSXcodeMissingSimct));
- }
-
- } else {
- xcodeStatus = ValidationType.missing;
- if (xcode.xcodeSelectPath == null || xcode.xcodeSelectPath.isEmpty) {
- messages.add(ValidationMessage.error(userMessages.iOSXcodeMissing));
- } else {
- messages.add(ValidationMessage.error(userMessages.iOSXcodeIncomplete));
- }
- }
int checksFailed = 0;
@@ -155,61 +117,9 @@
if (checksFailed == totalChecks)
packageManagerStatus = ValidationType.missing;
if (checksFailed > 0 && !hasHomebrew) {
- messages.add(ValidationMessage.error(userMessages.iOSBrewMissing));
+ messages.add(ValidationMessage.hint(userMessages.iOSBrewMissing));
}
- return ValidationResult(
- <ValidationType>[xcodeStatus, packageManagerStatus].reduce(_mergeValidationTypes),
- messages,
- statusInfo: xcodeVersionInfo,
- );
- }
-
- ValidationType _mergeValidationTypes(ValidationType t1, ValidationType t2) {
- return t1 == t2 ? t1 : ValidationType.partial;
- }
-}
-
-class CocoaPodsValidator extends DoctorValidator {
- const CocoaPodsValidator() : super('CocoaPods subvalidator');
-
- bool get hasHomebrew => os.which('brew') != null;
-
- @override
- Future<ValidationResult> validate() async {
- final List<ValidationMessage> messages = <ValidationMessage>[];
-
- ValidationType status = ValidationType.installed;
- if (hasHomebrew) {
- final CocoaPodsStatus cocoaPodsStatus = await cocoaPods
- .evaluateCocoaPodsInstallation;
-
- if (cocoaPodsStatus == CocoaPodsStatus.recommended) {
- if (await cocoaPods.isCocoaPodsInitialized) {
- messages.add(ValidationMessage(userMessages.cocoaPodsVersion(await cocoaPods.cocoaPodsVersionText)));
- } else {
- status = ValidationType.partial;
- messages.add(ValidationMessage.error(userMessages.cocoaPodsUninitialized(noCocoaPodsConsequence)));
- }
- } else {
- if (cocoaPodsStatus == CocoaPodsStatus.notInstalled) {
- status = ValidationType.missing;
- messages.add(ValidationMessage.error(
- userMessages.cocoaPodsMissing(noCocoaPodsConsequence, cocoaPodsInstallInstructions)));
- } else if (cocoaPodsStatus == CocoaPodsStatus.unknownVersion) {
- status = ValidationType.partial;
- messages.add(ValidationMessage.hint(
- userMessages.cocoaPodsUnknownVersion(unknownCocoaPodsConsequence, cocoaPodsUpgradeInstructions)));
- } else {
- status = ValidationType.partial;
- messages.add(ValidationMessage.hint(
- userMessages.cocoaPodsOutdated(cocoaPods.cocoaPodsRecommendedVersion, noCocoaPodsConsequence, cocoaPodsUpgradeInstructions)));
- }
- }
- } else {
- // Only set status. The main validator handles messages for missing brew.
- status = ValidationType.missing;
- }
- return ValidationResult(status, messages);
+ return ValidationResult(packageManagerStatus, messages);
}
}
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 41379c7..1b7a2e2 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -21,19 +21,16 @@
import '../build_info.dart';
import '../convert.dart';
import '../globals.dart';
+import '../macos/cocoapods.dart';
+import '../macos/xcode.dart';
import '../plugins.dart';
import '../project.dart';
import '../services.dart';
-import 'cocoapods.dart';
import 'code_signing.dart';
import 'xcodeproj.dart';
-const int kXcodeRequiredVersionMajor = 9;
-const int kXcodeRequiredVersionMinor = 0;
-
IMobileDevice get iMobileDevice => context.get<IMobileDevice>();
PlistBuddy get plistBuddy => context.get<PlistBuddy>();
-Xcode get xcode => context.get<Xcode>();
class PlistBuddy {
const PlistBuddy();
@@ -154,100 +151,6 @@
}
}
-class Xcode {
- bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory;
-
- String _xcodeSelectPath;
- String get xcodeSelectPath {
- if (_xcodeSelectPath == null) {
- try {
- _xcodeSelectPath = processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']).stdout.trim();
- } on ProcessException {
- // Ignored, return null below.
- }
- }
- return _xcodeSelectPath;
- }
-
- bool get isInstalled {
- if (xcodeSelectPath == null || xcodeSelectPath.isEmpty)
- return false;
- return xcodeProjectInterpreter.isInstalled;
- }
-
- int get majorVersion => xcodeProjectInterpreter.majorVersion;
-
- int get minorVersion => xcodeProjectInterpreter.minorVersion;
-
- String get versionText => xcodeProjectInterpreter.versionText;
-
- bool _eulaSigned;
- /// Has the EULA been signed?
- bool get eulaSigned {
- if (_eulaSigned == null) {
- try {
- final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'clang']);
- if (result.stdout != null && result.stdout.contains('license'))
- _eulaSigned = false;
- else if (result.stderr != null && result.stderr.contains('license'))
- _eulaSigned = false;
- else
- _eulaSigned = true;
- } on ProcessException {
- _eulaSigned = false;
- }
- }
- return _eulaSigned;
- }
-
- bool _isSimctlInstalled;
-
- /// 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.
- final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'simctl', 'list']);
- _isSimctlInstalled = result.stderr == null || result.stderr == '';
- } on ProcessException {
- _isSimctlInstalled = false;
- }
- }
- return _isSimctlInstalled;
- }
-
- bool get isVersionSatisfactory {
- if (!xcodeProjectInterpreter.isInstalled)
- return false;
- if (majorVersion > kXcodeRequiredVersionMajor)
- return true;
- if (majorVersion == kXcodeRequiredVersionMajor)
- return minorVersion >= kXcodeRequiredVersionMinor;
- return false;
- }
-
- Future<RunResult> cc(List<String> args) {
- return runCheckedAsync(<String>['xcrun', 'cc']..addAll(args));
- }
-
- Future<RunResult> clang(List<String> args) {
- return runCheckedAsync(<String>['xcrun', 'clang']..addAll(args));
- }
-
- String getSimulatorPath() {
- if (xcodeSelectPath == null)
- return null;
- final List<String> searchPaths = <String>[
- fs.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'),
- ];
- return searchPaths.where((String p) => p != null).firstWhere(
- (String p) => fs.directory(p).existsSync(),
- orElse: () => null,
- );
- }
-}
-
/// Sets the Xcode system.
///
/// Xcode 10 added a new (default) build system with better performance and
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 5fed6ac..9f36386 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -19,6 +19,7 @@
import '../convert.dart';
import '../device.dart';
import '../globals.dart';
+import '../macos/xcode.dart';
import '../project.dart';
import '../protocol_discovery.dart';
import 'ios_workflow.dart';
diff --git a/packages/flutter_tools/lib/src/ios/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart
similarity index 97%
rename from packages/flutter_tools/lib/src/ios/cocoapods.dart
rename to packages/flutter_tools/lib/src/macos/cocoapods.dart
index 2837371..c85555c 100644
--- a/packages/flutter_tools/lib/src/ios/cocoapods.dart
+++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart
@@ -17,12 +17,12 @@
import '../base/version.dart';
import '../cache.dart';
import '../globals.dart';
+import '../ios/xcodeproj.dart';
import '../project.dart';
-import 'xcodeproj.dart';
const String noCocoaPodsConsequence = '''
- CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
- Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
+ CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.
+ Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/platform-plugins''';
const String unknownCocoaPodsConsequence = '''
diff --git a/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart b/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart
new file mode 100644
index 0000000..615c3f5
--- /dev/null
+++ b/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart
@@ -0,0 +1,58 @@
+// Copyright 2016 The Chromium 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:async';
+
+import '../base/context.dart';
+import '../base/os.dart';
+import '../base/user_messages.dart';
+import '../doctor.dart';
+import 'cocoapods.dart';
+
+CocoaPodsValidator get cocoapodsValidator => context.get<CocoaPodsValidator>();
+
+class CocoaPodsValidator extends DoctorValidator {
+ const CocoaPodsValidator() : super('CocoaPods subvalidator');
+
+ bool get hasHomebrew => os.which('brew') != null;
+
+ @override
+ Future<ValidationResult> validate() async {
+ final List<ValidationMessage> messages = <ValidationMessage>[];
+
+ final CocoaPodsStatus cocoaPodsStatus = await cocoaPods
+ .evaluateCocoaPodsInstallation;
+
+ ValidationType status = ValidationType.installed;
+ if (cocoaPodsStatus == CocoaPodsStatus.recommended) {
+ if (await cocoaPods.isCocoaPodsInitialized) {
+ messages.add(ValidationMessage(userMessages.cocoaPodsVersion(await cocoaPods.cocoaPodsVersionText)));
+ } else {
+ status = ValidationType.partial;
+ messages.add(ValidationMessage.error(userMessages.cocoaPodsUninitialized(noCocoaPodsConsequence)));
+ }
+ } else {
+ if (cocoaPodsStatus == CocoaPodsStatus.notInstalled) {
+ status = ValidationType.missing;
+ messages.add(ValidationMessage.error(
+ userMessages.cocoaPodsMissing(noCocoaPodsConsequence, cocoaPodsInstallInstructions)));
+ } else if (cocoaPodsStatus == CocoaPodsStatus.unknownVersion) {
+ status = ValidationType.partial;
+ messages.add(ValidationMessage.hint(
+ userMessages.cocoaPodsUnknownVersion(unknownCocoaPodsConsequence, cocoaPodsUpgradeInstructions)));
+ } else {
+ status = ValidationType.partial;
+ messages.add(ValidationMessage.hint(
+ userMessages.cocoaPodsOutdated(cocoaPods.cocoaPodsRecommendedVersion, noCocoaPodsConsequence, cocoaPodsUpgradeInstructions)));
+ }
+ }
+
+ // Only check/report homebrew status if CocoaPods isn't installed.
+ if (status == ValidationType.missing && !hasHomebrew) {
+ messages.add(ValidationMessage.hint(userMessages.cocoaPodsBrewMissing));
+ }
+
+ return ValidationResult(status, messages);
+ }
+}
\ No newline at end of file
diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart
new file mode 100644
index 0000000..4cb57c7
--- /dev/null
+++ b/packages/flutter_tools/lib/src/macos/xcode.dart
@@ -0,0 +1,111 @@
+// Copyright 2016 The Chromium 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:async';
+
+import '../base/context.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/process.dart';
+import '../base/process_manager.dart';
+import '../ios/xcodeproj.dart';
+
+const int kXcodeRequiredVersionMajor = 9;
+const int kXcodeRequiredVersionMinor = 0;
+
+Xcode get xcode => context.get<Xcode>();
+
+class Xcode {
+ bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory;
+
+ String _xcodeSelectPath;
+ String get xcodeSelectPath {
+ if (_xcodeSelectPath == null) {
+ try {
+ _xcodeSelectPath = processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']).stdout.trim();
+ } on ProcessException {
+ // Ignored, return null below.
+ }
+ }
+ return _xcodeSelectPath;
+ }
+
+ bool get isInstalled {
+ if (xcodeSelectPath == null || xcodeSelectPath.isEmpty)
+ return false;
+ return xcodeProjectInterpreter.isInstalled;
+ }
+
+ int get majorVersion => xcodeProjectInterpreter.majorVersion;
+
+ int get minorVersion => xcodeProjectInterpreter.minorVersion;
+
+ String get versionText => xcodeProjectInterpreter.versionText;
+
+ bool _eulaSigned;
+ /// Has the EULA been signed?
+ bool get eulaSigned {
+ if (_eulaSigned == null) {
+ try {
+ final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'clang']);
+ if (result.stdout != null && result.stdout.contains('license'))
+ _eulaSigned = false;
+ else if (result.stderr != null && result.stderr.contains('license'))
+ _eulaSigned = false;
+ else
+ _eulaSigned = true;
+ } on ProcessException {
+ _eulaSigned = false;
+ }
+ }
+ return _eulaSigned;
+ }
+
+ bool _isSimctlInstalled;
+
+ /// 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.
+ final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'simctl', 'list']);
+ _isSimctlInstalled = result.stderr == null || result.stderr == '';
+ } on ProcessException {
+ _isSimctlInstalled = false;
+ }
+ }
+ return _isSimctlInstalled;
+ }
+
+ bool get isVersionSatisfactory {
+ if (!xcodeProjectInterpreter.isInstalled)
+ return false;
+ if (majorVersion > kXcodeRequiredVersionMajor)
+ return true;
+ if (majorVersion == kXcodeRequiredVersionMajor)
+ return minorVersion >= kXcodeRequiredVersionMinor;
+ return false;
+ }
+
+ Future<RunResult> cc(List<String> args) {
+ return runCheckedAsync(<String>['xcrun', 'cc']..addAll(args));
+ }
+
+ Future<RunResult> clang(List<String> args) {
+ return runCheckedAsync(<String>['xcrun', 'clang']..addAll(args));
+ }
+
+ String getSimulatorPath() {
+ if (xcodeSelectPath == null)
+ return null;
+ final List<String> searchPaths = <String>[
+ fs.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'),
+ ];
+ return searchPaths.where((String p) => p != null).firstWhere(
+ (String p) => fs.directory(p).existsSync(),
+ orElse: () => null,
+ );
+ }
+}
diff --git a/packages/flutter_tools/lib/src/macos/xcode_validator.dart b/packages/flutter_tools/lib/src/macos/xcode_validator.dart
new file mode 100644
index 0000000..38329a9
--- /dev/null
+++ b/packages/flutter_tools/lib/src/macos/xcode_validator.dart
@@ -0,0 +1,58 @@
+// Copyright 2016 The Chromium 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 '../base/context.dart';
+import '../base/user_messages.dart';
+import '../doctor.dart';
+import 'xcode.dart';
+
+XcodeValidator get xcodeValidator => context.get<XcodeValidator>();
+
+class XcodeValidator extends DoctorValidator {
+ const XcodeValidator() : super('Xcode - develop for iOS and macOS');
+
+ @override
+ Future<ValidationResult> validate() async {
+ final List<ValidationMessage> messages = <ValidationMessage>[];
+ ValidationType xcodeStatus = ValidationType.missing;
+ String xcodeVersionInfo;
+
+ if (xcode.isInstalled) {
+ xcodeStatus = ValidationType.installed;
+
+ messages.add(ValidationMessage(userMessages.xcodeLocation(xcode.xcodeSelectPath)));
+
+ xcodeVersionInfo = xcode.versionText;
+ if (xcodeVersionInfo.contains(','))
+ xcodeVersionInfo = xcodeVersionInfo.substring(0, xcodeVersionInfo.indexOf(','));
+ messages.add(ValidationMessage(xcode.versionText));
+
+ if (!xcode.isInstalledAndMeetsVersionCheck) {
+ xcodeStatus = ValidationType.partial;
+ messages.add(ValidationMessage.error(
+ userMessages.xcodeOutdated(kXcodeRequiredVersionMajor, kXcodeRequiredVersionMinor)
+ ));
+ }
+
+ if (!xcode.eulaSigned) {
+ xcodeStatus = ValidationType.partial;
+ messages.add(ValidationMessage.error(userMessages.xcodeEula));
+ }
+ if (!xcode.isSimctlInstalled) {
+ xcodeStatus = ValidationType.partial;
+ messages.add(ValidationMessage.error(userMessages.xcodeMissingSimct));
+ }
+
+ } else {
+ xcodeStatus = ValidationType.missing;
+ if (xcode.xcodeSelectPath == null || xcode.xcodeSelectPath.isEmpty) {
+ messages.add(ValidationMessage.error(userMessages.xcodeMissing));
+ } else {
+ messages.add(ValidationMessage.error(userMessages.xcodeIncomplete));
+ }
+ }
+
+ return ValidationResult(xcodeStatus, messages, statusInfo: xcodeVersionInfo);
+ }
+}
\ No newline at end of file
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index 5f35e9f..e8c0905 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -10,7 +10,7 @@
import 'base/file_system.dart';
import 'dart/package_map.dart';
import 'globals.dart';
-import 'ios/cocoapods.dart';
+import 'macos/cocoapods.dart';
import 'project.dart';
void _renderTemplateToFile(String template, dynamic context, String filePath) {
diff --git a/packages/flutter_tools/test/base/build_test.dart b/packages/flutter_tools/test/base/build_test.dart
index 3d2ca53..60d3724 100644
--- a/packages/flutter_tools/test/base/build_test.dart
+++ b/packages/flutter_tools/test/base/build_test.dart
@@ -14,7 +14,7 @@
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/process.dart';
-import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
diff --git a/packages/flutter_tools/test/emulator_test.dart b/packages/flutter_tools/test/emulator_test.dart
index 44a39bb..b0292bc 100644
--- a/packages/flutter_tools/test/emulator_test.dart
+++ b/packages/flutter_tools/test/emulator_test.dart
@@ -11,7 +11,7 @@
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/emulator.dart';
import 'package:flutter_tools/src/ios/ios_emulators.dart';
-import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart
index 66c1c66..8b1d2da 100644
--- a/packages/flutter_tools/test/ios/devices_test.dart
+++ b/packages/flutter_tools/test/ios/devices_test.dart
@@ -12,6 +12,7 @@
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
diff --git a/packages/flutter_tools/test/ios/ios_workflow_test.dart b/packages/flutter_tools/test/ios/ios_workflow_test.dart
index 6cd3401..a2e3745 100644
--- a/packages/flutter_tools/test/ios/ios_workflow_test.dart
+++ b/packages/flutter_tools/test/ios/ios_workflow_test.dart
@@ -5,11 +5,9 @@
import 'dart:async';
import 'package:file/memory.dart';
-import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/doctor.dart';
-import 'package:flutter_tools/src/ios/cocoapods.dart';
import 'package:flutter_tools/src/ios/ios_workflow.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:mockito/mockito.dart';
@@ -22,28 +20,17 @@
group('iOS Workflow validation', () {
MockIMobileDevice iMobileDevice;
MockIMobileDevice iMobileDeviceUninstalled;
- MockXcode xcode;
MockProcessManager processManager;
- MockCocoaPods cocoaPods;
FileSystem fs;
setUp(() {
iMobileDevice = MockIMobileDevice();
iMobileDeviceUninstalled = MockIMobileDevice(isInstalled: false);
- xcode = MockXcode();
processManager = MockProcessManager();
- cocoaPods = MockCocoaPods();
fs = MemoryFileSystem();
-
- when(cocoaPods.evaluateCocoaPodsInstallation)
- .thenAnswer((_) async => CocoaPodsStatus.recommended);
- when(cocoaPods.isCocoaPodsInitialized).thenAnswer((_) async => true);
- when(cocoaPods.cocoaPodsVersionText).thenAnswer((_) async => '1.8.0');
});
testUsingContext('Emit missing status when nothing is installed', () async {
- when(xcode.isInstalled).thenReturn(false);
- when(xcode.xcodeSelectPath).thenReturn(null);
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(
hasHomebrew: false,
hasIosDeploy: false,
@@ -54,121 +41,33 @@
expect(result.type, ValidationType.missing);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDeviceUninstalled,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
- });
-
- testUsingContext('Emits partial status when Xcode is not installed', () async {
- when(xcode.isInstalled).thenReturn(false);
- when(xcode.xcodeSelectPath).thenReturn(null);
- final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.partial);
- }, overrides: <Type, Generator>{
- IMobileDevice: () => iMobileDevice,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
- });
-
- testUsingContext('Emits partial status when Xcode is partially installed', () async {
- when(xcode.isInstalled).thenReturn(false);
- when(xcode.xcodeSelectPath).thenReturn('/Library/Developer/CommandLineTools');
- final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.partial);
- }, overrides: <Type, Generator>{
- IMobileDevice: () => iMobileDevice,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
- });
-
- testUsingContext('Emits partial status when Xcode version too low', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 7.0.1\nBuild version 7C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
- when(xcode.eulaSigned).thenReturn(true);
- when(xcode.isSimctlInstalled).thenReturn(true);
- final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.partial);
- }, overrides: <Type, Generator>{
- IMobileDevice: () => iMobileDevice,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
- });
-
- testUsingContext('Emits partial status when Xcode EULA not signed', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
- when(xcode.eulaSigned).thenReturn(false);
- when(xcode.isSimctlInstalled).thenReturn(true);
- final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.partial);
- }, overrides: <Type, Generator>{
- IMobileDevice: () => iMobileDevice,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
});
testUsingContext('Emits installed status when homebrew not installed, but not needed', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
- when(xcode.eulaSigned).thenReturn(true);
- when(xcode.isSimctlInstalled).thenReturn(true);
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(hasHomebrew: false);
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.installed);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDevice,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
});
testUsingContext('Emits partial status when libimobiledevice is not installed', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
- when(xcode.eulaSigned).thenReturn(true);
- when(xcode.isSimctlInstalled).thenReturn(true);
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => MockIMobileDevice(isInstalled: false, isWorking: false),
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
});
testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
- when(xcode.eulaSigned).thenReturn(true);
- when(xcode.isSimctlInstalled).thenReturn(true);
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => MockIMobileDevice(isWorking: false),
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
});
testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
- when(xcode.eulaSigned).thenReturn(true);
- when(xcode.isSimctlInstalled).thenReturn(true);
when(processManager.run(
<String>['ideviceinfo', '-u', '00008020-001C2D903C42002E'],
workingDirectory: anyNamed('workingDirectory'),
@@ -194,163 +93,42 @@
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
});
testUsingContext('Emits partial status when ios-deploy is not installed', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
- when(xcode.isSimctlInstalled).thenReturn(true);
- when(xcode.eulaSigned).thenReturn(true);
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(hasIosDeploy: false);
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDevice,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
});
testUsingContext('Emits partial status when ios-deploy version is too low', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
- when(xcode.eulaSigned).thenReturn(true);
- when(xcode.isSimctlInstalled).thenReturn(true);
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(iosDeployVersionText: '1.8.0');
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDevice,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
});
testUsingContext('Emits partial status when ios-deploy version is a known bad version', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
- when(xcode.eulaSigned).thenReturn(true);
- when(xcode.isSimctlInstalled).thenReturn(true);
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(iosDeployVersionText: '2.0.0');
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDevice,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
});
- testUsingContext('Emits partial status when simctl is not installed', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
- when(xcode.eulaSigned).thenReturn(true);
- when(xcode.isSimctlInstalled).thenReturn(false);
- final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.partial);
- }, overrides: <Type, Generator>{
- IMobileDevice: () => iMobileDevice,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
- });
-
-
testUsingContext('Succeeds when all checks pass', () async {
- when(xcode.isInstalled).thenReturn(true);
- when(xcode.versionText)
- .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
- when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
- when(xcode.eulaSigned).thenReturn(true);
- when(xcode.isSimctlInstalled).thenReturn(true);
-
- ensureDirectoryExists(fs.path.join(homeDirPath, '.cocoapods', 'repos', 'master', 'README.md'));
-
final ValidationResult result = await IOSWorkflowTestTarget().validate();
expect(result.type, ValidationType.installed);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
IMobileDevice: () => iMobileDevice,
- Xcode: () => xcode,
- CocoaPods: () => cocoaPods,
ProcessManager: () => processManager,
});
});
-
- group('iOS CocoaPods validation', () {
- MockCocoaPods cocoaPods;
-
- setUp(() {
- cocoaPods = MockCocoaPods();
- when(cocoaPods.evaluateCocoaPodsInstallation)
- .thenAnswer((_) async => CocoaPodsStatus.recommended);
- when(cocoaPods.isCocoaPodsInitialized).thenAnswer((_) async => true);
- when(cocoaPods.cocoaPodsVersionText).thenAnswer((_) async => '1.8.0');
- });
-
- testUsingContext('Emits installed status when CocoaPods is installed', () async {
- final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.installed);
- }, overrides: <Type, Generator>{
- CocoaPods: () => cocoaPods,
- });
-
- testUsingContext('Emits missing status when CocoaPods is not installed', () async {
- when(cocoaPods.evaluateCocoaPodsInstallation)
- .thenAnswer((_) async => CocoaPodsStatus.notInstalled);
- final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.missing);
- }, overrides: <Type, Generator>{
- CocoaPods: () => cocoaPods,
- });
-
- testUsingContext('Emits partial status when CocoaPods is installed with unknown version', () async {
- when(cocoaPods.evaluateCocoaPodsInstallation)
- .thenAnswer((_) async => CocoaPodsStatus.unknownVersion);
- final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.partial);
- }, overrides: <Type, Generator>{
- CocoaPods: () => cocoaPods,
- });
-
- testUsingContext('Emits partial status when CocoaPods is not initialized', () async {
- when(cocoaPods.isCocoaPodsInitialized).thenAnswer((_) async => false);
- final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.partial);
- }, overrides: <Type, Generator>{
- CocoaPods: () => cocoaPods,
- });
-
- testUsingContext('Emits partial status when CocoaPods version is too low', () async {
- when(cocoaPods.evaluateCocoaPodsInstallation)
- .thenAnswer((_) async => CocoaPodsStatus.belowRecommendedVersion);
- final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.partial);
- }, overrides: <Type, Generator>{
- CocoaPods: () => cocoaPods,
- });
-
- testUsingContext('Emits missing status when homebrew is not installed', () async {
- final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(hasHomebrew: false);
- final ValidationResult result = await workflow.validate();
- expect(result.type, ValidationType.missing);
- }, overrides: <Type, Generator>{
- CocoaPods: () => cocoaPods,
- });
- });
}
final ProcessResult exitsHappy = ProcessResult(
@@ -373,9 +151,7 @@
final Future<bool> isWorking;
}
-class MockXcode extends Mock implements Xcode {}
class MockProcessManager extends Mock implements ProcessManager {}
-class MockCocoaPods extends Mock implements CocoaPods {}
class MockProcessResult extends Mock implements ProcessResult {}
class IOSWorkflowTestTarget extends IOSValidator {
@@ -400,12 +176,3 @@
@override
final Future<bool> hasIDeviceInstaller;
}
-
-class CocoaPodsTestTarget extends CocoaPodsValidator {
- CocoaPodsTestTarget({
- this.hasHomebrew = true,
- });
-
- @override
- final bool hasHomebrew;
-}
diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart
index 9d4f75f..ba50d7e 100644
--- a/packages/flutter_tools/test/ios/mac_test.dart
+++ b/packages/flutter_tools/test/ios/mac_test.dart
@@ -178,102 +178,6 @@
});
});
- group('Xcode', () {
- MockProcessManager mockProcessManager;
- Xcode xcode;
- MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
-
- setUp(() {
- mockProcessManager = MockProcessManager();
- mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
- xcode = Xcode();
- });
-
- testUsingContext('xcodeSelectPath returns null when xcode-select is not installed', () {
- when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
- .thenThrow(const ProcessException('/usr/bin/xcode-select', <String>['--print-path']));
- expect(xcode.xcodeSelectPath, isNull);
- }, overrides: <Type, Generator>{
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('xcodeSelectPath returns path when xcode-select is installed', () {
- const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
- when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
- .thenReturn(ProcessResult(1, 0, xcodePath, ''));
- expect(xcode.xcodeSelectPath, xcodePath);
- }, overrides: <Type, Generator>{
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('xcodeVersionSatisfactory is false when version is less than minimum', () {
- when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
- when(mockXcodeProjectInterpreter.majorVersion).thenReturn(8);
- when(mockXcodeProjectInterpreter.minorVersion).thenReturn(17);
- expect(xcode.isVersionSatisfactory, isFalse);
- }, overrides: <Type, Generator>{
- XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
- });
-
- testUsingContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () {
- when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
- expect(xcode.isVersionSatisfactory, isFalse);
- }, overrides: <Type, Generator>{
- XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
- });
-
- testUsingContext('xcodeVersionSatisfactory is true when version meets minimum', () {
- when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
- when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
- when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
- expect(xcode.isVersionSatisfactory, isTrue);
- }, overrides: <Type, Generator>{
- XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
- });
-
- testUsingContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () {
- when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
- when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
- when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
- expect(xcode.isVersionSatisfactory, isTrue);
- }, overrides: <Type, Generator>{
- XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
- });
-
- testUsingContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () {
- when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
- when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
- when(mockXcodeProjectInterpreter.minorVersion).thenReturn(1);
- expect(xcode.isVersionSatisfactory, isTrue);
- }, overrides: <Type, Generator>{
- XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
- });
-
- testUsingContext('eulaSigned is false when clang is not installed', () {
- when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
- .thenThrow(const ProcessException('/usr/bin/xcrun', <String>['clang']));
- expect(xcode.eulaSigned, isFalse);
- }, overrides: <Type, Generator>{
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('eulaSigned is false when clang output indicates EULA not yet accepted', () {
- when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
- .thenReturn(ProcessResult(1, 1, '', 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.'));
- expect(xcode.eulaSigned, isFalse);
- }, overrides: <Type, Generator>{
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('eulaSigned is true when clang output indicates EULA has been accepted', () {
- when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
- .thenReturn(ProcessResult(1, 1, '', 'clang: error: no input files'));
- expect(xcode.eulaSigned, isTrue);
- }, overrides: <Type, Generator>{
- ProcessManager: () => mockProcessManager,
- });
- });
-
group('Diagnose Xcode build failure', () {
Map<String, String> buildSettings;
diff --git a/packages/flutter_tools/test/ios/simulators_test.dart b/packages/flutter_tools/test/ios/simulators_test.dart
index 61dd6af..c244adf 100644
--- a/packages/flutter_tools/test/ios/simulators_test.dart
+++ b/packages/flutter_tools/test/ios/simulators_test.dart
@@ -14,6 +14,7 @@
import 'package:flutter_tools/src/ios/ios_workflow.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/simulators.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
diff --git a/packages/flutter_tools/test/ios/cocoapods_test.dart b/packages/flutter_tools/test/macos/cocoapods_test.dart
similarity index 99%
rename from packages/flutter_tools/test/ios/cocoapods_test.dart
rename to packages/flutter_tools/test/macos/cocoapods_test.dart
index 9baa6db..1de1adf 100644
--- a/packages/flutter_tools/test/ios/cocoapods_test.dart
+++ b/packages/flutter_tools/test/macos/cocoapods_test.dart
@@ -10,10 +10,10 @@
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/macos/cocoapods.dart';
import 'package:flutter_tools/src/plugins.dart';
import 'package:flutter_tools/src/project.dart';
-import 'package:flutter_tools/src/ios/cocoapods.dart';
-import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
diff --git a/packages/flutter_tools/test/macos/cocoapods_validator_test.dart b/packages/flutter_tools/test/macos/cocoapods_validator_test.dart
new file mode 100644
index 0000000..bd49fc1
--- /dev/null
+++ b/packages/flutter_tools/test/macos/cocoapods_validator_test.dart
@@ -0,0 +1,91 @@
+// Copyright 2017 The Chromium 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 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/macos/cocoapods.dart';
+import 'package:flutter_tools/src/macos/cocoapods_validator.dart';
+import 'package:mockito/mockito.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+ group('CocoaPods validation', () {
+ MockCocoaPods cocoaPods;
+
+ setUp(() {
+ cocoaPods = MockCocoaPods();
+ when(cocoaPods.evaluateCocoaPodsInstallation)
+ .thenAnswer((_) async => CocoaPodsStatus.recommended);
+ when(cocoaPods.isCocoaPodsInitialized).thenAnswer((_) async => true);
+ when(cocoaPods.cocoaPodsVersionText).thenAnswer((_) async => '1.8.0');
+ });
+
+ testUsingContext('Emits installed status when CocoaPods is installed', () async {
+ final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+ final ValidationResult result = await workflow.validate();
+ expect(result.type, ValidationType.installed);
+ }, overrides: <Type, Generator>{
+ CocoaPods: () => cocoaPods,
+ });
+
+ testUsingContext('Emits missing status when CocoaPods is not installed', () async {
+ when(cocoaPods.evaluateCocoaPodsInstallation)
+ .thenAnswer((_) async => CocoaPodsStatus.notInstalled);
+ final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+ final ValidationResult result = await workflow.validate();
+ expect(result.type, ValidationType.missing);
+ }, overrides: <Type, Generator>{
+ CocoaPods: () => cocoaPods,
+ });
+
+ testUsingContext('Emits partial status when CocoaPods is installed with unknown version', () async {
+ when(cocoaPods.evaluateCocoaPodsInstallation)
+ .thenAnswer((_) async => CocoaPodsStatus.unknownVersion);
+ final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+ final ValidationResult result = await workflow.validate();
+ expect(result.type, ValidationType.partial);
+ }, overrides: <Type, Generator>{
+ CocoaPods: () => cocoaPods,
+ });
+
+ testUsingContext('Emits partial status when CocoaPods is not initialized', () async {
+ when(cocoaPods.isCocoaPodsInitialized).thenAnswer((_) async => false);
+ final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+ final ValidationResult result = await workflow.validate();
+ expect(result.type, ValidationType.partial);
+ }, overrides: <Type, Generator>{
+ CocoaPods: () => cocoaPods,
+ });
+
+ testUsingContext('Emits partial status when CocoaPods version is too low', () async {
+ when(cocoaPods.evaluateCocoaPodsInstallation)
+ .thenAnswer((_) async => CocoaPodsStatus.belowRecommendedVersion);
+ final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+ final ValidationResult result = await workflow.validate();
+ expect(result.type, ValidationType.partial);
+ }, overrides: <Type, Generator>{
+ CocoaPods: () => cocoaPods,
+ });
+
+ testUsingContext('Emits installed status when homebrew not installed, but not needed', () async {
+ final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(hasHomebrew: false);
+ final ValidationResult result = await workflow.validate();
+ expect(result.type, ValidationType.installed);
+ }, overrides: <Type, Generator>{
+ CocoaPods: () => cocoaPods,
+ });
+ });
+}
+
+class MockCocoaPods extends Mock implements CocoaPods {}
+
+class CocoaPodsTestTarget extends CocoaPodsValidator {
+ CocoaPodsTestTarget({
+ this.hasHomebrew = true,
+ });
+
+ @override
+ final bool hasHomebrew;
+}
diff --git a/packages/flutter_tools/test/macos/xcode_test.dart b/packages/flutter_tools/test/macos/xcode_test.dart
new file mode 100644
index 0000000..a0e11e0
--- /dev/null
+++ b/packages/flutter_tools/test/macos/xcode_test.dart
@@ -0,0 +1,113 @@
+// Copyright 2017 The Chromium 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 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
+
+void main() {
+ group('Xcode', () {
+ MockProcessManager mockProcessManager;
+ Xcode xcode;
+ MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
+
+ setUp(() {
+ mockProcessManager = MockProcessManager();
+ mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
+ xcode = Xcode();
+ });
+
+ testUsingContext('xcodeSelectPath returns null when xcode-select is not installed', () {
+ when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
+ .thenThrow(const ProcessException('/usr/bin/xcode-select', <String>['--print-path']));
+ expect(xcode.xcodeSelectPath, isNull);
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('xcodeSelectPath returns path when xcode-select is installed', () {
+ const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
+ when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
+ .thenReturn(ProcessResult(1, 0, xcodePath, ''));
+ expect(xcode.xcodeSelectPath, xcodePath);
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('xcodeVersionSatisfactory is false when version is less than minimum', () {
+ when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+ when(mockXcodeProjectInterpreter.majorVersion).thenReturn(8);
+ when(mockXcodeProjectInterpreter.minorVersion).thenReturn(17);
+ expect(xcode.isVersionSatisfactory, isFalse);
+ }, overrides: <Type, Generator>{
+ XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+ });
+
+ testUsingContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () {
+ when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
+ expect(xcode.isVersionSatisfactory, isFalse);
+ }, overrides: <Type, Generator>{
+ XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+ });
+
+ testUsingContext('xcodeVersionSatisfactory is true when version meets minimum', () {
+ when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+ when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
+ when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+ expect(xcode.isVersionSatisfactory, isTrue);
+ }, overrides: <Type, Generator>{
+ XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+ });
+
+ testUsingContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () {
+ when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+ when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
+ when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+ expect(xcode.isVersionSatisfactory, isTrue);
+ }, overrides: <Type, Generator>{
+ XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+ });
+
+ testUsingContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () {
+ when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+ when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
+ when(mockXcodeProjectInterpreter.minorVersion).thenReturn(1);
+ expect(xcode.isVersionSatisfactory, isTrue);
+ }, overrides: <Type, Generator>{
+ XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+ });
+
+ testUsingContext('eulaSigned is false when clang is not installed', () {
+ when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
+ .thenThrow(const ProcessException('/usr/bin/xcrun', <String>['clang']));
+ expect(xcode.eulaSigned, isFalse);
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('eulaSigned is false when clang output indicates EULA not yet accepted', () {
+ when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
+ .thenReturn(ProcessResult(1, 1, '', 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.'));
+ expect(xcode.eulaSigned, isFalse);
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ });
+
+ testUsingContext('eulaSigned is true when clang output indicates EULA has been accepted', () {
+ when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
+ .thenReturn(ProcessResult(1, 1, '', 'clang: error: no input files'));
+ expect(xcode.eulaSigned, isTrue);
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ });
+ });
+}
diff --git a/packages/flutter_tools/test/macos/xcode_validator_test.dart b/packages/flutter_tools/test/macos/xcode_validator_test.dart
new file mode 100644
index 0000000..d3dc929
--- /dev/null
+++ b/packages/flutter_tools/test/macos/xcode_validator_test.dart
@@ -0,0 +1,105 @@
+// Copyright 2017 The Chromium 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 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
+import 'package:flutter_tools/src/macos/xcode_validator.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcode extends Mock implements Xcode {}
+
+void main() {
+ group('Xcode validation', () {
+ MockXcode xcode;
+ MockProcessManager processManager;
+
+ setUp(() {
+ xcode = MockXcode();
+ processManager = MockProcessManager();
+ });
+
+ testUsingContext('Emits missing status when Xcode is not installed', () async {
+ when(xcode.isInstalled).thenReturn(false);
+ when(xcode.xcodeSelectPath).thenReturn(null);
+ const XcodeValidator validator = XcodeValidator();
+ final ValidationResult result = await validator.validate();
+ expect(result.type, ValidationType.missing);
+ }, overrides: <Type, Generator>{
+ Xcode: () => xcode,
+ });
+
+ testUsingContext('Emits missing status when Xcode installation is incomplete', () async {
+ when(xcode.isInstalled).thenReturn(false);
+ when(xcode.xcodeSelectPath).thenReturn('/Library/Developer/CommandLineTools');
+ const XcodeValidator validator = XcodeValidator();
+ final ValidationResult result = await validator.validate();
+ expect(result.type, ValidationType.missing);
+ }, overrides: <Type, Generator>{
+ Xcode: () => xcode,
+ });
+
+ testUsingContext('Emits partial status when Xcode version too low', () async {
+ when(xcode.isInstalled).thenReturn(true);
+ when(xcode.versionText)
+ .thenReturn('Xcode 7.0.1\nBuild version 7C1002\n');
+ when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
+ when(xcode.eulaSigned).thenReturn(true);
+ when(xcode.isSimctlInstalled).thenReturn(true);
+ const XcodeValidator validator = XcodeValidator();
+ final ValidationResult result = await validator.validate();
+ expect(result.type, ValidationType.partial);
+ }, overrides: <Type, Generator>{
+ Xcode: () => xcode,
+ });
+
+ testUsingContext('Emits partial status when Xcode EULA not signed', () async {
+ when(xcode.isInstalled).thenReturn(true);
+ when(xcode.versionText)
+ .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
+ when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
+ when(xcode.eulaSigned).thenReturn(false);
+ when(xcode.isSimctlInstalled).thenReturn(true);
+ const XcodeValidator validator = XcodeValidator();
+ final ValidationResult result = await validator.validate();
+ expect(result.type, ValidationType.partial);
+ }, overrides: <Type, Generator>{
+ Xcode: () => xcode,
+ });
+
+ testUsingContext('Emits partial status when simctl is not installed', () async {
+ when(xcode.isInstalled).thenReturn(true);
+ when(xcode.versionText)
+ .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
+ when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
+ when(xcode.eulaSigned).thenReturn(true);
+ when(xcode.isSimctlInstalled).thenReturn(false);
+ const XcodeValidator validator = XcodeValidator();
+ final ValidationResult result = await validator.validate();
+ expect(result.type, ValidationType.partial);
+ }, overrides: <Type, Generator>{
+ Xcode: () => xcode,
+ });
+
+
+ testUsingContext('Succeeds when all checks pass', () async {
+ when(xcode.isInstalled).thenReturn(true);
+ when(xcode.versionText)
+ .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
+ when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
+ when(xcode.eulaSigned).thenReturn(true);
+ when(xcode.isSimctlInstalled).thenReturn(true);
+ const XcodeValidator validator = XcodeValidator();
+ final ValidationResult result = await validator.validate();
+ expect(result.type, ValidationType.installed);
+ }, overrides: <Type, Generator>{
+ Xcode: () => xcode,
+ ProcessManager: () => processManager,
+ });
+ });
+}