From: RedNicStone Date: Fri, 7 Aug 2026 08:53:17 +0000 (+0100) Subject: compat: Hash contents of explicit module files (-fmodule-file=) (#1774) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ff65435cdb80e875099ed034727bf5ada7e856f8;p=thirdparty%2Fccache.git compat: Hash contents of explicit module files (-fmodule-file=) (#1774) compat: Hash contents of explicit module files (-fmodule-file=) The pcm content referenced by -fmodule-file== 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. --- diff --git a/src/ccache/argprocessing.cpp b/src/ccache/argprocessing.cpp index 975326ded..af9e0e8cd 100644 --- a/src/ccache/argprocessing.cpp +++ b/src/ccache/argprocessing.cpp @@ -1051,6 +1051,19 @@ process_option_arg(const Context& ctx, return Statistic::none; } + if (arg.starts_with("-fmodule-file=")) { + // -fmodule-file== or -fmodule-file= 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 "=" 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); diff --git a/src/ccache/argsinfo.hpp b/src/ccache/argsinfo.hpp index 1b406f12e..7db64d0cc 100644 --- a/src/ccache/argsinfo.hpp +++ b/src/ccache/argsinfo.hpp @@ -157,6 +157,9 @@ struct ArgsInfo // Files referenced by -fsanitize-ignorelist/-fsanitize-blacklist options. std::vector sanitize_ignorelists; + // Files referenced by -fmodule-file== (explicit C++ modules). + std::vector module_files; + // Architectures from -arch options. std::vector arch_args; diff --git a/src/ccache/ccache.cpp b/src/ccache/ccache.cpp index 594931904..379082678 100644 --- a/src/ccache/ccache.cpp +++ b/src/ccache/ccache.cpp @@ -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 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 40e7c969b..9819eb0ca 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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 index 000000000..9e21cd91f --- /dev/null +++ b/test/suites/fmodule_file.bash @@ -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 +int module_test() { + return 1; +} +EOF + + cat <<'EOF' >main.cpp +import somemodule; +int main() { + return module_test(); +} +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 +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 +}