From 674af169005057787fb586865a3ed04fb298055b Mon Sep 17 00:00:00 2001 From: dimension-zero Date: Sat, 23 May 2026 18:18:49 +0100 Subject: [PATCH 1/3] feat(AOT): Annotate Create with RequiresDynamicCode / RequiresUnreferencedCode * New file TrimAnnotations.fs: internal polyfills in System.Diagnostics.CodeAnalysis for RequiresDynamicCodeAttribute and RequiresUnreferencedCodeAttribute. The trimmer and AOT compiler resolve attributes by full type name, so internal copies in this assembly are picked up. Guarded with #if !NET7_0_OR_GREATER so a future multi-target picks up the real BCL attributes. * ArgumentParser.fs: - Add module TrimMessages with constant explanatory strings. - Annotate ArgumentParser.Create<'Template> with both attributes so AOT-publish consumers see IL3050 and trim-publish consumers see IL2026, each with a clear message pointing at the underlying reflection use (MakeGenericType / Activator / FSharpType). * Argu.fsproj: set IsTrimmable=true and IsAotCompatible=false guarded with MSBuild IsTargetFrameworkCompatible('net8.0') so netstandard2.0 builds still succeed without NETSDK1212 noise. A future multi-target pass to net8.0 will activate these flags automatically. Pure metadata addition. No runtime impact. All 112 tests pass. # Conflicts: # src/Argu/Argu.fsproj # src/Argu/ArgumentParser.fs --- src/Argu/Argu.fsproj | 7 +++++++ src/Argu/ArgumentParser.fs | 12 +++++++++++ src/Argu/TrimAnnotations.fs | 40 +++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 src/Argu/TrimAnnotations.fs diff --git a/src/Argu/Argu.fsproj b/src/Argu/Argu.fsproj index 45d89993..2e93b9dd 100644 --- a/src/Argu/Argu.fsproj +++ b/src/Argu/Argu.fsproj @@ -5,8 +5,15 @@ true logo.png enable + + true + false + diff --git a/src/Argu/ArgumentParser.fs b/src/Argu/ArgumentParser.fs index ff2b194b..617ad2a4 100644 --- a/src/Argu/ArgumentParser.fs +++ b/src/Argu/ArgumentParser.fs @@ -1,6 +1,8 @@ namespace Argu open FSharp.Quotations +open System.Diagnostics.CodeAnalysis + open Argu.UnionArgInfo /// Configuration record for . @@ -32,6 +34,14 @@ type ParseConfig = IgnoreUnrecognized = false RaiseOnUsage = true } +module internal TrimMessages = + [] + let aot = + "Argu reflects over the supplied F# discriminated union via MakeGenericType / Activator.CreateInstance to build its schema. This is not safe under publish-AOT; consider a hand-written parser for AOT scenarios." + [] + let trim = + "Argu walks the union schema via System.Reflection (FSharpType.GetUnionCases and friends). The trimmer cannot keep referenced union cases unless the consumer's root sees them; pin the template type with DynamicDependency or DynamicallyAccessedMembers attributes when trimming." + /// The Argu type generates an argument parser given a type argument /// that is an F# discriminated union. It can then be used to parse command line arguments /// or XML configuration. @@ -313,6 +323,8 @@ type ArgumentParser with /// Text width used when formatting the usage string. Defaults to 80 chars. /// The implementation of IExiter used for error handling. Exception is default. /// Indicate if the structure of the arguments discriminated union should be checked for errors. + [] + [] static member Create<'Template when 'Template :> IArgParserTemplate>(?programName : string, ?helpTextMessage : string, ?usageStringCharacterWidth : int, ?errorHandler : IExiter, ?checkStructure: bool) = new ArgumentParser<'Template>(?programName = programName, ?helpTextMessage = helpTextMessage, ?errorHandler = errorHandler, ?usageStringCharacterWidth = usageStringCharacterWidth, ?checkStructure = checkStructure) diff --git a/src/Argu/TrimAnnotations.fs b/src/Argu/TrimAnnotations.fs new file mode 100644 index 00000000..a78464cc --- /dev/null +++ b/src/Argu/TrimAnnotations.fs @@ -0,0 +1,40 @@ +// Polyfills for AOT/trim diagnostic attributes so that +// netstandard2.0 builds can carry the same metadata as .NET 7+ targets. +// The .NET trimmer / publish-AOT pipeline matches by full type name, +// not by assembly, so internal copies are picked up. +namespace System.Diagnostics.CodeAnalysis + +open System + +#if !NET7_0_OR_GREATER + +/// +/// Marker indicating that the annotated method requires dynamic-code +/// generation at runtime (e.g. MakeGenericType + +/// Activator.CreateInstance) and is therefore not safe under +/// publish-AOT. Callers that opt into AOT will see an IL3050 +/// warning when invoking the annotated member. +/// +[] +type internal RequiresDynamicCodeAttribute(message : string) = + inherit Attribute() + /// Reason the method requires dynamic code. + member _.Message = message + /// Optional URL pointing at extended documentation. + member val Url : string = null with get, set + +/// +/// Marker indicating that the annotated method uses reflection in a +/// way the trimmer cannot statically analyse (e.g. arbitrary type +/// reads via FSharpType.GetUnionCases). Callers that enable +/// trimming will see an IL2026 warning. +/// +[] +type internal RequiresUnreferencedCodeAttribute(message : string) = + inherit Attribute() + /// Reason the method holds unreferenced-code requirements. + member _.Message = message + /// Optional URL pointing at extended documentation. + member val Url : string = null with get, set + +#endif From ce619ee6d50ca1e36d77679f988cf2a925be7a40 Mon Sep 17 00:00:00 2001 From: dimension-zero <127850950+dimension-zero@users.noreply.github.com> Date: Sun, 7 Jun 2026 08:42:08 +0100 Subject: [PATCH 2/3] feat(AOT): Propagate trim/AOT annotations to all schema-building entry points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback that the warnings only covered ArgumentParser.Create while several other public APIs also enter the reflection schema-building path (preComputeUnionArgInfo / checkUnionArgInfo via the static lazies on ArgumentParser<'T>). Newly annotated public entry points: - new ArgumentParser<'T>(?programName, ?helpTextMessage, ?usageStringCharacterWidth, ?errorHandler, ?checkStructure) — the primary public constructor - ArgumentParser<'T>.CheckStructure() — forces argInfoWithCheck.Value - toParseResults — top-level helper that goes through Create - tagOf — top-level helper that goes through Create Schema work that goes through the internal (argInfo, ...) constructor (GetSubCommandParser, GetSubCommandParsers, ParseResults.Parser) does not trigger fresh reflection and so does not need annotating: it reuses an argInfo already produced behind one of the annotated entry points. No behaviour change. All 162 tests pass. --- src/Argu/ArgumentParser.fs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Argu/ArgumentParser.fs b/src/Argu/ArgumentParser.fs index 617ad2a4..9da0f5e5 100644 --- a/src/Argu/ArgumentParser.fs +++ b/src/Argu/ArgumentParser.fs @@ -145,6 +145,8 @@ and [] /// Text width used when formatting the usage string. Defaults to 80 chars. /// The implementation of IExiter used for error handling. Exception is default. /// Indicate if the structure of the arguments discriminated union should be checked for errors. + [] + [] new (?programName : string, ?helpTextMessage : string, ?usageStringCharacterWidth : int, ?errorHandler : IExiter, ?checkStructure: bool) = let usageStringCharacterWidth = usageStringCharacterWidth |> Option.defaultWith getDefaultCharacterWidthSafeMin80 let programName = programName |> Option.defaultWith currentProgramName @@ -160,6 +162,8 @@ and [] member val ProgramName = _programName /// Force a check of the discriminated union structure. + [] + [] static member CheckStructure() = argInfoWithCheck.Value |> ignore @@ -339,9 +343,13 @@ module ArgumentParserUtils = r.CharacterWidth, r.ErrorHandler) /// converts a sequence of inputs to a ParseResults instance + [] + [] let toParseResults (inputs : seq<'Template>) : ParseResults<'Template> = ArgumentParser.Create<'Template>().ToParseResults(inputs) /// gets the F# union tag representation of given argument instance + [] + [] let tagOf (input : 'Template) : int = ArgumentParser.Create<'Template>().GetTag input From cbf001f0b75c0781895fa9240ab8bb937904fb1c Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 8 Jun 2026 10:06:57 +0100 Subject: [PATCH 3/3] CL --- RELEASE_NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8047c4fb..d90b7c89 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,6 +8,7 @@ * Add `Argu.Samples.Introspect` sample [#298](https://github.com/fsprojects/Argu/pull/298) [@dimension-zero](https://github.com/dimension-zero) * Add `ArgumentParser.Parse(ParseConfig)` [#307](https://github.com/fsprojects/Argu/pull/307) [@dimension-zero](https://github.com/dimension-zero) * Add `ArgumentParser.PrintUsage(..., ?UsageStrings)` for localization support [#303](https://github.com/fsprojects/Argu/pull/303) [@dimension-zero](https://github.com/dimension-zero) +* Add AOT annotations on [#314](https://github.com/fsprojects/Argu/pull/314) [@dimension-zero](https://github.com/dimension-zero) * Obsolete `EqualsAssignmentAttribute`, `ColonAssignmentAttribute`, `CustomAssignmentAttribute`, `EqualsAssignmentOrSpacedAttribute`, `ColonAssignmentOrSpacedAttribute` and `CustomAssignmentOrSpacedAttribute` [#315](https://github.com/fsprojects/Argu/pull/315) [@dimension-zero](https://github.com/dimension-zero) * Obsolete `PostProcessResult`, `PostProcessResults`, `TryPostProcessResult` [#296](https://github.com/fsprojects/Argu/pull/296) [@dimension-zero](https://github.com/dimension-zero)