Commit 25f332d8 authored by Devon Carew's avatar Devon Carew

re-work the doctor command

parent 1db09ffe
...@@ -102,20 +102,16 @@ class AndroidSdk { ...@@ -102,20 +102,16 @@ class AndroidSdk {
String get adbPath => getPlatformToolsPath('adb'); String get adbPath => getPlatformToolsPath('adb');
bool validateSdkWellFormed({ bool complain: false }) { /// Validate the Android SDK. This returns an empty list if there are no
if (!FileSystemEntity.isFileSync(adbPath)) { /// issues; otherwise, it returns a list of issues found.
if (complain) List<String> validateSdkWellFormed() {
printError('Android SDK file not found: $adbPath.'); if (!FileSystemEntity.isFileSync(adbPath))
return false; return <String>['Android SDK file not found: $adbPath.'];
}
if (sdkVersions.isEmpty) { if (sdkVersions.isEmpty || latestVersion == null)
if (complain) return <String>['Android SDK does not have the proper build-tools.'];
printError('Android SDK does not have the proper build-tools.');
return false;
}
return latestVersion.validateSdkWellFormed(complain: complain); return latestVersion.validateSdkWellFormed();
} }
String getPlatformToolsPath(String binaryName) { String getPlatformToolsPath(String binaryName) {
...@@ -215,12 +211,20 @@ class AndroidSdkVersion implements Comparable<AndroidSdkVersion> { ...@@ -215,12 +211,20 @@ class AndroidSdkVersion implements Comparable<AndroidSdkVersion> {
String get zipalignPath => getBuildToolsPath('zipalign'); String get zipalignPath => getBuildToolsPath('zipalign');
bool validateSdkWellFormed({ bool complain: false }) { List<String> validateSdkWellFormed() {
return if (_exists(androidJarPath) != null)
_exists(androidJarPath, complain: complain) && return <String>[_exists(androidJarPath)];
_exists(aaptPath, complain: complain) &&
_exists(dxPath, complain: complain) && if (_exists(aaptPath) != null)
_exists(zipalignPath, complain: complain); return <String>[_exists(aaptPath)];
if (_exists(dxPath) != null)
return <String>[_exists(dxPath)];
if (_exists(zipalignPath) != null)
return <String>[_exists(zipalignPath)];
return <String>[];
} }
String getPlatformsPath(String itemName) { String getPlatformsPath(String itemName) {
...@@ -237,13 +241,9 @@ class AndroidSdkVersion implements Comparable<AndroidSdkVersion> { ...@@ -237,13 +241,9 @@ class AndroidSdkVersion implements Comparable<AndroidSdkVersion> {
@override @override
String toString() => '[${sdk.directory}, SDK version $sdkLevel, build-tools $buildToolsVersionName]'; String toString() => '[${sdk.directory}, SDK version $sdkLevel, build-tools $buildToolsVersionName]';
bool _exists(String path, { bool complain: false }) { String _exists(String path) {
if (!FileSystemEntity.isFileSync(path)) { if (!FileSystemEntity.isFileSync(path))
if (complain) return 'Android SDK file not found: $path.';
printError('Android SDK file not found: $path.'); return null;
return false;
}
return true;
} }
} }
...@@ -6,9 +6,8 @@ import '../doctor.dart'; ...@@ -6,9 +6,8 @@ import '../doctor.dart';
import '../globals.dart'; import '../globals.dart';
import 'android_sdk.dart'; import 'android_sdk.dart';
class AndroidWorkflow extends Workflow { class AndroidWorkflow extends DoctorValidator implements Workflow {
@override AndroidWorkflow() : super('Android toolchain - develop for Android devices');
String get label => 'Android toolchain';
@override @override
bool get appliesToHostPlatform => true; bool get appliesToHostPlatform => true;
...@@ -17,30 +16,43 @@ class AndroidWorkflow extends Workflow { ...@@ -17,30 +16,43 @@ class AndroidWorkflow extends Workflow {
bool get canListDevices => getAdbPath(androidSdk) != null; bool get canListDevices => getAdbPath(androidSdk) != null;
@override @override
bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed(complain: false); bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed().isEmpty;
@override @override
ValidationResult validate() { ValidationResult validate() {
Validator androidValidator = new Validator( List<ValidationMessage> messages = <ValidationMessage>[];
label, ValidationType type = ValidationType.missing;
description: 'develop for Android devices' String sdkVersionText;
);
if (androidSdk == null) {
ValidationType sdkExists() { messages.add(new ValidationMessage.error(
return androidSdk == null ? ValidationType.missing : ValidationType.installed; 'Android Studio / Android SDK not found. Download from https://developer.android.com/sdk/\n'
}; '(or visit https://flutter.io/setup/#android-setup for detailed instructions).'
androidValidator.addValidator(new Validator(
'Android Studio / Android SDK',
description: 'enable development for Android devices',
resolution: 'Download from https://developer.android.com/sdk/ (or visit '
'https://flutter.io/setup/#android-setup for detailed instructions)',
validatorFunction: sdkExists
)); ));
} else {
type = ValidationType.partial;
messages.add(new ValidationMessage('Android SDK at ${androidSdk.directory}'));
if (androidSdk.latestVersion != null) {
sdkVersionText = 'Android SDK ${androidSdk.latestVersion.buildToolsVersionName}';
return androidValidator.validate(); messages.add(new ValidationMessage('Platform ${androidSdk.latestVersion.platformVersionName}'));
messages.add(new ValidationMessage('Build-tools ${androidSdk.latestVersion.buildToolsVersionName}'));
} }
@override List<String> validationResult = androidSdk.validateSdkWellFormed();
void diagnose() => validate().print();
if (validationResult.isEmpty) {
type = ValidationType.installed;
} else {
messages.addAll(validationResult.map((String message) {
return new ValidationMessage.error(message);
}));
messages.add(new ValidationMessage('Try re-installing or updating your Android SDK.'));
}
}
return new ValidationResult(type, messages, statusInfo: sdkVersionText);
}
} }
...@@ -178,7 +178,9 @@ class BuildApkCommand extends FlutterCommand { ...@@ -178,7 +178,9 @@ class BuildApkCommand extends FlutterCommand {
return 1; return 1;
} }
if (!androidSdk.validateSdkWellFormed(complain: true)) { List<String> validationResult = androidSdk.validateSdkWellFormed();
if (validationResult.isNotEmpty) {
validationResult.forEach(printError);
printError('Try re-installing or updating your Android SDK.'); printError('Try re-installing or updating your Android SDK.');
return 1; return 1;
} }
...@@ -375,7 +377,9 @@ Future<int> buildAndroid({ ...@@ -375,7 +377,9 @@ Future<int> buildAndroid({
return 1; return 1;
} }
if (!androidSdk.validateSdkWellFormed(complain: true)) { List<String> validationResult = androidSdk.validateSdkWellFormed();
if (validationResult.isNotEmpty) {
validationResult.forEach(printError);
printError('Try re-installing or updating your Android SDK.'); printError('Try re-installing or updating your Android SDK.');
return 1; return 1;
} }
......
...@@ -4,10 +4,8 @@ ...@@ -4,10 +4,8 @@
import 'dart:async'; import 'dart:async';
import '../artifacts.dart';
import '../globals.dart'; import '../globals.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../runner/version.dart';
class DoctorCommand extends FlutterCommand { class DoctorCommand extends FlutterCommand {
@override @override
...@@ -21,18 +19,8 @@ class DoctorCommand extends FlutterCommand { ...@@ -21,18 +19,8 @@ class DoctorCommand extends FlutterCommand {
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
// general info
String flutterRoot = ArtifactStore.flutterRoot;
printStatus('Flutter root: $flutterRoot.');
printStatus('');
// doctor // doctor
doctor.diagnose(); doctor.diagnose();
printStatus('');
// version
printStatus(getVersion(flutterRoot));
return 0; return 0;
} }
} }
...@@ -22,7 +22,7 @@ class UpgradeCommand extends FlutterCommand { ...@@ -22,7 +22,7 @@ class UpgradeCommand extends FlutterCommand {
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
printStatus(getVersion(ArtifactStore.flutterRoot)); printStatus(FlutterVersion.getVersion(ArtifactStore.flutterRoot).toString());
try { try {
runCheckedSync(<String>[ runCheckedSync(<String>[
...@@ -50,7 +50,7 @@ class UpgradeCommand extends FlutterCommand { ...@@ -50,7 +50,7 @@ class UpgradeCommand extends FlutterCommand {
return code; return code;
printStatus(''); printStatus('');
printStatus(getVersion(ArtifactStore.flutterRoot)); printStatus(FlutterVersion.getVersion(ArtifactStore.flutterRoot).toString());
return 0; return 0;
} }
......
This diff is collapsed.
...@@ -79,7 +79,7 @@ class IOSDevice extends Device { ...@@ -79,7 +79,7 @@ class IOSDevice extends Device {
bool get supportsStartPaused => false; bool get supportsStartPaused => false;
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) { static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
if (!doctor.iosWorkflow.hasIdeviceId) if (!doctor.iosWorkflow.hasIDeviceId)
return <IOSDevice>[]; return <IOSDevice>[];
List<IOSDevice> devices = []; List<IOSDevice> devices = [];
......
...@@ -8,112 +8,96 @@ import '../base/process.dart'; ...@@ -8,112 +8,96 @@ import '../base/process.dart';
import '../doctor.dart'; import '../doctor.dart';
import 'mac.dart'; import 'mac.dart';
class IOSWorkflow extends Workflow { XCode get xcode => XCode.instance;
@override
String get label => 'iOS toolchain'; class IOSWorkflow extends DoctorValidator implements Workflow {
IOSWorkflow() : super('iOS toolchain - develop for iOS devices');
@override @override
bool get appliesToHostPlatform => Platform.isMacOS; bool get appliesToHostPlatform => Platform.isMacOS;
// We need xcode (+simctl) to list simulator devices, and idevice_id to list real devices. // We need xcode (+simctl) to list simulator devices, and idevice_id to list real devices.
@override @override
bool get canListDevices => XCode.instance.isInstalledAndMeetsVersionCheck; bool get canListDevices => xcode.isInstalledAndMeetsVersionCheck;
// We need xcode to launch simulator devices, and ideviceinstaller and ios-deploy // We need xcode to launch simulator devices, and ideviceinstaller and ios-deploy
// for real devices. // for real devices.
@override @override
bool get canLaunchDevices => XCode.instance.isInstalledAndMeetsVersionCheck; bool get canLaunchDevices => xcode.isInstalledAndMeetsVersionCheck;
bool get hasIDeviceId => exitsHappy(<String>['idevice_id', '-h']);
@override @override
ValidationResult validate() { ValidationResult validate() {
Validator iosValidator = new Validator( List<ValidationMessage> messages = <ValidationMessage>[];
label, int installCount = 0;
description: 'develop for iOS devices' String xcodeVersionInfo;
);
ValidationType xcodeExists() { if (xcode.isInstalled) {
return XCode.instance.isInstalled ? ValidationType.installed : ValidationType.missing; installCount++;
};
ValidationType xcodeVersionSatisfactory() {
return XCode.instance.isInstalledAndMeetsVersionCheck ? ValidationType.installed : ValidationType.missing;
};
ValidationType xcodeEulaSigned() {
return XCode.instance.eulaSigned ? ValidationType.installed : ValidationType.missing;
};
ValidationType brewExists() {
return exitsHappy(<String>['brew', '-v'])
? ValidationType.installed : ValidationType.missing;
};
ValidationType ideviceinstallerExists() {
return exitsHappy(<String>['ideviceinstaller', '-h'])
? ValidationType.installed : ValidationType.missing;
};
ValidationType iosdeployExists() {
return hasIdeviceId ? ValidationType.installed : ValidationType.missing;
};
Validator xcodeValidator = new Validator(
'XCode',
description: 'enable development for iOS devices',
resolution: 'Download at https://developer.apple.com/xcode/download/',
validatorFunction: xcodeExists
);
iosValidator.addValidator(xcodeValidator); xcodeVersionInfo = xcode.xcodeVersionText;
if (xcodeVersionInfo.contains(','))
xcodeVersionInfo = xcodeVersionInfo.substring(0, xcodeVersionInfo.indexOf(','));
xcodeValidator.addValidator(new Validator( messages.add(new ValidationMessage(xcode.xcodeVersionText));
'version',
description: 'Xcode minimum version of $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor.0', if (!xcode.isInstalledAndMeetsVersionCheck) {
resolution: 'Download the latest version or update via the Mac App Store', messages.add(new ValidationMessage.error(
validatorFunction: xcodeVersionSatisfactory 'Flutter requires a minimum XCode version of $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor.0.\n'
'Download the latest version or update via the Mac App Store.'
)); ));
}
xcodeValidator.addValidator(new Validator( if (!xcode.eulaSigned) {
'EULA', messages.add(new ValidationMessage.error(
description: 'XCode end user license agreement', 'XCode end user license agreement not signed; open XCode or run the command \'sudo xcodebuild -license\'.'
resolution: "Open XCode or run the command 'sudo xcodebuild -license'", ));
validatorFunction: xcodeEulaSigned }
} else {
messages.add(new ValidationMessage.error(
'XCode not installed; this is necessary for iOS development.\n'
'Download at https://developer.apple.com/xcode/download/.'
)); ));
}
Validator brewValidator = new Validator( // brew installed
'brew', if (exitsHappy(<String>['brew', '-v'])) {
description: 'install additional development packages', installCount++;
resolution: 'Download at http://brew.sh/',
validatorFunction: brewExists
);
iosValidator.addValidator(brewValidator); List<String> installed = <String>[];
brewValidator.addValidator(new Validator( if (!exitsHappy(<String>['ideviceinstaller', '-h'])) {
'ideviceinstaller', messages.add(new ValidationMessage.error(
description: 'discover connected iOS devices', 'ideviceinstaller not available; this is used to discover connected iOS devices.\n'
resolution: "Install via 'brew install ideviceinstaller'", 'Install via \'brew install ideviceinstaller\'.'
validatorFunction: ideviceinstallerExists
)); ));
} else {
installed.add('ideviceinstaller');
}
brewValidator.addValidator(new Validator( if (!hasIDeviceId) {
'ios-deploy', messages.add(new ValidationMessage.error(
description: 'deploy to connected iOS devices', 'ios-deploy not available; this is used to deploy to connected iOS devices.\n'
resolution: "Install via 'brew install ios-deploy'", 'Install via \'brew install ios-deploy\'.'
validatorFunction: iosdeployExists
)); ));
} else {
return iosValidator.validate(); installed.add('ios-deploy');
} }
@override if (installed.isNotEmpty)
void diagnose() => validate().print(); messages.add(new ValidationMessage(installed.join(', ') + ' installed'));
} else {
bool get hasIdeviceId => exitsHappy(<String>['idevice_id', '-h']); messages.add(new ValidationMessage.error(
'Brew not installed; use this to install tools for iOS device development.\n'
'Download brew at http://brew.sh/.'
));
}
/// Return whether the tooling to list and deploy to real iOS devices (not the return new ValidationResult(
/// simulator) is installed on the user's machine. installCount == 2 ? ValidationType.installed : installCount == 1 ? ValidationType.partial : ValidationType.missing,
bool get canWorkWithIOSDevices { messages,
return exitsHappy(<String>['ideviceinstaller', '-h']) && hasIdeviceId; statusInfo: xcodeVersionInfo
);
} }
} }
...@@ -54,27 +54,24 @@ class XCode { ...@@ -54,27 +54,24 @@ class XCode {
} }
} }
bool _xcodeVersionSatisfactory; String _xcodeVersionText;
bool get xcodeVersionSatisfactory {
if (_xcodeVersionSatisfactory != null)
return _xcodeVersionSatisfactory;
try { String get xcodeVersionText {
String output = runSync(<String>['xcodebuild', '-version']); if (_xcodeVersionText == null)
_xcodeVersionText = runSync(<String>['xcodebuild', '-version']).replaceAll('\n', ', ');
return _xcodeVersionText;
}
bool get xcodeVersionSatisfactory {
RegExp regex = new RegExp(r'Xcode ([0-9.]+)'); RegExp regex = new RegExp(r'Xcode ([0-9.]+)');
String version = regex.firstMatch(output).group(1); String version = regex.firstMatch(xcodeVersionText).group(1);
List<String> components = version.split('.'); List<String> components = version.split('.');
int major = int.parse(components[0]); int major = int.parse(components[0]);
int minor = components.length == 1 ? 0 : int.parse(components[1]); int minor = components.length == 1 ? 0 : int.parse(components[1]);
_xcodeVersionSatisfactory = _xcodeVersionCheckValid(major, minor); return _xcodeVersionCheckValid(major, minor);
} catch (error) {
_xcodeVersionSatisfactory = false;
}
return _xcodeVersionSatisfactory;
} }
} }
......
...@@ -200,16 +200,8 @@ class FlutterCommandRunner extends CommandRunner { ...@@ -200,16 +200,8 @@ class FlutterCommandRunner extends CommandRunner {
} }
} }
if (androidSdk != null) {
printTrace('Using Android SDK at ${androidSdk.directory}.');
if (androidSdk.latestVersion != null)
printTrace('${androidSdk.latestVersion}');
}
if (globalResults['version']) { if (globalResults['version']) {
printStatus(getVersion(ArtifactStore.flutterRoot)); printStatus(FlutterVersion.getVersion(ArtifactStore.flutterRoot).toString());
printStatus('');
doctor.summary();
return new Future<int>.value(0); return new Future<int>.value(0);
} }
......
...@@ -5,26 +5,54 @@ ...@@ -5,26 +5,54 @@
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/process.dart'; import '../base/process.dart';
String getVersion(String flutterRoot) { class FlutterVersion {
String upstream = runSync([ FlutterVersion(this.flutterRoot) {
'git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{u}' _channel = _runGit('git rev-parse --abbrev-ref --symbolic @{u}');
], workingDirectory: flutterRoot).trim();
String repository; int slash = _channel.indexOf('/');
int slash = upstream.indexOf('/');
if (slash != -1) { if (slash != -1) {
String remote = upstream.substring(0, slash); String remote = _channel.substring(0, slash);
repository = runSync([ _repositoryUrl = _runGit('git ls-remote --get-url $remote');
'git', 'ls-remote', '--get-url', remote _channel = _channel.substring(slash + 1);
], workingDirectory: flutterRoot).trim(); } else if (_channel.isEmpty) {
upstream = upstream.substring(slash + 1); _channel = 'unknown';
}
_frameworkRevision = _runGit('git log -n 1 --pretty=format:%H');
_frameworkAge = _runGit('git log -n 1 --pretty=format:%ar');
} }
String revision = runSync([
'git', 'log', '-n', '1', '--pretty=format:%H (%ar)'
], workingDirectory: flutterRoot).trim();
String from = repository == null ? 'Flutter from unknown source' : 'Flutter from $repository (on $upstream)'; final String flutterRoot;
String flutterVersion = 'Framework: $revision';
String engineRevision = 'Engine: ${ArtifactStore.engineRevision}'; String _repositoryUrl;
String get repositoryUrl => _repositoryUrl;
String _channel;
/// `master`, `alpha`, `hackathon`, ...
String get channel => _channel;
String _frameworkRevision;
String get frameworkRevision => _frameworkRevision;
String get frameworkRevisionShort => _runGit('git rev-parse --short $frameworkRevision');
String _frameworkAge;
String get frameworkAge => _frameworkAge;
return '$from\n$flutterVersion\n$engineRevision'; String get engineRevision => ArtifactStore.engineRevision;
String get engineRevisionShort => _runGit('git rev-parse --short $engineRevision');
String _runGit(String command) => runSync(command.split(' '), workingDirectory: flutterRoot);
@override
String toString() {
String from = repositoryUrl == null ? 'Flutter from unknown source' : 'Flutter from $repositoryUrl (on channel $channel)';
String flutterText = 'Framework: $frameworkRevisionShort ($frameworkAge)';
String engineText = 'Engine: $engineRevisionShort';
return '$from\n\n$flutterText\n$engineText';
}
static FlutterVersion getVersion([String flutterRoot]) {
return new FlutterVersion(flutterRoot != null ? flutterRoot : ArtifactStore.flutterRoot);
}
} }
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