Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way to recursively GC paths #8417

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
8 changes: 8 additions & 0 deletions doc/manual/rl-next/closure-gc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
synopsis: "`nix store gc` can now collect garbage whithin a closure"
issues: 7239
prs: 8417
---

`nix store gc` can now be called with an installable argument, in which case it
will only collect the dead paths that are part of the closure of its argument.
1 change: 0 additions & 1 deletion src/libcmd/command.hh
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ struct RawInstallablesCommand : virtual Args, SourceExprCommand

void run(ref<Store> store) override;

// FIXME make const after `CmdRepl`'s override is fixed up
virtual void applyDefaultInstallables(std::vector<std::string> & rawInstallables);

bool readFromStdIn = false;
Expand Down
7 changes: 4 additions & 3 deletions src/libstore/gc-store.hh
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ struct GCOptions
*
* - `gcDeleteDead`: actually delete the latter set.
*
* - `gcDeleteSpecific`: delete the paths listed in
* `pathsToDelete`, insofar as they are not reachable.
* - `gcDeleteSpecific`: delete all the paths, and fail if one of them
* isn't dead.
*/
typedef enum {
gcReturnLive,
Expand All @@ -44,7 +44,8 @@ struct GCOptions
bool ignoreLiveness{false};

/**
* For `gcDeleteSpecific`, the paths to delete.
* The paths from which to delete.
* If empty, and `action` is not `gcDeleteSpecific`, act on the whole store.
*/
StorePathSet pathsToDelete;

Expand Down
18 changes: 14 additions & 4 deletions src/libstore/gc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,14 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
bool gcKeepOutputs = settings.gcKeepOutputs;
bool gcKeepDerivations = settings.gcKeepDerivations;

if (options.action == GCOptions::gcDeleteSpecific && options.pathsToDelete.empty()) {
// This violates the convention that an empty `pathsToDelete` corresponds
// to the whole store, but deleting the whole store doesn't make sense,
// and `nix-store --delete` is a valid command that deletes nothing, so
// we need to keep it as-it-is.
return;
}

StorePathSet roots, dead, alive;

struct Shared
Expand Down Expand Up @@ -732,7 +740,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
return markAlive();
}

if (options.action == GCOptions::gcDeleteSpecific
if (!options.pathsToDelete.empty()
&& !options.pathsToDelete.count(*path))
return;

Expand Down Expand Up @@ -790,11 +798,13 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)

/* Either delete all garbage paths, or just the specified
paths (for gcDeleteSpecific). */
if (options.action == GCOptions::gcDeleteSpecific) {
if (!options.pathsToDelete.empty()) {

for (auto & i : options.pathsToDelete) {
deleteReferrersClosure(i);
if (!dead.count(i))
if (shouldDelete) {
deleteReferrersClosure(i);
}
if (options.action == GCOptions::gcDeleteSpecific && !dead.count(i))
throw Error(
"Cannot delete path '%1%' since it is still alive. "
"To find out why, use: "
Expand Down
7 changes: 7 additions & 0 deletions src/libstore/remote-store.cc
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,13 @@ void RemoteStore::collectGarbage(const GCOptions & options, GCResults & results)
{
auto conn(getConnection());

if (
options.action != GCOptions::gcDeleteSpecific &&
! options.pathsToDelete.empty() &&
GET_PROTOCOL_MINOR(conn->daemonVersion) < 38) {
warn("Your daemon version is too old to support garbage collecting a closure, falling back to a full gc");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should be a no-op, doing a GC when I tried to delete just one path can be surprising, and lead to unexpected data loss, especially given that this command is likely to be called in some weird circumstances.

}

conn->to
<< WorkerProto::Op::CollectGarbage << options.action;
WorkerProto::write(*this, *conn, options.pathsToDelete);
Expand Down
2 changes: 1 addition & 1 deletion src/libstore/worker-protocol.hh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace nix {
#define WORKER_MAGIC_1 0x6e697863
#define WORKER_MAGIC_2 0x6478696f

#define PROTOCOL_VERSION (1 << 8 | 37)
#define PROTOCOL_VERSION (1 << 8 | 38)
#define GET_PROTOCOL_MAJOR(x) ((x) & 0xff00)
#define GET_PROTOCOL_MINOR(x) ((x) & 0x00ff)

Expand Down
24 changes: 22 additions & 2 deletions src/nix/store-gc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

using namespace nix;

struct CmdStoreGC : StoreCommand, MixDryRun
struct CmdStoreGC : InstallablesCommand, MixDryRun
{
GCOptions options;

Expand All @@ -33,11 +33,31 @@ struct CmdStoreGC : StoreCommand, MixDryRun
;
}

void run(ref<Store> store) override
// Don't add a default installable if none is specified so that
// `nix store gc` runs a full gc
void applyDefaultInstallables(std::vector<std::string> & rawInstallables) override {
}

void run(ref<Store> store, Installables && installables) override
{
auto & gcStore = require<GcStore>(*store);

options.action = dryRun ? GCOptions::gcReturnDead : GCOptions::gcDeleteDead;

// Add the closure of the installables to the set of paths to delete.
// If there's no installable specified, this will leave an empty set
// of paths to delete, which means the whole store will be gc-ed.
StorePathSet closureRoots;
for (auto & i : installables) {
try {
auto installableOutPath = Installable::toStorePath(getEvalStore(), store, Realise::Derivation, OperateOn::Output, i);
if (store->isValidPath(installableOutPath)) {
closureRoots.insert(installableOutPath);
}
} catch (MissingRealisation &) {
}
}
store->computeFSClosure(closureRoots, options.pathsToDelete);
GCResults results;
PrintFreed freed(options.action == GCOptions::gcDeleteDead, results);
gcStore.collectGarbage(options, results);
Expand Down
11 changes: 10 additions & 1 deletion src/nix/store-gc.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@ R""(
# nix store gc --max 1G
```

* Delete the unreachable paths in the closure of the current development shell

```console
# nix store gc .#devShells.default
```

# Description

This command deletes unreachable paths in the Nix store.
This command deletes unreachable paths from the Nix store.

If called with no argument, it will delete all the unreachable paths from the store.
If called with an installable argument, it will delete the unreachable paths whithin the closure of that argument.

)""
31 changes: 31 additions & 0 deletions tests/functional/gc-closure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
source common.sh

nix_gc_closure() {
clearStore
nix build -f dependencies.nix input0_drv --out-link $TEST_ROOT/gc-root
input0=$(realpath $TEST_ROOT/gc-root)
input1=$(nix build -f dependencies.nix input1_drv --no-link --print-out-paths)
input2=$(nix build -f dependencies.nix input2_drv --no-link --print-out-paths)
top=$(nix build -f dependencies.nix --no-link --print-out-paths)
somthing_else=$(nix store add-path ./dependencies.nix)

nix store gc "$top"

if isDaemonNewer "2.21.0pre20240229"; then
# Check that nix store gc is best-effort (doesn't fail when some paths in the closure are alive)
[[ ! -e "$top" ]] || fail "top should have been deleted"
[[ -e "$input0" ]] || fail "input0 is a gc root, shouldn't have been deleted"
[[ ! -e "$input2" ]] || fail "input2 is not a gc root and is part of top's closure, it should have been deleted"
[[ -e "$input1" ]] || fail "input1 is not ins the closure of top, it shouldn't have been deleted"
[[ -e "$somthing_else" ]] || fail "somthing_else is not in the closure of top, it shouldn't have been deleted"
else
# If the daemon is too old to handle closure gc, fallback to a full GC
[[ ! -e "$top" ]] || fail "top should have been deleted"
[[ -e "$input0" ]] || fail "input0 is a gc root, shouldn't have been deleted"
[[ ! -e "$input2" ]] || fail "input2 is not a gc root and is part of top's closure, it should have been deleted"
[[ ! -e "$input1" ]] || fail "input1 is not a gc root, it should have been deleted"
[[ ! -e "$somthing_else" ]] || fail "somthing_else is not a gc root, it should have been deleted"
fi
}

nix_gc_closure
1 change: 1 addition & 0 deletions tests/functional/local.mk
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ nix_tests = \
hash-convert.sh \
hash-path.sh \
gc-non-blocking.sh \
gc-closure.sh \
check.sh \
nix-shell.sh \
check-refs.sh \
Expand Down
Loading