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.