From d6b0b8b382835ee3751044c115ff09b7ee7604bd Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Sun, 14 Sep 2025 22:08:52 +0200 Subject: [PATCH] nix-eval-jobs: add `--no-instantiate` flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #987 The patch adds a flag `--no-instantiate` which only performs evaluation without instantiating any derivations. Hence, GC root creation is also skipped. To achieve that, Lix is also put in read-only mode and all operations that require reading a derivation (e.g. constituents or listing input derivations) are disabled fallback values are set. This is a port of an upstream PR[1]. Given the divergence of the codebases (different restructurings on both ends, no more CA derivations) I decided to redo large portions from scratch instead of cherry-picking the patches. Hence, the authorship. Additionally the clean up of casts down to a local store are removed or guarded behind an if, as done in the upstream PR. [1] https://github.com/nix-community/nix-eval-jobs/pull/379 Co-authored-by: Jörg Thalheim Change-Id: Ib84f44e7799bc5577fd2ee98912458f16ebeab81 --- doc/manual/rl-next/nej-no-instantiate.md | 10 +++ subprojects/nix-eval-jobs/src/drv.cc | 41 ++++++---- subprojects/nix-eval-jobs/src/eval-args.cc | 13 +++ subprojects/nix-eval-jobs/src/eval-args.hh | 1 + .../nix-eval-jobs/src/nix-eval-jobs.cc | 15 ++++ subprojects/nix-eval-jobs/tests/test_eval.py | 79 +++++++++++++++++++ 6 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 doc/manual/rl-next/nej-no-instantiate.md diff --git a/doc/manual/rl-next/nej-no-instantiate.md b/doc/manual/rl-next/nej-no-instantiate.md new file mode 100644 index 000000000..30a8bece9 --- /dev/null +++ b/doc/manual/rl-next/nej-no-instantiate.md @@ -0,0 +1,10 @@ +--- +synopsis: "nix-eval-jobs: support `--no-instantiate` flag" +issues: [fj#987] +category: Features +credits: [mic92,ma27] +--- + +`nix-eval-jobs` now supports a flag called `--no-instantiate`. With this enabled, +no write operations on the eval store are performed. That means, only evaluation is +performed, but derivations (and their gcroots) aren't created. diff --git a/subprojects/nix-eval-jobs/src/drv.cc b/subprojects/nix-eval-jobs/src/drv.cc index 8821ee976..c35360a6d 100644 --- a/subprojects/nix-eval-jobs/src/drv.cc +++ b/subprojects/nix-eval-jobs/src/drv.cc @@ -48,12 +48,13 @@ Drv::Drv(std::string &attrPath, nix::EvalState &state, nix::DrvInfo &drvInfo, : constituents(constituents) { auto localStore = state.ctx.store.try_cast_shared(); + auto canReadDerivation = localStore && !nix::settings.readOnlyMode; try { for (auto &[outputName, optOutputPath] : drvInfo.queryOutputs(state, true)) { assert(optOutputPath); - outputs[outputName] = localStore->printStorePath(*optOutputPath); + outputs[outputName] = state.ctx.store->printStorePath(*optOutputPath); } } catch (const std::exception &e) { // NOLINT(lix-foreign-exceptions) state.ctx.errors.make( @@ -82,6 +83,8 @@ Drv::Drv(std::string &attrPath, nix::EvalState &state, nix::DrvInfo &drvInfo, } meta = meta_; } + + // !canReadDerivation together with checkCacheStatus is rejected in main(). if (args.checkCacheStatus) { cacheStatus = queryIsCached(state.aio, *localStore, outputs) ? Drv::CacheStatus::Cached @@ -90,18 +93,22 @@ Drv::Drv(std::string &attrPath, nix::EvalState &state, nix::DrvInfo &drvInfo, cacheStatus = Drv::CacheStatus::Unknown; } - drvPath = localStore->printStorePath(drvInfo.requireDrvPath(state)); - - auto drv = state.aio.blockOn(localStore->readDerivation(drvInfo.requireDrvPath(state))); - for (const auto &[inputDrvPath, inputNode] : drv.inputDrvs) { - std::set inputDrvOutputs; - for (auto &outputName : inputNode) { - inputDrvOutputs.insert(outputName); - } - inputDrvs[localStore->printStorePath(inputDrvPath)] = inputDrvOutputs; - } + drvPath = state.ctx.store->printStorePath(drvInfo.requireDrvPath(state)); name = drvInfo.queryName(state); - system = drv.platform; + + if (canReadDerivation) { + auto drv = state.aio.blockOn(localStore->readDerivation(drvInfo.requireDrvPath(state))); + for (const auto &[inputDrvPath, inputNode] : drv.inputDrvs) { + std::set inputDrvOutputs; + for (auto &outputName : inputNode) { + inputDrvOutputs.insert(outputName); + } + inputDrvs[localStore->printStorePath(inputDrvPath)] = inputDrvOutputs; + } + system = drv.platform; + } else { + system = drvInfo.querySystem(state); + } } void to_json(nix::JSON &json, const Drv &drv) { @@ -133,15 +140,17 @@ void to_json(nix::JSON &json, const Drv &drv) { void register_gc_root(nix::Path &gcRootsDir, std::string &drvPath, const nix::ref &store, nix::AsyncIoRoot &aio) { - if (!gcRootsDir.empty()) { + if (!gcRootsDir.empty() && !nix::settings.readOnlyMode) { nix::Path root = gcRootsDir + "/" + std::string(nix::baseNameOf(drvPath)); if (!nix::pathExists(root)) { auto localStore = store.try_cast_shared(); - auto storePath = - localStore->parseStorePath(drvPath); - aio.blockOn(localStore->addPermRoot(storePath, root)); + if (localStore) { + auto storePath = + localStore->parseStorePath(drvPath); + aio.blockOn(localStore->addPermRoot(storePath, root)); + } } } } diff --git a/subprojects/nix-eval-jobs/src/eval-args.cc b/subprojects/nix-eval-jobs/src/eval-args.cc index 5f6c1677d..0406d323f 100644 --- a/subprojects/nix-eval-jobs/src/eval-args.cc +++ b/subprojects/nix-eval-jobs/src/eval-args.cc @@ -82,6 +82,19 @@ MyArgs::MyArgs(nix::AsyncIoRoot & aio) : MixCommonArgs("nix-eval-jobs"), aio_(ai .description = "print out a stack trace in case of evaluation errors", .handler = {&showTrace, true}}); + addFlag({ + .longName = "no-instantiate", + .aliases = {}, + .shortName = 0, + .description = + "don't instantiate (write) derivations, only evaluate (faster)", + .category = "", + .labels = {}, + .handler = {&noInstantiate, true}, + .completer = nullptr, + .experimentalFeature = std::nullopt, + }); + addFlag({.longName = "expr", .shortName = 'E', .description = "treat the argument as a Nix expression", diff --git a/subprojects/nix-eval-jobs/src/eval-args.hh b/subprojects/nix-eval-jobs/src/eval-args.hh index e46ab1ad3..26adb80cf 100644 --- a/subprojects/nix-eval-jobs/src/eval-args.hh +++ b/subprojects/nix-eval-jobs/src/eval-args.hh @@ -29,6 +29,7 @@ class MyArgs : virtual public nix::MixEvalArgs, bool forceRecurse = false; bool checkCacheStatus = false; bool constituents = false; + bool noInstantiate = false; size_t nrWorkers = 1; size_t maxMemorySize = 4096; diff --git a/subprojects/nix-eval-jobs/src/nix-eval-jobs.cc b/subprojects/nix-eval-jobs/src/nix-eval-jobs.cc index f6a5482ef..f030c7515 100644 --- a/subprojects/nix-eval-jobs/src/nix-eval-jobs.cc +++ b/subprojects/nix-eval-jobs/src/nix-eval-jobs.cc @@ -326,6 +326,10 @@ void collector(MyArgs &myArgs, Sync &state_, } else { auto state(state_.lock()); state->jobs.insert_or_assign(response["attr"], response); + if (nix::settings.readOnlyMode) { + response.erase("namedConstituents"); + response.erase("constituents"); + } auto named = response.find("namedConstituents"); if (named == response.end() || named->empty()) { response.erase("namedConstituents"); @@ -367,6 +371,17 @@ int main(int argc, char **argv) { myArgs.parseArgs(argv, argc); + /* Set no-instantiate mode if requested (makes evaluation faster) */ + if (myArgs.noInstantiate) { + nix::settings.readOnlyMode = true; + if (myArgs.constituents) { + throw UsageError("--no-instantiate and --constituents are mutually exclusive"); + } + if (myArgs.checkCacheStatus) { + throw UsageError("--no-instantiate and --check-cache-status are mutually exclusive"); + } + } + /* When building a flake, use pure evaluation (no access to 'getEnv', 'currentSystem' etc. */ if (myArgs.impure) { diff --git a/subprojects/nix-eval-jobs/tests/test_eval.py b/subprojects/nix-eval-jobs/tests/test_eval.py index ef6c67667..42b77dc28 100755 --- a/subprojects/nix-eval-jobs/tests/test_eval.py +++ b/subprojects/nix-eval-jobs/tests/test_eval.py @@ -285,3 +285,82 @@ def test_transitivity() -> None: assert aggregate0["attr"] == "aggregate0" assert aggregate1["drvPath"] == aggregate0["constituents"][0] + + +def test_mutually_exclusive_combinations() -> None: + with TemporaryDirectory() as tempdir: + for flag in ["constituents", "check-cache-status"]: + result = subprocess.run( + [ + str(BIN), + "--gc-roots-dir", + tempdir, + "--meta", + "--extra-experimental-features", + "flakes", + "--no-instantiate", + f"--{flag}", + "--workers", + "1", + "--flake", + ".#legacyPackages.x86_64-linux.constituents.success", + ], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + assert result.returncode == 1 + assert f"--no-instantiate and --{flag} are mutually exclusive" in result.stderr + + +def test_no_instantiate_mode() -> None: + """Test that --no-instantiate flag works correctly""" + with TemporaryDirectory() as tempdir: + path = Path(tempdir) + gcroots = path / "gcroots" + gcroots.mkdir() + results, _ = evaluate( + tempdir, + 0, + [ + "--gc-roots-dir", + gcroots, + "--eval-store", + path / "root", + "--meta", + "--no-instantiate", + "--flake", + ".#hydraJobs", + ] + ) + assert len(results) == 4 + + # Check that all results have the expected structure + for result in results: + # In no-instantiate mode, drvPath should still be present (from the attr) + assert "drvPath" in result + assert result["drvPath"].endswith(".drv") + + assert not (path / "root" / result["drvPath"][1:]).exists() + + # System should still be present (from querySystem fallback) + assert "system" in result + assert result["system"] != "" + + # Name should still be present + assert "name" in result + + # Outputs should still be present but may be empty + assert "outputs" in result + + # Cache status should not be present (it's Unknown and not included) + assert "cacheStatus" not in result + assert "neededBuilds" not in result + assert "neededSubstitutes" not in result + + # Input drvs should not be present (requires reading derivation from store) + assert not result["inputDrvs"] + + # No GC roots should be created in no-instantiate mode + assert len(list(Path(gcroots).iterdir())) == 0