diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs index 6dd5bdd2e9..59e74a091a 100644 --- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs +++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs @@ -228,6 +228,7 @@ private void EnsureSymbolsForNativeRuntime(DiagnoserActionParameters parameters) // We install the tool in a dedicated directory in order to always use latest version and avoid issues with broken existing configs. string toolPath = Path.Combine(Path.GetTempPath(), "BenchmarkDotNet", "symbols"); DotNetCliCommand cliCommand = new ( + csProjPath: string.Empty, cliPath: cliPath, arguments: $"tool install dotnet-symbol --tool-path \"{toolPath}\"", generateResult: null, diff --git a/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs b/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs index 0c097f5bb0..e082d126ee 100644 --- a/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs +++ b/src/BenchmarkDotNet/Toolchains/ArtifactsPaths.cs @@ -4,7 +4,7 @@ namespace BenchmarkDotNet.Toolchains { public class ArtifactsPaths { - public static readonly ArtifactsPaths Empty = new ArtifactsPaths("", "", "", "", "", "", "", "", "", "", "", ""); + public static readonly ArtifactsPaths Empty = new ("", "", "", "", "", "", "", "", "", "", "", "", ""); [PublicAPI] public string RootArtifactsFolderPath { get; } [PublicAPI] public string BuildArtifactsDirectoryPath { get; } @@ -13,6 +13,7 @@ public class ArtifactsPaths [PublicAPI] public string ProgramCodePath { get; } [PublicAPI] public string AppConfigPath { get; } [PublicAPI] public string NuGetConfigPath { get; } + [PublicAPI] public string BuildForReferencesProjectFilePath { get; } [PublicAPI] public string ProjectFilePath { get; } [PublicAPI] public string BuildScriptFilePath { get; } [PublicAPI] public string ExecutablePath { get; } @@ -27,6 +28,7 @@ public ArtifactsPaths( string programCodePath, string appConfigPath, string nuGetConfigPath, + string buildForReferencesProjectFilePath, string projectFilePath, string buildScriptFilePath, string executablePath, @@ -40,6 +42,7 @@ public ArtifactsPaths( ProgramCodePath = programCodePath; AppConfigPath = appConfigPath; NuGetConfigPath = nuGetConfigPath; + BuildForReferencesProjectFilePath = buildForReferencesProjectFilePath; ProjectFilePath = projectFilePath; BuildScriptFilePath = buildScriptFilePath; ExecutablePath = executablePath; diff --git a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs index 7e91c36ff4..d614cf8912 100644 --- a/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs @@ -61,37 +61,64 @@ protected override string GetBuildArtifactsDirectoryPath(BuildPartition buildPar protected override string GetProjectFilePath(string buildArtifactsDirectoryPath) => Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Autogenerated.csproj"); + protected override string GetProjectFilePathForReferences(string buildArtifactsDirectoryPath) + => Path.Combine(buildArtifactsDirectoryPath, "BenchmarkDotNet.Autogenerated.ForReferences.csproj"); + protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration) => Path.Combine(buildArtifactsDirectoryPath, "bin", configuration, TargetFrameworkMoniker); protected override string GetIntermediateDirectoryPath(string buildArtifactsDirectoryPath, string configuration) => Path.Combine(buildArtifactsDirectoryPath, "obj", configuration, TargetFrameworkMoniker); - [SuppressMessage("ReSharper", "StringLiteralTypo")] // R# complains about $variables$ - protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) - { - var benchmark = buildPartition.RepresentativeBenchmarkCase; - var projectFile = GetProjectFilePath(benchmark.Descriptor.Type, logger); - - var xmlDoc = new XmlDocument(); - xmlDoc.Load(projectFile.FullName); - var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); - - var content = new StringBuilder(ResourceHelper.LoadTemplate("CsProj.txt")) + private string LoadCsProj(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, string projectFile, string customProperties, string sdkName) + => new StringBuilder(ResourceHelper.LoadTemplate("CsProj.txt")) .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) - .Replace("$CSPROJPATH$", projectFile.FullName) + .Replace("$CSPROJPATH$", projectFile) .Replace("$TFM$", TargetFrameworkMoniker) .Replace("$PROGRAMNAME$", artifactsPaths.ProgramName) - .Replace("$RUNTIMESETTINGS$", GetRuntimeSettings(benchmark.Job.Environment.Gc, buildPartition.Resolver)) + .Replace("$RUNTIMESETTINGS$", GetRuntimeSettings(buildPartition.RepresentativeBenchmarkCase.Job.Environment.Gc, buildPartition.Resolver)) .Replace("$COPIEDSETTINGS$", customProperties) .Replace("$CONFIGURATIONNAME$", buildPartition.BuildConfiguration) .Replace("$SDKNAME$", sdkName) .ToString(); + [SuppressMessage("ReSharper", "StringLiteralTypo")] // R# complains about $variables$ + protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) + { + var projectFile = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger); + + var xmlDoc = new XmlDocument(); + xmlDoc.Load(projectFile.FullName); + var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); + + GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + + var content = LoadCsProj(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + File.WriteAllText(artifactsPaths.ProjectFilePath, content); } + protected void GenerateBuildForReferencesProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, string projectFile, string customProperties, string sdkName) + { + var content = LoadCsProj(buildPartition, artifactsPaths, projectFile, customProperties, sdkName); + + // We don't include the generated .notcs file when building the reference dlls, only in the final build. + var xmlDoc = new XmlDocument(); + xmlDoc.Load(new StringReader(content)); + XmlElement projectElement = xmlDoc.DocumentElement; + projectElement.RemoveChild(projectElement.SelectSingleNode("ItemGroup/Compile").ParentNode); + + var startupObjectElement = projectElement.SelectSingleNode("PropertyGroup/StartupObject"); + startupObjectElement.ParentNode.RemoveChild(startupObjectElement); + + // We need to change the output type to library since we're only compiling for dlls. + var outputTypeElement = projectElement.SelectSingleNode("PropertyGroup/OutputType"); + outputTypeElement.InnerText = "Library"; + + xmlDoc.Save(artifactsPaths.BuildForReferencesProjectFilePath); + } + /// /// returns an MSBuild string that defines Runtime settings /// diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs index 7a7afc3184..1dccde39b6 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using System.Xml; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; @@ -25,16 +27,34 @@ public DotNetCliBuilder(string targetFrameworkMoniker, string? customDotNetCliPa public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) { - BuildResult buildResult = new DotNetCliCommand( - CustomDotNetCliPath, - string.Empty, - generateResult, - logger, - buildPartition, - Array.Empty(), - buildPartition.Timeout, - logOutput: LogOutput) + var cliCommand = new DotNetCliCommand( + generateResult.ArtifactsPaths.BuildForReferencesProjectFilePath, + CustomDotNetCliPath, + string.Empty, + generateResult, + logger, + buildPartition, + Array.Empty(), + buildPartition.Timeout, + logOutput: LogOutput); + + BuildResult buildResult; + // Integration tests are built without dependencies, so we skip the first step. + if (!buildPartition.ForcedNoDependenciesForIntegrationTests) + { + // We build the original project first to obtain all dlls. + buildResult = cliCommand.RestoreThenBuild(); + + if (!buildResult.IsBuildSuccess) + return buildResult; + + // After the dlls are built, we gather the assembly references, then build the benchmark project. + GatherReferences(generateResult.ArtifactsPaths); + } + + buildResult = cliCommand.WithCsProjPath(generateResult.ArtifactsPaths.ProjectFilePath) .RestoreThenBuild(); + if (buildResult.IsBuildSuccess && buildPartition.RepresentativeBenchmarkCase.Job.Environment.LargeAddressAware) { @@ -42,5 +62,34 @@ public BuildResult Build(GenerateResult generateResult, BuildPartition buildPart } return buildResult; } + + internal static void GatherReferences(ArtifactsPaths artifactsPaths) + { + var xmlDoc = new XmlDocument(); + xmlDoc.Load(artifactsPaths.ProjectFilePath); + XmlElement projectElement = xmlDoc.DocumentElement; + + // Add reference to every dll. + var itemGroup = xmlDoc.CreateElement("ItemGroup"); + projectElement.AppendChild(itemGroup); + foreach (var assemblyFile in Directory.GetFiles(artifactsPaths.BinariesDirectoryPath, "*.dll")) + { + var assemblyName = Path.GetFileNameWithoutExtension(assemblyFile); + // The dummy csproj was used to build the original project, but it also outputs a dll for itself which we need to ignore because it's not valid. + if (assemblyName == artifactsPaths.ProgramName) + { + continue; + } + var referenceElement = xmlDoc.CreateElement("Reference"); + itemGroup.AppendChild(referenceElement); + referenceElement.SetAttribute("Include", assemblyName); + var hintPath = xmlDoc.CreateElement("HintPath"); + referenceElement.AppendChild(hintPath); + var locationNode = xmlDoc.CreateTextNode(assemblyFile); + hintPath.AppendChild(locationNode); + } + + xmlDoc.Save(artifactsPaths.ProjectFilePath); + } } } diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs index e9078f293f..fdac302cea 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs @@ -16,6 +16,8 @@ namespace BenchmarkDotNet.Toolchains.DotNetCli { public class DotNetCliCommand { + [PublicAPI] public string CsProjPath { get; } + [PublicAPI] public string CliPath { get; } [PublicAPI] public string Arguments { get; } @@ -32,9 +34,10 @@ public class DotNetCliCommand [PublicAPI] public bool LogOutput { get; } - public DotNetCliCommand(string cliPath, string arguments, GenerateResult generateResult, ILogger logger, + public DotNetCliCommand(string csProjPath, string cliPath, string arguments, GenerateResult generateResult, ILogger logger, BuildPartition buildPartition, IReadOnlyList environmentVariables, TimeSpan timeout, bool logOutput = false) { + CsProjPath = csProjPath; CliPath = cliPath ?? DotNetCliCommandExecutor.DefaultDotNetCliPath.Value; Arguments = arguments; GenerateResult = generateResult; @@ -46,10 +49,13 @@ public DotNetCliCommand(string cliPath, string arguments, GenerateResult generat } public DotNetCliCommand WithArguments(string arguments) - => new (CliPath, arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput); + => new (CsProjPath, CliPath, arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput); + + public DotNetCliCommand WithCsProjPath(string csProjPath) + => new (csProjPath, CliPath, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput); public DotNetCliCommand WithCliPath(string cliPath) - => new (cliPath, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput); + => new (CsProjPath, cliPath, Arguments, GenerateResult, Logger, BuildPartition, EnvironmentVariables, Timeout, logOutput: LogOutput); [PublicAPI] public BuildResult RestoreThenBuild() @@ -71,12 +77,12 @@ public BuildResult RestoreThenBuild() if (BuildPartition.ForcedNoDependenciesForIntegrationTests) { var restoreResult = DotNetCliCommandExecutor.Execute(WithArguments( - GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-dependencies", "restore-no-deps", excludeOutput: true))); + GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, CsProjPath, $"{Arguments} --no-dependencies", "restore-no-deps", excludeOutput: true))); if (!restoreResult.IsSuccess) return BuildResult.Failure(GenerateResult, restoreResult.AllInformation); return DotNetCliCommandExecutor.Execute(WithArguments( - GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-restore --no-dependencies", "build-no-restore-no-deps", excludeOutput: true))) + GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, CsProjPath, $"{Arguments} --no-restore --no-dependencies", "build-no-restore-no-deps", excludeOutput: true))) .ToBuildResult(GenerateResult); } else @@ -118,7 +124,7 @@ public DotNetCliCommandResult AddPackages() { var executionTime = new TimeSpan(0); var stdOutput = new StringBuilder(); - foreach (var cmd in GetAddPackagesCommands(BuildPartition)) + foreach (var cmd in GetAddPackagesCommands(BuildPartition, CsProjPath)) { var result = DotNetCliCommandExecutor.Execute(WithArguments(cmd)); if (!result.IsSuccess) return result; @@ -130,31 +136,32 @@ public DotNetCliCommandResult AddPackages() public DotNetCliCommandResult Restore() => DotNetCliCommandExecutor.Execute(WithArguments( - GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, Arguments, "restore"))); + GetRestoreCommand(GenerateResult.ArtifactsPaths, BuildPartition, CsProjPath, Arguments, "restore"))); public DotNetCliCommandResult Build() => DotNetCliCommandExecutor.Execute(WithArguments( - GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, Arguments, "build"))); + GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, CsProjPath, Arguments, "build"))); public DotNetCliCommandResult BuildNoRestore() => DotNetCliCommandExecutor.Execute(WithArguments( - GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-restore", "build-no-restore"))); + GetBuildCommand(GenerateResult.ArtifactsPaths, BuildPartition, CsProjPath, $"{Arguments} --no-restore", "build-no-restore"))); public DotNetCliCommandResult Publish() => DotNetCliCommandExecutor.Execute(WithArguments( - GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, Arguments, "publish"))); + GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, CsProjPath, Arguments, "publish"))); // PublishNoBuildAndNoRestore was removed because we set --output in the build step. We use the implicit build included in the publish command. public DotNetCliCommandResult PublishNoRestore() => DotNetCliCommandExecutor.Execute(WithArguments( - GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, $"{Arguments} --no-restore", "publish-no-restore"))); + GetPublishCommand(GenerateResult.ArtifactsPaths, BuildPartition, CsProjPath, $"{Arguments} --no-restore", "publish-no-restore"))); - internal static IEnumerable GetAddPackagesCommands(BuildPartition buildPartition) - => GetNuGetAddPackageCommands(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver); + internal static IEnumerable GetAddPackagesCommands(BuildPartition buildPartition, string csProjPath) + => GetNuGetAddPackageCommands(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver, csProjPath); - internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) + internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string csProjPath, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) => new StringBuilder() .AppendArgument("restore") + .AppendArgument(string.IsNullOrEmpty(csProjPath) ? string.Empty : $"\"{csProjPath}\"") .AppendArgument(string.IsNullOrEmpty(artifactsPaths.PackagesDirectoryName) ? string.Empty : $"--packages \"{artifactsPaths.PackagesDirectoryName}\"") .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) @@ -163,9 +170,11 @@ internal static string GetRestoreCommand(ArtifactsPaths artifactsPaths, BuildPar .MaybeAppendOutputPaths(artifactsPaths, true, excludeOutput) .ToString(); - internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) + internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string csProjPath, string? extraArguments = null, string? binLogSuffix = null, bool excludeOutput = false) => new StringBuilder() - .AppendArgument($"build -c {buildPartition.BuildConfiguration}") // we don't need to specify TFM, our auto-generated project contains always single one + .AppendArgument("build") + .AppendArgument(string.IsNullOrEmpty(csProjPath) ? string.Empty : $"\"{csProjPath}\"") + .AppendArgument($"-c {buildPartition.BuildConfiguration}") // we don't need to specify TFM, our auto-generated project contains always single one .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) .AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration)) @@ -174,9 +183,11 @@ internal static string GetBuildCommand(ArtifactsPaths artifactsPaths, BuildParti .MaybeAppendOutputPaths(artifactsPaths, excludeOutput: excludeOutput) .ToString(); - internal static string GetPublishCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string? extraArguments = null, string? binLogSuffix = null) + internal static string GetPublishCommand(ArtifactsPaths artifactsPaths, BuildPartition buildPartition, string csProjPath, string? extraArguments = null, string? binLogSuffix = null) => new StringBuilder() - .AppendArgument($"publish -c {buildPartition.BuildConfiguration}") // we don't need to specify TFM, our auto-generated project contains always single one + .AppendArgument("publish") + .AppendArgument(string.IsNullOrEmpty(csProjPath) ? string.Empty : $"\"{csProjPath}\"") + .AppendArgument($"-c {buildPartition.BuildConfiguration}") // we don't need to specify TFM, our auto-generated project contains always single one .AppendArgument(GetCustomMsBuildArguments(buildPartition.RepresentativeBenchmarkCase, buildPartition.Resolver)) .AppendArgument(extraArguments) .AppendArgument(GetMandatoryMsBuildSettings(buildPartition.BuildConfiguration)) @@ -203,14 +214,14 @@ private static string GetCustomMsBuildArguments(BenchmarkCase benchmarkCase, IRe return string.Join(" ", msBuildArguments.Select(arg => arg.TextRepresentation)); } - private static IEnumerable GetNuGetAddPackageCommands(BenchmarkCase benchmarkCase, IResolver resolver) + private static IEnumerable GetNuGetAddPackageCommands(BenchmarkCase benchmarkCase, IResolver resolver, string csProjPath) { if (!benchmarkCase.Job.HasValue(InfrastructureMode.NuGetReferencesCharacteristic)) return Enumerable.Empty(); var nuGetRefs = benchmarkCase.Job.ResolveValue(InfrastructureMode.NuGetReferencesCharacteristic, resolver); - return nuGetRefs.Select(BuildAddPackageCommand); + return nuGetRefs.Select(nr => BuildAddPackageCommand(nr, csProjPath)); } private static string GetMandatoryMsBuildSettings(string buildConfiguration) @@ -228,11 +239,13 @@ private static string GetMandatoryMsBuildSettings(string buildConfiguration) return $"{NoMsBuildZombieProcesses} {EnforceOptimizations}"; } - private static string BuildAddPackageCommand(NuGetReference reference) + private static string BuildAddPackageCommand(NuGetReference reference, string csProjPath) { - var commandBuilder = new StringBuilder(); - commandBuilder.AppendArgument("add package"); - commandBuilder.AppendArgument(reference.PackageName); + var commandBuilder = new StringBuilder() + .AppendArgument("add") + .AppendArgument(string.IsNullOrEmpty(csProjPath) ? string.Empty : $"\"{csProjPath}\"") + .AppendArgument("package") + .AppendArgument(reference.PackageName); if (!string.IsNullOrWhiteSpace(reference.PackageVersion)) { commandBuilder.AppendArgument("-v"); diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs index 4348685369..ae50e1f9f6 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommandExecutor.cs @@ -157,6 +157,7 @@ private static string GetDefaultDotNetCliPath() internal static string GetSdkPath(string cliPath) { DotNetCliCommand cliCommand = new ( + csProjPath: string.Empty, cliPath: cliPath, arguments: "--info", generateResult: null, diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs index a432fbf881..ddf609d8df 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliGenerator.cs @@ -101,8 +101,10 @@ protected override void CopyAllRequiredFiles(ArtifactsPaths artifactsPaths) protected override void GenerateBuildScript(BuildPartition buildPartition, ArtifactsPaths artifactsPaths) { var content = new StringBuilder(300) - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition)}") - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetBuildCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath)}") .ToString(); File.WriteAllText(artifactsPaths.BuildScriptFilePath, content); diff --git a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs index 83158e0b1e..b8b6db96c7 100644 --- a/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs +++ b/src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliPublisher.cs @@ -25,14 +25,27 @@ public DotNetCliPublisher( private IReadOnlyList? EnvironmentVariables { get; } public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) - => new DotNetCliCommand( - CustomDotNetCliPath, - ExtraArguments, - generateResult, - logger, - buildPartition, - EnvironmentVariables, - buildPartition.Timeout) + { + var cliCommand = new DotNetCliCommand( + generateResult.ArtifactsPaths.BuildForReferencesProjectFilePath, + CustomDotNetCliPath, + ExtraArguments, + generateResult, + logger, + buildPartition, + EnvironmentVariables, + buildPartition.Timeout); + + // We build the original project first to obtain all dlls. + var buildResult = cliCommand.RestoreThenBuild(); + + if (!buildResult.IsBuildSuccess) + return buildResult; + + // After the dlls are built, we gather the assembly references, then build the benchmark project. + DotNetCliBuilder.GatherReferences(generateResult.ArtifactsPaths); + return cliCommand.WithCsProjPath(generateResult.ArtifactsPaths.ProjectFilePath) .RestoreThenBuildThenPublish(); + } } } diff --git a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs index cab9d811c4..067da24044 100644 --- a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs +++ b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs @@ -68,6 +68,13 @@ [PublicAPI] protected virtual string GetExecutableExtension() [PublicAPI] protected virtual string GetProjectFilePath(string buildArtifactsDirectoryPath) => string.Empty; + /// + /// returns a path to the auto-generated .csproj file that is used to build the reference dlls + /// + [PublicAPI] + protected virtual string GetProjectFilePathForReferences(string buildArtifactsDirectoryPath) + => string.Empty; + /// /// returns a list of artifacts that should be removed after running the benchmarks /// @@ -142,6 +149,7 @@ private ArtifactsPaths GetArtifactsPaths(BuildPartition buildPartition, string r appConfigPath: $"{executablePath}.config", nuGetConfigPath: Path.Combine(buildArtifactsDirectoryPath, "NuGet.config"), projectFilePath: GetProjectFilePath(buildArtifactsDirectoryPath), + buildForReferencesProjectFilePath: GetProjectFilePathForReferences(buildArtifactsDirectoryPath), buildScriptFilePath: Path.Combine(buildArtifactsDirectoryPath, $"{programName}{RuntimeInformation.ScriptFileExtension}"), executablePath: executablePath, programName: programName, diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs index 0686a322ac..9a32366989 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitArtifactsPath.cs @@ -17,6 +17,7 @@ public InProcessEmitArtifactsPath( baseArtifacts.AppConfigPath, baseArtifacts.NuGetConfigPath, baseArtifacts.ProjectFilePath, + baseArtifacts.BuildForReferencesProjectFilePath, baseArtifacts.BuildScriptFilePath, baseArtifacts.ExecutablePath, baseArtifacts.ProgramName, diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs index 18734fcf46..f4cd5bcc23 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs @@ -52,6 +52,7 @@ private ArtifactsPaths GetArtifactsPaths(BuildPartition buildPartition, string r appConfigPath: null, nuGetConfigPath: null, projectFilePath: null, + buildForReferencesProjectFilePath: null, buildScriptFilePath: null, executablePath: executablePath, programName: programName, diff --git a/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs b/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs index ff98540cbf..e450dcde9c 100644 --- a/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs +++ b/src/BenchmarkDotNet/Toolchains/Mono/MonoPublisher.cs @@ -25,14 +25,30 @@ public MonoPublisher(string customDotNetCliPath) private IReadOnlyList EnvironmentVariables { get; } public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger) - => new DotNetCliCommand( - CustomDotNetCliPath, - ExtraArguments, - generateResult, - logger, - buildPartition, - EnvironmentVariables, - buildPartition.Timeout) - .Publish().ToBuildResult(generateResult); + { + var cliCommand = new DotNetCliCommand( + generateResult.ArtifactsPaths.BuildForReferencesProjectFilePath, + CustomDotNetCliPath, + string.Empty, + generateResult, + logger, + buildPartition, + EnvironmentVariables, + buildPartition.Timeout); + + // We build the original project first to obtain all dlls. + var buildResult = cliCommand.RestoreThenBuild(); + + if (!buildResult.IsBuildSuccess) + return buildResult; + + // After the dlls are built, we gather the assembly references, then build the benchmark project. + DotNetCliBuilder.GatherReferences(generateResult.ArtifactsPaths); + return cliCommand + .WithArguments(ExtraArguments) + .WithCsProjPath(generateResult.ArtifactsPaths.ProjectFilePath) + .Publish() + .ToBuildResult(generateResult); + } } } diff --git a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs index 833c113d84..3bdffe9373 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs @@ -35,6 +35,8 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts xmlDoc.Load(projectFile.FullName); var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); + GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + string content = new StringBuilder(ResourceHelper.LoadTemplate("MonoAOTLLVMCsProj.txt")) .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs index 7c9aa8826f..3e138c94fc 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -49,6 +49,8 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths xmlDoc.Load(projectFile.FullName); var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); + GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + string content = new StringBuilder(ResourceHelper.LoadTemplate("WasmCsProj.txt")) .Replace("$PLATFORM$", buildPartition.Platform.ToConfig()) .Replace("$CODEFILENAME$", Path.GetFileName(artifactsPaths.ProgramCodePath)) diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs index bbeb0113d8..5154a26225 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Xml; using BenchmarkDotNet.ConsoleArguments; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; @@ -72,8 +73,10 @@ protected override void GenerateBuildScript(BuildPartition buildPartition, Artif string extraArguments = NativeAotToolchain.GetExtraArguments(runtimeIdentifier); var content = new StringBuilder(300) - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, extraArguments)}") - .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, artifactsPaths.BuildForReferencesProjectFilePath, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetRestoreCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, extraArguments)}") + .AppendLine($"call {CliPath ?? "dotnet"} {DotNetCliCommand.GetPublishCommand(artifactsPaths, buildPartition, artifactsPaths.ProjectFilePath, extraArguments)}") .ToString(); File.WriteAllText(artifactsPaths.BuildScriptFilePath, content); @@ -110,6 +113,14 @@ protected override void GenerateNuGetConfig(ArtifactsPaths artifactsPaths) protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger) { + var projectFile = GetProjectFilePath(buildPartition.RepresentativeBenchmarkCase.Descriptor.Type, logger); + + var xmlDoc = new XmlDocument(); + xmlDoc.Load(projectFile.FullName); + var (customProperties, sdkName) = GetSettingsThatNeedToBeCopied(xmlDoc, projectFile); + + GenerateBuildForReferencesProject(buildPartition, artifactsPaths, projectFile.FullName, customProperties, sdkName); + File.WriteAllText(artifactsPaths.ProjectFilePath, GenerateProjectForNuGetBuild(buildPartition, artifactsPaths, logger)); GenerateReflectionFile(artifactsPaths); }