diff --git a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs index 73150b149e39..f14a53261eb0 100644 --- a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs +++ b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs @@ -202,16 +202,25 @@ static bool HaveMatchingSizeAndTimeStamp(FileInfo sourceFile, FileInfo destinati } } - private void PrepareAuxiliaryFiles(out string rspPath) + internal static string WriteCscRspFile(string artifactsPath, ImmutableArray cscArguments) { - rspPath = Path.Join(ArtifactsPath, "csc.rsp"); + string rspPath = GetCscRspPath(artifactsPath); + File.WriteAllLines(rspPath, cscArguments); + return rspPath; + } + + private static string GetCscRspPath(string artifactsPath) => Path.Join(artifactsPath, "csc.rsp"); + private void PrepareAuxiliaryFiles(out string rspPath) + { if (!CscArguments.IsDefaultOrEmpty) { - File.WriteAllLines(rspPath, CscArguments); + rspPath = WriteCscRspFile(ArtifactsPath, CscArguments); return; } + rspPath = GetCscRspPath(ArtifactsPath); + // Note that Release builds won't go through this optimized code path because `-c Release` translates to global property `Configuration=Release` // and customizing global properties triggers a full MSBuild run. string objDir = Path.Join(ArtifactsPath, "obj", "debug"); diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index 0011c2604535..2282adbd535c 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -315,9 +315,15 @@ public override int Execute() // Then do a build. if (exitCode == 0 && !NoBuild && !evalOnly) { + var effectiveTargets = Builder.RequestedTargets is { Length: > 0 } requestedTargets + ? requestedTargets + : [Constants.Build, Constants.CoreCompile]; var buildRequest = new BuildRequestData( CreateProjectInstance(projectCollection), - targetsToBuild: Builder.RequestedTargets ?? [Constants.Build, Constants.CoreCompile]); + targetsToBuild: effectiveTargets, + hostServices: null, + // SkipNonexistentTargets: CoreCompile doesn't exist in the outer build of multi-target projects. + BuildRequestDataFlags.SkipNonexistentTargets); var buildResult = BuildManager.DefaultBuildManager.BuildRequest(buildRequest); if (buildResult.OverallResult != BuildResultCode.Success) @@ -339,6 +345,7 @@ public override int Execute() cache.CurrentEntry.Run = LastRunProperties; CacheCscArguments(cache, buildResult); + WriteCscRsp(cache); CollectAdditionalSources(cache, buildRequest.ProjectInstance); MarkBuildSuccess(cache); @@ -489,6 +496,17 @@ void ReuseInfoFromPreviousCacheEntry(CacheInfo cache) } } + void WriteCscRsp(CacheInfo cache) + { + if (cache.CurrentEntry.CscArguments.IsDefaultOrEmpty) + { + return; + } + + string rspPath = CSharpCompilerCommand.WriteCscRspFile(Builder.ArtifactsPath, cache.CurrentEntry.CscArguments); + Reporter.Verbose.WriteLine($"Wrote '{rspPath}'."); + } + bool CanSaveCache(ProjectInstance projectInstance) { if (!MSBuildUtilities.ConvertStringToBool(projectInstance.GetPropertyValue(FileBasedProgramCanSkipMSBuild), defaultValue: true)) diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index 8fe934968e4d..548e950d06be 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -5515,6 +5515,63 @@ public void CscOnly_AfterMSBuild_AuxiliaryFilesNotReused() Build(testInstance, BuildLevel.Csc, expectedOutput: "v3 "); } + /// + /// Verifies that csc.rsp is written to disk after a full MSBuild build, + /// so that IDEs can read it to create a virtual project. + /// + [Fact] + public void MSBuild_WritesCscRsp() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, """ + #:property Configuration=Release + Console.Write("Hello"); + """); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + // A build directive forces a full MSBuild build. + Build(testInstance, BuildLevel.All, expectedOutput: "Hello"); + + // csc.rsp should be written to disk after a full MSBuild build. + var rspPath = Path.Join(artifactsDir, "csc.rsp"); + File.Exists(rspPath).Should().BeTrue("csc.rsp should be written after a full MSBuild build"); + File.ReadAllLines(rspPath).Should().NotBeEmpty("csc.rsp should contain compiler arguments"); + } + + /// + /// Verifies that csc.rsp is written to disk after dotnet build file.cs, + /// so that IDEs can read it to create a virtual project. + /// + [Fact] + public void DotnetBuild_WritesCscRsp() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, """ + Console.Write("Hello"); + """); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "build", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + // csc.rsp should be written to disk after dotnet build. + var rspPath = Path.Join(artifactsDir, "csc.rsp"); + File.Exists(rspPath).Should().BeTrue("csc.rsp should be written after dotnet build file.cs"); + File.ReadAllLines(rspPath).Should().NotBeEmpty("csc.rsp should contain compiler arguments"); + } + /// /// Testing optimization when the NuGet cache is cleared between builds. /// See .