nix-eval-jobs: add --no-instantiate flag

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 <joerg@thalheim.io>
Change-Id: Ib84f44e7799bc5577fd2ee98912458f16ebeab81
This commit is contained in:
Maximilian Bosch
2025-09-16 13:03:53 +02:00
co-authored by Jörg Thalheim
parent 812f466e0d
commit d6b0b8b382
6 changed files with 143 additions and 16 deletions
+10
View File
@@ -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.
+13 -4
View File
@@ -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<nix::LocalFSStore>();
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<nix::EvalError>(
@@ -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,8 +93,10 @@ Drv::Drv(std::string &attrPath, nix::EvalState &state, nix::DrvInfo &drvInfo,
cacheStatus = Drv::CacheStatus::Unknown;
}
drvPath = localStore->printStorePath(drvInfo.requireDrvPath(state));
drvPath = state.ctx.store->printStorePath(drvInfo.requireDrvPath(state));
name = drvInfo.queryName(state);
if (canReadDerivation) {
auto drv = state.aio.blockOn(localStore->readDerivation(drvInfo.requireDrvPath(state)));
for (const auto &[inputDrvPath, inputNode] : drv.inputDrvs) {
std::set<std::string> inputDrvOutputs;
@@ -100,8 +105,10 @@ Drv::Drv(std::string &attrPath, nix::EvalState &state, nix::DrvInfo &drvInfo,
}
inputDrvs[localStore->printStorePath(inputDrvPath)] = inputDrvOutputs;
}
name = drvInfo.queryName(state);
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<nix::Store> &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<nix::LocalFSStore>();
if (localStore) {
auto storePath =
localStore->parseStorePath(drvPath);
aio.blockOn(localStore->addPermRoot(storePath, root));
}
}
}
}
@@ -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",
@@ -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;
@@ -326,6 +326,10 @@ void collector(MyArgs &myArgs, Sync<State> &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) {
@@ -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