0.19.2: musl's empty archives, so -lm is answered by the C library and never by the host - #43
Merged
Merged
Conversation
…d never by the host mcpp-community/mcpp#696 measured that a link whose C library comes from the dependency graph still lets clang search the build machine's own library directories, because the graph branch passes no --sysroot. A -l name the graph does not answer is then looked up on the host: silently, by pulling a glibc object into a musl static image on x86_64-linux-musl (a real fixture, `fmaximum`, resolved through `/usr/lib/x86_64-linux-gnu/libm-2.39.a`); loudly, by a foreign linker script or an outright `unable to find library`, on aarch64-linux-musl. mcpp's own repair closes this by handing such a link an empty --sysroot, which removes every host directory a link searches -- including the accidental answer, so `-lm` becomes a hard failure everywhere unless the graph itself answers it. musl's own `make install` already answers it: `EMPTY_LIB_NAMES = m rt pthread crypt util xnet resolv dl` are installed next to libc.a as archives with no members, because libc already holds everything POSIX ascribes to those eight names. This package never ran musl's install and never shipped them. This change adds `port/lib`, holding the same eight files (each exactly the eight bytes `!<arch>\n`, an ar archive with no members, built the same way musl's own Makefile builds them), and a package-relative `-Lport/lib` on the Linux link line. mcpp already resolves a dependency's relative `-L` in `[build] ldflags` against the dependency's own root and forwards it to the consumer, so no engine change is needed for this half -- which is why this half must land, and be registered, before mcpp's own sysroot fix ships. Measured (clang 22.1.8 and gcc 16.1.0; x86_64-linux-musl natively and cross-built for aarch64-linux-musl under qemu-aarch64; this package's own worktree as a path dependency): `-lm`, `-lpthread`, `-ldl` and `-lrt` each resolve to this package's own stub archive on every one of those configurations, ahead of every host directory clang and gcc still add today, and the resulting programs build, link and run correctly. Separately, a program declaring `fmaximum` (the exact symbol #696's fixture used, which musl does not implement) now fails to link with `undefined symbol: fmaximum` instead of silently resolving it from the host's glibc: the stub answers `-lm` first, and the linker does not fall through to a later directory once one has answered a name. `.github/workflows/ci.yml` gains `examples/link-stubs` and two Linux-only steps: one reads `mcpp build --verbose`'s own linker trace and asserts that each of the four names resolved inside this package and nowhere under `/usr` or `/lib`, the other runs the program and checks that the four calls it makes still behave correctly. The assertion holds under the mcpp release in use today (this package's `-L` precedes the host directories) and continues to hold once mcpp#696's own engine fix ships (this package's `-L` is then the only one left). Only `cfg(os = "linux")` gains the `-L`: it is the target #696 measured and this CI now asserts against. Extending it to `cfg(windows)` and `cfg(os = "macos")` is very likely correct -- musl's own interface makes libc the answer to these eight names on every target -- but is left for whoever next measures a graph link on those targets, per the manifest comment beside `port/lib`. Dependency pins (openkal-linux, openkal-macos, openkal-opensbi, openkal-windows) are unchanged.
The ordering paragraph beside port/lib attributed a clang-specific -### reading to "both toolchain families this package supports on Linux", but -### was only run against clang; the gcc row's evidence is GNU ld's own --verbose trace, already cited two sentences later. Restated to claim only what was run.
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mcpp-community/mcpp#696 measured that a link whose C library comes from the dependency graph (this package) still lets clang search the build machine's own library directories, because the graph branch passes no
--sysroot. A-lname the graph does not answer is then looked up on the host: silently, by pulling a glibc object into a musl static image on x86_64-linux-musl (the report'sfmaximumfixture resolved through/usr/lib/x86_64-linux-gnu/libm-2.39.a); loudly, by a foreign linker script or an outrightunable to find library, on aarch64-linux-musl. mcpp's own repair hands such a link an empty--sysroot, which removes the accidental answer along with every host directory, so-lmbecomes a hard failure everywhere unless the graph itself answers it.musl's own
make installalready answers it:EMPTY_LIB_NAMES = m rt pthread crypt util xnet resolv dlare installed next tolibc.aas archives with no members, becauselibc.aalready holds everything POSIX ascribes to those eight names. This package never ran musl's install and never shipped them. This PR:port/lib, holding the same eight files (libm.a librt.a libpthread.a libcrypt.a libutil.a libxnet.a libresolv.a libdl.a), each exactly the eight bytes!<arch>\n— anararchive with no members, built the way musl's own Makefile builds them.-Lport/libto[target.'cfg(os = "linux")'.build] ldflags. mcpp already resolves a dependency's relative-Lagainst the dependency's own root and forwards it to the consumer's link line, so this needs no engine change — which is why this half must land, and be registered in mcpp-index, before mcpp's own sysroot fix (mcpp#696's engine half) ships.port/lib, in the repository's own style, with a link to mcpp#696.examples/link-stubsand two Linux-only CI steps that assert the fix.openkal-linux,openkal-macos,openkal-opensbi,openkal-windows) are unchanged, matching what the previous release commit (f1f789f) touched.Scope
Only
cfg(os = "linux")gains the-L. musl's own interface makes libc the answer to these eight names on every target this package supports, so extending this tocfg(windows)andcfg(os = "macos")is very likely correct — but I only measured Linux (see below), so only Linux changes. Extending it is left for whoever next measures a graph link on those targets.What I measured
Toolchains: llvm 22.1.8 and gcc 16.1.0, from a local mcpp home. Engine: the currently released mcpp 2026.9.25.1 (no
--sysrootfix yet — that is mcpp#696's still-open engine half). This package was reached as a path dependency from a scratch consumer and fromexamples/link-stubs.--target x86_64-linux-musl, and natively with no--targetat all — this package answersmcpp:c-abi=musleither way):-lm,-lpthread,-ldl,-lrteach resolve to this package's ownport/lib/lib*.a. Fromld.lld --verbose:ld --verbose(viagcc@16.1.0), the same four names, each asattempt to open <checkout>/port/lib/lib<name>.a succeeded, with no/usror/libattempt for any of them. The resulting binary is a static ELF with no dynamic section and runs correctly (fmax,pthread_create/join,clock_gettime,dlopenall behave as expected).qemu-aarch64(throughopenkal-llvm-runtimefor a target-native libc++, so the link isn't blocked by an unrelated host/target architecture mismatch in the C++ runtime): the same four names resolve to the sameport/libarchives (ld.lld --verbose), and the binary runs correctly under qemu with the same output as the x86_64 build.clang -###shows clang collecting every-L— this package's own, then the host directories it still adds because no--sysrootis passed — ahead of every-lname in the actual invocation to the linker, regardless of where-l/-Ltokens sit in the ldflags text. This package's-Lis the only explicit one a graph link has today, so it is checked, and matches, first.extern double fmaximum(double, double)(musl 1.2.5 does not implement it; glibc does — the exact symbol mcpp#696's own fixture used) now fails to link withld.lld: error: undefined symbol: fmaximuminstead of silently resolving it from the host's glibc archive. Once-lmis answered by this package's empty stub, the linker does not fall through to a later-Ldirectory for the same name.Not done: macOS and Windows (
cfg(windows)) were not measured, so their ldflags are unchanged, per Scope above. A genuinely disk-heavy full ecosystem sweep (e.g. building the whole mcpp-index openkal matrix against this branch) was out of scope for a single-package PR.CI
.github/workflows/ci.yml,programsjob, Linux rows only (if: runner.os == 'Linux'):examples/link-stubswithmcpp build --verbose(the flag that makes mcpp show the underlying linker's own output even for a build that succeeds), and asserts that every log line naminglibm.a,libpthread.a,libdl.aorlibrt.anames this package's ownport/liband nothing else.examples/link-stubsthroughtools/run-probe.sh, checking the same four calls actually behave correctly at run time.Both were verified locally against the
linux, gccandlinux, llvmmatrix rows before pushing.Test plan
port/lib/*.aare each exactly 8 bytes,21 3c 61 72 63 68 3e 0a(!<arch>\n), verified withar t/nmand confirmed byte-identical toar rc <name>with no members.python3 -c 'import tomllib; ...').git diff origin/main --diff-filter=D --name-onlyis empty;git diff origin/main --statlists only the files in this PR;git log --oneline origin/main..HEADshows only this PR's one commit.