]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
feat: Add support for multiple directories in base_dir/CCACHE_BASEDIR
authorJoel Rosdahl <joel@rosdahl.net>
Sat, 16 Aug 2025 19:09:19 +0000 (21:09 +0200)
committerJoel Rosdahl <joel@rosdahl.net>
Sun, 17 Aug 2025 07:37:17 +0000 (09:37 +0200)
Closes #597.

doc/MANUAL.adoc
src/ccache/ccache.cpp
src/ccache/config.cpp
src/ccache/config.hpp
src/ccache/core/common.cpp
src/ccache/depfile.cpp
src/ccache/util/path.cpp
src/ccache/util/path.hpp
test/suites/basedir.bash
unittest/test_config.cpp
unittest/test_depfile.cpp

index 4ab1ddd3348e2c9aa1f0150449f443377ff4ec20..d304d71966169f13be09c4ca25fb4dbb6bfe66e7 100644 (file)
@@ -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
-    _<<Compiling in different directories>>_. 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 _<<Compiling in different directories>>_. 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
index 6177823e52735baaf76e89ef837b085bee726cf6..e8433eb17421685d1fb80d05403ed20694c864d6 100644 (file)
@@ -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);
index ef16a8dc4aeec61a1269353bd0dec28e50037619..a4f67d8c35436d827c57b3217c4457eb8904116e 100644 (file)
@@ -407,10 +407,12 @@ format_umask(std::optional<mode_t> umask)
 }
 
 void
-verify_absolute_path(const fs::path& value)
+verify_absolute_paths(const std::vector<fs::path>& 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:
index 2bb7997544164975e1f05e32f964b56431037c3e..4910a7c72d3dbe33ce4f1a641d66fdb03ee5f18c 100644 (file)
@@ -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<std::filesystem::path>& 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<std::filesystem::path>& 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<std::filesystem::path> 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<std::filesystem::path>&
+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<std::filesystem::path>& value)
+{
+  m_base_dirs.clear();
+  for (const auto& path : value) {
+    m_base_dirs.push_back(util::lexically_normal(path));
+  }
 }
 
 inline void
index aab32089f951c16485cc2408843241e5941532f1..f35eeea89455a7eef30e8b44d00643ff4e1795b4 100644 (file)
@@ -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;
index 011bbb94b3ada6585981ec3db773db5ff9c04e9b..a2c014e02e8751a4dcbb17f05a0a5b5b74ad4eef 100644 (file)
@@ -64,7 +64,7 @@ escape_filename(std::string_view filename)
 std::optional<std::string>
 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<void, std::string>
 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
   }
index 434ed20f9fcc5f4ee11d514c0ab2610c309c800d..4f58eb79233a948e46914fda184c76fb8d79d821 100644 (file)
@@ -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<std::filesystem::path>& prefixes)
+{
+  return std::any_of(
+    std::begin(prefixes), std::end(prefixes), [&](const fs::path& prefix) {
+      return path_starts_with(path, prefix);
+    });
+}
+
 } // namespace util
index 8e566ac484b77895e584114a4e9f79284e790ff0..22b2795ce676957e0383203d1ffa9f640b92e1eb 100644 (file)
 #pragma once
 
 #include <ccache/util/pathstring.hpp>
+#ifdef _WIN32
+#  include <ccache/util/string.hpp>
+#endif
 
 #include <filesystem>
 #include <string>
 #include <string_view>
-#ifdef _WIN32
-#  include <ccache/util/string.hpp>
-#endif
+#include <vector>
 
 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<std::filesystem::path>& 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;
index 43e26dafc7fb9ad6fd0349097ec93dbb13d55b3b..b8f964d753b5fb4a80885feb05ed6e411432f57c 100644 (file)
@@ -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"
index e6b5a00b76f562913691ec9e2370168c9fc8a362..00a00cde2ab1e346390f5a26e67368f0fadc58fa 100644 (file)
@@ -22,6 +22,7 @@
 #include <ccache/core/exceptions.hpp>
 #include <ccache/util/environment.hpp>
 #include <ccache/util/file.hpp>
+#include <ccache/util/filesystem.hpp>
 #include <ccache/util/format.hpp>
 
 #include <doctest/doctest.h>
@@ -30,6 +31,8 @@
 #include <string>
 #include <vector>
 
+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<fs::path>{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"));
index 0be8c53d30bfa7149294ae4958fd98f8aaab37cd..3b1a43480df0b4902251564721773a5b0cb4ffb8 100644 (file)
@@ -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"