// Copyright 2014 The Flutter 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 'package:meta/meta.dart'; import 'package:process/process.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/platform.dart'; import '../base/user_messages.dart' hide userMessages; import '../base/version.dart'; import '../convert.dart'; import '../doctor_validator.dart'; import '../ios/plist_parser.dart'; import 'intellij.dart'; const String _ultimateEditionTitle = 'IntelliJ IDEA Ultimate Edition'; const String _ultimateEditionId = 'IntelliJIdea'; const String _communityEditionTitle = 'IntelliJ IDEA Community Edition'; const String _communityEditionId = 'IdeaIC'; /// A doctor validator for both Intellij and Android Studio. abstract class IntelliJValidator extends DoctorValidator { IntelliJValidator(super.title, this.installPath, { required FileSystem fileSystem, required UserMessages userMessages, }) : _fileSystem = fileSystem, _userMessages = userMessages; final String installPath; final FileSystem _fileSystem; final UserMessages _userMessages; String get version; String? get pluginsPath; static const Map<String, String> _idToTitle = <String, String>{ _ultimateEditionId: _ultimateEditionTitle, _communityEditionId: _communityEditionTitle, }; static final Version kMinIdeaVersion = Version(2017, 1, 0); /// Create a [DoctorValidator] for each installation of Intellij. /// /// On platforms other than macOS, Linux, and Windows this returns an /// empty list. static Iterable<DoctorValidator> installedValidators({ required FileSystem fileSystem, required Platform platform, required UserMessages userMessages, required PlistParser plistParser, required ProcessManager processManager, }) { final FileSystemUtils fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform); if (platform.isWindows) { return IntelliJValidatorOnWindows.installed( fileSystem: fileSystem, fileSystemUtils: fileSystemUtils, platform: platform, userMessages: userMessages, ); } if (platform.isLinux) { return IntelliJValidatorOnLinux.installed( fileSystem: fileSystem, fileSystemUtils: fileSystemUtils, userMessages: userMessages, ); } if (platform.isMacOS) { return IntelliJValidatorOnMac.installed( fileSystem: fileSystem, fileSystemUtils: fileSystemUtils, userMessages: userMessages, plistParser: plistParser, processManager: processManager, ); } return <DoctorValidator>[]; } @override Future<ValidationResult> validate() async { final List<ValidationMessage> messages = <ValidationMessage>[]; if (pluginsPath == null) { messages.add(const ValidationMessage.error('Invalid IntelliJ version number.')); } else { messages.add(ValidationMessage(_userMessages.intellijLocation(installPath))); final IntelliJPlugins plugins = IntelliJPlugins(pluginsPath!, fileSystem: _fileSystem); plugins.validatePackage( messages, <String>['flutter-intellij', 'flutter-intellij.jar'], 'Flutter', IntelliJPlugins.kIntellijFlutterPluginUrl, minVersion: IntelliJPlugins.kMinFlutterPluginVersion, ); plugins.validatePackage( messages, <String>['Dart'], 'Dart', IntelliJPlugins.kIntellijDartPluginUrl, ); if (_hasIssues(messages)) { messages.add(ValidationMessage(_userMessages.intellijPluginInfo)); } _validateIntelliJVersion(messages, kMinIdeaVersion); } return ValidationResult( _hasIssues(messages) ? ValidationType.partial : ValidationType.installed, messages, statusInfo: _userMessages.intellijStatusInfo(version), ); } 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 = Version.parse(version); if (installedVersion == null) { return; } if (installedVersion < minVersion) { messages.add(ValidationMessage.error(_userMessages.intellijMinimumVersion(minVersion.toString()))); } } } /// A windows specific implementation of the intellij validator. class IntelliJValidatorOnWindows extends IntelliJValidator { IntelliJValidatorOnWindows(String title, this.version, String installPath, this.pluginsPath, { required FileSystem fileSystem, required UserMessages userMessages, }) : super(title, installPath, fileSystem: fileSystem, userMessages: userMessages); @override final String version; @override final String pluginsPath; static Iterable<DoctorValidator> installed({ required FileSystem fileSystem, required FileSystemUtils fileSystemUtils, required Platform platform, required UserMessages userMessages, }) { final List<DoctorValidator> validators = <DoctorValidator>[]; if (fileSystemUtils.homeDirPath == null) { return validators; } void addValidator(String title, String version, String installPath, String pluginsPath) { final IntelliJValidatorOnWindows validator = IntelliJValidatorOnWindows( title, version, installPath, pluginsPath, fileSystem: fileSystem, userMessages: userMessages, ); for (int index = 0; index < validators.length; index += 1) { final DoctorValidator other = validators[index]; if (other is IntelliJValidatorOnWindows && validator.installPath == other.installPath) { if (validator.version.compareTo(other.version) > 0) { validators[index] = validator; } return; } } validators.add(validator); } // before IntelliJ 2019 final Directory homeDir = fileSystem.directory(fileSystemUtils.homeDirPath); for (final Directory dir in homeDir.listSync().whereType<Directory>()) { final String name = fileSystem.path.basename(dir.path); IntelliJValidator._idToTitle.forEach((String id, String title) { if (name.startsWith('.$id')) { final String version = name.substring(id.length + 1); String? installPath; try { installPath = fileSystem.file(fileSystem.path.join(dir.path, 'system', '.home')).readAsStringSync(); } on FileSystemException { // ignored } if (installPath != null && fileSystem.isDirectorySync(installPath)) { final String pluginsPath = fileSystem.path.join(dir.path, 'config', 'plugins'); addValidator(title, version, installPath, pluginsPath); } } }); } // after IntelliJ 2020 if (!platform.environment.containsKey('LOCALAPPDATA')) { return validators; } final Directory cacheDir = fileSystem.directory(fileSystem.path.join(platform.environment['LOCALAPPDATA']!, 'JetBrains')); if (!cacheDir.existsSync()) { return validators; } for (final Directory dir in cacheDir.listSync().whereType<Directory>()) { final String name = fileSystem.path.basename(dir.path); IntelliJValidator._idToTitle.forEach((String id, String title) { if (name.startsWith(id)) { final String version = name.substring(id.length); String? installPath; try { installPath = fileSystem.file(fileSystem.path.join(dir.path, '.home')).readAsStringSync(); } on FileSystemException { // ignored } if (installPath != null && fileSystem.isDirectorySync(installPath)) { String pluginsPath; if (fileSystem.isDirectorySync('$installPath.plugins')) { // IntelliJ 2020.3 pluginsPath = '$installPath.plugins'; addValidator(title, version, installPath, pluginsPath); } else if (platform.environment.containsKey('APPDATA')) { final String pluginsPathInAppData = fileSystem.path.join( platform.environment['APPDATA']!, 'JetBrains', name, 'plugins'); if (fileSystem.isDirectorySync(pluginsPathInAppData)) { // IntelliJ 2020.1 ~ 2020.2 pluginsPath = pluginsPathInAppData; addValidator(title, version, installPath, pluginsPath); } } } } }); } return validators; } } /// A linux specific implementation of the intellij validator. class IntelliJValidatorOnLinux extends IntelliJValidator { IntelliJValidatorOnLinux(String title, this.version, String installPath, this.pluginsPath, { required FileSystem fileSystem, required UserMessages userMessages, }) : super(title, installPath, fileSystem: fileSystem, userMessages: userMessages); @override final String version; @override final String pluginsPath; static Iterable<DoctorValidator> installed({ required FileSystem fileSystem, required FileSystemUtils fileSystemUtils, required UserMessages userMessages, }) { final List<DoctorValidator> validators = <DoctorValidator>[]; final String? homeDirPath = fileSystemUtils.homeDirPath; if (homeDirPath == null) { return validators; } void addValidator(String title, String version, String installPath, String pluginsPath) { final IntelliJValidatorOnLinux validator = IntelliJValidatorOnLinux( title, version, installPath, pluginsPath, fileSystem: fileSystem, userMessages: userMessages, ); for (int index = 0; index < validators.length; index += 1) { final DoctorValidator other = validators[index]; if (other is IntelliJValidatorOnLinux && validator.installPath == other.installPath) { if (validator.version.compareTo(other.version) > 0) { validators[index] = validator; } return; } } validators.add(validator); } // before IntelliJ 2019 final Directory homeDir = fileSystem.directory(homeDirPath); for (final Directory dir in homeDir.listSync().whereType<Directory>()) { final String name = fileSystem.path.basename(dir.path); IntelliJValidator._idToTitle.forEach((String id, String title) { if (name.startsWith('.$id')) { final String version = name.substring(id.length + 1); String? installPath; try { installPath = fileSystem.file(fileSystem.path.join(dir.path, 'system', '.home')).readAsStringSync(); } on FileSystemException { // ignored } if (installPath != null && fileSystem.isDirectorySync(installPath)) { final String pluginsPath = fileSystem.path.join(dir.path, 'config', 'plugins'); addValidator(title, version, installPath, pluginsPath); } } }); } // after IntelliJ 2020 ~ final Directory cacheDir = fileSystem.directory(fileSystem.path.join(homeDirPath, '.cache', 'JetBrains')); if (!cacheDir.existsSync()) { return validators; } for (final Directory dir in cacheDir.listSync().whereType<Directory>()) { final String name = fileSystem.path.basename(dir.path); IntelliJValidator._idToTitle.forEach((String id, String title) { if (name.startsWith(id)) { final String version = name.substring(id.length); String? installPath; try { installPath = fileSystem.file(fileSystem.path.join(dir.path, '.home')).readAsStringSync(); } on FileSystemException { // ignored } if (installPath != null && fileSystem.isDirectorySync(installPath)) { final String pluginsPathInUserHomeDir = fileSystem.path.join( homeDirPath, '.local', 'share', 'JetBrains', name); if (installPath.contains(fileSystem.path.join('JetBrains','Toolbox','apps'))) { // via JetBrains ToolBox app final String pluginsPathInInstallDir = '$installPath.plugins'; if (fileSystem.isDirectorySync(pluginsPathInUserHomeDir)) { // after 2020.2.x final String pluginsPath = pluginsPathInUserHomeDir; addValidator(title, version, installPath, pluginsPath); } else if (fileSystem.isDirectorySync(pluginsPathInInstallDir)) { // only 2020.1.X final String pluginsPath = pluginsPathInInstallDir; addValidator(title, version, installPath, pluginsPath); } } else { // via tar.gz final String pluginsPath = pluginsPathInUserHomeDir; addValidator(title, version, installPath, pluginsPath); } } } }); } return validators; } } /// A macOS specific implementation of the intellij validator. class IntelliJValidatorOnMac extends IntelliJValidator { IntelliJValidatorOnMac(String title, this.id, String installPath, { required FileSystem fileSystem, required UserMessages userMessages, required PlistParser plistParser, required String? homeDirPath, }) : _plistParser = plistParser, _homeDirPath = homeDirPath, super(title, installPath, fileSystem: fileSystem, userMessages: userMessages); final String id; final PlistParser _plistParser; final String? _homeDirPath; static const Map<String, String> _dirNameToId = <String, String>{ 'IntelliJ IDEA.app': _ultimateEditionId, 'IntelliJ IDEA Ultimate.app': _ultimateEditionId, 'IntelliJ IDEA CE.app': _communityEditionId, }; static Iterable<DoctorValidator> installed({ required FileSystem fileSystem, required FileSystemUtils fileSystemUtils, required UserMessages userMessages, required PlistParser plistParser, required ProcessManager processManager, }) { final List<DoctorValidator> validators = <DoctorValidator>[]; final String? homeDirPath = fileSystemUtils.homeDirPath; final List<String> installPaths = <String>[ '/Applications', if (homeDirPath != null) fileSystem.path.join(homeDirPath, 'Applications'), ]; void checkForIntelliJ(Directory dir) { final String name = fileSystem.path.basename(dir.path); _dirNameToId.forEach((String dirName, String id) { if (name == dirName) { assert(IntelliJValidator._idToTitle.containsKey(id)); final String title = IntelliJValidator._idToTitle[id]!; validators.add(IntelliJValidatorOnMac( title, id, dir.path, fileSystem: fileSystem, userMessages: userMessages, plistParser: plistParser, homeDirPath: homeDirPath, )); } }); } try { final Iterable<Directory> installDirs = installPaths .map(fileSystem.directory) .map<List<FileSystemEntity>>((Directory dir) => dir.existsSync() ? dir.listSync() : <FileSystemEntity>[]) .expand<FileSystemEntity>((List<FileSystemEntity> mappedDirs) => mappedDirs) .whereType<Directory>(); for (final Directory dir in installDirs) { checkForIntelliJ(dir); if (!dir.path.endsWith('.app')) { for (final FileSystemEntity subdirectory in dir.listSync()) { if (subdirectory is Directory) { checkForIntelliJ(subdirectory); } } } } // Query Spotlight for unexpected installation locations. String ceSpotlightResult = ''; String ultimateSpotlightResult = ''; try { final ProcessResult ceQueryResult = processManager.runSync(<String>[ 'mdfind', 'kMDItemCFBundleIdentifier="com.jetbrains.intellij.ce"', ]); ceSpotlightResult = ceQueryResult.stdout as String; final ProcessResult ultimateQueryResult = processManager.runSync(<String>[ 'mdfind', 'kMDItemCFBundleIdentifier="com.jetbrains.intellij*"', ]); ultimateSpotlightResult = ultimateQueryResult.stdout as String; } on ProcessException { // The Spotlight query is a nice-to-have, continue checking known installation locations. } for (final String installPath in LineSplitter.split(ceSpotlightResult)) { if (!validators.whereType<IntelliJValidatorOnMac>().any((IntelliJValidatorOnMac e) => e.installPath == installPath)) { validators.add(IntelliJValidatorOnMac( _communityEditionTitle, _communityEditionId, installPath, fileSystem: fileSystem, userMessages: userMessages, plistParser: plistParser, homeDirPath: homeDirPath, )); } } for (final String installPath in LineSplitter.split(ultimateSpotlightResult)) { if (!validators.whereType<IntelliJValidatorOnMac>().any((IntelliJValidatorOnMac e) => e.installPath == installPath)) { validators.add(IntelliJValidatorOnMac( _ultimateEditionTitle, _ultimateEditionId, installPath, fileSystem: fileSystem, userMessages: userMessages, plistParser: plistParser, homeDirPath: homeDirPath, )); } } } on FileSystemException catch (e) { validators.add(ValidatorWithResult( userMessages.intellijMacUnknownResult, ValidationResult(ValidationType.missing, <ValidationMessage>[ ValidationMessage.error(e.message), ]), )); } return validators; } @visibleForTesting String get plistFile { _plistFile ??= _fileSystem.path.join(installPath, 'Contents', 'Info.plist'); return _plistFile!; } String? _plistFile; @override String get version { return _version ??= _plistParser.getStringValueFromFile( plistFile, PlistParser.kCFBundleShortVersionStringKey, ) ?? 'unknown'; } String? _version; @override String? get pluginsPath { if (_pluginsPath != null) { return _pluginsPath!; } final String? altLocation = _plistParser .getStringValueFromFile(plistFile, 'JetBrainsToolboxApp'); if (altLocation != null) { _pluginsPath = '$altLocation.plugins'; return _pluginsPath!; } final List<String> split = version.split('.'); if (split.length < 2) { return null; } final String major = split[0]; final String minor = split[1]; final String? homeDirPath = _homeDirPath; if (homeDirPath != null) { String pluginsPath = _fileSystem.path.join( homeDirPath, 'Library', 'Application Support', 'JetBrains', '$id$major.$minor', 'plugins', ); // Fallback to legacy location from < 2020. if (!_fileSystem.isDirectorySync(pluginsPath)) { pluginsPath = _fileSystem.path.join( homeDirPath, 'Library', 'Application Support', '$id$major.$minor', ); } _pluginsPath = pluginsPath; } return _pluginsPath; } String? _pluginsPath; }