diff options
author | Eelco Dolstra <edolstra@gmail.com> | 2020-06-04 20:02:50 +0200 |
---|---|---|
committer | Eelco Dolstra <edolstra@gmail.com> | 2020-06-04 20:22:25 +0200 |
commit | 810b2c6a48b5ecd468bd4f65963ce5a7aa96832e (patch) | |
tree | 6d0cf088c2b5ef713ef00d81da9413eb23916569 | |
parent | dc305500c38c5e2227bea696949970c562046a8f (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.cc | 8 | ||||
-rw-r--r-- | src/nix/flake-template.nix | 11 | ||||
-rw-r--r-- | src/nix/flake.cc | 192 | ||||
-rw-r--r-- | src/nix/installables.cc | 13 | ||||
-rw-r--r-- | src/nix/installables.hh | 5 | ||||
-rw-r--r-- | src/nix/local.mk | 2 | ||||
-rw-r--r-- | src/nix/search.cc | 2 | ||||
-rw-r--r-- | tests/flakes.sh | 66 |
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 |