doctor.dart 16.6 KB
Newer Older
1 2 3 4
// 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.

5
import 'dart:async';
6
import 'dart:convert' show UTF8;
Devon Carew's avatar
Devon Carew committed
7

8
import 'package:archive/archive.dart';
9

10
import 'android/android_studio_validator.dart';
11
import 'android/android_workflow.dart';
12
import 'artifacts.dart';
13
import 'base/common.dart';
14
import 'base/context.dart';
15
import 'base/file_system.dart';
16
import 'base/os.dart';
17
import 'base/platform.dart';
18
import 'base/process_manager.dart';
19
import 'base/version.dart';
20
import 'cache.dart';
21
import 'device.dart';
22
import 'globals.dart';
23
import 'ios/ios_workflow.dart';
24
import 'ios/plist_utils.dart';
25
import 'version.dart';
Devon Carew's avatar
Devon Carew committed
26

27 28
Doctor get doctor => context[Doctor];

29
class Doctor {
30 31 32 33 34 35 36
  List<DoctorValidator> _validators;

  List<DoctorValidator> get validators {
    if (_validators == null) {
      _validators = <DoctorValidator>[];
      _validators.add(new _FlutterValidator());

37 38
      if (androidWorkflow.appliesToHostPlatform)
        _validators.add(androidWorkflow);
39

40 41
      if (iosWorkflow.appliesToHostPlatform)
        _validators.add(iosWorkflow);
42

43
      final List<DoctorValidator> ideValidators = <DoctorValidator>[];
44 45 46 47 48 49 50 51 52 53 54
      ideValidators.addAll(AndroidStudioValidator.allValidators);
      ideValidators.addAll(IntelliJValidator.installedValidators);
      if (ideValidators.isNotEmpty)
        _validators.addAll(ideValidators);
      else
        _validators.add(new NoIdeValidator());

      _validators.add(new DeviceValidator());
    }
    return _validators;
  }
55

56
  List<Workflow> get workflows {
57
    return new List<Workflow>.from(validators.where((DoctorValidator validator) => validator is Workflow));
58
  }
59

60
  /// Print a summary of the state of the tooling, as well as how to get more info.
61 62 63
  Future<Null> summary() async {
    printStatus(await summaryText);
  }
64

65
  Future<String> get summaryText async {
66
    final StringBuffer buffer = new StringBuffer();
67 68 69

    bool allGood = true;

70
    for (DoctorValidator validator in validators) {
71
      final ValidationResult result = await validator.validate();
72
      buffer.write('${result.leadingBox} ${validator.title} is ');
73
      if (result.type == ValidationType.missing)
74
        buffer.write('not installed.');
75
      else if (result.type == ValidationType.partial)
76
        buffer.write('partially installed; more components are available.');
77
      else
78 79 80 81 82 83 84
        buffer.write('fully installed.');

      if (result.statusInfo != null)
        buffer.write(' (${result.statusInfo})');

      buffer.writeln();

85 86 87 88 89 90
      if (result.type != ValidationType.installed)
        allGood = false;
    }

    if (!allGood) {
      buffer.writeln();
Devon Carew's avatar
Devon Carew committed
91
      buffer.writeln('Run "flutter doctor" for information about installing additional components.');
92 93 94 95 96
    }

    return buffer.toString();
  }

97
  /// Print verbose information about the state of installed tooling.
98 99 100 101
  Future<bool> diagnose({ bool androidLicenses: false }) async {
    if (androidLicenses)
      return AndroidWorkflow.runLicenseManager();

102
    bool doctorResult = true;
103

104
    for (DoctorValidator validator in validators) {
105
      final ValidationResult result = await validator.validate();
106

107 108 109
      if (result.type == ValidationType.missing)
        doctorResult = false;

110 111 112 113 114 115
      if (result.statusInfo != null)
        printStatus('${result.leadingBox} ${validator.title} (${result.statusInfo})');
      else
        printStatus('${result.leadingBox} ${validator.title}');

      for (ValidationMessage message in result.messages) {
116
        final String text = message.message.replaceAll('\n', '\n      ');
117
        if (message.isError) {
118
          printStatus('    ✗ $text', emphasis: true);
119
        } else {
120
          printStatus('    • $text');
121 122
        }
      }
123 124

      printStatus('');
125
    }
126 127

    return doctorResult;
128 129 130 131 132 133 134
  }

  bool get canListAnything => workflows.any((Workflow workflow) => workflow.canListDevices);

  bool get canLaunchAnything => workflows.any((Workflow workflow) => workflow.canLaunchDevices);
}

Devon Carew's avatar
Devon Carew committed
135
/// A series of tools and required install steps for a target platform (iOS or Android).
136
abstract class Workflow {
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
  /// 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
}

153 154
abstract class DoctorValidator {
  DoctorValidator(this.title);
155

156
  final String title;
157

158
  Future<ValidationResult> validate();
159 160 161
}

class ValidationResult {
162
  ValidationResult(this.type, this.messages, { this.statusInfo });
163 164

  final ValidationType type;
165 166 167
  // A short message about the status.
  final String statusInfo;
  final List<ValidationMessage> messages;
168

169 170
  String get leadingBox {
    if (type == ValidationType.missing)
171
      return '[✗]';
172
    else if (type == ValidationType.installed)
173
      return '[✓]';
174 175 176
    else
      return '[-]';
  }
177
}
178

179 180 181
class ValidationMessage {
  ValidationMessage(this.message) : isError = false;
  ValidationMessage.error(this.message) : isError = true;
182

183 184
  final bool isError;
  final String message;
185

186 187 188
  @override
  String toString() => message;
}
189

190 191
class _FlutterValidator extends DoctorValidator {
  _FlutterValidator() : super('Flutter');
192

193
  @override
194
  Future<ValidationResult> validate() async {
195
    final List<ValidationMessage> messages = <ValidationMessage>[];
196
    ValidationType valid = ValidationType.installed;
197

198
    final FlutterVersion version = FlutterVersion.instance;
199

200
    messages.add(new ValidationMessage('Flutter at ${Cache.flutterRoot}'));
201 202
    if (Cache.flutterRoot.contains(' '))
      messages.add(new ValidationMessage.error(
203
        'Flutter SDK install paths with spaces are not yet supported. (https://github.com/flutter/flutter/issues/6577)\n'
204
        'Please move the SDK to a path that does not include spaces.'));
205 206
    messages.add(new ValidationMessage(
      'Framework revision ${version.frameworkRevisionShort} '
207
      '(${version.frameworkAge}), ${version.frameworkDate}'
208
    ));
209 210
    messages.add(new ValidationMessage('Engine revision ${version.engineRevisionShort}'));
    messages.add(new ValidationMessage('Tools Dart version ${version.dartSdkVersion}'));
211 212 213 214 215 216 217
    final String genSnapshotPath =
      artifacts.getArtifactPath(Artifact.genSnapshot);

    // Check that the binaries we downloaded for this platform actually run on it.
    if (!_genSnapshotRuns(genSnapshotPath)) {
      messages.add(new ValidationMessage.error('Downloaded executables cannot execute '
          'on host (see https://github.com/flutter/flutter/issues/6207 for more information)'));
218
      valid = ValidationType.partial;
219
    }
220

221
    return new ValidationResult(valid, messages,
222
      statusInfo: 'on ${os.name}, locale ${platform.localeName}, channel ${version.channel}');
223
  }
224
}
Devon Carew's avatar
Devon Carew committed
225

226 227 228 229 230 231
bool _genSnapshotRuns(String genSnapshotPath) {
  final int kExpectedExitCode = 255;
  try {
    return processManager.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
  } catch (error) {
    return false;
232 233 234
  }
}

235 236 237 238 239 240 241 242 243 244 245
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');
  }
}

246 247
abstract class IntelliJValidator extends DoctorValidator {
  IntelliJValidator(String title) : super(title);
248

249 250
  String get version;
  String get pluginsPath;
251

252 253 254 255
  static final Map<String, String> _idToTitle = <String, String>{
    'IntelliJIdea' : 'IntelliJ IDEA Ultimate Edition',
    'IdeaIC' : 'IntelliJ IDEA Community Edition',
  };
256

257 258 259
  static final Version kMinIdeaVersion = new Version(2017, 1, 0);
  static final Version kMinFlutterPluginVersion = new Version(14, 0, 0);

260
  static Iterable<DoctorValidator> get installedValidators {
261
    if (platform.isLinux || platform.isWindows)
262
      return IntelliJValidatorOnLinuxAndWindows.installed;
263
    if (platform.isMacOS)
264 265
      return IntelliJValidatorOnMac.installed;
    return <DoctorValidator>[];
266 267 268 269
  }

  @override
  Future<ValidationResult> validate() async {
270
    final List<ValidationMessage> messages = <ValidationMessage>[];
271

272 273
    _validatePackage(messages, 'flutter-intellij.jar', 'Flutter',
        minVersion: kMinFlutterPluginVersion);
274
    _validatePackage(messages, 'Dart', 'Dart');
275

276
    if (_hasIssues(messages)) {
277
      messages.add(new ValidationMessage(
278 279
        'For information about installing plugins, see\n'
        'https://flutter.io/intellij-setup/#installing-the-plugins'
280 281 282
      ));
    }

283
    _validateIntelliJVersion(messages, kMinIdeaVersion);
284

285
    return new ValidationResult(
286 287 288
      _hasIssues(messages) ? ValidationType.partial : ValidationType.installed,
      messages,
      statusInfo: 'version $version'
289 290 291
    );
  }

292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
  bool _hasIssues(List<ValidationMessage> messages) {
    return messages.any((ValidationMessage message) => message.isError);
  }

  void _validateIntelliJVersion(List<ValidationMessage> messages, Version minVersion) {
    // Ignore unknown versions.
    if (minVersion == Version.unknown)
      return;

    final Version installedVersion = new Version.parse(version);
    if (installedVersion == null)
      return;

    if (installedVersion < minVersion) {
      messages.add(new ValidationMessage.error(
        'This install is older than the minimum recommended version of $minVersion.'
      ));
    }
  }

  void _validatePackage(List<ValidationMessage> messages, String packageName, String title, {
    Version minVersion
  }) {
315
    if (!hasPackage(packageName)) {
316
      messages.add(new ValidationMessage.error(
317
        '$title plugin not installed; this adds $title specific functionality.'
318
      ));
319 320 321 322 323 324 325 326 327 328 329 330
      return;
    }
    final String versionText = _readPackageVersion(packageName);
    final Version version = new Version.parse(versionText);
    if (version != null && minVersion != null && version < minVersion) {
        messages.add(new ValidationMessage.error(
          '$title plugin version $versionText - the recommended minimum version is $minVersion'
        ));
    } else {
      messages.add(new ValidationMessage(
        '$title plugin ${version != null ? "version $version" : "installed"}'
      ));
331 332 333
    }
  }

334
  String _readPackageVersion(String packageName) {
335
    final String jarPath = packageName.endsWith('.jar')
336 337
        ? fs.path.join(pluginsPath, packageName)
        : fs.path.join(pluginsPath, packageName, 'lib', '$packageName.jar');
338 339 340
    // 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 {
341 342 343 344 345 346
      final Archive archive = new ZipDecoder().decodeBytes(fs.file(jarPath).readAsBytesSync());
      final ArchiveFile file = archive.findFile('META-INF/plugin.xml');
      final String content = UTF8.decode(file.content);
      final String versionStartTag = '<version>';
      final int start = content.indexOf(versionStartTag);
      final int end = content.indexOf('</version>', start);
347 348 349 350 351 352
      return content.substring(start + versionStartTag.length, end);
    } catch (_) {
      return null;
    }
  }

353
  bool hasPackage(String packageName) {
354
    final String packagePath = fs.path.join(pluginsPath, packageName);
355
    if (packageName.endsWith('.jar'))
356 357
      return fs.isFileSync(packagePath);
    return fs.isDirectorySync(packagePath);
358 359 360
  }
}

361 362
class IntelliJValidatorOnLinuxAndWindows extends IntelliJValidator {
  IntelliJValidatorOnLinuxAndWindows(String title, this.version, this.installPath, this.pluginsPath) : super(title);
363 364 365 366

  @override
  String version;

367 368
  final String installPath;

369 370 371 372
  @override
  String pluginsPath;

  static Iterable<DoctorValidator> get installed {
373
    final List<DoctorValidator> validators = <DoctorValidator>[];
374 375
    if (homeDirPath == null)
      return validators;
376 377

    void addValidator(String title, String version, String installPath, String pluginsPath) {
378
      final IntelliJValidatorOnLinuxAndWindows validator =
379
        new IntelliJValidatorOnLinuxAndWindows(title, version, installPath, pluginsPath);
380
      for (int index = 0; index < validators.length; ++index) {
381
        final DoctorValidator other = validators[index];
382
        if (other is IntelliJValidatorOnLinuxAndWindows && validator.installPath == other.installPath) {
383 384 385 386 387 388 389 390
          if (validator.version.compareTo(other.version) > 0)
            validators[index] = validator;
          return;
        }
      }
      validators.add(validator);
    }

391
    for (FileSystemEntity dir in fs.directory(homeDirPath).listSync()) {
392
      if (dir is Directory) {
393
        final String name = fs.path.basename(dir.path);
394 395
        IntelliJValidator._idToTitle.forEach((String id, String title) {
          if (name.startsWith('.$id')) {
396
            final String version = name.substring(id.length + 1);
397 398
            String installPath;
            try {
399
              installPath = fs.file(fs.path.join(dir.path, 'system', '.home')).readAsStringSync();
400 401 402
            } catch (e) {
              // ignored
            }
403
            if (installPath != null && fs.isDirectorySync(installPath)) {
404
              final String pluginsPath = fs.path.join(dir.path, 'config', 'plugins');
405
              addValidator(title, version, installPath, pluginsPath);
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
            }
          }
        });
      }
    }
    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',
423
    'IntelliJ IDEA Ultimate.app' : 'IntelliJIdea',
424 425 426 427
    'IntelliJ IDEA CE.app' : 'IdeaIC',
  };

  static Iterable<DoctorValidator> get installed {
428 429
    final List<DoctorValidator> validators = <DoctorValidator>[];
    final List<String> installPaths = <String>['/Applications', fs.path.join(homeDirPath, 'Applications')];
430 431

    void checkForIntelliJ(Directory dir) {
432
      final String name = fs.path.basename(dir.path);
433 434
      _dirNameToId.forEach((String dirName, String id) {
        if (name == dirName) {
435
          final String title = IntelliJValidator._idToTitle[id];
436 437 438 439 440
          validators.add(new IntelliJValidatorOnMac(title, id, dir.path));
        }
      });
    }

441
    try {
442
      final Iterable<FileSystemEntity> installDirs = installPaths
443 444
              .map((String installPath) => fs.directory(installPath))
              .map((Directory dir) => dir.existsSync() ? dir.listSync() : <FileSystemEntity>[])
445 446 447
              .expand((List<FileSystemEntity> mappedDirs) => mappedDirs)
              .where((FileSystemEntity mappedDir) => mappedDir is Directory);
      for (FileSystemEntity dir in installDirs) {
448 449 450 451
        if (dir is Directory) {
          checkForIntelliJ(dir);
          if (!dir.path.endsWith('.app')) {
            for (FileSystemEntity subdir in dir.listSync()) {
452
              if (subdir is Directory) {
453
                checkForIntelliJ(subdir);
454
              }
455
            }
456
          }
457
        }
458
      }
459 460 461 462 463 464 465
    } on FileSystemException catch (e) {
      validators.add(new ValidatorWithResult(
          'Cannot determine if IntelliJ is installed',
          new ValidationResult(ValidationType.missing, <ValidationMessage>[
             new ValidationMessage.error(e.message),
          ]),
      ));
466 467 468 469 470 471 472
    }
    return validators;
  }

  @override
  String get version {
    if (_version == null) {
473
      final String plistFile = fs.path.join(installPath, 'Contents', 'Info.plist');
474
      _version = getValueFromFile(plistFile, kCFBundleShortVersionStringKey) ?? 'unknown';
475 476 477 478 479 480 481
    }
    return _version;
  }
  String _version;

  @override
  String get pluginsPath {
482 483 484
    final List<String> split = version.split('.');
    final String major = split[0];
    final String minor = split[1];
485
    return fs.path.join(homeDirPath, 'Library', 'Application Support', '$id$major.$minor');
486 487 488
  }
}

489 490 491 492 493
class DeviceValidator extends DoctorValidator {
  DeviceValidator() : super('Connected devices');

  @override
  Future<ValidationResult> validate() async {
494
    final List<Device> devices = await deviceManager.getAllConnectedDevices().toList();
495 496 497 498
    List<ValidationMessage> messages;
    if (devices.isEmpty) {
      messages = <ValidationMessage>[new ValidationMessage('None')];
    } else {
499
      messages = await Device.descriptions(devices)
500 501 502 503 504
          .map((String msg) => new ValidationMessage(msg)).toList();
    }
    return new ValidationResult(ValidationType.installed, messages);
  }
}
505 506 507 508 509 510 511 512 513

class ValidatorWithResult extends DoctorValidator {
  final ValidationResult result;

  ValidatorWithResult(String title, this.result) : super(title);

  @override
  Future<ValidationResult> validate() async => result;
}