]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
feat: Add namespace support
authorJoel Rosdahl <joel@rosdahl.net>
Mon, 8 Nov 2021 18:38:35 +0000 (19:38 +0100)
committerJoel Rosdahl <joel@rosdahl.net>
Mon, 8 Nov 2021 20:32:27 +0000 (21:32 +0100)
If a namespace configured, the namespace string will be added to the
hashed data for each compilation. This will make the associated cache
entries logically separate from cache entries with other namespaces, but
they will still share the same storage space. Cache entries can also be
selectively removed from the primary cache with the command line option
--evict-namespace, potentially in combination with --evict-older-than.

Closes #870.

15 files changed:
doc/MANUAL.adoc
src/Config.cpp
src/Config.hpp
src/Manifest.cpp
src/Result.cpp
src/ccache.cpp
src/core/mainoptions.cpp
src/storage/primary/CacheFile.cpp
src/storage/primary/CacheFile.hpp
src/storage/primary/PrimaryStorage.cpp
src/storage/primary/PrimaryStorage.hpp
src/storage/primary/PrimaryStorage_cleanup.cpp
test/CMakeLists.txt
test/suites/namespace.bash [new file with mode: 0644]
unittest/test_Config.cpp

index 033dd0b71025899bd3cf8260b293592003a61c9f..1577fb4e70633d8006b7d638d93028159b51e80f 100644 (file)
@@ -99,10 +99,16 @@ compiler's documentation.
     has the same effect as setting the environment variable `CCACHE_DIR`
     temporarily.
 
+*--evict-namespace* _NAMESPACE_::
+
+    Remove files created with a certain <<config_namespace,*namespace*>> from
+    the cache.
+
 *--evict-older-than* _AGE_::
 
     Remove files older than _AGE_ from the cache. _AGE_ should be an unsigned
-    integer with a `d` (days) or `s` (seconds) suffix.
+    integer with a `d` (days) or `s` (seconds) suffix. If combined with
+    `--evict-namespace`, only remove old files within that namespace.
 
 *-h*, *--help*::
 
@@ -761,6 +767,21 @@ file in `/etc/rsyslog.d`:
     Gi, Ti (binary). The default suffix is G. See also
     _<<Cache size management>>_.
 
+[#config_namespace]
+*namespace* (*CCACHE_NAMESPACE*)::
+
+    If set, the namespace string will be added to the hashed data for each
+    compilation. This will make the associated cache entries logically separate
+    from cache entries with other namespaces, but they will still share the same
+    storage space. Cache entries can also be selectively removed from the
+    primary cache with the command line option `--evict-namespace`, potentially
+    in combination with `--evict-older-than`.
+.
+For instance, if you use the same primary cache for several disparate projects,
+you can use a unique namespace string for each one. This allows you to remove
+cache entries that belong to a certain project if stop working with that
+project.
+
 [#config_path]
 *path* (*CCACHE_PATH*)::
 
index ec739c8afa3022431a3bae77e833a325184bdab2..9b619b211e6f10eab2e04511996209b15aaafe59 100644 (file)
@@ -82,6 +82,7 @@ enum class ConfigItem {
   log_file,
   max_files,
   max_size,
+  namespace_,
   path,
   pch_external_checksum,
   prefix_command,
@@ -126,6 +127,7 @@ const std::unordered_map<std::string, ConfigItem> k_config_key_table = {
   {"log_file", ConfigItem::log_file},
   {"max_files", ConfigItem::max_files},
   {"max_size", ConfigItem::max_size},
+  {"namespace", ConfigItem::namespace_},
   {"path", ConfigItem::path},
   {"pch_external_checksum", ConfigItem::pch_external_checksum},
   {"prefix_command", ConfigItem::prefix_command},
@@ -172,6 +174,7 @@ const std::unordered_map<std::string, std::string> k_env_variable_table = {
   {"LOGFILE", "log_file"},
   {"MAXFILES", "max_files"},
   {"MAXSIZE", "max_size"},
+  {"NAMESPACE", "namespace"},
   {"PATH", "path"},
   {"PCH_EXTSUM", "pch_external_checksum"},
   {"PREFIX", "prefix_command"},
@@ -687,6 +690,9 @@ Config::get_string_value(const std::string& key) const
   case ConfigItem::max_size:
     return format_cache_size(m_max_size);
 
+  case ConfigItem::namespace_:
+    return m_namespace;
+
   case ConfigItem::path:
     return m_path;
 
@@ -927,6 +933,10 @@ Config::set_item(const std::string& key,
     m_max_size = Util::parse_size(value);
     break;
 
+  case ConfigItem::namespace_:
+    m_namespace = Util::expand_environment_variables(value);
+    break;
+
   case ConfigItem::path:
     m_path = Util::expand_environment_variables(value);
     break;
index 9383106bd24d8e5685f0946082f248f6fe9b49d9..4e386870d1675186f60ed239fe20cdee6a75c465 100644 (file)
@@ -81,6 +81,7 @@ public:
   core::Sloppiness sloppiness() const;
   bool stats() const;
   const std::string& stats_log() const;
+  const std::string& namespace_() const;
   const std::string& temporary_dir() const;
   nonstd::optional<mode_t> umask() const;
 
@@ -178,6 +179,7 @@ private:
   core::Sloppiness m_sloppiness;
   bool m_stats = true;
   std::string m_stats_log;
+  std::string m_namespace;
   std::string m_temporary_dir;
   nonstd::optional<mode_t> m_umask;
 
@@ -428,6 +430,12 @@ Config::stats_log() const
   return m_stats_log;
 }
 
+inline const std::string&
+Config::namespace_() const
+{
+  return m_namespace;
+}
+
 inline const std::string&
 Config::temporary_dir() const
 {
index 09d7683a0b7721759abc813e95dac838d96d22be..341e66dbf465293518eee0896b965a50f637382f 100644 (file)
@@ -341,7 +341,7 @@ write_manifest(const Config& config,
                                 compression::level_from_config(config),
                                 time(nullptr),
                                 CCACHE_VERSION,
-                                "");
+                                config.namespace_());
   header.set_entry_size_from_payload_size(payload_size);
 
   core::CacheEntryWriter writer(file_writer, header);
index e79334f347f624b380a99041392cd4599fa0773e..b36cd231bada96ae09be8644ed787f4ddfc9f043 100644 (file)
@@ -342,7 +342,7 @@ Writer::do_finalize()
                                 compression::level_from_config(m_ctx.config),
                                 time(nullptr),
                                 CCACHE_VERSION,
-                                "");
+                                m_ctx.config.namespace_());
   header.set_entry_size_from_payload_size(payload_size);
 
   core::FileWriter file_writer(atomic_result_file.stream());
index 406ae738e06132dcb036a8d17edc04a171a29b7e..85cc59a88483ac588fef34e943a1f5ca31a5daae 100644 (file)
@@ -1189,6 +1189,11 @@ hash_common_info(const Context& ctx,
 {
   hash.hash(HASH_PREFIX);
 
+  if (!ctx.config.namespace_().empty()) {
+    hash.hash_delimiter("namespace");
+    hash.hash(ctx.config.namespace_());
+  }
+
   // We have to hash the extension, as a .i file isn't treated the same by the
   // compiler as a .ii file.
   hash.hash_delimiter("ext");
index 7bb3c0bc50c78abcbcb35207f7dceaadc2343eec..1876ce93f47998f35467926d2ac85c4ff6d13f60 100644 (file)
@@ -90,6 +90,8 @@ Common options:
                                default
     -d, --dir PATH             operate on cache directory PATH instead of the
                                default
+        --evict-namespace NAMESPACE
+                               remove files created with namespace NAMESPACE
         --evict-older-than AGE remove files older than AGE (unsigned integer
                                with a d (days) or s (seconds) suffix)
     -F, --max-files NUM        set maximum number of files in cache to NUM (use
@@ -261,6 +263,7 @@ enum {
   CONFIG_PATH,
   DUMP_MANIFEST,
   DUMP_RESULT,
+  EVICT_NAMESPACE,
   EVICT_OLDER_THAN,
   EXTRACT_RESULT,
   HASH_FILE,
@@ -281,6 +284,7 @@ const option long_options[] = {
   {"directory", required_argument, nullptr, 'd'}, // backward compatibility
   {"dump-manifest", required_argument, nullptr, DUMP_MANIFEST},
   {"dump-result", required_argument, nullptr, DUMP_RESULT},
+  {"evict-namespace", required_argument, nullptr, EVICT_NAMESPACE},
   {"evict-older-than", required_argument, nullptr, EVICT_OLDER_THAN},
   {"extract-result", required_argument, nullptr, EXTRACT_RESULT},
   {"get-config", required_argument, nullptr, 'k'},
@@ -310,6 +314,8 @@ process_main_options(int argc, const char* const* argv)
   nonstd::optional<uint64_t> trim_max_size;
   bool trim_lru_mtime = false;
   uint8_t verbosity = 0;
+  nonstd::optional<std::string> evict_namespace;
+  nonstd::optional<uint64_t> evict_max_age;
 
   // First pass: Handle non-command options that affect command options.
   while ((c = getopt_long(argc,
@@ -392,14 +398,13 @@ process_main_options(int argc, const char* const* argv)
       return error ? EXIT_FAILURE : EXIT_SUCCESS;
     }
 
+    case EVICT_NAMESPACE: {
+      evict_namespace = arg;
+      break;
+    }
+
     case EVICT_OLDER_THAN: {
-      auto seconds = Util::parse_duration(arg);
-      ProgressBar progress_bar("Evicting...");
-      storage::primary::PrimaryStorage(config).clean_old(
-        [&](double progress) { progress_bar.update(progress); }, seconds);
-      if (isatty(STDOUT_FILENO)) {
-        PRINT_RAW(stdout, "\n");
-      }
+      evict_max_age = Util::parse_duration(arg);
       break;
     }
 
@@ -590,6 +595,20 @@ process_main_options(int argc, const char* const* argv)
     }
   }
 
+  if (evict_max_age || evict_namespace) {
+    Config config;
+    config.read();
+
+    ProgressBar progress_bar("Evicting...");
+    storage::primary::PrimaryStorage(config).evict(
+      [&](double progress) { progress_bar.update(progress); },
+      evict_max_age,
+      evict_namespace);
+    if (isatty(STDOUT_FILENO)) {
+      PRINT_RAW(stdout, "\n");
+    }
+  }
+
   return EXIT_SUCCESS;
 }
 
index 8ada17ff75319f6482c83efc22483aef0567d9b3..0068fcdd73e407066e894fcb61be538bd8d22313 100644 (file)
@@ -39,6 +39,8 @@ CacheFile::type() const
     return Type::manifest;
   } else if (util::ends_with(m_path, Result::k_file_suffix)) {
     return Type::result;
+  } else if (util::ends_with(m_path, "W")) {
+    return Type::raw;
   } else {
     return Type::unknown;
   }
index 14064fec4fdb52d3a1bc6c7cc758969edb8bd548..809ea38a4a27e0ea1a3aa32fedff4b3c1229089d 100644 (file)
@@ -27,7 +27,7 @@
 class CacheFile
 {
 public:
-  enum class Type { result, manifest, unknown };
+  enum class Type { result, manifest, raw, unknown };
 
   explicit CacheFile(const std::string& path);
 
index bbe4d53f1e9c6dfd49dceb7ac06c047630ef4f20..a4280b345370be5f0c54cc08bced612be051b546 100644 (file)
@@ -173,8 +173,12 @@ PrimaryStorage::finalize()
     const double factor = m_config.limit_multiple() / 16;
     const uint64_t max_size = round(m_config.max_size() * factor);
     const uint32_t max_files = round(m_config.max_files() * factor);
-    const time_t max_age = 0;
-    clean_dir(subdir, max_size, max_files, max_age, [](double /*progress*/) {});
+    clean_dir(subdir,
+              max_size,
+              max_files,
+              nonstd::nullopt,
+              nonstd::nullopt,
+              [](double /*progress*/) {});
   }
 }
 
index 99f47a59890663375695fd511c31ff73d1af44f5..bb8b505459e829e5cfa6963a55cc24d131ed917f 100644 (file)
@@ -77,7 +77,9 @@ public:
 
   // --- Cleanup ---
 
-  void clean_old(const ProgressReceiver& progress_receiver, uint64_t max_age);
+  void evict(const ProgressReceiver& progress_receiver,
+             nonstd::optional<uint64_t> max_age,
+             nonstd::optional<std::string> namespace_);
 
   void clean_all(const ProgressReceiver& progress_receiver);
 
@@ -137,7 +139,8 @@ private:
   static void clean_dir(const std::string& subdir,
                         uint64_t max_size,
                         uint64_t max_files,
-                        uint64_t max_age,
+                        nonstd::optional<uint64_t> max_age,
+                        nonstd::optional<std::string> namespace_,
                         const ProgressReceiver& progress_receiver);
 };
 
index 6f3790f9cfa929473dfb68d729431d7f62eba7cb..63b956ac4dfeb7112fb301e9422b065829b1c84c 100644 (file)
 
 #include <Config.hpp>
 #include <Context.hpp>
+#include <File.hpp>
 #include <Logging.hpp>
 #include <Util.hpp>
+#include <core/CacheEntryReader.hpp>
+#include <core/FileReader.hpp>
+#include <fmtmacros.hpp>
 #include <storage/primary/CacheFile.hpp>
 #include <storage/primary/StatsFile.hpp>
 #include <storage/primary/util.hpp>
@@ -75,14 +79,15 @@ update_counters(const std::string& dir,
 }
 
 void
-PrimaryStorage::clean_old(const ProgressReceiver& progress_receiver,
-                          const uint64_t max_age)
+PrimaryStorage::evict(const ProgressReceiver& progress_receiver,
+                      nonstd::optional<uint64_t> max_age,
+                      nonstd::optional<std::string> namespace_)
 {
   for_each_level_1_subdir(
     m_config.cache_dir(),
     [&](const std::string& subdir,
         const ProgressReceiver& sub_progress_receiver) {
-      clean_dir(subdir, 0, 0, max_age, sub_progress_receiver);
+      clean_dir(subdir, 0, 0, max_age, namespace_, sub_progress_receiver);
     },
     progress_receiver);
 }
@@ -92,7 +97,8 @@ void
 PrimaryStorage::clean_dir(const std::string& subdir,
                           const uint64_t max_size,
                           const uint64_t max_files,
-                          const uint64_t max_age,
+                          const nonstd::optional<uint64_t> max_age,
+                          const nonstd::optional<std::string> namespace_,
                           const ProgressReceiver& progress_receiver)
 {
   LOG("Cleaning up cache directory {}", subdir);
@@ -103,6 +109,9 @@ PrimaryStorage::clean_dir(const std::string& subdir,
   uint64_t cache_size = 0;
   uint64_t files_in_cache = 0;
   time_t current_time = time(nullptr);
+  std::unordered_map<std::string /*result_file*/,
+                     std::vector<std::string> /*associated_raw_files*/>
+    raw_files_map;
 
   for (size_t i = 0; i < files.size();
        ++i, progress_receiver(1.0 / 3 + 1.0 * i / files.size() / 3)) {
@@ -120,6 +129,12 @@ PrimaryStorage::clean_dir(const std::string& subdir,
       continue;
     }
 
+    if (namespace_ && file.type() == CacheFile::Type::raw) {
+      const auto result_filename =
+        FMT("{}R", file.path().substr(0, file.path().length() - 2));
+      raw_files_map[result_filename].push_back(file.path());
+    }
+
     cache_size += file.lstat().size_on_disk();
     files_in_cache += 1;
   }
@@ -148,12 +163,41 @@ PrimaryStorage::clean_dir(const std::string& subdir,
 
     if ((max_size == 0 || cache_size <= max_size)
         && (max_files == 0 || files_in_cache <= max_files)
-        && (max_age == 0
+        && (!max_age
             || file.lstat().mtime()
-                 > (current_time - static_cast<int64_t>(max_age)))) {
+                 > (current_time - static_cast<int64_t>(*max_age)))
+        && (!namespace_ || max_age)) {
       break;
     }
 
+    if (namespace_) {
+      try {
+        File file_stream(file.path(), "rb");
+        core::FileReader file_reader(*file_stream);
+        core::CacheEntryReader reader(file_reader);
+        if (reader.header().namespace_ != *namespace_) {
+          continue;
+        }
+      } catch (core::Error&) {
+        // Failed to read header: ignore.
+        continue;
+      }
+
+      // For namespace eviction we need to remove raw files based on result
+      // filename since they don't have a header.
+      if (file.type() == CacheFile::Type::result) {
+        const auto entry = raw_files_map.find(file.path());
+        if (entry != raw_files_map.end()) {
+          for (const auto& raw_file : entry->second) {
+            delete_file(raw_file,
+                        Stat::lstat(raw_file).size_on_disk(),
+                        &cache_size,
+                        &files_in_cache);
+          }
+        }
+      }
+    }
+
     if (util::ends_with(file.path(), ".stderr")) {
       // In order to be nice to legacy ccache versions, make sure that the .o
       // file is deleted before .stderr, because if the ccache process gets
@@ -199,7 +243,8 @@ PrimaryStorage::clean_all(const ProgressReceiver& progress_receiver)
       clean_dir(subdir,
                 m_config.max_size() / 16,
                 m_config.max_files() / 16,
-                0,
+                nonstd::nullopt,
+                nonstd::nullopt,
                 sub_progress_receiver);
     },
     progress_receiver);
index 0015e4117a8206fa2d935322404aa5bacb2e1441..42d4c22feed1606c6ddddb39a306963529b2e349 100644 (file)
@@ -39,6 +39,7 @@ addtest(ivfsoverlay)
 addtest(masquerading)
 addtest(modules)
 addtest(multi_arch)
+addtest(namespace)
 addtest(no_compression)
 addtest(nocpp2)
 addtest(nvcc)
diff --git a/test/suites/namespace.bash b/test/suites/namespace.bash
new file mode 100644 (file)
index 0000000..accf400
--- /dev/null
@@ -0,0 +1,58 @@
+SUITE_namespace_SETUP() {
+    unset CCACHE_NODIRECT
+    echo 'int x;' >test1.c
+    echo 'int y;' >test2.c
+}
+
+SUITE_namespace() {
+    # -------------------------------------------------------------------------
+    TEST "Namespace makes entries isolated"
+
+    $CCACHE_COMPILE -c test1.c
+    expect_stat direct_cache_hit 0
+    expect_stat cache_miss 1
+
+    $CCACHE_COMPILE -c test1.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 1
+
+    CCACHE_NAMESPACE=a $CCACHE_COMPILE -c test1.c
+    expect_stat direct_cache_hit 1
+    expect_stat cache_miss 2
+
+    CCACHE_NAMESPACE=a $CCACHE_COMPILE -c test1.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 2
+
+    CCACHE_NAMESPACE=b $CCACHE_COMPILE -c test1.c
+    expect_stat direct_cache_hit 2
+    expect_stat cache_miss 3
+
+    CCACHE_NAMESPACE=b $CCACHE_COMPILE -c test1.c
+    expect_stat direct_cache_hit 3
+    expect_stat cache_miss 3
+
+    # -------------------------------------------------------------------------
+    TEST "--evict-namespace + --evict-older-than"
+
+    CCACHE_NAMESPACE="a" $CCACHE_COMPILE -c test1.c
+    result_file="$(find $CCACHE_DIR -name '*R')"
+    backdate "$result_file"
+    for ns in a b c; do
+        CCACHE_NAMESPACE="$ns" $CCACHE_COMPILE -c test2.c
+    done
+    expect_stat cache_miss 4
+    expect_stat files_in_cache 8
+
+    $CCACHE --evict-namespace d >/dev/null
+    expect_stat files_in_cache 8
+
+    $CCACHE --evict-namespace c >/dev/null
+    expect_stat files_in_cache 6
+
+    $CCACHE --evict-namespace a --evict-older-than 1d >/dev/null
+    expect_stat files_in_cache 5
+
+    $CCACHE --evict-namespace a >/dev/null
+    expect_stat files_in_cache 2
+}
index 3b62174c4012e11edd917a58072dcdea934fd763..17a055d571727cae74b95fe834f1947d93d8dbd1 100644 (file)
@@ -402,6 +402,7 @@ TEST_CASE("Config::visit_items")
     "log_file = lf\n"
     "max_files = 4711\n"
     "max_size = 98.7M\n"
+    "namespace = ns\n"
     "path = p\n"
     "pch_external_checksum = true\n"
     "prefix_command = pc\n"
@@ -461,6 +462,7 @@ TEST_CASE("Config::visit_items")
     "(test.conf) log_file = lf",
     "(test.conf) max_files = 4711",
     "(test.conf) max_size = 98.7M",
+    "(test.conf) namespace = ns",
     "(test.conf) path = p",
     "(test.conf) pch_external_checksum = true",
     "(test.conf) prefix_command = pc",