-
-
Notifications
You must be signed in to change notification settings - Fork 40
Implement constituent globs #349
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
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| #include <fnmatch.h> | ||
| #include <nlohmann/json.hpp> | ||
| #include <nix/config.h> | ||
| #include <nix/derivations.hh> | ||
| #include <nix/local-fs-store.hh> | ||
|
|
||
| #include "constituents.hh" | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| auto insertMatchingConstituents( | ||
| const std::string &childJobName, const std::string &jobName, | ||
| const std::function<bool(const std::string &, const nlohmann::json &)> | ||
| &isBroken, | ||
| const std::map<std::string, nlohmann::json> &jobs, | ||
| std::set<std::string> &results) -> bool { | ||
| bool expansionFound = false; | ||
| for (const auto &[currentJobName, job] : jobs) { | ||
| // Never select the job itself as constituent. Trivial way | ||
| // to avoid obvious cycles. | ||
| if (currentJobName == jobName) { | ||
| continue; | ||
| } | ||
| auto jobName = currentJobName; | ||
| if (fnmatch(childJobName.c_str(), jobName.c_str(), 0) == 0 && | ||
| !isBroken(jobName, job)) { | ||
| results.insert(jobName); | ||
| expansionFound = true; | ||
| } | ||
| } | ||
|
|
||
| return expansionFound; | ||
| } | ||
| } // 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()) { | ||
| bool globConstituents = job.value<bool>("globConstituents", false); | ||
| 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()) { | ||
| if (!globConstituents) { | ||
| nix::logger->log( | ||
| nix::lvlError, | ||
| nix::fmt("aggregate job '%s' references " | ||
| "non-existent job '%s'", | ||
| jobName, childJobName)); | ||
| brokenJobs[childJobName] = "does not exist"; | ||
| } else if (!insertMatchingConstituents(childJobName, | ||
| jobName, isBroken, | ||
| jobs, results)) { | ||
| nix::warn("aggregate job '%s' references constituent " | ||
| "glob pattern '%s' with no matches", | ||
| jobName, childJobName); | ||
| brokenJobs[childJobName] = | ||
| "constituent glob pattern had no matches"; | ||
| } | ||
| } 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) { | ||
| 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(nix::hasSuffix(drvName, 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 = nix::writeDerivation(*store, drv); | ||
| auto newDrvPathS = store->printStorePath(newDrvPath); | ||
|
|
||
| /* Register the derivation as a GC root. !!! This | ||
| registers roots for jobs that we may have already | ||
| done. */ | ||
| auto localStore = store.dynamic_pointer_cast<nix::LocalFSStore>(); | ||
| assert(!gcRootsDir.empty()); | ||
| const nix::Path root = | ||
| gcRootsDir + "/" + std::string(nix::baseNameOf(newDrvPathS)); | ||
| if (!nix::pathExists(root)) { | ||
| localStore->addPermRoot(newDrvPath, root); | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| #pragma once | ||
|
|
||
| #include "fmt.hh" | ||
| #include <map> | ||
| #include <nlohmann/json_fwd.hpp> | ||
| #include <set> | ||
| #include <string> | ||
| #include <utility> | ||
| #include <variant> | ||
|
|
||
| #include <nix/config.h> | ||
| #include <nix/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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this an explanation for why you didn't just use
libutil/topo-sort.hh?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like if instead of having
std::set<AggregateJob>you hadstd::map<std::string, AggregateJob>you could bothUse
libutil/topo-sort.hhas-isUse the automatic
auto operator<=>(const AggregateJob &) const;rather than one which is "not lawful"The final
std::vector<std::string>tostd::vector<std::pair<std::string, AggregateJob>>would be not too bad, either a simple orstd::transform?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to do this in d9febaf but it seems to compute the wrong result.
So reverted the commit again. However feel free to fix this if you see what is wrong. To me it seems these both sorting functions are not functionally equivalent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, thanks for trying!