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..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; @@ -71,9 +72,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); @@ -81,6 +82,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 +103,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,43 +113,62 @@ 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(projectFile.FullName, 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); + // 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.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); + + 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")) @@ -151,26 +184,13 @@ protected void GatherReferences(string projectFilePath, BuildPartition buildPart } // 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); } 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)