Skip to content

Build the native library on VS 2022, not on whatever is latest - #7

Merged
TorinKS merged 3 commits into
mainfrom
fix/pin-vs2022-toolchain
Aug 30, 2026
Merged

Build the native library on VS 2022, not on whatever is latest#7
TorinKS merged 3 commits into
mainfrom
fix/pin-vs2022-toolchain

Conversation

@TorinKS

@TorinKS TorinKS commented Aug 30, 2026

Copy link
Copy Markdown
Owner

The measurement

dumpbin /dependents on the published WinDevicesNet 0.1.1 native library:

WinDevices.dll  →  SETUPAPI.dll, KERNEL32.dll, ole32.dll
                   MSVCP140.dll
                   VCRUNTIME140.dll
                   VCRUNTIME140_1.dll
                   api-ms-win-crt-*.dll

Nothing in the CMake configuration sets CMAKE_MSVC_RUNTIME_LIBRARY, so the default /MD applies and the library links the dynamic CRT.

Why that matters here

The toolset that builds this DLL sets the minimum Visual C++ Redistributable every consumer's machine must carry. The v14x runtimes are compatible forward, not backward.

windows-latest silently became windows-2025-vs2026. So 0.1.1 was built with the VS 2026 toolset and shipped inside a package intended for components that are built — and deployed — on VS 2022. It loaded during verification only because the verifying workstation happens to have a current redistributable installed. A clean endpoint, of the kind ADR 0015 pins the baseline to, does not.

Change

Pin the three jobs that compile to windows-2022:

Workflow Job Runner
build-test.yaml build windows-2022
build-test.yaml summary windows-latest — prints only
increment-version.yaml increment-version windows-latest — git only
increment-version.yaml build-and-release windows-2022
publish-nuget.yaml pack windows-2022

Also drops the CMake 3.29 pin from increment-version.yaml. It would have failed exactly as build-test.yaml did: install.cmd configures without a generator, and a CMake older than the image's Visual Studio falls back to NMake Makefiles and then reports CMAKE_CXX_COMPILER not set. That job had not run since the image changed, so the breakage was latent.

Deliberately not done here

Linking the CRT statically (/MT) would remove the redistributable question altogether and make the package self-contained. It is the stronger long-term answer, but it changes heap ownership semantics across the DLL boundary: with a static CRT the library and its host no longer share a heap, so any allocation made on one side and freed on the other becomes a bug. The C API appears to use explicit create/destroy pairs, which would be safe — but that needs checking rather than assuming, so it belongs in its own issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XnzCrAM46SttZnU6msznyN

WinDevices.dll links the dynamic CRT. dumpbin on the published 0.1.1 shows
MSVCP140.dll, VCRUNTIME140.dll and VCRUNTIME140_1.dll among its imports,
because CMake defaults to /MD and nothing overrides it.

The toolset that builds it therefore sets the minimum Visual C++
Redistributable every consumer's machine must carry: v14x runtimes are
compatible forward, not backward. windows-latest silently became
windows-2025-vs2026, so 0.1.1 was built with the VS 2026 toolset and shipped
inside a package consumed by components that are built, and deployed, on
VS 2022. It loaded during verification only because this workstation has a
current redistributable installed, which a clean endpoint does not.

Pin the three jobs that compile to windows-2022. The reporting job and the
git-only version job keep windows-latest; neither produces a binary.

Also drop the CMake 3.29 pin from increment-version.yaml, which would have
failed the same way build-test.yaml did: install.cmd configures without a
generator, and a CMake older than the image's Visual Studio falls back to
NMake Makefiles.

Static linking of the CRT would remove the redistributable question
entirely, but it needs the C API checked for heap ownership crossing the
DLL boundary first, so it is left to its own issue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XnzCrAM46SttZnU6msznyN
Version Increment pushes its tag with GITHUB_TOKEN, and events created with
that token deliberately do not start other workflows. The tag-push trigger
therefore never fires for an automated release, and publishing had to be
dispatched by hand after every version bump.

Trigger on workflow_run instead. It fires when Version Increment finishes
rather than from a token-created event, so no personal access token has to
be stored to make the chain work.

Two alternatives were rejected. Duplicating the publish steps inside
increment-version.yaml would break Trusted Publishing, because the policy on
nuget.org is bound to this workflow's file name. Converting this into a
reusable workflow called from there splits the OIDC claim between
workflow_ref and job_workflow_ref, and which of the two nuget.org validates
is not something to discover through a live publish.

Also:

- the job is skipped unless the triggering run succeeded, since workflow_run
  fires on failure too
- for workflow_run the checkout pins the triggering run's head_sha rather
  than the branch tip, which may already have moved
- the publish decision moves into one gate step, so the three conditional
  steps that depend on it cannot drift apart


Claude-Session: https://claude.ai/code/session_01XnzCrAM46SttZnU6msznyN

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread .github/workflows/publish-nuget.yaml Fixed
CodeQL flagged actions/untrusted-checkout at high severity against the
checkout I added for the workflow_run path.

It is right. A workflow_run job is privileged: it holds id-token: write and
can exchange it for a nuget.org publishing token. This job then builds what
it checks out, running cmake and dotnet over that tree. Pinning
workflow_run.head_sha means executing code from a commit this workflow did
not choose, in exactly the context that can publish a package. Version
Increment is dispatch-only today, so reaching it needs write access, but the
shape is an escalation path to the package feed and should not exist.

Take the default checkout instead, which is the default branch and therefore
trusted. Version Increment tags a commit on that branch, so the tag is
present when this runs. If the branch has moved past the tag in between, the
exact-tag guard refuses to publish rather than releasing a mislabelled
package - the correct failure, and the reason that guard was added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XnzCrAM46SttZnU6msznyN
@TorinKS
TorinKS merged commit ac6761e into main Aug 30, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants