// Beef port of JetBrains "Rider Source Code Access": // https://github.com/JetBrains/RiderSourceCodeAccess/tree/main/Source/RiderSourceCodeAccess/Private/RiderPathLocator // // Copyright Epic Games, Inc. All Rights Reserved. // Licensed under the Apache License, Version 2.0 - see LICENSE-RiderPathLocator.txt // // Changes from the original: rewritten in Beef against corlib instead of the // Unreal Engine types (FString/TArray/TOptional/FPaths/FRegexPattern/FJsonSerializer/...). // The "plugins/rider-cpp" requirement, the ESupportUproject state and the // RiderLocations.txt resource-file lookup were removed because they only make sense for Unreal projects. // // This file holds the platform independent part, the platform specific parts live in the extensions (e.g. RiderPathLocatorWindows.bf). using System; using System.Collections; using System.IO; using Beefy.utils; namespace GlitchyEditor.CodeEditors; /// A dot-separated version number, e.g. "2024.1.2" or the build number "241.15989.155". struct RiderVersion : IHashable { public const int32 InvalidVersion = -1; private const int32 MaxParts = 4; private int32[MaxParts] _parts; private int32 _count; public bool IsInitialized => _count != 0; public int32 Major => _count >= 1 ? _parts[0] : InvalidVersion; public int32 Minor => _count >= 2 ? _parts[1] : InvalidVersion; public int32 Patch => _count >= 3 ? _parts[2] : InvalidVersion; public static RiderVersion Parse(StringView versionString) { RiderVersion version = .(); for (StringView part in versionString.Split('.')) { if (version._count >= MaxParts) break; // Note: the original uses FCString::Atoi, which yields 0 for garbage instead of failing. int32 value = 0; if (int32.Parse(part) case .Ok(let parsedValue)) value = parsedValue; version._parts[version._count] = value; version._count++; } return version; } /// Compares two versions element by element. /// @returns A negative value if lhs is older than rhs, a positive value if it is newer; otherwise 0. public static int Compare(Self lhs, Self rhs) { int32 sharedCount = Math.Min(lhs._count, rhs._count); for (int32 i < sharedCount) { if (lhs._parts[i] != rhs._parts[i]) return lhs._parts[i] < rhs._parts[i] ? -1 : 1; } // Equal prefix: the shorter version is the older one, so that "5.0" is older than "5.0.1". return lhs._count <=> rhs._count; } public static bool operator<(Self lhs, Self rhs) => Compare(lhs, rhs) < 0; public static bool operator==(Self lhs, Self rhs) => Compare(lhs, rhs) == 0; public static bool operator!=(Self lhs, Self rhs) => Compare(lhs, rhs) != 0; public int GetHashCode() { int hash = 0; for (int32 i < _count) hash = (hash * 31) + (int)_parts[i]; return hash; } public override void ToString(String strBuffer) { if (_count == 0) { strBuffer.Append(""); return; } for (int32 i < _count) { if (i != 0) strBuffer.Append('.'); _parts[i].ToString(strBuffer); } } } /// How a Rider installation was set up. enum RiderInstallType { /// Installed by the regular Rider installer. Installed, /// Installed through the JetBrains Toolbox. Toolbox, /// Provided by the user. Custom } /// A single Rider installation found on this machine. class RiderInstallInfo { /// The full path of the Rider executable (rider64.exe). public String Path ~ delete _; public RiderVersion Version; public RiderInstallType InstallType; public String Name ~ delete _; public this(StringView path, RiderVersion version, RiderInstallType installType, StringView name) { Path = new String(path); Version = version; InstallType = installType; Name = new String(name); } } /// Locates the JetBrains Rider installations on this machine. static class RiderPathLocator { /// Maximum directory depth we descend into while looking for the Rider executable. /// The original search is unbounded, but it searches locations like "%LOCALAPPDATA%/Programs". protected const int MaxSearchDepth = 8; /// Maximum number of directories visited by a single recursive search. protected const int MaxSearchedDirectories = 4096; /// Creates the installation info for the given Rider executable. /// @param riderExePath The path of a rider64.exe. /// @returns The installation info; or .Err if the given path is not a usable Rider installation. public static extern Result GetInstallInfoFromRiderPath(StringView riderExePath, RiderInstallType installType); /// Collects every Rider installation that can be found on this machine. /// The entries are deduplicated by path and sorted, newest version first. /// @param outInstalls Receives the installations. The caller takes ownership of the entries. public static extern void CollectAllPaths(List outInstalls); /// Gets the file name of the Rider executable on this platform (e.g. "rider64.exe"). protected static extern StringView GetRiderExecutableName(); /// Gets the default location the Toolbox V2 installs IDEs into. protected static extern Result GetDefaultIdeInstallLocationForToolboxV2(String outPath); /// Moves the located installations into outInstalls, dropping the ones that were found more /// than once, and sorts them so that the newest installation comes first. /// @param found The located installations. The entries are moved into outInstalls or deleted. /// @param outInstalls Receives the installations. The caller takes ownership of the entries. protected static void DeduplicateAndSort(List found, List outInstalls) { // The same installation usually shows up through more than one source. for (int i < found.Count) { RiderInstallInfo install = found[i]; bool isDuplicate = false; for (RiderInstallInfo existing in outInstalls) { if (IsSamePath(existing.Path, install.Path)) { isDuplicate = true; break; } } if (isDuplicate) continue; outInstalls.Add(install); // Ownership moved to outInstalls, make sure the caller's cleanup doesn't delete it. found[i] = null; } // Newest first, so that the caller can simply take the first entry. outInstalls.Sort(scope (lhs, rhs) => RiderVersion.Compare(rhs.Version, lhs.Version)); } /// Gets the installation directory of Rider from the path of its executable. /// The executable is expected to live in a "bin" directory, i.e. "/bin/rider64.exe". protected static Result GetRiderDirectory(StringView riderExePath, String outDirectory) { // The original matches the regex "(.*)(?:\\|/)bin" against the executable path, // which is the same as taking the grandparent directory of a file inside "bin". String binDirectory = scope .(); Try!(Path.GetDirectoryPath(riderExePath, binDirectory)); String binDirectoryName = scope .(); Path.GetFileName(binDirectory, binDirectoryName); if (StringView.Compare(binDirectoryName, "bin", true) != 0) return .Err; return Path.GetDirectoryPath(binDirectory, outDirectory); } /// Compares two paths, ignoring case and the difference between the directory separators. protected static bool IsSamePath(StringView left, StringView right) { if (left.Length != right.Length) return false; for (int i < left.Length) { char8 leftChar = left[i]; char8 rightChar = right[i]; if (Path.IsDirectorySeparatorChar(leftChar) && Path.IsDirectorySeparatorChar(rightChar)) continue; if (leftChar.ToLower != rightChar.ToLower) return false; } return true; } /// Collects the Rider installations managed by the JetBrains Toolbox at the given location. protected static void CollectPathsFromToolbox(StringView toolboxPath, List outInstalls) { if (toolboxPath.IsEmpty || !Directory.Exists(toolboxPath)) return; String customInstallLocation = scope .(); ExtractPathFromSettingsJson(toolboxPath, customInstallLocation); if (!customInstallLocation.IsEmpty) { // Toolbox V1 puts the IDEs into an "apps" subdirectory of the install location. String appsPath = scope .(); Path.Combine(appsPath, customInstallLocation, "apps"); int countBefore = outInstalls.Count; CollectPathsFromToolboxDirectory(appsPath, outInstalls); if (outInstalls.Count != countBefore) return; // Toolbox V2 puts them directly into the install location. CollectPathsFromToolboxDirectory(customInstallLocation, outInstalls); return; } // Toolbox V1 default install location. String defaultAppsPath = scope .(); Path.Combine(defaultAppsPath, toolboxPath, "apps"); int countBefore = outInstalls.Count; CollectPathsFromToolboxDirectory(defaultAppsPath, outInstalls); if (outInstalls.Count != countBefore) return; // Toolbox V2 default install location. String defaultInstallLocation = scope .(); if (GetDefaultIdeInstallLocationForToolboxV2(defaultInstallLocation) case .Ok) CollectPathsFromToolboxDirectory(defaultInstallLocation, outInstalls); } /// Searches the given directory for Rider executables and creates the installation infos for them. protected static void CollectPathsFromToolboxDirectory(StringView toolboxRiderRootPath, List outInstalls) { if (toolboxRiderRootPath.IsEmpty || !Directory.Exists(toolboxRiderRootPath)) return; List riderPaths = scope .(); defer { ClearAndDeleteItems!(riderPaths); } int searchedDirectories = 0; FindFilesRecursive(toolboxRiderRootPath, GetRiderExecutableName(), riderPaths, MaxSearchDepth, ref searchedDirectories); // Reused across the iterations, see the note in FindFilesRecursive. String historyJsonPath = scope .(); for (String riderPath in riderPaths) { if (!(GetInstallInfoFromRiderPath(riderPath, .Toolbox) case .Ok(let installInfo))) continue; // The Toolbox leaves the directories of old versions behind after an update, so we // check which build it actually considers the current one for this installation. historyJsonPath.Clear(); if (GetHistoryJsonPath(riderPath, historyJsonPath) case .Ok) { RiderVersion lastBuildVersion = GetLastBuildVersion(historyJsonPath); if (lastBuildVersion.IsInitialized && installInfo.Version != lastBuildVersion) { delete installInfo; continue; } } outInstalls.Add(installInfo); } } /// Recursively searches the given directory for files with the given name. /// @remarks corlib's Directory.EnumerateFiles doesn't search subdirectories. protected static void FindFilesRecursive(StringView directory, StringView fileName, List outPaths, int remainingDepth, ref int searchedDirectories) { if (remainingDepth <= 0 || searchedDirectories >= MaxSearchedDirectories) return; searchedDirectories++; for (FileFindEntry entry in Directory.EnumerateFiles(directory, fileName)) { String path = new String(); entry.GetFilePath(path); outPaths.Add(path); } // Reused across the iterations: a "scope" allocation inside a loop body lives until the // enclosing method returns, so allocating per iteration would grow the stack unboundedly. String entryName = scope .(); String subDirectory = scope .(); for (FileFindEntry entry in Directory.EnumerateDirectories(directory)) { entryName.Clear(); entry.GetFileName(entryName); if (entryName == "." || entryName == "..") continue; subDirectory.Clear(); entry.GetFilePath(subDirectory); FindFilesRecursive(subDirectory, fileName, outPaths, remainingDepth - 1, ref searchedDirectories); } } /// Reads the custom install location out of the Toolbox' ".settings.json". protected static void ExtractPathFromSettingsJson(StringView toolboxPath, String outInstallLocation) { String settingsJsonPath = scope .(); Path.Combine(settingsJsonPath, toolboxPath, ".settings.json"); if (!File.Exists(settingsJsonPath)) return; StructuredData data = scope .(); if (data.Load(settingsJsonPath) case .Err) return; data.GetString("install_location", outInstallLocation); } /// Reads the version of a Rider installation out of its "product-info.json". protected static RiderVersion ParseProductInfoJson(StringView productInfoJsonPath, String outName, String outVersion) { StructuredData data = scope .(); if (data.Load(productInfoJsonPath) case .Err) return .(); String buildNumber = scope .(); data.GetString("buildNumber", buildNumber); data.GetString("name", outName); data.GetString("version", outVersion); return RiderVersion.Parse(buildNumber); } /// Walks up from the given Rider executable looking for the Toolbox' ".history.json". protected static Result GetHistoryJsonPath(StringView riderPath, String outPath) { String directory = scope .(); Try!(Path.GetDirectoryPath(riderPath, directory)); String parentDirectory = scope .(); String historyPath = scope .(); // Bounded so that we don't walk all the way up to the drive root. for (int i < 10) { if (!Directory.Exists(directory)) break; historyPath.Clear(); Path.Combine(historyPath, directory, ".history.json"); if (File.Exists(historyPath)) { outPath.Append(historyPath); return .Ok; } parentDirectory.Clear(); if (Path.GetDirectoryPath(directory, parentDirectory) case .Err) break; if (parentDirectory.IsEmpty || parentDirectory == directory) break; directory.Set(parentDirectory); } return .Err; } /// Reads the build number of the most recent entry of the Toolbox' ".history.json". protected static RiderVersion GetLastBuildVersion(StringView historyJsonPath) { StructuredData data = scope .(); if (data.Load(historyJsonPath) case .Err) return .(); String lastBuild = scope .(); // The most recent build is the last entry of the "history" array. for (data.Enumerate("history")) { lastBuild.Clear(); using (data.Open("item")) { data.GetString("build", lastBuild); } } return RiderVersion.Parse(lastBuild); } }