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, + }); + }); +}