Skip to content

Ship FSharp.Core with a net10.0 target framework - #20224

Closed
T-Gro wants to merge 12 commits into
dotnet:mainfrom
T-Gro:t-gro-fsharpcore-net10-tfm-plan
Closed

Ship FSharp.Core with a net10.0 target framework#20224
T-Gro wants to merge 12 commits into
dotnet:mainfrom
T-Gro:t-gro-fsharpcore-net10-tfm-plan

Conversation

@T-Gro

@T-GroT-Gro commented Aug 6, 2026

Copy link
Copy Markdown
Member

Ships FSharp.Core with an additional net10.0 target framework alongside netstandard2.0 and netstandard2.1. Consumers on modern .NET now bind a net-TFM assembly directly instead of the netstandard2.1 fallback.

The net version is a pinned knob (FSharpCoreShippedNetTargetFramework, default net10.0) that deliberately lags the in-development product TFM. It does not move automatically when the product advances: F# built on net11 still ships lib/net10.0, and the pin bumps only as an explicit, reviewed step. A build-time guard fails if the pin is ever >= the product TFM.

The net10.0 public surface is identical to the netstandard2.1 one — the only difference is the BCL reference set. That invariant is enforced by a blocking, strict/bidirectional assembly-level ApiCompat gate with an empty suppression set, plus a dedicated net SurfaceArea baseline and a TFM #if-guard audit.

Proto stays a single netstandard2.0 bootstrap; the extra target is added to non-Proto builds only.

Not done here (cross-repo, must follow this PR): the shipped-in-SDK path flows dotnet/fsharp → dotnet/dotnet (VMR) → dotnet/sdk, and full source-build validation needs a real VMR leg (DotNetBuildFromVMR). Those cannot be exercised from this repo.

Verified locally on macOS: the 3-TFM FSharp.Core build, pack producing lib/net10.0, the ApiCompat identity gate (passes with empty suppressions, fails on an injected member diff in either direction), the #if audit self-test, and the e2e pack + net10.0-consumer resolution witness (compile and runtime both bind lib/net10.0; a net8.0 control falls back to lib/netstandard2.1). The Windows-only trim/AOT legs and the full CI matrix run in the pipeline.

Draft: opening for CI signal and review of the pinned-TFM policy before finalizing.

T-Groand others added 11 commits August 6, 2026 14:57
…b (net10.0) + lag guard
The shipped FSharp.Core net TFM is a pinned-stable knob that lags the in-dev
product TFM (FSharpNetCoreProductTargetFramework=net11.0) and tracks the latest
stable released .NET. A build/pack-time guard scoped to the FSharp.Core project
fails if the pin is not strictly less than the product TFM.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add $(FSharpCoreShippedNetTargetFramework) (net10.0) to the non-Proto TFMs and
make FSharp.Core actually compile against the net10 BCL:
- prim-types.fs: move the #endif of the '#if !NET5_0_OR_GREATER' block so the
structural 'namespace Microsoft.FSharp.Core' + opens are unconditional (they
were trapped inside the pre-.NET5 polyfill guard and vanished on any net TFM,
causing ~100 'System type not defined' errors). ns2.x output is unchanged.
- prim-types.fsi: guard the System.Diagnostics.CodeAnalysis polyfill with the
matching '#if !NET5_0_OR_GREATER' so the signature drops it on net (the impl
already did), fixing the sig/impl mismatch (FS0193).
- prim-types.{fs,fsi}: floor the .NET8+ CollectionBuilder/ScopedRef polyfills
with '#if !NET8_0_OR_GREATER' (net BCL provides them).
- Widen collection-expression / IAsyncDisposable guards to '|| NET'
(set.{fs,fsi}, tasks.{fs,fsi}, prim-types.{fs,fsi} List builders).
- local.fs / Query.fs: disambiguate net10 BCL overloads
(String.Format ReadOnlySpan<obj>, ElementAt/Take Index/Range) by pinning the
original obj[]/int overloads. No public-surface change.
Builds clean on netstandard2.0, netstandard2.1 and net10.0 (no warnings).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- FSharp.Core.nuspec: add a net10.0 dependency group and lib/net10.0 file
entries (dll, xml, satellite resources), all driven by a
$FSharpCoreShippedNetTargetFramework$ token so the package layout tracks the
single pinned knob rather than hardcoding the TFM in the manifest.
- FSharp.Core.fsproj: surface the knob to the nuspec via
<NuspecProperty Include="FSharpCoreShippedNetTargetFramework=..."> (non-Proto).
- Microsoft.FSharp.Compiler.fsproj: include the pinned TFM in the shipping-path
pack override so the shipped FSharp.Core is packed for net10.0 too, not just
netstandard2.0/2.1.
Verified via Arcade pack: nupkg contains lib/{netstandard2.0,netstandard2.1,net10.0}
with dll+xml+satellites and a <group targetFramework="net10.0" /> dependency group.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Modern .NET hosts (net11+) now bind lib/net10.0, so the SurfaceArea test
must verify against a net baseline (references System.Runtime, not
netstandard). Reorder platform detection to '#if NET' first, add
FSharp.Core.SurfaceArea.net.{debug,release}.bsl, and document that the
netstandard2.1 baselines are intentionally retained but no longer
exercised (ns2.1 coverage moves to the ApiCompat identity gate).
The net member surface is byte-identical to ns2.1 (only assembly-
reference lines differ), confirming the net10.0 == ns2.1 invariant.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd0ca93f-cf73-450a-8a5a-f9e59841802e
Add a dedicated FSharp.Core.ApiCompat project that runs the SDK's
Microsoft.DotNet.ApiCompat.Task.ValidateAssembliesTask (registered by
Sdk.targets, not Arcade, not package validation) comparing the freshly
built netstandard2.1 (contract) and net10.0 implementation assemblies in
strict/bidirectional mode with an empty suppression set. This enforces
the net10.0 == netstandard2.1 public-surface invariant.
Wire it as a required, blocking FSharpCore_ApiCompat job (no
continueOnError) in azure-pipelines-PR.yml.
Verified locally: passes clean on identically-built DLLs; strict mode
correctly FAILS on injected additions (ns2.0-vs-net10), non-strict
silently passes them (why strict is mandated).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd0ca93f-cf73-450a-8a5a-f9e59841802e
Allow-list audit of every TFM-discriminating #if/#elif guard in
src/FSharp.Core/**/*.{fs,fsi}: each must be a reviewed <file>:<expr>
pair, so a bare '#if NETSTANDARD2_1' (excludes the net TFM) or bare
'#if !NET' (drops a BCL polyfill on every net TFM) or any un-reviewed
new TFM guard fails.
Implemented as a portable F# fsx (git ls-files + .NET regex, not
'git grep -P' which needs PCRE) and wired as a blocking step in the
required CheckCodeFormatting job, matching the repo's FSCompCheck.fsx /
TestSplit.fsx source-hygiene idiom.
Verified: self-test OK; passes on the shipped tree (10 allow-listed
guards); FAILS on an injected bare '#if NETSTANDARD2_1'.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd0ca93f-cf73-450a-8a5a-f9e59841802e
…rifier
Portable fsx that discovers the shipped net TFM lib folder from the produced
FSharp.Core nupkg (never a hard-coded literal) and asserts lib/{netstandard2.0,
netstandard2.1,<pin>} DLL+XML present and non-degenerate, satellites present,
a <group targetFramework> dependency group in the nuspec, and a UNIFORM
AssemblyVersion across all three lib assemblies.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd0ca93f-cf73-450a-8a5a-f9e59841802e
Isolated net-TFM consumer (targets $(FSharpCoreShippedNetTargetFramework)) that
source-maps FSharp.Core to the locally built package and STRUCTURALLY witnesses,
from obj/project.assets.json, that both compile and runtime bind to
lib/<pin>/FSharp.Core.dll and NOT to any netstandard asset. Driver pins the exact
built version, purges only the cached fsharp.core/<ver>, and runs a widened
IAsyncDisposable task member as the runtime smoke (roll-forward for the net run).
Verified locally: net10.0 -> lib/net10.0 (compile+runtime) + smoke green;
net8.0 control -> lib/netstandard2.1 (witness bites).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd0ca93f-cf73-450a-8a5a-f9e59841802e
Multi-target the AheadOfTime Trimming (x3) and NativeAOT consumers on
net9.0;$(FSharpCoreShippedNetTargetFramework) so the shipped net-TFM lib is
exercised under PublishTrimmed/PublishAot. The gate is publish SUCCESS under
TreatWarningsAsErrors=true (IL2xxx/IL3050 => errors); trimmed sizes for the net
pin are report-only (-expected_len -1) since byte counts churn per preview. Both
check.ps1 scripts derive the pin from the knob (no literal net10.0 to drift).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd0ca93f-cf73-450a-8a5a-f9e59841802e
…nto PR pipeline
- e2e-1 runs in EndToEndBuildTests right after the -pack step.
- e2e-2 runs in Build_And_Test_AOT_Windows after the AheadOfTime checks, with the
consumer's project.assets.json published as always()/continueOnError failure
evidence.
e2e-5 (trim/AOT net-TFM legs) needs no new wiring: the existing AheadOfTime
check.ps1 chain already runs the multi-targeted projects.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd0ca93f-cf73-450a-8a5a-f9e59841802e
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd0ca93f-cf73-450a-8a5a-f9e59841802e
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd0ca93f-cf73-450a-8a5a-f9e59841802e
@github-actions

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change pathRelease notes pathDescription
`src/FSharp.Core`docs/release-notes/.FSharp.Core/11.0.100.md

@T-Gro

T-Gro commented Aug 7, 2026

Copy link
Copy Markdown
MemberAuthor

Reopening with the branch hosted on dotnet/fsharp directly (fork PRs don't auto-run fsharp-ci). Superseded by the same branch pushed to origin.

@T-GroT-Gro closed this Aug 7, 2026
@T-Gro
T-Gro deleted the t-gro-fsharpcore-net10-tfm-plan branch August 12, 2026 11:49
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants

@T-Gro