Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

454. A force-link list belongs in the object, not on the link line

Date: 2026-09-21

Status

Accepted.

Context

With ADR-0450 in, the Windows superbuild compiled. It then failed at the last step but one, linking the library it had just built every object for:

[679/681] Linking CXX shared library goldberry.dll
FAILED: [code=1] goldberry.dll goldberry.lib
C:\Windows\system32\cmd.exe /C ""...link.exe" ... /INCLUDE:goldberry_abi_version
/INCLUDE:SDL_Init ... (253 of them) ... /DEF:.../goldberry.def"
The command line is too long.

Two numbers explain it. The command is 8541 characters, of which 7697 are the 253 /INCLUDE: flags. CreateProcess would take 32767 — but Ninja runs the link through cmd.exe /C, and cmd stops at 8191. We were 350 over.

The flags are not optional. Every exported symbol needs one, or the static archives contribute nothing: SDL_Init is referenced by no Goldberry source, so without /INCLUDE: the linker never pulls it out of SDL3-static.lib and the .def exports a name that is not in the image. That is the same force-link problem Linux solves with -u and macOS with -u _name.

What made this hard to see is that windows.yml was green on the same commit. It uses the Visual Studio generator, which drives MSBuild, which passes the link through a response file of its own; showcase.yml uses Ninja, which does not. One commit, two Windows jobs, opposite results, and nothing in the failure naming /INCLUDE: as the thing that was too long.

It had also been true for a while and only just crossed the line. eb30c1bf renamed 25 exported HarfBuzz symbols from hb_* to goldberry_hb_*, adding ten characters each, and ADR-0385 added six WebP animation entries. The list had been growing toward 8191 for months and the build that crossed it is not the build that caused it.

Decision

The MSVC force-link list is generated as #pragma comment(linker, ...) directives in a source file compiled into the library.

// Generated by CMake from exports/goldberry.symbols -- do not edit.
#pragma comment(linker, "/INCLUDE:goldberry_abi_version")
#pragma comment(linker, "/INCLUDE:SDL_Init")
...

The compiler writes each directive into the object’s .drectve section and the linker reads them from there. Nothing reaches a command line, so nothing can be too long for one.

The response file, which is what this record first said, does not work

It is the obvious answer, it fixed the Ninja build, and it broke the Visual Studio one:

LINK : fatal error LNK1104: cannot open file '@...\goldberry.force'

Response files do not nest. MSBuild already passes the whole link through one of its own, and an @file inside a response file is not expanded — link.exe takes it as the name of a file to link, @ and all, and cannot open it. Ninja does not use a response file for the options, so there the @file was expanded and worked.

That is the same disagreement between the two generators as the original bug, in the other direction. It is the reason the answer has to be off the command line altogether rather than merely shorter: any fix that is a link argument is a fix that one of the two generators will handle differently from the other, and there is no way to tell which from a machine that runs neither.

It is recorded here rather than quietly replaced because the reasoning that led to it was sound and still wrong — “every other platform writes its list to a file, so Windows should too” is a good argument that happens not to survive contact with MSBuild.

Linux and macOS are untouched

ld and ld64 are not invoked through cmd.exe, the limit there is ARG_MAX in the megabytes, and -u on the command line works under every generator. Changing a thing that works to match a thing that had to change is not symmetry worth having.

Not “drop the flags, the .def already forces them”

Plausible, and not taken. MSVC does resolve a .def export by pulling the defining object out of an archive, so most of the 253 would probably survive. “Probably” is the problem: the failure mode is a symbol silently missing from the DLL, which surfaces as an UnsatisfiedLinkError in Java on the first call through it, on Windows only. A response file changes how the flags are delivered and nothing about what they mean.

Consequences

The Windows link line loses 7.7 KB and does not grow with the export list again — which it does with every upstream symbol added, and nothing was watching.

Both generators agree. They are the same build twice, and this is the second time in one batch that a green Windows job beside a red one on the same commit turned out to be the two generators disagreeing rather than flakiness. That pattern is worth remembering: windows.yml uses Visual Studio, showcase.yml uses Ninja, and a Windows fix is not verified until both have run.

A test holds the shape, and it now names both wrong answers. msvcForceLinkListIsInTheObject refuses the inline /INCLUDE:${_symbol} and refuses the @${_force_file} — the second is in there because it is the mistake a reader is most likely to make again, having read the first half of this record. It reports the current inline size when it fires, 7950 bytes today, so whoever trips it learns the number without the test asserting one that moves.

The generated source was checked by generating it: the CMake block run standalone against exports/goldberry.symbols produces 253 pragmas for 253 symbols and compiles as C. What could not be checked here is that MSVC honours them, which is the whole point of the change and needs a Windows runner.