Skip to content

Commit d1363e0

Browse files
authored
Update dotnet run file.cs spec with new ignored directives syntax (#47543)
1 parent 17bdd73 commit d1363e0

File tree

1 file changed

+49
-43
lines changed

1 file changed

+49
-43
lines changed

documentation/general/dotnet-run-file.md

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ In fact, this command simply materializes the [implicit project file](#implicit-
4343
This action should not change the behavior of the target program.
4444

4545
```ps1
46-
dotnet project add
46+
dotnet project convert
4747
```
4848

4949
## Target path
@@ -92,10 +92,10 @@ For example, the remaining command-line arguments after the first argument (the
9292
If a file is given to `dotnet run`, it has to be an *entry-point file*, otherwise an error is reported.
9393
We want to report an error for non-entry-point files to avoid the confusion of being able to `dotnet run util.cs`.
9494

95-
Currently, entry-point files must contain top-level statements,
96-
but other entry-point forms like classic `Main` method can be recognized in the future.
97-
We could modify Roslyn to accept the entry-point path and then it would be the compiler's responsibility
98-
to check whether the file contains an entry point (of any kind) and report an error otherwise.
95+
We modify Roslyn to accept the entry-point path and then it is its responsibility
96+
to check whether the file contains an entry point (top-level statements or a valid Main method) and report an error otherwise.
97+
(We cannot simply use Roslyn APIs to detect entry points ourselves because parsing depends on conditional symbols like those from `<DefineConstants>`
98+
and we can reliably know the set of those only after invoking MSBuild, and doing that up front would be an unnecessary performance hit just to detect entry points.)
9999

100100
Because of the [implicit project file](#implicit-project-file),
101101
other files in the target directory or its subdirectories are included in the compilation.
@@ -129,6 +129,10 @@ Again, this problem exists with project-based programs as well.
129129
Note that having a project-based or file-based program in the drive root would result in
130130
[error MSB5029](https://learn.microsoft.com/visualstudio/msbuild/errors/msb5029).
131131

132+
For `.csproj` files inside the target directory and its parent directories, we do not report any errors/warnings.
133+
That's because it might be perfectly reasonable to have file-based programs nested in another project-based program
134+
(most likely excluded from that project's compilation via something like `<Compile Exclude="./my-scripts/**" />`).
135+
132136
### Multiple entry points
133137

134138
If there are multiple entry-point files in the target directory, the target path must be a file
@@ -172,56 +176,58 @@ App/Program2/bin/
172176
App/Program2/obj/
173177
```
174178

175-
## Package references
179+
## Directives for project metadata
176180

177-
It is possible to specify NuGet package references via the `#package` directive.
181+
It is possible to specify some project metadata via [ignored C# directives][ignored-directives].
182+
Directives `sdk`, `package`, and `property` are translated into `<Project Sdk="...">`, `<PackageReference>`, and `<Property>` project elements, respectively.
183+
Other directives result in a warning, reserving them for future use.
178184

179185
```cs
180-
#package Newtonsoft.Json 13.0.1
186+
#:sdk Microsoft.NET.Sdk.Web
187+
#:property TargetFramework=net11.0
188+
#:property LangVersion=preview
189+
#:package System.CommandLine=2.0.0-*
181190
```
182191

183-
The C# language needs to be updated to ignore these directives (instead of failing the compilation).
184-
See [the corresponding language proposal][pound].
185-
186-
If these directives were limited by the language to only appear near the top of the file (similar to `#define` directives),
187-
the dotnet CLI could be more efficient in searching for them.
188-
189-
It should be also possible to look for these directives from the dotnet CLI via a regex instead of parsing the whole C# file via Roslyn.
190-
191-
We do not limit `#package` directives to appear only in entry point files.
192-
Indeed, it might be beneficial to let a non-entry-point file like `Util.cs` be self-contained and have all the `#package`s it needs specified in it,
192+
The value must be separated from the name of the directive by white space and any leading and trailing white space is not considered part of the value.
193+
Any value can optionally have two parts separated by `=` or `/`
194+
(the former is consistent with how properties are usually passed, e.g., `/p:Prop=Value`, and the latter is what the `<Project Sdk="Name/Version">` attribute uses).
195+
The value of the first `#:sdk` is injected into `<Project Sdk="{0}">` with the separator (if any) replaced with `/`,
196+
and the subsequent `#:sdk` directive values are split by the separator and injected as `<Sdk Name="{0}" Version="{1}" />` elements (or without the `Version` attribute if there is no separator).
197+
It is an error if the first part (name) is empty (the version is allowed to be empty, but that results in empty `Version=""`).
198+
The value of `#:property` is split by the separator and injected as `<{0}>{1}</{0}>` in a `<PropertyGroup>`.
199+
It is an error if no separator appears in the value or if the first part (property name) is empty (the property value is allowed to be empty) or contains invalid characters.
200+
The value of `#:package` is split by the separator and injected as `<PackageReference Include="{0}" Version="{1}">` (or without the `Version` attribute if there is no separator) in an `<ItemGroup>`.
201+
It is an error if the first part (package name) is empty (the package version is allowed to be empty, but that results in empty `Version=""`).
202+
203+
Because these directives are limited by the C# language to only appear before the first "C# token" and any `#if`,
204+
dotnet CLI can look for them via a regex or Roslyn lexer without any knowledge of defined conditional symbols
205+
and can do that efficiently by stopping the search when it sees the first "C# token".
206+
207+
We do not limit these directives to appear only in entry point files.
208+
Indeed, it might be beneficial to let a non-entry-point file like `Util.cs` be self-contained and have all the `#:package`s it needs specified in it,
193209
which also makes it possible to share it independently or symlink it to multiple script folders.
194210
This is also similar to `global using`s which users usually put into a single file but don't have to.
195211

196-
We could consider deduplicating `#package` directives (if they have the same version)
197-
so separate "self-contained" utilities can reference overlapping sets of packages
212+
We could consider deduplicating `#:` directives
213+
(e.g., properties could be concatenated via `;`, more specific package versions could override less specific ones),
214+
so for example separate "self-contained" utilities could reference overlapping sets of packages
198215
even if they end up in the same compilation.
199-
But for starters we can simply translate every `#package` directive into `<PackageReference>`
216+
But for starters we can translate each directive into the corresponding project element
200217
and let the existing MSBuild/NuGet logic deal with duplicates.
201218

202-
It is valid to have a `#package` directive without a version.
219+
It is valid to have a `#:package` directive without a version.
203220
That's useful when central package management (CPM) is used.
204221
NuGet will report an appropriate error if the version is missing and CPM is not enabled.
205222

206-
During [grow up](#grow-up), `#package` directives are removed from the `.cs` files and turned into `<PackageReference>` elements in the corresponding `.csproj` files.
207-
For project-based programs, `#package` directives are an error (reported by Roslyn when it's told it is in "project-based" mode).
208-
209-
## SDK directive
210-
211-
We could also recognize `#sdk` directive to allow web file-based programs for example.
212-
213-
```cs
214-
#sdk Microsoft.NET.Sdk.Web
215-
```
216-
217-
It should have similar restrictions as the `#package` directive.
218-
It should also be an error to specify multiple different `#sdk` directives
219-
but it could be allowed to specify the same SDK multiple times similarly to `#package` directives
220-
(again so that self-contained utility files can declare their required SDK).
223+
During [grow up](#grow-up), `#:` directives are removed from the `.cs` files and turned into elements in the corresponding `.csproj` files.
224+
For project-based programs, `#:` directives are an error (reported by Roslyn when it's told it is in "project-based" mode).
225+
`#!` directives are also removed during grow up, although we could consider to have an option to preserve them
226+
(since they might still be valid after grow up, depending on which program they are actually specifying to "interpret" the file, i.e., it might not be `dotnet run` at all).
221227

222228
## Shebang
223229

224-
Along with `#package`, the language can also ignore `#!` which could be then used for [shebang][shebang] support.
230+
Along with `#:`, the language also ignores `#!` which could be then used for [shebang][shebang] support.
225231

226232
```cs
227233
#!/usr/bin/dotnet run
@@ -264,7 +270,7 @@ We could also add `dotnet compile` command that would be the equivalent of `dotn
264270
### `dotnet package add`
265271

266272
Adding package references via `dotnet package add` could be supported for file-based programs as well,
267-
i.e., the command would add a `#package` directive to the top of a `.cs` file.
273+
i.e., the command would add a `#:package` directive to the top of a `.cs` file.
268274

269275
## Implementation
270276

@@ -274,8 +280,8 @@ The build is performed using MSBuild APIs on in-memory project files.
274280

275281
MSBuild invocation can be skipped in subsequent `dotnet run file.cs` invocations if an up-to-date check detects that inputs didn't change.
276282
We always need to re-run MSBuild if implicit build files like `Directory.Build.props` change but
277-
from `.cs` files, the only relevant MSBuild inputs are the `#package` directives,
278-
hence we can first check the `.cs` file timestamps and for those that have changed, compare the sets of `#package` directives.
283+
from `.cs` files, the only relevant MSBuild inputs are the `#:` directives,
284+
hence we can first check the `.cs` file timestamps and for those that have changed, compare the sets of `#:` directives.
279285
If only `.cs` files change, it is enough to invoke `csc.exe` (directly or via a build server)
280286
re-using command-line arguments that the last MSBuild invocation passed to the compiler.
281287
If no inputs change, it is enough to start the target executable without invoking the build at all.
@@ -289,7 +295,7 @@ The plan is to implement the feature in stages (the order might be different):
289295
- Multiple entry points.
290296
- Grow up command.
291297
- Folder support: `dotnet run ./dir/`.
292-
- Package references via `#package`.
298+
- Project metadata via `#:` directives.
293299

294300
## Alternatives
295301

@@ -306,5 +312,5 @@ Instead of implicitly including files from the target directory, the importing c
306312
-->
307313

308314
[artifacts-output]: https://learn.microsoft.com/dotnet/core/sdk/artifacts-output
309-
[pound]: https://github.com/dotnet/csharplang/issues/3507
315+
[ignored-directives]: https://github.com/dotnet/csharplang/blob/main/proposals/ignored-directives.md
310316
[shebang]: https://en.wikipedia.org/wiki/Shebang_%28Unix%29

0 commit comments

Comments
 (0)