Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions documentation/general/dotnet-run-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ Similarly, implicit build files like `Directory.Build.props` or `Directory.Packa
> [!CAUTION]
> Multi-file support is postponed for .NET 11.
> In .NET 10, only the single file passed as the command-line argument to `dotnet run` is part of the compilation.
> Specifically, the virtual project has properties `EnableDefaultCompileItems=false` and `EnableDefaultEmbeddedResourceItems=false`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did we arrive at disabling this particular set of items? Do we have a reason for believing that if the directory contains some embedded resource, that we don't want to include it in the file-based app?

@jjonescz jjonescz Jul 2, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compile items are disabled since we are doing single file apps.

Embedded resources were discussed in the internal chat and I think the decision was to also exclude them, right, @baronfel @DamianEdwards? I'm not sure there is a strong reason for that. I think I would also prefer to include everything unless explicitly disabled (would make it easier to reason about this).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping embedded resources seems fine to me. @baronfel what was your reasoning for excluding them from single-file apps?

> (which can be customized via `#:property` directives), and a `Compile` item for the entry point file.
> During [conversion](#grow-up), any `Content`, `None`, `Compile`, and `EmbeddedResource` items that do not have metadata `ExcludeFromFileBasedAppConversion=true`
Comment thread
jjonescz marked this conversation as resolved.
> and that are files inside the entry point file's directory tree are copied to the converted directory.

### Nested files

Expand Down
64 changes: 64 additions & 0 deletions src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using Microsoft.Build.Evaluation;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.TemplateEngine.Cli.Commands;
Expand Down Expand Up @@ -32,6 +33,9 @@ public override int Execute()
var sourceFile = VirtualProjectBuildingCommand.LoadSourceFile(file);
var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !_force, errors: null);

// Find other items to copy over, e.g., default Content items like JSON files in Web apps.
var includeItems = FindIncludedItems().ToList();

Directory.CreateDirectory(targetDirectory);

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

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

// Copy over included items.
foreach (var item in includeItems)
{
string targetItemFullPath = Path.Combine(targetDirectory, item.RelativePath);

// Ignore already-copied files.
if (File.Exists(targetItemFullPath))
{
continue;
}

string targetItemDirectory = Path.GetDirectoryName(targetItemFullPath)!;
Directory.CreateDirectory(targetItemDirectory);
File.Copy(item.FullPath, targetItemFullPath);
}

return 0;

IEnumerable<(string FullPath, string RelativePath)> FindIncludedItems()
{
string entryPointFileDirectory = PathUtility.EnsureTrailingSlash(Path.GetDirectoryName(file)!);
var projectCollection = new ProjectCollection();
var command = new VirtualProjectBuildingCommand(
entryPointFileFullPath: file,
msbuildArgs: MSBuildArgs.FromOtherArgs([]))
{
Directives = directives,
};
var projectInstance = command.CreateProjectInstance(projectCollection);

// Include only items we know are files.
string[] itemTypes = ["Content", "None", "Compile", "EmbeddedResource"];
var items = itemTypes.SelectMany(t => projectInstance.GetItems(t));

foreach (var item in items)
{
// Escape hatch - exclude items that have metadata `ExcludeFromFileBasedAppConversion` set to `true`.
string include = item.GetMetadataValue("ExcludeFromFileBasedAppConversion");
Comment thread
jjonescz marked this conversation as resolved.
if (string.Equals(include, bool.TrueString, StringComparison.OrdinalIgnoreCase))
{
continue;
}

// Exclude items that are not contained within the entry point file directory.
string itemFullPath = Path.GetFullPath(path: item.GetMetadataValue("FullPath"), basePath: entryPointFileDirectory);
if (!itemFullPath.StartsWith(entryPointFileDirectory, StringComparison.OrdinalIgnoreCase))
{
continue;
}

// Exclude items that do not exist.
if (!File.Exists(itemFullPath))
{
continue;
}

string itemRelativePath = Path.GetRelativePath(relativeTo: entryPointFileDirectory, path: itemFullPath);
yield return (FullPath: itemFullPath, RelativePath: itemRelativePath);
}
}
}
}
2 changes: 0 additions & 2 deletions src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@ public override RunApiOutput Execute()
CustomArtifactsPath = ArtifactsPath,
};

buildCommand.PrepareProjectInstance();

var runCommand = new RunCommand(
noBuild: false,
projectFileFullPath: null,
Expand Down
2 changes: 1 addition & 1 deletion src/Cli/dotnet/Commands/Run/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public int Execute()
if (EntryPointFileFullPath is not null)
{
Debug.Assert(!ReadCodeFromStdin);
projectFactory = CreateVirtualCommand().PrepareProjectInstance().CreateProjectInstance;
projectFactory = CreateVirtualCommand().CreateProjectInstance;
}
}

Expand Down
42 changes: 19 additions & 23 deletions src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,6 @@ Override targets which don't work with project files that are not present on dis
</Target>
""";

private ImmutableArray<CSharpDirective> _directives;

public VirtualProjectBuildingCommand(
string entryPointFileFullPath,
MSBuildArgs msbuildArgs)
Expand All @@ -136,6 +134,23 @@ public VirtualProjectBuildingCommand(
/// </summary>
public bool NoBuildMarkers { get; init; }

public ImmutableArray<CSharpDirective> Directives
{
get
{
if (field.IsDefault)
Comment thread
jjonescz marked this conversation as resolved.
{
var sourceFile = LoadSourceFile(EntryPointFileFullPath);
field = FindDirectives(sourceFile, reportAllErrors: false, errors: null);
Debug.Assert(!field.IsDefault);
}

return field;
}

init;
}

public override int Execute()
{
Debug.Assert(!(NoRestore && NoBuild));
Expand All @@ -157,8 +172,6 @@ public override int Execute()
Reporter.Output.WriteLine(CliCommandStrings.NoBinaryLogBecauseUpToDate.Yellow());
}

PrepareProjectInstance();

return 0;
}

Expand Down Expand Up @@ -188,8 +201,6 @@ public override int Execute()
LogTaskInputs = binaryLoggers.Length != 0,
};

PrepareProjectInstance();

// Do a restore first (equivalent to MSBuild's "implicit restore", i.e., `/restore`).
// See https://github.com/dotnet/msbuild/blob/a1c2e7402ef0abe36bf493e395b04dd2cb1b3540/src/MSBuild/XMake.cs#L1838
// and https://github.com/dotnet/msbuild/issues/11519.
Expand Down Expand Up @@ -472,19 +483,6 @@ private void MarkBuildSuccess(RunFileBuildCacheEntry cacheEntry)
JsonSerializer.Serialize(stream, cacheEntry, RunFileJsonSerializerContext.Default.RunFileBuildCacheEntry);
}

/// <summary>
/// Needs to be called before the first call to <see cref="CreateProjectInstance(ProjectCollection)"/>.
/// </summary>
public VirtualProjectBuildingCommand PrepareProjectInstance()
{
Debug.Assert(_directives.IsDefault, $"{nameof(PrepareProjectInstance)} should not be called multiple times.");

var sourceFile = LoadSourceFile(EntryPointFileFullPath);
_directives = FindDirectives(sourceFile, reportAllErrors: false, errors: null);

return this;
}

public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection)
{
return CreateProjectInstance(projectCollection, addGlobalProperties: null);
Expand All @@ -511,13 +509,11 @@ private ProjectInstance CreateProjectInstance(

ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection)
{
Debug.Assert(!_directives.IsDefault, $"{nameof(PrepareProjectInstance)} should have been called first.");

var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj");
var projectFileWriter = new StringWriter();
WriteProjectFile(
projectFileWriter,
_directives,
Directives,
isVirtualProject: true,
targetFilePath: EntryPointFileFullPath,
artifactsPath: GetArtifactsPath(),
Expand Down Expand Up @@ -648,7 +644,7 @@ public static void WriteProjectFile(
writer.WriteLine("""

<PropertyGroup>
<EnableDefaultItems>false</EnableDefaultItems>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
""");
}
Expand Down
Loading
Loading