From 0b337fca3469dd1a40c9c00cb5f11d6f85b8cead Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 18 Sep 2020 13:40:29 +0200 Subject: [PATCH] Add 'nix doc' command This command generates HTML docs (using mdbook) for a flake. --- corepkgs/module.nix | 6 +-- src/nix/bundle.cc | 4 +- src/nix/command.hh | 17 ++++++++ src/nix/doc.cc | 101 ++++++++++++++++++++++++++++++++++++++++++++ src/nix/doc.nix | 94 +++++++++++++++++++++++++++++++++++++++++ src/nix/flake.cc | 61 ++++++++++++-------------- src/nix/local.mk | 2 + src/nix/options.cc | 10 ++--- 8 files changed, 251 insertions(+), 44 deletions(-) create mode 100644 src/nix/doc.cc create mode 100644 src/nix/doc.nix diff --git a/corepkgs/module.nix b/corepkgs/module.nix index c8c4228d0..bb1b809c2 100644 --- a/corepkgs/module.nix +++ b/corepkgs/module.nix @@ -12,14 +12,14 @@ let in -{ description ? null, extends ? [], options ? {}, config ? ({ config }: {}) } @ inArgs: +{ doc ? null, extends ? [], options ? {}, config ? ({ config }: {}) } @ inArgs: let thisModule = rec { type = "module"; _module = { - inherit description extends options config; - }; + inherit extends options config; + } // (if doc != null then { inherit doc; } else {}); _allModules = [thisModule] ++ builtins.concatLists (map (mod: assert mod.type or "" == "module"; mod._allModules) extends); diff --git a/src/nix/bundle.cc b/src/nix/bundle.cc index 241c8699b..fc41da9e4 100644 --- a/src/nix/bundle.cc +++ b/src/nix/bundle.cc @@ -98,14 +98,14 @@ struct CmdBundle : InstallableCommand if (!evalState->isDerivation(*vRes)) throw Error("the bundler '%s' does not produce a derivation", bundler.what()); - auto attr1 = vRes->attrs->find(evalState->sDrvPath); + auto attr1 = vRes->attrs->get(evalState->sDrvPath); if (!attr1) throw Error("the bundler '%s' does not produce a derivation", bundler.what()); PathSet context2; StorePath drvPath = store->parseStorePath(evalState->coerceToPath(*attr1->pos, *attr1->value, context2)); - auto attr2 = vRes->attrs->find(evalState->sOutPath); + auto attr2 = vRes->attrs->get(evalState->sOutPath); if (!attr2) throw Error("the bundler '%s' does not produce a derivation", bundler.what()); diff --git a/src/nix/command.hh b/src/nix/command.hh index d60c8aeb6..d862740ef 100644 --- a/src/nix/command.hh +++ b/src/nix/command.hh @@ -58,6 +58,23 @@ struct MixFlakeOptions : virtual Args, EvalCommand { return {}; } }; +class FlakeCommand : virtual Args, public MixFlakeOptions +{ + std::string flakeUrl = "."; + +public: + + FlakeCommand(); + + FlakeRef getFlakeRef(); + + flake::Flake getFlake(); + + flake::LockedFlake lockFlake(); + + std::optional getFlakeRefForCompletion() override; +}; + /* How to handle derivations in commands that operate on store paths. */ enum class OperateOn { /* Operate on the output path. */ diff --git a/src/nix/doc.cc b/src/nix/doc.cc new file mode 100644 index 000000000..3f46eda41 --- /dev/null +++ b/src/nix/doc.cc @@ -0,0 +1,101 @@ +#include "command.hh" +#include "common-args.hh" +#include "eval-cache.hh" +#include "eval-inline.hh" + +using namespace nix; +using namespace nix::flake; + +// FIXME: move +StorePath buildDerivation(EvalState & state, Value & vDerivation) +{ + state.forceValue(vDerivation); + if (!state.isDerivation(vDerivation)) + throw Error("value did not evaluate to a derivation"); + + auto aDrvPath = vDerivation.attrs->get(state.sDrvPath); + assert(aDrvPath); + PathSet context; + auto drvPath = state.store->parseStorePath(state.coerceToPath(*aDrvPath->pos, *aDrvPath->value, context)); + + state.store->buildPaths({{drvPath}}); + + auto aOutPath = vDerivation.attrs->get(state.sOutPath); + assert(aOutPath); + auto outPath = state.store->parseStorePath(state.coerceToPath(*aOutPath->pos, *aOutPath->value, context)); + + assert(state.store->isValidPath(outPath)); + + return outPath; +} + +struct CmdDoc : FlakeCommand +{ + std::optional outLink = "flake-doc"; + bool printMarkdown = false; + + CmdDoc() + { + // FIXME: cut&paste from 'nix build'. + addFlag({ + .longName = "out-link", + .shortName = 'o', + .description = "path of the symlink to the build result", + .labels = {"path"}, + .handler = {&outLink}, + .completer = completePath + }); + + addFlag({ + .longName = "no-link", + .description = "do not create a symlink to the build result", + .handler = {&outLink, {}}, + }); + + addFlag({ + .longName = "print-markdown", + .description = "show markdown, don't generate an HTML book", + .handler = {&this->printMarkdown, true}, + }); + } + + void run(nix::ref store) override + { + auto state = getEvalState(); + auto flake = std::make_shared(lockFlake()); + + auto vFlake = state->allocValue(); + flake::callFlake(*state, *flake, *vFlake); + + auto vFun = state->allocValue(); + state->eval(state->parseExprFromString( + #include "doc.nix.gen.hh" + , "/"), *vFun); + + auto vRes = state->allocValue(); + state->callFunction(*vFun, *vFlake, *vRes, noPos); + state->forceAttrs(*vRes, noPos); + + auto markdown = vRes->attrs->get(state->symbols.create("markdown")); + assert(markdown); + + if (printMarkdown) { + logger->stdout(state->forceString(*markdown->value)); + return; + } + + auto mdbook = vRes->attrs->get(state->symbols.create("mdbook")); + assert(mdbook); + + // FIXME: ugly, needed for getFlake. + evalSettings.pureEval = false; + + auto path = buildDerivation(*state, *mdbook->value); + + if (outLink) + if (auto store2 = store.dynamic_pointer_cast()) + store2->addPermRoot(path, absPath(*outLink)); + } +}; + +static auto r1 = registerCommand("doc"); diff --git a/src/nix/doc.nix b/src/nix/doc.nix new file mode 100644 index 000000000..203693c27 --- /dev/null +++ b/src/nix/doc.nix @@ -0,0 +1,94 @@ +with builtins; + +flake: + +let + + splitLines = s: filter (x: !isList x) (split "\n" s); + + concatStrings = concatStringsSep ""; + + modules = flake.modules or {}; + +in + +rec { + markdown = + # FIXME: split into multiple files. + + "# Outputs\n\n" + + + concatStrings (map + (outputName: + " - `${outputName}` \n\n") + (attrNames flake.outputs)) + + + (if modules != {} then + "# Modules\n\n" + + concatStrings (map + (moduleName: + let + module = modules.${moduleName}; + in + "## `${moduleName}`\n\n" + + (if module._module.doc or "" != "" + then "### Synopsis\n\n${module._module.doc}\n\n" + else "") + + (if module._module.options != {} + then + "### Options\n\n" + + concatStrings (map + (optionName: + let option = module._module.options.${optionName}; in + " - `${optionName}` \n\n" + + concatStrings (map (l: " ${l}\n") (splitLines option.doc)) + + "\n" + ) + (attrNames module._module.options)) + else "") + ) + (attrNames modules)) + else ""); + + mdbook = + let + nixpkgs = getFlake "nixpkgs"; + pkgs = nixpkgs.legacyPackages.x86_64-linux; # FIXME + in + pkgs.runCommand "flake-doc" + { buildInputs = [ pkgs.mdbook ]; + } + '' + mkdir $out + mkdir -p book/src + + cat > book/book.toml < book/custom.css < book/src/SUMMARY.md < getFlakeRefForCompletion() override - { - return getFlakeRef(); - } -}; +std::optional FlakeCommand::getFlakeRefForCompletion() +{ + return getFlakeRef(); +} static void printFlakeInfo(const Store & store, const Flake & flake) { diff --git a/src/nix/local.mk b/src/nix/local.mk index ab4e9121b..ad2cec930 100644 --- a/src/nix/local.mk +++ b/src/nix/local.mk @@ -31,3 +31,5 @@ src/nix-env/user-env.cc: src/nix-env/buildenv.nix.gen.hh src/nix/develop.cc: src/nix/get-env.sh.gen.hh src/nix-channel/nix-channel.cc: src/nix-channel/unpack-channel.nix.gen.hh + +src/nix/doc.cc: src/nix/doc.nix.gen.hh diff --git a/src/nix/options.cc b/src/nix/options.cc index 486063f01..4827ebc61 100644 --- a/src/nix/options.cc +++ b/src/nix/options.cc @@ -45,12 +45,12 @@ struct CmdListOptions : InstallableCommand state->forceAttrs(*option->value); - std::string description = ANSI_ITALIC "" ANSI_NORMAL; - auto aDescription = option->value->attrs->get(state->symbols.create("description")); - if (aDescription) + std::string doc = ANSI_ITALIC "" ANSI_NORMAL; + auto aDoc = option->value->attrs->get(state->symbols.create("doc")); + if (aDoc) // FIXME: render markdown. - description = state->forceString(*aDescription->value); - logger->stdout(" " ANSI_BOLD "Description:" ANSI_NORMAL " %s", description); + doc = state->forceString(*aDoc->value); + logger->stdout(" " ANSI_BOLD "Description:" ANSI_NORMAL " %s", doc); auto aValue = aFinal->value->attrs->get(option->name); assert(aValue);