Skip to content

Commit

Permalink
Support flakes in TOML format
Browse files Browse the repository at this point in the history
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
  • Loading branch information
edolstra committed Sep 23, 2020
1 parent 08992ab commit 1dc3f53
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 49 deletions.
64 changes: 56 additions & 8 deletions src/libexpr/flake/call-flake.nix
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
with builtins;

lockFileStr: rootSrc: rootSubdir:

let

lockFile = builtins.fromJSON lockFileStr;
lockFile = fromJSON lockFileStr;

allNodes =
builtins.mapAttrs
mapAttrs
(key: node:
let

Expand All @@ -16,17 +18,63 @@ 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;

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 = builtins.mapAttrs
inputs = mapAttrs
(inputName: inputSpec: allNodes.${resolveInput inputSpec})
(node.inputs or {});

# Resolve a input spec into a node name. An input spec is
# 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;

Expand All @@ -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
Expand Down
103 changes: 65 additions & 38 deletions src/libexpr/flake/flake.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -188,55 +185,85 @@ static Flake getFlake(
.sourceInfo = std::make_shared<fetchers::Tree>(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
auto sInputs = state.symbols.create("inputs");
auto sOutputs = state.symbols.create("outputs");

expectType(state, tAttrs, vInfo, Pos(foFile, state.symbols.create(flakeFile), 0, 0));
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));

auto sEdition = state.symbols.create("edition"); // FIXME: remove soon
Value vInfo;
state.evalFile(flakeFile, vInfo, true); // FIXME: symlink attack

if (vInfo.attrs->get(sEdition))
warn("flake '%s' has deprecated attribute 'edition'", lockedRef);
expectType(state, tAttrs, vInfo, Pos(foFile, state.symbols.create(flakeFile), 0, 0));

if (auto description = vInfo.attrs->get(state.sDescription)) {
expectType(state, tString, *description->value, *description->pos);
flake.description = description->value->string.s;
}
auto sEdition = state.symbols.create("edition"); // FIXME: remove soon

auto sInputs = state.symbols.create("inputs");
if (vInfo.attrs->get(sEdition))
warn("flake '%s' has deprecated attribute 'edition'", lockedRef);

if (auto inputs = vInfo.attrs->get(sInputs))
flake.inputs = parseFlakeInputs(state, inputs->value, *inputs->pos);
if (auto description = vInfo.attrs->get(state.sDescription)) {
expectType(state, tString, *description->value, *description->pos);
flake.description = description->value->string.s;
}

auto sOutputs = state.symbols.create("outputs");
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 (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)
});
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);
}

} 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);
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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/libexpr/flake/flakeref.cc
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ std::pair<FlakeRef, std::string> 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;
Expand Down
2 changes: 2 additions & 0 deletions src/libexpr/primops.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
4 changes: 3 additions & 1 deletion src/libexpr/primops/fromTOML.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -17,6 +17,8 @@ static void prim_fromTOML(EvalState & state, const Pos & pos, Value * * args, Va

visit = [&](Value & v, std::shared_ptr<base> t) {

// FIXME: set attribute positions

if (auto t2 = t->as_table()) {

size_t size = 0;
Expand Down

0 comments on commit 1dc3f53

Please sign in to comment.