Skip to content

Commit 6bec658

Browse files
committed
Add support for #:sdk directive and no restore failures
The SDK mode allows us to support the #:sdk directive such that our SDK imports the generated .sdk.props and .sdk.targets files. It can also set the required `ImportProjectExtension*` properties for automatically importing the targets during restore (if present in the base intermediate output path). Even with the added properties, the situation on initial clone/run of a project using SmallSharp was suboptimal: - whether in SDK mode or package mode, we'd need a second restore after the initial one and first EmitTargets run. VS does this automatically since it would detect the addition of new extension targets (due to the ImportProjectExtension* props) and restore again as needed. But it wouldn't work from the CLI - this meant a dotnet build would fail consistently So we have a dual mechanism that makes this seamless: - In SDK mode we can optimize things by just parsing the startup/active file and injecting the package references before the restore graph is generated - In package mode, we re-run restore and temporarily redirect the assets file to a different dynamic one to refresh the resolved assets. Fixes #143
1 parent 858c127 commit 6bec658

File tree

8 files changed

+234
-65
lines changed

8 files changed

+234
-65
lines changed

readme.md

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ To pay the Maintenance Fee, [become a Sponsor](https://github.com/sponsors/devlo
2828

2929
C# [top-level programs](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements)
3030
allow a very intuitive, simple and streamlined experience for quickly spiking or learning C#.
31-
The addition of [dotnet run app.cs](https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/) in
32-
.NET 10 takes this further by allowing package references and even MSBuild properties to be
31+
The addition of [file-based apps](https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/) in
32+
.NET 10 [takes this further](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives#file-based-apps) by allowing package references and even MSBuild properties to be
3333
specified per file:
3434

3535
```csharp
3636
37+
#:property LangVersion=preview
3738

3839
using Humanizer;
3940

@@ -70,42 +71,57 @@ files in subdirectories and those will behave like normal compile items.
7071
## Usage
7172

7273
SmallSharp works by just installing the
73-
[SmallSharp](https://nuget.org/packages/SmallSharp) nuget package in a C# console project.
74-
75-
Recommended installation as an SDK:
74+
[SmallSharp](https://nuget.org/packages/SmallSharp) nuget package in a C# console project
75+
and adding a couple extra properties to the project file:
7676

7777
```xml
7878
<Project Sdk="Microsoft.NET.Sdk">
7979

80-
<Sdk Name="SmallSharp" Version="2.0.0" />
81-
8280
<PropertyGroup>
8381
<OutputType>Exe</OutputType>
8482
<TargetFramework>net10.0</TargetFramework>
83+
84+
<!-- 👇 additional properties required in package mode -->
85+
<ImportProjectExtensionProps>true</ImportProjectExtensionProps>
86+
<ImportProjectExtensionTargets>true</ImportProjectExtensionTargets>
8587
</PropertyGroup>
8688

89+
<ItemGroup>
90+
<PackageReference Include="SmallSharp" Version="*" PrivateAssets="all" />
91+
</ItemGroup>
92+
8793
</Project>
8894
```
8995

90-
Or as a regular package reference:
96+
If your file-based apps use the `#:sdk` [directive](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives#file-based-apps),
97+
you need to add SmallSharp as an SDK reference instead so the SDK is picked up by the
98+
generated targets/props instead of the project file. You also don't need the additional
99+
properties since the SDK mode sets them automatically for you:
100+
91101
```xml
92-
<Project Sdk="Microsoft.NET.Sdk">
102+
<Project Sdk="SmallSharp/2.1.0">
93103

94104
<PropertyGroup>
95105
<OutputType>Exe</OutputType>
96106
<TargetFramework>net10.0</TargetFramework>
97107
</PropertyGroup>
98108

99-
<ItemGroup>
100-
<PackageReference Include="SmallSharp" Version="*" />
101-
</ItemGroup>
102-
103109
</Project>
104110
```
105111

112+
> [!IMPORTANT]
113+
> If no `#:sdk` directive is provided by a specific C# file-based app, the `Microsoft.NET.SDK` will be
114+
> used by default in this SDK mode.
115+
106116
Keep adding as many top-level programs as you need, and switch between them easily by simply
107117
selecting the desired file from the Start button dropdown.
108118

119+
When running from the command-line, you can select the file to run by passing it as an argument to `dotnet run`:
120+
121+
```bash
122+
dotnet run -p:ActiveFile program1.cs
123+
```
124+
109125
## How It Works
110126

111127
This nuget package leverages in concert the following standalone and otherwise
@@ -116,6 +132,7 @@ unrelated features of the compiler, nuget and MSBuild:
116132
3. Whenever changed, the dropdown selection is persisted as the `$(ActiveDebugProfile)` MSBuild property in a file
117133
named after the project with the `.user` extension
118134
4. This file is imported before NuGet-provided MSBuild targets
135+
5. VS ignores `#:` directives when adding the flag `FileBasedProgram` to the `$(Features)` project property.
119136

120137
Using the above features in concert, **SmallSharp** essentially does the following:
121138

@@ -138,7 +155,7 @@ since the "Main" file selection is performed exclusively via MSBuild item manipu
138155
> [!TIP]
139156
> It is recommended to keep the project file to its bare minimum, usually having just the SmallSharp
140157
> SDK reference, and do all project/package references in the top-level files using the `#:package` and
141-
> `#:property` directives for improved isolation between the top-level programs.
158+
> `#:property` directives for improved isolation between the different file-based apps.
142159
143160
```xml
144161
<Project Sdk="Microsoft.NET.Sdk">

src/SmallSharp/EmitTargets.cs

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.IO;
3+
using System.Linq;
34
using System.Text.RegularExpressions;
45
using System.Xml;
56
using System.Xml.Linq;
@@ -10,29 +11,52 @@ namespace SmallSharp;
1011

1112
public class EmitTargets : Task
1213
{
14+
static readonly Regex sdkExpr = new(@"^#:sdk\s+([^@]+?)(@(.+))?$");
1315
static readonly Regex packageExpr = new(@"^#:package\s+([^@]+)@(.+)$");
1416
static readonly Regex propertyExpr = new(@"^#:property\s+([^\s]+)\s+(.+)$");
1517

1618
[Required]
17-
public ITaskItem? StartupFile { get; set; }
19+
public required ITaskItem StartupFile { get; set; }
1820

1921
[Required]
20-
public string TargetsFile { get; set; } = "SmallSharp.targets";
22+
public required string BaseIntermediateOutputPath { get; set; }
23+
24+
[Required]
25+
public required string PropsFile { get; set; }
26+
27+
[Required]
28+
public required string TargetsFile { get; set; }
29+
30+
[Required]
31+
public bool UsingSmallSharpSDK { get; set; } = false;
2132

2233
[Output]
2334
public ITaskItem[] Packages { get; set; } = [];
2435

36+
[Output]
37+
public ITaskItem[] Sdks { get; set; } = [];
38+
39+
[Output]
40+
public ITaskItem[] Properties { get; set; } = [];
41+
42+
[Output]
43+
public bool Success { get; set; } = false;
44+
2545
public override bool Execute()
2646
{
2747
if (StartupFile is null)
2848
return false;
2949

3050
var packages = new List<ITaskItem>();
51+
var sdkItems = new List<ITaskItem>();
52+
var propItems = new List<ITaskItem>();
53+
3154
var filePath = StartupFile.GetMetadata("FullPath");
3255
var contents = File.ReadAllLines(filePath);
3356

3457
var items = new List<XElement>();
3558
var properties = new List<XElement>();
59+
var sdks = new List<XAttribute[]>();
3660

3761
foreach (var line in contents)
3862
{
@@ -50,27 +74,72 @@ public override bool Execute()
5074
new XAttribute("Include", id),
5175
new XAttribute("Version", version)));
5276
}
77+
else if (sdkExpr.Match(line) is { Success: true } sdkMatch)
78+
{
79+
var name = sdkMatch.Groups[1].Value.Trim();
80+
var version = sdkMatch.Groups[2].Value.Trim();
81+
if (!string.IsNullOrEmpty(version))
82+
{
83+
sdkItems.Add(new TaskItem(name, new Dictionary<string, string>
84+
{
85+
{ "Version", version }
86+
}));
87+
sdks.Add([new XAttribute("Sdk", name), new XAttribute("Version", version)]);
88+
}
89+
else
90+
{
91+
sdkItems.Add(new TaskItem(name));
92+
sdks.Add([new XAttribute("Sdk", name)]);
93+
}
94+
}
5395
else if (propertyExpr.Match(line) is { Success: true } propMatch)
5496
{
5597
var name = propMatch.Groups[1].Value.Trim();
5698
var value = propMatch.Groups[2].Value.Trim();
5799

100+
propItems.Add(new TaskItem(name, new Dictionary<string, string>
101+
{
102+
{ "Value", value }
103+
}));
58104
properties.Add(new XElement(name, value));
59105
}
60106
}
61107

62108
Packages = [.. packages];
109+
Sdks = [.. sdkItems];
110+
Properties = [.. propItems];
111+
112+
if (sdks.Count > 0 && !UsingSmallSharpSDK)
113+
{
114+
Log.LogError($"When using #:sdk directive(s), you must use SmallSharp as an SDK: <Project Sdk=\"SmallSharp/{ThisAssembly.Project.Version}\">.");
115+
return false;
116+
}
117+
118+
// We only emit the default SDK if the SmallSharpSDK is in use, since otherwise the
119+
// project file is expected to define its own SDK and we'd be duplicating it.
120+
if (sdks.Count == 0)
121+
sdks.Add([new XAttribute("Sdk", "Microsoft.NET.Sdk")]);
122+
123+
WriteXml(TargetsFile, new XElement("Project",
124+
new XElement("PropertyGroup", properties),
125+
new XElement("ItemGroup", items)
126+
));
127+
128+
WriteXml(Path.Combine(BaseIntermediateOutputPath, "SmallSharp.sdk.props"), new XElement("Project",
129+
sdks.Select(x => new XElement("Import", [new XAttribute("Project", "Sdk.props"), .. x]))));
63130

64-
var doc = new XDocument(
65-
new XElement("Project",
66-
new XElement("PropertyGroup", properties),
67-
new XElement("ItemGroup", items)
68-
)
69-
);
131+
WriteXml(Path.Combine(BaseIntermediateOutputPath, "SmallSharp.sdk.targets"), new XElement("Project",
132+
sdks.Select(x => new XElement("Import", [new XAttribute("Project", "Sdk.targets"), .. x]))));
70133

71-
using var writer = XmlWriter.Create(TargetsFile, new XmlWriterSettings { Indent = true });
72-
doc.Save(writer);
134+
WriteXml(PropsFile, new XElement("Project"));
73135

136+
Success = true;
74137
return true;
75138
}
139+
140+
void WriteXml(string path, XElement root)
141+
{
142+
using var writer = XmlWriter.Create(path, new XmlWriterSettings { Indent = true });
143+
root.Save(writer);
144+
}
76145
}

src/SmallSharp/Sdk.Empty.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<Project>
2+
<!-- This is needed since the Microsoft.NET.SDK Sdk.props imports the Microsoft.Common.props unconditionally -->
3+
</Project>

src/SmallSharp/Sdk.props

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
<Project>
22

3-
<Import Project="..\build\SmallSharp.props" />
4-
53
<PropertyGroup>
64
<ImportProjectExtensionProps>true</ImportProjectExtensionProps>
75
<ImportProjectExtensionTargets>true</ImportProjectExtensionTargets>
6+
7+
<!-- Since we use this to build StartupFile list and as an SDK, we might not have .NET SDK imported yet (i.e. no C# files at all) -->
8+
<UsingSmallSharpSDK>true</UsingSmallSharpSDK>
9+
10+
<!-- Workaround https://github.com/dotnet/sdk/issues/50573 -->
11+
<AlternateCommonProps>$(MSBuildThisFileDirectory)\Sdk.Empty.props</AlternateCommonProps>
812
</PropertyGroup>
913

10-
<ItemGroup>
11-
<PackageReference Include="JsonPoke" Version="1.2.0" PrivateAssets="all" />
12-
</ItemGroup>
14+
<!-- Import Common.props explicitly we're too early here to use MSBuildProjectExtensionsPath -->
15+
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="'$(MicrosoftCommonPropsHasBeenImported)' != 'true' and Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
16+
17+
<Import Project="Sdk.props" Sdk="Microsoft.NET.SDK"
18+
Condition="!Exists('$(MSBuildProjectExtensionsPath)SmallSharp.sdks.props')" />
19+
20+
<Import Project="$(MSBuildProjectExtensionsPath)SmallSharp.sdks.props"
21+
Condition="Exists('$(MSBuildProjectExtensionsPath)SmallSharp.sdks.props')" />
22+
23+
<Import Project="..\build\SmallSharp.props" />
1324

1425
</Project>

src/SmallSharp/Sdk.targets

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
<Project>
22

3+
<Import Project="Sdk.targets" Sdk="Microsoft.NET.SDK"
4+
Condition="!Exists('$(MSBuildProjectExtensionsPath)SmallSharp.sdks.targets')" />
5+
6+
<Import Project="$(MSBuildProjectExtensionsPath)SmallSharp.sdks.targets"
7+
Condition="Exists('$(MSBuildProjectExtensionsPath)SmallSharp.sdks.targets')" />
8+
39
<Import Project="..\build\SmallSharp.targets" />
410

11+
<Target Name="ImplicitPackageReferenceFromStartupFile" BeforeTargets="_GenerateProjectRestoreGraphPerFramework"
12+
DependsOnTargets="StartupFile"
13+
Condition="'$(StartupFile)' != '' and Exists('$(StartupFile)') and '$(RestoreNeeded)' == 'true'" >
14+
15+
<!-- Optimize for restore success on first run without previously running our targets -->
16+
<ReadLinesFromFile File="$(StartupFile)">
17+
<Output TaskParameter="Lines" ItemName="_StartupFileLines" />
18+
</ReadLinesFromFile>
19+
20+
<ItemGroup>
21+
<_PkgLines Include="@(_StartupFileLines)"
22+
Condition="$([MSBuild]::ValueOrDefault('%(Identity)', '').StartsWith('#:package '))" />
23+
24+
<_PkgReference Include="$([MSBuild]::ValueOrDefault('%(_PkgLines.Identity)', '').Substring(10))" />
25+
26+
<PackageReference Condition="'@(_PkgReference)' != ''" Include="$([MSBuild]::ValueOrDefault('%(_PkgReference.Identity)', '').Split('@')[0])">
27+
<Version>$([MSBuild]::ValueOrDefault('%(_PkgReference.Identity)', '').Split('@')[1])</Version>
28+
</PackageReference>
29+
</ItemGroup>
30+
31+
</Target>
32+
533
</Project>

src/SmallSharp/SmallSharp.csproj

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,26 @@
2424
</ItemGroup>
2525

2626
<ItemGroup>
27-
<PackageReference Include="NuGetizer" Version="1.3.0" />
28-
<PackageReference Include="JsonPoke" Version="1.2.0" />
2927
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="17.13.26" Pack="false" />
28+
<PackageReference Include="NuGetizer" Version="1.3.0" />
29+
<PackageReference Include="JsonPoke" Version="1.2.0" Pack="false" GeneratePathProperty="true" />
30+
<PackageReference Include="NuGet.Versioning" Version="6.14.0" PrivateAssets="all" />
31+
<PackageReference Include="PolySharp" Version="1.15.0" PrivateAssets="all" />
32+
<PackageReference Include="ThisAssembly.Project" Version="2.0.14" PrivateAssets="all" />
3033
</ItemGroup>
3134

3235
<ItemGroup>
3336
<None Include="..\_._" PackFolder="lib\netstandard2.0" Visible="false" />
3437
<None Update="SmallSharp.targets" PackFolder="$(PackFolder)" CopyToOutputDirectory="PreserveNewest" />
3538
<None Update="SmallSharp.Before.targets" PackFolder="$(PackFolder)" CopyToOutputDirectory="PreserveNewest" />
36-
<None Update="Sdk.props;Sdk.targets" PackFolder="Sdk" CopyToOutputDirectory="PreserveNewest" />
39+
<None Update="Sdk.*" PackFolder="Sdk" CopyToOutputDirectory="PreserveNewest" />
40+
<None Include="$(PkgJsonPoke)\build\JsonPoke.dll" PackFolder="$(PackFolder)" CopyToOutputDirectory="PreserveNewest" Visible="false" />
41+
<None Include="$(PkgJsonPoke)\build\Newtonsoft.Json.dll" PackFolder="$(PackFolder)" CopyToOutputDirectory="PreserveNewest" Visible="false" />
3742
</ItemGroup>
3843

3944
<ItemGroup>
40-
<UpToDateCheckInput Include="SmallSharp.targets;SmallSharp.Before.targets;Sdk.props;Sdk.targets" />
45+
<UpToDateCheckInput Include="SmallSharp.targets;SmallSharp.Before.targets;Sdk.props;Sdk.Empty.props;Sdk.targets" />
46+
<ProjectProperty Include="PackageVersion" />
4147
</ItemGroup>
4248

4349
</Project>

src/SmallSharp/SmallSharp.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
<!-- Capture project-level properties before they are defaulted by Microsoft.Common.targets -->
88
<CustomBeforeMicrosoftCSharpTargets>$(MSBuildThisFileDirectory)\SmallSharp.Before.props</CustomBeforeMicrosoftCSharpTargets>
9-
</PropertyGroup>
9+
10+
<UsingSmallSharpSDK Condition="'$(UsingSmallSharpSDK)' == ''">false</UsingSmallSharpSDK>
11+
</PropertyGroup>
1012

1113
</Project>

0 commit comments

Comments
 (0)