|
| 1 | +using System; |
| 2 | +using System.IO; |
| 3 | +using System.Threading.Tasks; |
| 4 | + |
| 5 | +namespace Riverside.CompilerPlatform.Helpers; |
| 6 | + |
| 7 | +/// <summary> |
| 8 | +/// Provides helpers for installing and locating .NET tools installed to a specific tool-path directory. |
| 9 | +/// </summary> |
| 10 | +public static class NETCoreToolHelpers |
| 11 | +{ |
| 12 | + /// <summary> |
| 13 | + /// Returns the full path to the tool executable expected in <paramref name="toolDirectory"/>. |
| 14 | + /// Appends <c>.exe</c> on Windows; uses the bare name on all other platforms. |
| 15 | + /// </summary> |
| 16 | + /// <param name="toolDirectory">The directory the tool was installed into via <c>--tool-path</c>.</param> |
| 17 | + /// <param name="toolName">The tool executable name (e.g. <c>kiota</c>).</param> |
| 18 | + /// <returns>The full path including the platform-appropriate extension.</returns> |
| 19 | + public static string GetExecutablePath(string toolDirectory, string toolName) |
| 20 | + => Path.Combine( |
| 21 | + toolDirectory, |
| 22 | + Environment.OSVersion.Platform == PlatformID.Win32NT ? toolName + ".exe" : toolName); |
| 23 | + |
| 24 | + /// <summary> |
| 25 | + /// Ensures the specified .NET tool is available in <paramref name="toolDirectory"/>. |
| 26 | + /// </summary> |
| 27 | + /// <remarks> |
| 28 | + /// <list type="bullet"> |
| 29 | + /// <item>If the executable already exists and no specific <paramref name="version"/> is requested, the tool is reused immediately.</item> |
| 30 | + /// <item>Otherwise <c>dotnet tool install</c> is attempted. If it fails because the tool is already installed, <c>dotnet tool update</c> is tried instead.</item> |
| 31 | + /// <item>Installation succeeds when the executable is present after the above steps.</item> |
| 32 | + /// </list> |
| 33 | + /// </remarks> |
| 34 | + /// <param name="toolName">The NuGet package ID of the tool (e.g. <c>Riverside.JsonBinder.Console</c>).</param> |
| 35 | + /// <param name="toolDirectory">The directory to install the tool into, passed to <c>--tool-path</c>.</param> |
| 36 | + /// <param name="version"> |
| 37 | + /// A specific version to pin. Pass <see langword="null"/> to install or keep the latest. |
| 38 | + /// </param> |
| 39 | + /// <param name="timeout">Maximum wait time per install or update process. Defaults to 5 minutes.</param> |
| 40 | + /// <returns> |
| 41 | + /// A tuple where <c>Success</c> is <see langword="true"/> when the executable is available, and <c>Error</c> carries the captured stderr when installation fails. |
| 42 | + /// </returns> |
| 43 | + public static async Task<(bool Success, string? Error)> EnsureToolAsync( |
| 44 | + string toolName, |
| 45 | + string toolDirectory, |
| 46 | + string? version = null, |
| 47 | + TimeSpan? timeout = null) |
| 48 | + { |
| 49 | + var exe = GetExecutablePath(toolDirectory, toolName); |
| 50 | + var effectiveTimeout = timeout ?? TimeSpan.FromMinutes(5); |
| 51 | + |
| 52 | + Directory.CreateDirectory(toolDirectory); |
| 53 | + |
| 54 | + if (File.Exists(exe) && string.IsNullOrWhiteSpace(version)) |
| 55 | + return (true, null); |
| 56 | + |
| 57 | + var toolPathArg = $"--tool-path \"{toolDirectory}\""; |
| 58 | + var versionArg = string.IsNullOrWhiteSpace(version) ? string.Empty : $" --version {version}"; |
| 59 | + |
| 60 | + var installResult = await ProcessHelpers.RunNETCoreCliAsync( |
| 61 | + $"tool install {toolName} {toolPathArg}{versionArg}", effectiveTimeout); |
| 62 | + |
| 63 | + if (installResult.ExitCode == 0) |
| 64 | + return (true, null); |
| 65 | + |
| 66 | + // install exits non-zero when the tool is already present; attempt an update instead |
| 67 | + var updateResult = await ProcessHelpers.RunNETCoreCliAsync( |
| 68 | + $"tool update {toolName} {toolPathArg}{versionArg}", effectiveTimeout); |
| 69 | + |
| 70 | + return File.Exists(exe) |
| 71 | + ? (true, null) |
| 72 | + : (false, updateResult.StandardError); |
| 73 | + } |
| 74 | +} |
0 commit comments