lix/src/nix/flake.cc
Eelco Dolstra 7d38060a0d Support non-x86_64-linux system types in flakes
A command like

  $ nix run nixpkgs#hello

will now build the attribute 'packages.${system}.hello' rather than
'packages.hello'. Note that this does mean that the flake needs to
export an attribute for every system type it supports, and you can't
build on unsupported systems. So 'packages' typically looks like this:

  packages = nixpkgs.lib.genAttrs ["x86_64-linux" "i686-linux"] (system: {
    hello = ...;
  });

The 'checks', 'defaultPackage', 'devShell', 'apps' and 'defaultApp'
outputs similarly are now attrsets that map system types to
derivations/apps. 'nix flake check' checks that the derivations for
all platforms evaluate correctly, but only builds the derivations in
'checks.${system}'.

Fixes #2861. (That issue also talks about access to ~/.config/nixpkgs
and --arg, but I think it's reasonable to say that flakes shouldn't
support those.)

The alternative to attribute selection is to pass the system type as
an argument to the flake's 'outputs' function, e.g. 'outputs = { self,
nixpkgs, system }: ...'. However, that approach would be at odds with
hermetic evaluation and make it impossible to enumerate the packages
provided by a flake.
2019-10-15 18:16:29 +02:00

666 lines
23 KiB
C++

#include "command.hh"
#include "common-args.hh"
#include "shared.hh"
#include "progress-bar.hh"
#include "eval.hh"
#include "eval-inline.hh"
#include "flake/flake.hh"
#include "get-drvs.hh"
#include "store-api.hh"
#include "derivations.hh"
#include "attr-path.hh"
#include <nlohmann/json.hpp>
#include <queue>
#include <iomanip>
using namespace nix;
using namespace nix::flake;
class FlakeCommand : virtual Args, public EvalCommand, public MixFlakeOptions
{
std::string flakeUrl = ".";
public:
FlakeCommand()
{
expectArg("flake-url", &flakeUrl, true);
}
FlakeRef getFlakeRef()
{
if (flakeUrl.find('/') != std::string::npos || flakeUrl == ".")
return FlakeRef(flakeUrl, true);
else
return FlakeRef(flakeUrl);
}
Flake getFlake()
{
auto evalState = getEvalState();
return flake::getFlake(*evalState, getFlakeRef(), useRegistries);
}
ResolvedFlake resolveFlake()
{
return flake::resolveFlake(*getEvalState(), getFlakeRef(), getLockFileMode());
}
};
struct CmdFlakeList : EvalCommand
{
std::string description() override
{
return "list available Nix flakes";
}
void run(nix::ref<nix::Store> store) override
{
auto registries = getEvalState()->getFlakeRegistries();
stopProgressBar();
for (auto & entry : registries[FLAG_REGISTRY]->entries)
std::cout << entry.first.to_string() << " flags " << entry.second.to_string() << "\n";
for (auto & entry : registries[USER_REGISTRY]->entries)
std::cout << entry.first.to_string() << " user " << entry.second.to_string() << "\n";
for (auto & entry : registries[GLOBAL_REGISTRY]->entries)
std::cout << entry.first.to_string() << " global " << entry.second.to_string() << "\n";
}
};
static void printSourceInfo(const SourceInfo & sourceInfo)
{
std::cout << fmt("URL: %s\n", sourceInfo.resolvedRef.to_string());
if (sourceInfo.resolvedRef.ref)
std::cout << fmt("Branch: %s\n",*sourceInfo.resolvedRef.ref);
if (sourceInfo.resolvedRef.rev)
std::cout << fmt("Revision: %s\n", sourceInfo.resolvedRef.rev->to_string(Base16, false));
if (sourceInfo.revCount)
std::cout << fmt("Revisions: %s\n", *sourceInfo.revCount);
if (sourceInfo.lastModified)
std::cout << fmt("Last modified: %s\n",
std::put_time(std::localtime(&*sourceInfo.lastModified), "%F %T"));
std::cout << fmt("Path: %s\n", sourceInfo.storePath);
}
static void sourceInfoToJson(const SourceInfo & sourceInfo, nlohmann::json & j)
{
j["url"] = sourceInfo.resolvedRef.to_string();
if (sourceInfo.resolvedRef.ref)
j["branch"] = *sourceInfo.resolvedRef.ref;
if (sourceInfo.resolvedRef.rev)
j["revision"] = sourceInfo.resolvedRef.rev->to_string(Base16, false);
if (sourceInfo.revCount)
j["revCount"] = *sourceInfo.revCount;
if (sourceInfo.lastModified)
j["lastModified"] = *sourceInfo.lastModified;
j["path"] = sourceInfo.storePath;
}
static void printFlakeInfo(const Flake & flake)
{
std::cout << fmt("Description: %s\n", flake.description);
std::cout << fmt("Edition: %s\n", flake.edition);
printSourceInfo(flake.sourceInfo);
}
static nlohmann::json flakeToJson(const Flake & flake)
{
nlohmann::json j;
j["description"] = flake.description;
j["edition"] = flake.edition;
sourceInfoToJson(flake.sourceInfo, j);
return j;
}
#if 0
// FIXME: merge info CmdFlakeInfo?
struct CmdFlakeDeps : FlakeCommand
{
std::string description() override
{
return "list informaton about dependencies";
}
void run(nix::ref<nix::Store> store) override
{
auto evalState = getEvalState();
std::queue<ResolvedFlake> todo;
todo.push(resolveFlake());
stopProgressBar();
while (!todo.empty()) {
auto resFlake = std::move(todo.front());
todo.pop();
for (auto & info : resFlake.flakeDeps) {
printFlakeInfo(info.second.flake);
todo.push(info.second);
}
}
}
};
#endif
struct CmdFlakeUpdate : FlakeCommand
{
std::string description() override
{
return "update flake lock file";
}
void run(nix::ref<nix::Store> store) override
{
auto evalState = getEvalState();
auto flakeRef = getFlakeRef();
if (std::get_if<FlakeRef::IsPath>(&flakeRef.data))
updateLockFile(*evalState, flakeRef, true);
else
throw Error("cannot update lockfile of flake '%s'", flakeRef);
}
};
static void enumerateOutputs(EvalState & state, Value & vFlake,
std::function<void(const std::string & name, Value & vProvide, const Pos & pos)> callback)
{
state.forceAttrs(vFlake);
auto aOutputs = vFlake.attrs->get(state.symbols.create("outputs"));
assert(aOutputs);
state.forceAttrs(*(*aOutputs)->value);
for (auto & attr : *((*aOutputs)->value->attrs))
callback(attr.name, *attr.value, *attr.pos);
}
struct CmdFlakeInfo : FlakeCommand, MixJSON
{
std::string description() override
{
return "list info about a given flake";
}
void run(nix::ref<nix::Store> store) override
{
if (json) {
auto state = getEvalState();
auto flake = resolveFlake();
auto json = flakeToJson(flake.flake);
auto vFlake = state->allocValue();
flake::callFlake(*state, flake, *vFlake);
auto outputs = nlohmann::json::object();
enumerateOutputs(*state,
*vFlake,
[&](const std::string & name, Value & vProvide, const Pos & pos) {
auto provide = nlohmann::json::object();
if (name == "checks" || name == "packages") {
state->forceAttrs(vProvide, pos);
for (auto & aCheck : *vProvide.attrs)
provide[aCheck.name] = nlohmann::json::object();
}
outputs[name] = provide;
});
json["outputs"] = std::move(outputs);
std::cout << json.dump() << std::endl;
} else {
auto flake = getFlake();
stopProgressBar();
printFlakeInfo(flake);
}
}
};
struct CmdFlakeCheck : FlakeCommand, MixJSON
{
bool build = true;
CmdFlakeCheck()
{
mkFlag()
.longName("no-build")
.description("do not build checks")
.set(&build, false);
}
std::string description() override
{
return "check whether the flake evaluates and run its tests";
}
void run(nix::ref<nix::Store> store) override
{
settings.readOnlyMode = !build;
auto state = getEvalState();
auto flake = resolveFlake();
auto checkSystemName = [&](const std::string & system, const Pos & pos) {
// FIXME: what's the format of "system"?
if (system.find('-') == std::string::npos)
throw Error("'%s' is not a valid system type, at %s", system, pos);
};
auto checkDerivation = [&](const std::string & attrPath, Value & v, const Pos & pos) {
try {
auto drvInfo = getDerivation(*state, v, false);
if (!drvInfo)
throw Error("flake attribute '%s' is not a derivation", attrPath);
// FIXME: check meta attributes
return drvInfo->queryDrvPath();
} catch (Error & e) {
e.addPrefix(fmt("while checking the derivation '" ANSI_BOLD "%s" ANSI_NORMAL "' at %s:\n", attrPath, pos));
throw;
}
};
PathSet drvPaths;
auto checkApp = [&](const std::string & attrPath, Value & v, const Pos & pos) {
try {
auto app = App(*state, v);
for (auto & i : app.context) {
auto [drvPath, outputName] = decodeContext(i);
if (!outputName.empty() && nix::isDerivation(drvPath))
drvPaths.insert(drvPath + "!" + outputName);
}
} catch (Error & e) {
e.addPrefix(fmt("while checking the app definition '" ANSI_BOLD "%s" ANSI_NORMAL "' at %s:\n", attrPath, pos));
throw;
}
};
auto checkOverlay = [&](const std::string & attrPath, Value & v, const Pos & pos) {
try {
state->forceValue(v, pos);
if (v.type != tLambda || v.lambda.fun->matchAttrs || std::string(v.lambda.fun->arg) != "final")
throw Error("overlay does not take an argument named 'final'");
auto body = dynamic_cast<ExprLambda *>(v.lambda.fun->body);
if (!body || body->matchAttrs || std::string(body->arg) != "prev")
throw Error("overlay does not take an argument named 'prev'");
// FIXME: if we have a 'nixpkgs' input, use it to
// evaluate the overlay.
} catch (Error & e) {
e.addPrefix(fmt("while checking the overlay '" ANSI_BOLD "%s" ANSI_NORMAL "' at %s:\n", attrPath, pos));
throw;
}
};
auto checkModule = [&](const std::string & attrPath, Value & v, const Pos & pos) {
try {
state->forceValue(v, pos);
if (v.type == tLambda) {
if (!v.lambda.fun->matchAttrs || !v.lambda.fun->formals->ellipsis)
throw Error("module must match an open attribute set ('{ config, ... }')");
} else if (v.type == tAttrs) {
for (auto & attr : *v.attrs)
try {
state->forceValue(*attr.value, *attr.pos);
} catch (Error & e) {
e.addPrefix(fmt("while evaluating the option '" ANSI_BOLD "%s" ANSI_NORMAL "' at %s:\n", attr.name, *attr.pos));
throw;
}
} else
throw Error("module must be a function or an attribute set");
// FIXME: if we have a 'nixpkgs' input, use it to
// check the module.
} catch (Error & e) {
e.addPrefix(fmt("while checking the NixOS module '" ANSI_BOLD "%s" ANSI_NORMAL "' at %s:\n", attrPath, pos));
throw;
}
};
std::function<void(const std::string & attrPath, Value & v, const Pos & pos)> checkHydraJobs;
checkHydraJobs = [&](const std::string & attrPath, Value & v, const Pos & pos) {
try {
state->forceAttrs(v, pos);
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);
if (!state->isDerivation(*attr.value))
checkHydraJobs(attrPath + "." + (std::string) attr.name,
*attr.value, *attr.pos);
}
} catch (Error & e) {
e.addPrefix(fmt("while checking the Hydra jobset '" ANSI_BOLD "%s" ANSI_NORMAL "' at %s:\n", attrPath, pos));
throw;
}
};
auto checkNixOSConfiguration = [&](const std::string & attrPath, Value & v, const Pos & pos) {
try {
Activity act(*logger, lvlChatty, actUnknown,
fmt("checking NixOS configuration '%s'", attrPath));
Bindings & bindings(*state->allocBindings(0));
auto vToplevel = findAlongAttrPath(*state, "config.system.build.toplevel", bindings, v);
state->forceAttrs(*vToplevel, pos);
if (!state->isDerivation(*vToplevel))
throw Error("attribute 'config.system.build.toplevel' is not a derivation");
} catch (Error & e) {
e.addPrefix(fmt("while checking the NixOS configuration '" ANSI_BOLD "%s" ANSI_NORMAL "' at %s:\n", attrPath, pos));
throw;
}
};
{
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 Pos & pos) {
Activity act(*logger, lvlChatty, actUnknown,
fmt("checking flake output '%s'", name));
try {
state->forceValue(vOutput, pos);
if (name == "checks") {
state->forceAttrs(vOutput, pos);
for (auto & attr : *vOutput.attrs) {
checkSystemName(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, attr2.name),
*attr2.value, *attr2.pos);
if ((std::string) attr.name == settings.thisSystem.get())
drvPaths.insert(drvPath);
}
}
}
else if (name == "packages") {
state->forceAttrs(vOutput, pos);
for (auto & attr : *vOutput.attrs) {
checkSystemName(attr.name, *attr.pos);
state->forceAttrs(*attr.value, *attr.pos);
for (auto & attr2 : *attr.value->attrs)
checkDerivation(
fmt("%s.%s.%s", name, attr.name, attr2.name),
*attr2.value, *attr2.pos);
}
}
else if (name == "apps") {
state->forceAttrs(vOutput, pos);
for (auto & attr : *vOutput.attrs) {
checkSystemName(attr.name, *attr.pos);
state->forceAttrs(*attr.value, *attr.pos);
for (auto & attr2 : *attr.value->attrs)
checkApp(
fmt("%s.%s.%s", name, attr.name, attr2.name),
*attr2.value, *attr2.pos);
}
}
else if (name == "defaultPackage" || name == "devShell") {
state->forceAttrs(vOutput, pos);
for (auto & attr : *vOutput.attrs) {
checkSystemName(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) {
checkSystemName(attr.name, *attr.pos);
checkApp(
fmt("%s.%s", name, attr.name),
*attr.value, *attr.pos);
}
}
else if (name == "legacyPackages") {
state->forceAttrs(vOutput, pos);
for (auto & attr : *vOutput.attrs) {
checkSystemName(attr.name, *attr.pos);
// FIXME: do getDerivations?
}
}
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, 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, attr.name),
*attr.value, *attr.pos);
}
else if (name == "nixosConfigurations") {
state->forceAttrs(vOutput, pos);
for (auto & attr : *vOutput.attrs)
checkNixOSConfiguration(fmt("%s.%s", name, attr.name),
*attr.value, *attr.pos);
}
else if (name == "hydraJobs")
checkHydraJobs(name, vOutput, pos);
else
warn("unknown flake output '%s'", name);
} catch (Error & e) {
e.addPrefix(fmt("while checking flake output '" ANSI_BOLD "%s" ANSI_NORMAL "':\n", name));
throw;
}
});
}
if (build && !drvPaths.empty()) {
Activity act(*logger, lvlInfo, actUnknown, "running flake checks");
store->buildPaths(drvPaths);
}
}
};
struct CmdFlakeAdd : MixEvalArgs, Command
{
FlakeUri alias;
FlakeUri url;
std::string description() override
{
return "upsert flake in user flake registry";
}
CmdFlakeAdd()
{
expectArg("alias", &alias);
expectArg("flake-url", &url);
}
void run() override
{
FlakeRef aliasRef(alias);
Path userRegistryPath = getUserRegistryPath();
auto userRegistry = readRegistry(userRegistryPath);
userRegistry->entries.erase(aliasRef);
userRegistry->entries.insert_or_assign(aliasRef, FlakeRef(url));
writeRegistry(*userRegistry, userRegistryPath);
}
};
struct CmdFlakeRemove : virtual Args, MixEvalArgs, Command
{
FlakeUri alias;
std::string description() override
{
return "remove flake from user flake registry";
}
CmdFlakeRemove()
{
expectArg("alias", &alias);
}
void run() override
{
Path userRegistryPath = getUserRegistryPath();
auto userRegistry = readRegistry(userRegistryPath);
userRegistry->entries.erase(FlakeRef(alias));
writeRegistry(*userRegistry, userRegistryPath);
}
};
struct CmdFlakePin : virtual Args, EvalCommand
{
FlakeUri alias;
std::string description() override
{
return "pin flake require in user flake registry";
}
CmdFlakePin()
{
expectArg("alias", &alias);
}
void run(nix::ref<nix::Store> store) override
{
auto evalState = getEvalState();
Path userRegistryPath = getUserRegistryPath();
FlakeRegistry userRegistry = *readRegistry(userRegistryPath);
auto it = userRegistry.entries.find(FlakeRef(alias));
if (it != userRegistry.entries.end()) {
it->second = getFlake(*evalState, it->second, true).sourceInfo.resolvedRef;
writeRegistry(userRegistry, userRegistryPath);
} else {
std::shared_ptr<FlakeRegistry> globalReg = evalState->getGlobalFlakeRegistry();
it = globalReg->entries.find(FlakeRef(alias));
if (it != globalReg->entries.end()) {
auto newRef = getFlake(*evalState, it->second, true).sourceInfo.resolvedRef;
userRegistry.entries.insert_or_assign(alias, newRef);
writeRegistry(userRegistry, userRegistryPath);
} else
throw Error("the flake alias '%s' does not exist in the user or global registry", alias);
}
}
};
struct CmdFlakeInit : virtual Args, Command
{
std::string description() override
{
return "create a skeleton 'flake.nix' file in the current directory";
}
void run() override
{
Path flakeDir = absPath(".");
if (!pathExists(flakeDir + "/.git"))
throw Error("the directory '%s' is not a Git repository", flakeDir);
Path flakePath = flakeDir + "/flake.nix";
if (pathExists(flakePath))
throw Error("file '%s' already exists", flakePath);
writeFile(flakePath,
#include "flake-template.nix.gen.hh"
);
}
};
struct CmdFlakeClone : FlakeCommand
{
Path destDir;
std::string description() override
{
return "clone flake repository";
}
CmdFlakeClone()
{
expectArg("dest-dir", &destDir, true);
}
void run(nix::ref<nix::Store> store) override
{
auto evalState = getEvalState();
Registries registries = evalState->getFlakeRegistries();
gitCloneFlake(getFlakeRef().to_string(), *evalState, registries, destDir);
}
};
struct CmdFlake : virtual MultiCommand, virtual Command
{
CmdFlake()
: MultiCommand({
{"list", []() { return make_ref<CmdFlakeList>(); }},
{"update", []() { return make_ref<CmdFlakeUpdate>(); }},
{"info", []() { return make_ref<CmdFlakeInfo>(); }},
{"check", []() { return make_ref<CmdFlakeCheck>(); }},
{"add", []() { return make_ref<CmdFlakeAdd>(); }},
{"remove", []() { return make_ref<CmdFlakeRemove>(); }},
{"pin", []() { return make_ref<CmdFlakePin>(); }},
{"init", []() { return make_ref<CmdFlakeInit>(); }},
{"clone", []() { return make_ref<CmdFlakeClone>(); }},
})
{
}
std::string description() override
{
return "manage Nix flakes";
}
void run() override
{
if (!command)
throw UsageError("'nix flake' requires a sub-command.");
command->run();
}
void printHelp(const string & programName, std::ostream & out) override
{
MultiCommand::printHelp(programName, out);
}
};
static auto r1 = registerCommand<CmdFlake>("flake");