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

5 6
// @dart = 2.8

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

16
AndroidStudio get androidStudio => context.get<AndroidStudio>();
17 18 19 20 21 22 23 24 25 26

// Android Studio layout:

// Linux/Windows:
// $HOME/.AndroidStudioX.Y/system/.home

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

27
final RegExp _dotHomeStudioVersionMatcher =
28
    RegExp(r'^\.(AndroidStudio[^\d]*)([\d.]+)');
29

30 31
String get javaPath => androidStudio?.javaPath;

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

  factory AndroidStudio.fromMacOSBundle(String bundlePath) {
44 45
    String studioPath = globals.fs.path.join(bundlePath, 'Contents');
    String plistFile = globals.fs.path.join(studioPath, 'Info.plist');
46
    Map<String, dynamic> plistValues = globals.plistParser.parseFile(plistFile);
47 48
    // 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.
49
    final String jetBrainsToolboxAppBundlePath = plistValues['JetBrainsToolboxApp'] as String;
50
    if (jetBrainsToolboxAppBundlePath != null) {
51 52
      studioPath = globals.fs.path.join(jetBrainsToolboxAppBundlePath, 'Contents');
      plistFile = globals.fs.path.join(studioPath, 'Info.plist');
53
      plistValues = globals.plistParser.parseFile(plistFile);
54 55
    }

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

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

63
    String pathsSelectorValue;
64
    final Map<String, dynamic> jvmOptions = castStringKeyedMap(plistValues['JVMOptions']);
65
    if (jvmOptions != null) {
66
      final Map<String, dynamic> jvmProperties = castStringKeyedMap(jvmOptions['Properties']);
67
      if (jvmProperties != null) {
68
        pathsSelectorValue = jvmProperties['idea.paths.selector'] as String;
69 70
      }
    }
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92

    final int major = version?.major;
    final int minor = version?.minor;
    String presetPluginsPath;
    if (pathsSelectorValue != null) {
      if (major != null && major >= 4 && minor != null && minor >= 1) {
        presetPluginsPath = globals.fs.path.join(
          globals.fsUtils.homeDirPath,
          'Library',
          'Application Support',
          'Google',
          pathsSelectorValue,
        );
      } else {
        presetPluginsPath = globals.fs.path.join(
          globals.fsUtils.homeDirPath,
          'Library',
          'Application Support',
          pathsSelectorValue,
        );
      }
    }
93
    return AndroidStudio(studioPath, version: version, presetPluginsPath: presetPluginsPath);
94 95 96
  }

  factory AndroidStudio.fromHomeDot(Directory homeDotDir) {
97
    final Match versionMatch =
98 99 100 101
        _dotHomeStudioVersionMatcher.firstMatch(homeDotDir.basename);
    if (versionMatch?.groupCount != 2) {
      return null;
    }
102
    final Version version = Version.parse(versionMatch[2]);
103 104
    final String studioAppName = versionMatch[1];
    if (studioAppName == null || version == null) {
105 106
      return null;
    }
107 108
    String installPath;
    try {
109 110
      installPath = globals.fs
          .file(globals.fs.path.join(homeDotDir.path, 'system', '.home'))
111
          .readAsStringSync();
112
    } on Exception {
113
      // ignored, installPath will be null, which is handled below
114
    }
115
    if (installPath != null && globals.fs.isDirectorySync(installPath)) {
116 117 118 119 120
      return AndroidStudio(
          installPath,
          version: version,
          studioAppName: studioAppName,
      );
121 122 123 124
    }
    return null;
  }

125 126 127 128
  final String directory;
  final String studioAppName;
  final Version version;
  final String configured;
129
  final String presetPluginsPath;
130 131 132 133 134

  String _javaPath;
  bool _isValid = false;
  final List<String> _validationMessages = <String>[];

135 136
  String get javaPath => _javaPath;

137 138
  bool get isValid => _isValid;

139
  String get pluginsPath {
140 141 142 143 144
    if (presetPluginsPath != null) {
      return presetPluginsPath;
    }
    final int major = version?.major;
    final int minor = version?.minor;
145
    if (globals.platform.isMacOS) {
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
      /// 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(
          globals.fsUtils.homeDirPath,
          'Library',
          'Application Support',
          'Google',
          'AndroidStudio$major.$minor',
        );
      } else {
        return globals.fs.path.join(
          globals.fsUtils.homeDirPath,
          'Library',
          'Application Support',
          'AndroidStudio$major.$minor',
        );
      }
163
    } else {
164 165 166 167 168 169
      return globals.fs.path.join(
        globals.fsUtils.homeDirPath,
        '.$studioAppName$major.$minor',
        'config',
        'plugins',
      );
170 171 172
    }
  }

173 174 175 176
  List<String> get validationMessages => _validationMessages;

  @override
  int compareTo(AndroidStudio other) {
177
    final int result = version.compareTo(other.version);
178
    if (result == 0) {
179
      return directory.compareTo(other.directory);
180
    }
181 182 183 184 185
    return result;
  }

  /// Locates the newest, valid version of Android Studio.
  static AndroidStudio latestValid() {
186
    final String configuredStudio = globals.config.getValue('android-studio-dir') as String;
187 188
    if (configuredStudio != null) {
      String configuredStudioPath = configuredStudio;
189 190
      if (globals.platform.isMacOS && !configuredStudioPath.endsWith('Contents')) {
        configuredStudioPath = globals.fs.path.join(configuredStudioPath, 'Contents');
191
      }
192
      return AndroidStudio(configuredStudioPath,
193 194 195 196
          configured: configuredStudio);
    }

    // Find all available Studio installations.
197
    final List<AndroidStudio> studios = allInstalled();
198 199 200 201 202 203 204 205 206
    if (studios.isEmpty) {
      return null;
    }
    studios.sort();
    return studios.lastWhere((AndroidStudio s) => s.isValid,
        orElse: () => null);
  }

  static List<AndroidStudio> allInstalled() =>
207
      globals.platform.isMacOS ? _allMacOS() : _allLinuxOrWindows();
208 209

  static List<AndroidStudio> _allMacOS() {
210
    final List<FileSystemEntity> candidatePaths = <FileSystemEntity>[];
211 212

    void _checkForStudio(String path) {
213
      if (!globals.fs.isDirectorySync(path)) {
214
        return;
215
      }
216
      try {
217
        final Iterable<Directory> directories = globals.fs
218
            .directory(path)
219
            .listSync(followLinks: false)
220
            .whereType<Directory>();
221
        for (final Directory directory in directories) {
222 223 224
          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')) {
225 226 227 228
            candidatePaths.add(directory);
          } else if (!directory.path.endsWith('.app')) {
            _checkForStudio(directory.path);
          }
229
        }
230
      } on Exception catch (e) {
231
        globals.printTrace('Exception while looking for Android Studio: $e');
232 233 234 235
      }
    }

    _checkForStudio('/Applications');
236 237 238 239
    _checkForStudio(globals.fs.path.join(
      globals.fsUtils.homeDirPath,
      'Applications',
    ));
240

241
    final String configuredStudioDir = globals.config.getValue('android-studio-dir') as String;
242
    if (configuredStudioDir != null) {
243
      FileSystemEntity configuredStudio = globals.fs.file(configuredStudioDir);
244 245 246 247 248 249 250 251 252 253
      if (configuredStudio.basename == 'Contents') {
        configuredStudio = configuredStudio.parent;
      }
      if (!candidatePaths
          .any((FileSystemEntity e) => e.path == configuredStudio.path)) {
        candidatePaths.add(configuredStudio);
      }
    }

    return candidatePaths
254
        .map<AndroidStudio>((FileSystemEntity e) => AndroidStudio.fromMacOSBundle(e.path))
255 256 257 258 259
        .where((AndroidStudio s) => s != null)
        .toList();
  }

  static List<AndroidStudio> _allLinuxOrWindows() {
260
    final List<AndroidStudio> studios = <AndroidStudio>[];
261

262
    bool _hasStudioAt(String path, { Version newerThan }) {
263
      return studios.any((AndroidStudio studio) {
264
        if (studio.directory != path) {
265
          return false;
266
        }
267 268 269 270 271 272 273
        if (newerThan != null) {
          return studio.version.compareTo(newerThan) >= 0;
        }
        return true;
      });
    }

274
    // Read all $HOME/.AndroidStudio*/system/.home files. There may be several
275
    // pointing to the same installation, so we grab only the latest one.
276 277 278 279 280 281 282 283 284 285 286
    if (globals.fsUtils.homeDirPath != null &&
        globals.fs.directory(globals.fsUtils.homeDirPath).existsSync()) {
      final Directory homeDir = globals.fs.directory(globals.fsUtils.homeDirPath);
      for (final Directory entity in homeDir.listSync(followLinks: false).whereType<Directory>()) {
        if (!entity.basename.startsWith('.AndroidStudio')) {
          continue;
        }
        final AndroidStudio studio = AndroidStudio.fromHomeDot(entity);
        if (studio != null && !_hasStudioAt(studio.directory, newerThan: studio.version)) {
          studios.removeWhere((AndroidStudio other) => other.directory == studio.directory);
          studios.add(studio);
287 288 289
        }
      }
    }
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
    // 4.1 has a different location for AndroidStudio installs on Windows.
    if (globals.platform.isWindows) {
      final File homeDot = globals.fs.file(globals.fs.path.join(
        globals.platform.environment['LOCALAPPDATA'],
        '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);
          }
        }
      }
    }
313

314
    final String configuredStudioDir = globals.config.getValue('android-studio-dir') as String;
315
    if (configuredStudioDir != null && !_hasStudioAt(configuredStudioDir)) {
316
      studios.add(AndroidStudio(configuredStudioDir,
317 318 319
          configured: configuredStudioDir));
    }

320
    if (globals.platform.isLinux) {
321
      void _checkWellKnownPath(String path) {
322
        if (globals.fs.isDirectorySync(path) && !_hasStudioAt(path)) {
323
          studios.add(AndroidStudio(path));
324 325 326 327 328
        }
      }

      // Add /opt/android-studio and $HOME/android-studio, if they exist.
      _checkWellKnownPath('/opt/android-studio');
329
      _checkWellKnownPath('${globals.fsUtils.homeDirPath}/android-studio');
330 331 332 333
    }
    return studios;
  }

334 335 336 337 338 339 340
  static String extractStudioPlistValueWithMatcher(String plistValue, RegExp keyMatcher) {
    if (plistValue == null || keyMatcher == null) {
      return null;
    }
    return keyMatcher?.stringMatch(plistValue)?.split('=')?.last?.trim()?.replaceAll('"', '');
  }

341 342 343 344 345 346 347 348
  void _init() {
    _isValid = false;
    _validationMessages.clear();

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

349
    if (!globals.fs.isDirectorySync(directory)) {
350 351 352 353
      _validationMessages.add('Android Studio not found at $directory');
      return;
    }

354 355 356 357 358
    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)) {
359
      _validationMessages.add('Unable to find bundled Java version.');
360
    } else {
361 362
      RunResult result;
      try {
363
        result = globals.processUtils.runSync(<String>[javaExecutable, '-version']);
364 365 366 367
      } on ProcessException catch (e) {
        _validationMessages.add('Failed to run Java: $e');
      }
      if (result != null && result.exitCode == 0) {
368 369
        final List<String> versionLines = result.stderr.split('\n');
        final String javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
370
        _validationMessages.add('Java version $javaVersion');
371
        _javaPath = javaPath;
372
        _isValid = true;
373 374 375
      } else {
        _validationMessages.add('Unable to determine bundled Java version.');
      }
376
    }
377 378 379
  }

  @override
380
  String toString() => 'Android Studio ($version)';
381
}