From 6ab0a48b0f7a8729a01c4dbe4d5249fb5603eec9 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 4 Jul 2026 12:05:02 +0200 Subject: [PATCH] Support Roslyn 1.x and 2.x compilers in tests on non-Windows The Roslyn 1.3.2 and 2.10.0 configurations were excluded from the compiler matrix on non-Windows platforms because Microsoft.Net.Compilers only ships .NET Framework executables. Both can be enabled: - Roslyn 2.10.0 has a dotnet-hosted sibling package, Microsoft.NETCore.Compilers, whose tools/bincore/csc.dll runs on the installed runtime with --roll-forward LatestMajor (its runtimeconfig pins the out-of-support .NET Core 2.0). Fetched on non-Windows into the version's tools/bincore directory; GetCSharpCompiler probes for a direct csc.dll next to the installed path in addition to the bincore/ subfolder layout of the newer toolset packages. - Roslyn 1.3.2 has no .NET build; when a mono executable is found on the PATH, it is kept in the matrix and WrapCompiler hosts the .exe compilers through mono. Because the native DiaSymReader needed for Windows PDBs is unavailable there, GeneratePdb requests portable PDBs on non-Windows (Roslyn 2.x+ falls back on its own, 1.x needs the explicit -debug:portable). The mcs configurations stay excluded: the bundled mcs 2.6.4 needs the Reflection.Emit COMPILER_ACCESS mode that current Mono runtimes no longer implement. Old-compiler configurations also stay excluded from correctness-style fixtures: their output targets .NET Framework or .NET Core 2.2, which the runners cannot execute here. Microsoft.NETCore.Compilers-2.10.0.nupkg should be added to ILSpy-tests/nuget to keep the fetch offline-capable. Assisted-by: Claude:claude-fable-5:Claude Code --- .../Helpers/RoslynToolset.cs | 23 ++++- .../Helpers/Tester.cs | 95 +++++++++++++++---- 2 files changed, 100 insertions(+), 18 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/Helpers/RoslynToolset.cs b/ICSharpCode.Decompiler.Tests/Helpers/RoslynToolset.cs index 3a75f331a..ec251070f 100644 --- a/ICSharpCode.Decompiler.Tests/Helpers/RoslynToolset.cs +++ b/ICSharpCode.Decompiler.Tests/Helpers/RoslynToolset.cs @@ -149,14 +149,33 @@ namespace ICSharpCode.Decompiler.Tests.Helpers // In the .NET ("netcore") build of the compiler toolset the executables live in a // "bincore" subfolder of the tasks directory, as .dlls launched through the dotnet host. + // The old Microsoft.Net.Compilers packages (Roslyn 1.x/2.x) have no .NET build; they + // only ship .NET Framework executables, which non-Windows platforms host with Mono. public string GetCSharpCompiler(string version) { - return GetCompiler(OperatingSystem.IsWindows() ? "csc.exe" : "bincore/csc.dll", version); + return GetHostedCompiler(version, "csc"); } public string GetVBCompiler(string version) { - return GetCompiler(OperatingSystem.IsWindows() ? "vbc.exe" : "bincore/vbc.dll", version); + return GetHostedCompiler(version, "vbc"); + } + + string GetHostedCompiler(string version, string name) + { + if (!OperatingSystem.IsWindows()) + { + // The installed path may point directly at a dotnet-hosted compiler directory + // (e.g. Microsoft.NETCore.Compilers' tools/bincore) or at a tasks directory + // with a bincore subfolder (Microsoft.Net.Compilers.Toolset). + string dll = GetCompiler($"{name}.dll", version); + if (File.Exists(dll)) + return dll; + dll = GetCompiler($"bincore/{name}.dll", version); + if (File.Exists(dll)) + return dll; + } + return GetCompiler($"{name}.exe", version); } string GetCompiler(string compiler, string version) diff --git a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs index 499e82c4c..c58ad2fa3 100644 --- a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs +++ b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs @@ -144,7 +144,17 @@ namespace ICSharpCode.Decompiler.Tests.Helpers internal static async Task Initialize() { await roslynToolset.Fetch("1.3.2", "Microsoft.Net.Compilers", "tools").ConfigureAwait(false); - await roslynToolset.Fetch("2.10.0", "Microsoft.Net.Compilers", "tools").ConfigureAwait(false); + if (OperatingSystem.IsWindows()) + { + await roslynToolset.Fetch("2.10.0", "Microsoft.Net.Compilers", "tools").ConfigureAwait(false); + } + else + { + // Microsoft.Net.Compilers only ships .NET Framework executables. The sibling + // Microsoft.NETCore.Compilers package contains the dotnet-hosted build of the + // same compiler version (tools/bincore/csc.dll), usable on any platform. + await roslynToolset.Fetch("2.10.0", "Microsoft.NETCore.Compilers", "tools/bincore").ConfigureAwait(false); + } // On non-Windows hosts the net472 compiler binaries cannot be executed; use the // .NET build of each toolset instead. Its tasks folder is named "netcoreapp3.1" // up to Roslyn 3.x and "netcore" from Roslyn 4.x on. @@ -175,34 +185,73 @@ namespace ICSharpCode.Decompiler.Tests.Helpers } } + /// + /// True when a Mono runtime is on the PATH. Mono hosts the .NET Framework builds of + /// compilers that have no .NET build (Roslyn 1.x csc.exe from Microsoft.Net.Compilers); + /// Roslyn 2.x+ configurations use dotnet-hosted builds and do not depend on Mono. + /// + public static readonly bool MonoRuntimeAvailable = + !OperatingSystem.IsWindows() + && (Environment.GetEnvironmentVariable("PATH") ?? "") + .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) + .Any(dir => File.Exists(Path.Combine(dir, "mono"))); + /// /// Reduces a compiler-configuration matrix to the entries usable on the current platform. /// On Windows every configuration is supported. On other platforms, configurations that - /// depend on Windows-only tools or runtimes are removed: the legacy (pre-Roslyn) csc/vbc, - /// Roslyn 1.x/2.x (their packages only ship .NET Framework binaries), mcs, and Force32Bit - /// (requires a 32-bit runtime). When is set - /// (correctness-style fixtures), configurations targeting .NET Framework 4.0 are removed - /// as well, because their output executables only run on Windows. + /// depend on Windows-only tools or runtimes are removed: the legacy (pre-Roslyn) csc/vbc + /// and Force32Bit (requires a 32-bit runtime) always; Roslyn 1.x/2.x (their packages only + /// ship .NET Framework binaries) and mcs unless a Mono runtime is available to host them. + /// When is set (correctness-style fixtures), + /// configurations targeting .NET Framework 4.0 are removed as well, because their output + /// executables only run on Windows, and the Mono-hosted compilers are not included either + /// (their output would likewise need a Mono host to execute). /// public static CompilerOptions[] SupportedOnCurrentPlatform(CompilerOptions[] configurations, bool executesCompiledOutput = false) { if (OperatingSystem.IsWindows()) return configurations; const CompilerOptions dotnetHostedCompilers = CompilerOptions.UseRoslyn3_11_0 | CompilerOptions.UseRoslyn4_14_0 | CompilerOptions.UseRoslynLatest; - return configurations.Where(c => (c & dotnetHostedCompilers) != 0 + CompilerOptions supportedCompilers = dotnetHostedCompilers; + if (!executesCompiledOutput) + { + // The dotnet-hosted Roslyn 2.x build (Microsoft.NETCore.Compilers) can compile + // on any platform, but its output targets .NET Core 2.2 or .NET Framework, + // which the correctness runners cannot execute here. + supportedCompilers |= CompilerOptions.UseRoslyn2_10_0; + // Roslyn 1.3.2 has only a .NET Framework build and needs Mono as its host. + // The configuration stays in the matrix even without Mono so that a missing + // runtime shows up as skipped tests (see WrapCompiler) instead of a silently + // smaller matrix. mcs 2.6.4 stays excluded: it needs the Reflection.Emit + // COMPILER_ACCESS mode that current Mono runtimes no longer implement. + supportedCompilers |= CompilerOptions.UseRoslyn1_3_2; + } + return configurations.Where(c => (c & supportedCompilers) != 0 && !c.HasFlag(CompilerOptions.Force32Bit) && !(executesCompiledOutput && c.HasFlag(CompilerOptions.TargetNet40))).ToArray(); } /// /// Wraps an external compiler invocation. The .NET Framework builds of the Roslyn - /// compilers (csc.exe/vbc.exe) are directly executable; the .NET builds ship as a - /// .dll that must be launched through the 'dotnet' host. + /// compilers (csc.exe/vbc.exe) are directly executable on Windows and are hosted by + /// Mono elsewhere; the .NET builds ship as a .dll that must be launched through the + /// 'dotnet' host. /// static Command WrapCompiler(string compilerPath, string arguments) { + // Old compiler builds pin an out-of-support runtime in their runtimeconfig + // (e.g. Roslyn 2.x pins Microsoft.NETCore.App 2.0), so allow the host to + // roll forward across major versions to whatever runtime is installed. if (compilerPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) - return Cli.Wrap("dotnet").WithArguments($"\"{compilerPath}\" {arguments}"); + return Cli.Wrap("dotnet").WithArguments($"--roll-forward LatestMajor \"{compilerPath}\" {arguments}"); + if (!OperatingSystem.IsWindows() && compilerPath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + { + if (!MonoRuntimeAvailable) + { + Assert.Ignore($"{Path.GetFileName(compilerPath)} requires a Mono runtime on the PATH to run on this platform."); + } + return Cli.Wrap("mono").WithArguments($"\"{compilerPath}\" {arguments}"); + } return Cli.Wrap(compilerPath).WithArguments(arguments); } @@ -695,7 +744,17 @@ namespace System.Runtime.CompilerServices if (flags.HasFlag(CompilerOptions.GeneratePdb)) { - otherOptions += "-debug:full "; + // The Windows PDB writer needs the native DiaSymReader, so only portable + // PDBs work on other platforms. Roslyn 2.x+ falls back to portable on its + // own; Roslyn 1.x needs the explicit request. + if (!OperatingSystem.IsWindows() && (flags & CompilerOptions.UseRoslynMask) != 0) + { + otherOptions += "-debug:portable "; + } + else + { + otherOptions += "-debug:full "; + } } else { @@ -764,9 +823,14 @@ namespace System.Runtime.CompilerServices Assert.Ignore($"Compilation with mcs ignored: test directory '{testBasePath}' needs to be checked out separately." + Environment.NewLine + $"git clone https://github.com/icsharpcode/ILSpy-tests \"{testBasePath}\""); } - string mcsPath = (flags & CompilerOptions.UseMcsMask) switch { - CompilerOptions.UseMcs5_23 => Path.Combine(testBasePath, @"mcs\5.23\bin\mcs.bat"), - _ => Path.Combine(testBasePath, @"mcs\2.6.4\bin\gmcs.bat") + // On Windows the submodule's bat launchers run the bundled Windows-native Mono; + // elsewhere the bundled compilers (managed .NET Framework assemblies) are hosted + // by the system Mono runtime via WrapCompiler. + string mcsPath = (flags & CompilerOptions.UseMcsMask, OperatingSystem.IsWindows()) switch { + (CompilerOptions.UseMcs5_23, true) => Path.Combine(testBasePath, @"mcs\5.23\bin\mcs.bat"), + (CompilerOptions.UseMcs5_23, false) => Path.Combine(testBasePath, "mcs", "5.23", "lib", "mono", "4.5", "mcs.exe"), + (_, true) => Path.Combine(testBasePath, @"mcs\2.6.4\bin\gmcs.bat"), + (_, false) => Path.Combine(testBasePath, "mcs", "2.6.4", "lib", "mono", "2.0", "gmcs.exe") }; string otherOptions = " -unsafe -o" + (flags.HasFlag(CompilerOptions.Optimize) ? "+ " : "- "); @@ -797,8 +861,7 @@ namespace System.Runtime.CompilerServices otherOptions += " \"-d:" + string.Join(";", preprocessorSymbols) + "\" "; } - var command = Cli.Wrap(mcsPath) - .WithArguments($"{otherOptions}-out:\"{Path.GetFullPath(results.PathToAssembly)}\" {string.Join(" ", sourceFileNames.Select(fn => '"' + Path.GetFullPath(fn) + '"'))}") + var command = WrapCompiler(mcsPath, $"{otherOptions}-out:\"{Path.GetFullPath(results.PathToAssembly)}\" {string.Join(" ", sourceFileNames.Select(fn => '"' + Path.GetFullPath(fn) + '"'))}") .WithValidation(CommandResultValidation.None); //Console.WriteLine($"\"{command.TargetFilePath}\" {command.Arguments}");