Unverified Commit 81c38b22 authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

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
parent 156b4220
......@@ -14,7 +14,7 @@ import '../cache.dart';
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';
......
......@@ -126,24 +126,26 @@ class UserMessages {
'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 @@ class UserMessages {
'$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';
......
......@@ -12,7 +12,7 @@ import '../build_info.dart';
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';
......
......@@ -30,13 +30,16 @@ import 'emulator.dart';
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 @@ Future<T> runInContext<T>(
WebCompiler: () => const WebCompiler(),
WindowsWorkflow: () => const WindowsWorkflow(),
Xcode: () => Xcode(),
XcodeValidator: () => const XcodeValidator(),
XcodeProjectInterpreter: () => XcodeProjectInterpreter(),
},
);
......
......@@ -26,7 +26,9 @@ import 'intellij/intellij.dart';
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 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
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);
......
......@@ -8,7 +8,7 @@ import '../base/platform.dart';
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 {
......
......@@ -11,13 +11,12 @@ import '../base/process.dart';
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 IOSWorkflow implements Workflow {
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 @@ class IOSValidator extends DoctorValidator {
@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 @@ class IOSValidator extends DoctorValidator {
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);
}
}
......@@ -21,19 +21,16 @@ import '../base/utils.dart';
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 IMobileDevice {
}
}
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
......
......@@ -19,6 +19,7 @@ import '../bundle.dart' as bundle;
import '../convert.dart';
import '../device.dart';
import '../globals.dart';
import '../macos/xcode.dart';
import '../project.dart';
import '../protocol_discovery.dart';
import 'ios_workflow.dart';
......
......@@ -17,12 +17,12 @@ import '../base/process_manager.dart';
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 = '''
......
// 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
// 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,
);
}
}
// 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
......@@ -10,7 +10,7 @@ import 'package:yaml/yaml.dart';
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) {
......
......@@ -14,7 +14,7 @@ import 'package:flutter_tools/src/base/file_system.dart';
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';
......
......@@ -11,7 +11,7 @@ import 'package:flutter_tools/src/base/config.dart';
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';
......
......@@ -12,6 +12,7 @@ import 'package:flutter_tools/src/base/io.dart';
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';
......
......@@ -178,102 +178,6 @@ 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,
});
});
group('Diagnose Xcode build failure', () {
Map<String, String> buildSettings;
......
......@@ -14,6 +14,7 @@ import 'package:flutter_tools/src/base/file_system.dart';
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';
......
......@@ -10,10 +10,10 @@ import 'package:flutter_tools/src/base/common.dart';
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';
......
// 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;
}
// 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,
});
});
}
// 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,
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment