Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions documentation/general/dotnet-run-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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($"""
<Project>

<PropertyGroup>
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
<ArtifactsPath>{EscapeValue(artifactsPath)}</ArtifactsPath>
<FileBasedAppArtifactsPath>{EscapeValue(artifactsPath)}</FileBasedAppArtifactsPath>
<AssemblyName>{EscapeValue(Path.GetFileNameWithoutExtension(entryPointFilePath))}</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace>
<PublishDir>artifacts/$(AssemblyName)</PublishDir>
<PackageOutputPath>artifacts/$(AssemblyName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
<EntryPointFilePath>{EscapeValue(entryPointFilePath)}</EntryPointFilePath>
<FileBasedProgramsItemMapping>{CSharpDirective.IncludeOrExclude.DefaultMappingString}</FileBasedProgramsItemMapping>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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). -->
<Import Project="$(MSBuildThisFileDirectory)..\targets\Microsoft.NET.DefaultArtifactsPath.props"
Condition="'$(UseArtifactsOutput)' == 'true' Or '$(ArtifactsPath)' != ''"/>
Condition="'$(UseArtifactsOutput)' == 'true' Or '$(ArtifactsPath)' != '' Or '$(FileBasedAppArtifactsPath)' != ''"/>

<PropertyGroup Condition="'$(UseArtifactsOutput)' == 'true'">
<UseArtifactsIntermediateOutput Condition="'$(UseArtifactsIntermediateOutput)' == ''">true</UseArtifactsIntermediateOutput>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ Copyright (c) .NET Foundation. All rights reserved.
<_ArtifactsPathLocationType>ProjectFolder</_ArtifactsPathLocationType>
</PropertyGroup>

<!-- Use FileBasedAppArtifactsPath if set -->
<PropertyGroup Condition="'$(ArtifactsPath)' == '' And '$(FileBasedAppArtifactsPath)' != ''">
<UseArtifactsOutput Condition="'$(UseArtifactsOutput)' == '' And '$(UsingMicrosoftArtifactsSdk)' != 'true'">true</UseArtifactsOutput>
<ArtifactsPath>$(FileBasedAppArtifactsPath)</ArtifactsPath>
<IncludeProjectNameInArtifactsPaths Condition="'$(IncludeProjectNameInArtifactsPaths)' == ''">false</IncludeProjectNameInArtifactsPaths>
Comment thread
jjonescz marked this conversation as resolved.
<_ArtifactsPathLocationType>FileBasedApp</_ArtifactsPathLocationType>

<!-- If artifacts are going to a temp folder, set publish and pack outputs next to the file-based app. -->
<PublishDir Condition="'$(PublishDir)' == ''">artifacts/$(AssemblyName)</PublishDir>
<PackageOutputPath Condition="'$(PackageOutputPath)' == ''">artifacts/$(AssemblyName)</PackageOutputPath>
</PropertyGroup>

<!-- Add the ArtifactsPath as a SourceRoot so that source linking works correctly when the ArtifactsPath is outside of a known repo root.
The path must be canonicalized to avoid issues with relative path portions (..\) in PathMap infrastructure. -->
<PropertyGroup>
Expand Down
115 changes: 115 additions & 0 deletions test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"), """
<Project>
<Target Name="_WriteSourceRoots" BeforeTargets="CoreCompile">
<WriteLinesToFile File="$(MSBuildThisFileDirectory)source-roots.txt"
Lines="@(SourceRoot)"
Overwrite="true" />
</Target>
</Project>
""");

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));
}

/// <summary>
/// When the surrounding repo uses artifacts layout, file-based apps place their artifacts there.
/// </summary>
[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"), """
<Project>
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>
</Project>
""");

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1390,12 +1390,9 @@ public void Api()
<Project>

<PropertyGroup>
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
<ArtifactsPath>/artifacts</ArtifactsPath>
<FileBasedAppArtifactsPath>/artifacts</FileBasedAppArtifactsPath>
<AssemblyName>Program</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace>
<PublishDir>artifacts/$(AssemblyName)</PublishDir>
<PackageOutputPath>artifacts/$(AssemblyName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
<EntryPointFilePath>{programPath}</EntryPointFilePath>
<FileBasedProgramsItemMapping>.cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content;.dll=Reference</FileBasedProgramsItemMapping>
Expand Down Expand Up @@ -1475,12 +1472,9 @@ public void Api_Evaluation()
<Project>

<PropertyGroup>
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
<ArtifactsPath>/artifacts</ArtifactsPath>
<FileBasedAppArtifactsPath>/artifacts</FileBasedAppArtifactsPath>
<AssemblyName>A</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace>
<PublishDir>artifacts/$(AssemblyName)</PublishDir>
<PackageOutputPath>artifacts/$(AssemblyName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
<EntryPointFilePath>{programPath}</EntryPointFilePath>
<FileBasedProgramsItemMapping>.cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content;.dll=Reference</FileBasedProgramsItemMapping>
Expand Down Expand Up @@ -1551,12 +1545,9 @@ public void Api_Diagnostic_01()
<Project>

<PropertyGroup>
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
<ArtifactsPath>/artifacts</ArtifactsPath>
<FileBasedAppArtifactsPath>/artifacts</FileBasedAppArtifactsPath>
<AssemblyName>Program</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace>
<PublishDir>artifacts/$(AssemblyName)</PublishDir>
<PackageOutputPath>artifacts/$(AssemblyName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
<EntryPointFilePath>{programPath}</EntryPointFilePath>
<FileBasedProgramsItemMapping>.cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content;.dll=Reference</FileBasedProgramsItemMapping>
Expand Down Expand Up @@ -1626,12 +1617,9 @@ public void Api_Diagnostic_02()
<Project>

<PropertyGroup>
<IncludeProjectNameInArtifactsPaths>false</IncludeProjectNameInArtifactsPaths>
<ArtifactsPath>/artifacts</ArtifactsPath>
<FileBasedAppArtifactsPath>/artifacts</FileBasedAppArtifactsPath>
<AssemblyName>Program</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace>
<PublishDir>artifacts/$(AssemblyName)</PublishDir>
<PackageOutputPath>artifacts/$(AssemblyName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
<EntryPointFilePath>{programPath}</EntryPointFilePath>
<FileBasedProgramsItemMapping>.cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content;.dll=Reference</FileBasedProgramsItemMapping>
Expand Down
Loading