Skip to content
Open
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
17 changes: 17 additions & 0 deletions docs/samples/samples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorldAttributes", "hel
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeyedServices", "dependency-injection\keyed\KeyedServices.csproj", "{30674755-09B2-41DC-8AAD-31AC4541E562}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "trimming", "trimming", "{05379A49-3DC7-FF82-2FE9-08CAF8CE680F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrimmingSample", "trimming\TrimmingSample.csproj", "{8A57091B-7A35-4EA6-A863-AC2076188700}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -389,6 +393,18 @@ Global
{30674755-09B2-41DC-8AAD-31AC4541E562}.Release|x64.Build.0 = Release|Any CPU
{30674755-09B2-41DC-8AAD-31AC4541E562}.Release|x86.ActiveCfg = Release|Any CPU
{30674755-09B2-41DC-8AAD-31AC4541E562}.Release|x86.Build.0 = Release|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|x64.Build.0 = Debug|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|x86.Build.0 = Debug|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Release|Any CPU.Build.0 = Release|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Release|x64.ActiveCfg = Release|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Release|x64.Build.0 = Release|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Release|x86.ActiveCfg = Release|Any CPU
{8A57091B-7A35-4EA6-A863-AC2076188700}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -409,6 +425,7 @@ Global
{A4F3D30A-4124-49B0-A888-F81CE682F491} = {13EC390A-3D6F-4A7E-A02C-D3CBA197FA40}
{B38FF032-9AFB-481F-A432-DFA7A5100C3A} = {13EC390A-3D6F-4A7E-A02C-D3CBA197FA40}
{30674755-09B2-41DC-8AAD-31AC4541E562} = {E03982B3-80F2-4D55-9501-378EACE93E5B}
{8A57091B-7A35-4EA6-A863-AC2076188700} = {05379A49-3DC7-FF82-2FE9-08CAF8CE680F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {43B81702-C1EF-43B6-B57D-C630AD1F555D}
Expand Down
25 changes: 25 additions & 0 deletions docs/samples/trimming/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using McMaster.Extensions.CommandLineUtils;

// Reproduces https://github.com/natemcmaster/CommandLineUtils/issues/539:
// a trimmed, self-contained, single-file publish using CommandLineApplication.Execute<TApp>
// with an attribute-based command model. Before the fix, a trimmed publish either warned
// during publish (when EnableTrimAnalyzer is set) or threw at run time with
// "No method named 'OnExecute' or 'OnExecuteAsync' could be found." /
// "Could not find any public constructors of type 'MyCommand'."
return CommandLineApplication.Execute<MyCommand>(args);

internal class MyCommand
{
[Option(Description = "The subject to greet")]
public string Subject { get; } = "world";

private int OnExecute()
{
Console.WriteLine($"Hello, {Subject}!");
return 0;
}
}
29 changes: 29 additions & 0 deletions docs/samples/trimming/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Trimming sample for McMaster.Extensions.CommandLineUtils
==========================================================

Demonstrates publishing a trimmed, self-contained, single-file app that uses
`McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<TApp>`.

See [issue #539](https://github.com/natemcmaster/CommandLineUtils/issues/539).

Instructions
------------

From the repository root, publish the sample for your runtime (Linux x64 example):

```bash
dotnet publish docs/samples/trimming/TrimmingSample.csproj -c Release -r linux-x64
```

Because the project sets `EnableTrimAnalyzer` and `TreatWarningsAsErrors`, the publish
itself fails if the trimmer finds any unannotated reflection reachable from this sample -
so this project doubles as a regression check, not just a demo.

Run the produced binary (path is printed at the end of the publish, under
`docs/samples/trimming/bin/Release/net8.0/<rid>/publish`):

```bash
./docs/samples/trimming/bin/Release/net8.0/linux-x64/publish/TrimmingSample --subject Trimming
```

Expected output: `Hello, Trimming!`
26 changes: 26 additions & 0 deletions docs/samples/trimming/TrimmingSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<InvariantGlobalization>true</InvariantGlobalization>
<IsPackable>false</IsPackable>

<!-- Publish self-contained, trimmed, single-file to exercise the library under trimming,
matching the repro in https://github.com/natemcmaster/CommandLineUtils/issues/539. -->
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<PublishTrimmed>true</PublishTrimmed>

<!-- Fail the build if the trimmer finds a problem, so this sample doubles as a regression
check for #539 instead of only a manual demo. -->
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\CommandLineUtils\McMaster.Extensions.CommandLineUtils.csproj" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions src/CommandLineUtils/Abstractions/IModelAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics.CodeAnalysis;

namespace McMaster.Extensions.CommandLineUtils.Abstractions
{
Expand All @@ -14,6 +15,7 @@ public interface IModelAccessor
/// Gets the type of the model.
/// </summary>
/// <returns>The type.</returns>
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
Type GetModelType();

/// <summary>
Expand Down
33 changes: 28 additions & 5 deletions src/CommandLineUtils/Abstractions/ValueParserProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -48,17 +49,29 @@ internal ValueParserProvider()
/// </remarks>
public CultureInfo ParseCulture { get; set; } = CultureInfo.CurrentCulture;

private static readonly MethodInfo s_getParserGeneric
= typeof(ValueParserProvider)
private static readonly MethodInfo s_getParserGeneric;

[UnconditionalSuppressMessage("Trimming", "IL2111",
Justification = "GetParser<T>() is looked up here purely so GetParser(Type) can re-dispatch to it " +
"generically below; the annotations on its T parameter are honored because the Type passed " +
"into GetParser(Type) is itself annotated with DynamicallyAccessedMemberTypes.All.")]
static ValueParserProvider()
{
s_getParserGeneric = typeof(ValueParserProvider)
.GetMethods(BindingFlags.Instance | BindingFlags.Public)
.Single(m => m.Name == nameof(GetParser) && m.IsGenericMethod);
}

/// <summary>
/// Returns a parser registered for the given type.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public IValueParser GetParser(Type type)
[UnconditionalSuppressMessage("Trimming", "IL2060",
Justification = "type is annotated with DynamicallyAccessedMemberTypes.All, so the members " +
"GetParser<T>() needs from it are preserved even though MakeGenericMethod's own use of type " +
"cannot be statically verified by the trimmer.")]
public IValueParser GetParser([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type)
{
var method = s_getParserGeneric.MakeGenericMethod(type);
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
Expand All @@ -76,7 +89,7 @@ public IValueParser GetParser(Type type)
/// </remarks>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public IValueParser<T>? GetParser<T>()
public IValueParser<T>? GetParser<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
{
var parser = GetParserImpl<T>();
return parser switch
Expand All @@ -87,7 +100,17 @@ public IValueParser GetParser(Type type)
};
}

internal IValueParser? GetParserImpl<T>()
[UnconditionalSuppressMessage("Trimming", "IL2026",
Justification = "TryGetParser<T> (TypeDescriptor-based) is only reached when T is not one of the " +
"stock value-parser types, an enum, or Nullable<T> thereof. T is annotated with " +
"DynamicallyAccessedMemberTypes.All so its members survive trimming when this fallback runs.")]
[UnconditionalSuppressMessage("Trimming", "IL2072",
Justification = "type is typeof(T) where T is annotated with DynamicallyAccessedMemberTypes.All, so " +
"the array/wrapped element types reached through Type.GetElementType() and ReflectionHelper's " +
"Is*Type helpers are reachable through T even though those helpers do not themselves carry a " +
"matching annotation on their return/out value.")]
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "See the IL2072 suppression above.")]
internal IValueParser? GetParserImpl<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
{
var type = typeof(T);
if (_parsers.TryGetValue(type, out var parser))
Expand Down
6 changes: 6 additions & 0 deletions src/CommandLineUtils/Attributes/OptionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using McMaster.Extensions.CommandLineUtils.Abstractions;

Expand Down Expand Up @@ -95,6 +96,11 @@ internal CommandOption Configure(CommandLineApplication app, PropertyInfo prop)
return option;
}

[UnconditionalSuppressMessage("Trimming", "IL2072",
Justification = "prop.PropertyType is not itself annotated (PropertyInfo.PropertyType has no " +
"DynamicallyAccessedMembers support), but prop's declaring model type flows from " +
"CommandLineApplication.Execute<TApp>'s DynamicallyAccessedMemberTypes.All-annotated TApp, so " +
"prop's own type is preserved by trimming.")]
private CommandOptionType GetOptionType(PropertyInfo prop, ValueParserProvider valueParsers)
{
CommandOptionType optionType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace McMaster.Extensions.CommandLineUtils
Expand Down Expand Up @@ -39,7 +40,7 @@ public VersionOptionFromMemberAttribute(string template)
/// </summary>
public new string Template { get; set; }

internal CommandOption Configure(CommandLineApplication app, Type type, Func<object> targetInstanceFactory)
internal CommandOption Configure(CommandLineApplication app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, Func<object> targetInstanceFactory)
{
Func<string>? shortFormGetter = null;
Func<string>? longFormGetter = null;
Expand Down
15 changes: 8 additions & 7 deletions src/CommandLineUtils/CommandLineApplication.Execute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -29,7 +30,7 @@ partial class CommandLineApplication
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static int Execute<TApp>(CommandLineContext context)
public static int Execute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(CommandLineContext context)
where TApp : class
=> ExecuteAsync<TApp>(context).GetAwaiter().GetResult();

Expand All @@ -47,7 +48,7 @@ public static int Execute<TApp>(CommandLineContext context)
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static async Task<int> ExecuteAsync<TApp>(CommandLineContext context, CancellationToken cancellationToken = default)
public static async Task<int> ExecuteAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(CommandLineContext context, CancellationToken cancellationToken = default)
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
where TApp : class
{
Expand Down Expand Up @@ -109,7 +110,7 @@ public static async Task<int> ExecuteAsync<TApp>(CommandLineContext context, Can
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static int Execute<TApp>(params string[] args)
public static int Execute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(params string[] args)
where TApp : class
=> Execute<TApp>(PhysicalConsole.Singleton, args);

Expand All @@ -126,7 +127,7 @@ public static int Execute<TApp>(params string[] args)
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static int Execute<TApp>(IConsole console, params string[] args)
public static int Execute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(IConsole console, params string[] args)
where TApp : class
{
args ??= Array.Empty<string>();
Expand All @@ -146,7 +147,7 @@ public static int Execute<TApp>(IConsole console, params string[] args)
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static Task<int> ExecuteAsync<TApp>(params string[] args)
public static Task<int> ExecuteAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(params string[] args)
where TApp : class
=> ExecuteAsync<TApp>(PhysicalConsole.Singleton, args);

Expand All @@ -164,7 +165,7 @@ public static Task<int> ExecuteAsync<TApp>(params string[] args)
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static Task<int> ExecuteAsync<TApp>(string[] args, CancellationToken cancellationToken = default)
public static Task<int> ExecuteAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(string[] args, CancellationToken cancellationToken = default)
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
where TApp : class
{
Expand All @@ -186,7 +187,7 @@ public static Task<int> ExecuteAsync<TApp>(string[] args, CancellationToken canc
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static Task<int> ExecuteAsync<TApp>(IConsole console, params string[] args)
public static Task<int> ExecuteAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(IConsole console, params string[] args)
where TApp : class
{
args ??= Array.Empty<string>();
Expand Down
Loading
Loading