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) + + + +