Skip to content

Commit 90d236a

Browse files
committed
Merge branch 'main' into DotNetFileScopeNamespace
# Conflicts: # src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs # src/Cli/dotnet/ReleasePropertyProjectLocator.cs # src/Cli/dotnet/commands/dotnet-run/Program.cs # src/Cli/dotnet/commands/dotnet-run/RunCommand.cs # src/Cli/dotnet/commands/dotnet-test/CliConstants.cs # src/Cli/dotnet/commands/dotnet-test/MSBuildUtility.cs # src/Cli/dotnet/commands/dotnet-test/Options.cs # src/Cli/dotnet/commands/dotnet-test/SolutionAndProjectUtility.cs # src/Cli/dotnet/commands/dotnet-test/TestCommandParser.cs # src/Cli/dotnet/commands/dotnet-workload/restore/WorkloadRestoreCommand.cs
2 parents d1927a0 + 9c64e2f commit 90d236a

File tree

90 files changed

+2903
-1104
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+2903
-1104
lines changed

Directory.Build.props

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838
<IsDebianBaseDistro Condition="$(HostRid.StartsWith('ubuntu')) OR $(HostRid.StartsWith('debian'))">true</IsDebianBaseDistro>
3939
<IsRPMBasedDistro Condition="$(HostRid.StartsWith('rhel'))">true</IsRPMBasedDistro>
4040
<IsRPMBasedDistro Condition="$(HostRid.StartsWith('centos'))">true</IsRPMBasedDistro>
41+
42+
<OSName Condition="'$(OSName)' == '' AND $(Rid) != ''">$(Rid.Substring(0, $(Rid.LastIndexOf('-'))))</OSName>
43+
<OSName Condition="'$(OSName)' == ''">$(HostOSName)</OSName>
44+
<Rid>$(OSName)-$(Architecture)</Rid>
45+
<ProductMonikerRid Condition="'$(ProductMonikerRid)' == ''">$(Rid)</ProductMonikerRid>
46+
47+
<PgoTerm Condition="'$(PgoInstrument)' == 'true'">-pgo</PgoTerm>
4148
</PropertyGroup>
4249

4350
<PropertyGroup>
@@ -59,6 +66,7 @@
5966
</PropertyGroup>
6067

6168
<PropertyGroup>
69+
<PackageProjectUrl>https://github.com/dotnet/sdk</PackageProjectUrl>
6270
<PackageLicenseExpression>MIT</PackageLicenseExpression>
6371
<LangVersion>Latest</LangVersion>
6472
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
<PackageVersion Include="Microsoft.VisualStudio.Sdk" Version="17.2.32505.173" />
7171
<PackageVersion Include="Microsoft.VSSDK.BuildTools" Version="17.11.435" />
7272
<PackageVersion Include="Microsoft.VisualStudio.Setup.Configuration.Interop" Version="$(MicrosoftVisualStudioSetupConfigurationInteropVersion)" />
73-
<PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="1.0.28" />
73+
<PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="$(MicrosoftVisualStudioSolutionPersistenceVersion)" />
7474
<PackageVersion Include="Microsoft.Web.Deployment" Version="$(WebDeploymentPackageVersion)" />
7575
<PackageVersion Include="Microsoft.Web.Xdt" Version="$(MicrosoftWebXdtPackageVersion)" />
7676
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="$(MicrosoftWin32SystemEventsPackageVersion)" />
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# `dotnet run file.cs`
2+
3+
This is a proposal for extending the dotnet CLI to allow running C# source files with no need for an explicit backing project.
4+
We call these *file-based programs* (as opposed to *project-based programs*).
5+
6+
```ps1
7+
dotnet run file.cs
8+
```
9+
10+
> [!NOTE]
11+
> This document describes the ideal final state, but the feature will be implemented in [stages](#stages).
12+
13+
> [!CAUTION]
14+
> The current implementation has been limited to single file support for the initial preview
15+
> (as if the implicit project file had `<EnableDefaultItems>false</EnableDefaultItems>` and an explicit `<Compile Include="file.cs" />`),
16+
> but this proposal describes a situation where all files in the target directory are included.
17+
> Once a final decision is made, the proposal will be updated.
18+
19+
## Motivation
20+
21+
File-based programs
22+
- should be a viable alternative to using PowerShell/bash scripts in .NET repos, and
23+
- lower the entry barrier for new customers.
24+
25+
## Guiding principle
26+
27+
The overarching guiding principle is that file-based programs have a simple and reliable [grow up](#grow-up) story to project-based programs.
28+
Previous file-based approaches like scripting are a variant of C# and as such have no simple and reliable grow up story.
29+
30+
## Implicit project file
31+
32+
The [guiding principle](#guiding-principle) implies that we can think of file-based programs as having an implicit project file.
33+
34+
The implicit project file is the default project that would be created by running `dotnet new console`.
35+
This means that the behavior of `dotnet run file.cs` can change between SDK versions if the `dotnet new console` template changes.
36+
In the future we can consider supporting more SDKs like the Web SDK.
37+
38+
## Grow up
39+
40+
When file-based programs reach an inflection point where build customizations in a project file are needed,
41+
a single CLI command can be executed to generate a project file.
42+
In fact, this command simply materializes the [implicit project file](#implicit-project-file) on disk.
43+
This action should not change the behavior of the target program.
44+
45+
```ps1
46+
dotnet project add
47+
```
48+
49+
## Target path
50+
51+
The path passed to `dotnet run ./some/path.cs` is called *the target path*.
52+
If it is a file, it is called *the target file*.
53+
*The target directory* is the directory of the target file, or the target path if it is not a file.
54+
55+
We can consider adding an option like `dotnet run --from-stdin` which would read the C# file from the standard input.
56+
In this case, the current working directory would not be used to search for project or other C# files,
57+
the compilation would consist solely of the single file read from the standard input.
58+
Similarly, it could be possible to specify the whole C# source text in a command-like argument
59+
like `dotnet run --code 'Console.WriteLine("Hi")'`.
60+
61+
## Integration into the existing `dotnet run` command
62+
63+
`dotnet run file.cs` already has a meaning if there is a project file inside the current directory,
64+
specifically `file.cs` is passed as the first command-line argument to the target program.
65+
We preserve this behavior to avoid a breaking change.
66+
The file-based build and run kicks in only when:
67+
- a project file cannot be found (in the current directory or via the `--project` option), and
68+
- if the target path is a file, it has the `.cs` file extension, and
69+
- the target path (file or directory) exists.
70+
71+
> [!NOTE]
72+
> This means that `dotnet run path` stops working when a file-based program [grows up](#grow-up) into a project-based program.
73+
>
74+
> Users could avoid that by using `cd path; dotnet run` instead. For that to work always (before and after grow up),
75+
> `dotnet run` without a `--project` argument and without a project file in the current directory
76+
> would need to search for a file-based program in the current directory instead of failing.
77+
>
78+
> We can also consider adding some universal option that would work with both project-based and file-based programs,
79+
> like `dotnet run --directory ./dir/`. For inspiration, `dotnet test` also has a `--directory` option.
80+
> Although users might expect there to be a `--file` option, as well. Both could be unified as `--path`.
81+
>
82+
> If we want to also support [multi-entry-point scenarios](#multiple-entry-points),
83+
> we might need an option like `dotnet run --entry ./dir/name`
84+
> which would work for both `./dir/name.cs` and `./dir/name/name.csproj`.
85+
86+
File-based programs are processed by `dotnet run` equivalently to project-based programs unless specified otherwise in this document.
87+
For example, the remaining command-line arguments after the first argument (the target path) are passed through to the target app
88+
(except for the arguments recognized by `dotnet run` unless they are after the `--` separator).
89+
90+
## Entry points
91+
92+
If a file is given to `dotnet run`, it has to be an *entry-point file*, otherwise an error is reported.
93+
We want to report an error for non-entry-point files to avoid the confusion of being able to `dotnet run util.cs`.
94+
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.
99+
100+
Because of the [implicit project file](#implicit-project-file),
101+
other files in the target directory or its subdirectories are included in the compilation.
102+
For example, other `.cs` files but also `.resx` (embedded resources).
103+
Similarly, implicit build files like `Directory.Build.props` or `Directory.Packages.props` are used during the build.
104+
105+
> [!NOTE]
106+
> Performance issues might arise if there are many [nested files](#nested-files) (possibly unintentionally),
107+
> and also it might not be clear to users that `dotnet run file.cs` will include other `.cs` files in the compilation.
108+
> Therefore we could consider some switch (a command-line option and/or a `#` language directive) to enable/disable this behavior.
109+
> When disabled, [grow up](#grow-up) would generate projects in subdirectories similarly to [multi-entry-point scenarios](#multiple-entry-points)
110+
> to preserve the behavior.
111+
112+
### Nested files
113+
114+
If there are nested project files like
115+
```
116+
App/File.cs
117+
App/Nested/Nested.csproj
118+
App/Nested/File.cs
119+
```
120+
executing `dotnet run app/file.cs` includes the nested `.cs` file in the compilation.
121+
That might be unexpected, hence we could consider reporting an error in such situation.
122+
However, the same problem exists for normal builds with explicit project files
123+
and usually the build fails because there are multiple entry points or other clashes.
124+
125+
Similarly, we could report an error if there are many nested directories and files,
126+
so for example if someone puts a C# file into `C:/sources`
127+
and executes `dotnet run C:/sources/file.cs` or opens that in the IDE, we do not walk all user's sources.
128+
Again, this problem exists with project-based programs as well.
129+
Note that having a project-based or file-based program in the drive root would result in
130+
[error MSB5029](https://learn.microsoft.com/visualstudio/msbuild/errors/msb5029).
131+
132+
### Multiple entry points
133+
134+
If there are multiple entry-point files in the target directory, the target path must be a file
135+
(an error is reported if it points to a directory instead).
136+
Then the build ignores other entry-point files.
137+
138+
Thanks to this, it is possible to have a structure like
139+
```
140+
App/Util.cs
141+
App/Program1.cs
142+
App/Program2.cs
143+
```
144+
where either `Program1.cs` or `Program2.cs` can be run and both of them have access to `Util.cs`.
145+
146+
In this case, there are multiple implicit projects
147+
(and during [grow up](#grow-up), multiple project files are materialized
148+
and the original C# files are moved to the corresponding project subdirectories):
149+
```
150+
App/Shared/Util.cs
151+
App/Program1/Program1.cs
152+
App/Program1/Program1.csproj
153+
App/Program2/Program2.cs
154+
App/Program2/Program2.csproj
155+
```
156+
157+
The generated folders might need to be named differently to avoid clashes with existing folders.
158+
159+
The entry-point projects (`Program1` and `Program2` in our example)
160+
have the shared `.cs` files source-included via `<Content Include="../Shared/**/*.cs" />`.
161+
We could consider having the projects directly in the top-level folder instead
162+
but that might result in clashes of build outputs that are not project-scoped, like `project.assets.json`.
163+
If we did that though, it would be enough to exclude the other entry points rather than including all the shared `.cs` files.
164+
165+
Unless the [artifacts output layout][artifacts-output] is used (which is recommended),
166+
those implicit projects mean that build artifacts are placed under those implicit directories
167+
even though they don't exist on disk prior to build:
168+
```
169+
App/Program1/bin/
170+
App/Program1/obj/
171+
App/Program2/bin/
172+
App/Program2/obj/
173+
```
174+
175+
## Package references
176+
177+
It is possible to specify NuGet package references via the `#package` directive.
178+
179+
```cs
180+
#package Newtonsoft.Json 13.0.1
181+
```
182+
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,
193+
which also makes it possible to share it independently or symlink it to multiple script folders.
194+
This is also similar to `global using`s which users usually put into a single file but don't have to.
195+
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
198+
even if they end up in the same compilation.
199+
But for starters we can simply translate every `#package` directive into `<PackageReference>`
200+
and let the existing MSBuild/NuGet logic deal with duplicates.
201+
202+
It is valid to have a `#package` directive without a version.
203+
That's useful when central package management (CPM) is used.
204+
NuGet will report an appropriate error if the version is missing and CPM is not enabled.
205+
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).
221+
222+
## Shebang
223+
224+
Along with `#package`, the language can also ignore `#!` which could be then used for [shebang][shebang] support.
225+
226+
```cs
227+
#!/usr/bin/dotnet run
228+
Console.WriteLine("Hello");
229+
```
230+
231+
It might be beneficial to also ship `dotnet-run` binary
232+
(or `dotnet-run-file` that would only work with file-based programs, not project-based ones, perhaps simply named `cs`)
233+
because some shells do not support multiple command-line arguments in the shebang
234+
which is needed if one wants to use `/usr/bin/env` to find the `dotnet` executable
235+
(although `-S` argument can be sometimes used to enable multiple argument support):
236+
237+
```cs
238+
#!/usr/bin/env dotnet run
239+
// ^ Might not work in all shells. "dotnet run" might be passed as a single argument to "env".
240+
```
241+
```cs
242+
#!/usr/bin/env dotnet-run
243+
// ^ Should work in all shells.
244+
```
245+
```cs
246+
#!/usr/bin/env -S dotnet run
247+
// ^ Workaround in some shells.
248+
```
249+
250+
We could also consider making `dotnet file.cs` work because `dotnet file.dll` also works today
251+
but that would require changes to the native dotnet host.
252+
253+
## Other commands
254+
255+
We can consider supporting other commands like `dotnet build`, `dotnet pack`, `dotnet watch`.
256+
257+
These commands need to have a way to receive the target path similarly to `dotnet run`,
258+
e.g., via options like `--directory`/`--entry` as described [above](#integration-into-the-existing-dotnet-run-command),
259+
or as the first argument if it makes sense for them.
260+
261+
We could also add `dotnet compile` command that would be the equivalent of `dotnet build` but for file-based programs
262+
(because "compiling" might make more sense for file-based programs than "building").
263+
264+
### `dotnet package add`
265+
266+
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.
268+
269+
## Implementation
270+
271+
The build is performed using MSBuild APIs on in-memory project files.
272+
273+
### Optimizations
274+
275+
MSBuild invocation can be skipped in subsequent `dotnet run file.cs` invocations if an up-to-date check detects that inputs didn't change.
276+
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.
279+
If only `.cs` files change, it is enough to invoke `csc.exe` (directly or via a build server)
280+
re-using command-line arguments that the last MSBuild invocation passed to the compiler.
281+
If no inputs change, it is enough to start the target executable without invoking the build at all.
282+
283+
### Stages
284+
285+
The plan is to implement the feature in stages (the order might be different):
286+
287+
- Bare bones `dotnet run file.cs` support: only files, not folders; a single entry-point; no optimizations.
288+
- Optimizations (caching / up-to-date check).
289+
- Multiple entry points.
290+
- Grow up command.
291+
- Folder support: `dotnet run ./dir/`.
292+
- Package references via `#package`.
293+
294+
## Alternatives
295+
296+
### Explicit importing
297+
298+
Instead of implicitly including files from the target directory, the importing could be explicit, like via a directive:
299+
300+
```cs
301+
#import ./another-file.cs
302+
```
303+
304+
<!--
305+
## Links
306+
-->
307+
308+
[artifacts-output]: https://learn.microsoft.com/dotnet/core/sdk/artifacts-output
309+
[pound]: https://github.com/dotnet/csharplang/issues/3507
310+
[shebang]: https://en.wikipedia.org/wiki/Shebang_%28Unix%29

documentation/project-docs/SDK-PR-guide.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,16 @@ All monthly servicing releases are done of our internal branches in case there a
6363
That is why we have removed all servicing builds from the installer main page as those builds do not include any changes from any repo other than the installer repo so are very limited.
6464
Internal codeflow is merged into public GitHub repos on patch Tuesday each month to ensure we are updated.
6565

66+
## Inter-branch codeflow
67+
In the SDK repo, we have automated codeflow set up for each non-preview release branch flowing from the oldest branch all the way to main. You can see the configuration for that in [github-merge-flow.jsonc](https://github.com/dotnet/sdk/blob/main/github-merge-flow.jsonc). The workflow configuration is in [inter-branch-merge-flow.yml](https://github.com/dotnet/sdk/blob/main/.github/workflows/inter-branch-merge-flow.yml)
68+
You can see the existing open branch codeflows [here](https://github.com/dotnet/sdk/issues?q=is%3Apr%20is%3Aopen%20author%3Aapp%2Fgithub-actions%20%20Merge%20branch)
69+
70+
### Further updates
71+
These PRs will not get updated as new changes come in. This allows time to run PR checks, review, and merge. Once the existing PR is merged and all changes since that PR was created will be generated in a new PR. This leads to [failed](https://github.com/dotnet/sdk/actions/workflows/inter-branch-merge-flow.yml) workflow automation but that's expected. The workflow will report this message: _hint: Updates were rejected because the tip of your current branch is behind_.
72+
73+
### Reviewing and merging inter-branch codeflow
74+
Generally we will check the commit list and scan the changes for anything out of the ordinary. We take a special look for any conflict merges that were done in getting the PR building.
75+
> [!Important]
76+
> Make sure to create a merge commit. *Do not squash*. If you squash, the next inter-branch PR will list all of the same commits.
77+
78+
**NOTE** Some inter-branch flow will have 0 changes once the merge conflicts are resolved. This is likely when the only changes were to version numbers in the eng/*. These can be closed to save time and resources or merged to catch the commit history up (the next codeflow will have fewer commits). There is no preference either way.

0 commit comments

Comments
 (0)