Skip to content

runtime.library_dirs also lands on the link line, where it can shadow another package's library #304

Description

@Sunrisepeak

Summary

A package's runtime.library_dirs is also emitted as -L on the link line. The name
says runtime, and the field exists to make dlopen work, but a directory listed there
participates in symbol resolution at build time too — where it can shadow another
package's library and break the link.

Not asking for a behaviour change; the current semantics may well be intended. Asking for
it to be documented, because the failure is remote from its cause and the workaround is
non-obvious.

Where it came up

compat.vulkan-runtime (mcpp-index) is a symlink farm of host Vulkan ICD libraries, in
the shape compat.glx-runtime already uses for OpenGL: an mcpp binary runs under mcpp's
own glibc, so a bare-soname dlopen never searches the host's library path, and the
loader finds every ICD manifest and then fails to open a single driver.

The farm has to carry the ICDs' transitive dependencies, and some of those — libxcb, zlib
— are also packaged in the index. Harvesting the host's libxcb.so (the unversioned
development symlink) put it on -L ahead of compat.xcb, and the link failed:

ld: …/vulkan_runtime/lib/libxcb.so: undefined reference to `XauDisposeAuth'
ld: …/vulkan_runtime/lib/libxcb.so: undefined reference to `XauGetBestAuthByAddr'
ld: …/vulkan_runtime/lib/libxcb.so: undefined reference to `XdmcpWrap'

Nothing in that message points at runtime.library_dirs.

Reproduction

  1. A package declaring runtime = { library_dirs = { "<dir>" } }, where <dir> contains
    an unversioned libfoo.so for some foo the index also provides.
  2. A consumer that links -lfoo through the index package.
  3. mcpp build → the host copy wins and the link fails.

Confirmed on 0.0.109 by inspecting the generated build.ninja: the directory appears as
a plain -L… entry.

Workaround, which is arguably the right spelling anyway

Harvest only versioned sonames (lib*.so.*), never the bare .so. Versioned names
are invisible to the linker — it resolves -lfoo through libfoo.so/libfoo.a — and are
exactly what dlopen asks for. compat.glx-runtime never hit this because the GL family
it harvests is not otherwise linked from the index, so the collision never arose.

Suggested resolution

Either of these would have saved the debugging:

  • Document that runtime.library_dirs contributes to the link search path, with the
    "versioned sonames only" guidance for symlink-farm packages; or
  • Keep it out of the link line, if runtime-only was the intent.

Happy to send a docs patch if the first is the preferred direction.

Activity

  1. speak-agent commented on Aug 31, 2026

    @speak-agent
    Member

    Fixed in 2026.8.10.1 (PR #400) — closing late, recording the evidence.

    The issue asked only that the behaviour be documented. It turned out not to be a trade-off at all, so the behaviour changed instead.

    The sweep that triaged this issue found a third option neither the report nor the response had considered: -Wl,-rpath-link satisfies link-time resolution of a transitive DT_NEEDED and does not participate in -lfoo resolution (measured identically on binutils 2.42 and ld.lld 22.1.8). "Keep the link-time capability" and "do not shadow another package's library" are therefore not in tension — the old behaviour was one flag spelled wrong.

    Link intent now keeps the discovery stages separate, one field per stage:

    Field ELF Mach-O PE/Windows
    link_library_dirs -L -L -L or /LIBPATH:
    transitive_needed_dirs -Wl,-rpath-link no flag no flag
    runtime_search_dirs RUNPATH/rpath only, never -L rpath only no flag
    frameworks no flag -framework no flag
    deploy_files copy edge copy edge copy beside the output

    Legacy runtime.library_dirs maps to run-time search only. src/build/plan.cppm:703:

    // Legacy library_dirs means run-time discovery only.  It deliberately
    // does not enter linkLibraryDirs; callers that need -L must opt into
    // the structured field.
    append_paths(runtime.libraryDirs, out.linkIntent.runtimeSearchDirs);

    So the compat.vulkan-runtime shape works as intended: a symlink farm of host ICDs and their transitive dependencies is reachable at run time, its harvested libxcb.so no longer precedes compat.xcb on the link line, and a package that genuinely needs -L says so with link_library_dirs.

    Documented in docs/05-mcpp-toml.md and docs/zh/05-mcpp-toml.md (the table above), which also covers what the issue originally asked for: the failure was remote from its cause, and the field names now state the stage each one belongs to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions