Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Build-Source.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ try
# script. But for a local "inner loop" development this might be the only script used.
$buildInfo = Initialize-BuildEnvironment -FullInit:$FullInit

# build the Managed code support
# build the core library support
Write-Information "dotnet build --tl:off 'src\Ubiquity.NET.Utils.slnx' -c $Configuration"
Invoke-External dotnet build --tl:off 'src\Ubiquity.NET.Utils.slnx' '-c' $Configuration

# build the demo that consumes the output packages as a package reference only.
# This ensures that the proper references are present and work. The demo uses the
# library and the source generation to validate it is actually functional.
Write-Information "dotnet build --tl:off 'src\DemoCommandLineSrcGen\DemoCommandLineSrcGen.slnx' -c $Configuration"
Invoke-External dotnet build --tl:off 'src\DemoCommandLineSrcGen\DemoCommandLineSrcGen.slnx' '-c' $Configuration
}
catch
{
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<PackageVersion Include="Basic.Reference.Assemblies.Net100" Version="1.8.4" />
<PackageVersion Include="PolySharp" Version="1.15.0" />
<PackageVersion Include="System.Buffers" Version="4.6.1" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="8.0.0" />
<PackageVersion Include="System.Memory" Version="4.5.5" />
<PackageVersion Include="System.CommandLine" Version="2.0.0" />

Expand Down
1 change: 1 addition & 0 deletions Invoke-Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ try
Push-Location $BuildInfo['SrcRootPath']
try
{
dir (Join-Path $BuildInfo['BuildOutputPath'] 'bin' 'Ubiquity.NET.CommandLine.SrcGen.UT' "$Configuration" 'net10.0' 'Ubiquity.NET.CommandLine.*')
Invoke-External dotnet test Ubiquity.NET.Utils.slnx '-c' $Configuration '-tl:off' '--logger:trx' '--no-build' '-s' '.\x64.runsettings'
}
finally
Expand Down
14 changes: 14 additions & 0 deletions src/DemoCommandLineSrcGen/DemoCommandLineSrcGen.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Ubiquity.NET.CommandLine" VersionOverride="20.1.*-*"/>
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions src/DemoCommandLineSrcGen/DemoCommandLineSrcGen.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Solution>
<Project Path="./DemoCommandLineSrcGen.csproj" />
</Solution>
56 changes: 56 additions & 0 deletions src/DemoCommandLineSrcGen/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.

#pragma warning disable IDE0130 // Namespace does not match folder structure
#pragma warning disable SA1600 // Elements should be documented

using System;
using System.Threading;
using System.Threading.Tasks;

using Ubiquity.NET.CommandLine;

namespace TestNamespace
{
public static class Program
{
public static async Task<int> Main( string[] args )
{
// pressing CTRL-C cancels the entire operation
using CancellationTokenSource cts = new();
Console.CancelKeyPress += ( _, e ) =>
{
e.Cancel = true;
cts.Cancel();
};

// start with information level for parsing; parsed options might specify different level
var reporter = new ColoredConsoleReporter( MsgLevel.Information );

return await TestOptions.BuildRootCommand( ( options, ct ) => AppMainAsync( options, reporter, ct ) )
.ParseAndInvokeResultAsync( reporter, cts.Token, args );
}

private static async Task<int> AppMainAsync(
TestOptions options,
ColoredConsoleReporter reporter,
CancellationToken ct
)
{
// Now that args are parsed, if a distinct verbosity level is specified use that
if(options.Verbosity != reporter.Level)
{
reporter = new ColoredConsoleReporter( options.Verbosity );
}

reporter.Verbose( "AppMainAsync" );

// Core application code here...

// Use the cancellation token to indicate cancellation
// This is set when CTRL-C is pressed in Main() above.
ct.ThrowIfCancellationRequested();
return 0;
}
}
}
38 changes: 38 additions & 0 deletions src/DemoCommandLineSrcGen/TestOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.

#pragma warning disable IDE0130 // Namespace does not match folder structure

using System.CommandLine;
using System.IO;

using Ubiquity.NET.CommandLine;
using Ubiquity.NET.CommandLine.GeneratorAttributes;

namespace TestNamespace
{
[RootCommand( Description = "Root command for tests" )]
internal partial class TestOptions
{
[Option( "-o", Description = "Test SomePath" )]
[FolderValidation( FolderValidation.CreateIfNotExist )]
public required DirectoryInfo SomePath { get; init; }

[Option( "-v", Description = "Verbosity Level" )]
public MsgLevel Verbosity { get; init; } = MsgLevel.Information;

[Option( "-b", Description = "Test Some existing Path" )]
[FolderValidation( FolderValidation.ExistingOnly )]
public required DirectoryInfo SomeExistingPath { get; init; }

[Option( "--thing1", Aliases = [ "-t" ], Required = true, Description = "Test Thing1", HelpName = "Help name for thing1" )]
public bool Thing1 { get; init; }

// This should be ignored by generator
public string? NotAnOption { get; set; }

[Option( "-a", Hidden = true, Required = false, ArityMin = 0, ArityMax = 3, Description = "Test SomeOtherPath" )]
[FileValidation( FileValidation.ExistingOnly )]
public required FileInfo SomeOtherPath { get; init; }
}
}
86 changes: 86 additions & 0 deletions src/Ubiquity.NET.CommandLine.Pkg/PackageReadMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Ubiquity.NET.CommandLine
Common Text CommandLine support. This provides a number of support classes for
Text based UI/UX, including command line parsing extensions. This is generally only relevant
for console based apps.

## Example Command line parsing
Normal single command application:
Options binding types are normally split into two partial classes. The normal types that
would contain attributes for source generation and the generated partial implementation.
While there is no source generator (yet) for the options [That is intended for a future
release] this keeps the responsibilities clearer and aids in migration to a source generator.

### Root command type
Options.cs:
```
using System.IO;

using Ubiquity.NET.CommandLine;

namespace TestSample
{
// Root command for the app, Disable the default options and directive support
// This puts the app in total control of the parsing. Normally an app would leave the
// defaults but it might not want to for command line syntax compat with a previous
// release...
[RootCommand(DefaultOptions = DefaultOption.None, DefaultDirectives = DefaultDirective.None )]
internal partial class ParsedArgs
{
[Option("-v", Description="Verbosity Level")]
internal MsgLevel Verbosity? { get; init; }

// Additional options here...
}
}
```

### Usage in common single command/no command case
Most applications don't have or use any concept of commands/sub-commands. Thus, there is
support to make this scenario as simple as possible. [This uses the previously listed
`ParsedArgs` class.

``` C#
public static async Task<int> Main( string[] args )
{
// pressing CTRL-C cancels the entire operation
using CancellationTokenSource cts = new();
Console.CancelKeyPress += ( _, e ) =>
{
e.Cancel = true;
cts.Cancel();
};

// start with information level for parsing; parsed options might specify a different
// level `AppMain` will adapt to any new value.
var reporter = new ColoredConsoleReporter( MsgLevel.Information );

return await TestOptions.BuildRootCommand( ( options, ct ) => AppMainAsync( options, reporter, ct ) )
.ParseAndInvokeResultAsync( reporter, cts.Token, args );
}

private static int AppMain( ParsedArgs args, ColoredConsoleReporter reporter )
{
// Real main here.
// The pre-parsed and validated command line arguments and the reporter are provided as
// arguments to this function

// Now that args are parsed, if a distinct verbosity level is specified use that
if(args.Verbosity != reporter.Level)
{
reporter = new ColoredConsoleReporter( args.Verbosity );
}

// ...
}
```
This is a simplified usage for the common case of an app without commands/sub-commands.

## Supported Functionality
`IDiagnosticReporter` interface is at the core of the UX. It is similar in many ways to many
of the logging interfaces available. The primary distinction is with the ***intention*** of
use. `IDiagnosticReporter`, specifically, assumes the use for UI/UX rather than a
debugging/diagnostic log. These have VERY distinct use cases and purposes and generally show
very different information. (Not to mention the overly complex requirements of
the anti-pattern DI container/Service Locator assumed in `Microsoft.Extensions.Logging`)


24 changes: 24 additions & 0 deletions src/Ubiquity.NET.CommandLine.Pkg/ReadMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Ubiquity.NET.CommandLine.Pkg
This is a special project that creates the meta package to reference the library AND the
associated Roslyn components. This is done without restoring any packages by a "NOTARGETS"
project that uses an explicit NUSPEC. This is used instead of the CSPROJ built-in support
for generation of the NuSpec as that would try and restore ALL references first. This causes
problems for build ordering. Since a CSPROJ will try to restore all packages it will fail to
restore anything that isn't built yet. While it is plausible to construct build order
dependencies, such things are not honored when restoring. NuGet package restore happens
***BEFORE*** build so the packages won't exist.

That limitation is strictly in how CSPROJ system works and not an actual limit of NuGet
itself. Thus, to workaround that, this project uses a NuSpec file and generates the meta
package directly (even if the dependencies are not resolved). The package is created
directly referencing what will exist in the future breaking the dependency cycle/problem.

## Build Caveat
Due to the nature of this, the version reference must remain constant across all project
builds. This is true for command line builds but NOT for IDE builds. Each project in an IDE
build gets a new version. This means that the version references in this package are wrong
and won't exist. The solution is to build this and all packages from the command line to
guarantee correct references. This is done automatically in all automated builds as they
don't use any IDE. Thus, the problem is for local loop development only. It is only an issue
when using the demo project as that is designed to leverage ONLY the package without any
project references to validate that works as expected.
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!--
Project to build the NuGet Meta package

NOTE:
To support a meta package where the referenced packages may not exist at build time this must use a NuSpec file directly.

The Build process for CSPROJ files will require resolving the referenced packages before it "generates" the NuSpec file.
The CSPROJ system for MSBUILD will try to restore referenced packages etc... and ultimately requires the ability to find
the listed dependencies. (They won't exist yet for this build/repo!) so either a mechanism to control build ordering EVEN
on NuGetRestore is needed, or this approach is used. Given the complexities of trying the former, this approach is used
as it is simpler.
-->
<Project Sdk="Microsoft.Build.NoTargets">
<PropertyGroup>
<TargetFrameworks>net10.0;net8.0</TargetFrameworks>

<!-- Disable default inclusion of all items and analyzers. This project doesn't use/need them -->
<EnableDefaultItems>false</EnableDefaultItems>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<NoPackageAnalysis>true</NoPackageAnalysis>
<NoCommonAnalyzers>true</NoCommonAnalyzers>

<!--NuGet packaging support -->
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<NuSpecFile>Ubiquity.NET.CommandLine.nuspec</NuSpecFile>

<!-- Override the name as this is the "Meta" package that references the lib and analyzers -->
<PackageId>Ubiquity.NET.CommandLine</PackageId>

<MinClientVersion>4.9.0</MinClientVersion>
<Authors>.NET Foundation,Ubiquity.NET</Authors>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<Description>General use Support for Command line parsing based on System.CommandLine. This is a meta package that references the library and the associated Roslyn components</Description>
<PackageTags>Extensions,.NET,Ubiquity.NET, Console</PackageTags>
<PackageReadmeFile>PackageReadMe.md</PackageReadmeFile>
<PackageProjectUrl>https://github.com/UbiquityDotNET/Ubiquity.NET.Utils</PackageProjectUrl>
<RepositoryUrl>https://github.com/UbiquityDotNET/Ubiquity.NET.Utils.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>Apache-2.0 WITH LLVM-exception</PackageLicenseExpression>

<!-- Meta Package only, there is no "lib" folder -->
<IncludeBuiltProjectOutputGroup>false</IncludeBuiltProjectOutputGroup>
<NoWarn>$(NoWarn);NU5128</NoWarn>
</PropertyGroup>

<ItemGroup>
<None Include="PackageReadMe.md" Pack="true" PackagePath="\" />
<None Include="ReadMe.md" />
<None Include="Ubiquity.NET.CommandLine.nuspec" />
</ItemGroup>

<!-- Sanity/safety ensure NO referenced packages [Even if injected by Directory.Build.packages etc...] -->
<ItemGroup>
<PackageReference Remove="@(PackageReference)" />
</ItemGroup>

<!--
Provide the standard properties for this project to the package generation so that the NUSPEC
file will complete the pack. The DependsOnTarget is REQUIRED as MSBUILD/VS/dotnet CLI all behave differently,
especially with regard to release/automated builds. In particular the `PrepareVersioningForBuild` target
may not trigger for automated builds on restore/pack even though it does for other targets. The output
from that task is observable in the output but apparently the output ignores that it was reloaded (no
indication of this happening is provided) so when the actual generation of the Nuspec file comes along,
the PackageVersion is default. This now adds the Depends on AND an error if the property is still at the
always invalid default setting.
-->
<Target Name="SetNuspecProperties" BeforeTargets="GenerateNuspec" DependsOnTargets="PrepareVersioningForBuild">
<Error Code="UNLL001" Condition="'$(PackageVersion)'=='1.0.0'" Text="$PackageVersion has default value!" />
<PropertyGroup>
<NuspecProperties>configuration=$(Configuration)</NuspecProperties>
<NuspecProperties>$(NuspecProperties);packageID=$(PackageID)</NuspecProperties>
<NuspecProperties>$(NuspecProperties);version=$(PackageVersion)</NuspecProperties>
<NuspecProperties>$(NuspecProperties);authors=$(Authors)</NuspecProperties>
<NuspecProperties>$(NuspecProperties);projectUrl=$(PackageProjectUrl)</NuspecProperties>
<NuspecProperties>$(NuspecProperties);description=$(Description)</NuspecProperties>
<NuspecProperties>$(NuspecProperties);tags=$(PackageTags)</NuspecProperties>
<NuspecProperties>$(NuSpecProperties);licExpression=$(PackageLicenseExpression)</NuspecProperties>
<NuSpecProperties>$(NuSpecProperties);tfmGroup=$(TargetFramework)</NuSpecProperties>
</PropertyGroup>
</Target>
</Project>
45 changes: 45 additions & 0 deletions src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.nuspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<!--
NOTE: NuSpec Substitution macros all include a leading and trailing `$`. That syntax is
not used in this comment to prevent build failures. The substitution is done before
XML validation of this file, which could cause these comments to include invalid
chars and break the build. The substitution ignores the fact that any such occurrences
are in a comment. This is especially problematic for Local, CI, and PR builds of this
library as the version could contain a double dash, which is invalid within an XML comment.
Because substitution is done in memory the resulting in memory string contains the
invalid chars, but the error is reported with a location in the generating project.
That is, a completely incorrect source location is reported making it VERY difficult
to find the root cause.

INPUT:
packageID => ID of the package
version => Version of this package
authors => Authors of the package
description => Description of the package
tags => Tags to mark this package (Helps searching)
licExpression => License expression
projectUrl => URL for the project
tfmGroup => minimum Target Framework needed for the source files
config => Configuration for the build (Debug/Release)
-->
<metadata minClientVersion="4.9.0">
<id>$packageID$</id>
<version>$version$</version>
<authors>$authors$</authors>
<description>$description$</description>
<tags>$tags$</tags>
<license type="expression">$licExpression$</license>
<projectUrl>$projectUrl$</projectUrl>
<readme>PackageReadMe.md</readme>
<dependencies>
<group>
<dependency id="Ubiquity.NET.CommandLine.Lib" version="$version$" exclude="Build,Analyzers" />
<dependency id="Ubiquity.NET.CommandLine.SrcGen" version="$version$" include="Analyzers" />
</group>
</dependencies>
</metadata>
<files>
<file src="PackageReadMe.md" target=".\"/>
</files>
</package>
Loading
Loading