| using System; |
| using System.Collections.Generic; |
| using System.IO; |
| using System.Linq; |
| using System.Text.RegularExpressions; |
| using JetBrains.Annotations; |
| using Microsoft.Win32; |
| using Packages.Rider.Editor.Util; |
| using Unity.CodeEditor; |
| using UnityEngine; |
|
|
| namespace Packages.Rider.Editor |
| { |
| internal interface IDiscovery |
| { |
| CodeEditor.Installation[] PathCallback(); |
| } |
|
|
| internal class Discovery : IDiscovery |
| { |
| public CodeEditor.Installation[] PathCallback() |
| { |
| var res = RiderPathLocator.GetAllRiderPaths() |
| .Select(riderInfo => new CodeEditor.Installation |
| { |
| Path = riderInfo.Path, |
| Name = riderInfo.Presentation |
| }) |
| .ToList(); |
|
|
| var editorPath = RiderScriptEditor.CurrentEditor; |
| if (RiderScriptEditor.IsRiderOrFleetInstallation(editorPath) && |
| !res.Any(a => a.Path == editorPath) && |
| FileSystemUtil.EditorPathExists(editorPath)) |
| { |
| |
| var info = new RiderPathLocator.RiderInfo(editorPath, false); |
| var installation = new CodeEditor.Installation |
| { |
| Path = info.Path, |
| Name = info.Presentation |
| }; |
| res.Add(installation); |
| } |
|
|
| return res.ToArray(); |
| } |
| } |
|
|
| |
| |
| |
| |
| internal static class RiderPathLocator |
| { |
| #if !(UNITY_4_7 || UNITY_5_5) |
| public static RiderInfo[] GetAllRiderPaths() |
| { |
| try |
| { |
| switch (SystemInfo.operatingSystemFamily) |
| { |
| case OperatingSystemFamily.Windows: |
| { |
| return CollectRiderInfosWindows(); |
| } |
|
|
| case OperatingSystemFamily.MacOSX: |
| { |
| return CollectRiderInfosMac(); |
| } |
|
|
| case OperatingSystemFamily.Linux: |
| { |
| return CollectAllRiderPathsLinux(); |
| } |
| } |
| } |
| catch (Exception e) |
| { |
| Debug.LogException(e); |
| } |
|
|
| return Array.Empty<RiderInfo>(); |
| } |
| #endif |
| private static RiderInfo[] CollectAllRiderPathsLinux() |
| { |
| var installInfos = new List<RiderInfo>(); |
| var appsPath = GetAppsRootPathInToolbox(); |
| |
| var riderRootPath = Path.Combine(appsPath, "Rider"); |
| installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "bin", "rider.sh", false) |
| .Select(a => new RiderInfo(a, true)).ToList()); |
| |
| var fleetRootPath = Path.Combine(appsPath, "Fleet"); |
| installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, "bin", "Fleet", false) |
| .Select(a => new RiderInfo(a, true)).ToList()); |
| |
| var home = Environment.GetEnvironmentVariable("HOME"); |
| if (!string.IsNullOrEmpty(home)) |
| { |
| |
| var shortcut = new FileInfo(Path.Combine(home, @".local/share/applications/jetbrains-rider.desktop")); |
|
|
| if (shortcut.Exists) |
| { |
| var lines = File.ReadAllLines(shortcut.FullName); |
| foreach (var line in lines) |
| { |
| if (!line.StartsWith("Exec=\"")) |
| continue; |
| var path = line.Split('"').Where((_, index) => index == 1).SingleOrDefault(); |
| if (string.IsNullOrEmpty(path)) |
| continue; |
|
|
| if (installInfos.Any(a => a.Path == path)) |
| continue; |
| installInfos.Add(new RiderInfo(path, false)); |
| } |
| } |
| } |
|
|
| |
| var snapInstallPath = "/snap/rider/current/bin/rider.sh"; |
| if (new FileInfo(snapInstallPath).Exists) |
| installInfos.Add(new RiderInfo(snapInstallPath, false)); |
| |
| return installInfos.ToArray(); |
| } |
|
|
| private static RiderInfo[] CollectRiderInfosMac() |
| { |
| var installInfos = new List<RiderInfo>(); |
| |
| var folder = new DirectoryInfo("/Applications"); |
| if (folder.Exists) |
| { |
| installInfos.AddRange(folder.GetDirectories("*Rider*.app") |
| .Select(a => new RiderInfo(a.FullName, false)) |
| .ToList()); |
| } |
|
|
| var appsPath = GetAppsRootPathInToolbox(); |
| var riderRootPath = Path.Combine(appsPath, "Rider"); |
| installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "", "Rider*.app", true) |
| .Select(a => new RiderInfo(a, true))); |
| |
| var fleetRootPath = Path.Combine(appsPath, "Fleet"); |
| installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, "", "Fleet*.app", true) |
| .Select(a => new RiderInfo(a, true))); |
|
|
| return installInfos.ToArray(); |
| } |
|
|
| private static RiderInfo[] CollectRiderInfosWindows() |
| { |
| var appsPath = GetAppsRootPathInToolbox(); |
| var installInfos = new List<RiderInfo>(); |
| var riderRootPath = Path.Combine(appsPath, "Rider"); |
| installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "bin", "rider64.exe", false).ToList() |
| .Select(a => new RiderInfo(a, true)).ToList()); |
| |
| var fleetRootPath = Path.Combine(appsPath, "Fleet"); |
| installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, string.Empty, "Fleet.exe", false).ToList() |
| .Select(a => new RiderInfo(a, true)).ToList()); |
| |
| var installPaths = new List<string>(); |
| const string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"; |
| CollectPathsFromRegistry(registryKey, installPaths); |
| const string wowRegistryKey = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"; |
| CollectPathsFromRegistry(wowRegistryKey, installPaths); |
| |
| installInfos.AddRange(installPaths.Select(a => new RiderInfo(a, false)).ToList()); |
|
|
| return installInfos.ToArray(); |
| } |
|
|
| private static string GetAppsRootPathInToolbox() |
| { |
| string localAppData = string.Empty; |
| switch (SystemInfo.operatingSystemFamily) |
| { |
| case OperatingSystemFamily.Windows: |
| { |
| localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); |
| break; |
| } |
|
|
| case OperatingSystemFamily.MacOSX: |
| { |
| var home = Environment.GetEnvironmentVariable("HOME"); |
| if (!string.IsNullOrEmpty(home)) |
| { |
| localAppData = Path.Combine(home, @"Library/Application Support"); |
| } |
| break; |
| } |
|
|
| case OperatingSystemFamily.Linux: |
| { |
| var home = Environment.GetEnvironmentVariable("HOME"); |
| if (!string.IsNullOrEmpty(home)) |
| { |
| localAppData = Path.Combine(home, @".local/share"); |
| } |
| break; |
| } |
| default: |
| throw new Exception("Unknown OS"); |
| } |
| |
| var toolboxPath = Path.Combine(localAppData, @"JetBrains/Toolbox"); |
| var settingsJson = Path.Combine(toolboxPath, ".settings.json"); |
|
|
| if (File.Exists(settingsJson)) |
| { |
| var path = SettingsJson.GetInstallLocationFromJson(File.ReadAllText(settingsJson)); |
| if (!string.IsNullOrEmpty(path)) |
| toolboxPath = path; |
| } |
|
|
| return Path.Combine(toolboxPath, "apps"); |
| } |
|
|
| internal static ProductInfo GetBuildVersion(string path) |
| { |
| var buildTxtFileInfo = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt())); |
| var dir = buildTxtFileInfo.DirectoryName; |
| if (!Directory.Exists(dir)) |
| return null; |
| var buildVersionFile = new FileInfo(Path.Combine(dir, "product-info.json")); |
| if (!buildVersionFile.Exists) |
| return null; |
| var json = File.ReadAllText(buildVersionFile.FullName); |
| return ProductInfo.GetProductInfo(json); |
| } |
| |
| internal static Version GetBuildNumber(string path) |
| { |
| var buildTxtFileInfo = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt())); |
| return GetBuildNumberWithBuildTxt(buildTxtFileInfo) ?? GetBuildNumberFromPath(path); |
| } |
| |
| private static Version GetBuildNumberWithBuildTxt(FileInfo file) |
| { |
| if (!file.Exists) |
| return null; |
| var text = File.ReadAllText(file.FullName); |
| var index = text.IndexOf("-", StringComparison.Ordinal) + 1; |
| if (index <= 0) |
| return null; |
| |
| var versionText = text.Substring(index); |
| return Version.TryParse(versionText, out var v) ? v : null; |
| } |
|
|
| [CanBeNull] |
| private static Version GetBuildNumberFromPath(string input) |
| { |
| if (string.IsNullOrEmpty(input)) |
| return null; |
|
|
| var match = Regex.Match(input, @"(?<major>\d+)\.(?<minor>\d+)\.(?<build>\d+)"); |
| var groups = match.Groups; |
| Version version = null; |
| if (match.Success) |
| { |
| version = Version.Parse($"{groups["major"].Value}.{groups["minor"].Value}.{groups["build"].Value}"); |
| } |
|
|
| return version; |
| } |
|
|
| public static bool GetIsToolbox(string path) |
| { |
| return Path.GetFullPath(path).StartsWith(Path.GetFullPath(GetAppsRootPathInToolbox())); |
| } |
|
|
| private static string GetRelativePathToBuildTxt() |
| { |
| switch (SystemInfo.operatingSystemFamily) |
| { |
| case OperatingSystemFamily.Windows: |
| case OperatingSystemFamily.Linux: |
| return "../../build.txt"; |
| case OperatingSystemFamily.MacOSX: |
| return "Contents/Resources/build.txt"; |
| } |
| throw new Exception("Unknown OS"); |
| } |
| private static void CollectPathsFromRegistry(string registryKey, List<string> installPaths) |
| { |
| using (var key = Registry.CurrentUser.OpenSubKey(registryKey)) |
| { |
| CollectPathsFromRegistry(installPaths, key); |
| } |
| using (var key = Registry.LocalMachine.OpenSubKey(registryKey)) |
| { |
| CollectPathsFromRegistry(installPaths, key); |
| } |
| } |
|
|
| private static void CollectPathsFromRegistry(List<string> installPaths, RegistryKey key) |
| { |
| if (key == null) return; |
| foreach (var subkeyName in key.GetSubKeyNames()) |
| { |
| using (var subkey = key.OpenSubKey(subkeyName)) |
| { |
| var folderObject = subkey?.GetValue("InstallLocation"); |
| if (folderObject == null) continue; |
| var folder = folderObject.ToString(); |
| if (folder.Length == 0) continue; |
| var displayName = subkey.GetValue("DisplayName"); |
| if (displayName == null) continue; |
| if (!displayName.ToString().Contains("Rider")) continue; |
| try |
| { |
| var possiblePath = Path.Combine(folder, @"bin\rider64.exe"); |
| if (File.Exists(possiblePath)) |
| installPaths.Add(possiblePath); |
| } |
| catch (ArgumentException) { } |
| } |
| } |
| } |
|
|
| private static string[] CollectPathsFromToolbox(string productRootPathInToolbox, string dirName, string searchPattern, |
| bool isMac) |
| { |
| if (!Directory.Exists(productRootPathInToolbox)) |
| return new string[0]; |
|
|
| var channelDirs = Directory.GetDirectories(productRootPathInToolbox); |
| var paths = channelDirs.SelectMany(channelDir => |
| { |
| try |
| { |
| |
| var historyFile = Path.Combine(channelDir, ".history.json"); |
| if (File.Exists(historyFile)) |
| { |
| var json = File.ReadAllText(historyFile); |
| var build = ToolboxHistory.GetLatestBuildFromJson(json); |
| if (build != null) |
| { |
| var buildDir = Path.Combine(channelDir, build); |
| var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir); |
| if (executablePaths.Any()) |
| return executablePaths; |
| } |
| } |
|
|
| var channelFile = Path.Combine(channelDir, ".channel.settings.json"); |
| if (File.Exists(channelFile)) |
| { |
| var json = File.ReadAllText(channelFile).Replace("active-application", "active_application"); |
| var build = ToolboxInstallData.GetLatestBuildFromJson(json); |
| if (build != null) |
| { |
| var buildDir = Path.Combine(channelDir, build); |
| var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir); |
| if (executablePaths.Any()) |
| return executablePaths; |
| } |
| } |
|
|
| |
| return Directory.GetDirectories(channelDir) |
| .SelectMany(buildDir => GetExecutablePaths(dirName, searchPattern, isMac, buildDir)); |
| } |
| catch (Exception e) |
| { |
| |
| Logger.Warn($"Failed to get path from {channelDir}", e); |
| } |
|
|
| return new string[0]; |
| }) |
| .Where(c => !string.IsNullOrEmpty(c)) |
| .ToArray(); |
| return paths; |
| } |
|
|
| private static string[] GetExecutablePaths(string dirName, string searchPattern, bool isMac, string buildDir) |
| { |
| var folder = new DirectoryInfo(Path.Combine(buildDir, dirName)); |
| if (!folder.Exists) |
| return new string[0]; |
|
|
| if (!isMac) |
| return new[] {Path.Combine(folder.FullName, searchPattern)}.Where(File.Exists).ToArray(); |
| return folder.GetDirectories(searchPattern).Select(f => f.FullName) |
| .Where(Directory.Exists).ToArray(); |
| } |
|
|
| |
| |
| #pragma warning disable 0649 |
| |
| [Serializable] |
| class SettingsJson |
| { |
| |
| public string install_location; |
| |
| [CanBeNull] |
| public static string GetInstallLocationFromJson(string json) |
| { |
| try |
| { |
| #if UNITY_4_7 || UNITY_5_5 |
| return JsonConvert.DeserializeObject<SettingsJson>(json).install_location; |
| #else |
| return JsonUtility.FromJson<SettingsJson>(json).install_location; |
| #endif |
| } |
| catch (Exception) |
| { |
| Logger.Warn($"Failed to get install_location from json {json}"); |
| } |
|
|
| return null; |
| } |
| } |
|
|
| [Serializable] |
| class ToolboxHistory |
| { |
| public List<ItemNode> history; |
|
|
| [CanBeNull] |
| public static string GetLatestBuildFromJson(string json) |
| { |
| try |
| { |
| #if UNITY_4_7 || UNITY_5_5 |
| return JsonConvert.DeserializeObject<ToolboxHistory>(json).history.LastOrDefault()?.item.build; |
| #else |
| return JsonUtility.FromJson<ToolboxHistory>(json).history.LastOrDefault()?.item.build; |
| #endif |
| } |
| catch (Exception) |
| { |
| Logger.Warn($"Failed to get latest build from json {json}"); |
| } |
|
|
| return null; |
| } |
| } |
|
|
| [Serializable] |
| class ItemNode |
| { |
| public BuildNode item; |
| } |
|
|
| [Serializable] |
| class BuildNode |
| { |
| public string build; |
| } |
|
|
| [Serializable] |
| internal class ProductInfo |
| { |
| public string version; |
| public string versionSuffix; |
|
|
| [CanBeNull] |
| internal static ProductInfo GetProductInfo(string json) |
| { |
| try |
| { |
| var productInfo = JsonUtility.FromJson<ProductInfo>(json); |
| return productInfo; |
| } |
| catch (Exception) |
| { |
| Logger.Warn($"Failed to get version from json {json}"); |
| } |
|
|
| return null; |
| } |
| } |
|
|
| |
| [Serializable] |
| class ToolboxInstallData |
| { |
| |
| public ActiveApplication active_application; |
|
|
| [CanBeNull] |
| public static string GetLatestBuildFromJson(string json) |
| { |
| try |
| { |
| #if UNITY_4_7 || UNITY_5_5 |
| var toolbox = JsonConvert.DeserializeObject<ToolboxInstallData>(json); |
| #else |
| var toolbox = JsonUtility.FromJson<ToolboxInstallData>(json); |
| #endif |
| var builds = toolbox.active_application.builds; |
| if (builds != null && builds.Any()) |
| return builds.First(); |
| } |
| catch (Exception) |
| { |
| Logger.Warn($"Failed to get latest build from json {json}"); |
| } |
|
|
| return null; |
| } |
| } |
|
|
| [Serializable] |
| class ActiveApplication |
| { |
| |
| public List<string> builds; |
| } |
|
|
| #pragma warning restore 0649 |
|
|
| internal struct RiderInfo |
| { |
| public bool IsToolbox; |
| public string Presentation; |
| public Version BuildNumber; |
| public ProductInfo ProductInfo; |
| public string Path; |
|
|
| public RiderInfo(string path, bool isToolbox) |
| { |
| if (path == RiderScriptEditor.CurrentEditor) |
| { |
| RiderScriptEditorData.instance.Init(); |
| BuildNumber = RiderScriptEditorData.instance.editorBuildNumber.ToVersion(); |
| ProductInfo = RiderScriptEditorData.instance.productInfo; |
| } |
| else |
| { |
| BuildNumber = GetBuildNumber(path); |
| ProductInfo = GetBuildVersion(path); |
| } |
|
|
| var fileInfo = new FileInfo(path); |
| var productName = RiderScriptEditor.GetProductNameForPresentation(fileInfo); |
| Path = fileInfo.FullName; |
| var presentation = $"{productName} {BuildNumber}"; |
|
|
| if (ProductInfo != null && !string.IsNullOrEmpty(ProductInfo.version)) |
| { |
| var suffix = string.IsNullOrEmpty(ProductInfo.versionSuffix) ? "" : $" {ProductInfo.versionSuffix}"; |
| presentation = $"{productName} {ProductInfo.version}{suffix}"; |
| } |
|
|
| if (isToolbox) |
| presentation += " (JetBrains Toolbox)"; |
|
|
| Presentation = presentation; |
| IsToolbox = isToolbox; |
| } |
| } |
|
|
| private static class Logger |
| { |
| internal static void Warn(string message, Exception e = null) |
| { |
| #if RIDER_EDITOR_PLUGIN // can't be used in com.unity.ide.rider |
| Log.GetLog(typeof(RiderPathLocator).Name).Warn(message); |
| if (e != null) |
| Log.GetLog(typeof(RiderPathLocator).Name).Warn(e); |
| #else |
| Debug.LogError(message); |
| if (e != null) |
| Debug.LogException(e); |
| #endif |
| } |
| } |
| } |
| } |