Skip to content

Commit feb621b

Browse files
authored
Include some default items in file-based apps (#49584)
1 parent bd8125d commit feb621b

File tree

7 files changed

+495
-54
lines changed

7 files changed

+495
-54
lines changed

documentation/general/dotnet-run-file.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ Similarly, implicit build files like `Directory.Build.props` or `Directory.Packa
125125
> [!CAUTION]
126126
> Multi-file support is postponed for .NET 11.
127127
> In .NET 10, only the single file passed as the command-line argument to `dotnet run` is part of the compilation.
128+
> Specifically, the virtual project has properties `EnableDefaultCompileItems=false` and `EnableDefaultEmbeddedResourceItems=false`
129+
> (which can be customized via `#:property` directives), and a `Compile` item for the entry point file.
130+
> During [conversion](#grow-up), any `Content`, `None`, `Compile`, and `EmbeddedResource` items that do not have metadata `ExcludeFromFileBasedAppConversion=true`
131+
> and that are files inside the entry point file's directory tree are copied to the converted directory.
128132
129133
### Nested files
130134

src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.CommandLine;
5+
using Microsoft.Build.Evaluation;
56
using Microsoft.DotNet.Cli.Commands.Run;
67
using Microsoft.DotNet.Cli.Utils;
78
using Microsoft.TemplateEngine.Cli.Commands;
@@ -32,6 +33,9 @@ public override int Execute()
3233
var sourceFile = VirtualProjectBuildingCommand.LoadSourceFile(file);
3334
var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !_force, errors: null);
3435

36+
// Find other items to copy over, e.g., default Content items like JSON files in Web apps.
37+
var includeItems = FindIncludedItems().ToList();
38+
3539
Directory.CreateDirectory(targetDirectory);
3640

3741
var targetFile = Path.Join(targetDirectory, Path.GetFileName(file));
@@ -47,11 +51,71 @@ public override int Execute()
4751
File.Move(file, targetFile);
4852
}
4953

54+
// Create project file.
5055
string projectFile = Path.Join(targetDirectory, Path.GetFileNameWithoutExtension(file) + ".csproj");
5156
using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write);
5257
using var writer = new StreamWriter(stream, Encoding.UTF8);
5358
VirtualProjectBuildingCommand.WriteProjectFile(writer, directives, isVirtualProject: false);
5459

60+
// Copy over included items.
61+
foreach (var item in includeItems)
62+
{
63+
string targetItemFullPath = Path.Combine(targetDirectory, item.RelativePath);
64+
65+
// Ignore already-copied files.
66+
if (File.Exists(targetItemFullPath))
67+
{
68+
continue;
69+
}
70+
71+
string targetItemDirectory = Path.GetDirectoryName(targetItemFullPath)!;
72+
Directory.CreateDirectory(targetItemDirectory);
73+
File.Copy(item.FullPath, targetItemFullPath);
74+
}
75+
5576
return 0;
77+
78+
IEnumerable<(string FullPath, string RelativePath)> FindIncludedItems()
79+
{
80+
string entryPointFileDirectory = PathUtility.EnsureTrailingSlash(Path.GetDirectoryName(file)!);
81+
var projectCollection = new ProjectCollection();
82+
var command = new VirtualProjectBuildingCommand(
83+
entryPointFileFullPath: file,
84+
msbuildArgs: MSBuildArgs.FromOtherArgs([]))
85+
{
86+
Directives = directives,
87+
};
88+
var projectInstance = command.CreateProjectInstance(projectCollection);
89+
90+
// Include only items we know are files.
91+
string[] itemTypes = ["Content", "None", "Compile", "EmbeddedResource"];
92+
var items = itemTypes.SelectMany(t => projectInstance.GetItems(t));
93+
94+
foreach (var item in items)
95+
{
96+
// Escape hatch - exclude items that have metadata `ExcludeFromFileBasedAppConversion` set to `true`.
97+
string include = item.GetMetadataValue("ExcludeFromFileBasedAppConversion");
98+
if (string.Equals(include, bool.TrueString, StringComparison.OrdinalIgnoreCase))
99+
{
100+
continue;
101+
}
102+
103+
// Exclude items that are not contained within the entry point file directory.
104+
string itemFullPath = Path.GetFullPath(path: item.GetMetadataValue("FullPath"), basePath: entryPointFileDirectory);
105+
if (!itemFullPath.StartsWith(entryPointFileDirectory, StringComparison.OrdinalIgnoreCase))
106+
{
107+
continue;
108+
}
109+
110+
// Exclude items that do not exist.
111+
if (!File.Exists(itemFullPath))
112+
{
113+
continue;
114+
}
115+
116+
string itemRelativePath = Path.GetRelativePath(relativeTo: entryPointFileDirectory, path: itemFullPath);
117+
yield return (FullPath: itemFullPath, RelativePath: itemRelativePath);
118+
}
119+
}
56120
}
57121
}

src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,6 @@ public override RunApiOutput Execute()
9494
CustomArtifactsPath = ArtifactsPath,
9595
};
9696

97-
buildCommand.PrepareProjectInstance();
98-
9997
var runCommand = new RunCommand(
10098
noBuild: false,
10199
projectFileFullPath: null,

src/Cli/dotnet/Commands/Run/RunCommand.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public int Execute()
140140
if (EntryPointFileFullPath is not null)
141141
{
142142
Debug.Assert(!ReadCodeFromStdin);
143-
projectFactory = CreateVirtualCommand().PrepareProjectInstance().CreateProjectInstance;
143+
projectFactory = CreateVirtualCommand().CreateProjectInstance;
144144
}
145145
}
146146

@@ -587,9 +587,13 @@ public static RunCommand FromParseResult(ParseResult parseResult)
587587
}
588588

589589
// If '-' is specified as the input file, read all text from stdin into a temporary file and use that as the entry point.
590-
entryPointFilePath = Path.GetTempFileName();
590+
// We create a new directory for each file so other files are not included in the compilation.
591+
// We fail if the file already exists to avoid reusing the same file for multiple stdin runs (in case the random name is duplicate).
592+
string directory = VirtualProjectBuildingCommand.GetTempSubdirectory(Path.GetRandomFileName());
593+
VirtualProjectBuildingCommand.CreateTempSubdirectory(directory);
594+
entryPointFilePath = Path.Join(directory, "app.cs");
591595
using (var stdinStream = Console.OpenStandardInput())
592-
using (var fileStream = File.OpenWrite(entryPointFilePath))
596+
using (var fileStream = new FileStream(entryPointFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
593597
{
594598
stdinStream.CopyTo(fileStream);
595599
}

src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,6 @@ Override targets which don't work with project files that are not present on dis
111111
</Target>
112112
""";
113113

114-
private ImmutableArray<CSharpDirective> _directives;
115-
116114
public VirtualProjectBuildingCommand(
117115
string entryPointFileFullPath,
118116
MSBuildArgs msbuildArgs)
@@ -136,6 +134,23 @@ public VirtualProjectBuildingCommand(
136134
/// </summary>
137135
public bool NoBuildMarkers { get; init; }
138136

137+
public ImmutableArray<CSharpDirective> Directives
138+
{
139+
get
140+
{
141+
if (field.IsDefault)
142+
{
143+
var sourceFile = LoadSourceFile(EntryPointFileFullPath);
144+
field = FindDirectives(sourceFile, reportAllErrors: false, errors: null);
145+
Debug.Assert(!field.IsDefault);
146+
}
147+
148+
return field;
149+
}
150+
151+
init;
152+
}
153+
139154
public override int Execute()
140155
{
141156
Debug.Assert(!(NoRestore && NoBuild));
@@ -157,8 +172,6 @@ public override int Execute()
157172
Reporter.Output.WriteLine(CliCommandStrings.NoBinaryLogBecauseUpToDate.Yellow());
158173
}
159174

160-
PrepareProjectInstance();
161-
162175
return 0;
163176
}
164177

@@ -188,8 +201,6 @@ public override int Execute()
188201
LogTaskInputs = binaryLoggers.Length != 0,
189202
};
190203

191-
PrepareProjectInstance();
192-
193204
// Do a restore first (equivalent to MSBuild's "implicit restore", i.e., `/restore`).
194205
// See https://github.com/dotnet/msbuild/blob/a1c2e7402ef0abe36bf493e395b04dd2cb1b3540/src/MSBuild/XMake.cs#L1838
195206
// and https://github.com/dotnet/msbuild/issues/11519.
@@ -445,17 +456,7 @@ private void MarkBuildStart()
445456

446457
string directory = GetArtifactsPath();
447458

448-
if (OperatingSystem.IsWindows())
449-
{
450-
Directory.CreateDirectory(directory);
451-
}
452-
else
453-
{
454-
// Ensure only the current user has access to the directory to avoid leaking the program to other users.
455-
// We don't mind that permissions might be different if the directory already exists,
456-
// since it's under user's local directory and its path should be unique.
457-
Directory.CreateDirectory(directory, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
458-
}
459+
CreateTempSubdirectory(directory);
459460

460461
File.WriteAllText(Path.Join(directory, BuildStartCacheFileName), EntryPointFileFullPath);
461462
}
@@ -472,19 +473,6 @@ private void MarkBuildSuccess(RunFileBuildCacheEntry cacheEntry)
472473
JsonSerializer.Serialize(stream, cacheEntry, RunFileJsonSerializerContext.Default.RunFileBuildCacheEntry);
473474
}
474475

475-
/// <summary>
476-
/// Needs to be called before the first call to <see cref="CreateProjectInstance(ProjectCollection)"/>.
477-
/// </summary>
478-
public VirtualProjectBuildingCommand PrepareProjectInstance()
479-
{
480-
Debug.Assert(_directives.IsDefault, $"{nameof(PrepareProjectInstance)} should not be called multiple times.");
481-
482-
var sourceFile = LoadSourceFile(EntryPointFileFullPath);
483-
_directives = FindDirectives(sourceFile, reportAllErrors: false, errors: null);
484-
485-
return this;
486-
}
487-
488476
public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection)
489477
{
490478
return CreateProjectInstance(projectCollection, addGlobalProperties: null);
@@ -511,13 +499,11 @@ private ProjectInstance CreateProjectInstance(
511499

512500
ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection)
513501
{
514-
Debug.Assert(!_directives.IsDefault, $"{nameof(PrepareProjectInstance)} should have been called first.");
515-
516502
var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj");
517503
var projectFileWriter = new StringWriter();
518504
WriteProjectFile(
519505
projectFileWriter,
520-
_directives,
506+
Directives,
521507
isVirtualProject: true,
522508
targetFilePath: EntryPointFileFullPath,
523509
artifactsPath: GetArtifactsPath(),
@@ -534,20 +520,46 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection)
534520

535521
private string GetArtifactsPath() => CustomArtifactsPath ?? GetArtifactsPath(EntryPointFileFullPath);
536522

537-
// internal for testing
538-
internal static string GetArtifactsPath(string entryPointFileFullPath)
523+
public static string GetArtifactsPath(string entryPointFileFullPath)
524+
{
525+
// Include entry point file name so the directory name is not completely opaque.
526+
string fileName = Path.GetFileNameWithoutExtension(entryPointFileFullPath);
527+
string hash = Sha256Hasher.HashWithNormalizedCasing(entryPointFileFullPath);
528+
string directoryName = $"{fileName}-{hash}";
529+
530+
return GetTempSubdirectory(directoryName);
531+
}
532+
533+
/// <summary>
534+
/// Obtains a temporary subdirectory for file-based apps.
535+
/// </summary>
536+
public static string GetTempSubdirectory(string name)
539537
{
540538
// We want a location where permissions are expected to be restricted to the current user.
541539
string directory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
542540
? Path.GetTempPath()
543541
: Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
544542

545-
// Include entry point file name so the directory name is not completely opaque.
546-
string fileName = Path.GetFileNameWithoutExtension(entryPointFileFullPath);
547-
string hash = Sha256Hasher.HashWithNormalizedCasing(entryPointFileFullPath);
548-
string directoryName = $"{fileName}-{hash}";
543+
return Path.Join(directory, "dotnet", "runfile", name);
544+
}
549545

550-
return Path.Join(directory, "dotnet", "runfile", directoryName);
546+
/// <summary>
547+
/// Creates a temporary subdirectory for file-based apps.
548+
/// Use <see cref="GetTempSubdirectory"/> to obtain the path.
549+
/// </summary>
550+
public static void CreateTempSubdirectory(string path)
551+
{
552+
if (OperatingSystem.IsWindows())
553+
{
554+
Directory.CreateDirectory(path);
555+
}
556+
else
557+
{
558+
// Ensure only the current user has access to the directory to avoid leaking the program to other users.
559+
// We don't mind that permissions might be different if the directory already exists,
560+
// since it's under user's local directory and its path should be unique.
561+
Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
562+
}
551563
}
552564

553565
public static void WriteProjectFile(
@@ -648,7 +660,7 @@ public static void WriteProjectFile(
648660
writer.WriteLine("""
649661
650662
<PropertyGroup>
651-
<EnableDefaultItems>false</EnableDefaultItems>
663+
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
652664
</PropertyGroup>
653665
""");
654666
}

0 commit comments

Comments
 (0)