1// Copyright 2017 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import '../base/common.dart'; 6import '../base/context.dart'; 7import '../base/file_system.dart'; 8import '../base/io.dart'; 9import '../base/platform.dart'; 10import '../base/process_manager.dart'; 11import '../base/version.dart'; 12import '../globals.dart'; 13import '../ios/plist_parser.dart'; 14 15AndroidStudio get androidStudio => context.get<AndroidStudio>(); 16 17// Android Studio layout: 18 19// Linux/Windows: 20// $HOME/.AndroidStudioX.Y/system/.home 21 22// macOS: 23// /Applications/Android Studio.app/Contents/ 24// $HOME/Applications/Android Studio.app/Contents/ 25 26final RegExp _dotHomeStudioVersionMatcher = 27 RegExp(r'^\.(AndroidStudio[^\d]*)([\d.]+)'); 28 29String get javaPath => androidStudio?.javaPath; 30 31class AndroidStudio implements Comparable<AndroidStudio> { 32 AndroidStudio( 33 this.directory, { 34 Version version, 35 this.configured, 36 this.studioAppName = 'AndroidStudio', 37 this.presetPluginsPath, 38 }) : version = version ?? Version.unknown { 39 _init(); 40 } 41 42 factory AndroidStudio.fromMacOSBundle(String bundlePath) { 43 String studioPath = fs.path.join(bundlePath, 'Contents'); 44 String plistFile = fs.path.join(studioPath, 'Info.plist'); 45 Map<String, dynamic> plistValues = PlistParser.instance.parseFile(plistFile); 46 // As AndroidStudio managed by JetBrainsToolbox could have a wrapper pointing to the real Android Studio. 47 // Check if we've found a JetBrainsToolbox wrapper and deal with it properly. 48 final String jetBrainsToolboxAppBundlePath = plistValues['JetBrainsToolboxApp']; 49 if (jetBrainsToolboxAppBundlePath != null) { 50 studioPath = fs.path.join(jetBrainsToolboxAppBundlePath, 'Contents'); 51 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 String pathsSelectorValue; 62 final Map<String, dynamic> jvmOptions = plistValues['JVMOptions']; 63 if (jvmOptions != null) { 64 final Map<String, dynamic> jvmProperties = jvmOptions['Properties']; 65 if (jvmProperties != null) { 66 pathsSelectorValue = jvmProperties['idea.paths.selector']; 67 } 68 } 69 final String presetPluginsPath = pathsSelectorValue == null 70 ? null 71 : fs.path.join(homeDirPath, 'Library', 'Application Support', '$pathsSelectorValue'); 72 return AndroidStudio(studioPath, version: version, presetPluginsPath: presetPluginsPath); 73 } 74 75 factory AndroidStudio.fromHomeDot(Directory homeDotDir) { 76 final Match versionMatch = 77 _dotHomeStudioVersionMatcher.firstMatch(homeDotDir.basename); 78 if (versionMatch?.groupCount != 2) { 79 return null; 80 } 81 final Version version = Version.parse(versionMatch[2]); 82 final String studioAppName = versionMatch[1]; 83 if (studioAppName == null || version == null) { 84 return null; 85 } 86 String installPath; 87 try { 88 installPath = fs 89 .file(fs.path.join(homeDotDir.path, 'system', '.home')) 90 .readAsStringSync(); 91 } catch (e) { 92 // ignored, installPath will be null, which is handled below 93 } 94 if (installPath != null && fs.isDirectorySync(installPath)) { 95 return AndroidStudio( 96 installPath, 97 version: version, 98 studioAppName: studioAppName, 99 ); 100 } 101 return null; 102 } 103 104 final String directory; 105 final String studioAppName; 106 final Version version; 107 final String configured; 108 final String presetPluginsPath; 109 110 String _javaPath; 111 bool _isValid = false; 112 final List<String> _validationMessages = <String>[]; 113 114 String get javaPath => _javaPath; 115 116 bool get isValid => _isValid; 117 118 String get pluginsPath { 119 if (presetPluginsPath != null) { 120 return presetPluginsPath; 121 } 122 final int major = version?.major; 123 final int minor = version?.minor; 124 if (platform.isMacOS) { 125 return fs.path.join( 126 homeDirPath, 127 'Library', 128 'Application Support', 129 'AndroidStudio$major.$minor'); 130 } else { 131 return fs.path.join(homeDirPath, 132 '.$studioAppName$major.$minor', 133 'config', 134 'plugins'); 135 } 136 } 137 138 List<String> get validationMessages => _validationMessages; 139 140 @override 141 int compareTo(AndroidStudio other) { 142 final int result = version.compareTo(other.version); 143 if (result == 0) 144 return directory.compareTo(other.directory); 145 return result; 146 } 147 148 /// Locates the newest, valid version of Android Studio. 149 static AndroidStudio latestValid() { 150 final String configuredStudio = config.getValue('android-studio-dir'); 151 if (configuredStudio != null) { 152 String configuredStudioPath = configuredStudio; 153 if (platform.isMacOS && !configuredStudioPath.endsWith('Contents')) 154 configuredStudioPath = fs.path.join(configuredStudioPath, 'Contents'); 155 return AndroidStudio(configuredStudioPath, 156 configured: configuredStudio); 157 } 158 159 // Find all available Studio installations. 160 final List<AndroidStudio> studios = allInstalled(); 161 if (studios.isEmpty) { 162 return null; 163 } 164 studios.sort(); 165 return studios.lastWhere((AndroidStudio s) => s.isValid, 166 orElse: () => null); 167 } 168 169 static List<AndroidStudio> allInstalled() => 170 platform.isMacOS ? _allMacOS() : _allLinuxOrWindows(); 171 172 static List<AndroidStudio> _allMacOS() { 173 final List<FileSystemEntity> candidatePaths = <FileSystemEntity>[]; 174 175 void _checkForStudio(String path) { 176 if (!fs.isDirectorySync(path)) 177 return; 178 try { 179 final Iterable<Directory> directories = fs 180 .directory(path) 181 .listSync(followLinks: false) 182 .whereType<Directory>(); 183 for (Directory directory in directories) { 184 final String name = directory.basename; 185 // An exact match, or something like 'Android Studio 3.0 Preview.app'. 186 if (name.startsWith('Android Studio') && name.endsWith('.app')) { 187 candidatePaths.add(directory); 188 } else if (!directory.path.endsWith('.app')) { 189 _checkForStudio(directory.path); 190 } 191 } 192 } catch (e) { 193 printTrace('Exception while looking for Android Studio: $e'); 194 } 195 } 196 197 _checkForStudio('/Applications'); 198 _checkForStudio(fs.path.join(homeDirPath, 'Applications')); 199 200 final String configuredStudioDir = config.getValue('android-studio-dir'); 201 if (configuredStudioDir != null) { 202 FileSystemEntity configuredStudio = fs.file(configuredStudioDir); 203 if (configuredStudio.basename == 'Contents') { 204 configuredStudio = configuredStudio.parent; 205 } 206 if (!candidatePaths 207 .any((FileSystemEntity e) => e.path == configuredStudio.path)) { 208 candidatePaths.add(configuredStudio); 209 } 210 } 211 212 return candidatePaths 213 .map<AndroidStudio>((FileSystemEntity e) => AndroidStudio.fromMacOSBundle(e.path)) 214 .where((AndroidStudio s) => s != null) 215 .toList(); 216 } 217 218 static List<AndroidStudio> _allLinuxOrWindows() { 219 final List<AndroidStudio> studios = <AndroidStudio>[]; 220 221 bool _hasStudioAt(String path, { Version newerThan }) { 222 return studios.any((AndroidStudio studio) { 223 if (studio.directory != path) 224 return false; 225 if (newerThan != null) { 226 return studio.version.compareTo(newerThan) >= 0; 227 } 228 return true; 229 }); 230 } 231 232 // Read all $HOME/.AndroidStudio*/system/.home files. There may be several 233 // pointing to the same installation, so we grab only the latest one. 234 if (fs.directory(homeDirPath).existsSync()) { 235 for (FileSystemEntity entity in fs.directory(homeDirPath).listSync(followLinks: false)) { 236 if (entity is Directory && entity.basename.startsWith('.AndroidStudio')) { 237 final AndroidStudio studio = AndroidStudio.fromHomeDot(entity); 238 if (studio != null && !_hasStudioAt(studio.directory, newerThan: studio.version)) { 239 studios.removeWhere((AndroidStudio other) => other.directory == studio.directory); 240 studios.add(studio); 241 } 242 } 243 } 244 } 245 246 final String configuredStudioDir = config.getValue('android-studio-dir'); 247 if (configuredStudioDir != null && !_hasStudioAt(configuredStudioDir)) { 248 studios.add(AndroidStudio(configuredStudioDir, 249 configured: configuredStudioDir)); 250 } 251 252 if (platform.isLinux) { 253 void _checkWellKnownPath(String path) { 254 if (fs.isDirectorySync(path) && !_hasStudioAt(path)) { 255 studios.add(AndroidStudio(path)); 256 } 257 } 258 259 // Add /opt/android-studio and $HOME/android-studio, if they exist. 260 _checkWellKnownPath('/opt/android-studio'); 261 _checkWellKnownPath('$homeDirPath/android-studio'); 262 } 263 return studios; 264 } 265 266 static String extractStudioPlistValueWithMatcher(String plistValue, RegExp keyMatcher) { 267 if (plistValue == null || keyMatcher == null) { 268 return null; 269 } 270 return keyMatcher?.stringMatch(plistValue)?.split('=')?.last?.trim()?.replaceAll('"', ''); 271 } 272 273 void _init() { 274 _isValid = false; 275 _validationMessages.clear(); 276 277 if (configured != null) { 278 _validationMessages.add('android-studio-dir = $configured'); 279 } 280 281 if (!fs.isDirectorySync(directory)) { 282 _validationMessages.add('Android Studio not found at $directory'); 283 return; 284 } 285 286 final String javaPath = platform.isMacOS ? 287 fs.path.join(directory, 'jre', 'jdk', 'Contents', 'Home') : 288 fs.path.join(directory, 'jre'); 289 final String javaExecutable = fs.path.join(javaPath, 'bin', 'java'); 290 if (!processManager.canRun(javaExecutable)) { 291 _validationMessages.add('Unable to find bundled Java version.'); 292 } else { 293 final ProcessResult result = processManager.runSync(<String>[javaExecutable, '-version']); 294 if (result.exitCode == 0) { 295 final List<String> versionLines = result.stderr.split('\n'); 296 final String javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0]; 297 _validationMessages.add('Java version $javaVersion'); 298 _javaPath = javaPath; 299 _isValid = true; 300 } else { 301 _validationMessages.add('Unable to determine bundled Java version.'); 302 } 303 } 304 } 305 306 @override 307 String toString() => 'Android Studio ($version)'; 308} 309