From 916fb01169b4dbe6baf48d4718819877af69ca85 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sat, 13 Dec 2025 14:16:36 -0500 Subject: [PATCH 1/3] Use a wrapper project to build the source project. --- .../Environments/Runtimes/CoreRuntime.cs | 9 +- .../Helpers/FrameworkVersionHelper.cs | 41 --------- .../Toolchains/CsProj/CsProjGenerator.cs | 84 +++++++++++-------- .../MonoAotLLVM/MonoAotLLVMGenerator.cs | 2 +- .../Toolchains/MonoWasm/WasmGenerator.cs | 2 +- .../Toolchains/NativeAot/Generator.cs | 2 +- .../DisassemblyDiagnoserTests.cs | 9 +- 7 files changed, 62 insertions(+), 87 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs index d2538cf055..4415530cbc 100644 --- a/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs +++ b/src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs @@ -232,7 +232,7 @@ private static CoreRuntime GetPlatformSpecific(CoreRuntime fallback, Assembly? a ? new CoreRuntime(fallback.RuntimeMoniker, $"{fallback.MsBuildMoniker}-{platform}", fallback.Name) : fallback; - internal static bool TryGetTargetPlatform(Assembly? assembly, [NotNullWhen(true)] out string? platform) + private static bool TryGetTargetPlatform(Assembly? assembly, [NotNullWhen(true)] out string? platform) { platform = null; @@ -252,11 +252,8 @@ internal static bool TryGetTargetPlatform(Assembly? assembly, [NotNullWhen(true) if (platformNameProperty is null) return false; - if (platformNameProperty.GetValue(attributeInstance) is not string platformName) - return false; - - platform = platformName; - return true; + platform = platformNameProperty.GetValue(attributeInstance) as string; + return platform.IsNotBlank(); } } } diff --git a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs index 0205c857ad..38a14f8973 100644 --- a/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs +++ b/src/BenchmarkDotNet/Helpers/FrameworkVersionHelper.cs @@ -1,10 +1,8 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Versioning; -using BenchmarkDotNet.Environments; using Microsoft.Win32; namespace BenchmarkDotNet.Helpers @@ -124,44 +122,5 @@ private static bool IsDeveloperPackInstalled(string version) => Directory.Exists Environment.Is64BitOperatingSystem ? Environment.SpecialFolder.ProgramFilesX86 : Environment.SpecialFolder.ProgramFiles); - - internal static string? GetTfm(Assembly assembly) - { - // We don't support exotic frameworks like Silverlight, WindowsPhone, Xamarin.Mac, etc. - const string CorePrefix = ".NETCoreApp,Version=v"; - const string FrameworkPrefix = ".NETFramework,Version=v"; - const string StandardPrefix = ".NETStandard,Version=v"; - - // Look for a TargetFrameworkAttribute with a supported Framework version. - string? framework = assembly.GetCustomAttribute()?.FrameworkName; - if (TryParseVersion(CorePrefix, out var version)) - { - return version.Major < 5 - ? $"netcoreapp{version.Major}.{version.Minor}" - : CoreRuntime.TryGetTargetPlatform(assembly, out var platform) - ? $"net{version.Major}.{version.Minor}-{platform}" - : $"net{version.Major}.{version.Minor}"; - } - if (TryParseVersion(FrameworkPrefix, out version)) - { - return version.Build > 0 - ? $"net{version.Major}{version.Minor}{version.Build}" - : $"net{version.Major}{version.Minor}"; - } - if (!TryParseVersion(StandardPrefix, out version)) - { - return $"netstandard{version.Major}.{version.Minor}"; - } - - // TargetFrameworkAttribute not found, or the assembly targeted a framework we don't support. - return null; - - bool TryParseVersion(string prefix, [NotNullWhen(true)] out Version? version) - { - version = null; - return framework?.StartsWith(prefix) == true - && Version.TryParse(framework[prefix.Length..], out version); - } - } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index fd0640a9d0..79cb676026 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -71,9 +71,9 @@ protected override void GenerateBuildScript(BuildPartition buildPartition, Artif var content = new StringBuilder(300) .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, projectFilePath)}") - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, projectFilePath, TargetFrameworkMoniker)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition, projectFilePath, TargetFrameworkMoniker)}") .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath)}") - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, TargetFrameworkMoniker)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, TargetFrameworkMoniker)}") .ToString(); File.WriteAllText(artifactsPaths.BuildScriptFilePath, content); @@ -105,37 +105,63 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts // Integration tests are built without dependencies, so we skip gathering dlls. if (!buildPartition.ForcedNoDependenciesForIntegrationTests) { - GatherReferences(projectFile.FullName, buildPartition, artifactsPaths, logger); + GatherReferences(buildPartition, artifactsPaths, logger); } } - protected void GatherReferences(string projectFilePath, BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) + private static string GetDllGathererPath(string filePath) + => Path.Combine(Path.GetDirectoryName(filePath), $"DllGatherer{Path.GetExtension(filePath)}"); + + protected void GatherReferences(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) { - // Build the original project then reference all of the built dlls. - BuildResult buildResult = BuildProject(TargetFrameworkMoniker); + // Copy csproj template without the generated C# file to build the original project for all necessary runtime dlls. + // We can't just build the original project directly because it could be a library project, so we need an exe project to reference it. + var xmlDoc = new XmlDocument(); + xmlDoc.Load(artifactsPaths.ProjectFilePath); + var projectElement = xmlDoc.DocumentElement; + var compileNode = projectElement.SelectSingleNode("ItemGroup/Compile"); + string emptyMainFile = GetDllGathererPath(artifactsPaths.ProgramCodePath); + compileNode.Attributes["Include"].Value = emptyMainFile; + string gathererProject = GetDllGathererPath(artifactsPaths.ProjectFilePath); + xmlDoc.Save(gathererProject); + + // Generate a C# file with an empty Main method to satisfy the exe build. + File.WriteAllText(emptyMainFile, """ + namespace BenchmarkDotNet.Autogenerated + { + public class UniqueProgramName + { + public static int Main(string[] args) + { + return 0; + } + } + } + """); - // The build could fail because the project doesn't have a tfm that matches the runtime, e.g. netstandard2.0 vs net10.0, - // So we try to get the actual tfm of the assembly and build again. - if (!buildResult.IsBuildSuccess - && FrameworkVersionHelper.GetTfm(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type.Assembly) is { } actualTfm - && actualTfm != TargetFrameworkMoniker) - { - buildResult = BuildProject(actualTfm); - } + // Build the original project then reference all of the built dlls. + BuildResult buildResult = new DotNetCliCommand( + CliPath, + gathererProject, + TargetFrameworkMoniker, + null, + GenerateResult.Success(artifactsPaths, []), + logger, + buildPartition, + [], + buildPartition.Timeout + ).RestoreThenBuild(); if (!buildResult.IsBuildSuccess) { - if (!buildResult.TryToExplainFailureReason(out string reason)) - { - reason = buildResult.ErrorMessage; - } - logger.WriteLineWarning($"Failed to build source project to obtain dll references. Moving forward without it. Reason: {reason}"); - return; + throw buildResult.TryToExplainFailureReason(out string reason) + ? new Exception(reason) + : new Exception(buildResult.ErrorMessage); } - var xmlDoc = new XmlDocument(); + xmlDoc = new XmlDocument(); xmlDoc.Load(artifactsPaths.ProjectFilePath); - XmlElement projectElement = xmlDoc.DocumentElement; + projectElement = xmlDoc.DocumentElement; var itemGroup = xmlDoc.CreateElement("ItemGroup"); projectElement.AppendChild(itemGroup); foreach (var assemblyFile in Directory.GetFiles(artifactsPaths.BinariesDirectoryPath, "*.dll")) @@ -157,20 +183,6 @@ protected void GatherReferences(string projectFilePath, BuildPartition buildPart } xmlDoc.Save(artifactsPaths.ProjectFilePath); - - BuildResult BuildProject(string tfm) - => new DotNetCliCommand( - CliPath, - projectFilePath, - tfm, - null, - GenerateResult.Success(artifactsPaths, []), - logger, - buildPartition, - [], - buildPartition.Timeout - ) - .RestoreThenBuild(); } /// diff --git a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs index 242d5d4269..ac58acd3d0 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs @@ -52,7 +52,7 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts File.WriteAllText(artifactsPaths.ProjectFilePath, content); - GatherReferences(projectFile.FullName, buildPartition, artifactsPaths, logger); + GatherReferences(buildPartition, artifactsPaths, logger); } protected override string GetPublishDirectoryPath(string buildArtifactsDirectoryPath, string configuration) diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs index e340e994f3..d1c6b41bb4 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -63,7 +63,7 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths File.WriteAllText(artifactsPaths.ProjectFilePath, content); - GatherReferences(projectFile.FullName, buildPartition, artifactsPaths, logger); + GatherReferences(buildPartition, artifactsPaths, logger); } protected void GenerateLinkerDescriptionFile(ArtifactsPaths artifactsPaths) diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs index 76dda258a4..9428245127 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs @@ -119,7 +119,7 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts File.WriteAllText(artifactsPaths.ProjectFilePath, GenerateProjectForNuGetBuild(projectFile, buildPartition, artifactsPaths, logger)); - GatherReferences(projectFile, buildPartition, artifactsPaths, logger); + GatherReferences(buildPartition, artifactsPaths, logger); GenerateReflectionFile(artifactsPaths); } diff --git a/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs b/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs index 6f2c05608b..62e76ddbb1 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/DisassemblyDiagnoserTests.cs @@ -173,7 +173,14 @@ private IConfig CreateConfig(Jit jit, Platform platform, IToolchain toolchain, I .AddJob(Job.Dry.WithJit(jit) .WithPlatform(platform) .WithToolchain(toolchain) - .WithStrategy(runStrategy)) + .WithStrategy(runStrategy) + // Ensure the build goes through the full process and doesn't build without dependencies like most of the integration tests do. +#if RELEASE + .WithCustomBuildConfiguration("Release") +#else + .WithCustomBuildConfiguration("Debug") +#endif + ) .AddLogger(DefaultConfig.Instance.GetLoggers().ToArray()) .AddColumnProvider(DefaultColumnProviders.Instance) .AddDiagnoser(disassemblyDiagnoser) From 39ee0543be4ffede20724464bb0b2bbfdcdfc957 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sat, 13 Dec 2025 17:01:38 -0500 Subject: [PATCH 2/3] Use default template to build for references. --- .../Toolchains/CsProj/CsProjGenerator.cs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index 79cb676026..295a9d7f93 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -81,6 +81,19 @@ protected override void GenerateBuildScript(BuildPartition buildPartition, Artif [SuppressMessage("ReSharper", "StringLiteralTypo")] // R# complains about $variables$ protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) + { + File.WriteAllText(artifactsPaths.ProjectFilePath, + GenerateBuildProject(buildPartition, artifactsPaths, logger) + ); + + // Integration tests are built without dependencies, so we skip gathering dlls. + if (!buildPartition.ForcedNoDependenciesForIntegrationTests) + { + GatherReferences(buildPartition, artifactsPaths, logger); + } + } + + private string GenerateBuildProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) { var benchmark = buildPartition.RepresentativeBenchmarkCase; var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger); @@ -89,7 +102,7 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts xmlDoc.Load(projectFile.FullName); var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); - var content = new StringBuilder(ResourceHelper.LoadTemplate("CsProj.txt")) + return new StringBuilder(ResourceHelper.LoadTemplate("CsProj.txt")) .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) .Replace("$CSPROJPATH$", projectFile.FullName) @@ -99,14 +112,6 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts .Replace("$COPIEDSETTINGS$", customProperties) .Replace("$SDKNAME$", sdkName) .ToString(); - - File.WriteAllText(artifactsPaths.ProjectFilePath, content); - - // Integration tests are built without dependencies, so we skip gathering dlls. - if (!buildPartition.ForcedNoDependenciesForIntegrationTests) - { - GatherReferences(buildPartition, artifactsPaths, logger); - } } private static string GetDllGathererPath(string filePath) @@ -114,18 +119,19 @@ private static string GetDllGathererPath(string filePath) protected void GatherReferences(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) { - // Copy csproj template without the generated C# file to build the original project for all necessary runtime dlls. + // Create a project using the default template to build the original project for all necessary runtime dlls. // We can't just build the original project directly because it could be a library project, so we need an exe project to reference it. var xmlDoc = new XmlDocument(); - xmlDoc.Load(artifactsPaths.ProjectFilePath); + xmlDoc.LoadXml(GenerateBuildProject(buildPartition, artifactsPaths, logger)); var projectElement = xmlDoc.DocumentElement; + + // Replace the default C# file with an empty Main method to satisfy the exe build. var compileNode = projectElement.SelectSingleNode("ItemGroup/Compile"); string emptyMainFile = GetDllGathererPath(artifactsPaths.ProgramCodePath); compileNode.Attributes["Include"].Value = emptyMainFile; string gathererProject = GetDllGathererPath(artifactsPaths.ProjectFilePath); xmlDoc.Save(gathererProject); - // Generate a C# file with an empty Main method to satisfy the exe build. File.WriteAllText(emptyMainFile, """ namespace BenchmarkDotNet.Autogenerated { From b32181c37ee649358ff500e8b8505e12dbb2c0c3 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sun, 14 Dec 2025 03:18:19 -0500 Subject: [PATCH 3/3] Don't remove ProjectReference outside of Mono. --- src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index 295a9d7f93..eca1b60df6 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -14,6 +14,7 @@ using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Toolchains.Mono; using BenchmarkDotNet.Toolchains.Results; using JetBrains.Annotations; @@ -183,7 +184,8 @@ public static int Main(string[] args) } // Mono80IsSupported test fails when BenchmarkDotNet is restored for net9.0 if we don't remove the ProjectReference. - if (XUnitHelper.IsIntegrationTest.Value) + // We still need to preserve the ProjectReference in every other case for disassembly, though. + if (XUnitHelper.IsIntegrationTest.Value && this is MonoGenerator) { projectElement.RemoveChild(projectElement.SelectSingleNode("ItemGroup/ProjectReference").ParentNode); }