// Copyright (c) Wojciech Figat. All rights reserved. using System; using System.IO; using System.Linq; using System.Collections.Generic; using System.Runtime.InteropServices; using Microsoft.Win32; namespace Flax.Build { partial class Configuration { /// /// Prints all .NET Runtimes found on system (use plaform/arch switches to filter results). /// [CommandLine("printDotNetRuntime", "Prints all .NET Runtimes found on system (use plaform/arch switches to filter results).")] public static void PrintDotNetRuntime() { Log.Info("Printing .NET Runtimes..."); DotNetSdk.Instance.PrintRuntimes(); } } /// /// The DotNet SDK. /// public sealed class DotNetSdk : Sdk { /// /// Runtime host types. /// public enum HostType { /// /// Core CLR runtime. /// CoreCLR, /// /// Old-school Mono runtime. /// Mono, } /// /// Host runtime description. /// public struct HostRuntime : IEquatable { /// /// The path to runtime host contents folder for a given target platform and architecture. /// In format: <RootPath>/packs/Microsoft.NETCore.App.Host.<os>/<VersionName>/runtimes/<os>-<arch>/native /// public string Path; /// /// Type of the host. /// public HostType Type; /// /// Initializes a new instance of the class. /// /// The target platform (used to guess the host type). /// The host contents folder path. public HostRuntime(TargetPlatform platform, string path) { Path = path; // Detect host type (this could check if monosgen-2.0 or hostfxr lib exists but platform-switch is easier) switch (platform) { case TargetPlatform.Windows: case TargetPlatform.Linux: case TargetPlatform.Mac: Type = HostType.CoreCLR; break; default: Type = HostType.Mono; break; } } /// public bool Equals(HostRuntime other) { return Path == other.Path && Type == other.Type; } /// public override bool Equals(object? obj) { return obj is HostRuntime other && Equals(other); } /// public override int GetHashCode() { return HashCode.Combine(Path, (int)Type); } /// public override string ToString() { return Path; } } /// /// Exception when .NET SDK is missing. /// public sealed class MissingException : Exception { /// /// Init with a proper message. /// public MissingException() : base(string.IsNullOrEmpty(Configuration.Dotnet) ? $"Missing .NET SDK {MinimumVersion} (or higher) for {Platform.BuildTargetPlatform} {Platform.BuildTargetArchitecture}." : $"Missing .NET SDK {Configuration.Dotnet}.") { } } private Dictionary, HostRuntime> _hostRuntimes = new(); /// /// The singleton instance. /// public static readonly DotNetSdk Instance = new DotNetSdk(); /// /// The minimum SDK version. /// public static Version MinimumVersion => new Version(8, 0); /// /// The maximum SDK version. /// public static Version MaximumVersion => new Version(10, 0); /// public override TargetPlatform[] Platforms { get { return new[] { TargetPlatform.Windows, TargetPlatform.Linux, TargetPlatform.Mac, }; } } /// /// Dotnet SDK version (eg. 7.0). /// public readonly string VersionName; /// /// Dotnet Shared FX library version (eg. 7.0). /// public readonly string RuntimeVersionName; /// /// Maximum supported C#-language version for the SDK. /// public string CSharpLanguageVersion => Version.Major switch { _ when Version.Major >= 10 => "14.0", _ when Version.Major >= 9 => "13.0", _ when Version.Major >= 8 => "12.0", _ when Version.Major >= 7 => "11.0", _ when Version.Major >= 6 => "10.0", _ when Version.Major >= 5 => "9.0", _ => "7.3", }; /// /// Initializes a new instance of the class. /// public DotNetSdk() { var platform = Platform.BuildTargetPlatform; var architecture = Platform.BuildTargetArchitecture; if (!Platforms.Contains(platform)) return; // Find system-installed SDK string dotnetPath = Environment.GetEnvironmentVariable("DOTNET_ROOT"); string arch; IEnumerable dotnetSdkVersions = null, dotnetRuntimeVersions = null; switch (architecture) { case TargetArchitecture.x86: arch = "x86"; break; case TargetArchitecture.x64: arch = "x64"; break; case TargetArchitecture.ARM: arch = "arm"; break; case TargetArchitecture.ARM64: arch = "arm64"; break; default: throw new InvalidArchitectureException(architecture); } switch (platform) { case TargetPlatform.Windows: { #pragma warning disable CA1416 if (string.IsNullOrEmpty(dotnetPath)) { using RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); using RegistryKey sdkVersionsKey = baseKey.OpenSubKey($@"SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\{arch}\sdk"); using RegistryKey runtimeKey = baseKey.OpenSubKey(@$"SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\{arch}\sharedfx\Microsoft.NETCore.App"); using RegistryKey hostKey = baseKey.OpenSubKey(@$"SOFTWARE\dotnet\Setup\InstalledVersions\{arch}\sharedhost"); dotnetPath = (string)hostKey?.GetValue("Path"); dotnetSdkVersions = sdkVersionsKey?.GetValueNames() ?? Enumerable.Empty(); dotnetRuntimeVersions = runtimeKey?.GetValueNames() ?? Enumerable.Empty(); if (string.IsNullOrEmpty(dotnetPath)) { // The sharedhost registry key seems to be deprecated, assume the default installation location instead var defaultPath = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "Program Files", "dotnet"); if (File.Exists(Path.Combine(defaultPath, "dotnet.exe"))) dotnetPath = defaultPath; } } #pragma warning restore CA1416 break; } case TargetPlatform.Linux: { if (string.IsNullOrEmpty(dotnetPath)) dotnetPath ??= SearchForDotnetLocationLinux(); break; } case TargetPlatform.Mac: { if (string.IsNullOrEmpty(dotnetPath)) { dotnetPath = "/usr/local/share/dotnet/"; // Officially recommended dotnet location if (!Directory.Exists(dotnetPath)) { if (Environment.GetEnvironmentVariable("PATH") is string globalBinPath) dotnetPath = globalBinPath.Split(':').FirstOrDefault(x => File.Exists(Path.Combine(x, "dotnet"))); else dotnetPath = string.Empty; } } if (Flax.Build.Platforms.MacPlatform.BuildingForx64) { dotnetPath = Path.Combine(dotnetPath, "x64"); architecture = TargetArchitecture.x64; } break; } default: throw new InvalidPlatformException(platform); } // Pick SDK version if (string.IsNullOrEmpty(dotnetPath)) { Log.Warning("Missing .NET SDK"); return; } if (!Directory.Exists(Path.Combine(dotnetPath, "sdk"))) { Log.Warning($"Missing .NET SDK ({dotnetPath})"); return; } dotnetSdkVersions = MergeVersions(dotnetSdkVersions, GetVersions(Path.Combine(dotnetPath, "sdk"))); dotnetRuntimeVersions = MergeVersions(dotnetRuntimeVersions, GetVersions(Path.Combine(dotnetPath, "shared", "Microsoft.NETCore.App"))); dotnetSdkVersions = dotnetSdkVersions.Where(x => File.Exists(Path.Combine(dotnetPath, "sdk", x, ".version"))); dotnetRuntimeVersions = dotnetRuntimeVersions.Where(x => File.Exists(Path.Combine(dotnetPath, "shared", "Microsoft.NETCore.App", x, "System.dll"))); dotnetRuntimeVersions = dotnetRuntimeVersions.Where(x => Directory.Exists(Path.Combine(dotnetPath, "packs", "Microsoft.NETCore.App.Ref", x))); dotnetSdkVersions = dotnetSdkVersions.OrderByDescending(ParseVersion); dotnetRuntimeVersions = dotnetRuntimeVersions.OrderByDescending(ParseVersion); Log.Verbose($"Found the following .NET SDK versions: {string.Join(", ", dotnetSdkVersions)}"); Log.Verbose($"Found the following .NET runtime versions: {string.Join(", ", dotnetRuntimeVersions)}"); string configuredDotnetVersion = Configuration.Dotnet; var dotnetSdkVersion = GetVersion(dotnetSdkVersions); var dotnetRuntimeVersion = GetVersion(dotnetRuntimeVersions); if (!string.IsNullOrEmpty(dotnetSdkVersion) && !string.IsNullOrEmpty(dotnetRuntimeVersion) && ParseVersion(dotnetRuntimeVersion).Major > ParseVersion(dotnetSdkVersion).Major) { // Make sure the reference assemblies are not newer than the SDK itself var dotnetRuntimeVersionsRemaining = dotnetRuntimeVersions; do { dotnetRuntimeVersionsRemaining = dotnetRuntimeVersionsRemaining.Skip(1); dotnetRuntimeVersion = GetVersion(dotnetRuntimeVersionsRemaining); } while (!string.IsNullOrEmpty(dotnetRuntimeVersion) && ParseVersion(dotnetRuntimeVersion).Major > ParseVersion(dotnetSdkVersion).Major); } if (string.IsNullOrEmpty(dotnetSdkVersion)) { string installedVersionsText = dotnetSdkVersions.Any() ? $"{string.Join(", ", dotnetSdkVersions)}" : "None"; Log.Warning(!string.IsNullOrEmpty(configuredDotnetVersion) ? $"Configured .NET SDK '{configuredDotnetVersion}' not found. Installed versions: {installedVersionsText}." : $"No compatible .NET SDK found within the supported range: .NET {MinimumVersion.ToString()} - {MaximumVersion.ToString()}. Installed versions: {installedVersionsText}."); return; } if (string.IsNullOrEmpty(dotnetRuntimeVersion)) { string installedRuntimeVersionsText = dotnetRuntimeVersions.Any() ? $"{string.Join(", ", dotnetRuntimeVersions)}" : "None"; Log.Warning(!string.IsNullOrEmpty(configuredDotnetVersion) ? $"Configured .NET runtime version '{configuredDotnetVersion}' not found. Installed versions: {installedRuntimeVersionsText}." : $"No compatible .NET runtime found within the supported range: .NET {MinimumVersion.ToString()} - {MaximumVersion.ToString()}. Installed versions: {installedRuntimeVersionsText}."); return; } RootPath = dotnetPath; Version = ParseVersion(dotnetSdkVersion); VersionName = dotnetSdkVersion; RuntimeVersionName = dotnetRuntimeVersion; string rid, ridFallback = ""; switch (platform) { case TargetPlatform.Windows: rid = $"win-{arch}"; break; case TargetPlatform.Linux: { rid = RuntimeInformation.RuntimeIdentifier; ridFallback = $"linux-{arch}"; break; } case TargetPlatform.Mac: rid = Flax.Build.Platforms.MacPlatform.BuildingForx64 ? "osx-x64" : $"osx-{arch}"; break; default: throw new InvalidPlatformException(platform); } // Pick SDK runtime if (!TryAddHostRuntime(platform, architecture, rid) && !TryAddHostRuntime(platform, architecture, ridFallback)) { var path = Path.Combine(RootPath, "packs", $"Microsoft.NETCore.App.Host.{rid}", RuntimeVersionName, "runtimes", rid, "native"); Log.Warning($"Missing .NET SDK host runtime for {platform} {architecture} ({path})."); return; } TryAddHostRuntime(TargetPlatform.Windows, TargetArchitecture.x86, "win-x86"); TryAddHostRuntime(TargetPlatform.Windows, TargetArchitecture.x64, "win-x64"); TryAddHostRuntime(TargetPlatform.Windows, TargetArchitecture.ARM64, "win-arm64"); TryAddHostRuntime(TargetPlatform.Mac, TargetArchitecture.x64, "osx-x64"); TryAddHostRuntime(TargetPlatform.Mac, TargetArchitecture.ARM64, "osx-arm64"); TryAddHostRuntime(TargetPlatform.Android, TargetArchitecture.ARM, "android-arm", "Runtime.Mono"); TryAddHostRuntime(TargetPlatform.Android, TargetArchitecture.ARM64, "android-arm64", "Runtime.Mono"); TryAddHostRuntime(TargetPlatform.iOS, TargetArchitecture.ARM, "ios-arm64", "Runtime.Mono"); TryAddHostRuntime(TargetPlatform.iOS, TargetArchitecture.ARM64, "ios-arm64", "Runtime.Mono"); // Found IsValid = true; Log.Info($"Using .NET SDK {VersionName}, runtime {RuntimeVersionName} ({RootPath})"); foreach (var e in _hostRuntimes) Log.Verbose($" - Host Runtime for {e.Key.Key} {e.Key.Value}"); } /// /// Initializes platforms which can register custom host runtime location /// private static void InitPlatforms() { Platform.GetPlatform(TargetPlatform.Windows, true); } /// /// Gets the host runtime identifier for a given platform (eg. linux-arm64). /// /// Target platform. /// Target architecture. /// Runtime identifier. public static string GetHostRuntimeIdentifier(TargetPlatform platform, TargetArchitecture architecture) { string result; switch (platform) { case TargetPlatform.Windows: case TargetPlatform.XboxOne: case TargetPlatform.XboxScarlett: case TargetPlatform.UWP: result = "win"; break; case TargetPlatform.Linux: result = "linux"; break; case TargetPlatform.PS4: result = "ps4"; break; case TargetPlatform.PS5: result = "ps5"; break; case TargetPlatform.Android: result = "android"; break; case TargetPlatform.Switch: result = "switch"; break; case TargetPlatform.Mac: result = "osx"; break; case TargetPlatform.iOS: result = "ios"; break; default: throw new InvalidPlatformException(platform); } switch (architecture) { case TargetArchitecture.x86: result += "-x86"; break; case TargetArchitecture.x64: result += "-x64"; break; case TargetArchitecture.ARM: result += "-arm"; break; case TargetArchitecture.ARM64: result += "-arm64"; break; default: throw new InvalidArchitectureException(architecture); } return result; } /// /// Prints the .NET runtimes hosts. /// public void PrintRuntimes() { InitPlatforms(); foreach (var e in _hostRuntimes) { // Filter with input commandline TargetPlatform[] platforms = Configuration.BuildPlatforms; if (platforms != null && !platforms.Contains(e.Key.Key)) continue; TargetArchitecture[] architectures = Configuration.BuildArchitectures; if (architectures != null && !architectures.Contains(e.Key.Value)) continue; Log.Message($"{e.Key.Key}, {e.Key.Value}, {e.Value}"); } } /// /// Gets the runtime host for a given target platform and architecture. /// public bool GetHostRuntime(TargetPlatform platform, TargetArchitecture arch, out HostRuntime hostRuntime) { InitPlatforms(); return _hostRuntimes.TryGetValue(new KeyValuePair(platform, arch), out hostRuntime); } /// /// Adds an external hostfx location for a given platform. /// /// Target platform. /// Target architecture. /// Folder path with contents. public void AddHostRuntime(TargetPlatform platform, TargetArchitecture arch, string path) { if (Directory.Exists(path)) _hostRuntimes[new KeyValuePair(platform, arch)] = new HostRuntime(platform, path); } private bool TryAddHostRuntime(TargetPlatform platform, TargetArchitecture arch, string rid, string runtimeName = null) { if (string.IsNullOrEmpty(rid)) return false; // Pick pack folder var packFolder = Path.Combine(RootPath, $"packs/Microsoft.NETCore.App.Host.{rid}"); var exists = Directory.Exists(packFolder); if (!exists && runtimeName != null) { packFolder = Path.Combine(RootPath, $"packs/Microsoft.NETCore.App.{runtimeName}.{rid}"); exists = Directory.Exists(packFolder); } if (!exists) return false; // Pick version folder var versions = GetVersions(packFolder); var version = GetVersion(versions); var path = Path.Combine(packFolder, $"{version}/runtimes/{rid}/native"); exists = Directory.Exists(path); if (exists) _hostRuntimes[new KeyValuePair(platform, arch)] = new HostRuntime(platform, Utilities.NormalizePath(path)); return exists; } internal static string SelectVersionFolder(string root) { var versions = GetVersions(root); var version = GetVersion(versions); if (version == null) throw new Exception($"Failed to select dotnet version from '{root}' ({string.Join(", ", versions)})"); return Path.Combine(root, version); } private static IEnumerable MergeVersions(IEnumerable a, IEnumerable b) { if (a == null || !a.Any()) return b; if (b == null || !b.Any()) return a; var result = new HashSet(); result.AddRange(a); result.AddRange(b); return result; } private static Version ParseVersion(string version) { // Give precedence to final releases over release candidate / beta releases int rev = 9999; if (version.Contains("-")) // e.g. 7.0.0-rc.2.22472.3 { version = version.Substring(0, version.IndexOf("-")); rev = 0; } if (!Version.TryParse(version, out var ver)) return null; if (ver.Build == -1) return new Version(ver.Major, ver.Minor); return new Version(ver.Major, ver.Minor, ver.Build, rev); } private static IEnumerable GetVersions(string folder) { if (!Directory.Exists(folder)) return Enumerable.Empty(); return Directory.GetDirectories(folder).Select(Path.GetFileName); } private static string GetVersion(IEnumerable versions) { Version dotnetVer = null; int dotnetVerNum = -1; if (!string.IsNullOrEmpty(Configuration.Dotnet)) { dotnetVer = ParseVersion(Configuration.Dotnet); if (int.TryParse(Configuration.Dotnet, out var tmp) && tmp >= MinimumVersion.Major) dotnetVerNum = tmp; } var sorted = versions.OrderByDescending(ParseVersion); foreach (var version in sorted) { var v = ParseVersion(version); // Filter by version specified in command line if (dotnetVer != null) { if (dotnetVer.Major != v.Major) continue; if (dotnetVer.Minor != v.Minor) continue; if (dotnetVer.Revision != -1 && dotnetVer.Revision != v.Revision) continue; if (dotnetVer.Build != -1 && dotnetVer.Build != v.Build) continue; } else if (dotnetVerNum != -1) { if (dotnetVerNum != v.Major) continue; } // Filter by min/max versions supported by Flax.Build if (v.Major >= MinimumVersion.Major && v.Major <= MaximumVersion.Major) return version; } return null; } private static string SearchForDotnetLocationLinux() { if (File.Exists("/etc/dotnet/install_location")) // Officially recommended dotnet location file return File.ReadAllText("/etc/dotnet/install_location").Trim(); if (File.Exists("/usr/share/dotnet/dotnet")) // Officially recommended dotnet location return "/usr/share/dotnet"; if (File.Exists("/usr/lib/dotnet/dotnet")) // Deprecated recommended dotnet location return "/usr/lib/dotnet"; if (Environment.GetEnvironmentVariable("PATH") is string globalBinPath) // Searching for dotnet binary return globalBinPath.Split(':').FirstOrDefault(x => File.Exists(Path.Combine(x, "dotnet"))); return null; } } }