Skip to content

Dependency module unreachable transitively: nested pcm.cache/<ns>.<pkg>/ but only the flat dir goes to -fprebuilt-module-path #816

Closed as not planned

Description

@FarnaHerry

Summary

A dependency package's module cannot be reached transitively through the project's own module. The dependency's BMI lands in a nested directory pcm.cache/<ns>.<pkg>/, but only the flat pcm.cache is passed to -fprebuilt-module-path, and the explicit -fmodule-file=<name>=<nested path> mapping is attached only to TUs that import the dependency module directly. A TU that imports the project's own module (which imports the dependency's module) gets neither, so clang cannot resolve the transitive import:

error: failed to find module file for module 'X'

Build-blocking for any project whose own module interface imports a dependency package's module — which is the documented use case of module packages such as godotengine.godot-cpp-m (its README: "import godot_cpp; re-exports the whole public godot namespace").

Minimal reproduction (no Godot, 3 files)

odygrd.quill is used only because it is a small module package; the bug is not package-specific.

# mcpp.toml
[package]
name = "bmi-probe2"
version = "0.1.0"

[language]
standard = "c++23"
modules = true
import_std = false

[modules]
sources = ["src/**/*.cppm", "src/**/*.cpp"]
exports = ["mylib"]

[targets.bmi-probe2]
kind = "bin"
main = "src/main.cpp"

[dependencies.odygrd]
quill = "13.0.0"
// src/mylib.cppm — the project's own module, importing a dependency's module
module;
#include <string>
export module mylib;
import quill;
export std::string greet() { return "hi"; }
// src/helper.h — a plain header that imports the project's own module (the shape of
// any existing header pulled into a .cpp)
#pragma once
import mylib;
inline const char *who() { return "x"; }
// src/main.cpp
#include "helper.h"
int main() { return who() ? 0 : 1; }
$ mcpp build
   Compiling bmi-probe2 v0.1.0 (.)
error: build failed in bmi-probe2 v0.1.0 (.)
failed: obj/main.o
In file included from /tmp/bmi-probe2/src/main.cpp:1:
/tmp/bmi-probe2/src/helper.h:2:1: error: failed to find module file for module 'quill'

Contrast that isolates the condition: the same dependency, imported directly, builds fine —

// src/main.cpp in a second probe, no own module at all
import quill;
int main() { return 0; }
$ mcpp build
   Compiling bmi-probe v0.1.0 (.)
    Finished dev [unoptimized + debuginfo] in 14.84s      # OK

So nested BMI directories are not broken per se; what breaks is the transitive path.

Evidence

Both builds place the dependency BMI in a nested directory, and both modmaps name it correctly:

# probe1 (direct import) — works
pcm.cache/odygrd.quill/quill.pcm
modmap: -fmodule-file=quill=…/pcm.cache/odygrd.quill/quill.pcm
  unit_cxxflags = -D__MCPP_TARGET_LINUX__=1 @…/modmap/bmi-probe-c7c96848.modmap
                                                       ^ modmap attached

# probe2 (transitive) — fails
pcm.cache/odygrd.quill/quill.pcm          + pcm.cache/mylib.pcm
modmap: -fmodule-file=quill=…/pcm.cache/odygrd.quill/quill.pcm
  unit_cxxflags = -D__MCPP_TARGET_LINUX__=1            # ← obj/main.o: NO modmap
  unit_cxxflags = -D…=1 @…/modmap/bmi-probe2-….modmap  # ← only the mylib TU has it

obj/main.cpp.ddi.dd correctly says it needs pcm.cache/mylib.pcm (flat, found), but loading that BMI requires quill, whose BMI is in the nested directory that -fprebuilt-module-path does not cover. The single global flag is:

-fprebuilt-module-path=/tmp/bmi-probe2/target/x86_64-linux-gnu/<fp>/pcm.cache

Compiling the failing TU by hand confirms both halves:

invocation (same flags otherwise) result
flat -fprebuilt-module-path only (today) failed to find module file for module 'quill'
+ -fprebuilt-module-path=…/pcm.cache/odygrd.quill compiles
+ @modmap (-fmodule-file=quill=<nested path>) compiles

So either of these closes it:

  1. put dependency BMIs in the flat pcm.cache (or add pcm.cache/<ns>.<pkg> to -fprebuilt-module-path), or
  2. attach the modmap to every TU, not only the ones that directly import the dependency module.

Impact

Build-blocking, and the workaround is not discoverable from the error message. It surfaced on a GDExtension project whose .cpp files include a header that does import mcpp_kaki.utils;, where mcpp_kaki.utils does import godot_cpp; — i.e. exactly the layering the module packages advertise. It is also why this looked Godot-specific at first: it needs a two-layer module graph, which most small projects do not have.

Current workaround in the affected project (a symlink per build, added automatically when the first mcpp build fails):

ln -s <ns>.<pkg>/<mod>.pcm  pcm.cache/<mod>.pcm

Environment

  • mcpp 2026.10.17.1 (also reproduced on 2026.10.8.1)
  • toolchain llvm@22.1.8 (clang 22.1.8, libc++ 22.1.8), C++23, modules = true
  • Linux x86_64 (Fedora), glibc payload 2.44
  • Headers of the index packages at their current versions (odygrd.quill 13.0.0, godotengine.godot-cpp-m 10.0.0-rc1 + compat.godot-cpp 10.0.0-rc1)

Related but distinct: #797 (host-module vs [dependencies] disagree on a module library's export surface) — same family of "the two module paths answer the same question differently", different mechanism.

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