diff --git a/readme.md b/readme.md
index d2fee42..37c6c5c 100644
--- a/readme.md
+++ b/readme.md
@@ -28,12 +28,13 @@ To pay the Maintenance Fee, [become a Sponsor](https://github.com/sponsors/devlo
C# [top-level programs](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements)
allow a very intuitive, simple and streamlined experience for quickly spiking or learning C#.
-The addition of [dotnet run app.cs](https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/) in
-.NET 10 takes this further by allowing package references and even MSBuild properties to be
+The addition of [file-based apps](https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/) in
+.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
specified per file:
```csharp
#:package Humanizer@2.14.1
+#:property LangVersion=preview
using Humanizer;
@@ -70,42 +71,57 @@ files in subdirectories and those will behave like normal compile items.
## Usage
SmallSharp works by just installing the
-[SmallSharp](https://nuget.org/packages/SmallSharp) nuget package in a C# console project.
-
-Recommended installation as an SDK:
+[SmallSharp](https://nuget.org/packages/SmallSharp) nuget package in a C# console project
+and adding a couple extra properties to the project file:
```xml
-
-
Exe
net10.0
+
+
+ true
+ true
+
+
+
+
```
-Or as a regular package reference:
+If your file-based apps use the `#:sdk` [directive](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives#file-based-apps),
+you need to add SmallSharp as an SDK reference instead so the SDK is picked up by the
+generated targets/props instead of the project file. You also don't need the additional
+properties since the SDK mode sets them automatically for you:
+
```xml
-
+
Exe
net10.0
-
-
-
-
```
+> [!IMPORTANT]
+> If no `#:sdk` directive is provided by a specific C# file-based app, the `Microsoft.NET.SDK` will be
+> used by default in this SDK mode.
+
Keep adding as many top-level programs as you need, and switch between them easily by simply
selecting the desired file from the Start button dropdown.
+When running from the command-line, you can select the file to run by passing it as an argument to `dotnet run`:
+
+```bash
+dotnet run -p:ActiveFile program1.cs
+```
+
## How It Works
This nuget package leverages in concert the following standalone and otherwise
@@ -116,6 +132,7 @@ unrelated features of the compiler, nuget and MSBuild:
3. Whenever changed, the dropdown selection is persisted as the `$(ActiveDebugProfile)` MSBuild property in a file
named after the project with the `.user` extension
4. This file is imported before NuGet-provided MSBuild targets
+5. VS ignores `#:` directives when adding the flag `FileBasedProgram` to the `$(Features)` project property.
Using the above features in concert, **SmallSharp** essentially does the following:
@@ -138,7 +155,7 @@ since the "Main" file selection is performed exclusively via MSBuild item manipu
> [!TIP]
> It is recommended to keep the project file to its bare minimum, usually having just the SmallSharp
> SDK reference, and do all project/package references in the top-level files using the `#:package` and
-> `#:property` directives for improved isolation between the top-level programs.
+> `#:property` directives for improved isolation between the different file-based apps.
```xml
diff --git a/src/SmallSharp/EmitTargets.cs b/src/SmallSharp/EmitTargets.cs
index c7dfef7..db322b1 100644
--- a/src/SmallSharp/EmitTargets.cs
+++ b/src/SmallSharp/EmitTargets.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
@@ -10,29 +11,52 @@ namespace SmallSharp;
public class EmitTargets : Task
{
+ static readonly Regex sdkExpr = new(@"^#:sdk\s+([^@]+?)(@(.+))?$");
static readonly Regex packageExpr = new(@"^#:package\s+([^@]+)@(.+)$");
static readonly Regex propertyExpr = new(@"^#:property\s+([^\s]+)\s+(.+)$");
[Required]
- public ITaskItem? StartupFile { get; set; }
+ public required ITaskItem StartupFile { get; set; }
[Required]
- public string TargetsFile { get; set; } = "SmallSharp.targets";
+ public required string BaseIntermediateOutputPath { get; set; }
+
+ [Required]
+ public required string PropsFile { get; set; }
+
+ [Required]
+ public required string TargetsFile { get; set; }
+
+ [Required]
+ public bool UsingSmallSharpSDK { get; set; } = false;
[Output]
public ITaskItem[] Packages { get; set; } = [];
+ [Output]
+ public ITaskItem[] Sdks { get; set; } = [];
+
+ [Output]
+ public ITaskItem[] Properties { get; set; } = [];
+
+ [Output]
+ public bool Success { get; set; } = false;
+
public override bool Execute()
{
if (StartupFile is null)
return false;
var packages = new List();
+ var sdkItems = new List();
+ var propItems = new List();
+
var filePath = StartupFile.GetMetadata("FullPath");
var contents = File.ReadAllLines(filePath);
var items = new List();
var properties = new List();
+ var sdks = new List();
foreach (var line in contents)
{
@@ -50,27 +74,72 @@ public override bool Execute()
new XAttribute("Include", id),
new XAttribute("Version", version)));
}
+ else if (sdkExpr.Match(line) is { Success: true } sdkMatch)
+ {
+ var name = sdkMatch.Groups[1].Value.Trim();
+ var version = sdkMatch.Groups[2].Value.Trim();
+ if (!string.IsNullOrEmpty(version))
+ {
+ sdkItems.Add(new TaskItem(name, new Dictionary
+ {
+ { "Version", version }
+ }));
+ sdks.Add([new XAttribute("Sdk", name), new XAttribute("Version", version)]);
+ }
+ else
+ {
+ sdkItems.Add(new TaskItem(name));
+ sdks.Add([new XAttribute("Sdk", name)]);
+ }
+ }
else if (propertyExpr.Match(line) is { Success: true } propMatch)
{
var name = propMatch.Groups[1].Value.Trim();
var value = propMatch.Groups[2].Value.Trim();
+ propItems.Add(new TaskItem(name, new Dictionary
+ {
+ { "Value", value }
+ }));
properties.Add(new XElement(name, value));
}
}
Packages = [.. packages];
+ Sdks = [.. sdkItems];
+ Properties = [.. propItems];
+
+ if (sdks.Count > 0 && !UsingSmallSharpSDK)
+ {
+ Log.LogError($"When using #:sdk directive(s), you must use SmallSharp as an SDK: .");
+ return false;
+ }
+
+ // We only emit the default SDK if the SmallSharpSDK is in use, since otherwise the
+ // project file is expected to define its own SDK and we'd be duplicating it.
+ if (sdks.Count == 0)
+ sdks.Add([new XAttribute("Sdk", "Microsoft.NET.Sdk")]);
+
+ WriteXml(TargetsFile, new XElement("Project",
+ new XElement("PropertyGroup", properties),
+ new XElement("ItemGroup", items)
+ ));
+
+ WriteXml(Path.Combine(BaseIntermediateOutputPath, "SmallSharp.sdk.props"), new XElement("Project",
+ sdks.Select(x => new XElement("Import", [new XAttribute("Project", "Sdk.props"), .. x]))));
- var doc = new XDocument(
- new XElement("Project",
- new XElement("PropertyGroup", properties),
- new XElement("ItemGroup", items)
- )
- );
+ WriteXml(Path.Combine(BaseIntermediateOutputPath, "SmallSharp.sdk.targets"), new XElement("Project",
+ sdks.Select(x => new XElement("Import", [new XAttribute("Project", "Sdk.targets"), .. x]))));
- using var writer = XmlWriter.Create(TargetsFile, new XmlWriterSettings { Indent = true });
- doc.Save(writer);
+ WriteXml(PropsFile, new XElement("Project"));
+ Success = true;
return true;
}
+
+ void WriteXml(string path, XElement root)
+ {
+ using var writer = XmlWriter.Create(path, new XmlWriterSettings { Indent = true });
+ root.Save(writer);
+ }
}
diff --git a/src/SmallSharp/Sdk.Empty.props b/src/SmallSharp/Sdk.Empty.props
new file mode 100644
index 0000000..6d2bdf9
--- /dev/null
+++ b/src/SmallSharp/Sdk.Empty.props
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/SmallSharp/Sdk.props b/src/SmallSharp/Sdk.props
index e085e05..69cd5fb 100644
--- a/src/SmallSharp/Sdk.props
+++ b/src/SmallSharp/Sdk.props
@@ -1,14 +1,25 @@
-
-
true
true
+
+
+ true
+
+
+ $(MSBuildThisFileDirectory)\Sdk.Empty.props
-
-
-
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SmallSharp/Sdk.targets b/src/SmallSharp/Sdk.targets
index e1153de..44164b2 100644
--- a/src/SmallSharp/Sdk.targets
+++ b/src/SmallSharp/Sdk.targets
@@ -1,5 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ <_PkgLines Include="@(_StartupFileLines)"
+ Condition="$([MSBuild]::ValueOrDefault('%(Identity)', '').StartsWith('#:package '))" />
+
+ <_PkgReference Include="$([MSBuild]::ValueOrDefault('%(_PkgLines.Identity)', '').Substring(10))" />
+
+
+ $([MSBuild]::ValueOrDefault('%(_PkgReference.Identity)', '').Split('@')[1])
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SmallSharp/SmallSharp.csproj b/src/SmallSharp/SmallSharp.csproj
index f208055..f530c0d 100644
--- a/src/SmallSharp/SmallSharp.csproj
+++ b/src/SmallSharp/SmallSharp.csproj
@@ -24,20 +24,26 @@
-
-
+
+
+
+
+
-
+
+
+
-
+
+
diff --git a/src/SmallSharp/SmallSharp.props b/src/SmallSharp/SmallSharp.props
index 953b7f9..e9d4037 100644
--- a/src/SmallSharp/SmallSharp.props
+++ b/src/SmallSharp/SmallSharp.props
@@ -6,6 +6,8 @@
$(MSBuildThisFileDirectory)\SmallSharp.Before.props
-
+
+ false
+
\ No newline at end of file
diff --git a/src/SmallSharp/SmallSharp.targets b/src/SmallSharp/SmallSharp.targets
index 681fd3e..f019d64 100644
--- a/src/SmallSharp/SmallSharp.targets
+++ b/src/SmallSharp/SmallSharp.targets
@@ -1,38 +1,46 @@
+
- $(ActiveDebugProfile)
- $(ActiveCompile)
- EnsureImportProjectExtensions;CollectStartupFile;SelectStartupFile;SelectTopLevelCompile;UpdateLaunchSettings;EmitTargets
+
+ $(ActiveCompile)
+ $(ActiveFile)
+ $(ActiveDebugProfile)
+ true
+ EnsureProperties;CollectStartupFile;SelectStartupFile;SelectTopLevelCompile;UpdateLaunchSettings;EmitTargets
-
+
+ $(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).smallsharp.props
$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).smallsharp.targets
-
+
-
+
-
+
-
+
-
-
+
+
+
@@ -41,27 +49,13 @@
-
-
-
-
-
-
-
-
-
-
+
-
- true
-
-
+
-
+
%(ReversedCompile.Identity)
@@ -142,6 +136,16 @@
+
+
+
+
+
+
+
+
+
@@ -159,14 +163,43 @@
Value="Project" />
-
-
+
+
+
+
+
+
+
+
+ $(MSBuildProjectExtensionsPath)smallsharp.assets.json
+
+
+
+
+
+
+
+
+ $(DynamicProjectAssetsFile)
+
+
+
+