1
1
Fork 0
mirror of https://github.com/NixOS/nix.git synced 2025-11-09 12:06:01 +01:00
This commit is contained in:
Mykyta Onipchenko 2025-11-07 18:00:49 +00:00 committed by GitHub
commit 3e87d22c38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 155 additions and 65 deletions

View file

@ -4,15 +4,15 @@
# Synopsis # 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 # Description
The command `nix-collect-garbage` is mostly an alias of [`nix-store --gc`](@docroot@/command-ref/nix-store/gc.md). 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. That is, it deletes all unreachable [store objects] in the Nix store to clean up your system.
However, it provides two additional options, However, it provides more additional options,
[`--delete-old`](#opt-delete-old) and [`--delete-older-than`](#opt-delete-older-than), [`--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. 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 These options are the equivalent of running
[`nix-env --delete-generations`](@docroot@/command-ref/nix-env/delete-generations.md) [`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 <period>`](@docroot@/command-ref/nix-env/delete-generations.md#generations-time) on each found profile. This is the equivalent of invoking [`nix-env --delete-generations <period>`](@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. See the documentation of that command for additional information about the *period* argument.
- <span id="opt-max-freed">[`--max-freed`](#opt-max-freed)</span> *bytes* - <span id="opt-keep-min">[`--keep-min`](#opt-keep-min)</span> *generations*
Minimum amount of generations to keep after deletion.
- <span id="opt-keep-max">[`--keep-max`](#opt-keep-max)</span> *generations*
Maximum amount of generations to keep after deletion.
- <span id="opt-max-freed">[`--max-freed`](#opt-max-freed)</span> *bytes*
<!-- duplication from https://github.com/NixOS/nix/blob/442a2623e48357ff72c77bb11cf2cf06d94d2f90/doc/manual/source/command-ref/nix-store/gc.md?plain=1#L39-L44 --> <!-- duplication from https://github.com/NixOS/nix/blob/442a2623e48357ff72c77bb11cf2cf06d94d2f90/doc/manual/source/command-ref/nix-store/gc.md?plain=1#L39-L44 -->
@ -75,7 +83,9 @@ These options are for deleting old [profiles] prior to deleting unreachable [sto
{{#include ./env-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 To delete from the Nix store everything that is not used by the current
generations of each profile, do generations of each profile, do
@ -84,5 +94,16 @@ generations of each profile, do
$ nix-collect-garbage -d $ 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 [profiles]: @docroot@/command-ref/files/profiles.md
[store objects]: @docroot@/store/store-object.md [store objects]: @docroot@/store/store-object.md

View file

@ -51,7 +51,11 @@ N getIntArg(const std::string & opt, Strings::iterator & i, const Strings::itera
++i; ++i;
if (i == end) if (i == end)
throw UsageError("'%1%' requires an argument", opt); throw UsageError("'%1%' requires an argument", opt);
return string2IntWithUnitPrefix<N>(*i); if (allowUnit)
return string2IntWithUnitPrefix<N>(*i);
else if (auto n = string2Int<N>(*i))
return *n;
throw UsageError("'%s' is not an integer", *i);
} }
struct LegacyArgs : public MixCommonArgs, public RootArgs struct LegacyArgs : public MixCommonArgs, public RootArgs

View file

@ -130,6 +130,45 @@ void deleteGeneration(const Path & profile, GenerationNumber gen);
*/ */
void deleteGenerations(const Path & profile, const std::set<GenerationNumber> & gensToDelete, bool dryRun); void deleteGenerations(const Path & profile, const std::set<GenerationNumber> & 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<time_t> olderThan,
std::optional<GenerationNumber> keepMin,
std::optional<GenerationNumber> keepMax,
bool dryRun);
/** /**
* Delete generations older than `max` passed the current generation. * Delete generations older than `max` passed the current generation.
* *

View file

@ -10,6 +10,7 @@
#include <unistd.h> #include <unistd.h>
#include <errno.h> #include <errno.h>
#include <stdio.h> #include <stdio.h>
#include <algorithm>
namespace nix { namespace nix {
@ -145,7 +146,16 @@ void deleteGenerations(const Path & profile, const std::set<GenerationNumber> &
} }
/** /**
* 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) 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<time_t> olderThan,
std::optional<GenerationNumber> keepMin,
std::optional<GenerationNumber> keepMax,
bool dryRun)
{ {
if (max == 0) if (keepMin.has_value() && keepMax.has_value() && *keepMin > *keepMax)
throw Error("Must keep at least one generation, otherwise the current one would be deleted"); throw Error("--keep-min cannot be greater than --keep-max");
PathLocks lock; PathLocks lock;
lockProfile(lock, profile); lockProfile(lock, profile);
auto [gens, _curGen] = findGenerations(profile); auto [gens, curGen] = findGenerations(profile);
auto curGen = _curGen;
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 // Compute minimum bound for kept generations
iterDropUntil(gens, i, [&](auto & g) { return g.number == curGen; }); auto start = current;
if (keepMin.has_value())
iterDrop(gens, start, *keepMin);
// Skip over `max` generations, preserving them // Compute maximum bound for kept generations
for (GenerationNumber keep = 0; i != gens.rend() && keep < max; ++i, ++keep) auto end = gens.rend();
; if (keepMax.has_value()) {
end = current;
iterDrop(gens, end, *keepMax);
}
// Delete the rest // Find the first older generation, if one exists
for (; i != gens.rend(); ++i) auto older = gens.rend();
deleteGeneration2(profile, i->number, dryRun); 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) void deleteOldGenerations(const Path & profile, bool dryRun)
{ {
PathLocks lock; deleteGenerationsFilter(profile, std::nullopt, std::nullopt, std::optional(0), dryRun);
lockProfile(lock, profile);
auto [gens, curGen] = findGenerations(profile);
for (auto & i : gens)
if (i.number != curGen)
deleteGeneration2(profile, i.number, dryRun);
} }
void deleteGenerationsOlderThan(const Path & profile, time_t t, bool dryRun) void deleteGenerationsOlderThan(const Path & profile, time_t t, bool dryRun)
{ {
PathLocks lock; deleteGenerationsFilter(profile, std::optional(t), std::nullopt, std::nullopt, dryRun);
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);
}
} }
time_t parseOlderThanTimeSpec(std::string_view timeSpec) time_t parseOlderThanTimeSpec(std::string_view timeSpec)

View file

@ -11,6 +11,7 @@
#include <iostream> #include <iostream>
#include <cerrno> #include <cerrno>
#include <optional>
namespace nix::fs { namespace nix::fs {
using namespace std::filesystem; using namespace std::filesystem;
@ -18,8 +19,10 @@ using namespace std::filesystem;
using namespace nix; using namespace nix;
std::string deleteOlderThan;
bool dryRun = false; bool dryRun = false;
std::optional<time_t> deleteOlderThan;
std::optional<GenerationNumber> keepMin = std::nullopt;
std::optional<GenerationNumber> keepMax = std::nullopt;
/* If `-d' was specified, remove all old generations of all profiles. /* If `-d' was specified, remove all old generations of all profiles.
* Of course, this makes rollbacks to before this point in time * 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) { if (link.find("link") != std::string::npos) {
printInfo("removing old generations of profile %s", path); printInfo("removing old generations of profile %s", path);
if (deleteOlderThan != "") {
auto t = parseOlderThanTimeSpec(deleteOlderThan); if (deleteOlderThan.has_value() || keepMax.has_value())
deleteGenerationsOlderThan(path, t, dryRun); deleteGenerationsFilter(path, deleteOlderThan, keepMin, keepMax, dryRun);
} else else
deleteOldGenerations(path, dryRun); deleteOldGenerations(path, dryRun);
} }
} else if (type == std::filesystem::file_type::directory) { } else if (type == std::filesystem::file_type::directory) {
@ -77,7 +80,14 @@ static int main_nix_collect_garbage(int argc, char ** argv)
removeOld = true; removeOld = true;
else if (*arg == "--delete-older-than") { else if (*arg == "--delete-older-than") {
removeOld = true; removeOld = true;
deleteOlderThan = getArg(*arg, arg, end); deleteOlderThan = std::optional<time_t>{parseOlderThanTimeSpec(getArg(*arg, arg, end))};
} else if (*arg == "--keep-min")
keepMin = std::optional<GenerationNumber>{
std::max(getIntArg<GenerationNumber>(*arg, arg, end, false), (GenerationNumber) 1)};
else if (*arg == "--keep-max") {
removeOld = true;
keepMax = std::optional<GenerationNumber>{
std::max(getIntArg<GenerationNumber>(*arg, arg, end, false), (GenerationNumber) 1)};
} else if (*arg == "--dry-run") } else if (*arg == "--dry-run")
dryRun = true; dryRun = true;
else if (*arg == "--max-freed") else if (*arg == "--max-freed")