Regression: renamed CPM runtime-package pins broke VMR source-build
Summary
To satisfy CPM transitive pinning, the central versions of the dotnet/runtime packages that MSBuild and Roslyn pull (System.Collections.Immutable, System.Reflection.Metadata, System.Composition, System.Diagnostics.DiagnosticSource, System.Security.Cryptography.Xml) were routed through computed $(System*CentralVersion) properties with a version floor, instead of the darc-named $(System*Version) properties. Source-build injects live runtime versions by overriding the darc-named properties, so the renamed, eagerly-floored pins froze at the floor value and the override never reached CPM. In the .NET 11 RC1 VMR the floor demanded 10.0.10 while source-build supplied the live 10.0.9, producing prebuilt packages and NU1109 downgrade errors that blocked the fsharp forward-flow (dotnet/dotnet#8413, #8414, #8415). Caught at the VMR source-only gate; it never shipped.
Error Manifestation
The VMR Source-Only Build SB_CentOSStream10_Online_MsftSdk_x64 leg failed two ways.
Prebuilt detection:
|
And, once the floor was partially relaxed, a downgrade in fsc/fsi:
|
fsharp's own PR/CI legs were green throughout — the failure appeared only downstream in dotnet/dotnet.
Root Cause
fsharp declares these System.* packages in eng/Version.Details.xml with an empty <Sha> and the note "Necessary for source-build. This allows the live version of the package to be used by source-build." When the VMR builds fsharp it passes /p:DotNetPackageVersionPropsPath=…/PackageVersions.fsharp.props, a generated file that unconditionally overrides the darc-named properties — $(SystemCollectionsImmutableVersion), etc. — with the live runtime version it just source-built. Whatever CPM reads must be that exact property for the override to take effect.
The CPM-transitive-pinning work introduced an indirection instead:
|
eng/Packages.props then pinned Version="$(SystemCollectionsImmutableCentralVersion)". Two mistakes combined:
- Rename. Source-build overrides
$(SystemCollectionsImmutableVersion), not$(...CentralVersion). The alias is a different property that source-build has no knowledge of. - Eager floor.
$(...CentralVersion)is computed once, before arcade importsPackageVersions.fsharp.props, and ismax(flowed, floor). Even in source-build it evaluated to the floor.
So the central pin froze at the floor (10.0.10). Source-build only produces the live runtime (10.0.9), which cannot satisfy a 10.0.10 pin from within the source tree, so restore reached an external feed → prebuilt; and where the graph also carried a 10.0.9 constraint, NU1109 downgrade.
The violated assumption: a source-build "live version" package must be consumed through the exact darc-named $(System*Version) property; any renamed or computed intermediate silently defeats the override. roslyn's eng/Packages.props does the correct thing — it references $(SystemCollectionsImmutableVersion) directly.
Why It Escaped
fsharp's PR/CI restores are allowed to pull from nuget.org, where 10.0.10 exists. The floored pin resolved cleanly there, so every fsharp leg — including its own Source-Build (Managed) legs — was green. The only environment that rejects an out-of-tree package is the VMR source-only prebuilt gate (SB_CentOSStream10_Online_MsftSdk_x64), which fsharp CI does not run. The defect was therefore structurally invisible until the change forward-flowed into dotnet/dotnet, roughly a day later.
It also recurred: the same source-build/runtime coherency was fought three times — the MSBuild pin (#20073), the CPM floor (#20084, extended by #20100), and the MSBuild unpin (#20278) — before the direct-property pattern fixed it.
Fix
#20290: reference $(System*Version) directly in eng/Packages.props (matching roslyn), delete the $(...CentralVersion)/SystemRuntimeCentralFloorVersion scaffolding from eng/Versions.props, and declare the packages at 10.0.10 in eng/Version.Details.{xml,props} so product/PR restore meets the MSBuild 18.11 / Roslyn 5.11 transitive minimum. In source-build, PackageVersions.fsharp.props overrides those same properties down to the live runtime version, so the pin always tracks what the VMR actually builds. Verified by feeding a simulated PackageVersions.fsharp.props through /p:DotNetPackageVersionPropsPath: with the fix CPM resolves the injected 10.0.9; with the old alias it stayed frozen at 10.0.10.
Timeline
Date (2026) |
PR |
Event |
|---|---|---|
08-04 |
#20084 |
CPM + transitive pinning introduces |
08-13 |
#20073 |
MSBuild pinned at |
08-18 |
#20100 |
Floor extended to the Roslyn-pulled |
08-18 |
#20278 |
MSBuild unpinned to |
08-19 |
dotnet/dotnet#8413/#8414/#8415 |
Forward-flow source-only build fails: prebuilt |
08-19 |
#20290 |
Direct |
Prevention
.github/instructions/EngVersioning.instructions.md scoped to eng/Packages.props, eng/Versions.props, and eng/Version.Details.{xml,props} encodes the rule: consume source-build "live version" packages through the darc-named $(System*Version) property directly; never route them through a renamed or computed central alias/floor. When a transitive consumer (MSBuild, Roslyn) needs a higher minimum than the flowed baseline, raise the declared version in eng/Version.Details.* rather than adding a floor — source-build overrides it back down to the live runtime version. fsharp CI cannot reproduce the VMR source-only prebuilt gate, so these files must be reasoned about against source-build, not just a green PR restore.
F# Compiler Guide