-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Process C# directives in file-based programs #47702
base: main
Are you sure you want to change the base?
Conversation
if (virtualProjectFile) | ||
{ | ||
writer.WriteLine($""" | ||
<Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried using XmlWriter for this, but couldn't make it write indented XML with blank lines between sections. MSBuild APIs also seem unable to do that. So I ended up building the project file as string.
@@ -35,13 +37,29 @@ public override int Execute() | |||
throw new GracefulException(LocalizableStrings.DirectoryAlreadyExists, targetDirectory); | |||
} | |||
|
|||
// Find directives (this can fail, so do this before creating the target directory). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a stupid example, but if you make .cs (a C# file with no name) then call this on it, I think it would fail? Not sure we really care. More relevant, though, I think it would be good to have some error handling logic including reverting state where relevant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a stupid example, but if you make .cs (a C# file with no name) then call this on it, I think it would fail?
You're right, good catch. It will fail with an error saying the target directory already exists because the target directory will be the current directory. I guess we could add a dedicated error message for this case, but it doesn't seem necessary. Users will be able to work around by specifying --output
. Although the generated project will also have an empty name, so probably won't work (but users can rename it and then it will work).
More relevant, though, I think it would be good to have some error handling logic including reverting state where relevant.
That shouldn't be needed in normal scenarios. That's why all the checks that can fail are done up front, and only then we do the file modifications (creating target directory, moving files over). If any of the checks fail, we fail early. If the checks succeed but any of those file operations fail for some reason, I guess we could abort and try to cleanup. The cleanup could also fail though. I think it might be easier to leave the revert to the user in the rare(?) case it's needed.
"sdk" => Sdk.Parse(sourceFile, span, name, value), | ||
"property" => Property.Parse(sourceFile, span, name, value), | ||
"package" => Package.Parse(sourceFile, span, name, value), | ||
_ => throw new GracefulException(LocalizableStrings.UnrecognizedDirective, name, sourceFile.GetLocationString(span)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are other directives that can just stay in the C# file, so I think it'd be best to just not make a directive in this case (and not remove it from the C# file)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only handling #:
directives and there shouldn't be any other than those three. (I.e., this code path is not reachable for existing C# directives like #pragma
.) We want to error on unknown #:
directives to reserve them for future use.
|
||
public static void RemoveDirectivesFromFile(ImmutableArray<CSharpDirective> directives, SourceText text, string filePath) | ||
{ | ||
if (RemoveDirectivesFromFile(directives, text) is { } modifiedText) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what case is this false? And should it mention to the caller that it failed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is false
if RemoveDirectivesFromFile(directives, text)
returns null
which happens if there are no directives, i.e., no changes to the file are necessary. The method could return bool
, sure, but currently the caller doesn't need it. It is not a failure, it is just a no-op.
_directives = FindDirectives(sourceFile); | ||
|
||
// If there were any `#:` directives, remove them from the file. | ||
// (This is temporary until Roslyn is updated to ignore them.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason not to remove them if roslyn would ignore them anyway? Just wondering why this wouldn't be permanent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because removing them changes line numbers, so for example location of errors reported by Roslyn is off. That's just one example, there are probably others where inconsistencies between the original file and the file passed to Roslyn would lead to unexpected behavior, like #pragma checksum
.
Another reason is performance - now during build we need to create a copy of the file with the directives stripped out and pass that to the compiler instead of the original file, that's just an unnecessary perf hit.
if (_directives.Length != 0) | ||
{ | ||
var targetDirectory = Path.Join(Path.GetDirectoryName(_targetFilePath), "obj"); | ||
Directory.CreateDirectory(targetDirectory); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the idea to make a .targets holding the directives information? That seems an odd choice to me compared with just putting them in the csproj
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this has nothing to do with .targets
. I'm assuming you are referring to targetDirectory
. Given dotnet run ./app/file.cs
, targetDirectory will be ./app/obj/
where we will place ./app/obj/file.cs
that has the directives stripped out so we can pass it to Roslyn. This is just temporary, we will skip all this when Roslyn recognizes the directives (i.e., doesn't error on them).
"""); | ||
} | ||
|
||
foreach (var sdk in sdkDirectives.Skip(1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be bad design-wise to just say that you can't have multiple sdks in a file? I can't think of a normal use case where you shouldn't just upgrade to a project file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was my original intention but Damian pointed out that multiple sdks are common for example in aspire projects:
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
</Project>
and those can be otherwise very simple project files so it seems unnecessary to restrict them from file-based design.
|
||
"""); | ||
|
||
foreach (var sdk in sdkDirectives) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't have to Skip(1)? And perhaps this is nitpicky, but shouldn't you go in reverse order?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't have to Skip(1)?
No, that's only needed when the first Sdk is put into <Project Sdk>
attribute but that never happens for virtual project files. Instead they get all sdks emitted as <Import Project="Sdk.props" />
elements at the beginning and <Import Project="Sdk.targets" />
at the end.
shouldn't you go in reverse order?
I considered that but as far as I can tell, the following
<Project>
<Sdk Name="1" />
<Sdk Name="2" />
</Project>
is equivalent to
<Project>
<Import Project="Sdk.props" Sdk="1" />
<Import Project="Sdk.props" Sdk="2" />
<Import Project="Sdk.targets" Sdk="1" />
<Import Project="Sdk.targets" Sdk="2" />
</Project>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here's the MSBuild logic for these implicit imports: https://github.com/dotnet/msbuild/blob/f1b335d0aa9423c304b5cb186bad40935a6e2f2b/src/Build/Construction/ProjectRootElement.cs#L1903-L1932
followed by https://github.com/dotnet/msbuild/blob/f1b335d0aa9423c304b5cb186bad40935a6e2f2b/src/Build/Definition/Project.cs#L2860-L2883 for the actual loading.
Short version: @jjonescz is correct here. it's not a nesting-doll model, it's an in-order Import for both the Sdk.props and Sdk.targets entrypoints.
"""); | ||
} | ||
|
||
writer.WriteLine(""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you still need this part if we're making a real project file on disk?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is inside if (virtualProjectFile)
, it won't be emitted for real project files.
Note that this method (WriteProjectFile
) is reused for both virtual project files (dotnet run file
scenarios) and physical project files (dotnet project convert
scenarios) to avoid duplicating the logic. Of course, there are some conditions like if (virtualProjectFile)
, so the sharing isn't 100%, but that should all go away when NuGet targets are fixed to handle virtual project files and we don't need the hacky Target overrides anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good from my point of view 👍
@RikkiGibson @chsienki @MiYanni for reviews, thanks |
https://github.com/dotnet/sdk/blob/main/documentation/general/dotnet-run-file.md#directives-for-project-metadata