From 1dc3f5355a3786cab37a4de98ca46a859e015d89 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 23 Sep 2020 14:57:47 +0200 Subject: [PATCH] Support flakes in TOML format So instead of a 'flake.nix', flakes can now contain a 'nix.toml' file like this: description = "My own Hello World" [inputs] configs.url = "github:tweag/nix-ux/configs?dir=configs" [my-hello] extends = [ "configs#hello" ] doc = ''' A specialized version of the Hello package! ''' who = "Springfield" 'my-hello' defines an output named 'modules.my-hello', which can be built as follows: $ nix build /path/to/flake#my-hello $ ./result/bin/hello Hello Springfield --- src/libexpr/flake/call-flake.nix | 64 ++++++++++++++--- src/libexpr/flake/flake.cc | 115 +++++++++++++++++++------------ src/libexpr/flake/flakeref.cc | 4 +- src/libexpr/primops.hh | 2 + src/libexpr/primops/fromTOML.cc | 4 +- 5 files changed, 134 insertions(+), 55 deletions(-) diff --git a/src/libexpr/flake/call-flake.nix b/src/libexpr/flake/call-flake.nix index 932ac5e90..173a7b572 100644 --- a/src/libexpr/flake/call-flake.nix +++ b/src/libexpr/flake/call-flake.nix @@ -1,11 +1,13 @@ +with builtins; + lockFileStr: rootSrc: rootSubdir: let - lockFile = builtins.fromJSON lockFileStr; + lockFile = fromJSON lockFileStr; allNodes = - builtins.mapAttrs + mapAttrs (key: node: let @@ -16,9 +18,55 @@ let subdir = if key == lockFile.root then rootSubdir else node.locked.dir or ""; - flake = import (sourceInfo + (if subdir != "" then "/" else "") + subdir + "/flake.nix"); + flakeDir = sourceInfo + (if subdir != "" then "/" else "") + subdir; - inputs = builtins.mapAttrs + flake = + if pathExists (flakeDir + "/flake.nix") + then import (flakeDir + "/flake.nix") + else if pathExists (flakeDir + "/nix.toml") + then + # Convert nix.toml to a flake containing a 'modules' + # output. + let + toml = fromTOML (readFile (flakeDir + "/nix.toml")); + in { + inputs = toml.inputs or {}; + outputs = inputs: { + modules = + listToAttrs ( + map (moduleName: + let + m = toml.${moduleName}; + in { + name = moduleName; + value = module { + extends = + map (flakeRef: + let + tokens = match ''(.*)#(.*)'' flakeRef; + in + assert tokens != null; + inputs.${elemAt tokens 0}.modules.${elemAt tokens 1} + ) (m.extends or []); + config = { config }: listToAttrs (map + (optionName: + { name = optionName; + value = m.${optionName}; + } + ) + (filter + (n: n != "extends" && n != "doc") + (attrNames m))); + }; + }) + (filter + (n: isAttrs toml.${n} && n != "inputs") + (attrNames toml))); + }; + } + else throw "flake does not contain a 'flake.nix' or 'nix.toml'"; + + inputs = mapAttrs (inputName: inputSpec: allNodes.${resolveInput inputSpec}) (node.inputs or {}); @@ -26,7 +74,7 @@ let # either a node name, or a 'follows' path from the root # node. resolveInput = inputSpec: - if builtins.isList inputSpec + if isList inputSpec then getInputByPath lockFile.root inputSpec else inputSpec; @@ -38,15 +86,15 @@ let else getInputByPath # Since this could be a 'follows' input, call resolveInput. - (resolveInput lockFile.nodes.${nodeName}.inputs.${builtins.head path}) - (builtins.tail path); + (resolveInput lockFile.nodes.${nodeName}.inputs.${head path}) + (tail path); outputs = flake.outputs (inputs // { self = result; }); result = outputs // sourceInfo // { inherit inputs; inherit outputs; inherit sourceInfo; }; in if node.flake or true then - assert builtins.isFunction flake.outputs; + assert isFunction flake.outputs; result else sourceInfo diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index 460eea5ea..b79508143 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -175,11 +175,8 @@ static Flake getFlake( auto [sourceInfo, resolvedRef, lockedRef] = fetchOrSubstituteTree( state, originalRef, allowLookup, flakeCache); - // Guard against symlink attacks. auto flakeFile = canonPath(sourceInfo.actualPath + "/" + lockedRef.subdir + "/flake.nix"); - if (!isInDir(flakeFile, sourceInfo.actualPath)) - throw Error("'flake.nix' file of flake '%s' escapes from '%s'", - lockedRef, state.store->printStorePath(sourceInfo.storePath)); + auto tomlFile = canonPath(sourceInfo.actualPath + "/" + lockedRef.subdir + "/nix.toml"); Flake flake { .originalRef = originalRef, @@ -188,55 +185,85 @@ static Flake getFlake( .sourceInfo = std::make_shared(std::move(sourceInfo)) }; - if (!pathExists(flakeFile)) - throw Error("source tree referenced by '%s' does not contain a '%s/flake.nix' file", lockedRef, lockedRef.subdir); - - Value vInfo; - state.evalFile(flakeFile, vInfo, true); // FIXME: symlink attack - - expectType(state, tAttrs, vInfo, Pos(foFile, state.symbols.create(flakeFile), 0, 0)); - - auto sEdition = state.symbols.create("edition"); // FIXME: remove soon - - if (vInfo.attrs->get(sEdition)) - warn("flake '%s' has deprecated attribute 'edition'", lockedRef); - - if (auto description = vInfo.attrs->get(state.sDescription)) { - expectType(state, tString, *description->value, *description->pos); - flake.description = description->value->string.s; - } - auto sInputs = state.symbols.create("inputs"); - - if (auto inputs = vInfo.attrs->get(sInputs)) - flake.inputs = parseFlakeInputs(state, inputs->value, *inputs->pos); - auto sOutputs = state.symbols.create("outputs"); - if (auto outputs = vInfo.attrs->get(sOutputs)) { - expectType(state, tLambda, *outputs->value, *outputs->pos); + if (pathExists(flakeFile)) { + // Guard against symlink attacks. + if (!isInDir(flakeFile, sourceInfo.actualPath)) + throw Error("'flake.nix' file of flake '%s' escapes from '%s'", + lockedRef, state.store->printStorePath(sourceInfo.storePath)); - if (outputs->value->lambda.fun->matchAttrs) { - for (auto & formal : outputs->value->lambda.fun->formals->formals) { - if (formal.name != state.sSelf) - flake.inputs.emplace(formal.name, FlakeInput { - .ref = parseFlakeRef(formal.name) - }); - } + Value vInfo; + state.evalFile(flakeFile, vInfo, true); // FIXME: symlink attack + + expectType(state, tAttrs, vInfo, Pos(foFile, state.symbols.create(flakeFile), 0, 0)); + + auto sEdition = state.symbols.create("edition"); // FIXME: remove soon + + if (vInfo.attrs->get(sEdition)) + warn("flake '%s' has deprecated attribute 'edition'", lockedRef); + + if (auto description = vInfo.attrs->get(state.sDescription)) { + expectType(state, tString, *description->value, *description->pos); + flake.description = description->value->string.s; } - } else - throw Error("flake '%s' lacks attribute 'outputs'", lockedRef); + if (auto inputs = vInfo.attrs->get(sInputs)) + flake.inputs = parseFlakeInputs(state, inputs->value, *inputs->pos); + + if (auto outputs = vInfo.attrs->get(sOutputs)) { + expectType(state, tLambda, *outputs->value, *outputs->pos); + + if (outputs->value->lambda.fun->matchAttrs) { + for (auto & formal : outputs->value->lambda.fun->formals->formals) { + if (formal.name != state.sSelf) + flake.inputs.emplace(formal.name, FlakeInput { + .ref = parseFlakeRef(formal.name) + }); + } + } + + } else + throw Error("flake '%s' lacks attribute 'outputs'", lockedRef); + + for (auto & attr : *vInfo.attrs) { + if (attr.name != sEdition && + attr.name != state.sDescription && + attr.name != sInputs && + attr.name != sOutputs) + throw Error("flake '%s' has an unsupported attribute '%s', at %s", + lockedRef, attr.name, *attr.pos); + } - for (auto & attr : *vInfo.attrs) { - if (attr.name != sEdition && - attr.name != state.sDescription && - attr.name != sInputs && - attr.name != sOutputs) - throw Error("flake '%s' has an unsupported attribute '%s', at %s", - lockedRef, attr.name, *attr.pos); } + else if (pathExists(tomlFile)) { + // Guard against symlink attacks. + if (!isInDir(tomlFile, sourceInfo.actualPath)) + throw Error("'nix.toml' file of flake '%s' escapes from '%s'", + lockedRef, state.store->printStorePath(sourceInfo.storePath)); + + auto vToml = state.allocValue(); + mkString(*vToml, readFile(tomlFile)); + auto vFlake = state.allocValue(); + prim_fromTOML(state, noPos, &vToml, *vFlake); + state.forceAttrs(*vFlake); + + if (auto description = vFlake->attrs->get(state.sDescription)) { + expectType(state, tString, *description->value, *description->pos); + flake.description = description->value->string.s; + } + + if (auto inputs = vFlake->attrs->get(sInputs)) + flake.inputs = parseFlakeInputs(state, inputs->value, *inputs->pos); + + // FIXME: complain about unknown attributes. + } + + else + throw Error("source tree referenced by '%1%' does not contain a '%2%/flake.nix' or '%2%/nix.toml' file %3%", lockedRef, lockedRef.subdir, flakeFile); + return flake; } diff --git a/src/libexpr/flake/flakeref.cc b/src/libexpr/flake/flakeref.cc index d5c2ffe66..97d6c61cd 100644 --- a/src/libexpr/flake/flakeref.cc +++ b/src/libexpr/flake/flakeref.cc @@ -117,8 +117,8 @@ std::pair parseFlakeRefWithFragment( if (!S_ISDIR(lstat(path).st_mode)) throw BadURL("path '%s' is not a flake (because it's not a directory)", path); - if (!allowMissing && !pathExists(path + "/flake.nix")) - throw BadURL("path '%s' is not a flake (because it doesn't contain a 'flake.nix' file)", path); + if (!allowMissing && !(pathExists(path + "/flake.nix") || pathExists(path + "/nix.toml"))) + throw BadURL("path '%s' is not a flake (because it doesn't contain a 'flake.nix' or 'nix.toml' file)", path); auto flakeRoot = path; std::string subdir; diff --git a/src/libexpr/primops.hh b/src/libexpr/primops.hh index ed5e2ea58..cef7667e0 100644 --- a/src/libexpr/primops.hh +++ b/src/libexpr/primops.hh @@ -41,4 +41,6 @@ void prim_importNative(EvalState & state, const Pos & pos, Value * * args, Value /* Execute a program and parse its output */ void prim_exec(EvalState & state, const Pos & pos, Value * * args, Value & v); +void prim_fromTOML(EvalState & state, const Pos & pos, Value * * args, Value & v); + } diff --git a/src/libexpr/primops/fromTOML.cc b/src/libexpr/primops/fromTOML.cc index b00827a4b..955373796 100644 --- a/src/libexpr/primops/fromTOML.cc +++ b/src/libexpr/primops/fromTOML.cc @@ -5,7 +5,7 @@ namespace nix { -static void prim_fromTOML(EvalState & state, const Pos & pos, Value * * args, Value & v) +void prim_fromTOML(EvalState & state, const Pos & pos, Value * * args, Value & v) { using namespace cpptoml; @@ -17,6 +17,8 @@ static void prim_fromTOML(EvalState & state, const Pos & pos, Value * * args, Va visit = [&](Value & v, std::shared_ptr t) { + // FIXME: set attribute positions + if (auto t2 = t->as_table()) { size_t size = 0;