android_studio.dart 16.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../base/file_system.dart';
6
import '../base/io.dart';
7
import '../base/process.dart';
8
import '../base/utils.dart';
9
import '../base/version.dart';
10
import '../convert.dart';
11
import '../globals_null_migrated.dart' as globals;
12
import '../ios/plist_parser.dart';
13 14 15 16 17

// Android Studio layout:

// Linux/Windows:
// $HOME/.AndroidStudioX.Y/system/.home
18
// $HOME/.cache/Google/AndroidStudioX.Y/.home
19 20 21 22 23

// macOS:
// /Applications/Android Studio.app/Contents/
// $HOME/Applications/Android Studio.app/Contents/

24 25
// Match Android Studio >= 4.1 base folder (AndroidStudio*.*)
// and < 4.1 (.AndroidStudio*.*)
26
final RegExp _dotHomeStudioVersionMatcher =
27
    RegExp(r'^\.?(AndroidStudio[^\d]*)([\d.]+)');
28

29
String? get javaPath => globals.androidStudio?.javaPath;
30

31
class AndroidStudio implements Comparable<AndroidStudio> {
32 33
  AndroidStudio(
    this.directory, {
34
    Version? version,
35 36 37 38
    this.configured,
    this.studioAppName = 'AndroidStudio',
    this.presetPluginsPath,
  }) : version = version ?? Version.unknown {
39 40 41 42
    _init();
  }

  factory AndroidStudio.fromMacOSBundle(String bundlePath) {
43 44
    String studioPath = globals.fs.path.join(bundlePath, 'Contents');
    String plistFile = globals.fs.path.join(studioPath, 'Info.plist');
45
    Map<String, dynamic> plistValues = globals.plistParser.parseFile(plistFile);
46 47
    // As AndroidStudio managed by JetBrainsToolbox could have a wrapper pointing to the real Android Studio.
    // Check if we've found a JetBrainsToolbox wrapper and deal with it properly.
48
    final String? jetBrainsToolboxAppBundlePath = plistValues['JetBrainsToolboxApp'] as String?;
49
    if (jetBrainsToolboxAppBundlePath != null) {
50 51
      studioPath = globals.fs.path.join(jetBrainsToolboxAppBundlePath, 'Contents');
      plistFile = globals.fs.path.join(studioPath, 'Info.plist');
52
      plistValues = globals.plistParser.parseFile(plistFile);
53 54
    }

55
    final String versionString = plistValues[PlistParser.kCFBundleShortVersionStringKey] as String;
56

57
    Version? version;
58
    if (versionString != null) {
59
      version = Version.parse(versionString);
60
    }
61

62 63
    String? pathsSelectorValue;
    final Map<String, dynamic>? jvmOptions = castStringKeyedMap(plistValues['JVMOptions']);
64
    if (jvmOptions != null) {
65
      final Map<String, dynamic>? jvmProperties = castStringKeyedMap(jvmOptions['Properties']);
66
      if (jvmProperties != null) {
67
        pathsSelectorValue = jvmProperties['idea.paths.selector'] as String;
68 69
      }
    }
70

71 72 73 74 75
    final int? major = version?.major;
    final int? minor = version?.minor;
    String? presetPluginsPath;
    final String? homeDirPath = globals.fsUtils.homeDirPath;
    if (homeDirPath != null && pathsSelectorValue != null) {
76 77
      if (major != null && major >= 4 && minor != null && minor >= 1) {
        presetPluginsPath = globals.fs.path.join(
78
          homeDirPath,
79 80 81 82 83 84 85
          'Library',
          'Application Support',
          'Google',
          pathsSelectorValue,
        );
      } else {
        presetPluginsPath = globals.fs.path.join(
86
          homeDirPath,
87 88 89 90 91 92
          'Library',
          'Application Support',
          pathsSelectorValue,
        );
      }
    }
93
    return AndroidStudio(studioPath, version: version, presetPluginsPath: presetPluginsPath);
94 95
  }

96 97
  static AndroidStudio? fromHomeDot(Directory homeDotDir) {
    final Match? versionMatch =
98 99 100 101
        _dotHomeStudioVersionMatcher.firstMatch(homeDotDir.basename);
    if (versionMatch?.groupCount != 2) {
      return null;
    }
102 103
    final Version? version = Version.parse(versionMatch![2]);
    final String? studioAppName = versionMatch[1];
104
    if (studioAppName == null || version == null) {
105 106
      return null;
    }
107

108 109
    final int major = version.major;
    final int minor = version.minor;
110 111 112 113 114 115 116 117 118 119 120 121 122

    // The install path is written in a .home text file,
    // it location is in <base dir>/.home for Android Studio >= 4.1
    // and <base dir>/system/.home for Android Studio < 4.1
    String dotHomeFilePath;

    if (major != null && major >= 4 && minor != null && minor >= 1) {
      dotHomeFilePath = globals.fs.path.join(homeDotDir.path, '.home');
    } else {
      dotHomeFilePath =
          globals.fs.path.join(homeDotDir.path, 'system', '.home');
    }

123
    String? installPath;
124

125
    try {
126
      installPath = globals.fs.file(dotHomeFilePath).readAsStringSync();
127
    } on Exception {
128
      // ignored, installPath will be null, which is handled below
129
    }
130

131
    if (installPath != null && globals.fs.isDirectorySync(installPath)) {
132 133 134 135 136
      return AndroidStudio(
          installPath,
          version: version,
          studioAppName: studioAppName,
      );
137 138 139 140
    }
    return null;
  }

141 142 143
  final String directory;
  final String studioAppName;
  final Version version;
144 145
  final String? configured;
  final String? presetPluginsPath;
146

147
  String? _javaPath;
148 149 150
  bool _isValid = false;
  final List<String> _validationMessages = <String>[];

151
  String? get javaPath => _javaPath;
152

153 154
  bool get isValid => _isValid;

155
  String? get pluginsPath {
156
    if (presetPluginsPath != null) {
157 158 159 160 161 162 163
      return presetPluginsPath!;
    }
    final int major = version.major;
    final int minor = version.minor;
    final String? homeDirPath = globals.fsUtils.homeDirPath;
    if (homeDirPath == null) {
      return null;
164
    }
165
    if (globals.platform.isMacOS) {
166 167 168
      /// plugin path of Android Studio has been changed after version 4.1.
      if (major != null && major >= 4 && minor != null && minor >= 1) {
        return globals.fs.path.join(
169
          homeDirPath,
170 171 172 173 174 175 176
          'Library',
          'Application Support',
          'Google',
          'AndroidStudio$major.$minor',
        );
      } else {
        return globals.fs.path.join(
177
          homeDirPath,
178 179 180 181 182
          'Library',
          'Application Support',
          'AndroidStudio$major.$minor',
        );
      }
183
    } else {
184 185 186 187 188 189 190 191 192 193
      // JetBrains Toolbox write plugins here
      final String toolboxPluginsPath = '$directory.plugins';

      if (globals.fs.directory(toolboxPluginsPath).existsSync()) {
        return toolboxPluginsPath;
      }

      if (major != null && major >= 4 && minor != null && minor >= 1 &&
          globals.platform.isLinux) {
        return globals.fs.path.join(
194
          homeDirPath,
195 196 197 198 199 200 201
          '.local',
          'share',
          'Google',
          '$studioAppName$major.$minor',
        );
      }

202
      return globals.fs.path.join(
203
        homeDirPath,
204 205 206 207
        '.$studioAppName$major.$minor',
        'config',
        'plugins',
      );
208 209 210
    }
  }

211 212 213 214
  List<String> get validationMessages => _validationMessages;

  @override
  int compareTo(AndroidStudio other) {
215
    final int result = version.compareTo(other.version);
216
    if (result == 0) {
217
      return directory.compareTo(other.directory);
218
    }
219 220 221 222
    return result;
  }

  /// Locates the newest, valid version of Android Studio.
223
  static AndroidStudio? latestValid() {
224
    final String? configuredStudio = globals.config.getValue('android-studio-dir') as String?;
225 226
    if (configuredStudio != null) {
      String configuredStudioPath = configuredStudio;
227 228
      if (globals.platform.isMacOS && !configuredStudioPath.endsWith('Contents')) {
        configuredStudioPath = globals.fs.path.join(configuredStudioPath, 'Contents');
229
      }
230
      return AndroidStudio(configuredStudioPath,
231 232 233 234
          configured: configuredStudio);
    }

    // Find all available Studio installations.
235
    final List<AndroidStudio> studios = allInstalled();
236 237 238
    if (studios.isEmpty) {
      return null;
    }
239 240 241 242 243 244 245 246
    AndroidStudio? newest;
    for (final AndroidStudio studio in studios.where((AndroidStudio s) => s.isValid)) {
      if (newest == null || studio.compareTo(newest) > 0) {
        newest = studio;
      }
    }

    return newest;
247 248 249
  }

  static List<AndroidStudio> allInstalled() =>
250
      globals.platform.isMacOS ? _allMacOS() : _allLinuxOrWindows();
251 252

  static List<AndroidStudio> _allMacOS() {
253
    final List<FileSystemEntity> candidatePaths = <FileSystemEntity>[];
254 255

    void _checkForStudio(String path) {
256
      if (!globals.fs.isDirectorySync(path)) {
257
        return;
258
      }
259
      try {
260
        final Iterable<Directory> directories = globals.fs
261
            .directory(path)
262
            .listSync(followLinks: false)
263
            .whereType<Directory>();
264
        for (final Directory directory in directories) {
265 266 267
          final String name = directory.basename;
          // An exact match, or something like 'Android Studio 3.0 Preview.app'.
          if (name.startsWith('Android Studio') && name.endsWith('.app')) {
268 269 270 271
            candidatePaths.add(directory);
          } else if (!directory.path.endsWith('.app')) {
            _checkForStudio(directory.path);
          }
272
        }
273
      } on Exception catch (e) {
274
        globals.printTrace('Exception while looking for Android Studio: $e');
275 276 277 278
      }
    }

    _checkForStudio('/Applications');
279 280 281 282 283 284 285
    final String? homeDirPath = globals.fsUtils.homeDirPath;
    if (homeDirPath != null) {
      _checkForStudio(globals.fs.path.join(
        homeDirPath,
        'Applications',
      ));
    }
286

287
    final String? configuredStudioDir = globals.config.getValue('android-studio-dir') as String?;
288
    if (configuredStudioDir != null) {
289
      FileSystemEntity configuredStudio = globals.fs.file(configuredStudioDir);
290 291 292 293 294 295 296 297 298
      if (configuredStudio.basename == 'Contents') {
        configuredStudio = configuredStudio.parent;
      }
      if (!candidatePaths
          .any((FileSystemEntity e) => e.path == configuredStudio.path)) {
        candidatePaths.add(configuredStudio);
      }
    }

299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
    // Query Spotlight for unexpected installation locations.
    String spotlightQueryResult = '';
    try {
      final ProcessResult spotlightResult = globals.processManager.runSync(<String>[
        'mdfind',
        // com.google.android.studio, com.google.android.studio-EAP
        'kMDItemCFBundleIdentifier="com.google.android.studio*"',
      ]);
      spotlightQueryResult = spotlightResult.stdout as String;
    } on ProcessException {
      // The Spotlight query is a nice-to-have, continue checking known installation locations.
    }
    for (final String studioPath in LineSplitter.split(spotlightQueryResult)) {
      final Directory appBundle = globals.fs.directory(studioPath);
      if (!candidatePaths.any((FileSystemEntity e) => e.path == studioPath)) {
        candidatePaths.add(appBundle);
      }
    }

318
    return candidatePaths
319
        .map<AndroidStudio>((FileSystemEntity e) => AndroidStudio.fromMacOSBundle(e.path))
320
        .whereType<AndroidStudio>()
321 322 323 324
        .toList();
  }

  static List<AndroidStudio> _allLinuxOrWindows() {
325
    final List<AndroidStudio> studios = <AndroidStudio>[];
326

327
    bool _hasStudioAt(String path, { Version? newerThan }) {
328
      return studios.any((AndroidStudio studio) {
329
        if (studio.directory != path) {
330
          return false;
331
        }
332 333 334 335 336 337 338
        if (newerThan != null) {
          return studio.version.compareTo(newerThan) >= 0;
        }
        return true;
      });
    }

339 340 341 342
    // Read all $HOME/.AndroidStudio*/system/.home
    // or $HOME/.cache/Google/AndroidStudio*/.home files.
    // There may be several pointing to the same installation,
    // so we grab only the latest one.
343
    final String? homeDirPath = globals.fsUtils.homeDirPath;
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367

    if (homeDirPath != null && globals.fs.directory(homeDirPath).existsSync()) {
      final Directory homeDir = globals.fs.directory(homeDirPath);

      final List<Directory> directoriesToSearch = <Directory>[homeDir];

      // >=4.1 has new install location at $HOME/.cache/Google
      final String cacheDirPath =
          globals.fs.path.join(homeDirPath, '.cache', 'Google');

      if (globals.fs.isDirectorySync(cacheDirPath)) {
        directoriesToSearch.add(globals.fs.directory(cacheDirPath));
      }

      final List<Directory> entities = <Directory>[];

      for (final Directory baseDir in directoriesToSearch) {
        final Iterable<Directory> directories =
            baseDir.listSync(followLinks: false).whereType<Directory>();
        entities.addAll(directories.where((Directory directory) =>
            _dotHomeStudioVersionMatcher.hasMatch(directory.basename)));
      }

      for (final Directory entity in entities) {
368
        final AndroidStudio? studio = AndroidStudio.fromHomeDot(entity);
369 370 371
        if (studio != null && !_hasStudioAt(studio.directory, newerThan: studio.version)) {
          studios.removeWhere((AndroidStudio other) => other.directory == studio.directory);
          studios.add(studio);
372 373 374
        }
      }
    }
375

376
    // 4.1 has a different location for AndroidStudio installs on Windows.
377
    if (globals.platform.isWindows && globals.platform.environment.containsKey('LOCALAPPDATA')) {
378
      final File homeDot = globals.fs.file(globals.fs.path.join(
379
        globals.platform.environment['LOCALAPPDATA']!,
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
        'Google',
        'AndroidStudio4.1',
        '.home',
      ));
      if (homeDot.existsSync()) {
        final String installPath = homeDot.readAsStringSync();
        if (globals.fs.isDirectorySync(installPath)) {
          final AndroidStudio studio = AndroidStudio(
            installPath,
            version: Version(4, 1, 0),
            studioAppName: 'Android Studio 4.1',
          );
          if (studio != null && !_hasStudioAt(studio.directory, newerThan: studio.version)) {
            studios.removeWhere((AndroidStudio other) => other.directory == studio.directory);
            studios.add(studio);
          }
        }
      }
    }
399

400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
    // 4.2 has a different location for AndroidStudio installs on Windows.
    if (globals.platform.isWindows && globals.platform.environment.containsKey('LOCALAPPDATA')) {
      final File homeDot = globals.fs.file(globals.fs.path.join(
        globals.platform.environment['LOCALAPPDATA']!,
        'Google',
        'AndroidStudio4.2',
        '.home',
      ));
      if (homeDot.existsSync()) {
        final String installPath = homeDot.readAsStringSync();
        if (globals.fs.isDirectorySync(installPath)) {
          final AndroidStudio studio = AndroidStudio(
            installPath,
            version: Version(4, 2, 0),
            studioAppName: 'Android Studio 4.2',
          );
          if (studio != null && !_hasStudioAt(studio.directory, newerThan: studio.version)) {
            studios.removeWhere((AndroidStudio other) => other.directory == studio.directory);
            studios.add(studio);
          }
        }
      }
    }

424
    final String? configuredStudioDir = globals.config.getValue('android-studio-dir') as String?;
425
    if (configuredStudioDir != null && !_hasStudioAt(configuredStudioDir)) {
426
      studios.add(AndroidStudio(configuredStudioDir,
427 428 429
          configured: configuredStudioDir));
    }

430
    if (globals.platform.isLinux) {
431
      void _checkWellKnownPath(String path) {
432
        if (globals.fs.isDirectorySync(path) && !_hasStudioAt(path)) {
433
          studios.add(AndroidStudio(path));
434 435 436 437 438
        }
      }

      // Add /opt/android-studio and $HOME/android-studio, if they exist.
      _checkWellKnownPath('/opt/android-studio');
439
      _checkWellKnownPath('${globals.fsUtils.homeDirPath}/android-studio');
440 441 442 443
    }
    return studios;
  }

444
  static String? extractStudioPlistValueWithMatcher(String plistValue, RegExp keyMatcher) {
445 446 447
    if (plistValue == null || keyMatcher == null) {
      return null;
    }
448
    return keyMatcher.stringMatch(plistValue)?.split('=').last.trim().replaceAll('"', '');
449 450
  }

451 452 453 454 455 456 457 458
  void _init() {
    _isValid = false;
    _validationMessages.clear();

    if (configured != null) {
      _validationMessages.add('android-studio-dir = $configured');
    }

459
    if (!globals.fs.isDirectorySync(directory)) {
460 461 462 463
      _validationMessages.add('Android Studio not found at $directory');
      return;
    }

464 465 466 467 468
    final String javaPath = globals.platform.isMacOS ?
        globals.fs.path.join(directory, 'jre', 'jdk', 'Contents', 'Home') :
        globals.fs.path.join(directory, 'jre');
    final String javaExecutable = globals.fs.path.join(javaPath, 'bin', 'java');
    if (!globals.processManager.canRun(javaExecutable)) {
469
      _validationMessages.add('Unable to find bundled Java version.');
470
    } else {
471
      RunResult? result;
472
      try {
473
        result = globals.processUtils.runSync(<String>[javaExecutable, '-version']);
474 475 476 477
      } on ProcessException catch (e) {
        _validationMessages.add('Failed to run Java: $e');
      }
      if (result != null && result.exitCode == 0) {
478 479
        final List<String> versionLines = result.stderr.split('\n');
        final String javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
480
        _validationMessages.add('Java version $javaVersion');
481
        _javaPath = javaPath;
482
        _isValid = true;
483 484 485
      } else {
        _validationMessages.add('Unable to determine bundled Java version.');
      }
486
    }
487 488 489
  }

  @override
490
  String toString() => 'Android Studio ($version)';
491
}