Skip to content

Commit 9a1c6fa

Browse files
authored
Add #:project directive (#49311)
1 parent a0515ab commit 9a1c6fa

18 files changed

+335
-116
lines changed

documentation/general/dotnet-run-file.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,16 @@ They are not cleaned immediately because they can be re-used on subsequent runs
163163

164164
It is possible to specify some project metadata via *file-level directives*
165165
which are [ignored][ignored-directives] by the C# language but recognized by the SDK CLI.
166-
Directives `sdk`, `package`, and `property` are translated into `<Project Sdk="...">`, `<PackageReference>`, and `<Property>` project elements, respectively.
166+
Directives `sdk`, `package`, `property`, and `project` are translated into
167+
`<Project Sdk="...">`, `<PackageReference>`, `<PropertyGroup>`, and `<ProjectReference>` project elements, respectively.
167168
Other directives result in an error, reserving them for future use.
168169

169170
```cs
170171
#:sdk Microsoft.NET.Sdk.Web
171172
#:property TargetFramework=net11.0
172173
#:property LangVersion=preview
173174
175+
#:project ../MyLibrary
174176
```
175177

176178
The value must be separated from the kind (`package`/`sdk`/`property`) of the directive by whitespace
@@ -184,6 +186,9 @@ The value of `#:property` is split by the separator and injected as `<{0}>{1}</{
184186
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.
185187
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>`.
186188
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=""`).
189+
The value of `#:project` is injected as `<ProjectReference Include="{0}" />` in an `<ItemGroup>`.
190+
If the value points to an existing directory, a project file is found inside that directory and its path is used instead
191+
(because `ProjectReference` items don't support directory paths).
187192

188193
Because these directives are limited by the C# language to only appear before the first "C# token" and any `#if`,
189194
dotnet CLI can look for them via a regex or Roslyn lexer without any knowledge of defined conditional symbols
@@ -330,8 +335,8 @@ We could also add `dotnet compile` command that would be the equivalent of `dotn
330335
e.g., via `dotnet clean --file-based-program <path-to-entry-point>`
331336
or `dotnet clean --all-file-based-programs`.
332337

333-
Adding package references via `dotnet package add` could be supported for file-based programs as well,
334-
i.e., the command would add a `#:package` directive to the top of a `.cs` file.
338+
Adding references via `dotnet package add`/`dotnet reference add` could be supported for file-based programs as well,
339+
i.e., the command would add a `#:package`/`#:project` directive to the top of a `.cs` file.
335340

336341
### Explicit importing
337342

src/Cli/dotnet/Commands/CliCommandStrings.resx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,10 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man
10461046
<value>Minimum expected tests policy violation, tests ran {0}, minimum expected {1}</value>
10471047
<comment>{0}, {1} number of tests</comment>
10481048
</data>
1049+
<data name="InvalidProjectDirective" xml:space="preserve">
1050+
<value>The '#:project' directive at '{0}' is invalid: {1}</value>
1051+
<comment>{0} is the file path and line number. {1} is the inner error message.</comment>
1052+
</data>
10491053
<data name="MissingDirectiveName" xml:space="preserve">
10501054
<value>Missing name of '{0}' at {1}.</value>
10511055
<comment>{0} is the directive name like 'package' or 'sdk', {1} is the file path and line number.</comment>

src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs

Lines changed: 117 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,56 @@ internal sealed class VirtualProjectBuildingCommand : CommandBase
5858
"MSBuild.rsp",
5959
];
6060

61+
internal static readonly string TargetOverrides = """
62+
<!--
63+
Override targets which don't work with project files that are not present on disk.
64+
See https://github.com/NuGet/Home/issues/14148.
65+
-->
66+
67+
<Target Name="_FilterRestoreGraphProjectInputItems"
68+
DependsOnTargets="_LoadRestoreGraphEntryPoints">
69+
<!-- No-op, the original output is not needed by the overwritten targets. -->
70+
</Target>
71+
72+
<Target Name="_GetAllRestoreProjectPathItems"
73+
DependsOnTargets="_FilterRestoreGraphProjectInputItems;_GenerateRestoreProjectPathWalk"
74+
Returns="@(_RestoreProjectPathItems)">
75+
<!-- Output from dependency _GenerateRestoreProjectPathWalk. -->
76+
</Target>
77+
78+
<Target Name="_GenerateRestoreGraph"
79+
DependsOnTargets="_FilterRestoreGraphProjectInputItems;_GetAllRestoreProjectPathItems;_GenerateRestoreGraphProjectEntry;_GenerateProjectRestoreGraph"
80+
Returns="@(_RestoreGraphEntry)">
81+
<!-- Output partly from dependency _GenerateRestoreGraphProjectEntry and _GenerateProjectRestoreGraph. -->
82+
83+
<ItemGroup>
84+
<_GenerateRestoreGraphProjectEntryInput Include="@(_RestoreProjectPathItems)" Exclude="$(MSBuildProjectFullPath)" />
85+
</ItemGroup>
86+
87+
<MSBuild
88+
BuildInParallel="$(RestoreBuildInParallel)"
89+
Projects="@(_GenerateRestoreGraphProjectEntryInput)"
90+
Targets="_GenerateRestoreGraphProjectEntry"
91+
Properties="$(_GenerateRestoreGraphProjectEntryInputProperties)">
92+
93+
<Output
94+
TaskParameter="TargetOutputs"
95+
ItemName="_RestoreGraphEntry" />
96+
</MSBuild>
97+
98+
<MSBuild
99+
BuildInParallel="$(RestoreBuildInParallel)"
100+
Projects="@(_GenerateRestoreGraphProjectEntryInput)"
101+
Targets="_GenerateProjectRestoreGraph"
102+
Properties="$(_GenerateRestoreGraphProjectEntryInputProperties)">
103+
104+
<Output
105+
TaskParameter="TargetOutputs"
106+
ItemName="_RestoreGraphEntry" />
107+
</MSBuild>
108+
</Target>
109+
""";
110+
61111
private ImmutableArray<CSharpDirective> _directives;
62112

63113
public VirtualProjectBuildingCommand(
@@ -468,6 +518,7 @@ public static void WriteProjectFile(
468518
var sdkDirectives = directives.OfType<CSharpDirective.Sdk>();
469519
var propertyDirectives = directives.OfType<CSharpDirective.Property>();
470520
var packageDirectives = directives.OfType<CSharpDirective.Package>();
521+
var projectDirectives = directives.OfType<CSharpDirective.Project>();
471522

472523
string sdkValue = "Microsoft.NET.Sdk";
473524

@@ -607,6 +658,25 @@ public static void WriteProjectFile(
607658
writer.WriteLine(" </ItemGroup>");
608659
}
609660

661+
if (projectDirectives.Any())
662+
{
663+
writer.WriteLine("""
664+
665+
<ItemGroup>
666+
""");
667+
668+
foreach (var projectReference in projectDirectives)
669+
{
670+
writer.WriteLine($"""
671+
<ProjectReference Include="{EscapeValue(projectReference.Name)}" />
672+
""");
673+
674+
processedDirectives++;
675+
}
676+
677+
writer.WriteLine(" </ItemGroup>");
678+
}
679+
610680
Debug.Assert(processedDirectives + directives.OfType<CSharpDirective.Shebang>().Count() == directives.Length);
611681

612682
if (isVirtualProject)
@@ -643,35 +713,8 @@ public static void WriteProjectFile(
643713
""");
644714
}
645715

646-
writer.WriteLine("""
647-
648-
<!--
649-
Override targets which don't work with project files that are not present on disk.
650-
See https://github.com/NuGet/Home/issues/14148.
651-
-->
652-
653-
<Target Name="_FilterRestoreGraphProjectInputItems"
654-
DependsOnTargets="_LoadRestoreGraphEntryPoints"
655-
Returns="@(FilteredRestoreGraphProjectInputItems)">
656-
<ItemGroup>
657-
<FilteredRestoreGraphProjectInputItems Include="@(RestoreGraphProjectInputItems)" />
658-
</ItemGroup>
659-
</Target>
660-
661-
<Target Name="_GetAllRestoreProjectPathItems"
662-
DependsOnTargets="_FilterRestoreGraphProjectInputItems"
663-
Returns="@(_RestoreProjectPathItems)">
664-
<ItemGroup>
665-
<_RestoreProjectPathItems Include="@(FilteredRestoreGraphProjectInputItems)" />
666-
</ItemGroup>
667-
</Target>
668-
669-
<Target Name="_GenerateRestoreGraph"
670-
DependsOnTargets="_FilterRestoreGraphProjectInputItems;_GetAllRestoreProjectPathItems;_GenerateRestoreGraphProjectEntry;_GenerateProjectRestoreGraph"
671-
Returns="@(_RestoreGraphEntry)">
672-
<!-- Output from dependency _GenerateRestoreGraphProjectEntry and _GenerateProjectRestoreGraph -->
673-
</Target>
674-
""");
716+
writer.WriteLine();
717+
writer.WriteLine(TargetOverrides);
675718
}
676719

677720
writer.WriteLine("""
@@ -943,16 +986,22 @@ private CSharpDirective() { }
943986
"sdk" => Sdk.Parse(errors, sourceFile, span, directiveKind, directiveText),
944987
"property" => Property.Parse(errors, sourceFile, span, directiveKind, directiveText),
945988
"package" => Package.Parse(errors, sourceFile, span, directiveKind, directiveText),
989+
"project" => Project.Parse(errors, sourceFile, span, directiveText),
946990
_ => ReportError<Named>(errors, sourceFile, span, string.Format(CliCommandStrings.UnrecognizedDirective, directiveKind, sourceFile.GetLocationString(span))),
947991
};
948992
}
949993

950994
private static T? ReportError<T>(ImmutableArray<SimpleDiagnostic>.Builder? errors, SourceFile sourceFile, TextSpan span, string message, Exception? inner = null)
995+
{
996+
ReportError(errors, sourceFile, span, message, inner);
997+
return default;
998+
}
999+
1000+
private static void ReportError(ImmutableArray<SimpleDiagnostic>.Builder? errors, SourceFile sourceFile, TextSpan span, string message, Exception? inner = null)
9511001
{
9521002
if (errors != null)
9531003
{
9541004
errors.Add(new SimpleDiagnostic { Location = sourceFile.GetFileLinePositionSpan(span), Message = message });
955-
return default;
9561005
}
9571006
else
9581007
{
@@ -1088,6 +1137,44 @@ private Package() { }
10881137
};
10891138
}
10901139
}
1140+
1141+
/// <summary>
1142+
/// <c>#:project</c> directive.
1143+
/// </summary>
1144+
public sealed class Project : Named
1145+
{
1146+
private Project() { }
1147+
1148+
public static Project Parse(ImmutableArray<SimpleDiagnostic>.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveText)
1149+
{
1150+
try
1151+
{
1152+
// If the path is a directory like '../lib', transform it to a project file path like '../lib/lib.csproj'.
1153+
// Also normalize blackslashes to forward slashes to ensure the directive works on all platforms.
1154+
var sourceDirectory = Path.GetDirectoryName(sourceFile.Path) ?? ".";
1155+
var resolvedProjectPath = Path.Combine(sourceDirectory, directiveText.Replace('\\', '/'));
1156+
if (Directory.Exists(resolvedProjectPath))
1157+
{
1158+
var fullFilePath = MsbuildProject.GetProjectFileFromDirectory(resolvedProjectPath).FullName;
1159+
directiveText = Path.GetRelativePath(relativeTo: sourceDirectory, fullFilePath);
1160+
}
1161+
else if (!File.Exists(resolvedProjectPath))
1162+
{
1163+
throw new GracefulException(CliStrings.CouldNotFindProjectOrDirectory, resolvedProjectPath);
1164+
}
1165+
}
1166+
catch (GracefulException e)
1167+
{
1168+
ReportError(errors, sourceFile, span, string.Format(CliCommandStrings.InvalidProjectDirective, sourceFile.GetLocationString(span), e.Message), e);
1169+
}
1170+
1171+
return new Project
1172+
{
1173+
Span = span,
1174+
Name = directiveText,
1175+
};
1176+
}
1177+
}
10911178
}
10921179

10931180
/// <summary>

src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)