diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index 3e0db6afaadf..6be1b9b74b3c 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -40,9 +40,11 @@ Additionally, the implicit project file has the following customizations: - The following are virtual only, i.e., not preserved after [converting to a project](#grow-up): - - `ArtifactsPath` is set to a [temp directory](#build-outputs). + - `ArtifactsPath` is set to a [temp directory](#build-outputs), + unless [artifacts output layout][artifacts-output] is enabled. - - `PublishDir` and `PackageOutputPath` are set to `./artifacts/` so the outputs of `dotnet publish` and `dotnet pack` are next to the file-based app. + - `PublishDir` and `PackageOutputPath` are set to `./artifacts/` so the outputs of `dotnet publish` and `dotnet pack` are next to the file-based app, + unless [artifacts output layout][artifacts-output] is enabled. - `RuntimeHostConfigurationOption`s are set for `EntryPointFilePath` and `EntryPointFileDirectoryPath` (except for `Publish` and `Pack` targets) which can be accessed in the app via `AppContext`: @@ -156,7 +158,9 @@ and the conversion process only copying the items that were included in the orig ## Build outputs -Build outputs are placed under a subdirectory whose name is hashed file path of the entry point +If [artifacts output layout][artifacts-output] is enabled, build outputs of the file-based app are placed there +(except caching markers which are placed in the global temp directory described next). +Otherwise, build outputs are placed under a subdirectory whose name is hashed file path of the entry point inside a temp or app data directory which should be owned by and unique to the current user per [runtime guidelines][temp-guidelines]. The subdirectory is created by the SDK CLI with permissions restricting access to it to the current user (`0700`) and the run fails if that is not possible. Note that it is possible for multiple users to run the same file-based program, however each user's run uses different build artifacts since the base directory is unique per user. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/VirtualProjectBuilder.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/VirtualProjectBuilder.cs index fc9548c650e1..fb63d848592b 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/VirtualProjectBuilder.cs +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/VirtualProjectBuilder.cs @@ -628,19 +628,16 @@ internal static void WriteProjectFile( Debug.Assert(!string.IsNullOrWhiteSpace(artifactsPath)); Debug.Assert(entryPointFilePath is not null); - // Note that ArtifactsPath needs to be specified before Sdk.props + // Note that FileBasedAppArtifactsPath needs to be specified before Sdk.props // (usually it's recommended to specify it in Directory.Build.props // but importing Sdk.props manually afterwards also works). writer.WriteLine($""" - false - {EscapeValue(artifactsPath)} + {EscapeValue(artifactsPath)} {EscapeValue(Path.GetFileNameWithoutExtension(entryPointFilePath))} $(AssemblyName) - artifacts/$(AssemblyName) - artifacts/$(AssemblyName) true {EscapeValue(entryPointFilePath)} {CSharpDirective.IncludeOrExclude.DefaultMappingString} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/sdk/UseArtifactsOutputPath.props b/src/Tasks/Microsoft.NET.Build.Tasks/sdk/UseArtifactsOutputPath.props index 1dd11c8d49ad..88425f9e864e 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/sdk/UseArtifactsOutputPath.props +++ b/src/Tasks/Microsoft.NET.Build.Tasks/sdk/UseArtifactsOutputPath.props @@ -15,7 +15,7 @@ Copyright (c) .NET Foundation. All rights reserved. If the .props file is not imported here, it will be imported from Microsoft.NET.DefaultOutputPaths.targets, so that artifacts output properties can be set directly in the project file too (only in that case they won't affect the intermediate output). --> + Condition="'$(UseArtifactsOutput)' == 'true' Or '$(ArtifactsPath)' != '' Or '$(FileBasedAppArtifactsPath)' != ''"/> true diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.DefaultArtifactsPath.props b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.DefaultArtifactsPath.props index d06c6aab0d1c..c76932819540 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.DefaultArtifactsPath.props +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.DefaultArtifactsPath.props @@ -41,6 +41,18 @@ Copyright (c) .NET Foundation. All rights reserved. <_ArtifactsPathLocationType>ProjectFolder + + + true + $(FileBasedAppArtifactsPath) + false + <_ArtifactsPathLocationType>FileBasedApp + + + artifacts/$(AssemblyName) + artifacts/$(AssemblyName) + + diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs index 27debf9ca339..f493602c273e 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs @@ -919,6 +919,121 @@ public void ArtifactsDirectory_Permissions() .Should().Be(actualMode, artifactsDir); } + [TestMethod] + public void ArtifactsPath() + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, s_program); + + var globalArtifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(globalArtifactsDir)) Directory.Delete(globalArtifactsDir, recursive: true); + + new DirectoryInfo(Path.Join(testInstance.Path, "artifacts")).Should().NotExist(); + new DirectoryInfo(Path.Join(testInstance.Path, "bin")).Should().NotExist(); + + new DotnetCommand(Log, "build", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(globalArtifactsDir).EnumerateDirectories().Should().NotBeEmpty(); + new FileInfo(Path.Join(globalArtifactsDir, "bin", "debug", "Program.dll")).Should().Exist(); + new DirectoryInfo(Path.Join(globalArtifactsDir, "bin")).EnumerateDirectories().Select(d => d.Name).Should().BeEquivalentTo(["debug"]); + new DirectoryInfo(Path.Join(globalArtifactsDir, "artifacts")).Should().NotExist(); + + new DirectoryInfo(Path.Join(testInstance.Path, "artifacts")).Should().NotExist(); + new DirectoryInfo(Path.Join(testInstance.Path, "bin")).Should().NotExist(); + } + + [TestMethod] + public void ArtifactsPath_IsAddedAsSourceRoot() + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + var programPath = Path.Join(testInstance.Path, "Program.cs"); + var sourceRootsPath = Path.Join(testInstance.Path, "source-roots.txt"); + File.WriteAllText(programPath, s_program); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.targets"), """ + + + + + + """); + + new DotnetCommand(Log, "build", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + var expectedSourceRoot = VirtualProjectBuilder.GetArtifactsPath(programPath) + Path.DirectorySeparatorChar; + File.ReadAllLines(sourceRootsPath).Should().Contain( + sourceRoot => sourceRoot.Equals(expectedSourceRoot, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// When the surrounding repo uses artifacts layout, file-based apps place their artifacts there. + /// + [TestMethod] + public void ArtifactsPath_ReusedFromRepo() + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, s_program); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + true + + + """); + + var globalArtifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(globalArtifactsDir)) Directory.Delete(globalArtifactsDir, recursive: true); + + var localArtifactsDir = Path.Join(testInstance.Path, "artifacts"); + new DirectoryInfo(localArtifactsDir).Should().NotExist(); + + new DotnetCommand(Log, "build", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + // We still put our marker files into the global artifacts directory, but it should not contain any subdirectories. + new DirectoryInfo(globalArtifactsDir).EnumerateDirectories().Should().BeEmpty(); + + new FileInfo(Path.Join(localArtifactsDir, "bin", "Program.cs", "debug", "Program.dll")).Should().Exist(); + new DirectoryInfo(Path.Join(localArtifactsDir, "bin", "debug")).Should().NotExist(); + + // Publish + + Directory.Delete(Path.Join(localArtifactsDir), recursive: true); + + new DotnetCommand(Log, "publish", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(globalArtifactsDir).EnumerateDirectories().Should().BeEmpty(); + new DirectoryInfo(localArtifactsDir).EnumerateDirectories().Select(d => d.Name).Should().BeEquivalentTo(["bin", "obj", "publish"]); + new FileInfo(Path.Join(localArtifactsDir, "publish", "Program.cs", "release", $"Program{Constants.ExeSuffix}")).Should().Exist(); + + // Pack + + Directory.Delete(localArtifactsDir, recursive: true); + + new DotnetCommand(Log, "pack", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(globalArtifactsDir).EnumerateDirectories().Should().BeEmpty(); + new DirectoryInfo(localArtifactsDir).EnumerateDirectories().Select(d => d.Name).Should().BeEquivalentTo(["bin", "obj", "package", "publish"]); + new FileInfo(Path.Join(localArtifactsDir, "package", "release", "Program.1.0.0.nupkg")).Should().Exist(); + } + [TestMethod, CombinatorialData] public void LaunchProfile( bool cscOnly, diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs index debafda15758..b8b4735ae5c8 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs @@ -1390,12 +1390,9 @@ public void Api() - false - /artifacts + /artifacts Program $(AssemblyName) - artifacts/$(AssemblyName) - artifacts/$(AssemblyName) true {programPath} .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content;.dll=Reference @@ -1475,12 +1472,9 @@ public void Api_Evaluation() - false - /artifacts + /artifacts A $(AssemblyName) - artifacts/$(AssemblyName) - artifacts/$(AssemblyName) true {programPath} .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content;.dll=Reference @@ -1551,12 +1545,9 @@ public void Api_Diagnostic_01() - false - /artifacts + /artifacts Program $(AssemblyName) - artifacts/$(AssemblyName) - artifacts/$(AssemblyName) true {programPath} .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content;.dll=Reference @@ -1626,12 +1617,9 @@ public void Api_Diagnostic_02() - false - /artifacts + /artifacts Program $(AssemblyName) - artifacts/$(AssemblyName) - artifacts/$(AssemblyName) true {programPath} .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content;.dll=Reference