584 lines
19 KiB
C#
584 lines
19 KiB
C#
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))
|
|
{
|
|
// External editor manually set from custom location
|
|
var info = new RiderPathLocator.RiderInfo(editorPath, false);
|
|
var installation = new CodeEditor.Installation
|
|
{
|
|
Path = info.Path,
|
|
Name = info.Presentation
|
|
};
|
|
res.Add(installation);
|
|
}
|
|
|
|
return res.ToArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This code is a modified version of the JetBrains resharper-unity plugin listed here:
|
|
/// https://github.com/JetBrains/resharper-unity/blob/master/unity/JetBrains.Rider.Unity.Editor/EditorPlugin/RiderPathLocator.cs
|
|
/// </summary>
|
|
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))
|
|
{
|
|
//$Home/.local/share/applications/jetbrains-rider.desktop
|
|
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)) // avoid adding similar build as from toolbox
|
|
continue;
|
|
installInfos.Add(new RiderInfo(path, false));
|
|
}
|
|
}
|
|
}
|
|
|
|
// snap install
|
|
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>();
|
|
// "/Applications/*Rider*.app"
|
|
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; // RD-191.7141.355
|
|
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 // possible "illegal characters in path"
|
|
{
|
|
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
|
|
{
|
|
// use history.json - last entry stands for the active build https://jetbrains.slack.com/archives/C07KNP99D/p1547807024066500?thread_ts=1547731708.057700&cid=C07KNP99D
|
|
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;
|
|
}
|
|
}
|
|
|
|
// changes in toolbox json files format may brake the logic above, so return all found installations
|
|
return Directory.GetDirectories(channelDir)
|
|
.SelectMany(buildDir => GetExecutablePaths(dirName, searchPattern, isMac, buildDir));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// do not write to Debug.Log, just log it.
|
|
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();
|
|
}
|
|
|
|
// Disable the "field is never assigned" compiler warning. We never assign it, but Unity does.
|
|
// Note that Unity disable this warning in the generated C# projects
|
|
#pragma warning disable 0649
|
|
|
|
[Serializable]
|
|
class SettingsJson
|
|
{
|
|
// ReSharper disable once InconsistentNaming
|
|
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;
|
|
}
|
|
}
|
|
|
|
// ReSharper disable once ClassNeverInstantiated.Global
|
|
[Serializable]
|
|
class ToolboxInstallData
|
|
{
|
|
// ReSharper disable once InconsistentNaming
|
|
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
|
|
{
|
|
// ReSharper disable once InconsistentNaming
|
|
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; // normalize separators
|
|
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
|
|
}
|
|
}
|
|
}
|
|
} |