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
1 change: 1 addition & 0 deletions .github/copilot/skills/incremental-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Use it after making source code changes to quickly build only the modified proje
## Prerequisites

- A full build must have been completed at least once (via `build.cmd` or `build.sh`) so that the redist SDK layout exists at `artifacts\bin\redist\Debug\dotnet\sdk\<version>\`.
- The repo-local `.dotnet` SDK must match the version expected by the test projects. If the runtime or SDK version is out of date (e.g., test build fails with a missing framework error), run `.\restore.cmd` (or `./restore.sh` on macOS/Linux) to download the correct SDK into `.dotnet`.
- This workflow uses Windows/PowerShell commands and paths. On macOS/Linux, substitute forward slashes and use `cp` instead of `Copy-Item`.

## When to use
Expand Down
31 changes: 26 additions & 5 deletions documentation/general/dotnet-run-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ If a dash (`-`) is given instead of the target path (i.e., `dotnet run -`), the
In this case, the current working directory is not used to search for other files (launch profiles, other sources in case of multi-file apps);
the compilation consists solely of the single file read from the standard input.
However, the current working directory is still used as the working directory for building and executing the program.
To reference projects relative to the current working directory (instead of relative to the temporary directory the file is isolated in),
you can use something like `#:project $(MSBuildStartupDirectory)/relative/path`.
To reference projects or files relative to the current working directory (instead of relative to the temporary directory the file is isolated in),
you can use something like `#:project $(MSBuildStartupDirectory)/relative/path` or `#:ref $(MSBuildStartupDirectory)/relative/lib.cs`.

`dotnet path.cs` is a shortcut for `dotnet run --file path.cs` provided that `path.cs` is a valid [target path](#target-path) (`dotnet -` is currently not supported)
and it is not a DLL path, built-in command, or a NuGet tool (e.g., `dotnet watch` invokes the `dotnet-watch` tool
Expand Down Expand Up @@ -180,11 +180,12 @@ which are [ignored][ignored-directives] by the C# language but recognized by the
#:property LangVersion=preview
#:package System.CommandLine@2.0.0-*
#:project ../MyLibrary
#:ref ../lib/lib.cs
#:include ./**/*.cs
```

Each directive has a kind (e.g., `package`), a name (e.g., `System.CommandLine`), a separator (e.g., `@`), and a value (e.g., the package version).
The value is required for `#:property`, optional for `#:package`/`#:sdk`, and disallowed for `#:project`/`#:include`.
The value is required for `#:property`, optional for `#:package`/`#:sdk`, and disallowed for `#:project`/`#:ref`/`#:include`.

The name must be separated from the kind of the directive by whitespace
and any leading and trailing white space is not considered part of the name and value.
Expand All @@ -211,6 +212,26 @@ The directives are processed as follows:
(because `ProjectReference` items don't support directory paths).
An error is reported if zero or more than one projects are found in the directory, just like `dotnet reference add` would do.

- Each `#:ref` references another `.cs` file as a separate project reference.
A virtual project is created for the referenced file (e.g., `lib.cs` produces a virtual `lib.cs.csproj`),
and a `<ProjectReference Include="lib.cs.csproj" />` is injected in an `<ItemGroup>`.
It is an error if the name is empty or if the referenced file does not exist.
Unlike `#:project`, `#:ref` points to a `.cs` file (not a `.csproj` file or directory).

The referenced file is itself a file-based program with its own virtual project (defaulting to `OutputType=Exe`).
Library files without an entry point should use `#:property OutputType=Library` to avoid compilation errors.
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.

While a "library FBA" might not have an "entry point", I think it should have a "main file".
We should be able to look at a file in isolation and realize it is specifically something which would be valid to reference via #:ref.

Otherwise, the editor is going to have to identify the closure of projects to load, by actually going ahead and loading every FBA it is able to discover, then examining all the targets of #:refs within all those FBAs, and loading those recursively until we reach a stable state. I think this will be more costly to implement on the editor side.

Also, I think this ambiguity would end up being passed on to users, who may struggled to determine whether something is the "main file" of a file-based library, when the file doesn't have a clear marker indicating that's what it is, rather than being some helper file which is meant to be #:included in something.

I would honestly consider requiring that "files referenced by #:ref, start with either #! (to indicate they're also executable), or for example #:library (to say they're just libraries), and using that to ensure that "automatic discovery" finds them. This should make it so that the editor "just works" with such reference graphs.

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.

What if we require they start with either #! or have #:property OutputType=Library somewhere among the directives of the entry point file?

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.

have #:property OutputType=Library somewhere among the directives of the entry point file?

I lean a bit against this as it seems surprising for the directive to have different semantics here than in a props file, and for this property name in particular to have this effect.

I think while we are behind an experimental flag, we can ship command line support without a "main file marker". I think we'll have to have a separate conversation about how to cost/schedule editor support also, and possibly make a change to add/require this marker later.

Does that seem reasonable? Adding @phil-allen-msft for visibility.

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.

Yes, I think the core dotnet CLI part of the #:ref is given (it's just a way to specify <ProjectReference Include="file.cs"/>), so no reason to block that. But we may need some add-ons to improve the IDE experience (since we don't have solution files like normal projects do) - but that feels independent and we have the feature flag to avoid breaking changes.

Because the referenced file is compiled as a separate assembly, internal members of the referenced file are not accessible from the referencing file.
The `#:ref` directive is transitive: a referenced file can itself contain `#:ref` directives (or any other directives).

Relative paths are resolved relative to the file containing the directive.
MSBuild variables (like `$(MSBuildProjectDirectory)`) can be used in the path.

During [conversion](#grow-up), each `#:ref` directive creates a separate library project in a sibling directory
and a corresponding `<ProjectReference>` entry is added to the converted project.
The conversion is recursive: any `#:ref` directives in the referenced files are also converted in the same way.

This directive is currently gated under a feature flag that can be enabled by setting the MSBuild property `ExperimentalFileBasedProgramEnableRefDirective=true`.

- Each `#:include` is injected as `<{1} Include="{0}" />` in an `<ItemGroup>`
where `{0}` is the directive's value and `{1}` is determined by its extension.
The mapping can be customized by setting the MSBuild property `FileBasedProgramsItemMapping`
Expand All @@ -230,9 +251,9 @@ The directives are processed as follows:
- Other directive kinds result in an error, reserving them for future use.

Directive values support MSBuild variables (like `$(..)`) normally as they are translated literally and left to MSBuild engine to process.
However, in `#:project` directives, variables might not be preserved during [grow up](#grow-up),
However, in `#:project` and `#:ref` directives, variables might not be preserved during [grow up](#grow-up),
because there is additional processing of those directives that makes it technically challenging to preserve variables in all cases
(project directive values need to be resolved to be relative to the target directory
(project and ref directive values need to be resolved to be relative to the target directory
and also to point to a project file rather than a directory).
Note that it is not expected that variables inside the path change their meaning during the conversion,
so for example `#:project ../$(LibName)` is translated to `<ProjectReference Include="../../$(LibName)/Lib.csproj" />` (i.e., the variable is preserved).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@
<value>The '#:project' directive is invalid: {0}</value>
<comment>{0} is the inner error message.</comment>
</data>
<data name="InvalidRefDirective" xml:space="preserve">
<value>The '#:ref' directive is invalid: {0}</value>
<comment>{Locked="#:ref"}{0} is the inner error message.</comment>
</data>
<data name="CouldNotFindRefFile" xml:space="preserve">
<value>Could not find file '{0}'.</value>
<comment>{0} is the file path.</comment>
</data>
<data name="MissingDirectiveName" xml:space="preserve">
<value>Missing name of '{0}'.</value>
<comment>{0} is the directive name like 'package' or 'sdk'.</comment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ public void ReportError(TextSpan span, string message)
case "property": return Property.Parse(context);
case "package": return Package.Parse(context);
case "project": return Project.Parse(context);
case "ref": return Ref.Parse(context);
case "include" or "exclude": return IncludeOrExclude.Parse(context);
default:
context.ReportError(string.Format(FileBasedProgramsResources.UnrecognizedDirective, context.DirectiveKind));
Expand Down Expand Up @@ -587,6 +588,100 @@ void ReportError(string message)
public override string ToString() => $"#:project {Name}";
}

/// <summary>
/// <c>#:ref</c> directive. References another file-based app as a library.
/// </summary>
public sealed class Ref : Named
{
public const string ExperimentalFileBasedProgramEnableRefDirective = nameof(ExperimentalFileBasedProgramEnableRefDirective);

[SetsRequiredMembers]
public Ref(in ParseInfo info, string name) : base(info)
{
Name = name;
OriginalName = name;
}

/// <summary>
/// Preserved across <see cref="WithName"/> calls, i.e.,
/// this is the original directive text as entered by the user.
/// </summary>
public string OriginalName { get; init; }

/// <summary>
/// This is the <see cref="OriginalName"/> with MSBuild <c>$(..)</c> vars expanded.
/// </summary>
public string? ExpandedName { get; init; }

/// <summary>
/// The resolved full path to the referenced <c>.cs</c> file.
/// </summary>
public string? ResolvedPath { get; init; }

public static new Ref? Parse(in ParseContext context)
{
var directiveText = context.DirectiveText;
if (directiveText.IsWhiteSpace())
{
context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, context.DirectiveKind));
return null;
}

return new Ref(context.Info, directiveText);
}

public enum NameKind
{
/// <summary>
/// Change <see cref="Named.Name"/> and <see cref="ExpandedName"/>.
/// </summary>
Expanded = 1,

/// <summary>
/// Change <see cref="Named.Name"/> and <see cref="ResolvedPath"/>.
/// </summary>
Resolved = 2,

/// <summary>
/// Change only <see cref="Named.Name"/>.
/// </summary>
Final = 3,
}

public Ref WithName(string name, NameKind kind)
{
return new Ref(Info, name)
{
OriginalName = OriginalName,
ExpandedName = kind == NameKind.Expanded ? name : ExpandedName,
ResolvedPath = kind == NameKind.Resolved ? name : ResolvedPath,
};
}

/// <summary>
/// Resolves the path relative to the source file's directory.
/// </summary>
public Ref EnsureResolvedPath(ErrorReporter errorReporter)
{
var sourcePath = Info.SourceFile.Path;
var sourceDirectory = Path.GetDirectoryName(sourcePath)
?? throw new InvalidOperationException($"Source file path '{sourcePath}' does not have a containing directory.");

var resolvedFilePath = Path.GetFullPath(Path.Combine(sourceDirectory, Name.Replace('\\', '/')));

if (!File.Exists(resolvedFilePath))
{
errorReporter(Info.SourceFile.Text, sourcePath, Info.Span,
string.Format(FileBasedProgramsResources.InvalidRefDirective,
string.Format(FileBasedProgramsResources.CouldNotFindRefFile, resolvedFilePath)));
}

return WithName(resolvedFilePath, NameKind.Resolved);
}

public override string ToString() => $"#:ref {Name}";
}

public enum IncludeOrExcludeKind
{
Include,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.Experi
const Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.ExperimentalFileBasedProgramEnableItemMapping = "ExperimentalFileBasedProgramEnableItemMapping" -> string!
const Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.ExperimentalFileBasedProgramEnableTransitiveDirectives = "ExperimentalFileBasedProgramEnableTransitiveDirectives" -> string!
const Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.MappingPropertyName = "FileBasedProgramsItemMapping" -> string!
const Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective = "ExperimentalFileBasedProgramEnableRefDirective" -> string!
Microsoft.DotNet.FileBasedPrograms.CSharpDirective
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.CSharpDirective(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude
Expand Down Expand Up @@ -71,6 +72,20 @@ Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Property(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Value.get -> string!
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Value.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.EnsureResolvedPath(Microsoft.DotNet.FileBasedPrograms.ErrorReporter! errorReporter) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref!
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ExpandedName.get -> string?
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ExpandedName.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind.Expanded = 1 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind.Final = 3 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind.Resolved = 2 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.OriginalName.get -> string!
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.OriginalName.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.Ref(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info, string! name) -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ResolvedPath.get -> string?
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ResolvedPath.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.WithName(string! name, Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind kind) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref!
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.Sdk(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.Version.get -> string?
Expand Down Expand Up @@ -123,6 +138,7 @@ override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.ToS
override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.ToString() -> string!
override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ToString() -> string!
override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.ToString() -> string!
override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ToString() -> string!
override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.ToString() -> string!
override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Shebang.ToString() -> string!
override Microsoft.DotNet.FileBasedPrograms.SourceFile.GetHashCode() -> int
Expand All @@ -134,6 +150,7 @@ static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.Parse(in Micro
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Named?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk?
static Microsoft.DotNet.FileBasedPrograms.ErrorReporters.CreateCollectingReporter(out System.Collections.Immutable.ImmutableArray<Microsoft.DotNet.FileBasedPrograms.SimpleDiagnostic!>.Builder! builder) -> Microsoft.DotNet.FileBasedPrograms.ErrorReporter!
static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.CombineHashCodes(int value1, int value2) -> int
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading