nix-super/src/nix/flake.cc

1474 lines
57 KiB
C++
Raw Normal View History

2018-11-29 20:18:36 +02:00
#include "command.hh"
#include "installable-flake.hh"
2018-11-29 20:18:36 +02:00
#include "common-args.hh"
#include "shared.hh"
#include "eval.hh"
#include "eval-inline.hh"
#include "eval-settings.hh"
#include "flake/flake.hh"
#include "get-drvs.hh"
#include "signals.hh"
#include "store-api.hh"
2019-06-17 18:59:57 +03:00
#include "derivations.hh"
#include "outputs-spec.hh"
#include "attr-path.hh"
#include "fetchers.hh"
#include "registry.hh"
2020-04-20 14:14:59 +03:00
#include "eval-cache.hh"
#include "markdown.hh"
#include "users.hh"
2019-04-16 15:10:05 +03:00
#include <nlohmann/json.hpp>
2019-03-29 17:18:25 +02:00
#include <queue>
#include <iomanip>
2018-11-29 20:18:36 +02:00
using namespace nix;
using namespace nix::flake;
using json = nlohmann::json;
2018-11-29 20:18:36 +02:00
struct CmdFlakeUpdate;
2020-06-08 17:20:00 +03:00
class FlakeCommand : virtual Args, public MixFlakeOptions
{
protected:
2019-10-08 17:30:04 +03:00
std::string flakeUrl = ".";
public:
FlakeCommand()
{
2020-05-11 23:10:33 +03:00
expectArgs({
.label = "flake-url",
.optional = true,
.handler = {&flakeUrl},
Overhaul completions, redo #6693 (#8131) As I complained in https://github.com/NixOS/nix/pull/6784#issuecomment-1421777030 (a comment on the wrong PR, sorry again!), #6693 introduced a second completions mechanism to fix a bug. Having two completion mechanisms isn't so nice. As @thufschmitt also pointed out, it was a bummer to go from `FlakeRef` to `std::string` when collecting flake refs. Now it is `FlakeRefs` again. The underlying issue that sought to work around was that completion of arguments not at the end can still benefit from the information from latter arguments. To fix this better, we rip out that change and simply defer all completion processing until after all the (regular, already-complete) arguments have been passed. In addition, I noticed the original completion logic used some global variables. I do not like global variables, because even if they save lines of code, they also obfuscate the architecture of the code. I got rid of them moved them to a new `RootArgs` class, which now has `parseCmdline` instead of `Args`. The idea is that we have many argument parsers from subcommands and what-not, but only one root args that owns the other per actual parsing invocation. The state that was global is now part of the root args instead. This did, admittedly, add a bunch of new code. And I do feel bad about that. So I went and added a lot of API docs to try to at least make the current state of things clear to the next person. -- This is needed for RFC 134 (tracking issue #7868). It was very hard to modularize `Installable` parsing when there were two completion arguments. I wouldn't go as far as to say it is *easy* now, but at least it is less hard (and the completions test finally passed). Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
2023-10-23 16:03:11 +03:00
.completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) {
completeFlakeRef(completions, getStore(), prefix);
2020-05-11 23:10:33 +03:00
}}
});
}
FlakeRef getFlakeRef()
{
return parseFlakeRef(flakeUrl, absPath(".")); //FIXME
}
LockedFlake lockFlake()
{
return flake::lockFlake(*getEvalState(), getFlakeRef(), lockFlags);
}
2020-06-08 17:20:00 +03:00
Overhaul completions, redo #6693 (#8131) As I complained in https://github.com/NixOS/nix/pull/6784#issuecomment-1421777030 (a comment on the wrong PR, sorry again!), #6693 introduced a second completions mechanism to fix a bug. Having two completion mechanisms isn't so nice. As @thufschmitt also pointed out, it was a bummer to go from `FlakeRef` to `std::string` when collecting flake refs. Now it is `FlakeRefs` again. The underlying issue that sought to work around was that completion of arguments not at the end can still benefit from the information from latter arguments. To fix this better, we rip out that change and simply defer all completion processing until after all the (regular, already-complete) arguments have been passed. In addition, I noticed the original completion logic used some global variables. I do not like global variables, because even if they save lines of code, they also obfuscate the architecture of the code. I got rid of them moved them to a new `RootArgs` class, which now has `parseCmdline` instead of `Args`. The idea is that we have many argument parsers from subcommands and what-not, but only one root args that owns the other per actual parsing invocation. The state that was global is now part of the root args instead. This did, admittedly, add a bunch of new code. And I do feel bad about that. So I went and added a lot of API docs to try to at least make the current state of things clear to the next person. -- This is needed for RFC 134 (tracking issue #7868). It was very hard to modularize `Installable` parsing when there were two completion arguments. I wouldn't go as far as to say it is *easy* now, but at least it is less hard (and the completions test finally passed). Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
2023-10-23 16:03:11 +03:00
std::vector<FlakeRef> getFlakeRefsForCompletion() override
2020-06-08 17:20:00 +03:00
{
Overhaul completions, redo #6693 (#8131) As I complained in https://github.com/NixOS/nix/pull/6784#issuecomment-1421777030 (a comment on the wrong PR, sorry again!), #6693 introduced a second completions mechanism to fix a bug. Having two completion mechanisms isn't so nice. As @thufschmitt also pointed out, it was a bummer to go from `FlakeRef` to `std::string` when collecting flake refs. Now it is `FlakeRefs` again. The underlying issue that sought to work around was that completion of arguments not at the end can still benefit from the information from latter arguments. To fix this better, we rip out that change and simply defer all completion processing until after all the (regular, already-complete) arguments have been passed. In addition, I noticed the original completion logic used some global variables. I do not like global variables, because even if they save lines of code, they also obfuscate the architecture of the code. I got rid of them moved them to a new `RootArgs` class, which now has `parseCmdline` instead of `Args`. The idea is that we have many argument parsers from subcommands and what-not, but only one root args that owns the other per actual parsing invocation. The state that was global is now part of the root args instead. This did, admittedly, add a bunch of new code. And I do feel bad about that. So I went and added a lot of API docs to try to at least make the current state of things clear to the next person. -- This is needed for RFC 134 (tracking issue #7868). It was very hard to modularize `Installable` parsing when there were two completion arguments. I wouldn't go as far as to say it is *easy* now, but at least it is less hard (and the completions test finally passed). Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
2023-10-23 16:03:11 +03:00
return {
// Like getFlakeRef but with expandTilde calld first
parseFlakeRef(expandTilde(flakeUrl), absPath("."))
};
2020-06-08 17:20:00 +03:00
}
};
struct CmdFlakeUpdate : FlakeCommand
{
public:
std::string description() override
{
return "update flake lock file";
}
CmdFlakeUpdate()
{
expectedArgs.clear();
addFlag({
.longName="flake",
.description="The flake to operate on. Default is the current directory.",
.labels={"flake-url"},
.handler={&flakeUrl},
.completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) {
completeFlakeRef(completions, getStore(), prefix);
}}
});
expectArgs({
.label="inputs",
.optional=true,
.handler={[&](std::vector<std::string> inputsToUpdate){
for (auto inputToUpdate : inputsToUpdate) {
InputPath inputPath;
try {
inputPath = flake::parseInputPath(inputToUpdate);
} catch (Error & e) {
warn("Invalid flake input '%s'. To update a specific flake, use 'nix flake update --flake %s' instead.", inputToUpdate, inputToUpdate);
throw e;
}
if (lockFlags.inputUpdates.contains(inputPath))
warn("Input '%s' was specified multiple times. You may have done this by accident.");
lockFlags.inputUpdates.insert(inputPath);
}
}},
.completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) {
completeFlakeInputPath(completions, getEvalState(), getFlakeRefsForCompletion(), prefix);
}}
});
/* Remove flags that don't make sense. */
removeFlag("no-update-lock-file");
removeFlag("no-write-lock-file");
}
2020-12-23 14:19:53 +02:00
std::string doc() override
{
return
#include "flake-update.md"
;
}
void run(nix::ref<nix::Store> store) override
{
settings.tarballTtl = 0;
auto updateAll = lockFlags.inputUpdates.empty();
lockFlags.recreateLockFile = updateAll;
lockFlags.writeLockFile = true;
lockFlags.applyNixConfig = true;
lockFlake();
}
};
struct CmdFlakeLock : FlakeCommand
{
std::string description() override
{
return "create missing lock file entries";
}
CmdFlakeLock()
{
/* Remove flags that don't make sense. */
removeFlag("no-write-lock-file");
}
std::string doc() override
{
return
#include "flake-lock.md"
;
}
void run(nix::ref<nix::Store> store) override
{
2020-02-01 13:26:05 +02:00
settings.tarballTtl = 0;
lockFlags.writeLockFile = true;
lockFlags.applyNixConfig = true;
lockFlake();
}
};
static void enumerateOutputs(EvalState & state, Value & vFlake,
std::function<void(const std::string & name, Value & vProvide, const PosIdx pos)> callback)
{
2022-01-21 17:43:16 +02:00
auto pos = vFlake.determinePos(noPos);
state.forceAttrs(vFlake, pos, "while evaluating a flake to get its outputs");
auto aOutputs = vFlake.attrs()->get(state.symbols.create("outputs"));
assert(aOutputs);
state.forceAttrs(*aOutputs->value, pos, "while evaluating the outputs of a flake");
auto sHydraJobs = state.symbols.create("hydraJobs");
/* Hack: ensure that hydraJobs is evaluated before anything
else. This way we can disable IFD for hydraJobs and then enable
it for other outputs. */
if (auto attr = aOutputs->value->attrs()->get(sHydraJobs))
callback(state.symbols[attr->name], *attr->value, attr->pos);
for (auto & attr : *aOutputs->value->attrs()) {
if (attr.name != sHydraJobs)
callback(state.symbols[attr.name], *attr.value, attr.pos);
}
}
struct CmdFlakeMetadata : FlakeCommand, MixJSON
2019-02-21 07:53:01 +02:00
{
std::string description() override
{
return "show flake metadata";
2019-02-21 07:53:01 +02:00
}
2020-12-23 14:19:53 +02:00
std::string doc() override
{
return
#include "flake-metadata.md"
2020-12-23 14:19:53 +02:00
;
}
2019-02-21 07:53:01 +02:00
void run(nix::ref<nix::Store> store) override
{
auto lockedFlake = lockFlake();
auto & flake = lockedFlake.flake;
// Currently, all flakes are in the Nix store via the rootFS accessor.
auto storePath = store->printStorePath(store->toStorePath(flake.path.path.abs()).first);
if (json) {
nlohmann::json j;
if (flake.description)
j["description"] = *flake.description;
j["originalUrl"] = flake.originalRef.to_string();
j["original"] = fetchers::attrsToJSON(flake.originalRef.toAttrs());
j["resolvedUrl"] = flake.resolvedRef.to_string();
j["resolved"] = fetchers::attrsToJSON(flake.resolvedRef.toAttrs());
j["url"] = flake.lockedRef.to_string(); // FIXME: rename to lockedUrl
// "locked" is a misnomer - this is the result of the
// attempt to lock.
j["locked"] = fetchers::attrsToJSON(flake.lockedRef.toAttrs());
if (auto rev = flake.lockedRef.input.getRev())
j["revision"] = rev->to_string(HashFormat::Base16, false);
if (auto dirtyRev = fetchers::maybeGetStrAttr(flake.lockedRef.toAttrs(), "dirtyRev"))
j["dirtyRevision"] = *dirtyRev;
if (auto revCount = flake.lockedRef.input.getRevCount())
j["revCount"] = *revCount;
if (auto lastModified = flake.lockedRef.input.getLastModified())
j["lastModified"] = *lastModified;
j["path"] = storePath;
j["locks"] = lockedFlake.lockFile.toJSON().first;
if (auto fingerprint = lockedFlake.getFingerprint(store))
j["fingerprint"] = fingerprint->to_string(HashFormat::Base16, false);
logger->cout("%s", j.dump());
} else {
logger->cout(
ANSI_BOLD "Resolved URL:" ANSI_NORMAL " %s",
flake.resolvedRef.to_string());
if (flake.lockedRef.input.isLocked())
logger->cout(
ANSI_BOLD "Locked URL:" ANSI_NORMAL " %s",
flake.lockedRef.to_string());
if (flake.description)
logger->cout(
ANSI_BOLD "Description:" ANSI_NORMAL " %s",
*flake.description);
logger->cout(
ANSI_BOLD "Path:" ANSI_NORMAL " %s",
storePath);
if (auto rev = flake.lockedRef.input.getRev())
logger->cout(
ANSI_BOLD "Revision:" ANSI_NORMAL " %s",
rev->to_string(HashFormat::Base16, false));
if (auto dirtyRev = fetchers::maybeGetStrAttr(flake.lockedRef.toAttrs(), "dirtyRev"))
logger->cout(
ANSI_BOLD "Revision:" ANSI_NORMAL " %s",
*dirtyRev);
if (auto revCount = flake.lockedRef.input.getRevCount())
logger->cout(
ANSI_BOLD "Revisions:" ANSI_NORMAL " %s",
*revCount);
if (auto lastModified = flake.lockedRef.input.getLastModified())
logger->cout(
ANSI_BOLD "Last modified:" ANSI_NORMAL " %s",
std::put_time(std::localtime(&*lastModified), "%F %T"));
if (auto fingerprint = lockedFlake.getFingerprint(store))
logger->cout(
ANSI_BOLD "Fingerprint:" ANSI_NORMAL " %s",
fingerprint->to_string(HashFormat::Base16, false));
if (!lockedFlake.lockFile.root->inputs.empty())
logger->cout(ANSI_BOLD "Inputs:" ANSI_NORMAL);
std::set<ref<Node>> visited;
std::function<void(const Node & node, const std::string & prefix)> recurse;
recurse = [&](const Node & node, const std::string & prefix)
{
for (const auto & [i, input] : enumerate(node.inputs)) {
bool last = i + 1 == node.inputs.size();
if (auto lockedNode = std::get_if<0>(&input.second)) {
std::string lastModifiedStr = "";
if (auto lastModified = (*lockedNode)->lockedRef.input.getLastModified())
lastModifiedStr = fmt(" (%s)", std::put_time(std::gmtime(&*lastModified), "%F %T"));
logger->cout("%s" ANSI_BOLD "%s" ANSI_NORMAL ": %s%s",
prefix + (last ? treeLast : treeConn), input.first,
(*lockedNode)->lockedRef,
lastModifiedStr);
bool firstVisit = visited.insert(*lockedNode).second;
if (firstVisit) recurse(**lockedNode, prefix + (last ? treeNull : treeLine));
} else if (auto follows = std::get_if<1>(&input.second)) {
logger->cout("%s" ANSI_BOLD "%s" ANSI_NORMAL " follows input '%s'",
prefix + (last ? treeLast : treeConn), input.first,
printInputPath(*follows));
}
}
};
visited.insert(lockedFlake.lockFile.root);
recurse(*lockedFlake.lockFile.root, "");
}
}
};
struct CmdFlakeInfo : CmdFlakeMetadata
{
void run(nix::ref<nix::Store> store) override
{
warn("'nix flake info' is a deprecated alias for 'nix flake metadata'");
CmdFlakeMetadata::run(store);
}
};
struct CmdFlakeCheck : FlakeCommand
{
bool build = true;
bool checkAllSystems = false;
CmdFlakeCheck()
{
addFlag({
.longName = "no-build",
.description = "Do not build checks.",
.handler = {&build, false}
});
addFlag({
.longName = "all-systems",
.description = "Check the outputs for all systems.",
.handler = {&checkAllSystems, true}
});
}
std::string description() override
{
return "check whether the flake evaluates and run its tests";
}
2020-12-23 14:19:53 +02:00
std::string doc() override
{
return
#include "flake-check.md"
;
}
void run(nix::ref<nix::Store> store) override
{
if (!build) {
settings.readOnlyMode = true;
evalSettings.enableImportFromDerivation.setDefault(false);
}
auto state = getEvalState();
lockFlags.applyNixConfig = true;
auto flake = lockFlake();
auto localSystem = std::string(settings.thisSystem.get());
bool hasErrors = false;
auto reportError = [&](const Error & e) {
try {
throw e;
} catch (Error & e) {
if (settings.keepGoing) {
ignoreException();
hasErrors = true;
}
else
throw;
}
};
std::set<std::string> omittedSystems;
// FIXME: rewrite to use EvalCache.
auto resolve = [&] (PosIdx p) {
return state->positions[p];
};
auto argHasName = [&] (Symbol arg, std::string_view expected) {
std::string_view name = state->symbols[arg];
return
name == expected
|| name == "_"
|| (hasPrefix(name, "_") && name.substr(1) == expected);
};
auto checkSystemName = [&](const std::string & system, const PosIdx pos) {
// FIXME: what's the format of "system"?
if (system.find('-') == std::string::npos)
reportError(Error("'%s' is not a valid system type, at %s", system, resolve(pos)));
};
auto checkSystemType = [&](const std::string & system, const PosIdx pos) {
if (!checkAllSystems && system != localSystem) {
omittedSystems.insert(system);
return false;
} else {
return true;
}
};
auto checkDerivation = [&](const std::string & attrPath, Value & v, const PosIdx pos) -> std::optional<StorePath> {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking derivation %s", attrPath));
auto packageInfo = getDerivation(*state, v, false);
if (!packageInfo)
throw Error("flake attribute '%s' is not a derivation", attrPath);
else {
// FIXME: check meta attributes
auto storePath = packageInfo->queryDrvPath();
if (storePath) {
logger->log(lvlInfo,
fmt("derivation evaluated to %s",
store->printStorePath(storePath.value())));
}
return storePath;
}
} catch (Error & e) {
2024-02-04 06:35:19 +02:00
e.addTrace(resolve(pos), HintFmt("while checking the derivation '%s'", attrPath));
reportError(e);
}
return std::nullopt;
};
2021-04-05 16:48:18 +03:00
std::vector<DerivedPath> drvPaths;
auto checkApp = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
2019-06-17 18:59:57 +03:00
try {
#if 0
// FIXME
2019-06-17 18:59:57 +03:00
auto app = App(*state, v);
for (auto & i : app.context) {
auto [drvPathS, outputName] = NixStringContextElem::parse(i);
store->parseStorePath(drvPathS);
2019-06-17 18:59:57 +03:00
}
#endif
2019-06-17 18:59:57 +03:00
} catch (Error & e) {
2024-02-04 06:35:19 +02:00
e.addTrace(resolve(pos), HintFmt("while checking the app definition '%s'", attrPath));
reportError(e);
2019-06-17 18:59:57 +03:00
}
};
auto checkOverlay = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
2019-09-10 15:52:22 +03:00
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking overlay '%s'", attrPath));
2019-09-10 18:39:55 +03:00
state->forceValue(v, pos);
nix flake check: improve error message if overlay is not a lambda (#8582) * nix flake check: improve error message if overlay is not a lambda Suppose you have an overlay like this { inputs = { /* ... */ }; outputs = { flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: { overlays.default = final: prev: { }; }); } then `nix flake check` (correctly) fails because `overlays` are supposed to have the structure `overlays.<name> = final: prev: exp`. However, the error-message is a little bit counter-intuitive: error: overlay does not take an argument named 'final' While one might guess where the error actually comes from because the trace above says `… while checking the overlay 'overlays.x86_64-linux'` this is still pretty confusing because it complains about an argument not being named `final` even though that's evidently the case. With this change, the error-message actually makes it clear what's wrong: [ma27@carsten:~/Projects/nix/tmp]$ nix flake check --extra-experimental-features 'nix-command flakes' path:$(pwd) error: … while checking flake output 'overlays' at /nix/store/clgblnxx003hyrq8qkz5ab6kgqkck6qc-source/flake.nix:4:5: 3| outputs = { ... }: { 4| overlays.x86_64-linux.snens = final: prev: { | ^ 5| kek = throw "snens"; … while checking the overlay 'overlays.x86_64-linux' at /nix/store/clgblnxx003hyrq8qkz5ab6kgqkck6qc-source/flake.nix:4:5: 3| outputs = { ... }: { 4| overlays.x86_64-linux.snens = final: prev: { | ^ 5| kek = throw "snens"; error: overlay is not a lambda, but a set instead
2023-06-27 15:58:29 +03:00
if (!v.isLambda()) {
throw Error("overlay is not a function, but %s instead", showType(v));
}
if (v.payload.lambda.fun->hasFormals()
|| !argHasName(v.payload.lambda.fun->arg, "final"))
2019-09-10 15:52:22 +03:00
throw Error("overlay does not take an argument named 'final'");
// FIXME: if we have a 'nixpkgs' input, use it to
// evaluate the overlay.
} catch (Error & e) {
2024-02-04 06:35:19 +02:00
e.addTrace(resolve(pos), HintFmt("while checking the overlay '%s'", attrPath));
reportError(e);
}
};
auto checkModule = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking NixOS module '%s'", attrPath));
2019-09-10 18:39:55 +03:00
state->forceValue(v, pos);
} catch (Error & e) {
2024-02-04 06:35:19 +02:00
e.addTrace(resolve(pos), HintFmt("while checking the NixOS module '%s'", attrPath));
reportError(e);
2019-09-10 15:52:22 +03:00
}
};
std::function<void(const std::string & attrPath, Value & v, const PosIdx pos)> checkHydraJobs;
2019-09-10 18:39:55 +03:00
checkHydraJobs = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
2019-09-10 18:39:55 +03:00
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking Hydra job '%s'", attrPath));
state->forceAttrs(v, pos, "");
2019-09-10 18:39:55 +03:00
if (state->isDerivation(v))
throw Error("jobset should not be a derivation at top-level");
for (auto & attr : *v.attrs()) {
state->forceAttrs(*attr.value, attr.pos, "");
auto attrPath2 = concatStrings(attrPath, ".", state->symbols[attr.name]);
if (state->isDerivation(*attr.value)) {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking Hydra job '%s'", attrPath2));
checkDerivation(attrPath2, *attr.value, attr.pos);
} else
checkHydraJobs(attrPath2, *attr.value, attr.pos);
2019-09-10 18:39:55 +03:00
}
} catch (Error & e) {
2024-02-04 06:35:19 +02:00
e.addTrace(resolve(pos), HintFmt("while checking the Hydra jobset '%s'", attrPath));
reportError(e);
2019-09-10 18:39:55 +03:00
}
};
auto checkNixOSConfiguration = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking NixOS configuration '%s'", attrPath));
Bindings & bindings(*state->allocBindings(0));
2020-02-07 15:08:24 +02:00
auto vToplevel = findAlongAttrPath(*state, "config.system.build.toplevel", bindings, v).first;
state->forceValue(*vToplevel, pos);
if (!state->isDerivation(*vToplevel))
throw Error("attribute 'config.system.build.toplevel' is not a derivation");
} catch (Error & e) {
2024-02-04 06:35:19 +02:00
e.addTrace(resolve(pos), HintFmt("while checking the NixOS configuration '%s'", attrPath));
reportError(e);
}
};
auto checkTemplate = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking template '%s'", attrPath));
state->forceAttrs(v, pos, "");
if (auto attr = v.attrs()->get(state->symbols.create("path"))) {
if (attr->name == state->symbols.create("path")) {
Use `std::set<StringContextElem>` not `PathSet` for string contexts Motivation `PathSet` is not correct because string contexts have other forms (`Built` and `DrvDeep`) that are not rendered as plain store paths. Instead of wrongly using `PathSet`, or "stringly typed" using `StringSet`, use `std::std<StringContextElem>`. ----- In support of this change, `NixStringContext` is now defined as `std::std<StringContextElem>` not `std:vector<StringContextElem>`. The old definition was just used by a `getContext` method which was only used by the eval cache. It can be deleted altogether since the types are now unified and the preexisting `copyContext` function already suffices. Summarizing the previous paragraph: Old: - `value/context.hh`: `NixStringContext = std::vector<StringContextElem>` - `value.hh`: `NixStringContext Value::getContext(...)` - `value.hh`: `copyContext(...)` New: - `value/context.hh`: `NixStringContext = std::set<StringContextElem>` - `value.hh`: `copyContext(...)` ---- The string representation of string context elements no longer contains the store dir. The diff of `src/libexpr/tests/value/context.cc` should make clear what the new representation is, so we recommend reviewing that file first. This was done for two reasons: Less API churn: `Value::mkString` and friends did not take a `Store` before. But if `NixStringContextElem::{parse, to_string}` *do* take a store (as they did before), then we cannot have the `Value` functions use them (in order to work with the fully-structured `NixStringContext`) without adding that argument. That would have been a lot of churn of threading the store, and this diff is already large enough, so the easier and less invasive thing to do was simply make the element `parse` and `to_string` functions not take the `Store` reference, and the easiest way to do that was to simply drop the store dir. Space usage: Dropping the `/nix/store/` (or similar) from the internal representation will safe space in the heap of the Nix programming being interpreted. If the heap contains many strings with non-trivial contexts, the saving could add up to something significant. ---- The eval cache version is bumped. The eval cache serialization uses `NixStringContextElem::{parse, to_string}`, and since those functions are changed per the above, that means the on-disk representation is also changed. This is simply done by changing the name of the used for the eval cache from `eval-cache-v4` to eval-cache-v5`. ---- To avoid some duplication `EvalCache::mkPathString` is added to abstract over the simple case of turning a store path to a string with just that string in the context. Context This PR picks up where #7543 left off. That one introduced the fully structured `NixStringContextElem` data type, but kept `PathSet context` as an awkward middle ground between internal `char[][]` interpreter heap string contexts and `NixStringContext` fully parsed string contexts. The infelicity of `PathSet context` was specifically called out during Nix team group review, but it was agreeing that fixing it could be left as future work. This is that future work. A possible follow-up step would be to get rid of the `char[][]` evaluator heap representation, too, but it is not yet clear how to do that. To use `NixStringContextElem` there we would need to get the STL containers to GC pointers in the GC build, and I am not sure how to do that. ---- PR #7543 effectively is writing the inverse of a `mkPathString`, `mkOutputString`, and one more such function for the `DrvDeep` case. I would like that PR to have property tests ensuring it is actually the inverse as expected. This PR sets things up nicely so that reworking that PR to be in that more elegant and better tested way is possible. Co-authored-by: Théophane Hufschmitt <7226587+thufschmitt@users.noreply.github.com>
2023-01-29 03:31:10 +02:00
NixStringContext context;
auto path = state->coerceToPath(attr->pos, *attr->value, context, "");
if (!path.pathExists())
throw Error("template '%s' refers to a non-existent path '%s'", attrPath, path);
// TODO: recursively check the flake in 'path'.
}
} else
throw Error("template '%s' lacks attribute 'path'", attrPath);
if (auto attr = v.attrs()->get(state->symbols.create("description")))
state->forceStringNoCtx(*attr->value, attr->pos, "");
else
throw Error("template '%s' lacks attribute 'description'", attrPath);
for (auto & attr : *v.attrs()) {
std::string_view name(state->symbols[attr.name]);
if (name != "path" && name != "description" && name != "welcomeText")
throw Error("template '%s' has unsupported attribute '%s'", attrPath, name);
}
} catch (Error & e) {
2024-02-04 06:35:19 +02:00
e.addTrace(resolve(pos), HintFmt("while checking the template '%s'", attrPath));
reportError(e);
}
};
auto checkBundler = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking bundler '%s'", attrPath));
state->forceValue(v, pos);
if (!v.isLambda())
2020-07-30 23:03:57 +03:00
throw Error("bundler must be a function");
// TODO: check types of inputs/outputs?
} catch (Error & e) {
2024-02-04 06:35:19 +02:00
e.addTrace(resolve(pos), HintFmt("while checking the template '%s'", attrPath));
reportError(e);
}
};
{
Activity act(*logger, lvlInfo, actUnknown, "evaluating flake");
auto vFlake = state->allocValue();
flake::callFlake(*state, flake, *vFlake);
enumerateOutputs(*state,
*vFlake,
[&](const std::string & name, Value & vOutput, const PosIdx pos) {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking flake output '%s'", name));
try {
evalSettings.enableImportFromDerivation.setDefault(name != "hydraJobs");
2019-09-10 18:39:55 +03:00
state->forceValue(vOutput, pos);
std::string_view replacement =
name == "defaultPackage" ? "packages.<system>.default" :
2022-05-21 15:41:24 +03:00
name == "defaultApp" ? "apps.<system>.default" :
name == "defaultTemplate" ? "templates.default" :
name == "defaultBundler" ? "bundlers.<system>.default" :
name == "overlay" ? "overlays.default" :
name == "devShell" ? "devShells.<system>.default" :
name == "nixosModule" ? "nixosModules.default" :
"";
if (replacement != "")
warn("flake output attribute '%s' is deprecated; use '%s' instead", name, replacement);
if (name == "checks") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs()) {
auto drvPath = checkDerivation(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
*attr2.value, attr2.pos);
if (drvPath && attr_name == settings.thisSystem.get()) {
drvPaths.push_back(DerivedPath::Built {
.drvPath = makeConstantStorePathRef(*drvPath),
.outputs = OutputsSpec::All { },
});
}
}
}
}
}
2022-03-11 15:57:28 +02:00
else if (name == "formatter") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
checkApp(
fmt("%s.%s", name, attr_name),
*attr.value, attr.pos);
};
2022-03-11 15:57:28 +02:00
}
}
else if (name == "packages" || name == "devShells") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs())
checkDerivation(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
*attr2.value, attr2.pos);
};
}
}
2019-06-17 18:59:57 +03:00
else if (name == "apps") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs())
checkApp(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
*attr2.value, attr2.pos);
};
}
2019-06-17 18:59:57 +03:00
}
else if (name == "defaultPackage" || name == "devShell") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
checkDerivation(
fmt("%s.%s", name, attr_name),
*attr.value, attr.pos);
};
}
}
else if (name == "defaultApp") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos) ) {
checkApp(
fmt("%s.%s", name, attr_name),
*attr.value, attr.pos);
};
}
}
2019-06-17 18:59:57 +03:00
else if (name == "legacyPackages") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
checkSystemName(state->symbols[attr.name], attr.pos);
checkSystemType(state->symbols[attr.name], attr.pos);
// FIXME: do getDerivations?
}
}
2019-06-18 10:45:14 +03:00
2019-09-10 15:52:22 +03:00
else if (name == "overlay")
checkOverlay(name, vOutput, pos);
else if (name == "overlays") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs())
checkOverlay(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
}
else if (name == "nixosModule")
checkModule(name, vOutput, pos);
else if (name == "nixosModules") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs())
checkModule(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
}
2019-09-10 15:52:22 +03:00
else if (name == "nixosConfigurations") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs())
checkNixOSConfiguration(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
}
2019-09-10 18:39:55 +03:00
else if (name == "hydraJobs")
checkHydraJobs(name, vOutput, pos);
else if (name == "defaultTemplate")
checkTemplate(name, vOutput, pos);
else if (name == "templates") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs())
checkTemplate(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
}
else if (name == "defaultBundler") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
checkBundler(
fmt("%s.%s", name, attr_name),
*attr.value, attr.pos);
};
}
}
2020-07-30 23:03:57 +03:00
else if (name == "bundlers") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs()) {
checkBundler(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
*attr2.value, attr2.pos);
}
};
}
}
else if (
name == "lib"
|| name == "darwinConfigurations"
|| name == "darwinModules"
|| name == "flakeModule"
|| name == "flakeModules"
|| name == "herculesCI"
|| name == "homeConfigurations"
|| name == "homeModule"
|| name == "homeModules"
|| name == "nixopsConfigurations"
)
// Known but unchecked community attribute
;
else
warn("unknown flake output '%s'", name);
} catch (Error & e) {
2024-02-04 06:35:19 +02:00
e.addTrace(resolve(pos), HintFmt("while checking flake output '%s'", name));
reportError(e);
}
});
}
2019-06-17 18:59:57 +03:00
if (build && !drvPaths.empty()) {
2023-09-01 23:11:58 +03:00
Activity act(*logger, lvlInfo, actUnknown,
fmt("running %d flake checks", drvPaths.size()));
store->buildPaths(drvPaths);
}
if (hasErrors)
throw Error("some errors were encountered during the evaluation");
if (!omittedSystems.empty()) {
warn(
"The check omitted these incompatible systems: %s\n"
"Use '--all-systems' to check all.",
concatStringsSep(", ", omittedSystems)
);
};
};
};
static Strings defaultTemplateAttrPathsPrefixes{"templates."};
static Strings defaultTemplateAttrPaths = {"templates.default", "defaultTemplate"};
struct CmdFlakeInitCommon : virtual Args, EvalCommand
{
std::string templateUrl = "templates";
Path destDir;
2020-06-05 15:09:12 +03:00
const LockFlags lockFlags{ .writeLockFile = false };
CmdFlakeInitCommon()
{
addFlag({
.longName = "template",
.shortName = 't',
.description = "The template to use.",
.labels = {"template"},
.handler = {&templateUrl},
Overhaul completions, redo #6693 (#8131) As I complained in https://github.com/NixOS/nix/pull/6784#issuecomment-1421777030 (a comment on the wrong PR, sorry again!), #6693 introduced a second completions mechanism to fix a bug. Having two completion mechanisms isn't so nice. As @thufschmitt also pointed out, it was a bummer to go from `FlakeRef` to `std::string` when collecting flake refs. Now it is `FlakeRefs` again. The underlying issue that sought to work around was that completion of arguments not at the end can still benefit from the information from latter arguments. To fix this better, we rip out that change and simply defer all completion processing until after all the (regular, already-complete) arguments have been passed. In addition, I noticed the original completion logic used some global variables. I do not like global variables, because even if they save lines of code, they also obfuscate the architecture of the code. I got rid of them moved them to a new `RootArgs` class, which now has `parseCmdline` instead of `Args`. The idea is that we have many argument parsers from subcommands and what-not, but only one root args that owns the other per actual parsing invocation. The state that was global is now part of the root args instead. This did, admittedly, add a bunch of new code. And I do feel bad about that. So I went and added a lot of API docs to try to at least make the current state of things clear to the next person. -- This is needed for RFC 134 (tracking issue #7868). It was very hard to modularize `Installable` parsing when there were two completion arguments. I wouldn't go as far as to say it is *easy* now, but at least it is less hard (and the completions test finally passed). Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
2023-10-23 16:03:11 +03:00
.completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) {
2020-06-05 15:09:12 +03:00
completeFlakeRefWithFragment(
Overhaul completions, redo #6693 (#8131) As I complained in https://github.com/NixOS/nix/pull/6784#issuecomment-1421777030 (a comment on the wrong PR, sorry again!), #6693 introduced a second completions mechanism to fix a bug. Having two completion mechanisms isn't so nice. As @thufschmitt also pointed out, it was a bummer to go from `FlakeRef` to `std::string` when collecting flake refs. Now it is `FlakeRefs` again. The underlying issue that sought to work around was that completion of arguments not at the end can still benefit from the information from latter arguments. To fix this better, we rip out that change and simply defer all completion processing until after all the (regular, already-complete) arguments have been passed. In addition, I noticed the original completion logic used some global variables. I do not like global variables, because even if they save lines of code, they also obfuscate the architecture of the code. I got rid of them moved them to a new `RootArgs` class, which now has `parseCmdline` instead of `Args`. The idea is that we have many argument parsers from subcommands and what-not, but only one root args that owns the other per actual parsing invocation. The state that was global is now part of the root args instead. This did, admittedly, add a bunch of new code. And I do feel bad about that. So I went and added a lot of API docs to try to at least make the current state of things clear to the next person. -- This is needed for RFC 134 (tracking issue #7868). It was very hard to modularize `Installable` parsing when there were two completion arguments. I wouldn't go as far as to say it is *easy* now, but at least it is less hard (and the completions test finally passed). Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
2023-10-23 16:03:11 +03:00
completions,
2020-06-05 15:09:12 +03:00
getEvalState(),
lockFlags,
defaultTemplateAttrPathsPrefixes,
defaultTemplateAttrPaths,
2020-06-05 15:09:12 +03:00
prefix);
}}
});
}
void run(nix::ref<nix::Store> store) override
{
auto flakeDir = absPath(destDir);
auto evalState = getEvalState();
auto [templateFlakeRef, templateName] = parseFlakeRefWithFragment(templateUrl, absPath("."));
auto installable = InstallableFlake(nullptr,
evalState, std::move(templateFlakeRef), templateName, ExtendedOutputsSpec::Default(),
defaultTemplateAttrPaths,
defaultTemplateAttrPathsPrefixes,
lockFlags);
auto cursor = installable.getCursor(*evalState);
auto templateDirAttr = cursor->getAttr("path");
auto templateDir = templateDirAttr->getString();
2022-03-07 21:07:43 +02:00
if (!store->isInStore(templateDir))
evalState->error<TypeError>(
2022-03-07 21:07:43 +02:00
"'%s' was not found in the Nix store\n"
"If you've set '%s' to a string, try using a path instead.",
templateDir, templateDirAttr->getAttrPathStr()).debugThrow();
std::vector<Path> changedFiles;
std::vector<Path> conflictedFiles;
std::function<void(const Path & from, const Path & to)> copyDir;
copyDir = [&](const Path & from, const Path & to)
{
createDirs(to);
for (auto & entry : std::filesystem::directory_iterator{from}) {
checkInterrupt();
auto from2 = entry.path().string();
auto to2 = to + "/" + entry.path().filename().string();
auto st = lstat(from2);
if (S_ISDIR(st.st_mode))
copyDir(from2, to2);
else if (S_ISREG(st.st_mode)) {
auto contents = readFile(from2);
if (pathExists(to2)) {
auto contents2 = readFile(to2);
if (contents != contents2) {
printError("refusing to overwrite existing file '%s'\n please merge it manually with '%s'", to2, from2);
conflictedFiles.push_back(to2);
} else {
notice("skipping identical file: %s", from2);
}
continue;
} else
writeFile(to2, contents);
}
else if (S_ISLNK(st.st_mode)) {
auto target = readLink(from2);
if (pathExists(to2)) {
if (readLink(to2) != target) {
printError("refusing to overwrite existing file '%s'\n please merge it manually with '%s'", to2, from2);
conflictedFiles.push_back(to2);
} else {
notice("skipping identical file: %s", from2);
}
continue;
} else
createSymlink(target, to2);
}
else
throw Error("file '%s' has unsupported type", from2);
changedFiles.push_back(to2);
notice("wrote: %s", to2);
}
};
copyDir(templateDir, flakeDir);
if (!changedFiles.empty() && pathExists(flakeDir + "/.git")) {
2020-06-23 17:25:32 +03:00
Strings args = { "-C", flakeDir, "add", "--intent-to-add", "--force", "--" };
for (auto & s : changedFiles) args.push_back(s);
runProgram("git", true, args);
}
auto welcomeText = cursor->maybeGetAttr("welcomeText");
if (welcomeText) {
notice("\n");
notice(renderMarkdownToTerminal(welcomeText->getString()));
}
if (!conflictedFiles.empty())
throw Error("Encountered %d conflicts - see above", conflictedFiles.size());
}
};
struct CmdFlakeInit : CmdFlakeInitCommon
{
std::string description() override
{
return "create a flake in the current directory from a template";
}
2020-12-23 14:19:53 +02:00
std::string doc() override
{
return
#include "flake-init.md"
;
}
CmdFlakeInit()
{
destDir = ".";
}
};
struct CmdFlakeNew : CmdFlakeInitCommon
{
std::string description() override
{
return "create a flake in the specified directory from a template";
}
2020-12-23 14:19:53 +02:00
std::string doc() override
{
return
#include "flake-new.md"
;
}
CmdFlakeNew()
{
expectArgs({
.label = "dest-dir",
.handler = {&destDir},
.completer = completePath
});
}
};
struct CmdFlakeClone : FlakeCommand
2019-03-21 10:30:16 +02:00
{
Path destDir;
2019-03-21 10:30:16 +02:00
std::string description() override
{
return "clone flake repository";
}
2020-12-23 14:19:53 +02:00
std::string doc() override
{
return
#include "flake-clone.md"
;
}
2019-03-21 10:30:16 +02:00
CmdFlakeClone()
{
addFlag({
.longName = "dest",
.shortName = 'f',
.description = "Clone the flake to path *dest*.",
.labels = {"path"},
.handler = {&destDir}
});
2019-03-21 10:30:16 +02:00
}
void run(nix::ref<nix::Store> store) override
{
if (destDir.empty())
throw Error("missing flag '--dest'");
2019-03-21 10:30:16 +02:00
getFlakeRef().resolve(store).input.clone(destDir);
2019-03-21 10:30:16 +02:00
}
};
struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun
{
std::string dstUri;
CmdFlakeArchive()
{
addFlag({
.longName = "to",
.description = "URI of the destination Nix store",
.labels = {"store-uri"},
.handler = {&dstUri}
});
}
std::string description() override
{
return "copy a flake and all its inputs to a store";
}
2020-12-23 14:19:53 +02:00
std::string doc() override
{
return
#include "flake-archive.md"
;
}
void run(nix::ref<nix::Store> store) override
{
auto flake = lockFlake();
StorePathSet sources;
auto storePath = store->toStorePath(flake.flake.path.path.abs()).first;
sources.insert(storePath);
// FIXME: use graph output, handle cycles.
std::function<nlohmann::json(const Node & node)> traverse;
traverse = [&](const Node & node)
{
nlohmann::json jsonObj2 = json ? json::object() : nlohmann::json(nullptr);
for (auto & [inputName, input] : node.inputs) {
if (auto inputNode = std::get_if<0>(&input)) {
auto storePath =
dryRun
? (*inputNode)->lockedRef.input.computeStorePath(*store)
: (*inputNode)->lockedRef.input.fetchToStore(store).first;
if (json) {
auto& jsonObj3 = jsonObj2[inputName];
jsonObj3["path"] = store->printStorePath(storePath);
sources.insert(std::move(storePath));
jsonObj3["inputs"] = traverse(**inputNode);
} else {
sources.insert(std::move(storePath));
traverse(**inputNode);
}
}
}
return jsonObj2;
};
if (json) {
nlohmann::json jsonRoot = {
{"path", store->printStorePath(storePath)},
{"inputs", traverse(*flake.lockFile.root)},
};
logger->cout("%s", jsonRoot);
} else {
traverse(*flake.lockFile.root);
}
if (!dryRun && !dstUri.empty()) {
ref<Store> dstStore = dstUri.empty() ? openStore() : openStore(dstUri);
2021-07-19 13:01:06 +03:00
copyPaths(*store, *dstStore, sources);
}
}
};
struct CmdFlakeShow : FlakeCommand, MixJSON
2020-04-16 16:36:15 +03:00
{
bool showLegacy = false;
2022-09-07 21:28:30 +03:00
bool showAllSystems = false;
2020-04-16 16:36:15 +03:00
CmdFlakeShow()
{
addFlag({
.longName = "legacy",
.description = "Show the contents of the `legacyPackages` output.",
.handler = {&showLegacy, true}
});
addFlag({
2022-09-07 21:28:30 +03:00
.longName = "all-systems",
.description = "Show the contents of outputs for all systems.",
.handler = {&showAllSystems, true}
});
2020-04-16 16:36:15 +03:00
}
std::string description() override
{
return "show the outputs provided by a flake";
}
2020-12-23 14:19:53 +02:00
std::string doc() override
{
return
#include "flake-show.md"
;
}
2020-04-16 16:36:15 +03:00
void run(nix::ref<nix::Store> store) override
{
evalSettings.enableImportFromDerivation.setDefault(false);
2020-04-16 16:36:15 +03:00
auto state = getEvalState();
auto flake = std::make_shared<LockedFlake>(lockFlake());
auto localSystem = std::string(settings.thisSystem.get());
2020-04-16 16:36:15 +03:00
std::function<bool(
eval_cache::AttrCursor & visitor,
const std::vector<Symbol> &attrPath,
const Symbol &attr)> hasContent;
// For frameworks it's important that structures are as lazy as possible
// to prevent infinite recursions, performance issues and errors that
// aren't related to the thing to evaluate. As a consequence, they have
// to emit more attributes than strictly (sic) necessary.
// However, these attributes with empty values are not useful to the user
// so we omit them.
hasContent = [&](
eval_cache::AttrCursor & visitor,
const std::vector<Symbol> &attrPath,
const Symbol &attr) -> bool
{
auto attrPath2(attrPath);
attrPath2.push_back(attr);
auto attrPathS = state->symbols.resolve(attrPath2);
const auto & attrName = state->symbols[attr];
auto visitor2 = visitor.getAttr(attrName);
try {
if ((attrPathS[0] == "apps"
|| attrPathS[0] == "checks"
|| attrPathS[0] == "devShells"
|| attrPathS[0] == "legacyPackages"
|| attrPathS[0] == "packages")
&& (attrPathS.size() == 1 || attrPathS.size() == 2)) {
for (const auto &subAttr : visitor2->getAttrs()) {
if (hasContent(*visitor2, attrPath2, subAttr)) {
return true;
}
}
return false;
}
if ((attrPathS.size() == 1)
&& (attrPathS[0] == "formatter"
|| attrPathS[0] == "nixosConfigurations"
|| attrPathS[0] == "nixosModules"
|| attrPathS[0] == "overlays"
)) {
for (const auto &subAttr : visitor2->getAttrs()) {
if (hasContent(*visitor2, attrPath2, subAttr)) {
return true;
}
}
return false;
}
// If we don't recognize it, it's probably content
return true;
} catch (EvalError & e) {
2024-04-19 20:42:05 +03:00
// Some attrs may contain errors, e.g. legacyPackages of
// nixpkgs. We still want to recurse into it, instead of
// skipping it at all.
return true;
}
};
std::function<nlohmann::json(
eval_cache::AttrCursor & visitor,
const std::vector<Symbol> & attrPath,
const std::string & headerPrefix,
const std::string & nextPrefix)> visit;
visit = [&](
eval_cache::AttrCursor & visitor,
const std::vector<Symbol> & attrPath,
const std::string & headerPrefix,
const std::string & nextPrefix)
-> nlohmann::json
{
auto j = nlohmann::json::object();
auto attrPathS = state->symbols.resolve(attrPath);
2020-04-16 16:36:15 +03:00
Activity act(*logger, lvlInfo, actUnknown,
fmt("evaluating '%s'", concatStringsSep(".", attrPathS)));
2020-04-16 16:36:15 +03:00
try {
auto recurse = [&]()
{
if (!json)
logger->cout("%s", headerPrefix);
std::vector<Symbol> attrs;
for (const auto &attr : visitor.getAttrs()) {
if (hasContent(visitor, attrPath, attr))
attrs.push_back(attr);
}
2020-04-16 16:36:15 +03:00
for (const auto & [i, attr] : enumerate(attrs)) {
const auto & attrName = state->symbols[attr];
2020-04-16 16:36:15 +03:00
bool last = i + 1 == attrs.size();
auto visitor2 = visitor.getAttr(attrName);
2020-04-16 16:36:15 +03:00
auto attrPath2(attrPath);
attrPath2.push_back(attr);
auto j2 = visit(*visitor2, attrPath2,
fmt(ANSI_GREEN "%s%s" ANSI_NORMAL ANSI_BOLD "%s" ANSI_NORMAL, nextPrefix, last ? treeLast : treeConn, attrName),
2020-04-16 16:36:15 +03:00
nextPrefix + (last ? treeNull : treeLine));
if (json) j.emplace(attrName, std::move(j2));
2020-04-16 16:36:15 +03:00
}
};
auto showDerivation = [&]()
{
auto name = visitor.getAttr(state->sName)->getString();
if (json) {
std::optional<std::string> description;
if (auto aMeta = visitor.maybeGetAttr(state->sMeta)) {
if (auto aDescription = aMeta->maybeGetAttr(state->sDescription))
description = aDescription->getString();
}
j.emplace("type", "derivation");
j.emplace("name", name);
if (description)
j.emplace("description", *description);
} else {
logger->cout("%s: %s '%s'",
headerPrefix,
attrPath.size() == 2 && attrPathS[0] == "devShell" ? "development environment" :
attrPath.size() >= 2 && attrPathS[0] == "devShells" ? "development environment" :
attrPath.size() == 3 && attrPathS[0] == "checks" ? "derivation" :
attrPath.size() >= 1 && attrPathS[0] == "hydraJobs" ? "derivation" :
"package",
name);
}
2020-04-16 16:36:15 +03:00
};
if (attrPath.size() == 0
|| (attrPath.size() == 1 && (
attrPathS[0] == "defaultPackage"
|| attrPathS[0] == "devShell"
|| attrPathS[0] == "formatter"
|| attrPathS[0] == "nixosConfigurations"
|| attrPathS[0] == "nixosModules"
|| attrPathS[0] == "defaultApp"
|| attrPathS[0] == "templates"
|| attrPathS[0] == "overlays"))
2020-04-16 16:36:15 +03:00
|| ((attrPath.size() == 1 || attrPath.size() == 2)
&& (attrPathS[0] == "checks"
|| attrPathS[0] == "packages"
|| attrPathS[0] == "devShells"
|| attrPathS[0] == "apps"))
2020-04-16 16:36:15 +03:00
)
{
recurse();
}
else if (
(attrPath.size() == 2 && (attrPathS[0] == "defaultPackage" || attrPathS[0] == "devShell" || attrPathS[0] == "formatter"))
|| (attrPath.size() == 3 && (attrPathS[0] == "checks" || attrPathS[0] == "packages" || attrPathS[0] == "devShells"))
2020-04-16 16:36:15 +03:00
)
{
2022-09-07 21:28:30 +03:00
if (!showAllSystems && std::string(attrPathS[1]) != localSystem) {
if (!json)
2022-09-07 21:28:30 +03:00
logger->cout(fmt("%s " ANSI_WARNING "omitted" ANSI_NORMAL " (use '--all-systems' to show)", headerPrefix));
else {
2022-09-07 21:28:30 +03:00
logger->warn(fmt("%s omitted (use '--all-systems' to show)", concatStringsSep(".", attrPathS)));
}
} else {
if (visitor.isDerivation())
showDerivation();
else
throw Error("expected a derivation");
}
2020-04-16 16:36:15 +03:00
}
else if (attrPath.size() > 0 && attrPathS[0] == "hydraJobs") {
2020-04-16 16:36:15 +03:00
if (visitor.isDerivation())
showDerivation();
else
recurse();
}
else if (attrPath.size() > 0 && attrPathS[0] == "legacyPackages") {
2020-04-16 16:36:15 +03:00
if (attrPath.size() == 1)
recurse();
else if (!showLegacy){
if (!json)
logger->cout(fmt("%s " ANSI_WARNING "omitted" ANSI_NORMAL " (use '--legacy' to show)", headerPrefix));
else {
logger->warn(fmt("%s omitted (use '--legacy' to show)", concatStringsSep(".", attrPathS)));
}
} else if (!showAllSystems && std::string(attrPathS[1]) != localSystem) {
if (!json)
logger->cout(fmt("%s " ANSI_WARNING "omitted" ANSI_NORMAL " (use '--all-systems' to show)", headerPrefix));
else {
logger->warn(fmt("%s omitted (use '--all-systems' to show)", concatStringsSep(".", attrPathS)));
}
} else {
2020-04-16 16:36:15 +03:00
if (visitor.isDerivation())
showDerivation();
else if (attrPath.size() <= 2)
// FIXME: handle recurseIntoAttrs
recurse();
}
}
2020-04-17 02:21:24 +03:00
else if (
(attrPath.size() == 2 && attrPathS[0] == "defaultApp") ||
(attrPath.size() == 3 && attrPathS[0] == "apps"))
2020-04-17 02:21:24 +03:00
{
auto aType = visitor.maybeGetAttr("type");
if (!aType || aType->getString() != "app")
state->error<EvalError>("not an app definition").debugThrow();
if (json) {
j.emplace("type", "app");
} else {
logger->cout("%s: app", headerPrefix);
}
2020-04-17 02:21:24 +03:00
}
else if (
(attrPath.size() == 1 && attrPathS[0] == "defaultTemplate") ||
(attrPath.size() == 2 && attrPathS[0] == "templates"))
{
auto description = visitor.getAttr("description")->getString();
if (json) {
j.emplace("type", "template");
j.emplace("description", description);
} else {
logger->cout("%s: template: " ANSI_BOLD "%s" ANSI_NORMAL, headerPrefix, description);
}
}
2020-04-16 16:36:15 +03:00
else {
auto [type, description] =
(attrPath.size() == 1 && attrPathS[0] == "overlay")
|| (attrPath.size() == 2 && attrPathS[0] == "overlays") ? std::make_pair("nixpkgs-overlay", "Nixpkgs overlay") :
attrPath.size() == 2 && attrPathS[0] == "nixosConfigurations" ? std::make_pair("nixos-configuration", "NixOS configuration") :
(attrPath.size() == 1 && attrPathS[0] == "nixosModule")
|| (attrPath.size() == 2 && attrPathS[0] == "nixosModules") ? std::make_pair("nixos-module", "NixOS module") :
std::make_pair("unknown", "unknown");
if (json) {
j.emplace("type", type);
} else {
logger->cout("%s: " ANSI_WARNING "%s" ANSI_NORMAL, headerPrefix, description);
}
2020-04-16 16:36:15 +03:00
}
} catch (EvalError & e) {
if (!(attrPath.size() > 0 && attrPathS[0] == "legacyPackages"))
throw;
2020-04-16 16:36:15 +03:00
}
return j;
2020-04-16 16:36:15 +03:00
};
2020-08-07 15:13:24 +03:00
auto cache = openEvalCache(*state, flake);
2020-04-16 16:36:15 +03:00
auto j = visit(*cache->getRoot(), {}, fmt(ANSI_BOLD "%s" ANSI_NORMAL, flake->flake.lockedRef), "");
if (json)
logger->cout("%s", j.dump());
2020-04-16 16:36:15 +03:00
}
};
struct CmdFlakePrefetch : FlakeCommand, MixJSON
{
CmdFlakePrefetch()
{
}
std::string description() override
{
return "download the source tree denoted by a flake reference into the Nix store";
}
std::string doc() override
{
return
#include "flake-prefetch.md"
;
}
void run(ref<Store> store) override
{
auto originalRef = getFlakeRef();
auto resolvedRef = originalRef.resolve(store);
auto [storePath, lockedRef] = resolvedRef.fetchTree(store);
auto hash = store->queryPathInfo(storePath)->narHash;
if (json) {
auto res = nlohmann::json::object();
res["storePath"] = store->printStorePath(storePath);
res["hash"] = hash.to_string(HashFormat::SRI, true);
res["original"] = fetchers::attrsToJSON(resolvedRef.toAttrs());
res["locked"] = fetchers::attrsToJSON(lockedRef.toAttrs());
logger->cout(res.dump());
} else {
notice("Downloaded '%s' to '%s' (hash '%s').",
lockedRef.to_string(),
store->printStorePath(storePath),
hash.to_string(HashFormat::SRI, true));
}
}
};
struct CmdFlake : NixMultiCommand
2018-11-29 20:18:36 +02:00
{
CmdFlake()
: NixMultiCommand(
"flake",
{
{"update", []() { return make_ref<CmdFlakeUpdate>(); }},
{"lock", []() { return make_ref<CmdFlakeLock>(); }},
{"metadata", []() { return make_ref<CmdFlakeMetadata>(); }},
{"info", []() { return make_ref<CmdFlakeInfo>(); }},
{"check", []() { return make_ref<CmdFlakeCheck>(); }},
{"init", []() { return make_ref<CmdFlakeInit>(); }},
{"new", []() { return make_ref<CmdFlakeNew>(); }},
{"clone", []() { return make_ref<CmdFlakeClone>(); }},
{"archive", []() { return make_ref<CmdFlakeArchive>(); }},
2020-04-16 16:36:15 +03:00
{"show", []() { return make_ref<CmdFlakeShow>(); }},
{"prefetch", []() { return make_ref<CmdFlakePrefetch>(); }},
})
2018-11-29 20:18:36 +02:00
{
}
std::string description() override
{
return "manage Nix flakes";
}
2020-12-23 14:19:53 +02:00
std::string doc() override
{
return
#include "flake.md"
;
}
2018-11-29 20:18:36 +02:00
void run() override
{
experimentalFeatureSettings.require(Xp::Flakes);
NixMultiCommand::run();
2018-11-29 20:18:36 +02:00
}
};
static auto rCmdFlake = registerCommand<CmdFlake>("flake");