]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
compat: Hash contents of explicit module files (-fmodule-file=) (#1774)
authorRedNicStone <nic@struktur.de>
Fri, 7 Aug 2026 08:53:17 +0000 (09:53 +0100)
committerGitHub <noreply@github.com>
Fri, 7 Aug 2026 08:53:17 +0000 (10:53 +0200)
compat: Hash contents of explicit module files (-fmodule-file=)

The pcm content referenced by -fmodule-file=<name>=<path> is not part of the preprocessed output of a consumer TU, so ccache's key (command line + preprocessed source) does not change when the module changes.
A change to e.g. a template function body in the module therefore produces a false cache hit: the consumer keeps its old object file and silently runs stale code, even across full rebuilds.

Fix: collect the paths from -fmodule-file= arguments and hash the file contents into the common hash, mirroring the existing sanitize-ignorelist handling.
Regenerating the pcm from unchanged source yields identical bytes, so unchanged modules still get cache hits.

Adds a test suite (fmodule_file) that verifies a consumer compile is cached while the module is unchanged and that a template body change invalidates the consumer's cache entry.

src/ccache/argprocessing.cpp
src/ccache/argsinfo.hpp
src/ccache/ccache.cpp
test/CMakeLists.txt
test/suites/fmodule_file.bash [new file with mode: 0644]

index 975326ded428ae04ce52b03728c3a3d40117e64c..af9e0e8cd5debf554be438fa0bfd545aa614db90 100644 (file)
@@ -1051,6 +1051,19 @@ process_option_arg(const Context& ctx,
     return Statistic::none;
   }
 
+  if (arg.starts_with("-fmodule-file=")) {
+    // -fmodule-file=<name>=<path> or -fmodule-file=<path> for explicit C++
+    // module imports.
+    constexpr std::string_view module_file_flag = "-fmodule-file=";
+    auto value = std::string_view(arg).substr(module_file_flag.size());
+    if (auto sep = value.find('='); sep != std::string_view::npos) {
+      value = value.substr(sep + 1); // drop the optional "<name>=" prefix
+    }
+    args_info.module_files.emplace_back(value);
+    state.add_common_arg(args[i]);
+    return Statistic::none;
+  }
+
   if (arg.starts_with("--sysroot=")) {
     auto path = std::string_view(arg).substr(10);
     auto relpath = core::make_relative_path(ctx, path);
index 1b406f12e5067f3ea3639cc0d9cc734ea45da064..7db64d0cc6f22f1092cebbe75da57c7d56fef96b 100644 (file)
@@ -157,6 +157,9 @@ struct ArgsInfo
   // Files referenced by -fsanitize-ignorelist/-fsanitize-blacklist options.
   std::vector<std::filesystem::path> sanitize_ignorelists;
 
+  // Files referenced by -fmodule-file=<name>=<path> (explicit C++ modules).
+  std::vector<std::filesystem::path> module_files;
+
   // Architectures from -arch options.
   std::vector<std::string> arch_args;
 
index 5949319043fc7c4922f369c5d11258b0d8020ec1..3790826789b88c26a09f1e83923d4ad418e1fda9 100644 (file)
@@ -1870,6 +1870,16 @@ hash_common_info(const Context& ctx, const util::Args& args, Hash& hash)
     }
   }
 
+  // Hash the contents of explicitly imported module files (-fmodule-file=).
+  // The pcm content is not visible in the preprocessed output
+  for (const auto& module_file : ctx.args_info.module_files) {
+    LOG("Hashing module file {}", module_file);
+    hash.hash_delimiter("modulefile");
+    if (!hash_binary_file(ctx, hash, module_file)) {
+      return tl::unexpected(Statistic::error_hashing_extra_file);
+    }
+  }
+
   if (!(ctx.args_info.build_session_file.empty())) {
     // When using -fbuild-session-file, the actual mtime needs to be
     // added to the hash to prevent false positive cache hits if the
index 40e7c969bd33c442c13fd5a948bcc197293db3ec..9819eb0ca144cfb4a070b00ca42119049cc4adfa 100644 (file)
@@ -17,6 +17,7 @@ set_property(
   ADDITIONAL_CLEAN_FILES "${CMAKE_BINARY_DIR}/testdir")
 
 addtest(base)
+addtest(fmodule_file)
 addtest(basedir)
 addtest(cache_levels)
 addtest(clang_cu)
diff --git a/test/suites/fmodule_file.bash b/test/suites/fmodule_file.bash
new file mode 100644 (file)
index 0000000..9e21cd9
--- /dev/null
@@ -0,0 +1,76 @@
+# Verify that the contents of explicitly imported module files (-fmodule-file=)
+# are part of the hash, so that changes to a module (e.g. template function
+# bodies) invalidate cached consumer object files.
+#
+# A precompiled module file (pcm) is not part of the preprocessed
+# output of a consumer translation unit, so without explicit hashing the consumer
+# gets a false cache hit after the module changes and silently runs stale code.
+
+SUITE_fmodule_file_PROBE() {
+    if ! $COMPILER_TYPE_CLANG || $COMPILER_USES_MSVC; then
+        echo "-fmodule-file not supported by compiler"
+    else
+        echo 'export module probe_module;' >probe_module.ixx
+        $COMPILER -std=gnu++23 -x c++-module -fmodule-output=probe_module.pcm \
+            -c probe_module.ixx -o probe_module.o 2>/dev/null \
+            || echo "compiler does not support C++ modules"
+    fi
+}
+
+SUITE_fmodule_file_SETUP() {
+    unset CCACHE_NODIRECT
+    export CCACHE_DEPEND=1
+
+    cat <<'EOF' >module.ixx
+export module somemodule;
+export template<typename T>
+int module_test() {
+    return 1;
+}
+EOF
+
+    cat <<'EOF' >main.cpp
+import somemodule;
+int main() {
+    return module_test<int>();
+}
+EOF
+}
+
+SUITE_fmodule_file() {
+    # -------------------------------------------------------------------------
+    TEST "consumer compile is cached and hits while the module is unchanged"
+
+    $COMPILER -std=gnu++23 -x c++-module -fmodule-output=somemodule.pcm -c module.ixx -o module.o
+    $CCACHE_COMPILE -std=gnu++23 -fmodule-file=somemodule=somemodule.pcm -c main.cpp -o main.o
+    expect_stat cache_miss 1
+    expect_stat direct_cache_hit 0
+
+    # Same directory, same module: the consumer must be a direct cache hit.
+    $CCACHE_COMPILE -std=gnu++23 -fmodule-file=somemodule=somemodule.pcm -c main.cpp -o main.o
+    expect_stat cache_miss 1
+    expect_stat direct_cache_hit 1
+
+    # -------------------------------------------------------------------------
+    TEST "template body change invalidates the consumer cache"
+
+    $COMPILER -std=gnu++23 -x c++-module -fmodule-output=somemodule.pcm -c module.ixx -o module.o
+    $CCACHE_COMPILE -std=gnu++23 -fmodule-file=somemodule=somemodule.pcm -c main.cpp -o main.o
+    expect_stat cache_miss 1
+    expect_stat direct_cache_hit 0
+
+    # Change a template function body in the module and regenerate the pcm.
+    cat <<'EOF' >module.ixx
+export module somemodule;
+export template<typename T>
+int module_test() {
+    return 2;
+}
+EOF
+    $COMPILER -std=gnu++23 -x c++-module -fmodule-output=somemodule.pcm -c module.ixx -o module.o
+
+    # The consumer must NOT be served from the cache: its object file would contain the old instantiated template body.
+    $CCACHE_COMPILE -std=gnu++23 -fmodule-file=somemodule=somemodule.pcm -c main.cpp -o main.o
+    expect_stat cache_miss 2
+    expect_stat direct_cache_hit 0
+}