Closes #1760.
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 <<config_namespace,*namespace*>> from the
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
}
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<std::optional<int8_t>> recompress_level,
uint32_t threads)
{
+ ASSERT(dry_run == DryRun::no
+ || recompress_level == std::nullopt); // Verified by caller
+
std::vector<DirEntry> files;
uint64_t initial_size = 0;
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();
}
enum : uint8_t {
CHECKSUM_FILE,
CONFIG_PATH,
+ DRY_RUN,
EVICT_NAMESPACE,
EVICT_OLDER_THAN,
EXTRACT_RESULT,
{"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 },
std::optional<util::SizeUnitPrefixType> trim_suffix_type;
bool trim_lru_mtime = false;
std::optional<std::optional<int8_t>> trim_recompress;
+ core::DryRun dry_run = DryRun::no;
std::optional<std::string> evict_namespace;
std::optional<uint64_t> evict_max_age;
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;
switch (c) {
case CONFIG_PATH:
case 'd': // --dir
+ case DRY_RUN:
case FORMAT:
case THREADS:
case TRIM_MAX_SIZE:
}
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) {
{
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); });
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<Error>(util::parse_unsigned(arg));
config.set_value_in_file(
util::pstr(config.config_path()), "max_files", arg);
}
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<Error>(util::parse_size(arg));
uint64_t max_size = size;
}
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);
}
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,
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...");
}
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;
ProgressBar progress_bar("Evicting...");
storage::local::LocalStorage(config).evict(
+ dry_run,
[&](double progress) { progress_bar.update(progress); },
evict_max_age,
evict_namespace);
-// 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.
//
namespace core {
+enum class DryRun { yes, no };
+
enum class CacheEntryType : uint8_t { result = 0, manifest = 1 };
std::string to_string(CacheEntryType type);
}
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
static CleanDirResult
clean_dir(
+ core::DryRun dry_run,
const fs::path& l2_dir,
const uint64_t max_size,
const uint64_t max_files,
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<std::string /*result_file*/,
std::vector<fs::path> /*associated_raw_files*/>
raw_files_map;
}
// 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;
}
LOG("Before cleanup: {:.0f} KiB, {:.0f} files",
static_cast<double>(cache_size) / 1024,
static_cast<double>(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;
}
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;
}
}
void
-LocalStorage::evict(const ProgressReceiver& progress_receiver,
+LocalStorage::evict(core::DryRun dry_run,
+ const ProgressReceiver& progress_receiver,
std::optional<uint64_t> max_age,
std::optional<std::string> 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,
const uint64_t target_files = static_cast<uint64_t>(
0.9 * static_cast<double>(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 =
}
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<uint64_t> max_age,
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,
++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)) {
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());
}
// --- Cleanup ---
- void evict(const ProgressReceiver& progress_receiver,
+ void evict(core::DryRun dry_run,
+ const ProgressReceiver& progress_receiver,
std::optional<uint64_t> max_age,
std::optional<std::string> 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);
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<uint64_t> max_age,
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"