godot/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using OS = GodotTools.Utils.OS;

namespace GodotTools.Build
{
    public static class DotNetFinder
    {
        public static string? FindDotNetExe()
        {
            // In the future, this method may do more than just search in PATH. We could look in
            // known locations or use Godot's linked nethost to search from the hostfxr location.

            if (OS.IsMacOS)
            {
                if (RuntimeInformation.OSArchitecture == Architecture.X64)
                {
                    string dotnet_x64 = "/usr/local/share/dotnet/x64/dotnet"; // Look for x64 version, when running under Rosetta 2.
                    if (File.Exists(dotnet_x64))
                    {
                        return dotnet_x64;
                    }
                }
                string dotnet = "/usr/local/share/dotnet/dotnet"; // Look for native version.
                if (File.Exists(dotnet))
                {
                    return dotnet;
                }
            }

            return OS.PathWhich("dotnet");
        }

        public static bool TryFindDotNetSdk(
            Version expectedVersion,
            [NotNullWhen(true)] out Version? version,
            [NotNullWhen(true)] out string? path
        )
        {
            version = null;
            path = null;

            string? dotNetExe = FindDotNetExe();

            if (string.IsNullOrEmpty(dotNetExe))
                return false;

            using Process process = new Process();
            process.StartInfo = new ProcessStartInfo(dotNetExe, "--list-sdks")
            {
                UseShellExecute = false,
                RedirectStandardOutput = true
            };

            if (OperatingSystem.IsWindows())
            {
                process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
            }

            process.StartInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"] = "en-US";

            var lines = new List<string>();

            process.OutputDataReceived += (_, e) =>
            {
                if (!string.IsNullOrWhiteSpace(e.Data))
                    lines.Add(e.Data);
            };

            try
            {
                process.Start();
            }
            catch
            {
                return false;
            }

            process.BeginOutputReadLine();
            process.WaitForExit();

            Version? latestVersionMatch = null;
            string? matchPath = null;

            foreach (var line in lines)
            {
                string[] sdkLineParts = line.Trim()
                    .Split(' ', 2, StringSplitOptions.TrimEntries);

                if (sdkLineParts.Length < 2)
                    continue;

                if (!Version.TryParse(sdkLineParts[0], out var lineVersion))
                    continue;

                // We're looking for the exact same major version
                if (lineVersion.Major != expectedVersion.Major)
                    continue;

                if (latestVersionMatch != null && lineVersion < latestVersionMatch)
                    continue;

                latestVersionMatch = lineVersion;
                matchPath = sdkLineParts[1].TrimStart('[').TrimEnd(']');
            }

            if (latestVersionMatch == null)
                return false;

            version = latestVersionMatch;
            path = Path.Combine(matchPath!, version.ToString());

            return true;
        }
    }
}