diff --git a/src/Squirrel/RuntimeInfo.cs b/src/Squirrel/RuntimeInfo.cs index 95f4d74a9..7abc94fbf 100644 --- a/src/Squirrel/RuntimeInfo.cs +++ b/src/Squirrel/RuntimeInfo.cs @@ -1,15 +1,11 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Linq; -using System.Net; -using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32; -using Squirrel.SimpleSplat; using Squirrel.Sources; namespace Squirrel @@ -156,14 +152,12 @@ public override Task CheckIsInstalled() public class DotnetInfo : RuntimeInfo { /// - public override string Id => MinVersion.Major >= 5 - ? $"net{TrimVersion(MinVersion)}-{CpuArchitecture.ToString().ToLower()}" - : $"netcoreapp{TrimVersion(MinVersion)}-{CpuArchitecture.ToString().ToLower()}"; + public override string Id => + $"{(MinVersion.Major >= 5 ? "net" : "netcoreapp")}{TrimVersion(MinVersion)}-{CpuArchitecture.ToString().ToLower()}-{_runtimeShortForm[RuntimeType]}"; /// - public override string DisplayName => MinVersion.Major >= 5 - ? $".NET {TrimVersion(MinVersion)} Desktop Runtime ({CpuArchitecture.ToString().ToLower()})" - : $".NET Core {TrimVersion(MinVersion)} Desktop Runtime ({CpuArchitecture.ToString().ToLower()})"; + public override string DisplayName => + $"{(MinVersion.Major >= 5 ? ".NET" : ".NET Core")} {TrimVersion(MinVersion)} {RuntimeType} ({CpuArchitecture.ToString().ToLower()})"; /// The minimum compatible version that must be installed. public SemanticVersion MinVersion { get; } @@ -172,11 +166,22 @@ public class DotnetInfo : RuntimeInfo /// For example, if the Squirrel app was deployed with 'win-x64', this must be X64 also. public RuntimeCpu CpuArchitecture { get; } + /// The type of runtime required, eg. Windows Desktop, AspNetCore, Sdk. + public DotnetRuntimeType RuntimeType { get; } + + private static readonly Dictionary _runtimeShortForm = new() { + { DotnetRuntimeType.DotNet, "base" }, + { DotnetRuntimeType.SDK, "sdk" }, + { DotnetRuntimeType.WindowsDesktop, "desktop" }, + { DotnetRuntimeType.AspNetCore, "asp" }, + }; + /// - protected DotnetInfo(Version minversion, RuntimeCpu architecture) + protected DotnetInfo(Version minversion, RuntimeCpu architecture, DotnetRuntimeType runtimeType = DotnetRuntimeType.WindowsDesktop) { MinVersion = new SemanticVersion(minversion); CpuArchitecture = architecture; + RuntimeType = runtimeType; if (minversion.Major == 6 && minversion.Build < 0) { Log.Warn( $"Automatically upgrading minimum dotnet version from net{minversion} to net6.0.2, " + @@ -186,8 +191,8 @@ protected DotnetInfo(Version minversion, RuntimeCpu architecture) } } - internal DotnetInfo(string minversion, RuntimeCpu architecture) - : this(ParseVersion(minversion), architecture) + internal DotnetInfo(string minversion, RuntimeCpu architecture, DotnetRuntimeType runtimeType = DotnetRuntimeType.WindowsDesktop) + : this(ParseVersion(minversion), architecture, runtimeType) { } @@ -197,13 +202,17 @@ internal DotnetInfo(string minversion, RuntimeCpu architecture) /// public override Task CheckIsInstalled() { - switch (CpuArchitecture) { + var versionDir = GetDotnetVersionDir(CpuArchitecture, RuntimeType); + if (!Directory.Exists(versionDir)) + return Task.FromResult(false); - case RuntimeCpu.x64: return Task.FromResult(CheckIsInstalledX64()); - case RuntimeCpu.x86: return Task.FromResult(CheckIsInstalledX86()); - default: return Task.FromResult(false); + var dirs = Directory.EnumerateDirectories(versionDir) + .Select(d => Path.GetFileName(d)) + .Where(d => SemanticVersion.TryParse(d, out var _)) + .Select(d => SemanticVersion.Parse(d)); - } + var foundCompatibleVer = dirs.Any(v => v.Major == MinVersion.Major && v.Minor == MinVersion.Minor && v >= MinVersion); + return Task.FromResult(foundCompatibleVer); } /// @@ -216,34 +225,43 @@ public override Task CheckIsSupported() return Task.FromResult(true); } - private bool CheckIsInstalledX86() + private static string GetDotnetVersionDir(RuntimeCpu runtimeArch, DotnetRuntimeType runtimeType) { - var pf86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); - return CheckIsInstalledInBaseDirectory(pf86); + var baseDir = GetDotnetBaseDir(runtimeArch); + if (String.IsNullOrEmpty(baseDir)) + return null; + + return runtimeType switch { + DotnetRuntimeType.DotNet => Path.Combine(baseDir, "shared", "Microsoft.NETCore.App"), + DotnetRuntimeType.AspNetCore => Path.Combine(baseDir, "shared", "Microsoft.AspNetCore.App"), + DotnetRuntimeType.WindowsDesktop => Path.Combine(baseDir, "shared", "Microsoft.WindowsDesktop.App"), + DotnetRuntimeType.SDK => Path.Combine(baseDir, "sdk"), + _ => throw new ArgumentOutOfRangeException(nameof(DotnetRuntimeType)), + }; } - private bool CheckIsInstalledX64() + private static string GetDotnetBaseDir(RuntimeCpu runtime) { - if (!Environment.Is64BitOperatingSystem) - return false; + var system = SquirrelRuntimeInfo.SystemArchitecture; + var pf86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); - // we are probably an x86 process, and I don't know of any great ways to - // get the x64 ProgramFiles directory from an x86 process, so this code - // is extremely unfortunate. + if (runtime == RuntimeCpu.x86) + return Path.Combine(pf86, "dotnet"); - if (Environment.Is64BitProcess) { - // this only works in a 64 bit process, otherwise it points to ProgramFilesX86 - var pf64 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - if (CheckIsInstalledInBaseDirectory(pf64)) - return true; - } + // this only works in a 64 bit process, otherwise it points to ProgramFilesX86 + var pf64 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - // https://docs.microsoft.com/en-us/windows/win32/winprog64/wow64-implementation-details + // try to get the real 64 bit program files directory var pf64compat = Environment.GetEnvironmentVariable("ProgramW6432"); if (Directory.Exists(pf64compat)) - return CheckIsInstalledInBaseDirectory(pf64compat); + pf64 = pf64compat; - return false; + if (runtime == system) { + // looking for x64 on an x64 system will always be in pf64. + return Path.Combine(pf64, "dotnet"); + } + + return null; } private bool CheckIsInstalledInBaseDirectory(string baseDirectory) @@ -270,10 +288,10 @@ public override async Task GetDownloadUrl() _ => throw new ArgumentOutOfRangeException(nameof(CpuArchitecture)), }; - return GetDotNetDownloadUrl(DotnetRuntimeType.WindowsDesktop, latest, architecture); + return GetDotNetDownloadUrl(RuntimeType, latest, architecture); } - private static Regex _dotnetRegex = new Regex(@"^net(?:coreapp)?(?[\d\.]{1,6})(?:-(?[\w\d]+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static Regex _dotnetRegex = new Regex(@"^net(?:coreapp)?(?[\d\.]{1,6})(?:-(?[\w\d]+))?(?:-(?\w+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled); /// /// Parses a string such as 'net6' or net5.0.14-x86 into a DotnetInfo class capable of checking @@ -287,13 +305,26 @@ public static DotnetInfo Parse(string input) var verstr = match.Groups["version"].Value; var archstr = match.Groups["arch"].Value; // default is x64 if not specified + var typestr = match.Groups["type"].Value; // default is WindowsDesktop var archValid = Enum.TryParse(String.IsNullOrWhiteSpace(archstr) ? "x64" : archstr, true, out var cpu); if (!archValid) throw new ArgumentException($"Invalid machine architecture '{archstr}'."); + var type = DotnetRuntimeType.WindowsDesktop; + if (!String.IsNullOrEmpty(typestr)) { + var q = _runtimeShortForm.Where(kvp => kvp.Value.Equals(typestr, StringComparison.InvariantCultureIgnoreCase)); + if (Enum.TryParse(typestr, true, out var parsed)) { + type = parsed; + } else if (q.Any()) { + type = q.First().Key; + } else { + throw new ArgumentException($"Invalid dotnet runtime sku '{typestr}'. Valid values: {String.Join(", ", _runtimeShortForm.Values)}"); + } + } + var ver = ParseVersion(verstr); - return new DotnetInfo(ver, cpu); + return new DotnetInfo(ver, cpu, type); } /// @@ -322,7 +353,7 @@ protected static Version ParseVersion(string input) throw new ArgumentException("Version must only be a 3-part version string.", nameof(input)); if ((v.Major == 3 && v.Minor == 1) || v.Major >= 5) { - if (v.Major > 7) { + if (v.Major > 8) { Log.Warn( $"Runtime version '{input}' was resolved to major version '{v.Major}', but this is greater than any known dotnet version, " + $"and if this version does not exist this package may fail to install."); diff --git a/src/Squirrel/Runtimes.cs b/src/Squirrel/Runtimes.cs index 763d37626..a73150ba1 100644 --- a/src/Squirrel/Runtimes.cs +++ b/src/Squirrel/Runtimes.cs @@ -40,13 +40,17 @@ public static partial class Runtimes /// Runtime for .NET Core 3.1 Desktop Runtime (x64) public static readonly DotnetInfo DOTNETCORE31_X64 = new("3.1", RuntimeCpu.x64); // eg. netcoreapp3.1-x64 /// Runtime for .NET 5.0 Desktop Runtime (x86) - public static readonly DotnetInfo DOTNET5_X86 = new("5.0", RuntimeCpu.x86); // eg. net5.0.14-x86 + public static readonly DotnetInfo DOTNET5_X86 = new("5.0", RuntimeCpu.x86); // eg. net5.0-x86 /// Runtime for .NET 5.0 Desktop Runtime (x64) - public static readonly DotnetInfo DOTNET5_X64 = new("5.0", RuntimeCpu.x64); // eg. net5 + public static readonly DotnetInfo DOTNET5_X64 = new("5.0", RuntimeCpu.x64); // eg. net5.0-x64 /// Runtime for .NET 6.0 Desktop Runtime (x86) - public static readonly DotnetInfo DOTNET6_X86 = new("6.0.2", RuntimeCpu.x86); // eg. net6-x86 + public static readonly DotnetInfo DOTNET6_X86 = new("6.0.2", RuntimeCpu.x86); // eg. net6.0-x86 /// Runtime for .NET 6.0 Desktop Runtime (x64) - public static readonly DotnetInfo DOTNET6_X64 = new("6.0.2", RuntimeCpu.x64); // eg. net6.0.2 + public static readonly DotnetInfo DOTNET6_X64 = new("6.0.2", RuntimeCpu.x64); // eg. net6.0-x64 + /// Runtime for .NET 7.0 Desktop Runtime (x86) + public static readonly DotnetInfo DOTNET7_X86 = new("7.0", RuntimeCpu.x86); // eg. net7.0-x86 + /// Runtime for .NET 7.0 Desktop Runtime (x64) + public static readonly DotnetInfo DOTNET7_X64 = new("7.0", RuntimeCpu.x64); // eg. net7.0-x64 /// Runtime for Visual C++ 2010 Redistributable (x86) diff --git a/test/RuntimeTests.cs b/test/RuntimeTests.cs index 2e2c8a0c0..60e0ea52a 100644 --- a/test/RuntimeTests.cs +++ b/test/RuntimeTests.cs @@ -12,17 +12,21 @@ public class RuntimeTests // we are upgrading net6 to a minimum version of 6.0.2 to work // around a dotnet SDK bug right now. [Theory] - [InlineData("net6", "net6.0.2-x64")] - [InlineData("net6.0", "net6.0.2-x64")] - [InlineData("net6-x64", "net6.0.2-x64")] - [InlineData("net6-x86", "net6.0.2-x86")] - [InlineData("net3.1", "netcoreapp3.1-x64")] - [InlineData("netcoreapp3.1", "netcoreapp3.1-x64")] - [InlineData("net3.1-x86", "netcoreapp3.1-x86")] - [InlineData("net6.0.2", "net6.0.2-x64")] - [InlineData("net6.0.2-x86", "net6.0.2-x86")] - [InlineData("net6.0.1-x86", "net6.0.1-x86")] - [InlineData("net6.0.0", "net6-x64")] + [InlineData("net6", "net6.0.2-x64-desktop")] + [InlineData("net6.0", "net6.0.2-x64-desktop")] + [InlineData("net6-x64", "net6.0.2-x64-desktop")] + [InlineData("net6-x86", "net6.0.2-x86-desktop")] + [InlineData("net3.1", "netcoreapp3.1-x64-desktop")] + [InlineData("netcoreapp3.1", "netcoreapp3.1-x64-desktop")] + [InlineData("net3.1-x86", "netcoreapp3.1-x86-desktop")] + [InlineData("net6.0.2", "net6.0.2-x64-desktop")] + [InlineData("net6.0.2-x86", "net6.0.2-x86-desktop")] + [InlineData("net6.0.1-x86", "net6.0.1-x86-desktop")] + [InlineData("net6.0.0", "net6-x64-desktop")] + [InlineData("net6.0-x64-desktop", "net6.0.2-x64-desktop")] + [InlineData("net7.0-x64-base", "net7-x64-base")] + [InlineData("net7.0-x64-asp", "net7-x64-asp")] + [InlineData("net7.0-x64-sdk", "net7-x64-sdk")] public void DotnetParsesValidVersions(string input, string expected) { var p = Runtimes.DotnetInfo.Parse(input); @@ -33,6 +37,8 @@ public void DotnetParsesValidVersions(string input, string expected) [InlineData("net3.2")] [InlineData("net4.9")] [InlineData("net6.0.0.4")] + [InlineData("net6-basd")] + [InlineData("net6-x64-aakaka")] public void DotnetParseThrowsInvalidVersion(string input) { Assert.ThrowsAny(() => Runtimes.DotnetInfo.Parse(input)); @@ -54,6 +60,8 @@ public void DotnetParseThrowsInvalidVersion(string input) [InlineData("asd", false)] [InlineData("", false)] [InlineData(null, false)] + [InlineData("net6-x64", true)] + [InlineData("net6-x64-sdk", true)] public void GetRuntimeTests(string input, bool expected) { var dn = Runtimes.GetRuntimeByName(input);