blob: a7742f1b930409dcbb83ebd589ef441ba370bf01 [file] [log] [blame]
// 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 'dart:convert' show UTF8;
import 'package:archive/archive.dart';
import 'android/android_workflow.dart';
import 'base/common.dart';
import 'base/context.dart';
import 'base/file_system.dart';
import 'base/platform.dart';
import 'device.dart';
import 'globals.dart';
import 'ios/ios_workflow.dart';
import 'version.dart';
Doctor get doctor => context[Doctor];
const Map<String, String> _osNames = const <String, String>{
'macos': 'Mac OS',
'linux': 'Linux',
'windows': 'Windows'
};
String osName() {
String os = platform.operatingSystem;
return _osNames.containsKey(os) ? _osNames[os] : os;
}
class Doctor {
Doctor() {
_validators.add(new _FlutterValidator());
_androidWorkflow = new AndroidWorkflow();
if (_androidWorkflow.appliesToHostPlatform)
_validators.add(_androidWorkflow);
_iosWorkflow = new IOSWorkflow();
if (_iosWorkflow.appliesToHostPlatform)
_validators.add(_iosWorkflow);
List<DoctorValidator> ideValidators = <DoctorValidator>[];
ideValidators.addAll(IntelliJValidator.installedValidators);
if (ideValidators.isNotEmpty)
_validators.addAll(ideValidators);
else
_validators.add(new NoIdeValidator());
_validators.add(new DeviceValidator());
}
IOSWorkflow _iosWorkflow;
AndroidWorkflow _androidWorkflow;
/// This can return null for platforms that don't support developing for iOS.
IOSWorkflow get iosWorkflow => _iosWorkflow;
AndroidWorkflow get androidWorkflow => _androidWorkflow;
List<DoctorValidator> _validators = <DoctorValidator>[];
List<Workflow> get workflows {
return new List<Workflow>.from(_validators.where((DoctorValidator validator) => validator is Workflow));
}
/// Print a summary of the state of the tooling, as well as how to get more info.
Future<Null> summary() async {
printStatus(await summaryText);
}
Future<String> get summaryText async {
StringBuffer buffer = new StringBuffer();
bool allGood = true;
for (DoctorValidator validator in _validators) {
ValidationResult result = await validator.validate();
buffer.write('${result.leadingBox} ${validator.title} is ');
if (result.type == ValidationType.missing)
buffer.write('not installed.');
else if (result.type == ValidationType.partial)
buffer.write('partially installed; more components are available.');
else
buffer.write('fully installed.');
if (result.statusInfo != null)
buffer.write(' (${result.statusInfo})');
buffer.writeln();
if (result.type != ValidationType.installed)
allGood = false;
}
if (!allGood) {
buffer.writeln();
buffer.writeln('Run "flutter doctor" for information about installing additional components.');
}
return buffer.toString();
}
/// Print verbose information about the state of installed tooling.
Future<bool> diagnose() async {
bool firstLine = true;
bool doctorResult = true;
for (DoctorValidator validator in _validators) {
if (!firstLine)
printStatus('');
firstLine = false;
ValidationResult result = await validator.validate();
if (result.type == ValidationType.missing)
doctorResult = false;
if (result.statusInfo != null)
printStatus('${result.leadingBox} ${validator.title} (${result.statusInfo})');
else
printStatus('${result.leadingBox} ${validator.title}');
for (ValidationMessage message in result.messages) {
String text = message.message.replaceAll('\n', '\n ');
if (message.isError) {
printStatus(' x $text', emphasis: true);
} else {
printStatus(' • $text');
}
}
}
return doctorResult;
}
bool get canListAnything => workflows.any((Workflow workflow) => workflow.canListDevices);
bool get canLaunchAnything => workflows.any((Workflow workflow) => workflow.canLaunchDevices);
}
/// A series of tools and required install steps for a target platform (iOS or Android).
abstract class Workflow {
/// Whether the workflow applies to this platform (as in, should we ever try and use it).
bool get appliesToHostPlatform;
/// Are we functional enough to list devices?
bool get canListDevices;
/// Could this thing launch *something*? It may still have minor issues.
bool get canLaunchDevices;
}
enum ValidationType {
missing,
partial,
installed
}
abstract class DoctorValidator {
DoctorValidator(this.title);
final String title;
Future<ValidationResult> validate();
}
class ValidationResult {
ValidationResult(this.type, this.messages, { this.statusInfo });
final ValidationType type;
// A short message about the status.
final String statusInfo;
final List<ValidationMessage> messages;
String get leadingBox {
if (type == ValidationType.missing)
return '[✗]';
else if (type == ValidationType.installed)
return '[✓]';
else
return '[-]';
}
}
class ValidationMessage {
ValidationMessage(this.message) : isError = false;
ValidationMessage.error(this.message) : isError = true;
final bool isError;
final String message;
@override
String toString() => message;
}
class _FlutterValidator extends DoctorValidator {
_FlutterValidator() : super('Flutter');
@override
Future<ValidationResult> validate() async {
List<ValidationMessage> messages = <ValidationMessage>[];
ValidationType valid = ValidationType.installed;
FlutterVersion version = FlutterVersion.getVersion();
messages.add(new ValidationMessage('Flutter at ${version.flutterRoot}'));
messages.add(new ValidationMessage(
'Framework revision ${version.frameworkRevisionShort} '
'(${version.frameworkAge}), ${version.frameworkDate}'
));
messages.add(new ValidationMessage('Engine revision ${version.engineRevisionShort}'));
messages.add(new ValidationMessage('Tools Dart version ${version.dartSdkVersion}'));
if (platform.isWindows) {
valid = ValidationType.missing;
messages.add(new ValidationMessage.error(
'Flutter tools are not (yet) supported on Windows: '
'https://github.com/flutter/flutter/issues/138.'
));
}
return new ValidationResult(valid, messages,
statusInfo: 'on ${osName()}, channel ${version.channel}');
}
}
class NoIdeValidator extends DoctorValidator {
NoIdeValidator() : super('Flutter IDE Support');
@override
Future<ValidationResult> validate() async {
return new ValidationResult(ValidationType.missing, <ValidationMessage>[
new ValidationMessage('IntelliJ - https://www.jetbrains.com/idea/'),
], statusInfo: 'No supported IDEs installed');
}
}
abstract class IntelliJValidator extends DoctorValidator {
IntelliJValidator(String title) : super(title);
String get version;
String get pluginsPath;
static final Map<String, String> _idToTitle = <String, String>{
'IntelliJIdea' : 'IntelliJ IDEA Ultimate Edition',
'IdeaIC' : 'IntelliJ IDEA Community Edition',
};
static Iterable<DoctorValidator> get installedValidators {
if (platform.isLinux || platform.isWindows)
return IntelliJValidatorOnLinuxAndWindows.installed;
if (platform.isMacOS)
return IntelliJValidatorOnMac.installed;
return <DoctorValidator>[];
}
@override
Future<ValidationResult> validate() async {
List<ValidationMessage> messages = <ValidationMessage>[];
int installCount = 0;
if (_validateHasPackage(messages, 'Dart', 'Dart'))
installCount++;
if (_validateHasPackage(messages, 'flutter-intellij.jar', 'Flutter'))
installCount++;
if (installCount < 2) {
messages.add(new ValidationMessage(
'For information about managing plugins, see\n'
'https://www.jetbrains.com/help/idea/managing-plugins.html'
));
}
return new ValidationResult(
installCount == 2 ? ValidationType.installed : ValidationType.partial,
messages,
statusInfo: 'version $version'
);
}
bool _validateHasPackage(List<ValidationMessage> messages, String packageName, String title) {
if (!hasPackage(packageName)) {
messages.add(new ValidationMessage(
'$title plugin not installed; this adds $title specific functionality.'
));
return false;
}
String version = _readPackageVersion(packageName);
messages.add(new ValidationMessage('$title plugin '
'${version != null ? "version $version" : "installed"}'));
return true;
}
String _readPackageVersion(String packageName) {
String jarPath = packageName.endsWith('.jar')
? fs.path.join(pluginsPath, packageName)
: fs.path.join(pluginsPath, packageName, 'lib', '$packageName.jar');
// TODO(danrubel) look for a better way to extract a single 2K file from the zip
// rather than reading the entire file into memory.
try {
Archive archive = new ZipDecoder().decodeBytes(fs.file(jarPath).readAsBytesSync());
ArchiveFile file = archive.findFile('META-INF/plugin.xml');
String content = UTF8.decode(file.content);
String versionStartTag = '<version>';
int start = content.indexOf(versionStartTag);
int end = content.indexOf('</version>', start);
return content.substring(start + versionStartTag.length, end);
} catch (_) {
return null;
}
}
bool hasPackage(String packageName) {
String packagePath = fs.path.join(pluginsPath, packageName);
if (packageName.endsWith('.jar'))
return fs.isFileSync(packagePath);
return fs.isDirectorySync(packagePath);
}
}
class IntelliJValidatorOnLinuxAndWindows extends IntelliJValidator {
IntelliJValidatorOnLinuxAndWindows(String title, this.version, this.installPath, this.pluginsPath) : super(title);
@override
String version;
final String installPath;
@override
String pluginsPath;
static Iterable<DoctorValidator> get installed {
List<DoctorValidator> validators = <DoctorValidator>[];
if (homeDirPath == null) return validators;
void addValidator(String title, String version, String installPath, String pluginsPath) {
IntelliJValidatorOnLinuxAndWindows validator =
new IntelliJValidatorOnLinuxAndWindows(title, version, installPath, pluginsPath);
for (int index = 0; index < validators.length; ++index) {
DoctorValidator other = validators[index];
if (other is IntelliJValidatorOnLinuxAndWindows && validator.installPath == other.installPath) {
if (validator.version.compareTo(other.version) > 0)
validators[index] = validator;
return;
}
}
validators.add(validator);
}
for (FileSystemEntity dir in fs.directory(homeDirPath).listSync()) {
if (dir is Directory) {
String name = fs.path.basename(dir.path);
IntelliJValidator._idToTitle.forEach((String id, String title) {
if (name.startsWith('.$id')) {
String version = name.substring(id.length + 1);
String installPath;
try {
installPath = fs.file(fs.path.join(dir.path, 'system', '.home')).readAsStringSync();
} catch (e) {
// ignored
}
if (installPath != null && fs.isDirectorySync(installPath)) {
String pluginsPath = fs.path.join(dir.path, 'config', 'plugins');
addValidator(title, version, installPath, pluginsPath);
}
}
});
}
}
return validators;
}
}
class IntelliJValidatorOnMac extends IntelliJValidator {
IntelliJValidatorOnMac(String title, this.id, this.installPath) : super(title);
final String id;
final String installPath;
static final Map<String, String> _dirNameToId = <String, String>{
'IntelliJ IDEA.app' : 'IntelliJIdea',
'IntelliJ IDEA Ultimate.app' : 'IntelliJIdea',
'IntelliJ IDEA CE.app' : 'IdeaIC',
};
static Iterable<DoctorValidator> get installed {
List<DoctorValidator> validators = <DoctorValidator>[];
List<String> installPaths = <String>['/Applications', fs.path.join(homeDirPath, 'Applications')];
void checkForIntelliJ(Directory dir) {
String name = fs.path.basename(dir.path);
_dirNameToId.forEach((String dirName, String id) {
if (name == dirName) {
String title = IntelliJValidator._idToTitle[id];
validators.add(new IntelliJValidatorOnMac(title, id, dir.path));
}
});
}
try {
Iterable<FileSystemEntity> installDirs = installPaths
.map((String installPath) => fs.directory(installPath).listSync())
.expand((List<FileSystemEntity> mappedDirs) => mappedDirs)
.where((FileSystemEntity mappedDir) => mappedDir is Directory);
for (FileSystemEntity dir in installDirs) {
if (dir is Directory) {
checkForIntelliJ(dir);
if (!dir.path.endsWith('.app')) {
for (FileSystemEntity subdir in dir.listSync()) {
if (subdir is Directory) {
checkForIntelliJ(subdir);
}
}
}
}
}
} on FileSystemException catch (e) {
validators.add(new ValidatorWithResult(
'Cannot determine if IntelliJ is installed',
new ValidationResult(ValidationType.missing, <ValidationMessage>[
new ValidationMessage.error(e.message),
]),
));
}
return validators;
}
@override
String get version {
if (_version == null) {
String plist;
try {
plist = fs.file(fs.path.join(installPath, 'Contents', 'Info.plist')).readAsStringSync();
int index = plist.indexOf('CFBundleShortVersionString');
if (index > 0) {
int start = plist.indexOf('<string>', index);
if (start > 0) {
int end = plist.indexOf('</string>', start);
if (end > 0) {
_version = plist.substring(start + 8, end);
}
}
}
} on FileSystemException catch (_) {
// ignored
}
_version ??= 'unknown';
}
return _version;
}
String _version;
@override
String get pluginsPath {
List<String> split = version.split('.');
String major = split[0];
String minor = split[1];
return fs.path.join(homeDirPath, 'Library', 'Application Support', '$id$major.$minor');
}
}
class DeviceValidator extends DoctorValidator {
DeviceValidator() : super('Connected devices');
@override
Future<ValidationResult> validate() async {
List<Device> devices = await deviceManager.getAllConnectedDevices();
List<ValidationMessage> messages;
if (devices.isEmpty) {
messages = <ValidationMessage>[new ValidationMessage('None')];
} else {
messages = Device.descriptions(devices)
.map((String msg) => new ValidationMessage(msg)).toList();
}
return new ValidationResult(ValidationType.installed, messages);
}
}
class ValidatorWithResult extends DoctorValidator {
final ValidationResult result;
ValidatorWithResult(String title, this.result) : super(title);
@override
Future<ValidationResult> validate() async => result;
}