android_studio.dart 10.4 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2017 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 '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/platform.dart';
9
import '../base/process.dart';
10
import '../base/process_manager.dart';
11
import '../base/version.dart';
12
import '../globals.dart';
13
import '../ios/plist_parser.dart';
14

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

// Android Studio layout:

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

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

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

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

31
class AndroidStudio implements Comparable<AndroidStudio> {
32 33 34 35 36 37 38
  AndroidStudio(
    this.directory, {
    Version version,
    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 = fs.path.join(bundlePath, 'Contents');
    String plistFile = fs.path.join(studioPath, 'Info.plist');
45
    Map<String, dynamic> plistValues = PlistParser.instance.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'];
49 50 51
    if (jetBrainsToolboxAppBundlePath != null) {
      studioPath = fs.path.join(jetBrainsToolboxAppBundlePath, 'Contents');
      plistFile = fs.path.join(studioPath, 'Info.plist');
52
      plistValues = PlistParser.instance.parseFile(plistFile);
53 54
    }

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

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

62 63 64 65 66 67 68 69
    String pathsSelectorValue;
    final Map<String, dynamic> jvmOptions = plistValues['JVMOptions'];
    if (jvmOptions != null) {
      final Map<String, dynamic> jvmProperties = jvmOptions['Properties'];
      if (jvmProperties != null) {
        pathsSelectorValue = jvmProperties['idea.paths.selector'];
      }
    }
70 71 72 73
    final String presetPluginsPath = pathsSelectorValue == null
        ? null
        : fs.path.join(homeDirPath, 'Library', 'Application Support', '$pathsSelectorValue');
    return AndroidStudio(studioPath, version: version, presetPluginsPath: presetPluginsPath);
74 75 76
  }

  factory AndroidStudio.fromHomeDot(Directory homeDotDir) {
77
    final Match versionMatch =
78 79 80 81
        _dotHomeStudioVersionMatcher.firstMatch(homeDotDir.basename);
    if (versionMatch?.groupCount != 2) {
      return null;
    }
82
    final Version version = Version.parse(versionMatch[2]);
83 84
    final String studioAppName = versionMatch[1];
    if (studioAppName == null || version == null) {
85 86
      return null;
    }
87 88 89 90 91 92
    String installPath;
    try {
      installPath = fs
          .file(fs.path.join(homeDotDir.path, 'system', '.home'))
          .readAsStringSync();
    } catch (e) {
93
      // ignored, installPath will be null, which is handled below
94 95
    }
    if (installPath != null && fs.isDirectorySync(installPath)) {
96 97 98 99 100
      return AndroidStudio(
          installPath,
          version: version,
          studioAppName: studioAppName,
      );
101 102 103 104
    }
    return null;
  }

105 106 107 108
  final String directory;
  final String studioAppName;
  final Version version;
  final String configured;
109
  final String presetPluginsPath;
110 111 112 113 114

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

115 116
  String get javaPath => _javaPath;

117 118
  bool get isValid => _isValid;

119
  String get pluginsPath {
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
    if (presetPluginsPath != null) {
      return presetPluginsPath;
    }
    final int major = version?.major;
    final int minor = version?.minor;
    if (platform.isMacOS) {
      return fs.path.join(
          homeDirPath,
          'Library',
          'Application Support',
          'AndroidStudio$major.$minor');
    } else {
      return fs.path.join(homeDirPath,
          '.$studioAppName$major.$minor',
          'config',
          'plugins');
136 137 138
    }
  }

139 140 141 142
  List<String> get validationMessages => _validationMessages;

  @override
  int compareTo(AndroidStudio other) {
143
    final int result = version.compareTo(other.version);
144
    if (result == 0) {
145
      return directory.compareTo(other.directory);
146
    }
147 148 149 150 151
    return result;
  }

  /// Locates the newest, valid version of Android Studio.
  static AndroidStudio latestValid() {
152
    final String configuredStudio = config.getValue('android-studio-dir');
153 154
    if (configuredStudio != null) {
      String configuredStudioPath = configuredStudio;
155
      if (platform.isMacOS && !configuredStudioPath.endsWith('Contents')) {
156
        configuredStudioPath = fs.path.join(configuredStudioPath, 'Contents');
157
      }
158
      return AndroidStudio(configuredStudioPath,
159 160 161 162
          configured: configuredStudio);
    }

    // Find all available Studio installations.
163
    final List<AndroidStudio> studios = allInstalled();
164 165 166 167 168 169 170 171 172 173 174 175
    if (studios.isEmpty) {
      return null;
    }
    studios.sort();
    return studios.lastWhere((AndroidStudio s) => s.isValid,
        orElse: () => null);
  }

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

  static List<AndroidStudio> _allMacOS() {
176
    final List<FileSystemEntity> candidatePaths = <FileSystemEntity>[];
177 178

    void _checkForStudio(String path) {
179
      if (!fs.isDirectorySync(path)) {
180
        return;
181
      }
182
      try {
183
        final Iterable<Directory> directories = fs
184
            .directory(path)
185
            .listSync(followLinks: false)
186
            .whereType<Directory>();
187
        for (Directory directory in directories) {
188 189 190
          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')) {
191 192 193 194
            candidatePaths.add(directory);
          } else if (!directory.path.endsWith('.app')) {
            _checkForStudio(directory.path);
          }
195
        }
196 197
      } catch (e) {
        printTrace('Exception while looking for Android Studio: $e');
198 199 200 201 202 203
      }
    }

    _checkForStudio('/Applications');
    _checkForStudio(fs.path.join(homeDirPath, 'Applications'));

204
    final String configuredStudioDir = config.getValue('android-studio-dir');
205 206 207 208 209 210 211 212 213 214 215 216
    if (configuredStudioDir != null) {
      FileSystemEntity configuredStudio = fs.file(configuredStudioDir);
      if (configuredStudio.basename == 'Contents') {
        configuredStudio = configuredStudio.parent;
      }
      if (!candidatePaths
          .any((FileSystemEntity e) => e.path == configuredStudio.path)) {
        candidatePaths.add(configuredStudio);
      }
    }

    return candidatePaths
217
        .map<AndroidStudio>((FileSystemEntity e) => AndroidStudio.fromMacOSBundle(e.path))
218 219 220 221 222
        .where((AndroidStudio s) => s != null)
        .toList();
  }

  static List<AndroidStudio> _allLinuxOrWindows() {
223
    final List<AndroidStudio> studios = <AndroidStudio>[];
224

225
    bool _hasStudioAt(String path, { Version newerThan }) {
226
      return studios.any((AndroidStudio studio) {
227
        if (studio.directory != path) {
228
          return false;
229
        }
230 231 232 233 234 235 236
        if (newerThan != null) {
          return studio.version.compareTo(newerThan) >= 0;
        }
        return true;
      });
    }

237
    // Read all $HOME/.AndroidStudio*/system/.home files. There may be several
238
    // pointing to the same installation, so we grab only the latest one.
239
    if (fs.directory(homeDirPath).existsSync()) {
240
      for (FileSystemEntity entity in fs.directory(homeDirPath).listSync(followLinks: false)) {
241
        if (entity is Directory && entity.basename.startsWith('.AndroidStudio')) {
242
          final AndroidStudio studio = AndroidStudio.fromHomeDot(entity);
243 244 245 246
          if (studio != null && !_hasStudioAt(studio.directory, newerThan: studio.version)) {
            studios.removeWhere((AndroidStudio other) => other.directory == studio.directory);
            studios.add(studio);
          }
247 248 249 250
        }
      }
    }

251
    final String configuredStudioDir = config.getValue('android-studio-dir');
252
    if (configuredStudioDir != null && !_hasStudioAt(configuredStudioDir)) {
253
      studios.add(AndroidStudio(configuredStudioDir,
254 255 256 257 258 259
          configured: configuredStudioDir));
    }

    if (platform.isLinux) {
      void _checkWellKnownPath(String path) {
        if (fs.isDirectorySync(path) && !_hasStudioAt(path)) {
260
          studios.add(AndroidStudio(path));
261 262 263 264 265 266 267 268 269 270
        }
      }

      // Add /opt/android-studio and $HOME/android-studio, if they exist.
      _checkWellKnownPath('/opt/android-studio');
      _checkWellKnownPath('$homeDirPath/android-studio');
    }
    return studios;
  }

271 272 273 274 275 276 277
  static String extractStudioPlistValueWithMatcher(String plistValue, RegExp keyMatcher) {
    if (plistValue == null || keyMatcher == null) {
      return null;
    }
    return keyMatcher?.stringMatch(plistValue)?.split('=')?.last?.trim()?.replaceAll('"', '');
  }

278 279 280 281 282 283 284 285 286 287 288 289 290
  void _init() {
    _isValid = false;
    _validationMessages.clear();

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

    if (!fs.isDirectorySync(directory)) {
      _validationMessages.add('Android Studio not found at $directory');
      return;
    }

291 292 293
    final String javaPath = platform.isMacOS ?
        fs.path.join(directory, 'jre', 'jdk', 'Contents', 'Home') :
        fs.path.join(directory, 'jre');
294 295
    final String javaExecutable = fs.path.join(javaPath, 'bin', 'java');
    if (!processManager.canRun(javaExecutable)) {
296
      _validationMessages.add('Unable to find bundled Java version.');
297
    } else {
298
      final RunResult result = processUtils.runSync(<String>[javaExecutable, '-version']);
299 300 301
      if (result.exitCode == 0) {
        final List<String> versionLines = result.stderr.split('\n');
        final String javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
302
        _validationMessages.add('Java version $javaVersion');
303
        _javaPath = javaPath;
304
        _isValid = true;
305 306 307
      } else {
        _validationMessages.add('Unable to determine bundled Java version.');
      }
308
    }
309 310 311
  }

  @override
312
  String toString() => 'Android Studio ($version)';
313
}