From 67f2d1070c02af835675ee007b59574659f79006 Mon Sep 17 00:00:00 2001 From: Joel Rosdahl Date: Sat, 16 Aug 2025 21:09:19 +0200 Subject: [PATCH] feat: Add support for multiple directories in base_dir/CCACHE_BASEDIR Closes #597. --- doc/MANUAL.adoc | 13 +++++++------ src/ccache/ccache.cpp | 6 +++--- src/ccache/config.cpp | 17 ++++++++--------- src/ccache/config.hpp | 22 ++++++++++++++++------ src/ccache/core/common.cpp | 6 +++--- src/ccache/depfile.cpp | 4 ++-- src/ccache/util/path.cpp | 10 ++++++++++ src/ccache/util/path.hpp | 12 +++++++++--- test/suites/basedir.bash | 17 +++++++++++++++++ unittest/test_config.cpp | 14 ++++++++------ unittest/test_depfile.cpp | 6 +++--- 11 files changed, 86 insertions(+), 41 deletions(-) diff --git a/doc/MANUAL.adoc b/doc/MANUAL.adoc index 4ab1ddd3..d304d719 100644 --- a/doc/MANUAL.adoc +++ b/doc/MANUAL.adoc @@ -448,13 +448,14 @@ option key. [#config_base_dir] *base_dir* (*CCACHE_BASEDIR*):: - This option should be an absolute path to a directory. If set, ccache will + This option is a list of absolute directory paths. The list separator is + semicolon on Windows systems and colon on other systems. If set, ccache will rewrite absolute paths into paths relative to the current working directory, - but only absolute paths that begin with *base_dir*. Cache results can then - be shared for compilations in different directories even if the project uses - absolute paths in the compiler command line. See also the discussion under - _<>_. If set to the empty string (which - is the default), no rewriting is done. + but only absolute paths that begin with one of the *base_dir* paths. Cache + results can then be shared for compilations in different directories even if + the project uses absolute paths in the compiler command line. See also the + discussion under _<>_. If set to the + empty string (which is the default), no rewriting is done. + A typical path to use as *base_dir* is your home directory or another directory that is a parent of your project directories. Don't use `/` as the base diff --git a/src/ccache/ccache.cpp b/src/ccache/ccache.cpp index 6177823e..e8433eb1 100644 --- a/src/ccache/ccache.cpp +++ b/src/ccache/ccache.cpp @@ -618,7 +618,7 @@ process_preprocessed_file(Context& ctx, Hash& hash, const fs::path& path) while (!inc_path.empty() && inc_path.back() == '/') { inc_path.pop_back(); } - if (!ctx.config.base_dir().empty()) { + if (!ctx.config.base_dirs().empty()) { auto it = relative_inc_path_cache.find(inc_path); if (it == relative_inc_path_cache.end()) { std::string rel_inc_path = @@ -1085,7 +1085,7 @@ rewrite_stdout_from_compiler(const Context& ctx, util::Bytes&& stdout_data) // paths because otherwise Ninja will use the abs path to original header // to check if a file needs to be recompiled. else if (ctx.config.compiler_type() == CompilerType::msvc - && !ctx.config.base_dir().empty() + && !ctx.config.base_dirs().empty() && util::starts_with(line, ctx.config.msvc_dep_prefix())) { std::string orig_line(line.data(), line.length()); std::string abs_inc_path = @@ -1101,7 +1101,7 @@ rewrite_stdout_from_compiler(const Context& ctx, util::Bytes&& stdout_data) // The MSVC /FC option causes paths in diagnostics messages to become // absolute. Those within basedir need to be changed into relative paths. else if (ctx.config.compiler_type() == CompilerType::msvc - && !ctx.config.base_dir().empty()) { + && !ctx.config.base_dirs().empty()) { size_t path_end = core::get_diagnostics_path_length(line); if (path_end != 0) { std::string_view abs_path = line.substr(0, path_end); diff --git a/src/ccache/config.cpp b/src/ccache/config.cpp index ef16a8dc..a4f67d8c 100644 --- a/src/ccache/config.cpp +++ b/src/ccache/config.cpp @@ -407,10 +407,12 @@ format_umask(std::optional umask) } void -verify_absolute_path(const fs::path& value) +verify_absolute_paths(const std::vector& paths) { - if (!value.is_absolute()) { - throw core::Error(FMT("not an absolute path: \"{}\"", value)); + for (const auto& path : paths) { + if (!path.is_absolute()) { + throw core::Error(FMT("not an absolute path: \"{}\"", path)); + } } } @@ -796,7 +798,7 @@ Config::get_string_value(const std::string& key) const return format_bool(m_absolute_paths_in_stderr); case ConfigItem::base_dir: - return util::pstr(m_base_dir); + return util::join_path_list(m_base_dirs); case ConfigItem::cache_dir: return m_cache_dir.string(); @@ -1022,11 +1024,8 @@ Config::set_item(const std::string& key, break; case ConfigItem::base_dir: - m_base_dir = value; - if (!m_base_dir.empty()) { // The empty string means "disable" - verify_absolute_path(m_base_dir); - m_base_dir = util::lexically_normal(m_base_dir); - } + set_base_dirs(util::split_path_list(value)); + verify_absolute_paths(m_base_dirs); break; case ConfigItem::cache_dir: diff --git a/src/ccache/config.hpp b/src/ccache/config.hpp index 2bb79975..4910a7c7 100644 --- a/src/ccache/config.hpp +++ b/src/ccache/config.hpp @@ -58,7 +58,7 @@ public: bool absolute_paths_in_stderr() const; util::Args::ResponseFileFormat response_file_format() const; - const std::filesystem::path& base_dir() const; + const std::vector& base_dirs() const; const std::filesystem::path& cache_dir() const; const std::string& compiler() const; const std::string& compiler_check() const; @@ -111,6 +111,7 @@ public: std::filesystem::path default_temporary_dir() const; void set_base_dir(const std::filesystem::path& value); + void set_base_dirs(const std::vector& value); void set_cache_dir(const std::filesystem::path& value); void set_compiler(const std::string& value); void set_compiler_type(CompilerType value); @@ -173,7 +174,7 @@ private: bool m_absolute_paths_in_stderr = false; util::Args::ResponseFileFormat m_response_file_format = util::Args::ResponseFileFormat::auto_guess; - std::filesystem::path m_base_dir; + std::vector m_base_dirs; std::filesystem::path m_cache_dir; std::string m_compiler; std::string m_compiler_check = "mtime"; @@ -251,10 +252,10 @@ Config::response_file_format() const : util::Args::ResponseFileFormat::posix; } -inline const std::filesystem::path& -Config::base_dir() const +inline const std::vector& +Config::base_dirs() const { - return m_base_dir; + return m_base_dirs; } inline const std::filesystem::path& @@ -534,7 +535,16 @@ Config::size_unit_prefix_type() const inline void Config::set_base_dir(const std::filesystem::path& value) { - m_base_dir = util::lexically_normal(value); + set_base_dirs({value}); +} + +inline void +Config::set_base_dirs(const std::vector& value) +{ + m_base_dirs.clear(); + for (const auto& path : value) { + m_base_dirs.push_back(util::lexically_normal(path)); + } } inline void diff --git a/src/ccache/core/common.cpp b/src/ccache/core/common.cpp index aab32089..f35eeea8 100644 --- a/src/ccache/core/common.cpp +++ b/src/ccache/core/common.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Joel Rosdahl and other contributors +// Copyright (C) 2023-2025 Joel Rosdahl and other contributors // // See doc/AUTHORS.adoc for a complete list of contributors. // @@ -82,8 +82,8 @@ ensure_dir_exists(const fs::path& dir) fs::path make_relative_path(const Context& ctx, const fs::path& path) { - if (!ctx.config.base_dir().empty() && path.is_absolute() - && util::path_starts_with(path, ctx.config.base_dir())) { + if (!ctx.config.base_dirs().empty() && path.is_absolute() + && util::path_starts_with(path, ctx.config.base_dirs())) { return util::make_relative_path(ctx.actual_cwd, ctx.apparent_cwd, path); } else { return path; diff --git a/src/ccache/depfile.cpp b/src/ccache/depfile.cpp index 011bbb94..a2c014e0 100644 --- a/src/ccache/depfile.cpp +++ b/src/ccache/depfile.cpp @@ -64,7 +64,7 @@ escape_filename(std::string_view filename) std::optional rewrite_source_paths(const Context& ctx, std::string_view content) { - ASSERT(!ctx.config.base_dir().empty()); + ASSERT(!ctx.config.base_dirs().empty()); bool rewritten = false; bool first = true; @@ -96,7 +96,7 @@ rewrite_source_paths(const Context& ctx, std::string_view content) tl::expected make_paths_relative_in_output_dep(const Context& ctx) { - if (ctx.config.base_dir().empty()) { + if (ctx.config.base_dirs().empty()) { LOG_RAW("Base dir not set, skip using relative paths"); return {}; // nothing to do } diff --git a/src/ccache/util/path.cpp b/src/ccache/util/path.cpp index 434ed20f..4f58eb79 100644 --- a/src/ccache/util/path.cpp +++ b/src/ccache/util/path.cpp @@ -150,4 +150,14 @@ path_starts_with(const fs::path& path, const fs::path& prefix) == p2_end; } +bool +path_starts_with(const std::filesystem::path& path, + const std::vector& prefixes) +{ + return std::any_of( + std::begin(prefixes), std::end(prefixes), [&](const fs::path& prefix) { + return path_starts_with(path, prefix); + }); +} + } // namespace util diff --git a/src/ccache/util/path.hpp b/src/ccache/util/path.hpp index 8e566ac4..22b2795c 100644 --- a/src/ccache/util/path.hpp +++ b/src/ccache/util/path.hpp @@ -19,13 +19,14 @@ #pragma once #include +#ifdef _WIN32 +# include +#endif #include #include #include -#ifdef _WIN32 -# include -#endif +#include namespace util { @@ -80,6 +81,11 @@ make_path(const T&... args) bool path_starts_with(const std::filesystem::path& path, const std::filesystem::path& prefix); +// Return whether `path` starts with any of `prefixes` considering path +// specifics on Windows. +bool path_starts_with(const std::filesystem::path& path, + const std::vector& prefixes); + // Access the underlying path string without having to copy it if // std::filesystem::path::value_type is char (that is, not wchar_t). using pstr = PathString; diff --git a/test/suites/basedir.bash b/test/suites/basedir.bash index 43e26daf..b8f964d7 100644 --- a/test/suites/basedir.bash +++ b/test/suites/basedir.bash @@ -55,6 +55,23 @@ SUITE_basedir() { expect_stat preprocessed_cache_hit 0 expect_stat cache_miss 2 + # ------------------------------------------------------------------------- + TEST "Several entries in CCACHE_BASEDIR" + + basedir="$(pwd)/dir1:$(pwd)/dir2" + + cd dir1 + CCACHE_BASEDIR="${basedir}" $CCACHE_COMPILE -I"$(pwd)"/include -c src/test.c + expect_stat direct_cache_hit 0 + expect_stat preprocessed_cache_hit 0 + expect_stat cache_miss 1 + + cd ../dir2 + CCACHE_BASEDIR="${basedir}" $CCACHE_COMPILE -I"$(pwd)"/include -c src/test.c + expect_stat direct_cache_hit 1 + expect_stat preprocessed_cache_hit 0 + expect_stat cache_miss 1 + # ------------------------------------------------------------------------- if ! $HOST_OS_WINDOWS && ! $HOST_OS_CYGWIN; then TEST "Path normalization" diff --git a/unittest/test_config.cpp b/unittest/test_config.cpp index e6b5a00b..00a00cde 100644 --- a/unittest/test_config.cpp +++ b/unittest/test_config.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -30,6 +31,8 @@ #include #include +namespace fs = util::filesystem; + using doctest::Approx; using TestUtil::TestContext; @@ -39,7 +42,7 @@ TEST_CASE("Config: default values") { Config config; - CHECK(config.base_dir().empty()); + CHECK(config.base_dirs().empty()); CHECK(config.cache_dir().empty()); // Set later CHECK(config.compiler().empty()); CHECK(config.compiler_check() == "mtime"); @@ -150,7 +153,7 @@ TEST_CASE("Config::update_from_file") Config config; REQUIRE(config.update_from_file("ccache.conf")); - CHECK(config.base_dir() == base_dir); + CHECK(config.base_dirs() == std::vector{base_dir}); CHECK(config.cache_dir() == FMT("{0}$/{0}/.ccache", user)); CHECK(config.compiler() == "foo"); CHECK(config.compiler_check() == "none"); @@ -283,10 +286,9 @@ TEST_CASE("Config::update_from_file, error handling") SUBCASE("relative base dir") { - REQUIRE(util::write_file("ccache.conf", "base_dir = relative/path")); - REQUIRE_THROWS_WITH( - config.update_from_file("ccache.conf"), - "ccache.conf:1: not an absolute path: \"relative/path\""); + REQUIRE(util::write_file("ccache.conf", "base_dir = relative")); + REQUIRE_THROWS_WITH(config.update_from_file("ccache.conf"), + "ccache.conf:1: not an absolute path: \"relative\""); REQUIRE(util::write_file("ccache.conf", "base_dir =")); CHECK(config.update_from_file("ccache.conf")); diff --git a/unittest/test_depfile.cpp b/unittest/test_depfile.cpp index 0be8c53d..3b1a4348 100644 --- a/unittest/test_depfile.cpp +++ b/unittest/test_depfile.cpp @@ -64,21 +64,21 @@ TEST_CASE("depfile::rewrite_source_paths") SUBCASE("Base directory not in dep file content") { - ctx.config.set_base_dir("/foo/bar"); + ctx.config.set_base_dirs({"/foo/bar"}); CHECK(!depfile::rewrite_source_paths(ctx, "")); CHECK(!depfile::rewrite_source_paths(ctx, content)); } SUBCASE("Base directory in dep file content but not matching") { - ctx.config.set_base_dir((cwd.parent_path() / "other").string()); + ctx.config.set_base_dirs({(cwd.parent_path() / "other").string()}); CHECK(!depfile::rewrite_source_paths(ctx, "")); CHECK(!depfile::rewrite_source_paths(ctx, content)); } SUBCASE("Absolute paths under base directory rewritten") { - ctx.config.set_base_dir(cwd.string()); + ctx.config.set_base_dirs({cwd.string()}); const auto actual = depfile::rewrite_source_paths(ctx, content); const auto expected = FMT( "{0}/foo.o: \\\n" -- 2.47.3