aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEelco Dolstra <edolstra@gmail.com>2020-06-04 20:02:50 +0200
committerEelco Dolstra <edolstra@gmail.com>2020-06-04 20:22:25 +0200
commit810b2c6a48b5ecd468bd4f65963ce5a7aa96832e (patch)
tree6d0cf088c2b5ef713ef00d81da9413eb23916569
parentdc305500c38c5e2227bea696949970c562046a8f (diff)
nix flake init: Add a '--template' flag
The initial contents of the flake is specified by the 'templates.<name>' or 'defaultTemplate' output of another flake. E.g. outputs = { self }: { templates = { nixos-container = { path = ./nixos-container; description = "An example of a NixOS container"; }; }; }; allows $ nix flake init -t templates#nixos-container Also add a command 'nix flake new', which is identical to 'nix flake init' except that it initializes a specified directory rather than the current directory.
-rw-r--r--src/libexpr/eval-cache.cc8
-rw-r--r--src/nix/flake-template.nix11
-rw-r--r--src/nix/flake.cc192
-rw-r--r--src/nix/installables.cc13
-rw-r--r--src/nix/installables.hh5
-rw-r--r--src/nix/local.mk2
-rw-r--r--src/nix/search.cc2
-rw-r--r--tests/flakes.sh66
8 files changed, 258 insertions, 41 deletions
diff --git a/src/libexpr/eval-cache.cc b/src/libexpr/eval-cache.cc
index 1b8edf9c1..a47a539f5 100644
--- a/src/libexpr/eval-cache.cc
+++ b/src/libexpr/eval-cache.cc
@@ -320,6 +320,8 @@ Value & AttrCursor::forceValue()
if (root->db && (!cachedValue || std::get_if<placeholder_t>(&cachedValue->second))) {
if (v.type == tString)
cachedValue = {root->db->setString(getKey(), v.string.s), v.string.s};
+ else if (v.type == tPath)
+ cachedValue = {root->db->setString(getKey(), v.path), v.path};
else if (v.type == tBool)
cachedValue = {root->db->setBool(getKey(), v.boolean), v.boolean};
else if (v.type == tAttrs)
@@ -434,10 +436,10 @@ std::string AttrCursor::getString()
auto & v = forceValue();
- if (v.type != tString)
- throw TypeError("'%s' is not a string", getAttrPathStr());
+ if (v.type != tString && v.type != tPath)
+ throw TypeError("'%s' is not a string but %s", getAttrPathStr(), showType(v.type));
- return v.string.s;
+ return v.type == tString ? v.string.s : v.path;
}
bool AttrCursor::getBool()
diff --git a/src/nix/flake-template.nix b/src/nix/flake-template.nix
deleted file mode 100644
index 195aef2cc..000000000
--- a/src/nix/flake-template.nix
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- description = "A flake for building Hello World";
-
- outputs = { self, nixpkgs }: {
-
- packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
-
- defaultPackage.x86_64-linux = self.packages.x86_64-linux.hello;
-
- };
-}
diff --git a/src/nix/flake.cc b/src/nix/flake.cc
index 15a3e261a..439003908 100644
--- a/src/nix/flake.cc
+++ b/src/nix/flake.cc
@@ -207,6 +207,8 @@ struct CmdFlakeCheck : FlakeCommand
auto state = getEvalState();
auto flake = lockFlake();
+ // FIXME: rewrite to use EvalCache.
+
auto checkSystemName = [&](const std::string & system, const Pos & pos) {
// FIXME: what's the format of "system"?
if (system.find('-') == std::string::npos)
@@ -320,6 +322,40 @@ struct CmdFlakeCheck : FlakeCommand
}
};
+ auto checkTemplate = [&](const std::string & attrPath, Value & v, const Pos & pos) {
+ try {
+ Activity act(*logger, lvlChatty, 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")) {
+ PathSet context;
+ auto path = state->coerceToPath(*attr->pos, *attr->value, context);
+ if (!store->isInStore(path))
+ throw Error("template '%s' has a bad 'path' attribute");
+ // 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 name(attr.name);
+ if (name != "path" && name != "description")
+ throw Error("template '%s' has unsupported attribute '%s'", attrPath, name);
+ }
+ } catch (Error & e) {
+ e.addPrefix(fmt("while checking the template '" ANSI_BOLD "%s" ANSI_NORMAL "' at %s:\n", attrPath, pos));
+ throw;
+ }
+ };
+
{
Activity act(*logger, lvlInfo, actUnknown, "evaluating flake");
@@ -432,6 +468,16 @@ struct CmdFlakeCheck : FlakeCommand
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, attr.name),
+ *attr.value, *attr.pos);
+ }
+
else
warn("unknown flake output '%s'", name);
@@ -449,29 +495,135 @@ struct CmdFlakeCheck : FlakeCommand
}
};
-struct CmdFlakeInit : virtual Args, Command
+struct CmdFlakeInitCommon : virtual Args, EvalCommand
{
- std::string description() override
+ std::string templateUrl = "templates";
+ Path destDir;
+
+ CmdFlakeInitCommon()
{
- return "create a skeleton 'flake.nix' file in the current directory";
+ addFlag({
+ .longName = "template",
+ .shortName = 't',
+ .description = "the template to use",
+ .labels = {"template"},
+ .handler = {&templateUrl},
+ .completer = {[&](size_t, std::string_view prefix) {
+ completeFlakeRef(prefix);
+ }}
+ });
}
- void run() override
+ void run(nix::ref<nix::Store> store) override
+ {
+ auto flakeDir = absPath(destDir);
+
+ auto evalState = getEvalState();
+
+ auto [templateFlakeRef, templateName] = parseFlakeRefWithFragment(templateUrl, absPath("."));
+
+ auto installable = InstallableFlake(
+ evalState, std::move(templateFlakeRef),
+ Strings{templateName == "" ? "defaultTemplate" : templateName},
+ Strings{"templates."}, { .writeLockFile = false });
+
+ auto cursor = installable.getCursor(*evalState, true);
+
+ auto templateDir = cursor.first->getAttr("path")->getString();
+
+ assert(store->isInStore(templateDir));
+
+ std::vector<Path> files;
+
+ std::function<void(const Path & from, const Path & to)> copyDir;
+ copyDir = [&](const Path & from, const Path & to)
+ {
+ createDirs(to);
+
+ for (auto & entry : readDirectory(from)) {
+ auto from2 = from + "/" + entry.name;
+ auto to2 = to + "/" + entry.name;
+ 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)
+ throw Error("refusing to overwrite existing file '%s'", to2);
+ } else
+ writeFile(to2, contents);
+ }
+ else if (S_ISLNK(st.st_mode)) {
+ auto target = readLink(from2);
+ if (pathExists(to2)) {
+ if (readLink(to2) != target)
+ throw Error("refusing to overwrite existing symlink '%s'", to2);
+ } else
+ createSymlink(target, to2);
+ }
+ else
+ throw Error("file '%s' has unsupported type", from2);
+ files.push_back(to2);
+ }
+ };
+
+ copyDir(templateDir, flakeDir);
+
+ if (pathExists(flakeDir + "/.git")) {
+ Strings args = { "-C", flakeDir, "add", "--intent-to-add", "--" };
+ for (auto & s : files) args.push_back(s);
+ runProgram("git", true, args);
+ }
+ }
+};
+
+struct CmdFlakeInit : CmdFlakeInitCommon
+{
+ std::string description() override
{
- Path flakeDir = absPath(".");
+ return "create a flake in the current directory from a template";
+ }
- Path flakePath = flakeDir + "/flake.nix";
+ Examples examples() override
+ {
+ return {
+ Example{
+ "To create a flake using the default template:",
+ "nix flake init"
+ },
+ Example{
+ "To see available templates:",
+ "nix flake show templates"
+ },
+ Example{
+ "To create a flake from a specific template:",
+ "nix flake init -t templates#nixos-container"
+ },
+ };
+ }
- if (pathExists(flakePath))
- throw Error("file '%s' already exists", flakePath);
+ CmdFlakeInit()
+ {
+ destDir = ".";
+ }
+};
- writeFile(flakePath,
- #include "flake-template.nix.gen.hh"
- );
+struct CmdFlakeNew : CmdFlakeInitCommon
+{
+ std::string description() override
+ {
+ return "create a flake in the specified directory from a template";
+ }
- if (pathExists(flakeDir + "/.git"))
- runProgram("git", true,
- { "-C", flakeDir, "add", "--intent-to-add", "flake.nix" });
+ CmdFlakeNew()
+ {
+ expectArgs({
+ .label = "dest-dir",
+ .handler = {&destDir},
+ .completer = completePath
+ });
}
};
@@ -662,7 +814,8 @@ struct CmdFlakeShow : FlakeCommand
|| attrPath[0] == "devShell"
|| attrPath[0] == "nixosConfigurations"
|| attrPath[0] == "nixosModules"
- || attrPath[0] == "defaultApp"))
+ || attrPath[0] == "defaultApp"
+ || attrPath[0] == "templates"))
|| ((attrPath.size() == 1 || attrPath.size() == 2)
&& (attrPath[0] == "checks"
|| attrPath[0] == "packages"
@@ -714,6 +867,14 @@ struct CmdFlakeShow : FlakeCommand
logger->stdout("%s: app", headerPrefix);
}
+ else if (
+ (attrPath.size() == 1 && attrPath[0] == "defaultTemplate") ||
+ (attrPath.size() == 2 && attrPath[0] == "templates"))
+ {
+ auto description = visitor.getAttr("description")->getString();
+ logger->stdout("%s: template: " ANSI_BOLD "%s" ANSI_NORMAL, headerPrefix, description);
+ }
+
else {
logger->stdout("%s: %s",
headerPrefix,
@@ -743,6 +904,7 @@ struct CmdFlake : virtual MultiCommand, virtual Command
{"list-inputs", []() { return make_ref<CmdFlakeListInputs>(); }},
{"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>(); }},
{"show", []() { return make_ref<CmdFlakeShow>(); }},
diff --git a/src/nix/installables.cc b/src/nix/installables.cc
index f471319be..4b171dcba 100644
--- a/src/nix/installables.cc
+++ b/src/nix/installables.cc
@@ -237,7 +237,7 @@ App Installable::toApp(EvalState & state)
}
std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>>
-Installable::getCursor(EvalState & state, bool useEvalCache)
+Installable::getCursors(EvalState & state, bool useEvalCache)
{
auto evalCache =
std::make_shared<nix::eval_cache::EvalCache>(false, Hash(), state,
@@ -245,6 +245,15 @@ Installable::getCursor(EvalState & state, bool useEvalCache)
return {{evalCache->getRoot(), ""}};
}
+std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>
+Installable::getCursor(EvalState & state, bool useEvalCache)
+{
+ auto cursors = getCursors(state, useEvalCache);
+ if (cursors.empty())
+ throw Error("cannot find flake attribute '%s'", what());
+ return cursors[0];
+}
+
struct InstallableStorePath : Installable
{
ref<Store> store;
@@ -474,7 +483,7 @@ std::pair<Value *, Pos> InstallableFlake::toValue(EvalState & state)
}
std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>>
-InstallableFlake::getCursor(EvalState & state, bool useEvalCache)
+InstallableFlake::getCursors(EvalState & state, bool useEvalCache)
{
auto evalCache = openEvalCache(state,
std::make_shared<flake::LockedFlake>(lockFlake(state, flakeRef, lockFlags)),
diff --git a/src/nix/installables.hh b/src/nix/installables.hh
index a2db71389..1e6623f88 100644
--- a/src/nix/installables.hh
+++ b/src/nix/installables.hh
@@ -56,6 +56,9 @@ struct Installable
}
virtual std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>>
+ getCursors(EvalState & state, bool useEvalCache);
+
+ std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>
getCursor(EvalState & state, bool useEvalCache);
virtual FlakeRef nixpkgsFlakeRef() const
@@ -109,7 +112,7 @@ struct InstallableFlake : InstallableValue
std::pair<Value *, Pos> toValue(EvalState & state) override;
std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>>
- getCursor(EvalState & state, bool useEvalCache) override;
+ getCursors(EvalState & state, bool useEvalCache) override;
std::shared_ptr<flake::LockedFlake> getLockedFlake() const;
diff --git a/src/nix/local.mk b/src/nix/local.mk
index 9ed55f1f6..43b7754e3 100644
--- a/src/nix/local.mk
+++ b/src/nix/local.mk
@@ -29,5 +29,3 @@ $(eval $(call install-symlink, $(bindir)/nix, $(libexecdir)/nix/build-remote))
src/nix-env/user-env.cc: src/nix-env/buildenv.nix.gen.hh
src/nix/develop.cc: src/nix/get-env.sh.gen.hh
-
-$(d)/flake.cc: $(d)/flake-template.nix.gen.hh
diff --git a/src/nix/search.cc b/src/nix/search.cc
index bbac56fcb..65a1e1818 100644
--- a/src/nix/search.cc
+++ b/src/nix/search.cc
@@ -177,7 +177,7 @@ struct CmdSearch : InstallableCommand, MixJSON
}
};
- for (auto & [cursor, prefix] : installable->getCursor(*state, true))
+ for (auto & [cursor, prefix] : installable->getCursors(*state, true))
visit(*cursor, parseAttrPath(*state, prefix));
if (!json && !results)
diff --git a/tests/flakes.sh b/tests/flakes.sh
index 6a550ef32..fdf31f5c1 100644
--- a/tests/flakes.sh
+++ b/tests/flakes.sh
@@ -20,12 +20,14 @@ flake2Dir=$TEST_ROOT/flake2
flake3Dir=$TEST_ROOT/flake3
flake4Dir=$TEST_ROOT/flake4
flake5Dir=$TEST_ROOT/flake5
+flake6Dir=$TEST_ROOT/flake6
flake7Dir=$TEST_ROOT/flake7
+templatesDir=$TEST_ROOT/templates
nonFlakeDir=$TEST_ROOT/nonFlake
flakeA=$TEST_ROOT/flakeA
flakeB=$TEST_ROOT/flakeB
-for repo in $flake1Dir $flake2Dir $flake3Dir $flake7Dir $nonFlakeDir $flakeA $flakeB; do
+for repo in $flake1Dir $flake2Dir $flake3Dir $flake7Dir $templatesDir $nonFlakeDir $flakeA $flakeB; do
rm -rf $repo $repo.tmp
mkdir $repo
git -C $repo init
@@ -145,13 +147,22 @@ cat > $registry <<EOF
"type": "indirect",
"id": "flake1"
}
+ },
+ { "from": {
+ "type": "indirect",
+ "id": "templates"
+ },
+ "to": {
+ "type": "git",
+ "url": "file://$templatesDir"
+ }
}
]
}
EOF
# Test 'nix flake list'.
-[[ $(nix registry list | wc -l) == 6 ]]
+[[ $(nix registry list | wc -l) == 7 ]]
# Test 'nix flake info'.
nix flake info flake1 | grep -q 'URL: .*flake1.*'
@@ -392,18 +403,61 @@ nix build -o $TEST_ROOT/result flake4/removeXyzzy#sth
# Testing the nix CLI
nix registry add flake1 flake3
-[[ $(nix registry list | wc -l) == 7 ]]
+[[ $(nix registry list | wc -l) == 8 ]]
nix registry pin flake1
-[[ $(nix registry list | wc -l) == 7 ]]
+[[ $(nix registry list | wc -l) == 8 ]]
nix registry remove flake1
-[[ $(nix registry list | wc -l) == 6 ]]
+[[ $(nix registry list | wc -l) == 7 ]]
# Test 'nix flake init'.
+cat > $templatesDir/flake.nix <<EOF
+{
+ description = "Some templates";
+
+ outputs = { self }: {
+ templates = {
+ trivial = {
+ path = ./trivial;
+ description = "A trivial flake";
+ };
+ };
+ defaultTemplate = self.templates.trivial;
+ };
+}
+EOF
+
+mkdir $templatesDir/trivial
+
+cat > $templatesDir/trivial/flake.nix <<EOF
+{
+ description = "A flake for building Hello World";
+
+ outputs = { self, nixpkgs }: {
+ packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
+ defaultPackage.x86_64-linux = self.packages.x86_64-linux.hello;
+ };
+}
+EOF
+
+git -C $templatesDir add flake.nix trivial/flake.nix
+git -C $templatesDir commit -m 'Initial'
+
+nix flake check templates
+nix flake show templates
+
(cd $flake7Dir && nix flake init)
+(cd $flake7Dir && nix flake init) # check idempotence
git -C $flake7Dir add flake.nix
nix flake check $flake7Dir
+nix flake show $flake7Dir
git -C $flake7Dir commit -a -m 'Initial'
+# Test 'nix flake new'.
+rm -rf $flake6Dir
+nix flake new -t templates#trivial $flake6Dir
+nix flake new -t templates#trivial $flake6Dir # check idempotence
+nix flake check $flake6Dir
+
# Test 'nix flake clone'.
rm -rf $TEST_ROOT/flake1-v2
nix flake clone flake1 --dest $TEST_ROOT/flake1-v2
@@ -663,4 +717,4 @@ git -C $flakeB commit -a -m 'Foo'
[[ $(nix eval --update-input b $flakeA#foo) = 1912 ]]
# Test list-inputs with circular dependencies
-nix flake list-inputs $flakeA \ No newline at end of file
+nix flake list-inputs $flakeA