subprojects/nix-eval-jobs: bring back constituents
Hydra used to support aggregate jobs that only succeeded when their constituents succeed. This is still used by e.g. nixpkgs[1]. Prior art: * https://git.lix.systems/lix-project/nix-eval-jobs/pulls/17: got ported into the CppNix implementation[2] * https://github.com/nix-community/nix-eval-jobs/pull/349: implements glob expressions for constituents - something we needed at work. This also restructures the code a bit which is what I re-used here. The globbing is not part of this patch. Essentially, the following things happen here (assuming `--constituents` is set): * Derivations with `_hydraAggregate = true;` are considered aggregates. These are not written to stdout when received by a worker, but stored until the end. * Constituents can be drv paths or strings (that must be the `attr` of another job). In that case, the derivation of the aggregate job is rewritten so that it depends on the drv of the constituent job. * At the very end the aggregate jobs are also written to stdout. Additionally, this fixes one bug, the old `hydra-eval-jobs` implementation had (and we actually hit at work): Given the leaf jobs `packages.foo` & `packages.bar`, an aggregate job `aggregate0` with _hydraAggregate = true; constituents = [ "packages.bar" "packages.foo" ]; and an aggregate job `aggregate1` with constituents = [ "aggregate0" ]; then it may happen depending on the order of evaluation that `aggregate1` depends on the old derivation of `aggregate0` (i.e. the one without rewritten constituents) and doesn't depend on `packages.foo` and `packages.bar` because it was rewritten before `aggregate0` was rewritten. This is done in here correctly, but topologically sorting the aggregate jobs before rewriting those. [1] https://github.com/NixOS/nixpkgs/blob/bba6b37c9d0898867a7d9c38a1b5b77efcfb07b9/nixos/release-combined.nix#L69 [2] https://github.com/nix-community/nix-eval-jobs/pull/340 Change-Id: I5baad5e57336b4985ef8595e903814de83eb01c1
This commit is contained in:
@@ -90,6 +90,57 @@ single large log file. In the
|
||||
[wiki](https://github.com/nix-community/nix-eval-jobs/wiki#ci-example-configurations)
|
||||
we collect example ci configuration for various CIs.
|
||||
|
||||
## Aggregate jobs
|
||||
|
||||
`nix-eval-jobs` supports the [Hydra's aggregate job feature](https://determinate.systems/posts/hydra-deployment-source-of-truth/#aggregate-jobs). The behavior is turned off by default and must be activated by passing `--constituents`.
|
||||
|
||||
When evaluating
|
||||
```nix
|
||||
stdenv.mkDerivation {
|
||||
pname = "aggregate-job";
|
||||
|
||||
_hydraAggregate = true;
|
||||
constituents = [
|
||||
/* drvs */
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
a JSON object is returned with a field `constituents` containing
|
||||
the drv paths of each constituent.
|
||||
|
||||
It's generally recommended to pass strings into `constituents` given
|
||||
it makes the evaluation way cheaper at the expense of `nix-eval-jobs`
|
||||
producing a different derivation in contrast to other Nix tools. In that
|
||||
case, the strings must correspond to the `attr` key of other jobs from
|
||||
the same evaluation. The aggregate job will be rewritten such that
|
||||
it has its constituents as input derivations.
|
||||
|
||||
For example,
|
||||
|
||||
```nix
|
||||
{
|
||||
jobs.constituent = mkDerivation {
|
||||
name = "foo";
|
||||
};
|
||||
aggregate = mkDerivation {
|
||||
name = "aggregate";
|
||||
_hydraAggregate = true;
|
||||
constituents = [ "jobs.constituent" ];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
results in two JSON objects returned from `nix-eval-jobs` where
|
||||
|
||||
* `jobs.constituent` isn't changed.
|
||||
* `aggregate` is a modified variant of the derivation in the expression
|
||||
above which also depends on the derivation of `jobs.constituent`.
|
||||
The `constituents` field doesn't contain the string `jobs.constituent`,
|
||||
but the corresponding drv path.
|
||||
|
||||
Cycles in aggregate jobs are not allowed and cause an error.
|
||||
|
||||
## Organisation of this repository
|
||||
|
||||
`main` follows Lix HEAD, and is updated alongside the Lix NixOS module. When we
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
#include <fnmatch.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <lix/config.h>
|
||||
#include <lix/libstore/derivations.hh>
|
||||
#include <lix/libstore/local-fs-store.hh>
|
||||
|
||||
#include "constituents.hh"
|
||||
#include "lix/libutil/async.hh"
|
||||
#include "drv.hh"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
// This is copied from `libutil/topo-sort.hh` in CppNix and slightly modified.
|
||||
// However, I needed a way to use strings as identifiers to sort, but still be
|
||||
// able to put AggregateJob objects into this function since I'd rather not have
|
||||
// to transform back and forth between a list of strings and AggregateJobs in
|
||||
// resolveNamedConstituents.
|
||||
auto topoSort(const std::set<AggregateJob> &items)
|
||||
-> std::vector<AggregateJob> {
|
||||
std::vector<AggregateJob> sorted;
|
||||
std::set<std::string> visited;
|
||||
std::set<std::string> parents;
|
||||
|
||||
std::map<std::string, AggregateJob> dictIdentToObject;
|
||||
for (const auto &it : items) {
|
||||
dictIdentToObject.insert({it.name, it});
|
||||
}
|
||||
|
||||
std::function<void(const std::string &path, const std::string *parent)>
|
||||
dfsVisit;
|
||||
|
||||
dfsVisit = [&](const std::string &path, const std::string *parent) {
|
||||
if (parents.contains(path)) {
|
||||
dictIdentToObject.erase(path);
|
||||
dictIdentToObject.erase(*parent);
|
||||
std::set<std::string> remaining;
|
||||
for (auto &[k, _] : dictIdentToObject) {
|
||||
remaining.insert(k);
|
||||
}
|
||||
throw DependencyCycle(path, *parent, remaining);
|
||||
}
|
||||
|
||||
if (!visited.insert(path).second) {
|
||||
return;
|
||||
}
|
||||
parents.insert(path);
|
||||
|
||||
std::set<std::string> references = dictIdentToObject[path].dependencies;
|
||||
|
||||
for (const auto &i : references) {
|
||||
/* Don't traverse into items that don't exist in our starting set.
|
||||
*/
|
||||
if (i != path &&
|
||||
dictIdentToObject.find(i) != dictIdentToObject.end()) {
|
||||
dfsVisit(i, &path);
|
||||
}
|
||||
}
|
||||
|
||||
sorted.push_back(dictIdentToObject[path]);
|
||||
parents.erase(path);
|
||||
};
|
||||
|
||||
for (auto &[i, _] : dictIdentToObject) {
|
||||
dfsVisit(i, nullptr);
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
auto resolveNamedConstituents(const std::map<std::string, nlohmann::json> &jobs)
|
||||
-> std::variant<std::vector<AggregateJob>, DependencyCycle> {
|
||||
std::set<AggregateJob> aggregateJobs;
|
||||
for (auto const &[jobName, job] : jobs) {
|
||||
auto named = job.find("namedConstituents");
|
||||
if (named != job.end() && !named->empty()) {
|
||||
std::unordered_map<std::string, std::string> brokenJobs;
|
||||
std::set<std::string> results;
|
||||
|
||||
auto isBroken = [&brokenJobs,
|
||||
&jobName](const std::string &childJobName,
|
||||
const nlohmann::json &job) -> bool {
|
||||
if (job.find("error") != job.end()) {
|
||||
std::string error = job["error"];
|
||||
nix::logger->log(
|
||||
nix::lvlError,
|
||||
nix::fmt(
|
||||
"aggregate job '%s' references broken job '%s': %s",
|
||||
jobName, childJobName, error));
|
||||
brokenJobs[childJobName] = error;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const std::string childJobName : *named) {
|
||||
auto childJobIter = jobs.find(childJobName);
|
||||
if (childJobIter == jobs.end()) {
|
||||
nix::logger->log(nix::lvlError,
|
||||
nix::fmt("aggregate job '%s' references "
|
||||
"non-existent job '%s'",
|
||||
jobName, childJobName));
|
||||
brokenJobs[childJobName] = "does not exist";
|
||||
} else if (!isBroken(childJobName, childJobIter->second)) {
|
||||
results.insert(childJobName);
|
||||
}
|
||||
}
|
||||
|
||||
aggregateJobs.insert(AggregateJob(jobName, results, brokenJobs));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return topoSort(aggregateJobs);
|
||||
} catch (DependencyCycle &e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
void rewriteAggregates(std::map<std::string, nlohmann::json> &jobs,
|
||||
const std::vector<AggregateJob> &aggregateJobs,
|
||||
nix::ref<nix::Store> &store, nix::Path &gcRootsDir,
|
||||
nix::AsyncIoRoot &aio) {
|
||||
for (const auto &aggregateJob : aggregateJobs) {
|
||||
auto &job = jobs.find(aggregateJob.name)->second;
|
||||
auto drvPath = store->parseStorePath(std::string(job["drvPath"]));
|
||||
auto drv = store->readDerivation(drvPath);
|
||||
|
||||
if (aggregateJob.brokenJobs.empty()) {
|
||||
for (const auto &childJobName : aggregateJob.dependencies) {
|
||||
auto childDrvPath = store->parseStorePath(
|
||||
std::string(jobs.find(childJobName)->second["drvPath"]));
|
||||
auto childDrv = store->readDerivation(childDrvPath);
|
||||
job["constituents"].push_back(
|
||||
store->printStorePath(childDrvPath));
|
||||
drv.inputDrvs.map[childDrvPath].value = {
|
||||
childDrv.outputs.begin()->first};
|
||||
}
|
||||
|
||||
std::string drvName(drvPath.name());
|
||||
assert(drvName.ends_with(nix::drvExtension));
|
||||
drvName.resize(drvName.size() - nix::drvExtension.size());
|
||||
|
||||
auto hashModulo = hashDerivationModulo(*store, drv, true);
|
||||
if (hashModulo.kind != nix::DrvHash::Kind::Regular) {
|
||||
continue;
|
||||
}
|
||||
auto h = hashModulo.hashes.find("out");
|
||||
if (h == hashModulo.hashes.end()) {
|
||||
continue;
|
||||
}
|
||||
auto outPath = store->makeOutputPath("out", h->second, drvName);
|
||||
drv.env["out"] = store->printStorePath(outPath);
|
||||
drv.outputs.insert_or_assign(
|
||||
"out", nix::DerivationOutput::InputAddressed{.path = outPath});
|
||||
|
||||
auto newDrvPath = aio.blockOn(nix::writeDerivation(*store, drv));
|
||||
auto newDrvPathS = store->printStorePath(newDrvPath);
|
||||
|
||||
register_gc_root(gcRootsDir, newDrvPathS, store, aio);
|
||||
|
||||
nix::logger->log(nix::lvlDebug,
|
||||
nix::fmt("rewrote aggregate derivation %s -> %s",
|
||||
store->printStorePath(drvPath),
|
||||
newDrvPathS));
|
||||
|
||||
job["drvPath"] = newDrvPathS;
|
||||
job["outputs"]["out"] = store->printStorePath(outPath);
|
||||
}
|
||||
|
||||
job.erase("namedConstituents");
|
||||
|
||||
if (!aggregateJob.brokenJobs.empty()) {
|
||||
std::stringstream ss;
|
||||
for (const auto &[jobName, error] : aggregateJob.brokenJobs) {
|
||||
ss << jobName << ": " << error << "\n";
|
||||
}
|
||||
job["error"] = ss.str();
|
||||
}
|
||||
|
||||
std::cout << job.dump() << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "lix/libutil/fmt.hh"
|
||||
#include <map>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include <lix/config.h>
|
||||
#include <lix/libstore/store-api.hh>
|
||||
|
||||
struct DependencyCycle : public std::exception {
|
||||
std::string a;
|
||||
std::string b;
|
||||
std::set<std::string> remainingAggregates;
|
||||
|
||||
DependencyCycle(std::string a, std::string b,
|
||||
const std::set<std::string> &remainingAggregates)
|
||||
: a(std::move(a)), b(std::move(b)),
|
||||
remainingAggregates(remainingAggregates) {}
|
||||
|
||||
[[nodiscard]] auto message() const -> std::string {
|
||||
return nix::fmt("Dependency cycle: %s <-> %s", a, b);
|
||||
}
|
||||
};
|
||||
|
||||
struct AggregateJob {
|
||||
std::string name;
|
||||
std::set<std::string> dependencies;
|
||||
std::unordered_map<std::string, std::string> brokenJobs;
|
||||
|
||||
auto operator<(const AggregateJob &b) const -> bool {
|
||||
return name < b.name;
|
||||
}
|
||||
};
|
||||
|
||||
auto resolveNamedConstituents(const std::map<std::string, nlohmann::json> &jobs)
|
||||
-> std::variant<std::vector<AggregateJob>, DependencyCycle>;
|
||||
|
||||
void rewriteAggregates(std::map<std::string, nlohmann::json> &jobs,
|
||||
const std::vector<AggregateJob> &aggregateJobs,
|
||||
nix::ref<nix::Store> &store, nix::Path &gcRootsDir,
|
||||
nix::AsyncIoRoot &aio);
|
||||
@@ -43,7 +43,8 @@ queryIsCached(nix::Store &store,
|
||||
|
||||
/* The fields of a derivation that are printed in json form */
|
||||
Drv::Drv(std::string &attrPath, nix::EvalState &state, nix::DrvInfo &drvInfo,
|
||||
MyArgs &args) {
|
||||
MyArgs &args, std::optional<Constituents> constituents)
|
||||
: constituents(constituents) {
|
||||
|
||||
auto localStore = state.ctx.store.dynamic_pointer_cast<nix::LocalFSStore>();
|
||||
|
||||
@@ -128,7 +129,29 @@ void to_json(nlohmann::json &json, const Drv &drv) {
|
||||
json["meta"] = drv.meta.value();
|
||||
}
|
||||
|
||||
if (auto constituents = drv.constituents) {
|
||||
json["constituents"] = constituents->constituents;
|
||||
json["namedConstituents"] = constituents->namedConstituents;
|
||||
}
|
||||
|
||||
if (drv.cacheStatus != Drv::CacheStatus::Unknown) {
|
||||
json["isCached"] = drv.cacheStatus == Drv::CacheStatus::Cached;
|
||||
}
|
||||
}
|
||||
|
||||
void register_gc_root(nix::Path &gcRootsDir, std::string &drvPath, const nix::ref<nix::Store> &store,
|
||||
nix::AsyncIoRoot &aio) {
|
||||
if (!gcRootsDir.empty()) {
|
||||
nix::Path root =
|
||||
gcRootsDir + "/" +
|
||||
std::string(nix::baseNameOf(drvPath));
|
||||
if (!nix::pathExists(root)) {
|
||||
auto localStore =
|
||||
store
|
||||
.dynamic_pointer_cast<nix::LocalFSStore>();
|
||||
auto storePath =
|
||||
localStore->parseStorePath(drvPath);
|
||||
aio.blockOn(localStore->addPermRoot(storePath, root));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,22 @@
|
||||
#include <optional>
|
||||
|
||||
#include "eval-args.hh"
|
||||
#include "lix/libstore/store-api.hh"
|
||||
|
||||
class MyArgs;
|
||||
|
||||
namespace nix {
|
||||
class EvalState;
|
||||
struct DrvInfo;
|
||||
} // namespace nix
|
||||
} // namespace nix
|
||||
|
||||
struct Constituents {
|
||||
std::vector<std::string> constituents;
|
||||
std::vector<std::string> namedConstituents;
|
||||
Constituents(std::vector<std::string> constituents,
|
||||
std::vector<std::string> namedConstituents)
|
||||
: constituents(constituents), namedConstituents(namedConstituents) {};
|
||||
};
|
||||
|
||||
/* The fields of a derivation that are printed in json form */
|
||||
struct Drv {
|
||||
@@ -27,7 +36,12 @@ struct Drv {
|
||||
std::map<std::string, std::optional<std::string>> outputs;
|
||||
std::map<std::string, std::set<std::string>> inputDrvs;
|
||||
std::optional<nlohmann::json> meta;
|
||||
std::optional<Constituents> constituents;
|
||||
|
||||
Drv(std::string &attrPath, nix::EvalState &state, nix::DrvInfo &drvInfo, MyArgs &args);
|
||||
Drv(std::string &attrPath, nix::EvalState &state, nix::DrvInfo &drvInfo,
|
||||
MyArgs &args, std::optional<Constituents> constituents);
|
||||
};
|
||||
void to_json(nlohmann::json &json, const Drv &drv);
|
||||
|
||||
void register_gc_root(nix::Path &gcRootsDir, std::string &drvPath,
|
||||
const nix::ref<nix::Store> &store, nix::AsyncIoRoot &aio);
|
||||
|
||||
@@ -37,6 +37,12 @@ MyArgs::MyArgs(nix::AsyncIoRoot & aio) : MixCommonArgs("nix-eval-jobs"), aio_(ai
|
||||
.description = "force recursion (don't respect recurseIntoAttrs)",
|
||||
.handler = {&forceRecurse, true}});
|
||||
|
||||
addFlag(
|
||||
{.longName = "constituents",
|
||||
.description =
|
||||
"whether to evaluate constituents for Hydra's aggregate feature",
|
||||
.handler = {&constituents, true}});
|
||||
|
||||
addFlag({.longName = "gc-roots-dir",
|
||||
.description = "garbage collector roots directory",
|
||||
.labels = {"path"},
|
||||
|
||||
@@ -28,6 +28,7 @@ class MyArgs : virtual public nix::MixEvalArgs,
|
||||
bool impure = false;
|
||||
bool forceRecurse = false;
|
||||
bool checkCacheStatus = false;
|
||||
bool constituents = false;
|
||||
size_t nrWorkers = 1;
|
||||
size_t maxMemorySize = 4096;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ src = files(
|
||||
'nix-eval-jobs.cc',
|
||||
'eval-args.cc',
|
||||
'drv.cc',
|
||||
'constituents.cc',
|
||||
'buffered-io.cc',
|
||||
'worker.cc',
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "constituents.hh"
|
||||
#include "eval-args.hh"
|
||||
#include "buffered-io.hh"
|
||||
#include "worker.hh"
|
||||
@@ -153,6 +154,7 @@ struct State {
|
||||
std::set<json> todo = json::array({json::array()});
|
||||
std::set<json> active;
|
||||
std::exception_ptr exc;
|
||||
std::map<std::string, nlohmann::json> jobs;
|
||||
};
|
||||
|
||||
void handleBrokenWorkerPipe(Proc &proc, std::string_view msg) {
|
||||
@@ -313,7 +315,12 @@ void collector(MyArgs &myArgs, Sync<State> &state_,
|
||||
}
|
||||
} else {
|
||||
auto state(state_.lock());
|
||||
std::cout << respString << "\n" << std::flush;
|
||||
state->jobs.insert_or_assign(response["attr"], response);
|
||||
auto named = response.find("namedConstituents");
|
||||
if (named == response.end() || named->empty()) {
|
||||
response.erase("namedConstituents");
|
||||
std::cout << response.dump() << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
proc_ = std::move(proc);
|
||||
@@ -392,5 +399,37 @@ int main(int argc, char **argv) {
|
||||
|
||||
if (state->exc)
|
||||
std::rethrow_exception(state->exc);
|
||||
|
||||
if (myArgs.constituents) {
|
||||
auto store = aio.blockOn(myArgs.evalStoreUrl
|
||||
? nix::openStore(*myArgs.evalStoreUrl)
|
||||
: nix::openStore());
|
||||
std::visit(
|
||||
nix::overloaded{
|
||||
[&](const std::vector<AggregateJob> &namedConstituents) {
|
||||
rewriteAggregates(state->jobs, namedConstituents, store,
|
||||
myArgs.gcRootsDir, aio);
|
||||
},
|
||||
[&](const DependencyCycle &e) {
|
||||
nix::logger->log(nix::lvlError,
|
||||
nix::fmt("Found dependency cycle "
|
||||
"between jobs '%s' and '%s'",
|
||||
e.a, e.b));
|
||||
state->jobs[e.a]["error"] = e.message();
|
||||
state->jobs[e.b]["error"] = e.message();
|
||||
|
||||
std::cout << state->jobs[e.a].dump() << "\n"
|
||||
<< state->jobs[e.b].dump() << "\n";
|
||||
|
||||
for (const auto &jobName : e.remainingAggregates) {
|
||||
state->jobs[jobName]["error"] =
|
||||
"Skipping aggregate because of a dependency "
|
||||
"cycle";
|
||||
std::cout << state->jobs[jobName].dump() << "\n";
|
||||
}
|
||||
},
|
||||
},
|
||||
resolveNamedConstituents(state->jobs));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,6 +74,53 @@ static std::string attrPathJoin(nlohmann::json input) {
|
||||
});
|
||||
}
|
||||
|
||||
static std::optional<Constituents>
|
||||
readConstituents(const nix::Value *v, nix::box_ptr<nix::EvalState> &state,
|
||||
nix::ref<nix::eval_cache::CachingEvaluator> &evaluator) {
|
||||
auto a = v->attrs->get(state->ctx.symbols.create("_hydraAggregate"));
|
||||
if (a && state->forceBool(*a->value, a->pos,
|
||||
"while evaluating the "
|
||||
"`_hydraAggregate` attribute")) {
|
||||
std::vector<std::string> constituents;
|
||||
std::vector<std::string> namedConstituents;
|
||||
auto a = v->attrs->get(state->ctx.symbols.create("constituents"));
|
||||
if (!a)
|
||||
state->ctx.errors
|
||||
.make<nix::EvalError>("derivation must have a ‘constituents’ "
|
||||
"attribute")
|
||||
.debugThrow();
|
||||
|
||||
nix::NixStringContext context;
|
||||
state->coerceToString(a->pos, *a->value, context,
|
||||
"while evaluating the `constituents` attribute",
|
||||
true, false);
|
||||
for (auto &c : context)
|
||||
std::visit(nix::overloaded{
|
||||
[&](const nix::NixStringContextElem::Built &b) {
|
||||
constituents.push_back(
|
||||
b.drvPath->to_string(*evaluator->store));
|
||||
},
|
||||
[&](const nix::NixStringContextElem::Opaque &) {},
|
||||
[&](const nix::NixStringContextElem::DrvDeep &) {},
|
||||
},
|
||||
c.raw);
|
||||
|
||||
state->forceList(*a->value, a->pos,
|
||||
"while evaluating the "
|
||||
"`constituents` attribute");
|
||||
for (unsigned int n = 0; n < a->value->listSize(); ++n) {
|
||||
auto v = a->value->listElems()[n];
|
||||
state->forceValue(*v, nix::noPos);
|
||||
if (v->type() == nix::nString)
|
||||
namedConstituents.push_back(v->string.s);
|
||||
}
|
||||
|
||||
return Constituents(constituents, namedConstituents);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void worker(nix::ref<nix::eval_cache::CachingEvaluator> evaluator,
|
||||
nix::Bindings &autoArgs, nix::AutoCloseFD &to,
|
||||
nix::AutoCloseFD &from, MyArgs &args, nix::AsyncIoRoot &aio) {
|
||||
@@ -128,25 +175,19 @@ void worker(nix::ref<nix::eval_cache::CachingEvaluator> evaluator,
|
||||
|
||||
if (v->type() == nix::nAttrs) {
|
||||
if (auto drvInfo = nix::getDerivation(*state, *v, false)) {
|
||||
auto drv = Drv(attrPathS, *state, *drvInfo, args);
|
||||
std::optional<Constituents> maybeConstituents;
|
||||
if (args.constituents) {
|
||||
maybeConstituents =
|
||||
readConstituents(v, state, evaluator);
|
||||
}
|
||||
auto drv = Drv(attrPathS, *state, *drvInfo, args,
|
||||
maybeConstituents);
|
||||
reply.update(drv);
|
||||
|
||||
/* Register the derivation as a GC root. !!! This
|
||||
registers roots for jobs that we may have already
|
||||
done. */
|
||||
if (args.gcRootsDir != "") {
|
||||
nix::Path root =
|
||||
args.gcRootsDir + "/" +
|
||||
std::string(nix::baseNameOf(drv.drvPath));
|
||||
if (!nix::pathExists(root)) {
|
||||
auto localStore =
|
||||
evaluator->store
|
||||
.dynamic_pointer_cast<nix::LocalFSStore>();
|
||||
auto storePath =
|
||||
localStore->parseStorePath(drv.drvPath);
|
||||
aio.blockOn(localStore->addPermRoot(storePath, root));
|
||||
}
|
||||
}
|
||||
register_gc_root(args.gcRootsDir, drv.drvPath, evaluator->store, aio);
|
||||
} else {
|
||||
auto attrs = nlohmann::json::array();
|
||||
bool recurse =
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, ... }:
|
||||
{ self, nixpkgs, ... }:
|
||||
let
|
||||
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
|
||||
mkDrvWithConstituents =
|
||||
name: constituents:
|
||||
pkgs.runCommand name {
|
||||
_hydraAggregate = true;
|
||||
inherit constituents;
|
||||
} "touch $out";
|
||||
in
|
||||
{
|
||||
hydraJobs = import ./ci.nix { inherit pkgs; };
|
||||
@@ -25,6 +33,42 @@
|
||||
builder = ":";
|
||||
};
|
||||
};
|
||||
constituents = {
|
||||
success = {
|
||||
indirect_aggregate = mkDrvWithConstituents "indirect_aggregate" [
|
||||
"anotherone"
|
||||
];
|
||||
direct_aggregate = mkDrvWithConstituents "direct_aggregate" [
|
||||
self.hydraJobs.builtJob
|
||||
];
|
||||
mixed_aggregate = mkDrvWithConstituents "mixed_aggregate" [
|
||||
self.hydraJobs.builtJob
|
||||
"anotherone"
|
||||
];
|
||||
anotherone = pkgs.writeText "constituent" "text";
|
||||
};
|
||||
failures = {
|
||||
aggregate = mkDrvWithConstituents "aggregate" [
|
||||
"doesntexist"
|
||||
"doesnteval"
|
||||
];
|
||||
doesnteval = pkgs.writeText "constituent" (toString { });
|
||||
};
|
||||
cycle = {
|
||||
aggregate0 = mkDrvWithConstituents "aggregate0" [
|
||||
"aggregate1"
|
||||
];
|
||||
aggregate1 = mkDrvWithConstituents "aggregate1" [
|
||||
"aggregate0"
|
||||
];
|
||||
};
|
||||
transitive = {
|
||||
constituent = pkgs.hello;
|
||||
# also flip the order to make sure the toposort works as intended.
|
||||
aggregate1 = mkDrvWithConstituents "aggregate1" [ "constituent" ];
|
||||
aggregate0 = mkDrvWithConstituents "aggregate0" [ "aggregate1" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,3 +140,133 @@ def test_recursion_error() -> None:
|
||||
print(stderr)
|
||||
assert "packageWithInfiniteRecursion" in stderr
|
||||
assert "possible infinite recursion" in stderr
|
||||
|
||||
|
||||
def test_constituents() -> None:
|
||||
with TemporaryDirectory() as tempdir:
|
||||
results, _ = evaluate(
|
||||
tempdir,
|
||||
0,
|
||||
[
|
||||
"--workers",
|
||||
"1",
|
||||
"--flake",
|
||||
".#legacyPackages.x86_64-linux.constituents.success",
|
||||
"--constituents",
|
||||
],
|
||||
)
|
||||
assert len(results) == 4
|
||||
|
||||
child = results[0]
|
||||
assert child["attr"] == "anotherone"
|
||||
assert "constituents" not in child
|
||||
assert "namedConstituents" not in child
|
||||
|
||||
direct = results[1]
|
||||
assert direct["attr"] == "direct_aggregate"
|
||||
assert "constituents" in direct
|
||||
assert "namedConstituents" not in direct
|
||||
|
||||
indirect = results[2]
|
||||
assert indirect["attr"] == "indirect_aggregate"
|
||||
assert "constituents" in indirect
|
||||
assert "namedConstituents" not in indirect
|
||||
|
||||
mixed = results[3]
|
||||
assert mixed["attr"] == "mixed_aggregate"
|
||||
|
||||
def absent_or_empty(f: str, d: dict) -> bool:
|
||||
return f not in d or len(d[f]) == 0
|
||||
|
||||
assert absent_or_empty("namedConstituents", direct)
|
||||
assert absent_or_empty("namedConstituents", indirect)
|
||||
assert absent_or_empty("namedConstituents", mixed)
|
||||
|
||||
assert direct["constituents"][0].endswith("-job1.drv")
|
||||
|
||||
assert indirect["constituents"][0] == child["drvPath"]
|
||||
|
||||
assert mixed["constituents"][0].endswith("-job1.drv")
|
||||
assert mixed["constituents"][1] == child["drvPath"]
|
||||
|
||||
assert "error" not in direct
|
||||
assert "error" not in indirect
|
||||
assert "error" not in mixed
|
||||
|
||||
check_gc_root(tempdir, direct["drvPath"])
|
||||
check_gc_root(tempdir, indirect["drvPath"])
|
||||
check_gc_root(tempdir, mixed["drvPath"])
|
||||
|
||||
|
||||
def test_constituents_cycle() -> None:
|
||||
with TemporaryDirectory() as tempdir:
|
||||
results, _ = evaluate(
|
||||
tempdir,
|
||||
0,
|
||||
[
|
||||
"--workers",
|
||||
"1",
|
||||
"--flake",
|
||||
".#legacyPackages.x86_64-linux.constituents.cycle",
|
||||
"--constituents",
|
||||
],
|
||||
)
|
||||
assert len(results) == 2
|
||||
|
||||
assert list(map(lambda x: x["name"], results)) == ["aggregate0", "aggregate1"]
|
||||
for i in results:
|
||||
assert i["error"] == "Dependency cycle: aggregate0 <-> aggregate1"
|
||||
|
||||
|
||||
def test_constituents_error() -> None:
|
||||
with TemporaryDirectory() as tempdir:
|
||||
results, _ = evaluate(
|
||||
tempdir,
|
||||
0,
|
||||
[
|
||||
"--workers",
|
||||
"1",
|
||||
"--flake",
|
||||
".#legacyPackages.x86_64-linux.constituents.failures",
|
||||
"--constituents",
|
||||
],
|
||||
)
|
||||
assert len(results) == 2
|
||||
|
||||
child = results[0]
|
||||
assert child["attr"] == "doesnteval"
|
||||
assert "error" in child
|
||||
|
||||
aggregate = results[1]
|
||||
assert aggregate["attr"] == "aggregate"
|
||||
assert "namedConstituents" not in aggregate
|
||||
assert "doesntexist: does not exist\n" in aggregate["error"]
|
||||
assert "constituents" in aggregate
|
||||
|
||||
|
||||
def test_transitivity() -> None:
|
||||
with TemporaryDirectory() as tempdir:
|
||||
results, _ = evaluate(
|
||||
tempdir,
|
||||
0,
|
||||
[
|
||||
"--workers",
|
||||
"1",
|
||||
"--flake",
|
||||
".#legacyPackages.x86_64-linux.constituents.transitive",
|
||||
"--constituents",
|
||||
],
|
||||
)
|
||||
assert len(results) == 3
|
||||
|
||||
job = results[0]
|
||||
assert job["attr"] == "constituent"
|
||||
assert "constituents" not in job
|
||||
|
||||
aggregate1 = results[1]
|
||||
assert aggregate1["attr"] == "aggregate1"
|
||||
|
||||
aggregate0 = results[2]
|
||||
assert aggregate0["attr"] == "aggregate0"
|
||||
|
||||
assert aggregate1["drvPath"] == aggregate0["constituents"][0]
|
||||
|
||||
Reference in New Issue
Block a user