Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions doc/manual/source/command-ref/nix-collect-garbage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -62,20 +62,30 @@ 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.
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 -->

Keep deleting paths until at least *bytes* bytes have been deleted,
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
Expand All @@ -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
6 changes: 5 additions & 1 deletion src/libmain/include/nix/main/shared.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
Expand Down
39 changes: 39 additions & 0 deletions src/libstore/include/nix/store/profiles.hh
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,45 @@ void deleteGeneration(const Path & profile, GenerationNumber gen);
*/
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.
*
Expand Down
120 changes: 68 additions & 52 deletions src/libstore/profiles.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <algorithm>

namespace nix {

Expand Down Expand Up @@ -145,82 +146,97 @@ 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)
{
for (; i != gens.rend() && !cond(*i); ++i)
;
}

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)
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);

// Compute minimum bound for kept generations
auto start = current;
if (keepMin.has_value())
iterDrop(gens, start, *keepMin);

// Compute maximum bound for kept generations
auto end = gens.rend();
if (keepMax.has_value()) {
end = current;
iterDrop(gens, end, *keepMax);
}

// Find the current generation
iterDropUntil(gens, i, [&](auto & g) { return g.number == curGen; });
// 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);
}

// Skip over `max` generations, preserving them
for (GenerationNumber keep = 0; i != gens.rend() && keep < max; ++i, ++keep)
;
// 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;

// Delete the rest
for (; i != gens.rend(); ++i)
deleteGeneration2(profile, i->number, dryRun);
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 deleteOldGenerations(const Path & profile, bool dryRun)
void deleteGenerationsGreaterThan(const Path & profile, GenerationNumber max, bool dryRun)
{
PathLocks lock;
lockProfile(lock, profile);

auto [gens, curGen] = findGenerations(profile);
deleteGenerationsFilter(profile, std::nullopt, std::nullopt, std::optional(max), dryRun);
}

for (auto & i : gens)
if (i.number != curGen)
deleteGeneration2(profile, i.number, dryRun);
void deleteOldGenerations(const Path & profile, bool 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)
Expand Down
22 changes: 16 additions & 6 deletions src/nix/nix-collect-garbage/nix-collect-garbage.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@

#include <iostream>
#include <cerrno>
#include <optional>

namespace nix::fs {
using namespace std::filesystem;
}

using namespace nix;

std::string deleteOlderThan;
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.
* Of course, this makes rollbacks to before this point in time
Expand Down Expand Up @@ -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) {
Expand All @@ -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<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")
dryRun = true;
else if (*arg == "--max-freed")
Expand Down