From: Joel Rosdahl Date: Wed, 5 Aug 2026 13:54:03 +0000 (+0200) Subject: feat: Add --dry-run option X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6e774054081ecafbbf8166e6b362b61ff8a566c8;p=thirdparty%2Fccache.git feat: Add --dry-run option Closes #1760. --- diff --git a/doc/manual.adoc b/doc/manual.adoc index a7faa803d..6480a4c48 100644 --- a/doc/manual.adoc +++ b/doc/manual.adoc @@ -109,6 +109,10 @@ should use the normal compiler options and refer to your compiler's documentatio has the same effect as setting the environment variable `CCACHE_DIR` temporarily. +*--dry-run*:: + + Do not perform any write operations. + *--evict-namespace* _NAMESPACE_:: Remove files created in the given <> from the diff --git a/src/ccache/core/mainoptions.cpp b/src/ccache/core/mainoptions.cpp index 736d2f8f2..1062e0995 100644 --- a/src/ccache/core/mainoptions.cpp +++ b/src/ccache/core/mainoptions.cpp @@ -113,6 +113,7 @@ Common options: default -d, --dir PATH operate on cache directory PATH instead of the default + --dry-run do not perform any write operations --evict-namespace NAMESPACE remove files created in namespace NAMESPACE --evict-older-than AGE remove files used less recently than AGE @@ -309,13 +310,17 @@ print_compression_statistics(const Config& config, } static void -trim_dir(const std::string& dir, +trim_dir(core::DryRun dry_run, + const std::string& dir, const uint64_t trim_max_size, const util::SizeUnitPrefixType suffix_type, const bool trim_lru_mtime, std::optional> recompress_level, uint32_t threads) { + ASSERT(dry_run == DryRun::no + || recompress_level == std::nullopt); // Verified by caller + std::vector files; uint64_t initial_size = 0; @@ -390,7 +395,7 @@ trim_dir(const std::string& dir, if (final_size <= trim_max_size) { break; } - if (util::remove(file.path())) { + if (dry_run == DryRun::yes || util::remove(file.path())) { ++removed_files; final_size -= file.size_on_disk(); } @@ -440,6 +445,7 @@ get_usage_text(const std::string_view ccache_name) enum : uint8_t { CHECKSUM_FILE, CONFIG_PATH, + DRY_RUN, EVICT_NAMESPACE, EVICT_OLDER_THAN, EXTRACT_RESULT, @@ -469,6 +475,7 @@ const option long_options[] = { {"config-path", REQUIRED, nullptr, CONFIG_PATH }, {"dir", REQUIRED, nullptr, 'd' }, {"directory", REQUIRED, nullptr, 'd' }, // compat + {"dry-run", NO_ARGUMENT, nullptr, DRY_RUN }, {"dump-manifest", REQUIRED, nullptr, INSPECT }, // compat {"dump-result", REQUIRED, nullptr, INSPECT }, // compat {"evict-namespace", REQUIRED, nullptr, EVICT_NAMESPACE }, @@ -517,6 +524,7 @@ process_main_options(int argc, const char* const* argv) std::optional trim_suffix_type; bool trim_lru_mtime = false; std::optional> trim_recompress; + core::DryRun dry_run = DryRun::no; std::optional evict_namespace; std::optional evict_max_age; @@ -534,6 +542,9 @@ process_main_options(int argc, const char* const* argv) case 'd': // --dir util::setenv("CCACHE_DIR", arg); break; + case DRY_RUN: + dry_run = DryRun::yes; + break; case FORMAT: if (arg == "tab") { format = StatisticsFormat::Tab; @@ -599,6 +610,7 @@ process_main_options(int argc, const char* const* argv) switch (c) { case CONFIG_PATH: case 'd': // --dir + case DRY_RUN: case FORMAT: case THREADS: case TRIM_MAX_SIZE: @@ -640,6 +652,11 @@ process_main_options(int argc, const char* const* argv) } case EXTRACT_RESULT: { + if (dry_run == DryRun::yes) { + PRINT(stderr, "--dry-run cannot be used with --extract-result\n"); + return EXIT_FAILURE; + } + umask_scope.release(); // Use original umask for files outside cache dir const auto cache_entry_data = read_from_path_or_stdin(arg); if (!cache_entry_data) { @@ -693,15 +710,16 @@ process_main_options(int argc, const char* const* argv) { ProgressBar progress_bar("Cleaning..."); storage::local::LocalStorage(config).clean_all( - [&](double progress) { progress_bar.update(progress); }); - if (isatty(STDOUT_FILENO)) { - PRINT(stdout, "\n"); - } + dry_run, [&](double progress) { progress_bar.update(progress); }); break; } case 'C': // --clear { + if (dry_run == DryRun::yes) { + PRINT(stderr, "--dry-run cannot be used with --clear\n"); + return EXIT_FAILURE; + } ProgressBar progress_bar("Clearing..."); storage::local::LocalStorage(config).wipe_all( [&](double progress) { progress_bar.update(progress); }); @@ -720,6 +738,10 @@ process_main_options(int argc, const char* const* argv) break; case 'F': { // --max-files + if (dry_run == DryRun::yes) { + PRINT(stderr, "--dry-run cannot be used with --max-files\n"); + return EXIT_FAILURE; + } auto files = util::value_or_throw(util::parse_unsigned(arg)); config.set_value_in_file( util::pstr(config.config_path()), "max_files", arg); @@ -732,6 +754,10 @@ process_main_options(int argc, const char* const* argv) } case 'M': { // --max-size + if (dry_run == DryRun::yes) { + PRINT(stderr, "--dry-run cannot be used with --max-size\n"); + return EXIT_FAILURE; + } auto [size, suffix_type] = util::value_or_throw(util::parse_size(arg)); uint64_t max_size = size; @@ -748,6 +774,10 @@ process_main_options(int argc, const char* const* argv) } case 'o': { // --set-config + if (dry_run == DryRun::yes) { + PRINT(stderr, "--dry-run cannot be used with --set-config\n"); + return EXIT_FAILURE; + } // Start searching for equal sign at position 1 to improve error message // for the -o=K=V case (key "=K" and value "V"). size_t eq_pos = arg.find('=', 1); @@ -803,16 +833,26 @@ process_main_options(int argc, const char* const* argv) } case STOP_STORAGE_HELPERS: { + if (dry_run == DryRun::yes) { + PRINT(stderr, "--dry-run cannot be used with --stop-storage-helpers\n"); + return EXIT_FAILURE; + } storage::Storage storage(config, fs::path(argv[0]).parent_path()); storage.stop_remote_storage_helpers(); break; } case TRIM_DIR: + if (dry_run == DryRun::yes && trim_recompress) { + PRINT(stderr, "--dry-run cannot be used with --trim-recompress\n"); + return EXIT_FAILURE; + } + if (!trim_max_size) { throw Error("please specify --trim-max-size when using --trim-dir"); } - trim_dir(arg, + trim_dir(dry_run, + arg, *trim_max_size, *trim_suffix_type, trim_lru_mtime, @@ -847,6 +887,10 @@ process_main_options(int argc, const char* const* argv) case 'X': // --recompress { + if (dry_run == DryRun::yes) { + PRINT(stderr, "--dry-run cannot be used with --recompress\n"); + return EXIT_FAILURE; + } auto wanted_level = parse_compression_level(arg); ProgressBar progress_bar("Recompressing..."); @@ -858,6 +902,10 @@ process_main_options(int argc, const char* const* argv) } case 'z': // --zero-stats + if (dry_run == DryRun::yes) { + PRINT(stderr, "--dry-run cannot be used with --zero-stats\n"); + return EXIT_FAILURE; + } storage::local::LocalStorage(config).zero_all_statistics(); PRINT(stdout, "Statistics zeroed\n"); break; @@ -874,6 +922,7 @@ process_main_options(int argc, const char* const* argv) ProgressBar progress_bar("Evicting..."); storage::local::LocalStorage(config).evict( + dry_run, [&](double progress) { progress_bar.update(progress); }, evict_max_age, evict_namespace); diff --git a/src/ccache/core/types.hpp b/src/ccache/core/types.hpp index 8d0297f39..86dd677b3 100644 --- a/src/ccache/core/types.hpp +++ b/src/ccache/core/types.hpp @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2022 Joel Rosdahl and other contributors +// Copyright (C) 2021-2026 Joel Rosdahl and other contributors // // See doc/authors.adoc for a complete list of contributors. // @@ -25,6 +25,8 @@ class Config; namespace core { +enum class DryRun { yes, no }; + enum class CacheEntryType : uint8_t { result = 0, manifest = 1 }; std::string to_string(CacheEntryType type); diff --git a/src/ccache/storage/local/localstorage.cpp b/src/ccache/storage/local/localstorage.cpp index 22dc7a988..14a225ed5 100644 --- a/src/ccache/storage/local/localstorage.cpp +++ b/src/ccache/storage/local/localstorage.cpp @@ -203,10 +203,17 @@ calculate_wanted_cache_level(const uint64_t files_in_level_1) } static void -delete_file(const DirEntry& dir_entry, +delete_file(core::DryRun dry_run, + const DirEntry& dir_entry, uint64_t& cache_size, uint64_t& files_in_cache) { + if (dry_run == core::DryRun::yes) { + cache_size -= dir_entry.size_on_disk(); + --files_in_cache; + return; + } + const auto result = util::remove_nfs_safe(dir_entry.path(), util::LogFailure::no); if (!result && result.error().value() != ENOENT @@ -323,6 +330,7 @@ result_path_from_raw_file(const std::string& path) static CleanDirResult clean_dir( + core::DryRun dry_run, const fs::path& l2_dir, const uint64_t max_size, const uint64_t max_files, @@ -337,7 +345,13 @@ clean_dir( uint64_t cache_size = 0; uint64_t files_in_cache = 0; + uint64_t stale_tmp_size = 0; + uint64_t stale_tmp_files = 0; auto current_time = util::now(); + auto is_stale_tmp_file = [&](const DirEntry& file) { + return file.mtime() + 1h < current_time + && util::TemporaryFile::is_tmp_file(file.path()); + }; std::unordered_map /*associated_raw_files*/> raw_files_map; @@ -352,9 +366,12 @@ clean_dir( } // Delete any tmp files older than 1 hour right away. - if (file.mtime() + 1h < current_time - && util::TemporaryFile::is_tmp_file(file.path())) { - std::ignore = util::remove(file.path()); + if (is_stale_tmp_file(file)) { + stale_tmp_size += file.size_on_disk(); + ++stale_tmp_files; + if (dry_run == core::DryRun::no) { + std::ignore = util::remove(file.path()); + } continue; } @@ -377,14 +394,16 @@ clean_dir( LOG("Before cleanup: {:.0f} KiB, {:.0f} files", static_cast(cache_size) / 1024, static_cast(files_in_cache)); - Level2Counters counters_before{files_in_cache, cache_size}; + Level2Counters counters_before{files_in_cache + stale_tmp_files, + cache_size + stale_tmp_size}; bool cleaned = false; for (size_t i = 0; i < files.size(); ++i, progress_receiver(2.0 / 3 + 1.0 * ratio(i, files.size()) / 3)) { const auto& file = files[i]; - if (!file || file.is_directory()) { + if (!file || file.is_directory() + || (file.is_regular_file() && is_stale_tmp_file(file))) { continue; } @@ -412,12 +431,12 @@ clean_dir( const auto entry = raw_files_map.find(util::pstr(file.path())); if (entry != raw_files_map.end()) { for (const auto& raw_file : entry->second) { - delete_file(DirEntry(raw_file), cache_size, files_in_cache); + delete_file(dry_run, DirEntry(raw_file), cache_size, files_in_cache); } } } - delete_file(file, cache_size, files_in_cache); + delete_file(dry_run, file, cache_size, files_in_cache); cleaned = true; } @@ -769,17 +788,20 @@ LocalStorage::get_all_statistics() const } void -LocalStorage::evict(const ProgressReceiver& progress_receiver, +LocalStorage::evict(core::DryRun dry_run, + const ProgressReceiver& progress_receiver, std::optional max_age, std::optional namespace_) { - do_clean_all(progress_receiver, 0, 0, max_age, namespace_); + do_clean_all(dry_run, progress_receiver, 0, 0, max_age, namespace_); } void -LocalStorage::clean_all(const ProgressReceiver& progress_receiver) +LocalStorage::clean_all(core::DryRun dry_run, + const ProgressReceiver& progress_receiver) { - do_clean_all(progress_receiver, + do_clean_all(dry_run, + progress_receiver, m_config.max_size(), m_config.max_files(), std::nullopt, @@ -1269,8 +1291,11 @@ LocalStorage::perform_automatic_cleanup() const uint64_t target_files = static_cast( 0.9 * static_cast(evaluation->total_files) / 256); - auto clean_dir_result = clean_dir( - get_subdir(evaluation->l1_index, largest_level_2_index), 0, target_files); + auto clean_dir_result = + clean_dir(core::DryRun::no, + get_subdir(evaluation->l1_index, largest_level_2_index), + 0, + target_files); stats_file.update([&](auto& cs) { const auto old_files = @@ -1295,7 +1320,8 @@ LocalStorage::perform_automatic_cleanup() } void -LocalStorage::do_clean_all(const ProgressReceiver& progress_receiver, +LocalStorage::do_clean_all(core::DryRun dry_run, + const ProgressReceiver& progress_receiver, uint64_t max_size, uint64_t max_files, std::optional max_age, @@ -1327,7 +1353,8 @@ LocalStorage::do_clean_all(const ProgressReceiver& progress_receiver, current_size > max_size ? max_size / 256 : 0; uint64_t level_2_max_files = current_files > max_files ? max_files / 256 : 0; - auto clean_dir_result = clean_dir(get_subdir(l1_index, l2_index), + auto clean_dir_result = clean_dir(dry_run, + get_subdir(l1_index, l2_index), level_2_max_size, level_2_max_files, max_age, @@ -1352,18 +1379,22 @@ LocalStorage::do_clean_all(const ProgressReceiver& progress_receiver, ++level_1_counters.cleanups; } - // Fix erroneous files/size counters for raw files in L2 stats files. - // See also comments in finalize(). - get_stats_file(l1_index, l2_index) - .update( - [](auto& cs) { - cs.set(Statistic::cache_size_kibibyte, 0); - cs.set(Statistic::files_in_cache, 0); - }, - StatsFile::OnlyIfChanged::yes); + if (dry_run == core::DryRun::no) { + // Fix erroneous files/size counters for raw files in L2 stats + // files. See also comments in finalize(). + get_stats_file(l1_index, l2_index) + .update( + [](auto& cs) { + cs.set(Statistic::cache_size_kibibyte, 0); + cs.set(Statistic::files_in_cache, 0); + }, + StatsFile::OnlyIfChanged::yes); + } }); - set_counters(get_stats_file(l1_index), level_1_counters); + if (dry_run == core::DryRun::no) { + set_counters(get_stats_file(l1_index), level_1_counters); + } }); if (isatty(STDOUT_FILENO)) { @@ -1381,10 +1412,12 @@ LocalStorage::do_clean_all(const ProgressReceiver& progress_receiver, using C = util::TextTable::Cell; util::TextTable table; - table.add_row({"Removed data:", + const char* description = + dry_run == core::DryRun::yes ? "Would remove" : "Removed"; + table.add_row({FMT("{} data:", description), C(removed_size_quantity).right_align(), *removed_size_unit}); - table.add_row({"Removed files:", C(total_removed_files)}); + table.add_row({FMT("{} files:", description), C(total_removed_files)}); PRINT(stdout, "{}", table.render()); } diff --git a/src/ccache/storage/local/localstorage.hpp b/src/ccache/storage/local/localstorage.hpp index fa137a7ad..6b3d8fd11 100644 --- a/src/ccache/storage/local/localstorage.hpp +++ b/src/ccache/storage/local/localstorage.hpp @@ -111,11 +111,13 @@ public: // --- Cleanup --- - void evict(const ProgressReceiver& progress_receiver, + void evict(core::DryRun dry_run, + const ProgressReceiver& progress_receiver, std::optional max_age, std::optional namespace_); - void clean_all(const ProgressReceiver& progress_receiver); + void clean_all(core::DryRun dry_run, + const ProgressReceiver& progress_receiver); void wipe_all(const ProgressReceiver& progress_receiver); @@ -173,7 +175,8 @@ private: void perform_automatic_cleanup(); - void do_clean_all(const ProgressReceiver& progress_receiver, + void do_clean_all(core::DryRun dry_run, + const ProgressReceiver& progress_receiver, uint64_t max_size, uint64_t max_files, std::optional max_age, diff --git a/test/suites/cleanup.bash b/test/suites/cleanup.bash index 9f2dd20a6..4ec05686c 100644 --- a/test/suites/cleanup.bash +++ b/test/suites/cleanup.bash @@ -98,6 +98,27 @@ SUITE_cleanup() { expect_missing $CCACHE_DIR/a/a/abcd.tmp.efgh expect_stat files_in_cache 2560 + # ------------------------------------------------------------------------- + TEST "Dry-run eviction of tmp file" + + $CCACHE -C >/dev/null + mkdir -p $CCACHE_DIR/a/a + printf x >$CCACHE_DIR/a/a/abcd.tmp.efgh + $CCACHE -c >/dev/null # update counters + expect_stat files_in_cache 1 + + backdate $CCACHE_DIR/a/a/abcd.tmp.efgh + $CCACHE --dry-run --evict-older-than 1h >dry-run.txt + expect_exists $CCACHE_DIR/a/a/abcd.tmp.efgh + expect_stat files_in_cache 1 + + $CCACHE --evict-older-than 1h >real-run.txt + expect_missing $CCACHE_DIR/a/a/abcd.tmp.efgh + expect_stat files_in_cache 0 + expect_stat cache_size_kibibyte 0 + expect_content_pattern dry-run.txt $'Would remove data: *\nWould remove files: *1' + expect_content_pattern real-run.txt $'Removed data: *\nRemoved files: *1' + # ------------------------------------------------------------------------- TEST "No cleanup of .nfs* files"