diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index 3fe98605ab2b..bdaf0176fdaa 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -65,6 +65,9 @@ Additionally, the implicit project file has the following customizations: This avoids including files like `./**/*.resx` in simple file-based apps where users usually don't expect that. See [multiple files](#multiple-files) for more details. + - `AssemblyName` and `RootNamespace` are set to the entry-point file name without extension (e.g., `Program` for `Program.cs`) + to ensure the binary name and generated code namespaces are not affected by the virtual project file name (which includes the `.cs` extension). + ## Grow up When file-based programs reach an inflection point where build customizations in a project file are needed, diff --git a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs index 8960eabd51cc..9504d64da1dd 100644 --- a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs +++ b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs @@ -75,7 +75,7 @@ public override RunApiOutput Execute() builder.CreateProjectInstance( new ProjectCollection(), errorReporter, - out _, + out var project, out var evaluatedDirectives, validateAllDirectives: true); @@ -91,6 +91,7 @@ public override RunApiOutput Execute() return new RunApiOutput.Project { Content = csprojWriter.ToString(), + ProjectPath = project.FullPath, Diagnostics = diagnostics.ToImmutableArray(), }; } @@ -163,6 +164,7 @@ public sealed class Error : RunApiOutput public sealed class Project : RunApiOutput { public required string Content { get; init; } + public required string ProjectPath { get; init; } public required ImmutableArray Diagnostics { get; init; } } diff --git a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs index 94e889267f86..d32604edcfeb 100644 --- a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs +++ b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs @@ -7,6 +7,7 @@ namespace Microsoft.DotNet.Cli.Commands.Run; partial class CSharpCompilerCommand { private IEnumerable GetCscArguments( + string fileName, string fileNameWithoutExtension, string objDir, string binDir) @@ -202,7 +203,7 @@ private IEnumerable GetCscArguments( "/deterministic+", "/langversion:14.0", "/features:FileBasedProgram", - $"/analyzerconfig:{objDir}/{fileNameWithoutExtension}.GeneratedMSBuildEditorConfig.editorconfig", + $"/analyzerconfig:{objDir}/{fileName}.GeneratedMSBuildEditorConfig.editorconfig", $"/analyzerconfig:{SdkPath}/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig", $"/analyzer:{SdkPath}/Sdks/Microsoft.NET.Sdk/targets/../analyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll", $"/analyzer:{SdkPath}/Sdks/Microsoft.NET.Sdk/targets/../analyzers/Microsoft.CodeAnalysis.NetAnalyzers.dll", @@ -215,9 +216,9 @@ private IEnumerable GetCscArguments( $"/analyzer:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/analyzers/dotnet/cs/System.Text.Json.SourceGeneration.dll", $"/analyzer:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/analyzers/dotnet/cs/System.Text.RegularExpressions.Generator.dll", $"{EntryPointFileFullPath}", - $"{objDir}/{fileNameWithoutExtension}.GlobalUsings.g.cs", + $"{objDir}/{fileName}.GlobalUsings.g.cs", $"{objDir}/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs", - $"{objDir}/{fileNameWithoutExtension}.AssemblyInfo.cs", + $"{objDir}/{fileName}.AssemblyInfo.cs", "/warnaserror+:NU1605,SYSLIB0011", ]; } diff --git a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs index 675a638def76..d1c4c0c96a68 100644 --- a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs +++ b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs @@ -210,6 +210,7 @@ private void PrepareAuxiliaryFiles(out string rspPath) } string fileDirectory = Path.GetDirectoryName(EntryPointFileFullPath) ?? string.Empty; + string fileName = Path.GetFileName(EntryPointFileFullPath); string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(EntryPointFileFullPath); // Note that Release builds won't go through this optimized code path because `-c Release` translates to global property `Configuration=Release` @@ -231,7 +232,7 @@ private void PrepareAuxiliaryFiles(out string rspPath) """); } - string globalUsings = Path.Join(objDir, $"{fileNameWithoutExtension}.GlobalUsings.g.cs"); + string globalUsings = Path.Join(objDir, $"{fileName}.GlobalUsings.g.cs"); if (ShouldEmit(globalUsings)) { File.WriteAllText(globalUsings, /* lang=C#-test */ """ @@ -247,7 +248,7 @@ private void PrepareAuxiliaryFiles(out string rspPath) """); } - string assemblyInfo = Path.Join(objDir, $"{fileNameWithoutExtension}.AssemblyInfo.cs"); + string assemblyInfo = Path.Join(objDir, $"{fileName}.AssemblyInfo.cs"); if (ShouldEmit(assemblyInfo)) { File.WriteAllText(assemblyInfo, /* lang=C#-test */ $""" @@ -277,7 +278,7 @@ private void PrepareAuxiliaryFiles(out string rspPath) """); } - string editorconfig = Path.Join(objDir, $"{fileNameWithoutExtension}.GeneratedMSBuildEditorConfig.editorconfig"); + string editorconfig = Path.Join(objDir, $"{fileName}.GeneratedMSBuildEditorConfig.editorconfig"); if (ShouldEmit(editorconfig)) { File.WriteAllText(editorconfig, $""" @@ -366,6 +367,7 @@ private void PrepareAuxiliaryFiles(out string rspPath) if (ShouldEmit(rspPath)) { IEnumerable args = GetCscArguments( + fileName: fileName, fileNameWithoutExtension: fileNameWithoutExtension, objDir: objDir, binDir: binDir); diff --git a/src/Dotnet.Watch/Watch/Build/ProjectGraphFactory.cs b/src/Dotnet.Watch/Watch/Build/ProjectGraphFactory.cs index 79aada5750ab..71f7fc7f1b02 100644 --- a/src/Dotnet.Watch/Watch/Build/ProjectGraphFactory.cs +++ b/src/Dotnet.Watch/Watch/Build/ProjectGraphFactory.cs @@ -107,8 +107,9 @@ private ProjectInstance CreateProjectInstance(string projectPath, Dictionary diff --git a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs index 2356b742ea2c..12d818f8eac1 100644 --- a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs +++ b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs @@ -85,7 +85,7 @@ internal static string GetArtifactsPath(string entryPointFileFullPath) } public static string GetVirtualProjectPath(string entryPointFilePath) - => Path.ChangeExtension(entryPointFilePath, ".csproj"); + => entryPointFilePath + ".csproj"; /// /// Obtains a temporary subdirectory for file-based app artifacts, e.g., /tmp/dotnet/runfile/. @@ -375,7 +375,7 @@ ProjectRootElement CreateProjectRootElement(string projectFileText, ProjectColle using var reader = new StringReader(projectFileText); using var xmlReader = XmlReader.Create(reader); var projectRoot = ProjectRootElement.Create(xmlReader, projectCollection); - projectRoot.FullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj"); + projectRoot.FullPath = GetVirtualProjectPath(EntryPointFileFullPath); return projectRoot; } } @@ -465,6 +465,7 @@ internal static void WriteProjectFile( if (isVirtualProject) { Debug.Assert(!string.IsNullOrWhiteSpace(artifactsPath)); + Debug.Assert(entryPointFilePath is not null); // Note that ArtifactsPath needs to be specified before Sdk.props // (usually it's recommended to specify it in Directory.Build.props @@ -475,8 +476,10 @@ internal static void WriteProjectFile( false {EscapeValue(artifactsPath)} - artifacts/$(MSBuildProjectName) - artifacts/$(MSBuildProjectName) + {EscapeValue(Path.GetFileNameWithoutExtension(entryPointFilePath))} + $(AssemblyName) + artifacts/$(AssemblyName) + artifacts/$(AssemblyName) true {CSharpDirective.IncludeOrExclude.DefaultMappingString} false diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.props b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.props index ccb497097cd2..98363805e375 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.props +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.props @@ -131,7 +131,7 @@ Copyright (c) .NET Foundation. All rights reserved. - <_ImplicitFileBasedProgramUserSecretsId Condition="'$(FileBasedProgram)' == 'true'">$(MSBuildProjectName)-$([MSBuild]::StableStringHash($(MSBuildProjectFullPath.ToLowerInvariant()), 'Sha256')) + <_ImplicitFileBasedProgramUserSecretsId Condition="'$(FileBasedProgram)' == 'true'">$(AssemblyName)-$([MSBuild]::StableStringHash($(MSBuildProjectFullPath.ToLowerInvariant()), 'Sha256')) $(_ImplicitFileBasedProgramUserSecretsId) diff --git a/test/dotnet-watch.Tests/Build/ProjectGraphFactoryTests.cs b/test/dotnet-watch.Tests/Build/ProjectGraphFactoryTests.cs index 755009e2afe9..237c860329c8 100644 --- a/test/dotnet-watch.Tests/Build/ProjectGraphFactoryTests.cs +++ b/test/dotnet-watch.Tests/Build/ProjectGraphFactoryTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.FileBasedPrograms; +using Microsoft.DotNet.ProjectTools; namespace Microsoft.DotNet.Watch.UnitTests; @@ -45,7 +46,7 @@ public void VirtualProject() Assert.NotNull(graph); var root = graph.Graph.GraphRoots.Single(); - Assert.Equal(Path.ChangeExtension(entryPointFilePath, ".csproj"), root.ProjectInstance.FullPath); + Assert.Equal(VirtualProjectBuilder.GetVirtualProjectPath(entryPointFilePath), root.ProjectInstance.FullPath); } [Fact] @@ -97,7 +98,7 @@ public void VirtualProject_ProjectDirective() Assert.NotNull(graph); AssertEx.SequenceEqual( - [projectPath, Path.ChangeExtension(entryPointFilePath, ".csproj")], + [projectPath, VirtualProjectBuilder.GetVirtualProjectPath(entryPointFilePath)], graph.Graph.ProjectNodesTopologicallySorted.Select(p => p.ProjectInstance.FullPath)); } } diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs index adcb13515a90..fc2676b8d35d 100644 --- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs +++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs @@ -982,7 +982,7 @@ public void UserSecretsId_Overridden_ViaDirectoryBuildProps() [Theory, CombinatorialData] public void UserSecretsId_Overridden_SameAsImplicit(bool hasDirective, bool hasDirectiveBuildProps) { - const string implicitValue = "$(MSBuildProjectName)-$([MSBuild]::StableStringHash($(MSBuildProjectFullPath.ToLowerInvariant()), 'Sha256'))"; + const string implicitValue = "$(AssemblyName)-$([MSBuild]::StableStringHash($(MSBuildProjectFullPath.ToLowerInvariant()), 'Sha256'))"; var testInstance = _testAssetsManager.CreateTestDirectory(); File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index c908a7142cf2..d16b5f8fafcf 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -1903,6 +1903,23 @@ public void EmbeddedResource_AlongsideProj([CombinatorialValues("sln", "slnx", " .And.HaveStdOut(considered ? "Resource not found" : "[MyString, TestValue]"); } + [Fact] + public void Restore_NonExistentPackage() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programFile, """ + #:package Microsoft.ThisPackageDoesNotExist@1.0.0 + Console.WriteLine(); + """); + + new DotnetCommand(Log, "restore", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdOutContaining("Program.cs.csproj : error NU1101"); + } + [Fact] public void NoRestore_01() { @@ -2121,7 +2138,7 @@ class C; .Execute() .Should().Fail() .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, - Path.ChangeExtension(programFile, ".csproj"), + VirtualProjectBuilder.GetVirtualProjectPath(programFile), ToolsetInfo.CurrentTargetFrameworkVersion, "Library")); } @@ -2167,7 +2184,7 @@ class C; .Execute() .Should().Fail() .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, - Path.ChangeExtension(programFile, ".csproj"), + VirtualProjectBuilder.GetVirtualProjectPath(programFile), ToolsetInfo.CurrentTargetFrameworkVersion, "Library")); } @@ -2196,7 +2213,7 @@ class C; .Execute() .Should().Fail() .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, - Path.ChangeExtension(programFile, ".csproj"), + VirtualProjectBuilder.GetVirtualProjectPath(programFile), ToolsetInfo.CurrentTargetFrameworkVersion, "Module")); } @@ -2317,7 +2334,7 @@ public void Build_AppContainerExe() .Execute() .Should().Fail() .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRun, - Path.ChangeExtension(programFile, ".csproj"), + VirtualProjectBuilder.GetVirtualProjectPath(programFile), ToolsetInfo.CurrentTargetFrameworkVersion, "AppContainerExe")); } @@ -3980,6 +3997,7 @@ namespace Microsoft.DotNet.Cli.Commands.Run; partial class CSharpCompilerCommand { private IEnumerable GetCscArguments( + string fileName, string fileNameWithoutExtension, string objDir, string binDir) @@ -4062,13 +4080,20 @@ private IEnumerable GetCscArguments( needsInterpolation = true; } - // Use variable file name. + // Use variable file path. if (rewritten.Contains(entryPointPathNormalized, StringComparison.OrdinalIgnoreCase)) { rewritten = rewritten.Replace(entryPointPathNormalized, "{" + nameof(CSharpCompilerCommand.EntryPointFileFullPath) + "}", StringComparison.OrdinalIgnoreCase); needsInterpolation = true; } + // Use variable file name. + if (rewritten.Contains(fileName, StringComparison.OrdinalIgnoreCase)) + { + rewritten = rewritten.Replace(fileName, "{fileName}", StringComparison.OrdinalIgnoreCase); + needsInterpolation = true; + } + // Use variable program name. if (rewritten.Contains(programName, StringComparison.OrdinalIgnoreCase)) { @@ -5520,6 +5545,7 @@ public void Api() Console.WriteLine(); """); + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); new DotnetCommand(Log, "run-api") .WithStandardInput($$""" {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} @@ -5533,8 +5559,10 @@ public void Api() false /artifacts - artifacts/$(MSBuildProjectName) - artifacts/$(MSBuildProjectName) + Program + $(AssemblyName) + artifacts/$(AssemblyName) + artifacts/$(AssemblyName) true .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content false @@ -5579,7 +5607,7 @@ public void Api() - """)}},"Diagnostics":[]} + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics":[]} """); } @@ -5609,6 +5637,7 @@ public void Api_Evaluation() var bPath = Path.Join(testInstance.Path, "B.cs"); File.WriteAllText(bPath, ""); + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); new DotnetCommand(Log, "run-api") .WithStandardInput($$""" {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} @@ -5622,8 +5651,10 @@ public void Api_Evaluation() false /artifacts - artifacts/$(MSBuildProjectName) - artifacts/$(MSBuildProjectName) + A + $(AssemblyName) + artifacts/$(AssemblyName) + artifacts/$(AssemblyName) true .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content false @@ -5667,7 +5698,7 @@ public void Api_Evaluation() - """)}},"Diagnostics":[]} + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics":[]} """); } @@ -5681,6 +5712,7 @@ public void Api_Diagnostic_01() #:property LangVersion=preview """); + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); new DotnetCommand(Log, "run-api") .WithStandardInput($$""" {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} @@ -5694,8 +5726,10 @@ public void Api_Diagnostic_01() false /artifacts - artifacts/$(MSBuildProjectName) - artifacts/$(MSBuildProjectName) + Program + $(AssemblyName) + artifacts/$(AssemblyName) + artifacts/$(AssemblyName) true .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content false @@ -5734,7 +5768,7 @@ public void Api_Diagnostic_01() - """)}},"Diagnostics": + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics": [{"Location":{ "Path":{{ToJson(programPath)}}, "Span":{"Start":{"Line":1,"Character":0},"End":{"Line":1,"Character":30}{{nop}}}{{nop}}}, @@ -5752,6 +5786,7 @@ public void Api_Diagnostic_02() Console.WriteLine(); """); + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); new DotnetCommand(Log, "run-api") .WithStandardInput($$""" {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} @@ -5765,8 +5800,10 @@ public void Api_Diagnostic_02() false /artifacts - artifacts/$(MSBuildProjectName) - artifacts/$(MSBuildProjectName) + Program + $(AssemblyName) + artifacts/$(AssemblyName) + artifacts/$(AssemblyName) true .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content false @@ -5805,7 +5842,7 @@ public void Api_Diagnostic_02() - """)}},"Diagnostics": + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics": [{"Location":{ "Path":{{ToJson(programPath)}}, "Span":{"Start":{"Line":0,"Character":0},"End":{"Line":1,"Character":0}{{nop}}}{{nop}}}, @@ -6065,7 +6102,7 @@ public void MSBuildGet_Consistent(bool success, string subcommand, params string var projectBasedFiles = ReadFiles(); fileBasedResult.StdOut.Should().Be(projectBasedResult.StdOut); - fileBasedResult.StdErr.Should().Be(projectBasedResult.StdErr); + fileBasedResult.StdErr?.Replace("Program.cs.csproj", "Program.csproj").Should().Be(projectBasedResult.StdErr); fileBasedResult.ExitCode.Should().Be(projectBasedResult.ExitCode).And.Be(success ? 0 : 1); fileBasedFiles.Should().Equal(projectBasedFiles);