Stop the SDK's own trimmer version deciding whether CI can restore #11

Merged
jaap-jan merged 1 commits from claude/illink-lock-drift into main 2026-08-12 10:03:25 +00:00
Owner

This unblocks main and every open pull request. They all went red together on a restore that never reached a compiler:

error NU1004: The package reference Microsoft.NET.ILLink.Tasks version has changed
from [10.0.10, ) to [10.0.11, ). The packages lock file is inconsistent with the
project dependencies so restore can't be run in locked mode.

Nothing in any of those commits touched a package. .NET had shipped SDK 10.0.400.

The version in the lock files was never this repository's to decide

Microsoft.NET.ILLink.Tasks is referenced by nothing here. The SDK adds it to any project setting IsTrimmable or IsAotCompatibleDodoSSH.Contracts and DodoSSH.Crypto do, and the Android head gets it from trimming being on by default — and supplies the version itself from the KnownILLinkPack item in its own Microsoft.NETCoreSdk.BundledVersions.props. SDK 10.0.302 says 10.0.10; 10.0.400 says 10.0.11.

packages.lock.json records that as a Direct reference with a requested range, so the committed file actually means whichever SDK last ran a restore. global.json says rollForward: latestMinor, so setup-dotnet installs the newest 10.x SDK that exists on the morning it runs. The gate did its job — an unreviewed dependency change is what it is there to stop — but the change it caught was not one anybody could have reviewed, and it recurs on every servicing release.

Regenerating the lock files alone would have been the worse repair, and not only because it holds until the next release: it cannot be done from a machine whose newest SDK is older than the runner's. Every SDK on mine tops out at 10.0.302, which writes 10.0.10 straight back and re-breaks CI. The recorded version would flip according to who restored last — the state locking exists to prevent.

The change

The version is pinned in a new Directory.Build.targets, and the three lock files are regenerated against the pin. It is an Update on the SDK's item rather than a PackageVersion because the reference is implicit, so central package management is never consulted; it sits inside a target because conditioning on %(TargetFramework) is item batching, which is legal in a target and MSB4191 during evaluation.

Pinned forward to 10.0.11 rather than back to 10.0.10, which would have been a one-liner with no lock churn. Holding the trimmer a release behind the framework it analyses is how a missed trim warning happens, and taking the newer one makes the bump a reviewed diff — which is what the gate was asking for.

Verification, against the SDK that broke it

check SDK result
dotnet restore DodoSSH.slnx --locked-mode in sdk:10.0-alpine — ci.yml's line 10.0.400 exit 0
locked-mode restore of DodoSSH.Client.Android with the android workload on sdk:10.0-noble — ci-android.sh's line 10.0.400 exit 0
the same solution restore locally 10.0.302 exit 0

One set of lock files satisfying both SDKs is the point of the pin, and the third row is what demonstrates it.

Release build clean: 0 errors, and 0 IL-prefixed diagnostics from the newer analyser on the two trimmable projects. 1,869 tests over 19 suites, none failing.

One caution recorded in the commit: --force-evaluate on Windows rewrites every lock file with CRLF, and 23 of the 26 had no content change at all. Only the three that really moved are here.

**This unblocks `main` and every open pull request.** They all went red together on a restore that never reached a compiler: ``` error NU1004: The package reference Microsoft.NET.ILLink.Tasks version has changed from [10.0.10, ) to [10.0.11, ). The packages lock file is inconsistent with the project dependencies so restore can't be run in locked mode. ``` Nothing in any of those commits touched a package. .NET had shipped SDK 10.0.400. ### The version in the lock files was never this repository's to decide `Microsoft.NET.ILLink.Tasks` is referenced by nothing here. The SDK adds it to any project setting `IsTrimmable` or `IsAotCompatible` — `DodoSSH.Contracts` and `DodoSSH.Crypto` do, and the Android head gets it from trimming being on by default — and supplies the version itself from the `KnownILLinkPack` item in its own `Microsoft.NETCoreSdk.BundledVersions.props`. SDK 10.0.302 says 10.0.10; 10.0.400 says 10.0.11. `packages.lock.json` records that as a Direct reference with a requested range, so the committed file actually means *whichever SDK last ran a restore*. `global.json` says `rollForward: latestMinor`, so `setup-dotnet` installs the newest 10.x SDK that exists on the morning it runs. The gate did its job — an unreviewed dependency change is what it is there to stop — but the change it caught was not one anybody could have reviewed, and it recurs on every servicing release. Regenerating the lock files alone would have been the worse repair, and not only because it holds until the next release: it cannot be done from a machine whose newest SDK is older than the runner's. Every SDK on mine tops out at 10.0.302, which writes 10.0.10 straight back and re-breaks CI. The recorded version would flip according to who restored last — the state locking exists to prevent. ### The change The version is pinned in a new `Directory.Build.targets`, and the three lock files are regenerated against the pin. It is an `Update` on the SDK's item rather than a `PackageVersion` because the reference is implicit, so central package management is never consulted; it sits inside a target because conditioning on `%(TargetFramework)` is item batching, which is legal in a target and `MSB4191` during evaluation. Pinned **forward** to 10.0.11 rather than back to 10.0.10, which would have been a one-liner with no lock churn. Holding the trimmer a release behind the framework it analyses is how a missed trim warning happens, and taking the newer one makes the bump a reviewed diff — which is what the gate was asking for. ### Verification, against the SDK that broke it | check | SDK | result | | --- | --- | --- | | `dotnet restore DodoSSH.slnx --locked-mode` in `sdk:10.0-alpine` — ci.yml's line | 10.0.400 | exit 0 | | locked-mode restore of `DodoSSH.Client.Android` with the android workload on `sdk:10.0-noble` — ci-android.sh's line | 10.0.400 | exit 0 | | the same solution restore locally | 10.0.302 | exit 0 | One set of lock files satisfying both SDKs is the point of the pin, and the third row is what demonstrates it. Release build clean: 0 errors, and 0 IL-prefixed diagnostics from the newer analyser on the two trimmable projects. 1,869 tests over 19 suites, none failing. One caution recorded in the commit: `--force-evaluate` on Windows rewrites every lock file with CRLF, and 23 of the 26 had no content change at all. Only the three that really moved are here.
jaap-jan added 1 commit 2026-08-12 09:46:00 +00:00
Stop the SDK's own trimmer version deciding whether CI can restore
ci / build and test (pull_request) Successful in 2m24s
ci / desktop nightly (pull_request) Skipped
ci / android head (pull_request) Successful in 3m22s
ci / api image (pull_request) Successful in 21s
93e35a0095
CI went red across the whole repository — main's run 125 and every open pull
request at once — on a restore that never reached a compiler:

    error NU1004: The package reference Microsoft.NET.ILLink.Tasks version has
    changed from [10.0.10, ) to [10.0.11, ). The packages lock file is
    inconsistent with the project dependencies so restore can't be run in
    locked mode.

Nothing in any of those commits touched a package. .NET had shipped SDK 10.0.400.

◆ THE VERSION IN THE LOCK FILES WAS NEVER THIS REPOSITORY'S TO DECIDE.

Microsoft.NET.ILLink.Tasks is referenced by nothing here. The SDK adds it to any
project setting IsTrimmable or IsAotCompatible — DodoSSH.Contracts and
DodoSSH.Crypto do, and the Android head gets it from trimming being on by
default — and it supplies the version itself, from the KnownILLinkPack item in
its own Microsoft.NETCoreSdk.BundledVersions.props. 10.0.302 says 10.0.10;
10.0.400 says 10.0.11.

packages.lock.json records that as a Direct reference with a requested range, so
what the committed file actually means is "whichever SDK last ran a restore".
global.json says rollForward: latestMinor, so setup-dotnet installs the newest
10.x SDK that exists on the morning it runs. The gate did its job — an unreviewed
dependency change is exactly what it is there to stop — but the change it caught
was not one anybody could have reviewed, and it will recur on every servicing
release.

Regenerating the lock files alone would have been the worse repair, and not only
because it holds until the next release. It cannot be done from this machine at
all: every SDK installed here tops out at 10.0.302, which writes 10.0.10 straight
back and re-breaks CI. The recorded version would flip according to who restored
last — the precise state locking exists to prevent.

So the version is pinned in Directory.Build.targets and the three lock files are
regenerated against the pin. It is an Update on the SDK's item rather than a
PackageVersion in Directory.Packages.props because the reference is implicit:
the SDK supplies a version, so central package management is never consulted. It
sits in a target because the conditioning is on %(TargetFramework) — all the
KnownILLinkPack items share one identity and only that metadata separates
net10.0's from net8.0's — and item batching in a condition is legal inside a
target and MSB4191 during evaluation.

Pinned forward to 10.0.11 rather than back to 10.0.10, which would have been a
one-line change with no lock file churn. Holding the trimmer a release behind the
framework it analyses to dodge an error is how a missed trim warning happens, and
taking the newer one makes the bump a reviewed diff, which is what the gate was
asking for.

Verified against the SDK that broke it rather than only the one here:

  - sdk:10.0-alpine, 10.0.400, `dotnet restore DodoSSH.slnx --locked-mode` —
    exit 0. That is ci.yml's line, on CI's SDK.
  - the android workload on sdk:10.0-noble, 10.0.400, locked-mode restore of
    DodoSSH.Client.Android — exit 0. That is scripts/ci-android.sh's line.
  - locally on 10.0.302, the same locked-mode restore of the solution — exit 0.

One set of lock files satisfying both SDKs is the whole point of the pin, and the
third check is the one that demonstrates it.

Release build clean: 0 errors, and 0 IL-prefixed diagnostics from the newer
analyser on the two trimmable projects. 1,869 tests over 19 suites, none failing.

A caution for the next person, learned the hard way here: `--force-evaluate` on
Windows rewrites every lock file it touches with CRLF, and 23 of the 26 had no
content change at all. Only the three that really moved are in this commit.
jaap-jan merged commit a763f4b113 into main 2026-08-12 10:03:25 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DodoTech-Public/DodoSSH#11