diff --git a/doc/manual/source/command-ref/nix-collect-garbage.md b/doc/manual/source/command-ref/nix-collect-garbage.md index 763179b8e..c95fe3002 100644 --- a/doc/manual/source/command-ref/nix-collect-garbage.md +++ b/doc/manual/source/command-ref/nix-collect-garbage.md @@ -4,15 +4,15 @@ # Synopsis -`nix-collect-garbage` [`--delete-old`] [`-d`] [`--delete-older-than` *period*] [`--max-freed` *bytes*] [`--dry-run`] +`nix-collect-garbage` [`--delete-old`] [`-d`] [`--delete-older-than` *period*] [`--keep-min` *generations*] [`--keep-max` *generations*] [`--max-freed` *bytes*] [`--dry-run`] # Description The command `nix-collect-garbage` is mostly an alias of [`nix-store --gc`](@docroot@/command-ref/nix-store/gc.md). That is, it deletes all unreachable [store objects] in the Nix store to clean up your system. -However, it provides two additional options, -[`--delete-old`](#opt-delete-old) and [`--delete-older-than`](#opt-delete-older-than), +However, it provides more additional options, +[`--delete-old`](#opt-delete-old), [`--delete-older-than`](#opt-delete-older-than), [`--keep-min`](#opt-keep-min), and [`--keep-max`](#opt-keep-max), which also delete old [profiles], allowing potentially more [store objects] to be deleted because profiles are also garbage collection roots. These options are the equivalent of running [`nix-env --delete-generations`](@docroot@/command-ref/nix-env/delete-generations.md) @@ -62,7 +62,15 @@ These options are for deleting old [profiles] prior to deleting unreachable [sto This is the equivalent of invoking [`nix-env --delete-generations `](@docroot@/command-ref/nix-env/delete-generations.md#generations-time) on each found profile. See the documentation of that command for additional information about the *period* argument. - - [`--max-freed`](#opt-max-freed) *bytes* +- [`--keep-min`](#opt-keep-min) *generations* + + Minimum amount of generations to keep after deletion. + +- [`--keep-max`](#opt-keep-max) *generations* + + Maximum amount of generations to keep after deletion. + +- [`--max-freed`](#opt-max-freed) *bytes* @@ -70,12 +78,14 @@ These options are for deleting old [profiles] prior to deleting unreachable [sto then stop. The argument *bytes* can be followed by the multiplicative suffix `K`, `M`, `G` or `T`, denoting KiB, MiB, GiB or TiB units. - + {{#include ./opt-common.md}} {{#include ./env-common.md}} -# Example +# Examples + +## Delete all older To delete from the Nix store everything that is not used by the current generations of each profile, do @@ -84,5 +94,16 @@ generations of each profile, do $ nix-collect-garbage -d ``` +## Keep most-recent by time (number of days) and trim by amount + +This command will delete generations older than a week if possible, while keeping an amount of generations between `10` and `20`. + +```console +$ nix-collect-garbage --delete-older-than 7d --keep-min 10 --keep-max 20 +``` + +If there were more than 20 generations built in the past week, it will only keep 20 most recent ones. +If there were less than 10 generations built in the past week, it will keep even older generations, until there is 10. + [profiles]: @docroot@/command-ref/files/profiles.md [store objects]: @docroot@/store/store-object.md diff --git a/src/libmain/include/nix/main/shared.hh b/src/libmain/include/nix/main/shared.hh index 43069ba82..12c54c89b 100644 --- a/src/libmain/include/nix/main/shared.hh +++ b/src/libmain/include/nix/main/shared.hh @@ -51,7 +51,11 @@ N getIntArg(const std::string & opt, Strings::iterator & i, const Strings::itera ++i; if (i == end) throw UsageError("'%1%' requires an argument", opt); - return string2IntWithUnitPrefix(*i); + if (allowUnit) + return string2IntWithUnitPrefix(*i); + else if (auto n = string2Int(*i)) + return *n; + throw UsageError("'%s' is not an integer", *i); } struct LegacyArgs : public MixCommonArgs, public RootArgs diff --git a/src/libstore/include/nix/store/profiles.hh b/src/libstore/include/nix/store/profiles.hh index 75cd11340..c5c5d9de2 100644 --- a/src/libstore/include/nix/store/profiles.hh +++ b/src/libstore/include/nix/store/profiles.hh @@ -130,6 +130,45 @@ void deleteGeneration(const Path & profile, GenerationNumber gen); */ void deleteGenerations(const Path & profile, const std::set & gensToDelete, bool dryRun); +/** + * Delete old generations. Will never delete the current or future generations. + * + * Examples: + * - All parameters are nullopt + * No generations are deleted. + * - keepMin is 5 + * No generations are deleted, only keepMax and olderThan delete generations. + * - keepMax is 10 + * 10 most recent generations after the current one are kept, the rest is deleted. + * - olderThan is 2025-09-16 + * Generations older than 2025-09-16 are deleted. + * - olderThan is 2025-09-16, keepMin is 5, keepMax is 10 - + * Will try to delete generations older than 2025-09-16. + * If there are more than 10 generations to be kept, continues to delete old generations until there are 10. + * If there are less than 5 generations to be kept, preserves the most recent of generations to be deleted until there + * are 5. + * + * @param profile The profile, specified by its name and location combined into a path, whose generations we want to + * delete. + * + * @param olderThan Age of the oldest generation to keep. + * If nullopt, no generation will be deleted based on its age. + * + * @param keepMin Minimum amount of recent generations to keep after deletion (not counting the current or future ones). + * If nullopt, all old generations will be deleted. + * + * @param keepMax Maximum amount of recent generations to keep after deletion (not counting the current or future ones). + * If nullopt, all recent generations will be kept. + * + * @param dryRun Log what would be deleted instead of actually doing so. + */ +void deleteGenerationsFilter( + const Path & profile, + std::optional olderThan, + std::optional keepMin, + std::optional keepMax, + bool dryRun); + /** * Delete generations older than `max` passed the current generation. * diff --git a/src/libstore/profiles.cc b/src/libstore/profiles.cc index 3f6fcb6ff..919398e0d 100644 --- a/src/libstore/profiles.cc +++ b/src/libstore/profiles.cc @@ -10,6 +10,7 @@ #include #include #include +#include namespace nix { @@ -145,7 +146,16 @@ void deleteGenerations(const Path & profile, const std::set & } /** - * Advanced the iterator until the given predicate `cond` returns `true`. + * Advance the iterator `count` times. + */ +static inline void iterDrop(Generations & gens, auto && i, GenerationNumber count = 1) +{ + for (GenerationNumber keep = 0; i != gens.rend() && keep < count; ++i, ++keep) + ; +} + +/** + * Advance the iterator until the given predicate `cond` returns `true`. */ static inline void iterDropUntil(Generations & gens, auto && i, auto && cond) { @@ -153,74 +163,80 @@ static inline void iterDropUntil(Generations & gens, auto && i, auto && cond) ; } -void deleteGenerationsGreaterThan(const Path & profile, GenerationNumber max, bool dryRun) +void deleteGenerationsFilter( + const Path & profile, + std::optional olderThan, + std::optional keepMin, + std::optional keepMax, + bool dryRun) { - if (max == 0) - throw Error("Must keep at least one generation, otherwise the current one would be deleted"); + if (keepMin.has_value() && keepMax.has_value() && *keepMin > *keepMax) + throw Error("--keep-min cannot be greater than --keep-max"); PathLocks lock; lockProfile(lock, profile); - auto [gens, _curGen] = findGenerations(profile); - auto curGen = _curGen; + auto [gens, curGen] = findGenerations(profile); - auto i = gens.rbegin(); + // Keep current and future generations + auto current = gens.rbegin(); + iterDropUntil(gens, current, [&](auto & g) { return g.number == curGen; }); + iterDrop(gens, current); - // Find the current generation - iterDropUntil(gens, i, [&](auto & g) { return g.number == curGen; }); + // Compute minimum bound for kept generations + auto start = current; + if (keepMin.has_value()) + iterDrop(gens, start, *keepMin); - // Skip over `max` generations, preserving them - for (GenerationNumber keep = 0; i != gens.rend() && keep < max; ++i, ++keep) - ; + // Compute maximum bound for kept generations + auto end = gens.rend(); + if (keepMax.has_value()) { + end = current; + iterDrop(gens, end, *keepMax); + } - // Delete the rest - for (; i != gens.rend(); ++i) - deleteGeneration2(profile, i->number, dryRun); + // Find the first older generation, if one exists + auto older = gens.rend(); + if (olderThan.has_value()) { + older = current; + iterDropUntil(gens, older, [&](auto & g) { return g.creationTime < *olderThan; }); + /* Take the previous generation + + We don't want delete this one yet because it + existed at the requested point in time, and + we want to be able to roll back to it. */ + iterDrop(gens, older); + } + + // Find first generation to delete by clamping between keepMin and keepMax + auto toDelete = older; + + auto clampBackward = std::distance(gens.rbegin(), older) - std::distance(gens.rbegin(), end); + for (int i = clampBackward; i > 0; --i) + --toDelete; + + auto clampForward = std::distance(gens.rbegin(), start) - std::distance(gens.rbegin(), older); + for (int i = clampForward; i > 0; --i) + ++toDelete; + + // Delete + for (; toDelete != gens.rend(); ++toDelete) + deleteGeneration2(profile, toDelete->number, dryRun); +} + +void deleteGenerationsGreaterThan(const Path & profile, GenerationNumber max, bool dryRun) +{ + deleteGenerationsFilter(profile, std::nullopt, std::nullopt, std::optional(max), dryRun); } void deleteOldGenerations(const Path & profile, bool dryRun) { - PathLocks lock; - lockProfile(lock, profile); - - auto [gens, curGen] = findGenerations(profile); - - for (auto & i : gens) - if (i.number != curGen) - deleteGeneration2(profile, i.number, dryRun); + deleteGenerationsFilter(profile, std::nullopt, std::nullopt, std::optional(0), dryRun); } void deleteGenerationsOlderThan(const Path & profile, time_t t, bool dryRun) { - PathLocks lock; - lockProfile(lock, profile); - - auto [gens, curGen] = findGenerations(profile); - - auto i = gens.rbegin(); - - // Predicate that the generation is older than the given time. - auto older = [&](auto & g) { return g.creationTime < t; }; - - // Find the first older generation, if one exists - iterDropUntil(gens, i, older); - - /* Take the previous generation - - We don't want delete this one yet because it - existed at the requested point in time, and - we want to be able to roll back to it. */ - if (i != gens.rend()) - ++i; - - // Delete all previous generations (unless current). - for (; i != gens.rend(); ++i) { - /* Creating date and generations should be monotonic, so lower - numbered derivations should also be older. */ - assert(older(*i)); - if (i->number != curGen) - deleteGeneration2(profile, i->number, dryRun); - } + deleteGenerationsFilter(profile, std::optional(t), std::nullopt, std::nullopt, dryRun); } time_t parseOlderThanTimeSpec(std::string_view timeSpec) diff --git a/src/nix/nix-collect-garbage/nix-collect-garbage.cc b/src/nix/nix-collect-garbage/nix-collect-garbage.cc index 4d6e60bf3..0f8b4393a 100644 --- a/src/nix/nix-collect-garbage/nix-collect-garbage.cc +++ b/src/nix/nix-collect-garbage/nix-collect-garbage.cc @@ -11,6 +11,7 @@ #include #include +#include namespace nix::fs { using namespace std::filesystem; @@ -18,8 +19,10 @@ using namespace std::filesystem; using namespace nix; -std::string deleteOlderThan; bool dryRun = false; +std::optional deleteOlderThan; +std::optional keepMin = std::nullopt; +std::optional keepMax = std::nullopt; /* If `-d' was specified, remove all old generations of all profiles. * Of course, this makes rollbacks to before this point in time @@ -49,10 +52,10 @@ void removeOldGenerations(std::filesystem::path dir) } if (link.find("link") != std::string::npos) { printInfo("removing old generations of profile %s", path); - if (deleteOlderThan != "") { - auto t = parseOlderThanTimeSpec(deleteOlderThan); - deleteGenerationsOlderThan(path, t, dryRun); - } else + + if (deleteOlderThan.has_value() || keepMax.has_value()) + deleteGenerationsFilter(path, deleteOlderThan, keepMin, keepMax, dryRun); + else deleteOldGenerations(path, dryRun); } } else if (type == std::filesystem::file_type::directory) { @@ -77,7 +80,14 @@ static int main_nix_collect_garbage(int argc, char ** argv) removeOld = true; else if (*arg == "--delete-older-than") { removeOld = true; - deleteOlderThan = getArg(*arg, arg, end); + deleteOlderThan = std::optional{parseOlderThanTimeSpec(getArg(*arg, arg, end))}; + } else if (*arg == "--keep-min") + keepMin = std::optional{ + std::max(getIntArg(*arg, arg, end, false), (GenerationNumber) 1)}; + else if (*arg == "--keep-max") { + removeOld = true; + keepMax = std::optional{ + std::max(getIntArg(*arg, arg, end, false), (GenerationNumber) 1)}; } else if (*arg == "--dry-run") dryRun = true; else if (*arg == "--max-freed")