diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index b6dd8368455c..2c555d5d51ad 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -708,18 +708,36 @@ static void performOp(TunnelLogger * logger, ref store, case wopCollectGarbage: { GCOptions options; - options.action = (GCOptions::GCAction) readInt(from); - options.pathsToDelete = WorkerProto::read(*store, from); - from >> options.ignoreLiveness >> options.maxFreed; + int action; + bool ignoreLiveness; + StorePathSet pathsToDelete; + bool recursive {false}; + action = readInt(from); + pathsToDelete = WorkerProto::read(*store, from); + from >> ignoreLiveness >> options.maxFreed; // obsolete fields readInt(from); readInt(from); readInt(from); + if (GET_PROTOCOL_MINOR(clientVersion) >= 31) { + from >> recursive; + }; + + switch (action) { + case 0: + options.action = GCReturn::Live; break; + case 1: + options.action = GCReturn::Dead; break; + case 2: + options.action = GCDelete { .ignoreLiveness = ignoreLiveness }; break; + case 3: + options.action = GCDelete { .pathsToDelete = GCPathsToDelete { .paths = pathsToDelete, .recursive = recursive }, .ignoreLiveness = ignoreLiveness, }; break; + }; GCResults results; logger->startWork(); - if (options.ignoreLiveness) + if (ignoreLiveness) throw Error("you are not allowed to ignore liveness"); auto & gcStore = require(*store); gcStore.collectGarbage(options, results); diff --git a/src/libstore/gc-store.hh b/src/libstore/gc-store.hh index 2c26c65c42ca..d3cfeafba526 100644 --- a/src/libstore/gc-store.hh +++ b/src/libstore/gc-store.hh @@ -9,52 +9,35 @@ namespace nix { typedef std::unordered_map> Roots; +/* Return either live (reachable) or dead (unreachable) paths */ +enum class GCReturn { Live, Dead }; -struct GCOptions -{ - /** - * Garbage collector operation: - * - * - `gcReturnLive`: return the set of paths reachable from - * (i.e. in the closure of) the roots. - * - * - `gcReturnDead`: return the set of paths not reachable from - * the roots. - * - * - `gcDeleteDead`: actually delete the latter set. - * - * - `gcDeleteSpecific`: delete the paths listed in - * `pathsToDelete`, insofar as they are not reachable. - */ - typedef enum { - gcReturnLive, - gcReturnDead, - gcDeleteDead, - gcDeleteSpecific, - } GCAction; +/* Set of paths to delete, and whether their transitive closures should be deleted as well */ +struct GCPathsToDelete { + StorePathSet paths; + bool recursive; +}; - GCAction action{gcDeleteDead}; +/* Delete either a given set of paths, or all dead paths */ +struct GCDelete { + /* Delete this set, or all dead paths if it is std::nullopt */ + std::optional pathsToDelete; + /* If `ignoreLiveness' is set, then reachability from the roots is + ignored (dangerous!). However, the paths must still be + unreferenced *within* the store (i.e., there can be no other + store paths that depend on them). */ + bool ignoreLiveness{false}; +}; - /** - * If `ignoreLiveness` is set, then reachability from the roots is - * ignored (dangerous!). However, the paths must still be - * unreferenced *within* the store (i.e., there can be no other - * store paths that depend on them). - */ - bool ignoreLiveness{false}; +/* Garbage collection action: either return paths, or delete them */ +using GCAction = std::variant; - /** - * For `gcDeleteSpecific`, the paths to delete. - */ - StorePathSet pathsToDelete; - - /** - * Stop after at least `maxFreed` bytes have been freed. - */ - uint64_t maxFreed{std::numeric_limits::max()}; +struct GCOptions { + GCAction action; + /* Stop after at least `maxFreed' bytes have been freed. */ + uint64_t maxFreed{std::numeric_limits::max()}; }; - struct GCResults { /** @@ -63,10 +46,7 @@ struct GCResults */ PathSet paths; - /** - * For `gcReturnDead`, `gcDeleteDead` and `gcDeleteSpecific`, the - * number of bytes that would be or was freed. - */ + /* For `GCDelete', the number of bytes that would be or was freed. */ uint64_t bytesFreed = 0; }; diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc index a4e49372d796..9301b0c9e26b 100644 --- a/src/libstore/gc.cc +++ b/src/libstore/gc.cc @@ -464,7 +464,7 @@ struct GCLimitReached { }; void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) { - bool shouldDelete = options.action == GCOptions::gcDeleteDead || options.action == GCOptions::gcDeleteSpecific; + bool shouldDelete = std::holds_alternative(options.action); bool gcKeepOutputs = settings.gcKeepOutputs; bool gcKeepDerivations = settings.gcKeepDerivations; @@ -489,10 +489,12 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) consequences if `keep-outputs' or `keep-derivations' are true (the garbage collector will recurse into deleting the outputs or derivers, respectively). So disable them. */ - if (options.action == GCOptions::gcDeleteSpecific && options.ignoreLiveness) { - gcKeepOutputs = false; - gcKeepDerivations = false; - } + std::visit(overloaded{[&](GCDelete del){ + if (del.pathsToDelete.has_value() && del.ignoreLiveness) { + gcKeepOutputs = false; + gcKeepDerivations = false; + } + }, [](GCReturn arg){}}, options.action); if (shouldDelete) deletePath(reservedPath); @@ -612,8 +614,14 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) permanent roots cannot increase now. */ printInfo("finding garbage collector roots..."); Roots rootMap; - if (!options.ignoreLiveness) - findRootsNoTemp(rootMap, true); + + std::visit(overloaded{ + [&](GCDelete del) { + if (!del.ignoreLiveness) + findRootsNoTemp(rootMap, true); + }, + [&](GCReturn r){}}, + options.action); for (auto & i : rootMap) roots.insert(i.first); @@ -652,10 +660,12 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) deletePath(realPath, bytesFreed); results.bytesFreed += bytesFreed; - if (results.bytesFreed > options.maxFreed) { - printInfo("deleted more than %d bytes; stopping", options.maxFreed); - throw GCLimitReached(); - } + std::visit(overloaded{[&](GCDelete del) { + if (results.bytesFreed > options.maxFreed) { + printInfo("deleted more than %d bytes; stopping", options.maxFreed); + throw GCLimitReached(); + } + }, [](GCReturn r){}}, options.action); }; std::map referrersCache; @@ -716,9 +726,9 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) return markAlive(); } - if (options.action == GCOptions::gcDeleteSpecific - && !options.pathsToDelete.count(*path)) - return; + if (const GCDelete * del = std::get_if(&options.action)) + if (del->pathsToDelete.has_value() && del->pathsToDelete->paths.count(*path)) + return; { auto hashPart = std::string(path->hashPart()); @@ -778,107 +788,108 @@ 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 || options.action == GCOptions::gcDeleteDead) { - - for (auto & i : options.pathsToDelete) { - 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: " - "nix-store --query --roots", - printStorePath(i)); - } - - } else if (options.maxFreed > 0) { - - if (shouldDelete) - printInfo("deleting garbage..."); - else - printInfo("determining live/dead paths..."); - - try { - AutoCloseDir dir(opendir(realStoreDir.get().c_str())); - if (!dir) throw SysError("opening directory '%1%'", realStoreDir); - - /* Read the store and delete all paths that are invalid or - unreachable. We don't use readDirectory() here so that - GCing can start faster. */ - auto linksName = baseNameOf(linksDir); - Paths entries; - struct dirent * dirent; - while (errno = 0, dirent = readdir(dir.get())) { - checkInterrupt(); - std::string name = dirent->d_name; - if (name == "." || name == ".." || name == linksName) continue; + std::visit(overloaded{ + [&](GCDelete del){ + if (del.pathsToDelete.has_value()) { + for (auto & i : del.pathsToDelete->paths) { + deleteReferrersClosure(i); + if (!del.pathsToDelete->recursive && !dead.count(i)) + throw Error( + "Cannot delete path '%1%' since it is still alive. " + "To find out why, use: " + "nix-store --query --roots", + printStorePath(i)); + } + } if (options.maxFreed > 0) { - if (auto storePath = maybeParseStorePath(storeDir + "/" + name)) - deleteReferrersClosure(*storePath); + if (shouldDelete) + printInfo("deleting garbage..."); else - deleteFromStore(name); + printInfo("determining live/dead paths..."); - } - } catch (GCLimitReached & e) { - } - } + try { + AutoCloseDir dir(opendir(realStoreDir.get().c_str())); + if (!dir) throw SysError("opening directory '%1%'", realStoreDir); + + /* Read the store and delete all paths that are invalid or + unreachable. We don't use readDirectory() here so that + GCing can start faster. */ + auto linksName = baseNameOf(linksDir); + Paths entries; + struct dirent * dirent; + while (errno = 0, dirent = readdir(dir.get())) { + checkInterrupt(); + std::string name = dirent->d_name; + if (name == "." || name == ".." || name == linksName) continue; + + if (auto storePath = maybeParseStorePath(storeDir + "/" + name)) + deleteReferrersClosure(*storePath); + else + deleteFromStore(name); - if (options.action == GCOptions::gcReturnLive) { - for (auto & i : alive) - results.paths.insert(printStorePath(i)); - return; - } + } + } catch (GCLimitReached & e) { + } + } - if (options.action == GCOptions::gcReturnDead) { - for (auto & i : dead) - results.paths.insert(printStorePath(i)); - return; - } + /* Unlink all files in /nix/store/.links that have a link count of 1, + which indicates that there are no other links and so they can be + safely deleted. FIXME: race condition with optimisePath(): we + might see a link count of 1 just before optimisePath() increases + the link count. */ - /* Unlink all files in /nix/store/.links that have a link count of 1, - which indicates that there are no other links and so they can be - safely deleted. FIXME: race condition with optimisePath(): we - might see a link count of 1 just before optimisePath() increases - the link count. */ - if (options.action == GCOptions::gcDeleteDead || options.action == GCOptions::gcDeleteSpecific) { - printInfo("deleting unused links..."); + printInfo("deleting unused links..."); - AutoCloseDir dir(opendir(linksDir.c_str())); - if (!dir) throw SysError("opening directory '%1%'", linksDir); + AutoCloseDir dir(opendir(linksDir.c_str())); + if (!dir) throw SysError("opening directory '%1%'", linksDir); - int64_t actualSize = 0, unsharedSize = 0; + int64_t actualSize = 0, unsharedSize = 0; - struct dirent * dirent; - while (errno = 0, dirent = readdir(dir.get())) { - checkInterrupt(); - std::string name = dirent->d_name; - if (name == "." || name == "..") continue; - Path path = linksDir + "/" + name; + struct dirent * dirent; + while (errno = 0, dirent = readdir(dir.get())) { + checkInterrupt(); + std::string name = dirent->d_name; + if (name == "." || name == "..") continue; + Path path = linksDir + "/" + name; - auto st = lstat(path); + auto st = lstat(path); - if (st.st_nlink != 1) { - actualSize += st.st_size; - unsharedSize += (st.st_nlink - 1) * st.st_size; - continue; - } + if (st.st_nlink != 1) { + actualSize += st.st_size; + unsharedSize += (st.st_nlink - 1) * st.st_size; + continue; + } - printMsg(lvlTalkative, "deleting unused link '%1%'", path); + printMsg(lvlTalkative, "deleting unused link '%1%'", path); - if (unlink(path.c_str()) == -1) - throw SysError("deleting '%1%'", path); + if (unlink(path.c_str()) == -1) + throw SysError("deleting '%1%'", path); - /* Do not accound for deleted file here. Rely on deletePath() - accounting. */ - } + /* Do not accound for deleted file here. Rely on deletePath() + accounting. */ + } - struct stat st; - if (stat(linksDir.c_str(), &st) == -1) - throw SysError("statting '%1%'", linksDir); - int64_t overhead = st.st_blocks * 512ULL; + struct stat st; + if (stat(linksDir.c_str(), &st) == -1) + throw SysError("statting '%1%'", linksDir); + int64_t overhead = st.st_blocks * 512ULL; + + printInfo("note: currently hard linking saves %.2f MiB", + ((unsharedSize - actualSize - overhead) / (1024.0 * 1024.0))); + }, + [&](GCReturn ret){ + if (ret == GCReturn::Live) { + for (auto & i : alive) + results.paths.insert(printStorePath(i)); + return; + } - printInfo("note: currently hard linking saves %.2f MiB", - ((unsharedSize - actualSize - overhead) / (1024.0 * 1024.0))); - } + if (ret == GCReturn::Dead) { + for (auto & i : dead) + results.paths.insert(printStorePath(i)); + return; + } + }}, options.action); /* While we're at it, vacuum the database. */ //if (options.action == GCOptions::gcDeleteDead) vacuumDB(); @@ -940,8 +951,7 @@ void LocalStore::autoGC(bool sync) promise.set_value(); }); - GCOptions options; - options.maxFreed = settings.maxFree - avail; + GCOptions options{.action = GCAction{GCDelete{}}, .maxFreed = settings.maxFree - avail}; printInfo("running auto-GC to free %d bytes", options.maxFreed); diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index c3dfb5979159..2b3092d72c14 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -862,15 +862,45 @@ Roots RemoteStore::findRoots(bool censor) void RemoteStore::collectGarbage(const GCOptions & options, GCResults & results) { auto conn(getConnection()); + int action; + StorePathSet pathsToDelete; + bool ignoreLiveness {false}; + bool recursive {false}; + std::visit(overloaded{ + [&](GCReturn ret){ + switch (ret) { + case (GCReturn::Live): + action = 0; + break; + case (GCReturn::Dead): + action = 1; + break; + } + }, + [&](GCDelete del) { + ignoreLiveness = del.ignoreLiveness; + if (!del.pathsToDelete.has_value()) { + action = 3; + } else { + action = 4; + pathsToDelete = del.pathsToDelete->paths; + recursive = del.pathsToDelete->recursive; + }; + } + }, options.action); conn->to - << wopCollectGarbage << options.action; - workerProtoWrite(*this, conn->to, options.pathsToDelete); - conn->to << options.ignoreLiveness + << wopCollectGarbage << action; + workerProtoWrite(*this, conn->to, pathsToDelete); + conn->to << ignoreLiveness << options.maxFreed /* removed options */ << 0 << 0 << 0; + if (GET_PROTOCOL_MINOR(conn->daemonVersion) >= 31) { + conn->to << recursive; + } + conn.processStderr(); results.paths = readStrings(conn->from); diff --git a/src/nix-collect-garbage/nix-collect-garbage.cc b/src/nix-collect-garbage/nix-collect-garbage.cc index cb1f42e35ae3..ccc45a205fb0 100644 --- a/src/nix-collect-garbage/nix-collect-garbage.cc +++ b/src/nix-collect-garbage/nix-collect-garbage.cc @@ -88,7 +88,7 @@ static int main_nix_collect_garbage(int argc, char * * argv) if (!dryRun) { auto store = openStore(); auto & gcStore = require(*store); - options.action = GCOptions::gcDeleteDead; + options.action = GCDelete {}; GCResults results; PrintFreed freed(true, results); gcStore.collectGarbage(options, results); diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index 61c189efbf54..ae7f9b6dbd32 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -588,15 +588,15 @@ static void opGC(Strings opFlags, Strings opArgs) { bool printRoots = false; GCOptions options; - options.action = GCOptions::gcDeleteDead; + options.action = GCAction{GCDelete{}}; GCResults results; /* Do what? */ for (auto i = opFlags.begin(); i != opFlags.end(); ++i) if (*i == "--print-roots") printRoots = true; - else if (*i == "--print-live") options.action = GCOptions::gcReturnLive; - else if (*i == "--print-dead") options.action = GCOptions::gcReturnDead; + else if (*i == "--print-live") options.action = GCAction{GCReturn::Live}; + else if (*i == "--print-dead") options.action = GCAction{GCReturn::Dead}; else if (*i == "--max-freed") options.maxFreed = std::max(getIntArg(*i, i, opFlags.end(), true), (int64_t) 0); else throw UsageError("bad sub-operation '%1%' in GC", *i); @@ -617,10 +617,10 @@ static void opGC(Strings opFlags, Strings opArgs) } else { - PrintFreed freed(options.action == GCOptions::gcDeleteDead, results); + PrintFreed freed(std::holds_alternative(options.action), results); gcStore.collectGarbage(options, results); - if (options.action != GCOptions::gcDeleteDead) + if (!std::holds_alternative(options.action)) for (auto & i : results.paths) cout << i << std::endl; } @@ -632,18 +632,19 @@ static void opGC(Strings opFlags, Strings opArgs) roots). */ static void opDelete(Strings opFlags, Strings opArgs) { - GCOptions options; - options.action = GCOptions::gcDeleteSpecific; + GCDelete del = GCDelete{.pathsToDelete = GCPathsToDelete{}}; for (auto & i : opFlags) - if (i == "--ignore-liveness") options.ignoreLiveness = true; + if (i == "--ignore-liveness") del.ignoreLiveness = true; else throw UsageError("unknown flag '%1%'", i); for (auto & i : opArgs) - options.pathsToDelete.insert(store->followLinksToStorePath(i)); + del.pathsToDelete->paths.insert(store->followLinksToStorePath(i)); + auto & gcStore = require(*store); + GCOptions options{GCAction{del}}; GCResults results; PrintFreed freed(true, results); gcStore.collectGarbage(options, results); diff --git a/src/nix/store-delete.cc b/src/nix/store-delete.cc index 922cc699fc2a..2f8905bf283f 100644 --- a/src/nix/store-delete.cc +++ b/src/nix/store-delete.cc @@ -9,14 +9,20 @@ using namespace nix; struct CmdStoreDelete : StorePathsCommand { - GCOptions options { .action = GCOptions::gcDeleteDead }; + GCDelete deleteOpts{.pathsToDelete = GCPathsToDelete{}}; CmdStoreDelete() { addFlag({ .longName = "ignore-liveness", .description = "Do not check whether the paths are reachable from a root.", - .handler = {&options.ignoreLiveness, true} + .handler = {&deleteOpts.ignoreLiveness, true} + }); + addFlag({ + .longName = "recursive", + .shortName = 'r', + .description = "Delete all unreachable (dead) paths in the transitive closure of the provided paths.", + .handler = {&deleteOpts.pathsToDelete->recursive, true} }); } @@ -37,8 +43,9 @@ struct CmdStoreDelete : StorePathsCommand auto & gcStore = require(*store); for (auto & path : storePaths) - options.pathsToDelete.insert(path); + deleteOpts.pathsToDelete->paths.insert(path); + GCOptions options {GCAction{deleteOpts}}; GCResults results; PrintFreed freed(true, results); gcStore.collectGarbage(options, results); diff --git a/src/nix/store-gc.cc b/src/nix/store-gc.cc index 8b9b5d1642a8..aa7ef18723d6 100644 --- a/src/nix/store-gc.cc +++ b/src/nix/store-gc.cc @@ -37,9 +37,12 @@ struct CmdStoreGC : StoreCommand, MixDryRun { auto & gcStore = require(*store); - options.action = dryRun ? GCOptions::gcReturnDead : GCOptions::gcDeleteDead; + if (dryRun) + options.action = GCReturn::Dead; + else + options.action = GCDelete{}; GCResults results; - PrintFreed freed(options.action == GCOptions::gcDeleteDead, results); + PrintFreed freed(!dryRun, results); gcStore.collectGarbage(options, results); } };