diff options
author | John Ericson <John.Ericson@Obsidian.Systems> | 2020-09-01 18:01:48 +0000 |
---|---|---|
committer | John Ericson <John.Ericson@Obsidian.Systems> | 2020-09-01 18:01:48 +0000 |
commit | ef278d00f92cddba48a463a38107276b029dd66b (patch) | |
tree | be87cd383b0bf9610f63e45258614f99c6ed62c2 /src | |
parent | 8017fe7487ff545ac7be68bd7b339fffffa12b8f (diff) | |
parent | 6d7f7efb896c482ff6e7ce6c105e4dd3f7ee7218 (diff) |
Merge remote-tracking branch 'upstream/master' into single-ca-drv-build
Diffstat (limited to 'src')
35 files changed, 2657 insertions, 435 deletions
diff --git a/src/libexpr/common-eval-args.cc b/src/libexpr/common-eval-args.cc index 6b48ead1f..10c1a6975 100644 --- a/src/libexpr/common-eval-args.cc +++ b/src/libexpr/common-eval-args.cc @@ -29,7 +29,7 @@ MixEvalArgs::MixEvalArgs() addFlag({ .longName = "include", .shortName = 'I', - .description = "add a path to the list of locations used to look up <...> file names", + .description = "add a path to the list of locations used to look up `<...>` file names", .labels = {"path"}, .handler = {[&](std::string s) { searchPath.push_back(s); }} }); diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 00191bce0..8c97b3760 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -381,10 +381,14 @@ EvalState::EvalState(const Strings & _searchPath, ref<Store> store) auto path = r.second; if (store->isInStore(r.second)) { - StorePathSet closure; - store->computeFSClosure(store->toStorePath(r.second).first, closure); - for (auto & path : closure) - allowedPaths->insert(store->printStorePath(path)); + try { + StorePathSet closure; + store->computeFSClosure(store->toStorePath(r.second).first, closure); + for (auto & path : closure) + allowedPaths->insert(store->printStorePath(path)); + } catch (InvalidPath &) { + allowedPaths->insert(r.second); + } } else allowedPaths->insert(r.second); } @@ -509,7 +513,7 @@ Value * EvalState::addPrimOp(const string & name, if (arity == 0) { auto vPrimOp = allocValue(); vPrimOp->type = tPrimOp; - vPrimOp->primOp = new PrimOp(primOp, 1, sym); + vPrimOp->primOp = new PrimOp { .fun = primOp, .arity = 1, .name = sym }; Value v; mkApp(v, *vPrimOp, *vPrimOp); return addConstant(name, v); @@ -517,7 +521,7 @@ Value * EvalState::addPrimOp(const string & name, Value * v = allocValue(); v->type = tPrimOp; - v->primOp = new PrimOp(primOp, arity, sym); + v->primOp = new PrimOp { .fun = primOp, .arity = arity, .name = sym }; staticBaseEnv.vars[symbols.create(name)] = baseEnvDispl; baseEnv.values[baseEnvDispl++] = v; baseEnv.values[0]->attrs->push_back(Attr(sym, v)); @@ -525,12 +529,59 @@ Value * EvalState::addPrimOp(const string & name, } +Value * EvalState::addPrimOp(PrimOp && primOp) +{ + /* Hack to make constants lazy: turn them into a application of + the primop to a dummy value. */ + if (primOp.arity == 0) { + primOp.arity = 1; + auto vPrimOp = allocValue(); + vPrimOp->type = tPrimOp; + vPrimOp->primOp = new PrimOp(std::move(primOp)); + Value v; + mkApp(v, *vPrimOp, *vPrimOp); + return addConstant(primOp.name, v); + } + + Symbol envName = primOp.name; + if (hasPrefix(primOp.name, "__")) + primOp.name = symbols.create(std::string(primOp.name, 2)); + + Value * v = allocValue(); + v->type = tPrimOp; + v->primOp = new PrimOp(std::move(primOp)); + staticBaseEnv.vars[envName] = baseEnvDispl; + baseEnv.values[baseEnvDispl++] = v; + baseEnv.values[0]->attrs->push_back(Attr(primOp.name, v)); + return v; +} + + Value & EvalState::getBuiltin(const string & name) { return *baseEnv.values[0]->attrs->find(symbols.create(name))->value; } +std::optional<EvalState::Doc> EvalState::getDoc(Value & v) +{ + if (v.type == tPrimOp || v.type == tPrimOpApp) { + auto v2 = &v; + while (v2->type == tPrimOpApp) + v2 = v2->primOpApp.left; + if (v2->primOp->doc) + return Doc { + .pos = noPos, + .name = v2->primOp->name, + .arity = v2->primOp->arity, + .args = v2->primOp->args, + .doc = v2->primOp->doc, + }; + } + return {}; +} + + /* Every "format" object (even temporary) takes up a few hundred bytes of stack space, which is a real killer in the recursive evaluator. So here are some helper functions for throwing diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 5855b4ef2..80078d8a5 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -30,8 +30,8 @@ struct PrimOp PrimOpFun fun; size_t arity; Symbol name; - PrimOp(PrimOpFun fun, size_t arity, Symbol name) - : fun(fun), arity(arity), name(name) { } + std::vector<std::string> args; + const char * doc = nullptr; }; @@ -242,10 +242,23 @@ private: Value * addPrimOp(const string & name, size_t arity, PrimOpFun primOp); + Value * addPrimOp(PrimOp && primOp); + public: Value & getBuiltin(const string & name); + struct Doc + { + Pos pos; + std::optional<Symbol> name; + size_t arity; + std::vector<std::string> args; + const char * doc; + }; + + std::optional<Doc> getDoc(Value & v); + private: inline Value * lookupVar(Env * env, const ExprVar & var, bool noEval); @@ -357,24 +370,57 @@ struct EvalSettings : Config Setting<bool> enableNativeCode{this, false, "allow-unsafe-native-code-during-evaluation", "Whether builtin functions that allow executing native code should be enabled."}; - Setting<Strings> nixPath{this, getDefaultNixPath(), "nix-path", - "List of directories to be searched for <...> file references."}; + Setting<Strings> nixPath{ + this, getDefaultNixPath(), "nix-path", + "List of directories to be searched for `<...>` file references."}; - Setting<bool> restrictEval{this, false, "restrict-eval", - "Whether to restrict file system access to paths in $NIX_PATH, " - "and network access to the URI prefixes listed in 'allowed-uris'."}; + Setting<bool> restrictEval{ + this, false, "restrict-eval", + R"( + If set to `true`, the Nix evaluator will not allow access to any + files outside of the Nix search path (as set via the `NIX_PATH` + environment variable or the `-I` option), or to URIs outside of + `allowed-uri`. The default is `false`. + )"}; Setting<bool> pureEval{this, false, "pure-eval", "Whether to restrict file system and network access to files specified by cryptographic hash."}; - Setting<bool> enableImportFromDerivation{this, true, "allow-import-from-derivation", - "Whether the evaluator allows importing the result of a derivation."}; + Setting<bool> enableImportFromDerivation{ + this, true, "allow-import-from-derivation", + R"( + By default, Nix allows you to `import` from a derivation, allowing + building at evaluation time. With this option set to false, Nix will + throw an error when evaluating an expression that uses this feature, + allowing users to ensure their evaluation will not require any + builds to take place. + )"}; Setting<Strings> allowedUris{this, {}, "allowed-uris", - "Prefixes of URIs that builtin functions such as fetchurl and fetchGit are allowed to fetch."}; + R"( + A list of URI prefixes to which access is allowed in restricted + evaluation mode. For example, when set to + `https://github.com/NixOS`, builtin functions such as `fetchGit` are + allowed to access `https://github.com/NixOS/patchelf.git`. + )"}; Setting<bool> traceFunctionCalls{this, false, "trace-function-calls", - "Emit log messages for each function entry and exit at the 'vomit' log level (-vvvv)."}; + R"( + If set to `true`, the Nix evaluator will trace every function call. + Nix will print a log message at the "vomit" level for every function + entrance and function exit. + + function-trace entered undefined position at 1565795816999559622 + function-trace exited undefined position at 1565795816999581277 + function-trace entered /nix/store/.../example.nix:226:41 at 1565795253249935150 + function-trace exited /nix/store/.../example.nix:226:41 at 1565795253249941684 + + The `undefined position` means the function call is a builtin. + + Use the `contrib/stack-collapse.py` script distributed with the Nix + source code to convert the trace logs in to a format suitable for + `flamegraph.pl`. + )"}; Setting<bool> useEvalCache{this, true, "eval-cache", "Whether to use the flake evaluation cache."}; diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 62af42136..4e248f979 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -94,10 +94,10 @@ static void mkOutputString(EvalState & state, Value & v, /* Load and evaluate an expression from path specified by the argument. */ -static void prim_scopedImport(EvalState & state, const Pos & pos, Value * * args, Value & v) +static void import(EvalState & state, const Pos & pos, Value & vPath, Value * vScope, Value & v) { PathSet context; - Path path = state.coerceToPath(pos, *args[1], context); + Path path = state.coerceToPath(pos, vPath, context); try { state.realiseContext(context); @@ -119,6 +119,7 @@ static void prim_scopedImport(EvalState & state, const Pos & pos, Value * * args return std::nullopt; return storePath; }; + if (auto optStorePath = isValidDerivationInStore()) { auto storePath = *optStorePath; Derivation drv = state.store->readDerivation(storePath); @@ -152,17 +153,18 @@ static void prim_scopedImport(EvalState & state, const Pos & pos, Value * * args mkApp(v, **fun, w); state.forceAttrs(v, pos); } else { - state.forceAttrs(*args[0]); - if (args[0]->attrs->empty()) + if (!vScope) state.evalFile(realPath, v); else { - Env * env = &state.allocEnv(args[0]->attrs->size()); + state.forceAttrs(*vScope); + + Env * env = &state.allocEnv(vScope->attrs->size()); env->up = &state.baseEnv; StaticEnv staticEnv(false, &state.staticBaseEnv); unsigned int displ = 0; - for (auto & attr : *args[0]->attrs) { + for (auto & attr : *vScope->attrs) { staticEnv.vars[attr.name] = displ; env->values[displ++] = attr.value; } @@ -175,6 +177,76 @@ static void prim_scopedImport(EvalState & state, const Pos & pos, Value * * args } } +static RegisterPrimOp primop_scopedImport(RegisterPrimOp::Info { + .name = "scopedImport", + .arity = 2, + .fun = [](EvalState & state, const Pos & pos, Value * * args, Value & v) + { + import(state, pos, *args[1], args[0], v); + } +}); + +static RegisterPrimOp primop_import({ + .name = "import", + .args = {"path"}, + .doc = R"( + Load, parse and return the Nix expression in the file *path*. If + *path* is a directory, the file ` default.nix ` in that directory + is loaded. Evaluation aborts if the file doesn’t exist or contains + an incorrect Nix expression. `import` implements Nix’s module + system: you can put any Nix expression (such as a set or a + function) in a separate file, and use it from Nix expressions in + other files. + + > **Note** + > + > Unlike some languages, `import` is a regular function in Nix. + > Paths using the angle bracket syntax (e.g., `import` *\<foo\>*) + > are [normal path values](language-values.md). + + A Nix expression loaded by `import` must not contain any *free + variables* (identifiers that are not defined in the Nix expression + itself and are not built-in). Therefore, it cannot refer to + variables that are in scope at the call site. For instance, if you + have a calling expression + + ```nix + rec { + x = 123; + y = import ./foo.nix; + } + ``` + + then the following `foo.nix` will give an error: + + ```nix + x + 456 + ``` + + since `x` is not in scope in `foo.nix`. If you want `x` to be + available in `foo.nix`, you should pass it as a function argument: + + ```nix + rec { + x = 123; + y = import ./foo.nix x; + } + ``` + + and + + ```nix + x: x + 456 + ``` + + (The function argument doesn’t have to be called `x` in `foo.nix`; + any name would work.) + )", + .fun = [](EvalState & state, const Pos & pos, Value * * args, Value & v) + { + import(state, pos, *args[0], nullptr, v); + } +}); /* Want reasonable symbol names, so extern C */ /* !!! Should we pass the Pos or the file name too? */ @@ -294,6 +366,16 @@ static void prim_typeOf(EvalState & state, const Pos & pos, Value * * args, Valu mkString(v, state.symbols.create(t)); } +static RegisterPrimOp primop_typeOf({ + .name = "__typeOf", + .args = {"e"}, + .doc = R"( + Return a string representing the type of the value *e*, namely + `"int"`, `"bool"`, `"string"`, `"path"`, `"null"`, `"set"`, + `"list"`, `"lambda"` or `"float"`. + )", + .fun = prim_typeOf, +}); /* Determine whether the argument is the null value. */ static void prim_isNull(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -302,6 +384,18 @@ static void prim_isNull(EvalState & state, const Pos & pos, Value * * args, Valu mkBool(v, args[0]->type == tNull); } +static RegisterPrimOp primop_isNull({ + .name = "isNull", + .args = {"e"}, + .doc = R"( + Return `true` if *e* evaluates to `null`, and `false` otherwise. + + > **Warning** + > + > This function is *deprecated*; just write `e == null` instead. + )", + .fun = prim_isNull, +}); /* Determine whether the argument is a function. */ static void prim_isFunction(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -321,6 +415,14 @@ static void prim_isFunction(EvalState & state, const Pos & pos, Value * * args, mkBool(v, res); } +static RegisterPrimOp primop_isFunction({ + .name = "__isFunction", + .args = {"e"}, + .doc = R"( + Return `true` if *e* evaluates to a function, and `false` otherwise. + )", + .fun = prim_isFunction, +}); /* Determine whether the argument is an integer. */ static void prim_isInt(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -329,6 +431,15 @@ static void prim_isInt(EvalState & state, const Pos & pos, Value * * args, Value mkBool(v, args[0]->type == tInt); } +static RegisterPrimOp primop_isInt({ + .name = "__isInt", + .args = {"e"}, + .doc = R"( + Return `true` if *e* evaluates to an integer, and `false` otherwise. + )", + .fun = prim_isInt, +}); + /* Determine whether the argument is a float. */ static void prim_isFloat(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -336,6 +447,15 @@ static void prim_isFloat(EvalState & state, const Pos & pos, Value * * args, Val mkBool(v, args[0]->type == tFloat); } +static RegisterPrimOp primop_isFloat({ + .name = "__isFloat", + .args = {"e"}, + .doc = R"( + Return `true` if *e* evaluates to a float, and `false` otherwise. + )", + .fun = prim_isFloat, +}); + /* Determine whether the argument is a string. */ static void prim_isString(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -343,6 +463,14 @@ static void prim_isString(EvalState & state, const Pos & pos, Value * * args, Va mkBool(v, args[0]->type == tString); } +static RegisterPrimOp primop_isString({ + .name = "__isString", + .args = {"e"}, + .doc = R"( + Return `true` if *e* evaluates to a string, and `false` otherwise. + )", + .fun = prim_isString, +}); /* Determine whether the argument is a Boolean. */ static void prim_isBool(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -351,6 +479,15 @@ static void prim_isBool(EvalState & state, const Pos & pos, Value * * args, Valu mkBool(v, args[0]->type == tBool); } +static RegisterPrimOp primop_isBool({ + .name = "__isBool", + .args = {"e"}, + .doc = R"( + Return `true` if *e* evaluates to a bool, and `false` otherwise. + )", + .fun = prim_isBool, +}); + /* Determine whether the argument is a path. */ static void prim_isPath(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -358,6 +495,15 @@ static void prim_isPath(EvalState & state, const Pos & pos, Value * * args, Valu mkBool(v, args[0]->type == tPath); } +static RegisterPrimOp primop_isPath({ + .name = "__isPath", + .args = {"e"}, + .doc = R"( + Return `true` if *e* evaluates to a path, and `false` otherwise. + )", + .fun = prim_isPath, +}); + struct CompareValues { bool operator () (const Value * v1, const Value * v2) const @@ -463,22 +609,43 @@ static void prim_genericClosure(EvalState & state, const Pos & pos, Value * * ar v.listElems()[n++] = i; } +static RegisterPrimOp primop_genericClosure(RegisterPrimOp::Info { + .name = "__genericClosure", + .arity = 1, + .fun = prim_genericClosure, +}); -static void prim_abort(EvalState & state, const Pos & pos, Value * * args, Value & v) -{ - PathSet context; - string s = state.coerceToString(pos, *args[0], context); - throw Abort("evaluation aborted with the following error message: '%1%'", s); -} - - -static void prim_throw(EvalState & state, const Pos & pos, Value * * args, Value & v) -{ - PathSet context; - string s = state.coerceToString(pos, *args[0], context); - throw ThrownError(s); -} - +static RegisterPrimOp primop_abort({ + .name = "abort", + .args = {"s"}, + .doc = R"( + Abort Nix expression evaluation and print the error message *s*. + )", + .fun = [](EvalState & state, const Pos & pos, Value * * args, Value & v) + { + PathSet context; + string s = state.coerceToString(pos, *args[0], context); + throw Abort("evaluation aborted with the following error message: '%1%'", s); + } +}); + +static RegisterPrimOp primop_throw({ + .name = "throw", + .args = {"s"}, + .doc = R"( + Throw an error message *s*. This usually aborts Nix expression + evaluation, but in `nix-env -qa` and other commands that try to + evaluate a set of derivations to get information about those + derivations, a derivation that throws an error is silently skipped + (which is not the case for `abort`). + )", + .fun = [](EvalState & state, const Pos & pos, Value * * args, Value & v) + { + PathSet context; + string s = state.coerceToString(pos, *args[0], context); + throw ThrownError(s); + } +}); static void prim_addErrorContext(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -492,6 +659,11 @@ static void prim_addErrorContext(EvalState & state, const Pos & pos, Value * * a } } +static RegisterPrimOp primop_addErrorContext(RegisterPrimOp::Info { + .name = "__addErrorContext", + .arity = 2, + .fun = prim_addErrorContext, +}); /* Try evaluating the argument. Success => {success=true; value=something;}, * else => {success=false; value=false;} */ @@ -509,6 +681,22 @@ static void prim_tryEval(EvalState & state, const Pos & pos, Value * * args, Val v.attrs->sort(); } +static RegisterPrimOp primop_tryEval({ + .name = "__tryEval", + .args = {"e"}, + .doc = R"( + Try to shallowly evaluate *e*. Return a set containing the + attributes `success` (`true` if *e* evaluated successfully, + `false` if an error was thrown) and `value`, equalling *e* if + successful and `false` otherwise. Note that this doesn't evaluate + *e* deeply, so ` let e = { x = throw ""; }; in (builtins.tryEval + e).success ` will be `true`. Using ` builtins.deepSeq ` one can + get the expected result: `let e = { x = throw ""; }; in + (builtins.tryEval (builtins.deepSeq e e)).success` will be + `false`. + )", + .fun = prim_tryEval, +}); /* Return an environment variable. Use with care. */ static void prim_getEnv(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -517,6 +705,22 @@ static void prim_getEnv(EvalState & state, const Pos & pos, Value * * args, Valu mkString(v, evalSettings.restrictEval || evalSettings.pureEval ? "" : getEnv(name).value_or("")); } +static RegisterPrimOp primop_getEnv({ + .name = "__getEnv", + .args = {"s"}, + .doc = R"( + `getEnv` returns the value of the environment variable *s*, or an + empty string if the variable doesn’t exist. This function should be + used with care, as it can introduce all sorts of nasty environment + dependencies in your Nix expression. + + `getEnv` is used in Nix Packages to locate the file + `~/.nixpkgs/config.nix`, which contains user-local settings for Nix + Packages. (That is, it does a `getEnv "HOME"` to locate the user’s + home directory.) + )", + .fun = prim_getEnv, +}); /* Evaluate the first argument, then return the second argument. */ static void prim_seq(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -526,6 +730,15 @@ static void prim_seq(EvalState & state, const Pos & pos, Value * * args, Value & v = *args[1]; } +static RegisterPrimOp primop_seq({ + .name = "__seq", + .args = {"e1", "e2"}, + .doc = R"( + Evaluate *e1*, then evaluate and return *e2*. This ensures that a + computation is strict in the value of *e1*. + )", + .fun = prim_seq, +}); /* Evaluate the first argument deeply (i.e. recursing into lists and attrsets), then return the second argument. */ @@ -536,6 +749,16 @@ static void prim_deepSeq(EvalState & state, const Pos & pos, Value * * args, Val v = *args[1]; } +static RegisterPrimOp primop_deepSeq({ + .name = "__deepSeq", + .args = {"e1", "e2"}, + .doc = R"( + This is like `seq e1 e2`, except that *e1* is evaluated *deeply*: + if it’s a list or set, its elements or attributes are also + evaluated recursively. + )", + .fun = prim_deepSeq, +}); /* Evaluate the first expression and print it on standard error. Then return the second expression. Useful for debugging. */ @@ -550,6 +773,17 @@ static void prim_trace(EvalState & state, const Pos & pos, Value * * args, Value v = *args[1]; } +static RegisterPrimOp primop_trace({ + .name = "__trace", + .args = {"e1", "e2"}, + .doc = R"( + Evaluate *e1* and print its abstract syntax representation on + standard error. Then return *e2*. This function is useful for + debugging. + )", + .fun = prim_trace, +}); + /************************************************************* * Derivations @@ -880,6 +1114,11 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * * v.attrs->sort(); } +static RegisterPrimOp primop_derivationStrict(RegisterPrimOp::Info { + .name = "derivationStrict", + .arity = 1, + .fun = prim_derivationStrict, +}); /* Return a placeholder string for the specified output that will be substituted by the corresponding output path at build time. For @@ -893,6 +1132,17 @@ static void prim_placeholder(EvalState & state, const Pos & pos, Value * * args, mkString(v, hashPlaceholder(state.forceStringNoCtx(*args[0], pos))); } +static RegisterPrimOp primop_placeholder({ + .name = "placeholder", + .args = {"output"}, + .doc = R"( + Return a placeholder string for the specified *output* that will be + substituted by the corresponding output path at build time. Typical + outputs would be `"out"`, `"bin"` or `"dev"`. + )", + .fun = prim_placeholder, +}); + /************************************************************* * Paths @@ -907,6 +1157,15 @@ static void prim_toPath(EvalState & state, const Pos & pos, Value * * args, Valu mkString(v, canonPath(path), context); } +static RegisterPrimOp primop_toPath({ + .name = "__toPath", + .args = {"s"}, + .doc = R"( + **DEPRECATED.** Use `/. + "/path"` to convert a string into an absolute + path. For relative paths, use `./. + "/path"`. + )", + .fun = prim_toPath, +}); /* Allow a valid store path to be used in an expression. This is useful in some generated expressions such as in nix-push, which @@ -918,6 +1177,9 @@ static void prim_toPath(EvalState & state, const Pos & pos, Value * * args, Valu corner cases. */ static void prim_storePath(EvalState & state, const Pos & pos, Value * * args, Value & v) { + if (evalSettings.pureEval) + throw EvalError("builtins.storePath' is not allowed in pure evaluation mode"); + PathSet context; Path path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context)); /* Resolve symlinks in ‘path’, unless ‘path’ itself is a symlink @@ -936,6 +1198,23 @@ static void prim_storePath(EvalState & state, const Pos & pos, Value * * args, V mkString(v, path, context); } +static RegisterPrimOp primop_storePath({ + .name = "__storePath", + .args = {"path"}, + .doc = R"( + This function allows you to define a dependency on an already + existing store path. For example, the derivation attribute `src + = builtins.storePath /nix/store/f1d18v1y…-source` causes the + derivation to depend on the specified path, which must exist or + be substitutable. Note that this differs from a plain path + (e.g. `src = /nix/store/f1d18v1y…-source`) in that the latter + causes the path to be *copied* again to the Nix store, resulting + in a new path (e.g. `/nix/store/ld01dnzc…-source-source`). + + This function is not available in pure evaluation mode. + )", + .fun = prim_storePath, +}); static void prim_pathExists(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -963,6 +1242,15 @@ static void prim_pathExists(EvalState & state, const Pos & pos, Value * * args, } } +static RegisterPrimOp primop_pathExists({ + .name = "__pathExists", + .args = {"path"}, + .doc = R"( + Return `true` if the path *path* exists at evaluation time, and + `false` otherwise. + )", + .fun = prim_pathExists, +}); /* Return the base name of the given string, i.e., everything following the last slash. */ @@ -972,6 +1260,16 @@ static void prim_baseNameOf(EvalState & state, const Pos & pos, Value * * args, mkString(v, baseNameOf(state.coerceToString(pos, *args[0], context, false, false)), context); } +static RegisterPrimOp primop_baseNameOf({ + .name = "baseNameOf", + .args = {"s"}, + .doc = R"( + Return the *base name* of the string *s*, that is, everything + following the final slash in the string. This is similar to the GNU + `basename` command. + )", + .fun = prim_baseNameOf, +}); /* Return the directory of the given path, i.e., everything before the last slash. Return either a path or a string depending on the type @@ -983,6 +1281,16 @@ static void prim_dirOf(EvalState & state, const Pos & pos, Value * * args, Value if (args[0]->type == tPath) mkPath(v, dir.c_str()); else mkString(v, dir, context); } +static RegisterPrimOp primop_dirOf({ + .name = "dirOf", + .args = {"s"}, + .doc = R"( + Return the directory part of the string *s*, that is, everything + before the final slash in the string. This is similar to the GNU + `dirname` command. + )", + .fun = prim_dirOf, +}); /* Return the contents of a file as a string. */ static void prim_readFile(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -1003,6 +1311,14 @@ static void prim_readFile(EvalState & state, const Pos & pos, Value * * args, Va mkString(v, s.c_str()); } +static RegisterPrimOp primop_readFile({ + .name = "__readFile", + .args = {"path"}, + .doc = R"( + Return the contents of the file *path* as a string. + )", + .fun = prim_readFile, +}); /* Find a file in the Nix search path. Used to implement <x> paths, which are desugared to 'findFile __nixPath "x"'. */ @@ -1048,6 +1364,12 @@ static void prim_findFile(EvalState & state, const Pos & pos, Value * * args, Va mkPath(v, state.checkSourcePath(state.findFile(searchPath, path, pos)).c_str()); } +static RegisterPrimOp primop_findFile(RegisterPrimOp::Info { + .name = "__findFile", + .arity = 2, + .fun = prim_findFile, +}); + /* Return the cryptographic hash of a file in base-16. */ static void prim_hashFile(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -1065,6 +1387,17 @@ static void prim_hashFile(EvalState & state, const Pos & pos, Value * * args, Va mkString(v, hashFile(*ht, state.checkSourcePath(p)).to_string(Base16, false), context); } +static RegisterPrimOp primop_hashFile({ + .name = "__hashFile", + .args = {"type", "p"}, + .doc = R"( + Return a base-16 representation of the cryptographic hash of the + file at path *p*. The hash algorithm specified by *type* must be one + of `"md5"`, `"sha1"`, `"sha256"` or `"sha512"`. + )", + .fun = prim_hashFile, +}); + /* Read a directory (without . or ..) */ static void prim_readDir(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -1096,6 +1429,25 @@ static void prim_readDir(EvalState & state, const Pos & pos, Value * * args, Val v.attrs->sort(); } +static RegisterPrimOp primop_readDir({ + .name = "__readDir", + .args = {"path"}, + .doc = R"( + Return the contents of the directory *path* as a set mapping + directory entries to the corresponding file type. For instance, if + directory `A` contains a regular file `B` and another directory + `C`, then `builtins.readDir ./A` will return the set + + ```nix + { B = "regular"; C = "directory"; } + ``` + + The possible values for the file type are `"regular"`, + `"directory"`, `"symlink"` and `"unknown"`. + )", + .fun = prim_readDir, +}); + /************************************************************* * Creating files @@ -1113,6 +1465,102 @@ static void prim_toXML(EvalState & state, const Pos & pos, Value * * args, Value mkString(v, out.str(), context); } +static RegisterPrimOp primop_toXML({ + .name = "__toXML", + .args = {"e"}, + .doc = R"( + Return a string containing an XML representation of *e*. The main + application for `toXML` is to communicate information with the + builder in a more structured format than plain environment + variables. + + Here is an example where this is the case: + + ```nix + { stdenv, fetchurl, libxslt, jira, uberwiki }: + + stdenv.mkDerivation (rec { + name = "web-server"; + + buildInputs = [ libxslt ]; + + builder = builtins.toFile "builder.sh" " + source $stdenv/setup + mkdir $out + echo "$servlets" | xsltproc ${stylesheet} - > $out/server-conf.xml ① + "; + + stylesheet = builtins.toFile "stylesheet.xsl" ② + "<?xml version='1.0' encoding='UTF-8'?> + <xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='1.0'> + <xsl:template match='/'> + <Configure> + <xsl:for-each select='/expr/list/attrs'> + <Call name='addWebApplication'> + <Arg><xsl:value-of select=\"attr[@name = 'path']/string/@value\" /></Arg> + <Arg><xsl:value-of select=\"attr[@name = 'war']/path/@value\" /></Arg> + </Call> + </xsl:for-each> + </Configure> + </xsl:template> + </xsl:stylesheet> + "; + + servlets = builtins.toXML [ ③ + { path = "/bugtracker"; war = jira + "/lib/atlassian-jira.war"; } + { path = "/wiki"; war = uberwiki + "/uberwiki.war"; } + ]; + }) + ``` + + The builder is supposed to generate the configuration file for a + [Jetty servlet container](http://jetty.mortbay.org/). A servlet + container contains a number of servlets (`*.war` files) each + exported under a specific URI prefix. So the servlet configuration + is a list of sets containing the `path` and `war` of the servlet + (①). This kind of information is difficult to communicate with the + normal method of passing information through an environment + variable, which just concatenates everything together into a + string (which might just work in this case, but wouldn’t work if + fields are optional or contain lists themselves). Instead the Nix + expression is converted to an XML representation with `toXML`, + which is unambiguous and can easily be processed with the + appropriate tools. For instance, in the example an XSLT stylesheet + (at point ②) is applied to it (at point ①) to generate the XML + configuration file for the Jetty server. The XML representation + produced at point ③ by `toXML` is as follows: + + ```xml + <?xml version='1.0' encoding='utf-8'?> + <expr> + <list> + <attrs> + <attr name="path"> + <string value="/bugtracker" /> + </attr> + <attr name="war"> + <path value="/nix/store/d1jh9pasa7k2...-jira/lib/atlassian-jira.war" /> + </attr> + </attrs> + <attrs> + <attr name="path"> + <string value="/wiki" /> + </attr> + <attr name="war"> + <path value="/nix/store/y6423b1yi4sx...-uberwiki/uberwiki.war" /> + </attr> + </attrs> + </list> + </expr> + ``` + + Note that we used the `toFile` built-in to write the builder and + the stylesheet “inline” in the Nix expression. The path of the + stylesheet is spliced into the builder using the syntax `xsltproc + ${stylesheet}`. + )", + .fun = prim_toXML, +}); /* Convert the argument (which can be any Nix expression) to a JSON string. Not all Nix expressions can be sensibly or completely @@ -1125,6 +1573,19 @@ static void prim_toJSON(EvalState & state, const Pos & pos, Value * * args, Valu mkString(v, out.str(), context); } +static RegisterPrimOp primop_toJSON({ + .name = "__toJSON", + .args = {"e"}, + .doc = R"( + Return a string containing a JSON representation of *e*. Strings, + integers, floats, booleans, nulls and lists are mapped to their JSON + equivalents. Sets (except derivations) are represented as objects. + Derivations are translated to a JSON string containing the + derivation’s output path. Paths are copied to the store and + represented as a JSON string of the resulting store path. + )", + .fun = prim_toJSON, +}); /* Parse a JSON string to a value. */ static void prim_fromJSON(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -1133,6 +1594,20 @@ static void prim_fromJSON(EvalState & state, const Pos & pos, Value * * args, Va parseJSON(state, s, v); } +static RegisterPrimOp primop_fromJSON({ + .name = "__fromJSON", + .args = {"e"}, + .doc = R"( + Convert a JSON string to a Nix value. For example, + + ```nix + builtins.fromJSON ''{"x": [1, 2, 3], "y": null}'' + ``` + + returns the value `{ x = [ 1 2 3 ]; y = null; }`. + )", + .fun = prim_fromJSON, +}); /* Store a string in the Nix store as a source file that can be used as an input by derivations. */ @@ -1167,6 +1642,83 @@ static void prim_toFile(EvalState & state, const Pos & pos, Value * * args, Valu mkString(v, storePath, {storePath}); } +static RegisterPrimOp primop_toFile({ + .name = "__toFile", + .args = {"name", "s"}, + .doc = R"( + Store the string *s* in a file in the Nix store and return its + path. The file has suffix *name*. This file can be used as an + input to derivations. One application is to write builders + “inline”. For instance, the following Nix expression combines the + [Nix expression for GNU Hello](expression-syntax.md) and its + [build script](build-script.md) into one file: + + ```nix + { stdenv, fetchurl, perl }: + + stdenv.mkDerivation { + name = "hello-2.1.1"; + + builder = builtins.toFile "builder.sh" " + source $stdenv/setup + + PATH=$perl/bin:$PATH + + tar xvfz $src + cd hello-* + ./configure --prefix=$out + make + make install + "; + + src = fetchurl { + url = "http://ftp.nluug.nl/pub/gnu/hello/hello-2.1.1.tar.gz"; + sha256 = "1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465"; + }; + inherit perl; + } + ``` + + It is even possible for one file to refer to another, e.g., + + ```nix + builder = let + configFile = builtins.toFile "foo.conf" " + # This is some dummy configuration file. + ... + "; + in builtins.toFile "builder.sh" " + source $stdenv/setup + ... + cp ${configFile} $out/etc/foo.conf + "; + ``` + + Note that `${configFile}` is an + [antiquotation](language-values.md), so the result of the + expression `configFile` + (i.e., a path like `/nix/store/m7p7jfny445k...-foo.conf`) will be + spliced into the resulting string. + + It is however *not* allowed to have files mutually referring to each + other, like so: + + ```nix + let + foo = builtins.toFile "foo" "...${bar}..."; + bar = builtins.toFile "bar" "...${foo}..."; + in foo + ``` + + This is not allowed because it would cause a cyclic dependency in + the computation of the cryptographic hashes for `foo` and `bar`. + + It is also not possible to reference the result of a derivation. If + you are using Nixpkgs, the `writeTextFile` function is able to do + that. + )", + .fun = prim_toFile, +}); static void addPath(EvalState & state, const Pos & pos, const string & name, const Path & path_, Value * filterFun, FileIngestionMethod method, const std::optional<Hash> expectedHash, Value & v) @@ -1237,6 +1789,48 @@ static void prim_filterSource(EvalState & state, const Pos & pos, Value * * args addPath(state, pos, std::string(baseNameOf(path)), path, args[0], FileIngestionMethod::Recursive, std::nullopt, v); } +static RegisterPrimOp primop_filterSource({ + .name = "__filterSource", + .args = {"e1", "e2"}, + .doc = R"( + This function allows you to copy sources into the Nix store while + filtering certain files. For instance, suppose that you want to use + the directory `source-dir` as an input to a Nix expression, e.g. + + ```nix + stdenv.mkDerivation { + ... + src = ./source-dir; + } + ``` + + However, if `source-dir` is a Subversion working copy, then all + those annoying `.svn` subdirectories will also be copied to the + store. Worse, the contents of those directories may change a lot, + causing lots of spurious rebuilds. With `filterSource` you can + filter out the `.svn` directories: + + ```nix + src = builtins.filterSource + (path: type: type != "directory" || baseNameOf path != ".svn") + ./source-dir; + ``` + + Thus, the first argument *e1* must be a predicate function that is + called for each regular file, directory or symlink in the source + tree *e2*. If the function returns `true`, the file is copied to the + Nix store, otherwise it is omitted. The function is called with two + arguments. The first is the full path of the file. The second is a + string that identifies the type of the file, which is either + `"regular"`, `"directory"`, `"symlink"` or `"unknown"` (for other + kinds of files such as device nodes or fifos — but note that those + cannot be copied to the Nix store, so if the predicate returns + `true` for them, the copy will fail). If you exclude a directory, + the entire corresponding subtree of *e2* will be excluded. + )", + .fun = prim_filterSource, +}); + static void prim_path(EvalState & state, const Pos & pos, Value * * args, Value & v) { state.forceAttrs(*args[0], pos); @@ -1282,6 +1876,41 @@ static void prim_path(EvalState & state, const Pos & pos, Value * * args, Value addPath(state, pos, name, path, filterFun, method, expectedHash, v); } +static RegisterPrimOp primop_path({ + .name = "__path", + .args = {"args"}, + .doc = R"( + An enrichment of the built-in path type, based on the attributes + present in *args*. All are optional except `path`: + + - path + The underlying path. + + - name + The name of the path when added to the store. This can used to + reference paths that have nix-illegal characters in their names, + like `@`. + + - filter + A function of the type expected by `builtins.filterSource`, + with the same semantics. + + - recursive + When `false`, when `path` is added to the store it is with a + flat hash, rather than a hash of the NAR serialization of the + file. Thus, `path` must refer to a regular file, not a + directory. This allows similar behavior to `fetchurl`. Defaults + to `true`. + + - sha256 + When provided, this is the expected hash of the file at the + path. Evaluation will fail if the hash is incorrect, and + providing a hash allows `builtins.path` to be used even when the + `pure-eval` nix config option is on. + )", + .fun = prim_path, +}); + /************************************************************* * Sets @@ -1304,6 +1933,16 @@ static void prim_attrNames(EvalState & state, const Pos & pos, Value * * args, V [](Value * v1, Value * v2) { return strcmp(v1->string.s, v2->string.s) < 0; }); } +static RegisterPrimOp primop_attrNames({ + .name = "__attrNames", + .args = {"set"}, + .doc = R"( + Return the names of the attributes in the set *set* in an + alphabetically sorted list. For instance, `builtins.attrNames { y + = 1; x = "foo"; }` evaluates to `[ "x" "y" ]`. + )", + .fun = prim_attrNames, +}); /* Return the values of the attributes in a set as a list, in the same order as attrNames. */ @@ -1324,6 +1963,15 @@ static void prim_attrValues(EvalState & state, const Pos & pos, Value * * args, v.listElems()[i] = ((Attr *) v.listElems()[i])->value; } +static RegisterPrimOp primop_attrValues({ + .name = "__attrValues", + .args = {"set"}, + .doc = R"( + Return the values of the attributes in the set *set* in the order + corresponding to the sorted attribute names. + )", + .fun = prim_attrValues, +}); /* Dynamic version of the `.' operator. */ void prim_getAttr(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -1343,9 +1991,20 @@ void prim_getAttr(EvalState & state, const Pos & pos, Value * * args, Value & v) v = *i->value; } +static RegisterPrimOp primop_getAttr({ + .name = "__getAttr", + .args = {"s", "set"}, + .doc = R"( + `getAttr` returns the attribute named *s* from *set*. Evaluation + aborts if the attribute doesn’t exist. This is a dynamic version of + the `.` operator, since *s* is an expression rather than an + identifier. + )", + .fun = prim_getAttr, +}); /* Return position information of the specified attribute. */ -void prim_unsafeGetAttrPos(EvalState & state, const Pos & pos, Value * * args, Value & v) +static void prim_unsafeGetAttrPos(EvalState & state, const Pos & pos, Value * * args, Value & v) { string attr = state.forceStringNoCtx(*args[0], pos); state.forceAttrs(*args[1], pos); @@ -1356,6 +2015,11 @@ void prim_unsafeGetAttrPos(EvalState & state, const Pos & pos, Value * * args, V state.mkPos(v, i->pos); } +static RegisterPrimOp primop_unsafeGetAttrPos(RegisterPrimOp::Info { + .name = "__unsafeGetAttrPos", + .arity = 2, + .fun = prim_unsafeGetAttrPos, +}); /* Dynamic version of the `?' operator. */ static void prim_hasAttr(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -1365,6 +2029,16 @@ static void prim_hasAttr(EvalState & state, const Pos & pos, Value * * args, Val mkBool(v, args[1]->attrs->find(state.symbols.create(attr)) != args[1]->attrs->end()); } +static RegisterPrimOp primop_hasAttr({ + .name = "__hasAttr", + .args = {"s", "set"}, + .doc = R"( + `hasAttr` returns `true` if *set* has an attribute named *s*, and + `false` otherwise. This is a dynamic version of the `?` operator, + since *s* is an expression rather than an identifier. + )", + .fun = prim_hasAttr, +}); /* Determine whether the argument is a set. */ static void prim_isAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -1373,6 +2047,14 @@ static void prim_isAttrs(EvalState & state, const Pos & pos, Value * * args, Val mkBool(v, args[0]->type == tAttrs); } +static RegisterPrimOp primop_isAttrs({ + .name = "__isAttrs", + .args = {"e"}, + .doc = R"( + Return `true` if *e* evaluates to a set, and `false` otherwise. + )", + .fun = prim_isAttrs, +}); static void prim_removeAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -1396,6 +2078,21 @@ static void prim_removeAttrs(EvalState & state, const Pos & pos, Value * * args, } } +static RegisterPrimOp primop_removeAttrs({ + .name = "removeAttrs", + .args = {"set", "list"}, + .doc = R"( + Remove the attributes listed in *list* from *set*. The attributes + don’t have to exist in *set*. For instance, + + ```nix + removeAttrs { x = 1; y = 2; z = 3; } [ "a" "x" "z" ] + ``` + + evaluates to `{ y = 2; }`. + )", + .fun = prim_removeAttrs, +}); /* Builds a set from a list specifying (name, value) pairs. To be precise, a list [{name = "name1"; value = value1;} ... {name = @@ -1437,10 +2134,31 @@ static void prim_listToAttrs(EvalState & state, const Pos & pos, Value * * args, v.attrs->sort(); } +static RegisterPrimOp primop_listToAttrs({ + .name = "__listToAttrs", + .args = {"e"}, + .doc = R"( + Construct a set from a list specifying the names and values of each + attribute. Each element of the list should be a set consisting of a + string-valued attribute `name` specifying the name of the attribute, + and an attribute `value` specifying its value. Example: + + ```nix + builtins.listToAttrs + [ { name = "foo"; value = 123; } + { name = "bar"; value = 456; } + ] + ``` + + evaluates to + + ```nix + { foo = 123; bar = 456; } + ``` + )", + .fun = prim_listToAttrs, +}); -/* Return the right-biased intersection of two sets as1 and as2, - i.e. a set that contains every attribute from as2 that is also a - member of as1. */ static void prim_intersectAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) { state.forceAttrs(*args[0], pos); @@ -1455,14 +2173,16 @@ static void prim_intersectAttrs(EvalState & state, const Pos & pos, Value * * ar } } +static RegisterPrimOp primop_intersectAttrs({ + .name = "__intersectAttrs", + .args = {"e1", "e2"}, + .doc = R"( + Return a set consisting of the attributes in the set *e2* that also + exist in the set *e1*. + )", + .fun = prim_intersectAttrs, +}); -/* Collect each attribute named `attr' from a list of attribute sets. - Sets that don't contain the named attribute are ignored. - - Example: - catAttrs "a" [{a = 1;} {b = 0;} {a = 2;}] - => [1 2] -*/ static void prim_catAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) { Symbol attrName = state.symbols.create(state.forceStringNoCtx(*args[0], pos)); @@ -1484,20 +2204,23 @@ static void prim_catAttrs(EvalState & state, const Pos & pos, Value * * args, Va v.listElems()[n] = res[n]; } +static RegisterPrimOp primop_catAttrs({ + .name = "__catAttrs", + .args = {"attr", "list"}, + .doc = R"( + Collect each attribute named *attr* from a list of attribute + sets. Attrsets that don't contain the named attribute are + ignored. For example, -/* Return a set containing the names of the formal arguments expected - by the function `f'. The value of each attribute is a Boolean - denoting whether the corresponding argument has a default value. For instance, + ```nix + builtins.catAttrs "a" [{a = 1;} {b = 0;} {a = 2;}] + ``` - functionArgs ({ x, y ? 123}: ...) - => { x = false; y = true; } + evaluates to `[1 2]`. + )", + .fun = prim_catAttrs, +}); - "Formal argument" here refers to the attributes pattern-matched by - the function. Plain lambdas are not included, e.g. - - functionArgs (x: ...) - => { } -*/ static void prim_functionArgs(EvalState & state, const Pos & pos, Value * * args, Value & v) { state.forceValue(*args[0], pos); @@ -1522,8 +2245,24 @@ static void prim_functionArgs(EvalState & state, const Pos & pos, Value * * args v.attrs->sort(); } - -/* Apply a function to every element of an attribute set. */ +static RegisterPrimOp primop_functionArgs({ + .name = "__functionArgs", + .args = {"f"}, + .doc = R"( + Return a set containing the names of the formal arguments expected + by the function *f*. The value of each attribute is a Boolean + denoting whether the corresponding argument has a default value. For + instance, `functionArgs ({ x, y ? 123}: ...) = { x = false; y = + true; }`. + + "Formal argument" here refers to the attributes pattern-matched by + the function. Plain lambdas are not included, e.g. `functionArgs (x: + ...) = { }`. + )", + .fun = prim_functionArgs, +}); + +/* */ static void prim_mapAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) { state.forceAttrs(*args[1], pos); @@ -1539,6 +2278,20 @@ static void prim_mapAttrs(EvalState & state, const Pos & pos, Value * * args, Va } } +static RegisterPrimOp primop_mapAttrs({ + .name = "__mapAttrs", + .args = {"f", "attrset"}, + .doc = R"( + Apply function *f* to every element of *attrset*. For example, + + ```nix + builtins.mapAttrs (name: value: value * 10) { a = 1; b = 2; } + ``` + + evaluates to `{ a = 10; b = 20; }`. + )", + .fun = prim_mapAttrs, +}); /************************************************************* @@ -1553,6 +2306,14 @@ static void prim_isList(EvalState & state, const Pos & pos, Value * * args, Valu mkBool(v, args[0]->isList()); } +static RegisterPrimOp primop_isList({ + .name = "__isList", + .args = {"e"}, + .doc = R"( + Return `true` if *e* evaluates to a list, and `false` otherwise. + )", + .fun = prim_isList, +}); static void elemAt(EvalState & state, const Pos & pos, Value & list, int n, Value & v) { @@ -1566,13 +2327,21 @@ static void elemAt(EvalState & state, const Pos & pos, Value & list, int n, Valu v = *list.listElems()[n]; } - /* Return the n-1'th element of a list. */ static void prim_elemAt(EvalState & state, const Pos & pos, Value * * args, Value & v) { elemAt(state, pos, *args[0], state.forceInt(*args[1], pos), v); } +static RegisterPrimOp primop_elemAt({ + .name = "__elemAt", + .args = {"xs", "n"}, + .doc = R"( + Return element *n* from the list *xs*. Elements are counted starting + from 0. A fatal error occurs if the index is out of bounds. + )", + .fun = prim_elemAt, +}); /* Return the first element of a list. */ static void prim_head(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -1580,6 +2349,16 @@ static void prim_head(EvalState & state, const Pos & pos, Value * * args, Value elemAt(state, pos, *args[0], 0, v); } +static RegisterPrimOp primop_head({ + .name = "__head", + .args = {"list"}, + .doc = R"( + Return the first element of a list; abort evaluation if the argument + isn’t a list or is an empty list. You can test whether a list is + empty by comparing it with `[]`. + )", + .fun = prim_head, +}); /* Return a list consisting of everything but the first element of a list. Warning: this function takes O(n) time, so you probably @@ -1598,6 +2377,21 @@ static void prim_tail(EvalState & state, const Pos & pos, Value * * args, Value v.listElems()[n] = args[0]->listElems()[n + 1]; } +static RegisterPrimOp primop_tail({ + .name = "__tail", + .args = {"list"}, + .doc = R"( + Return the second to last elements of a list; abort evaluation if + the argument isn’t a list or is an empty list. + + > **Warning** + > + > This function should generally be avoided since it's inefficient: + > unlike Haskell's `tail`, it takes O(n) time, so recursing over a + > list by repeatedly calling `tail` takes O(n^2) time. + )", + .fun = prim_tail, +}); /* Apply a function to every element of a list. */ static void prim_map(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -1611,6 +2405,21 @@ static void prim_map(EvalState & state, const Pos & pos, Value * * args, Value & *args[0], *args[1]->listElems()[n]); } +static RegisterPrimOp primop_map({ + .name = "map", + .args = {"f", "list"}, + .doc = R"( + Apply the function *f* to each element in the list *list*. For + example, + + ```nix + map (x: "foo" + x) [ "bar" "bla" "abc" ] + ``` + + evaluates to `[ "foobar" "foobla" "fooabc" ]`. + )", + .fun = prim_map, +}); /* Filter a list using a predicate; that is, return a list containing every element from the list for which the predicate function @@ -1642,6 +2451,15 @@ static void prim_filter(EvalState & state, const Pos & pos, Value * * args, Valu } } +static RegisterPrimOp primop_filter({ + .name = "__filter", + .args = {"f", "list"}, + .doc = R"( + Return a list consisting of the elements of *list* for which the + function *f* returns `true`. + )", + .fun = prim_filter, +}); /* Return true if a list contains a given element. */ static void prim_elem(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -1656,6 +2474,15 @@ static void prim_elem(EvalState & state, const Pos & pos, Value * * args, Value mkBool(v, res); } +static RegisterPrimOp primop_elem({ + .name = "__elem", + .args = {"x", "xs"}, + .doc = R"( + Return `true` if a value equal to *x* occurs in the list *xs*, and + `false` otherwise. + )", + .fun = prim_elem, +}); /* Concatenate a list of lists. */ static void prim_concatLists(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -1664,6 +2491,14 @@ static void prim_concatLists(EvalState & state, const Pos & pos, Value * * args, state.concatLists(v, args[0]->listSize(), args[0]->listElems(), pos); } +static RegisterPrimOp primop_concatLists({ + .name = "__concatLists", + .args = {"lists"}, + .doc = R"( + Concatenate a list of lists into a single list. + )", + .fun = prim_concatLists, +}); /* Return the length of a list. This is an O(1) time operation. */ static void prim_length(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -1672,6 +2507,14 @@ static void prim_length(EvalState & state, const Pos & pos, Value * * args, Valu mkInt(v, args[0]->listSize()); } +static RegisterPrimOp primop_length({ + .name = "__length", + .args = {"e"}, + .doc = R"( + Return the length of the list *e*. + )", + .fun = prim_length, +}); /* Reduce a list by applying a binary operator, from left to right. The operator is applied strictly. */ @@ -1696,6 +2539,18 @@ static void prim_foldlStrict(EvalState & state, const Pos & pos, Value * * args, } } +static RegisterPrimOp primop_foldlStrict({ + .name = "__foldl'", + .args = {"op", "nul", "list"}, + .doc = R"( + Reduce a list by applying a binary operator, from left to right, + e.g. `foldl’ op nul [x0 x1 x2 ...] = op (op (op nul x0) x1) x2) + ...`. The operator is applied strictly, i.e., its arguments are + evaluated first. For example, `foldl’ (x: y: x + y) 0 [1 2 3]` + evaluates to 6. + )", + .fun = prim_foldlStrict, +}); static void anyOrAll(bool any, EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -1721,12 +2576,30 @@ static void prim_any(EvalState & state, const Pos & pos, Value * * args, Value & anyOrAll(true, state, pos, args, v); } +static RegisterPrimOp primop_any({ + .name = "__any", + .args = {"pred", "list"}, + .doc = R"( + Return `true` if the function *pred* returns `true` for at least one + element of *list*, and `false` otherwise. + )", + .fun = prim_any, +}); static void prim_all(EvalState & state, const Pos & pos, Value * * args, Value & v) { anyOrAll(false, state, pos, args, v); } +static RegisterPrimOp primop_all({ + .name = "__all", + .args = {"pred", "list"}, + .doc = R"( + Return `true` if the function *pred* returns `true` for all elements + of *list*, and `false` otherwise. + )", + .fun = prim_all, +}); static void prim_genList(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -1747,6 +2620,21 @@ static void prim_genList(EvalState & state, const Pos & pos, Value * * args, Val } } +static RegisterPrimOp primop_genList({ + .name = "__genList", + .args = {"generator", "length"}, + .doc = R"( + Generate list of size *length*, with each element *i* equal to the + value returned by *generator* `i`. For example, + + ```nix + builtins.genList (x: x * x) 5 + ``` + + returns the list `[ 0 1 4 9 16 ]`. + )", + .fun = prim_genList, +}); static void prim_lessThan(EvalState & state, const Pos & pos, Value * * args, Value & v); @@ -1782,6 +2670,26 @@ static void prim_sort(EvalState & state, const Pos & pos, Value * * args, Value std::stable_sort(v.listElems(), v.listElems() + len, comparator); } +static RegisterPrimOp primop_sort({ + .name = "__sort", + .args = {"comparator", "list"}, + .doc = R"( + Return *list* in sorted order. It repeatedly calls the function + *comparator* with two elements. The comparator should return `true` + if the first element is less than the second, and `false` otherwise. + For example, + + ```nix + builtins.sort builtins.lessThan [ 483 249 526 147 42 77 ] + ``` + + produces the list `[ 42 77 147 249 483 526 ]`. + + This is a stable sort: it preserves the relative order of elements + deemed equal by the comparator. + )", + .fun = prim_sort, +}); static void prim_partition(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -1820,9 +2728,29 @@ static void prim_partition(EvalState & state, const Pos & pos, Value * * args, V v.attrs->sort(); } +static RegisterPrimOp primop_partition({ + .name = "__partition", + .args = {"pred", "list"}, + .doc = R"( + Given a predicate function *pred*, this function returns an + attrset containing a list named `right`, containing the elements + in *list* for which *pred* returned `true`, and a list named + `wrong`, containing the elements for which it returned + `false`. For example, + + ```nix + builtins.partition (x: x > 10) [1 23 9 3 42] + ``` + + evaluates to + + ```nix + { right = [ 23 42 ]; wrong = [ 1 9 3 ]; } + ``` + )", + .fun = prim_partition, +}); -/* concatMap = f: list: concatLists (map f list); */ -/* C++-version is to avoid allocating `mkApp', call `f' eagerly */ static void prim_concatMap(EvalState & state, const Pos & pos, Value * * args, Value & v) { state.forceFunction(*args[0], pos); @@ -1849,6 +2777,16 @@ static void prim_concatMap(EvalState & state, const Pos & pos, Value * * args, V } } +static RegisterPrimOp primop_concatMap({ + .name = "__concatMap", + .args = {"f", "list"}, + .doc = R"( + This function is equivalent to `builtins.concatLists (map f list)` + but is more efficient. + )", + .fun = prim_concatMap, +}); + /************************************************************* * Integer arithmetic @@ -1865,6 +2803,14 @@ static void prim_add(EvalState & state, const Pos & pos, Value * * args, Value & mkInt(v, state.forceInt(*args[0], pos) + state.forceInt(*args[1], pos)); } +static RegisterPrimOp primop_add({ + .name = "__add", + .args = {"e1", "e2"}, + .doc = R"( + Return the sum of the numbers *e1* and *e2*. + )", + .fun = prim_add, +}); static void prim_sub(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -1876,6 +2822,14 @@ static void prim_sub(EvalState & state, const Pos & pos, Value * * args, Value & mkInt(v, state.forceInt(*args[0], pos) - state.forceInt(*args[1], pos)); } +static RegisterPrimOp primop_sub({ + .name = "__sub", + .args = {"e1", "e2"}, + .doc = R"( + Return the difference between the numbers *e1* and *e2*. + )", + .fun = prim_sub, +}); static void prim_mul(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -1887,6 +2841,14 @@ static void prim_mul(EvalState & state, const Pos & pos, Value * * args, Value & mkInt(v, state.forceInt(*args[0], pos) * state.forceInt(*args[1], pos)); } +static RegisterPrimOp primop_mul({ + .name = "__mul", + .args = {"e1", "e2"}, + .doc = R"( + Return the product of the numbers *e1* and *e2*. + )", + .fun = prim_mul, +}); static void prim_div(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -1916,21 +2878,57 @@ static void prim_div(EvalState & state, const Pos & pos, Value * * args, Value & } } +static RegisterPrimOp primop_div({ + .name = "__div", + .args = {"e1", "e2"}, + .doc = R"( + Return the quotient of the numbers *e1* and *e2*. + )", + .fun = prim_div, +}); + static void prim_bitAnd(EvalState & state, const Pos & pos, Value * * args, Value & v) { mkInt(v, state.forceInt(*args[0], pos) & state.forceInt(*args[1], pos)); } +static RegisterPrimOp primop_bitAnd({ + .name = "__bitAnd", + .args = {"e1", "e2"}, + .doc = R"( + Return the bitwise AND of the integers *e1* and *e2*. + )", + .fun = prim_bitAnd, +}); + static void prim_bitOr(EvalState & state, const Pos & pos, Value * * args, Value & v) { mkInt(v, state.forceInt(*args[0], pos) | state.forceInt(*args[1], pos)); } +static RegisterPrimOp primop_bitOr({ + .name = "__bitOr", + .args = {"e1", "e2"}, + .doc = R"( + Return the bitwise OR of the integers *e1* and *e2*. + )", + .fun = prim_bitOr, +}); + static void prim_bitXor(EvalState & state, const Pos & pos, Value * * args, Value & v) { mkInt(v, state.forceInt(*args[0], pos) ^ state.forceInt(*args[1], pos)); } +static RegisterPrimOp primop_bitXor({ + .name = "__bitXor", + .args = {"e1", "e2"}, + .doc = R"( + Return the bitwise XOR of the integers *e1* and *e2*. + )", + .fun = prim_bitXor, +}); + static void prim_lessThan(EvalState & state, const Pos & pos, Value * * args, Value & v) { state.forceValue(*args[0], pos); @@ -1939,6 +2937,17 @@ static void prim_lessThan(EvalState & state, const Pos & pos, Value * * args, Va mkBool(v, comp(args[0], args[1])); } +static RegisterPrimOp primop_lessThan({ + .name = "__lessThan", + .args = {"e1", "e2"}, + .doc = R"( + Return `true` if the number *e1* is less than the number *e2*, and + `false` otherwise. Evaluation aborts if either *e1* or *e2* does not + evaluate to a number. + )", + .fun = prim_lessThan, +}); + /************************************************************* * String manipulation @@ -1955,6 +2964,29 @@ static void prim_toString(EvalState & state, const Pos & pos, Value * * args, Va mkString(v, s, context); } +static RegisterPrimOp primop_toString({ + .name = "toString", + .args = {"e"}, + .doc = R"( + Convert the expression *e* to a string. *e* can be: + + - A string (in which case the string is returned unmodified). + + - A path (e.g., `toString /foo/bar` yields `"/foo/bar"`. + + - A set containing `{ __toString = self: ...; }`. + + - An integer. + + - A list, in which case the string representations of its elements + are joined with spaces. + + - A Boolean (`false` yields `""`, `true` yields `"1"`). + + - `null`, which yields the empty string. + )", + .fun = prim_toString, +}); /* `substring start len str' returns the substring of `str' starting at character position `min(start, stringLength str)' inclusive and @@ -1976,6 +3008,25 @@ static void prim_substring(EvalState & state, const Pos & pos, Value * * args, V mkString(v, (unsigned int) start >= s.size() ? "" : string(s, start, len), context); } +static RegisterPrimOp primop_substring({ + .name = "__substring", + .args = {"start", "len", "s"}, + .doc = R"( + Return the substring of *s* from character position *start* + (zero-based) up to but not including *start + len*. If *start* is + greater than the length of the string, an empty string is returned, + and if *start + len* lies beyond the end of the string, only the + substring up to the end of the string is returned. *start* must be + non-negative. For example, + + ```nix + builtins.substring 0 3 "nixos" + ``` + + evaluates to `"nix"`. + )", + .fun = prim_substring, +}); static void prim_stringLength(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -1984,6 +3035,15 @@ static void prim_stringLength(EvalState & state, const Pos & pos, Value * * args mkInt(v, s.size()); } +static RegisterPrimOp primop_stringLength({ + .name = "__stringLength", + .args = {"e"}, + .doc = R"( + Return the length of the string *e*. If *e* is not a string, + evaluation is aborted. + )", + .fun = prim_stringLength, +}); /* Return the cryptographic hash of a string in base-16. */ static void prim_hashString(EvalState & state, const Pos & pos, Value * * args, Value & v) @@ -2002,6 +3062,16 @@ static void prim_hashString(EvalState & state, const Pos & pos, Value * * args, mkString(v, hashString(*ht, s).to_string(Base16, false), context); } +static RegisterPrimOp primop_hashString({ + .name = "__hashString", + .args = {"type", "s"}, + .doc = R"( + Return a base-16 representation of the cryptographic hash of string + *s*. The hash algorithm specified by *type* must be one of `"md5"`, + `"sha1"`, `"sha256"` or `"sha512"`. + )", + .fun = prim_hashString, +}); /* Match a regular expression against a string and return either ‘null’ or a list containing substring matches. */ @@ -2050,6 +3120,41 @@ void prim_match(EvalState & state, const Pos & pos, Value * * args, Value & v) } } +static RegisterPrimOp primop_match({ + .name = "__match", + .args = {"regex", "str"}, + .doc = R"s( + Returns a list if the [extended POSIX regular + expression](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_04) + *regex* matches *str* precisely, otherwise returns `null`. Each item + in the list is a regex group. + + ```nix + builtins.match "ab" "abc" + ``` + + Evaluates to `null`. + + ```nix + builtins.match "abc" "abc" + ``` + + Evaluates to `[ ]`. + + ```nix + builtins.match "a(b)(c)" "abc" + ``` + + Evaluates to `[ "b" "c" ]`. + + ```nix + builtins.match "[[:space:]]+([[:upper:]]+)[[:space:]]+" " FOO " + ``` + + Evaluates to `[ "foo" ]`. + )s", + .fun = prim_match, +}); /* Split a string with a regular expression, and return a list of the non-matching parts interleaved by the lists of the matching groups. */ @@ -2123,8 +3228,44 @@ static void prim_split(EvalState & state, const Pos & pos, Value * * args, Value } } +static RegisterPrimOp primop_split({ + .name = "__split", + .args = {"regex", "str"}, + .doc = R"s( + Returns a list composed of non matched strings interleaved with the + lists of the [extended POSIX regular + expression](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_04) + *regex* matches of *str*. Each item in the lists of matched + sequences is a regex group. + + ```nix + builtins.split "(a)b" "abc" + ``` + + Evaluates to `[ "" [ "a" ] "c" ]`. + + ```nix + builtins.split "([ac])" "abc" + ``` -static void prim_concatStringSep(EvalState & state, const Pos & pos, Value * * args, Value & v) + Evaluates to `[ "" [ "a" ] "b" [ "c" ] "" ]`. + + ```nix + builtins.split "(a)|(c)" "abc" + ``` + + Evaluates to `[ "" [ "a" null ] "b" [ null "c" ] "" ]`. + + ```nix + builtins.split "([[:upper:]]+)" " FOO " + ``` + + Evaluates to `[ " " [ "FOO" ] " " ]`. + )s", + .fun = prim_split, +}); + +static void prim_concatStringsSep(EvalState & state, const Pos & pos, Value * * args, Value & v) { PathSet context; @@ -2143,6 +3284,16 @@ static void prim_concatStringSep(EvalState & state, const Pos & pos, Value * * a mkString(v, res, context); } +static RegisterPrimOp primop_concatStringsSep({ + .name = "__concatStringsSep", + .args = {"separator", "list"}, + .doc = R"( + Concatenate a list of strings with a separator between each + element, e.g. `concatStringsSep "/" ["usr" "local" "bin"] == + "usr/local/bin"`. + )", + .fun = prim_concatStringsSep, +}); static void prim_replaceStrings(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -2202,6 +3353,22 @@ static void prim_replaceStrings(EvalState & state, const Pos & pos, Value * * ar mkString(v, res, context); } +static RegisterPrimOp primop_replaceStrings({ + .name = "__replaceStrings", + .args = {"from", "to", "s"}, + .doc = R"( + Given string *s*, replace every occurrence of the strings in *from* + with the corresponding string in *to*. For example, + + ```nix + builtins.replaceStrings ["oo" "a"] ["a" "i"] "foobar" + ``` + + evaluates to `"fabir"`. + )", + .fun = prim_replaceStrings, +}); + /************************************************************* * Versions @@ -2218,6 +3385,19 @@ static void prim_parseDrvName(EvalState & state, const Pos & pos, Value * * args v.attrs->sort(); } +static RegisterPrimOp primop_parseDrvName({ + .name = "__parseDrvName", + .args = {"s"}, + .doc = R"( + Split the string *s* into a package name and version. The package + name is everything up to but not including the first dash followed + by a digit, and the version is everything following that dash. The + result is returned in a set `{ name, version }`. Thus, + `builtins.parseDrvName "nix-0.12pre12876"` returns `{ name = + "nix"; version = "0.12pre12876"; }`. + )", + .fun = prim_parseDrvName, +}); static void prim_compareVersions(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -2226,6 +3406,18 @@ static void prim_compareVersions(EvalState & state, const Pos & pos, Value * * a mkInt(v, compareVersions(version1, version2)); } +static RegisterPrimOp primop_compareVersions({ + .name = "__compareVersions", + .args = {"s1", "s2"}, + .doc = R"( + Compare two strings representing versions and return `-1` if + version *s1* is older than version *s2*, `0` if they are the same, + and `1` if *s1* is newer than *s2*. The version comparison + algorithm is the same as the one used by [`nix-env + -u`](../command-ref/nix-env.md#operation---upgrade). + )", + .fun = prim_compareVersions, +}); static void prim_splitVersion(EvalState & state, const Pos & pos, Value * * args, Value & v) { @@ -2246,6 +3438,17 @@ static void prim_splitVersion(EvalState & state, const Pos & pos, Value * * args } } +static RegisterPrimOp primop_splitVersion({ + .name = "__splitVersion", + .args = {"s"}, + .doc = R"( + Split a string representing a version into its components, by the + same version splitting logic underlying the version comparison in + [`nix-env -u`](../command-ref/nix-env.md#operation---upgrade). + )", + .fun = prim_splitVersion, +}); + /************************************************************* * Primop registration @@ -2259,7 +3462,20 @@ RegisterPrimOp::RegisterPrimOp(std::string name, size_t arity, PrimOpFun fun, std::optional<std::string> requiredFeature) { if (!primOps) primOps = new PrimOps; - primOps->push_back({name, arity, fun, requiredFeature}); + primOps->push_back({ + .name = name, + .args = {}, + .arity = arity, + .requiredFeature = std::move(requiredFeature), + .fun = fun + }); +} + + +RegisterPrimOp::RegisterPrimOp(Info && info) +{ + if (!primOps) primOps = new PrimOps; + primOps->push_back(std::move(info)); } @@ -2283,15 +3499,6 @@ void EvalState::createBaseEnv() mkNull(v); addConstant("null", v); - auto vThrow = addPrimOp("throw", 1, prim_throw); - - auto addPurityError = [&](const std::string & name) { - Value * v2 = allocValue(); - mkString(*v2, fmt("'%s' is not allowed in pure evaluation mode", name)); - mkApp(v, *vThrow, *v2); - addConstant(name, v); - }; - if (!evalSettings.pureEval) { mkInt(v, time(0)); addConstant("__currentTime", v); @@ -2316,132 +3523,16 @@ void EvalState::createBaseEnv() addConstant("__langVersion", v); // Miscellaneous - auto vScopedImport = addPrimOp("scopedImport", 2, prim_scopedImport); - Value * v2 = allocValue(); - mkAttrs(*v2, 0); - mkApp(v, *vScopedImport, *v2); - forceValue(v); - addConstant("import", v); if (evalSettings.enableNativeCode) { addPrimOp("__importNative", 2, prim_importNative); addPrimOp("__exec", 1, prim_exec); } - addPrimOp("__typeOf", 1, prim_typeOf); - addPrimOp("isNull", 1, prim_isNull); - addPrimOp("__isFunction", 1, prim_isFunction); - addPrimOp("__isString", 1, prim_isString); - addPrimOp("__isInt", 1, prim_isInt); - addPrimOp("__isFloat", 1, prim_isFloat); - addPrimOp("__isBool", 1, prim_isBool); - addPrimOp("__isPath", 1, prim_isPath); - addPrimOp("__genericClosure", 1, prim_genericClosure); - addPrimOp("abort", 1, prim_abort); - addPrimOp("__addErrorContext", 2, prim_addErrorContext); - addPrimOp("__tryEval", 1, prim_tryEval); - addPrimOp("__getEnv", 1, prim_getEnv); - - // Strictness - addPrimOp("__seq", 2, prim_seq); - addPrimOp("__deepSeq", 2, prim_deepSeq); - - // Debugging - addPrimOp("__trace", 2, prim_trace); - - // Paths - addPrimOp("__toPath", 1, prim_toPath); - if (evalSettings.pureEval) - addPurityError("__storePath"); - else - addPrimOp("__storePath", 1, prim_storePath); - addPrimOp("__pathExists", 1, prim_pathExists); - addPrimOp("baseNameOf", 1, prim_baseNameOf); - addPrimOp("dirOf", 1, prim_dirOf); - addPrimOp("__readFile", 1, prim_readFile); - addPrimOp("__readDir", 1, prim_readDir); - addPrimOp("__findFile", 2, prim_findFile); - addPrimOp("__hashFile", 2, prim_hashFile); - - // Creating files - addPrimOp("__toXML", 1, prim_toXML); - addPrimOp("__toJSON", 1, prim_toJSON); - addPrimOp("__fromJSON", 1, prim_fromJSON); - addPrimOp("__toFile", 2, prim_toFile); - addPrimOp("__filterSource", 2, prim_filterSource); - addPrimOp("__path", 1, prim_path); - - // Sets - addPrimOp("__attrNames", 1, prim_attrNames); - addPrimOp("__attrValues", 1, prim_attrValues); - addPrimOp("__getAttr", 2, prim_getAttr); - addPrimOp("__unsafeGetAttrPos", 2, prim_unsafeGetAttrPos); - addPrimOp("__hasAttr", 2, prim_hasAttr); - addPrimOp("__isAttrs", 1, prim_isAttrs); - addPrimOp("removeAttrs", 2, prim_removeAttrs); - addPrimOp("__listToAttrs", 1, prim_listToAttrs); - addPrimOp("__intersectAttrs", 2, prim_intersectAttrs); - addPrimOp("__catAttrs", 2, prim_catAttrs); - addPrimOp("__functionArgs", 1, prim_functionArgs); - addPrimOp("__mapAttrs", 2, prim_mapAttrs); - - // Lists - addPrimOp("__isList", 1, prim_isList); - addPrimOp("__elemAt", 2, prim_elemAt); - addPrimOp("__head", 1, prim_head); - addPrimOp("__tail", 1, prim_tail); - addPrimOp("map", 2, prim_map); - addPrimOp("__filter", 2, prim_filter); - addPrimOp("__elem", 2, prim_elem); - addPrimOp("__concatLists", 1, prim_concatLists); - addPrimOp("__length", 1, prim_length); - addPrimOp("__foldl'", 3, prim_foldlStrict); - addPrimOp("__any", 2, prim_any); - addPrimOp("__all", 2, prim_all); - addPrimOp("__genList", 2, prim_genList); - addPrimOp("__sort", 2, prim_sort); - addPrimOp("__partition", 2, prim_partition); - addPrimOp("__concatMap", 2, prim_concatMap); - - // Integer arithmetic - addPrimOp("__add", 2, prim_add); - addPrimOp("__sub", 2, prim_sub); - addPrimOp("__mul", 2, prim_mul); - addPrimOp("__div", 2, prim_div); - addPrimOp("__bitAnd", 2, prim_bitAnd); - addPrimOp("__bitOr", 2, prim_bitOr); - addPrimOp("__bitXor", 2, prim_bitXor); - addPrimOp("__lessThan", 2, prim_lessThan); - - // String manipulation - addPrimOp("toString", 1, prim_toString); - addPrimOp("__substring", 3, prim_substring); - addPrimOp("__stringLength", 1, prim_stringLength); - addPrimOp("__hashString", 2, prim_hashString); - addPrimOp("__match", 2, prim_match); - addPrimOp("__split", 2, prim_split); - addPrimOp("__concatStringsSep", 2, prim_concatStringSep); - addPrimOp("__replaceStrings", 3, prim_replaceStrings); - - // Versions - addPrimOp("__parseDrvName", 1, prim_parseDrvName); - addPrimOp("__compareVersions", 2, prim_compareVersions); - addPrimOp("__splitVersion", 1, prim_splitVersion); - - // Derivations - addPrimOp("derivationStrict", 1, prim_derivationStrict); - addPrimOp("placeholder", 1, prim_placeholder); - - /* Add a wrapper around the derivation primop that computes the - `drvPath' and `outPath' attributes lazily. */ - string path = canonPath(settings.nixDataDir + "/nix/corepkgs/derivation.nix", true); - sDerivationNix = symbols.create(path); - evalFile(path, v); - addConstant("derivation", v); /* Add a value containing the current Nix expression search path. */ mkList(v, searchPath.size()); int n = 0; for (auto & i : searchPath) { - v2 = v.listElems()[n++] = allocValue(); + auto v2 = v.listElems()[n++] = allocValue(); mkAttrs(*v2, 2); mkString(*allocAttr(*v2, symbols.create("path")), i.second); mkString(*allocAttr(*v2, symbols.create("prefix")), i.first); @@ -2452,7 +3543,20 @@ void EvalState::createBaseEnv() if (RegisterPrimOp::primOps) for (auto & primOp : *RegisterPrimOp::primOps) if (!primOp.requiredFeature || settings.isExperimentalFeatureEnabled(*primOp.requiredFeature)) - addPrimOp(primOp.name, primOp.arity, primOp.primOp); + addPrimOp({ + .fun = primOp.fun, + .arity = std::max(primOp.args.size(), primOp.arity), + .name = symbols.create(primOp.name), + .args = std::move(primOp.args), + .doc = primOp.doc, + }); + + /* Add a wrapper around the derivation primop that computes the + `drvPath' and `outPath' attributes lazily. */ + string path = canonPath(settings.nixDataDir + "/nix/corepkgs/derivation.nix", true); + sDerivationNix = symbols.create(path); + evalFile(path, v); + addConstant("derivation", v); /* Now that we've added all primops, sort the `builtins' set, because attribute lookups expect it to be sorted. */ diff --git a/src/libexpr/primops.hh b/src/libexpr/primops.hh index 75c460ecf..ed5e2ea58 100644 --- a/src/libexpr/primops.hh +++ b/src/libexpr/primops.hh @@ -10,9 +10,11 @@ struct RegisterPrimOp struct Info { std::string name; - size_t arity; - PrimOpFun primOp; + std::vector<std::string> args; + size_t arity = 0; + const char * doc; std::optional<std::string> requiredFeature; + PrimOpFun fun; }; typedef std::vector<Info> PrimOps; @@ -26,6 +28,8 @@ struct RegisterPrimOp size_t arity, PrimOpFun fun, std::optional<std::string> requiredFeature = {}); + + RegisterPrimOp(Info && info); }; /* These primops are disabled without enableNativeCode, but plugins diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 0dbf4ae1d..06e8304b8 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -226,18 +226,187 @@ static void prim_fetchurl(EvalState & state, const Pos & pos, Value * * args, Va fetch(state, pos, args, v, "fetchurl", false, ""); } +static RegisterPrimOp primop_fetchurl({ + .name = "__fetchurl", + .args = {"url"}, + .doc = R"( + Download the specified URL and return the path of the downloaded + file. This function is not available if [restricted evaluation + mode](../command-ref/conf-file.md) is enabled. + )", + .fun = prim_fetchurl, +}); + static void prim_fetchTarball(EvalState & state, const Pos & pos, Value * * args, Value & v) { fetch(state, pos, args, v, "fetchTarball", true, "source"); } +static RegisterPrimOp primop_fetchTarball({ + .name = "fetchTarball", + .args = {"args"}, + .doc = R"( + Download the specified URL, unpack it and return the path of the + unpacked tree. The file must be a tape archive (`.tar`) compressed + with `gzip`, `bzip2` or `xz`. The top-level path component of the + files in the tarball is removed, so it is best if the tarball + contains a single directory at top level. The typical use of the + function is to obtain external Nix expression dependencies, such as + a particular version of Nixpkgs, e.g. + + ```nix + with import (fetchTarball https://github.com/NixOS/nixpkgs/archive/nixos-14.12.tar.gz) {}; + + stdenv.mkDerivation { … } + ``` + + The fetched tarball is cached for a certain amount of time (1 hour + by default) in `~/.cache/nix/tarballs/`. You can change the cache + timeout either on the command line with `--option tarball-ttl number + of seconds` or in the Nix configuration file with this option: ` + number of seconds to cache `. + + Note that when obtaining the hash with ` nix-prefetch-url ` the + option `--unpack` is required. + + This function can also verify the contents against a hash. In that + case, the function takes a set instead of a URL. The set requires + the attribute `url` and the attribute `sha256`, e.g. + + ```nix + with import (fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/nixos-14.12.tar.gz"; + sha256 = "1jppksrfvbk5ypiqdz4cddxdl8z6zyzdb2srq8fcffr327ld5jj2"; + }) {}; + + stdenv.mkDerivation { … } + ``` + + This function is not available if [restricted evaluation + mode](../command-ref/conf-file.md) is enabled. + )", + .fun = prim_fetchTarball, +}); + static void prim_fetchGit(EvalState &state, const Pos &pos, Value **args, Value &v) { fetchTree(state, pos, args, v, "git", true); } -static RegisterPrimOp r2("__fetchurl", 1, prim_fetchurl); -static RegisterPrimOp r3("fetchTarball", 1, prim_fetchTarball); -static RegisterPrimOp r4("fetchGit", 1, prim_fetchGit); +static RegisterPrimOp primop_fetchGit({ + .name = "fetchGit", + .args = {"args"}, + .doc = R"( + Fetch a path from git. *args* can be a URL, in which case the HEAD + of the repo at that URL is fetched. Otherwise, it can be an + attribute with the following attributes (all except `url` optional): + + - url + The URL of the repo. + + - name + The name of the directory the repo should be exported to in the + store. Defaults to the basename of the URL. + + - rev + The git revision to fetch. Defaults to the tip of `ref`. + + - ref + The git ref to look for the requested revision under. This is + often a branch or tag name. Defaults to `HEAD`. + + By default, the `ref` value is prefixed with `refs/heads/`. As + of Nix 2.3.0 Nix will not prefix `refs/heads/` if `ref` starts + with `refs/`. + + - submodules + A Boolean parameter that specifies whether submodules should be + checked out. Defaults to `false`. + + Here are some examples of how to use `fetchGit`. + + - To fetch a private repository over SSH: + + ```nix + builtins.fetchGit { + url = "git@github.com:my-secret/repository.git"; + ref = "master"; + rev = "adab8b916a45068c044658c4158d81878f9ed1c3"; + } + ``` + + - To fetch an arbitrary reference: + + ```nix + builtins.fetchGit { + url = "https://github.com/NixOS/nix.git"; + ref = "refs/heads/0.5-release"; + } + ``` + + - If the revision you're looking for is in the default branch of + the git repository you don't strictly need to specify the branch + name in the `ref` attribute. + + However, if the revision you're looking for is in a future + branch for the non-default branch you will need to specify the + the `ref` attribute as well. + + ```nix + builtins.fetchGit { + url = "https://github.com/nixos/nix.git"; + rev = "841fcbd04755c7a2865c51c1e2d3b045976b7452"; + ref = "1.11-maintenance"; + } + ``` + + > **Note** + > + > It is nice to always specify the branch which a revision + > belongs to. Without the branch being specified, the fetcher + > might fail if the default branch changes. Additionally, it can + > be confusing to try a commit from a non-default branch and see + > the fetch fail. If the branch is specified the fault is much + > more obvious. + + - If the revision you're looking for is in the default branch of + the git repository you may omit the `ref` attribute. + + ```nix + builtins.fetchGit { + url = "https://github.com/nixos/nix.git"; + rev = "841fcbd04755c7a2865c51c1e2d3b045976b7452"; + } + ``` + + - To fetch a specific tag: + + ```nix + builtins.fetchGit { + url = "https://github.com/nixos/nix.git"; + ref = "refs/tags/1.9"; + } + ``` + + - To fetch the latest version of a remote branch: + + ```nix + builtins.fetchGit { + url = "ssh://git@github.com/nixos/nix.git"; + ref = "master"; + } + ``` + + > **Note** + > + > Nix will refetch the branch in accordance with + > the option `tarball-ttl`. + + > **Note** + > + > This behavior is disabled in *Pure evaluation mode*. + )", + .fun = prim_fetchGit, +}); } diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc index 9f84ffb68..1cc0c5e2e 100644 --- a/src/libfetchers/github.cc +++ b/src/libfetchers/github.cc @@ -182,11 +182,21 @@ struct GitHubInputScheme : GitArchiveInputScheme { std::string type() override { return "github"; } + void addAccessToken(std::string & url) const + { + std::string accessToken = settings.githubAccessToken.get(); + if (accessToken != "") + url += "?access_token=" + accessToken; + } + Hash getRevFromRef(nix::ref<Store> store, const Input & input) const override { auto host_url = maybeGetStrAttr(input.attrs, "url").value_or("github.com"); auto url = fmt("https://api.%s/repos/%s/%s/commits/%s", // FIXME: check host_url, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"), *input.getRef()); + + addAccessToken(url); + auto json = nlohmann::json::parse( readFile( store->toRealPath( @@ -205,9 +215,7 @@ struct GitHubInputScheme : GitArchiveInputScheme host_url, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"), input.getRev()->to_string(Base16, false)); - std::string accessToken = settings.githubAccessToken.get(); - if (accessToken != "") - url += "?access_token=" + accessToken; + addAccessToken(url); return url; } diff --git a/src/libmain/common-args.cc b/src/libmain/common-args.cc index 09f4cd133..3411e2d7a 100644 --- a/src/libmain/common-args.cc +++ b/src/libmain/common-args.cc @@ -28,7 +28,7 @@ MixCommonArgs::MixCommonArgs(const string & programName) addFlag({ .longName = "option", - .description = "set a Nix configuration option (overriding nix.conf)", + .description = "set a Nix configuration option (overriding `nix.conf`)", .labels = {"name", "value"}, .handler = {[](std::string name, std::string value) { try { @@ -51,8 +51,8 @@ MixCommonArgs::MixCommonArgs(const string & programName) addFlag({ .longName = "log-format", - .description = "format of log output; \"raw\", \"internal-json\", \"bar\" " - "or \"bar-with-logs\"", + .description = "format of log output; `raw`, `internal-json`, `bar` " + "or `bar-with-logs`", .labels = {"format"}, .handler = {[](std::string format) { setLogFormat(format); }}, }); diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc index 2b1f25ca3..22ae51e47 100644 --- a/src/libmain/shared.cc +++ b/src/libmain/shared.cc @@ -277,6 +277,8 @@ void printVersion(const string & programName) #if HAVE_SODIUM cfg.push_back("signed-caches"); #endif + std::cout << "System type: " << settings.thisSystem << "\n"; + std::cout << "Additional system types: " << concatStringsSep(", ", settings.extraPlatforms.get()) << "\n"; std::cout << "Features: " << concatStringsSep(", ", cfg) << "\n"; std::cout << "System configuration file: " << settings.nixConfDir + "/nix.conf" << "\n"; std::cout << "User configuration files: " << diff --git a/src/libstore/dummy-store.cc b/src/libstore/dummy-store.cc new file mode 100644 index 000000000..7a5744bc1 --- /dev/null +++ b/src/libstore/dummy-store.cc @@ -0,0 +1,59 @@ +#include "store-api.hh" + +namespace nix { + +static std::string uriScheme = "dummy://"; + +struct DummyStore : public Store +{ + DummyStore(const Params & params) + : Store(params) + { } + + string getUri() override + { + return uriScheme; + } + + void queryPathInfoUncached(const StorePath & path, + Callback<std::shared_ptr<const ValidPathInfo>> callback) noexcept override + { + callback(nullptr); + } + + std::optional<StorePath> queryPathFromHashPart(const std::string & hashPart) override + { unsupported("queryPathFromHashPart"); } + + void addToStore(const ValidPathInfo & info, Source & source, + RepairFlag repair, CheckSigsFlag checkSigs) override + { unsupported("addToStore"); } + + StorePath addToStore(const string & name, const Path & srcPath, + FileIngestionMethod method, HashType hashAlgo, + PathFilter & filter, RepairFlag repair) override + { unsupported("addToStore"); } + + StorePath addTextToStore(const string & name, const string & s, + const StorePathSet & references, RepairFlag repair) override + { unsupported("addTextToStore"); } + + void narFromPath(const StorePath & path, Sink & sink) override + { unsupported("narFromPath"); } + + void ensurePath(const StorePath & path) override + { unsupported("ensurePath"); } + + BuildResult buildDerivation(const StorePath & drvPath, const BasicDerivation & drv, + BuildMode buildMode) override + { unsupported("buildDerivation"); } +}; + +static RegisterStoreImplementation regStore([]( + const std::string & uri, const Store::Params & params) + -> std::shared_ptr<Store> +{ + if (uri != uriScheme) return nullptr; + return std::make_shared<DummyStore>(params); +}); + +} diff --git a/src/libstore/filetransfer.hh b/src/libstore/filetransfer.hh index 25ade0add..0d608c8d8 100644 --- a/src/libstore/filetransfer.hh +++ b/src/libstore/filetransfer.hh @@ -17,15 +17,30 @@ struct FileTransferSettings : Config Setting<std::string> userAgentSuffix{this, "", "user-agent-suffix", "String appended to the user agent in HTTP requests."}; - Setting<size_t> httpConnections{this, 25, "http-connections", - "Number of parallel HTTP connections.", + Setting<size_t> httpConnections{ + this, 25, "http-connections", + R"( + The maximum number of parallel TCP connections used to fetch + files from binary caches and by other downloads. It defaults + to 25. 0 means no limit. + )", {"binary-caches-parallel-connections"}}; - Setting<unsigned long> connectTimeout{this, 0, "connect-timeout", - "Timeout for connecting to servers during downloads. 0 means use curl's builtin default."}; - - Setting<unsigned long> stalledDownloadTimeout{this, 300, "stalled-download-timeout", - "Timeout (in seconds) for receiving data from servers during download. Nix cancels idle downloads after this timeout's duration."}; + Setting<unsigned long> connectTimeout{ + this, 0, "connect-timeout", + R"( + The timeout (in seconds) for establishing connections in the + binary cache substituter. It corresponds to `curl`’s + `--connect-timeout` option. + )"}; + + Setting<unsigned long> stalledDownloadTimeout{ + this, 300, "stalled-download-timeout", + R"( + The timeout (in seconds) for receiving data from servers + during download. Nix cancels idle downloads after this + timeout's duration. + )"}; Setting<unsigned int> tries{this, 5, "download-attempts", "How often Nix will attempt to download a file before giving up."}; diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index 683fa5196..4a5971c3f 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -9,6 +9,8 @@ #include <dlfcn.h> #include <sys/utsname.h> +#include <nlohmann/json.hpp> + namespace nix { @@ -160,9 +162,9 @@ template<> std::string BaseSetting<SandboxMode>::to_string() const else abort(); } -template<> void BaseSetting<SandboxMode>::toJSON(JSONPlaceholder & out) +template<> nlohmann::json BaseSetting<SandboxMode>::toJSON() { - AbstractSetting::toJSON(out); + return AbstractSetting::toJSON(); } template<> void BaseSetting<SandboxMode>::convertToArg(Args & args, const std::string & category) diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index e3bb4cf84..ab9f42ce6 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -80,89 +80,209 @@ public: Setting<bool> keepGoing{this, false, "keep-going", "Whether to keep building derivations when another build fails."}; - Setting<bool> tryFallback{this, false, "fallback", - "Whether to fall back to building when substitution fails.", + Setting<bool> tryFallback{ + this, false, "fallback", + R"( + If set to `true`, Nix will fall back to building from source if a + binary substitute fails. This is equivalent to the `--fallback` + flag. The default is `false`. + )", {"build-fallback"}}; /* Whether to show build log output in real time. */ bool verboseBuild = true; Setting<size_t> logLines{this, 10, "log-lines", - "If verbose-build is false, the number of lines of the tail of " + "If `verbose-build` is false, the number of lines of the tail of " "the log to show if a build fails."}; - MaxBuildJobsSetting maxBuildJobs{this, 1, "max-jobs", - "Maximum number of parallel build jobs. \"auto\" means use number of cores.", + MaxBuildJobsSetting maxBuildJobs{ + this, 1, "max-jobs", + R"( + This option defines the maximum number of jobs that Nix will try to + build in parallel. The default is `1`. The special value `auto` + causes Nix to use the number of CPUs in your system. `0` is useful + when using remote builders to prevent any local builds (except for + `preferLocalBuild` derivation attribute which executes locally + regardless). It can be overridden using the `--max-jobs` (`-j`) + command line switch. + )", {"build-max-jobs"}}; - Setting<unsigned int> buildCores{this, getDefaultCores(), "cores", - "Number of CPU cores to utilize in parallel within a build, " - "i.e. by passing this number to Make via '-j'. 0 means that the " - "number of actual CPU cores on the local host ought to be " - "auto-detected.", {"build-cores"}}; + Setting<unsigned int> buildCores{ + this, getDefaultCores(), "cores", + R"( + Sets the value of the `NIX_BUILD_CORES` environment variable in the + invocation of builders. Builders can use this variable at their + discretion to control the maximum amount of parallelism. For + instance, in Nixpkgs, if the derivation attribute + `enableParallelBuilding` is set to `true`, the builder passes the + `-jN` flag to GNU Make. It can be overridden using the `--cores` + command line switch and defaults to `1`. The value `0` means that + the builder should use all available CPU cores in the system. + )", + {"build-cores"}}; /* Read-only mode. Don't copy stuff to the store, don't change the database. */ bool readOnlyMode = false; - Setting<std::string> thisSystem{this, SYSTEM, "system", - "The canonical Nix system name."}; - - Setting<time_t> maxSilentTime{this, 0, "max-silent-time", - "The maximum time in seconds that a builer can go without " - "producing any output on stdout/stderr before it is killed. " - "0 means infinity.", + Setting<std::string> thisSystem{ + this, SYSTEM, "system", + R"( + This option specifies the canonical Nix system name of the current + installation, such as `i686-linux` or `x86_64-darwin`. Nix can only + build derivations whose `system` attribute equals the value + specified here. In general, it never makes sense to modify this + value from its default, since you can use it to ‘lie’ about the + platform you are building on (e.g., perform a Mac OS build on a + Linux machine; the result would obviously be wrong). It only makes + sense if the Nix binaries can run on multiple platforms, e.g., + ‘universal binaries’ that run on `x86_64-linux` and `i686-linux`. + + It defaults to the canonical Nix system name detected by `configure` + at build time. + )"}; + + Setting<time_t> maxSilentTime{ + this, 0, "max-silent-time", + R"( + This option defines the maximum number of seconds that a builder can + go without producing any data on standard output or standard error. + This is useful (for instance in an automated build system) to catch + builds that are stuck in an infinite loop, or to catch remote builds + that are hanging due to network problems. It can be overridden using + the `--max-silent-time` command line switch. + + The value `0` means that there is no timeout. This is also the + default. + )", {"build-max-silent-time"}}; - Setting<time_t> buildTimeout{this, 0, "timeout", - "The maximum duration in seconds that a builder can run. " - "0 means infinity.", {"build-timeout"}}; + Setting<time_t> buildTimeout{ + this, 0, "timeout", + R"( + This option defines the maximum number of seconds that a builder can + run. This is useful (for instance in an automated build system) to + catch builds that are stuck in an infinite loop but keep writing to + their standard output or standard error. It can be overridden using + the `--timeout` command line switch. + + The value `0` means that there is no timeout. This is also the + default. + )", + {"build-timeout"}}; PathSetting buildHook{this, true, nixLibexecDir + "/nix/build-remote", "build-hook", "The path of the helper program that executes builds to remote machines."}; - Setting<std::string> builders{this, "@" + nixConfDir + "/machines", "builders", - "A semicolon-separated list of build machines, in the format of nix.machines."}; - - Setting<bool> buildersUseSubstitutes{this, false, "builders-use-substitutes", - "Whether build machines should use their own substitutes for obtaining " - "build dependencies if possible, rather than waiting for this host to " - "upload them."}; + Setting<std::string> builders{ + this, "@" + nixConfDir + "/machines", "builders", + "A semicolon-separated list of build machines, in the format of `nix.machines`."}; + + Setting<bool> buildersUseSubstitutes{ + this, false, "builders-use-substitutes", + R"( + If set to `true`, Nix will instruct remote build machines to use + their own binary substitutes if available. In practical terms, this + means that remote hosts will fetch as many build dependencies as + possible from their own substitutes (e.g, from `cache.nixos.org`), + instead of waiting for this host to upload them all. This can + drastically reduce build times if the network connection between + this computer and the remote build host is slow. + )"}; Setting<off_t> reservedSize{this, 8 * 1024 * 1024, "gc-reserved-space", "Amount of reserved disk space for the garbage collector."}; - Setting<bool> fsyncMetadata{this, true, "fsync-metadata", - "Whether SQLite should use fsync()."}; + Setting<bool> fsyncMetadata{ + this, true, "fsync-metadata", + R"( + If set to `true`, changes to the Nix store metadata (in + `/nix/var/nix/db`) are synchronously flushed to disk. This improves + robustness in case of system crashes, but reduces performance. The + default is `true`. + )"}; Setting<bool> useSQLiteWAL{this, !isWSL1(), "use-sqlite-wal", "Whether SQLite should use WAL mode."}; Setting<bool> syncBeforeRegistering{this, false, "sync-before-registering", - "Whether to call sync() before registering a path as valid."}; - - Setting<bool> useSubstitutes{this, true, "substitute", - "Whether to use substitutes.", + "Whether to call `sync()` before registering a path as valid."}; + + Setting<bool> useSubstitutes{ + this, true, "substitute", + R"( + If set to `true` (default), Nix will use binary substitutes if + available. This option can be disabled to force building from + source. + )", {"build-use-substitutes"}}; - Setting<std::string> buildUsersGroup{this, "", "build-users-group", - "The Unix group that contains the build users."}; + Setting<std::string> buildUsersGroup{ + this, "", "build-users-group", + R"( + This options specifies the Unix group containing the Nix build user + accounts. In multi-user Nix installations, builds should not be + performed by the Nix account since that would allow users to + arbitrarily modify the Nix store and database by supplying specially + crafted builders; and they cannot be performed by the calling user + since that would allow him/her to influence the build result. + + Therefore, if this option is non-empty and specifies a valid group, + builds will be performed under the user accounts that are a member + of the group specified here (as listed in `/etc/group`). Those user + accounts should not be used for any other purpose\! + + Nix will never run two builds under the same user account at the + same time. This is to prevent an obvious security hole: a malicious + user writing a Nix expression that modifies the build result of a + legitimate Nix expression being built by another user. Therefore it + is good to have as many Nix build user accounts as you can spare. + (Remember: uids are cheap.) + + The build users should have permission to create files in the Nix + store, but not delete them. Therefore, `/nix/store` should be owned + by the Nix account, its group should be the group specified here, + and its mode should be `1775`. + + If the build users group is empty, builds will be performed under + the uid of the Nix process (that is, the uid of the caller if + `NIX_REMOTE` is empty, the uid under which the Nix daemon runs if + `NIX_REMOTE` is `daemon`). Obviously, this should not be used in + multi-user settings with untrusted users. + )"}; Setting<bool> impersonateLinux26{this, false, "impersonate-linux-26", "Whether to impersonate a Linux 2.6 machine on newer kernels.", {"build-impersonate-linux-26"}}; - Setting<bool> keepLog{this, true, "keep-build-log", - "Whether to store build logs.", + Setting<bool> keepLog{ + this, true, "keep-build-log", + R"( + If set to `true` (the default), Nix will write the build log of a + derivation (i.e. the standard output and error of its builder) to + the directory `/nix/var/log/nix/drvs`. The build log can be + retrieved using the command `nix-store -l path`. + )", {"build-keep-log"}}; - Setting<bool> compressLog{this, true, "compress-build-log", - "Whether to compress logs.", + Setting<bool> compressLog{ + this, true, "compress-build-log", + R"( + If set to `true` (the default), build logs written to + `/nix/var/log/nix/drvs` will be compressed on the fly using bzip2. + Otherwise, they will not be compressed. + )", {"build-compress-log"}}; - Setting<unsigned long> maxLogSize{this, 0, "max-build-log-size", - "Maximum number of bytes a builder can write to stdout/stderr " - "before being killed (0 means no limit).", + Setting<unsigned long> maxLogSize{ + this, 0, "max-build-log-size", + R"( + This option defines the maximum number of bytes that a builder can + write to its stdout/stderr. If the builder exceeds this limit, it’s + killed. A value of `0` (the default) means that there is no limit. + )", {"build-max-log-size"}}; /* When buildRepeat > 0 and verboseBuild == true, whether to print @@ -177,53 +297,156 @@ public: "Whether to check if new GC roots can in fact be found by the " "garbage collector."}; - Setting<bool> gcKeepOutputs{this, false, "keep-outputs", - "Whether the garbage collector should keep outputs of live derivations.", + Setting<bool> gcKeepOutputs{ + this, false, "keep-outputs", + R"( + If `true`, the garbage collector will keep the outputs of + non-garbage derivations. If `false` (default), outputs will be + deleted unless they are GC roots themselves (or reachable from other + roots). + + In general, outputs must be registered as roots separately. However, + even if the output of a derivation is registered as a root, the + collector will still delete store paths that are used only at build + time (e.g., the C compiler, or source tarballs downloaded from the + network). To prevent it from doing so, set this option to `true`. + )", {"gc-keep-outputs"}}; - Setting<bool> gcKeepDerivations{this, true, "keep-derivations", - "Whether the garbage collector should keep derivers of live paths.", + Setting<bool> gcKeepDerivations{ + this, true, "keep-derivations", + R"( + If `true` (default), the garbage collector will keep the derivations + from which non-garbage store paths were built. If `false`, they will + be deleted unless explicitly registered as a root (or reachable from + other roots). + + Keeping derivation around is useful for querying and traceability + (e.g., it allows you to ask with what dependencies or options a + store path was built), so by default this option is on. Turn it off + to save a bit of disk space (or a lot if `keep-outputs` is also + turned on). + )", {"gc-keep-derivations"}}; - Setting<bool> autoOptimiseStore{this, false, "auto-optimise-store", - "Whether to automatically replace files with identical contents with hard links."}; - - Setting<bool> envKeepDerivations{this, false, "keep-env-derivations", - "Whether to add derivations as a dependency of user environments " - "(to prevent them from being GCed).", + Setting<bool> autoOptimiseStore{ + this, false, "auto-optimise-store", + R"( + If set to `true`, Nix automatically detects files in the store + that have identical contents, and replaces them with hard links to + a single copy. This saves disk space. If set to `false` (the + default), you can still run `nix-store --optimise` to get rid of + duplicate files. + )"}; + + Setting<bool> envKeepDerivations{ + this, false, "keep-env-derivations", + R"( + If `false` (default), derivations are not stored in Nix user + environments. That is, the derivations of any build-time-only + dependencies may be garbage-collected. + + If `true`, when you add a Nix derivation to a user environment, the + path of the derivation is stored in the user environment. Thus, the + derivation will not be garbage-collected until the user environment + generation is deleted (`nix-env --delete-generations`). To prevent + build-time-only dependencies from being collected, you should also + turn on `keep-outputs`. + + The difference between this option and `keep-derivations` is that + this one is “sticky”: it applies to any user environment created + while this option was enabled, while `keep-derivations` only applies + at the moment the garbage collector is run. + )", {"env-keep-derivations"}}; /* Whether to lock the Nix client and worker to the same CPU. */ bool lockCPU; - Setting<SandboxMode> sandboxMode{this, + Setting<SandboxMode> sandboxMode{ + this, #if __linux__ smEnabled #else smDisabled #endif , "sandbox", - "Whether to enable sandboxed builds. Can be \"true\", \"false\" or \"relaxed\".", + R"( + If set to `true`, builds will be performed in a *sandboxed + environment*, i.e., they’re isolated from the normal file system + hierarchy and will only see their dependencies in the Nix store, + the temporary build directory, private versions of `/proc`, + `/dev`, `/dev/shm` and `/dev/pts` (on Linux), and the paths + configured with the `sandbox-paths` option. This is useful to + prevent undeclared dependencies on files in directories such as + `/usr/bin`. In addition, on Linux, builds run in private PID, + mount, network, IPC and UTS namespaces to isolate them from other + processes in the system (except that fixed-output derivations do + not run in private network namespace to ensure they can access the + network). + + Currently, sandboxing only work on Linux and macOS. The use of a + sandbox requires that Nix is run as root (so you should use the + “build users” feature to perform the actual builds under different + users than root). + + If this option is set to `relaxed`, then fixed-output derivations + and derivations that have the `__noChroot` attribute set to `true` + do not run in sandboxes. + + The default is `true` on Linux and `false` on all other platforms. + )", {"build-use-chroot", "build-use-sandbox"}}; - Setting<PathSet> sandboxPaths{this, {}, "sandbox-paths", - "The paths to make available inside the build sandbox.", + Setting<PathSet> sandboxPaths{ + this, {}, "sandbox-paths", + R"( + A list of paths bind-mounted into Nix sandbox environments. You can + use the syntax `target=source` to mount a path in a different + location in the sandbox; for instance, `/bin=/nix-bin` will mount + the path `/nix-bin` as `/bin` inside the sandbox. If *source* is + followed by `?`, then it is not an error if *source* does not exist; + for example, `/dev/nvidiactl?` specifies that `/dev/nvidiactl` will + only be mounted in the sandbox if it exists in the host filesystem. + + Depending on how Nix was built, the default value for this option + may be empty or provide `/bin/sh` as a bind-mount of `bash`. + )", {"build-chroot-dirs", "build-sandbox-paths"}}; Setting<bool> sandboxFallback{this, true, "sandbox-fallback", "Whether to disable sandboxing when the kernel doesn't allow it."}; - Setting<PathSet> extraSandboxPaths{this, {}, "extra-sandbox-paths", - "Additional paths to make available inside the build sandbox.", + Setting<PathSet> extraSandboxPaths{ + this, {}, "extra-sandbox-paths", + R"( + A list of additional paths appended to `sandbox-paths`. Useful if + you want to extend its default value. + )", {"build-extra-chroot-dirs", "build-extra-sandbox-paths"}}; - Setting<size_t> buildRepeat{this, 0, "repeat", - "The number of times to repeat a build in order to verify determinism.", + Setting<size_t> buildRepeat{ + this, 0, "repeat", + R"( + How many times to repeat builds to check whether they are + deterministic. The default value is 0. If the value is non-zero, + every build is repeated the specified number of times. If the + contents of any of the runs differs from the previous ones and + `enforce-determinism` is true, the build is rejected and the + resulting store paths are not registered as “valid” in Nix’s + database. + )", {"build-repeat"}}; #if __linux__ - Setting<std::string> sandboxShmSize{this, "50%", "sandbox-dev-shm-size", - "The size of /dev/shm in the build sandbox."}; + Setting<std::string> sandboxShmSize{ + this, "50%", "sandbox-dev-shm-size", + R"( + This option determines the maximum size of the `tmpfs` filesystem + mounted on `/dev/shm` in Linux sandboxes. For the format, see the + description of the `size` option of `tmpfs` in mount8. The default + is `50%`. + )"}; Setting<Path> sandboxBuildDir{this, "/build", "sandbox-build-dir", "The build directory inside the sandbox."}; @@ -237,121 +460,411 @@ public: "Whether to log Darwin sandbox access violations to the system log."}; #endif - Setting<bool> runDiffHook{this, false, "run-diff-hook", - "Whether to run the program specified by the diff-hook setting " - "repeated builds produce a different result. Typically used to " - "plug in diffoscope."}; + Setting<bool> runDiffHook{ + this, false, "run-diff-hook", + R"( + If true, enable the execution of the `diff-hook` program. - PathSetting diffHook{this, true, "", "diff-hook", - "A program that prints out the differences between the two paths " - "specified on its command line."}; + When using the Nix daemon, `run-diff-hook` must be set in the + `nix.conf` configuration file, and cannot be passed at the command + line. + )"}; - Setting<bool> enforceDeterminism{this, true, "enforce-determinism", - "Whether to fail if repeated builds produce different output."}; + PathSetting diffHook{ + this, true, "", "diff-hook", + R"( + Absolute path to an executable capable of diffing build + results. The hook is executed if `run-diff-hook` is true, and the + output of a build is known to not be the same. This program is not + executed to determine if two results are the same. - Setting<Strings> trustedPublicKeys{this, - {"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="}, - "trusted-public-keys", - "Trusted public keys for secure substitution.", - {"binary-cache-public-keys"}}; + The diff hook is executed by the same user and group who ran the + build. However, the diff hook does not have write access to the + store path just built. + + The diff hook program receives three parameters: + + 1. A path to the previous build's results + + 2. A path to the current build's results - Setting<Strings> secretKeyFiles{this, {}, "secret-key-files", - "Secret keys with which to sign local builds."}; + 3. The path to the build's derivation - Setting<unsigned int> tarballTtl{this, 60 * 60, "tarball-ttl", - "How long downloaded files are considered up-to-date."}; + 4. The path to the build's scratch directory. This directory will + exist only if the build was run with `--keep-failed`. - Setting<bool> requireSigs{this, true, "require-sigs", - "Whether to check that any non-content-addressed path added to the " - "Nix store has a valid signature (that is, one signed using a key " - "listed in 'trusted-public-keys'."}; + The stderr and stdout output from the diff hook will not be + displayed to the user. Instead, it will print to the nix-daemon's + log. - Setting<StringSet> extraPlatforms{this, + When using the Nix daemon, `diff-hook` must be set in the `nix.conf` + configuration file, and cannot be passed at the command line. + )"}; + + Setting<bool> enforceDeterminism{ + this, true, "enforce-determinism", + "Whether to fail if repeated builds produce different output. See `repeat`."}; + + Setting<Strings> trustedPublicKeys{ + this, + {"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="}, + "trusted-public-keys", + R"( + A whitespace-separated list of public keys. When paths are copied + from another Nix store (such as a binary cache), they must be + signed with one of these keys. For example: + `cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= + hydra.nixos.org-1:CNHJZBh9K4tP3EKF6FkkgeVYsS3ohTl+oS0Qa8bezVs=`. + )", + {"binary-cache-public-keys"}}; + + Setting<Strings> secretKeyFiles{ + this, {}, "secret-key-files", + R"( + A whitespace-separated list of files containing secret (private) + keys. These are used to sign locally-built paths. They can be + generated using `nix-store --generate-binary-cache-key`. The + corresponding public key can be distributed to other users, who + can add it to `trusted-public-keys` in their `nix.conf`. + )"}; + + Setting<unsigned int> tarballTtl{ + this, 60 * 60, "tarball-ttl", + R"( + The number of seconds a downloaded tarball is considered fresh. If + the cached tarball is stale, Nix will check whether it is still up + to date using the ETag header. Nix will download a new version if + the ETag header is unsupported, or the cached ETag doesn't match. + + Setting the TTL to `0` forces Nix to always check if the tarball is + up to date. + + Nix caches tarballs in `$XDG_CACHE_HOME/nix/tarballs`. + + Files fetched via `NIX_PATH`, `fetchGit`, `fetchMercurial`, + `fetchTarball`, and `fetchurl` respect this TTL. + )"}; + + Setting<bool> requireSigs{ + this, true, "require-sigs", + R"( + If set to `true` (the default), any non-content-addressed path added + or copied to the Nix store (e.g. when substituting from a binary + cache) must have a valid signature, that is, be signed using one of + the keys listed in `trusted-public-keys` or `secret-key-files`. Set + to `false` to disable signature checking. + )"}; + + Setting<StringSet> extraPlatforms{ + this, std::string{SYSTEM} == "x86_64-linux" && !isWSL1() ? StringSet{"i686-linux"} : StringSet{}, "extra-platforms", - "Additional platforms that can be built on the local system. " - "These may be supported natively (e.g. armv7 on some aarch64 CPUs " - "or using hacks like qemu-user."}; - - Setting<StringSet> systemFeatures{this, getDefaultSystemFeatures(), + R"( + Platforms other than the native one which this machine is capable of + building for. This can be useful for supporting additional + architectures on compatible machines: i686-linux can be built on + x86\_64-linux machines (and the default for this setting reflects + this); armv7 is backwards-compatible with armv6 and armv5tel; some + aarch64 machines can also natively run 32-bit ARM code; and + qemu-user may be used to support non-native platforms (though this + may be slow and buggy). Most values for this are not enabled by + default because build systems will often misdetect the target + platform and generate incompatible code, so you may wish to + cross-check the results of using this option against proper + natively-built versions of your derivations. + )"}; + + Setting<StringSet> systemFeatures{ + this, getDefaultSystemFeatures(), "system-features", - "Optional features that this system implements (like \"kvm\")."}; + R"( + A set of system “features” supported by this machine, e.g. `kvm`. + Derivations can express a dependency on such features through the + derivation attribute `requiredSystemFeatures`. For example, the + attribute + + requiredSystemFeatures = [ "kvm" ]; + + ensures that the derivation can only be built on a machine with the + `kvm` feature. - Setting<Strings> substituters{this, + This setting by default includes `kvm` if `/dev/kvm` is accessible, + and the pseudo-features `nixos-test`, `benchmark` and `big-parallel` + that are used in Nixpkgs to route builds to specific machines. + )"}; + + Setting<Strings> substituters{ + this, nixStore == "/nix/store" ? Strings{"https://cache.nixos.org/"} : Strings(), "substituters", - "The URIs of substituters (such as https://cache.nixos.org/).", + R"( + A list of URLs of substituters, separated by whitespace. The default + is `https://cache.nixos.org`. + )", {"binary-caches"}}; // FIXME: provide a way to add to option values. - Setting<Strings> extraSubstituters{this, {}, "extra-substituters", - "Additional URIs of substituters.", + Setting<Strings> extraSubstituters{ + this, {}, "extra-substituters", + R"( + Additional binary caches appended to those specified in + `substituters`. When used by unprivileged users, untrusted + substituters (i.e. those not listed in `trusted-substituters`) are + silently ignored. + )", {"extra-binary-caches"}}; - Setting<StringSet> trustedSubstituters{this, {}, "trusted-substituters", - "Disabled substituters that may be enabled via the substituters option by untrusted users.", + Setting<StringSet> trustedSubstituters{ + this, {}, "trusted-substituters", + R"( + A list of URLs of substituters, separated by whitespace. These are + not used by default, but can be enabled by users of the Nix daemon + by specifying `--option substituters urls` on the command + line. Unprivileged users are only allowed to pass a subset of the + URLs listed in `substituters` and `trusted-substituters`. + )", {"trusted-binary-caches"}}; - Setting<Strings> trustedUsers{this, {"root"}, "trusted-users", - "Which users or groups are trusted to ask the daemon to do unsafe things."}; - - Setting<unsigned int> ttlNegativeNarInfoCache{this, 3600, "narinfo-cache-negative-ttl", - "The TTL in seconds for negative lookups in the disk cache i.e binary cache lookups that " - "return an invalid path result"}; - - Setting<unsigned int> ttlPositiveNarInfoCache{this, 30 * 24 * 3600, "narinfo-cache-positive-ttl", - "The TTL in seconds for positive lookups in the disk cache i.e binary cache lookups that " - "return a valid path result."}; + Setting<Strings> trustedUsers{ + this, {"root"}, "trusted-users", + R"( + A list of names of users (separated by whitespace) that have + additional rights when connecting to the Nix daemon, such as the + ability to specify additional binary caches, or to import unsigned + NARs. You can also specify groups by prefixing them with `@`; for + instance, `@wheel` means all users in the `wheel` group. The default + is `root`. + + > **Warning** + > + > Adding a user to `trusted-users` is essentially equivalent to + > giving that user root access to the system. For example, the user + > can set `sandbox-paths` and thereby obtain read access to + > directories that are otherwise inacessible to them. + )"}; + + Setting<unsigned int> ttlNegativeNarInfoCache{ + this, 3600, "narinfo-cache-negative-ttl", + R"( + The TTL in seconds for negative lookups. If a store path is queried + from a substituter but was not found, there will be a negative + lookup cached in the local disk cache database for the specified + duration. + )"}; + + Setting<unsigned int> ttlPositiveNarInfoCache{ + this, 30 * 24 * 3600, "narinfo-cache-positive-ttl", + R"( + The TTL in seconds for positive lookups. If a store path is queried + from a substituter, the result of the query will be cached in the + local disk cache database including some of the NAR metadata. The + default TTL is a month, setting a shorter TTL for positive lookups + can be useful for binary caches that have frequent garbage + collection, in which case having a more frequent cache invalidation + would prevent trying to pull the path again and failing with a hash + mismatch if the build isn't reproducible. + )"}; /* ?Who we trust to use the daemon in safe ways */ - Setting<Strings> allowedUsers{this, {"*"}, "allowed-users", - "Which users or groups are allowed to connect to the daemon."}; + Setting<Strings> allowedUsers{ + this, {"*"}, "allowed-users", + R"( + A list of names of users (separated by whitespace) that are allowed + to connect to the Nix daemon. As with the `trusted-users` option, + you can specify groups by prefixing them with `@`. Also, you can + allow all users by specifying `*`. The default is `*`. + + Note that trusted users are always allowed to connect. + )"}; Setting<bool> printMissing{this, true, "print-missing", "Whether to print what paths need to be built or downloaded."}; - Setting<std::string> preBuildHook{this, "", - "pre-build-hook", - "A program to run just before a build to set derivation-specific build settings."}; + Setting<std::string> preBuildHook{ + this, "", "pre-build-hook", + R"( + If set, the path to a program that can set extra derivation-specific + settings for this system. This is used for settings that can't be + captured by the derivation model itself and are too variable between + different versions of the same system to be hard-coded into nix. + + The hook is passed the derivation path and, if sandboxes are + enabled, the sandbox directory. It can then modify the sandbox and + send a series of commands to modify various settings to stdout. The + currently recognized commands are: + + - `extra-sandbox-paths` + Pass a list of files and directories to be included in the + sandbox for this build. One entry per line, terminated by an + empty line. Entries have the same format as `sandbox-paths`. + )"}; + + Setting<std::string> postBuildHook{ + this, "", "post-build-hook", + R"( + Optional. The path to a program to execute after each build. + + This option is only settable in the global `nix.conf`, or on the + command line by trusted users. + + When using the nix-daemon, the daemon executes the hook as `root`. + If the nix-daemon is not involved, the hook runs as the user + executing the nix-build. + + - The hook executes after an evaluation-time build. + + - The hook does not execute on substituted paths. + + - The hook's output always goes to the user's terminal. + + - If the hook fails, the build succeeds but no further builds + execute. - Setting<std::string> postBuildHook{this, "", "post-build-hook", - "A program to run just after each successful build."}; + - The hook executes synchronously, and blocks other builds from + progressing while it runs. - Setting<std::string> netrcFile{this, fmt("%s/%s", nixConfDir, "netrc"), "netrc-file", - "Path to the netrc file used to obtain usernames/passwords for downloads."}; + The program executes with no arguments. The program's environment + contains the following environment variables: + + - `DRV_PATH` + The derivation for the built paths. + + Example: + `/nix/store/5nihn1a7pa8b25l9zafqaqibznlvvp3f-bash-4.4-p23.drv` + + - `OUT_PATHS` + Output paths of the built derivation, separated by a space + character. + + Example: + `/nix/store/zf5lbh336mnzf1nlswdn11g4n2m8zh3g-bash-4.4-p23-dev + /nix/store/rjxwxwv1fpn9wa2x5ssk5phzwlcv4mna-bash-4.4-p23-doc + /nix/store/6bqvbzjkcp9695dq0dpl5y43nvy37pq1-bash-4.4-p23-info + /nix/store/r7fng3kk3vlpdlh2idnrbn37vh4imlj2-bash-4.4-p23-man + /nix/store/xfghy8ixrhz3kyy6p724iv3cxji088dx-bash-4.4-p23`. + )"}; + + Setting<std::string> netrcFile{ + this, fmt("%s/%s", nixConfDir, "netrc"), "netrc-file", + R"( + If set to an absolute path to a `netrc` file, Nix will use the HTTP + authentication credentials in this file when trying to download from + a remote host through HTTP or HTTPS. Defaults to + `$NIX_CONF_DIR/netrc`. + + The `netrc` file consists of a list of accounts in the following + format: + + machine my-machine + login my-username + password my-password + + For the exact syntax, see [the `curl` + documentation](https://ec.haxx.se/usingcurl-netrc.html). + + > **Note** + > + > This must be an absolute path, and `~` is not resolved. For + > example, `~/.netrc` won't resolve to your home directory's + > `.netrc`. + )"}; /* Path to the SSL CA file used */ Path caFile; #if __linux__ - Setting<bool> filterSyscalls{this, true, "filter-syscalls", - "Whether to prevent certain dangerous system calls, such as " - "creation of setuid/setgid files or adding ACLs or extended " - "attributes. Only disable this if you're aware of the " - "security implications."}; - - Setting<bool> allowNewPrivileges{this, false, "allow-new-privileges", - "Whether builders can acquire new privileges by calling programs with " - "setuid/setgid bits or with file capabilities."}; + Setting<bool> filterSyscalls{ + this, true, "filter-syscalls", + R"( + Whether to prevent certain dangerous system calls, such as + creation of setuid/setgid files or adding ACLs or extended + attributes. Only disable this if you're aware of the + security implications. + )"}; + + Setting<bool> allowNewPrivileges{ + this, false, "allow-new-privileges", + R"( + (Linux-specific.) By default, builders on Linux cannot acquire new + privileges by calling setuid/setgid programs or programs that have + file capabilities. For example, programs such as `sudo` or `ping` + will fail. (Note that in sandbox builds, no such programs are + available unless you bind-mount them into the sandbox via the + `sandbox-paths` option.) You can allow the use of such programs by + enabling this option. This is impure and usually undesirable, but + may be useful in certain scenarios (e.g. to spin up containers or + set up userspace network interfaces in tests). + )"}; #endif - Setting<Strings> hashedMirrors{this, {}, "hashed-mirrors", - "A list of servers used by builtins.fetchurl to fetch files by hash."}; - - Setting<uint64_t> minFree{this, 0, "min-free", - "Automatically run the garbage collector when free disk space drops below the specified amount."}; - - Setting<uint64_t> maxFree{this, std::numeric_limits<uint64_t>::max(), "max-free", - "Stop deleting garbage when free disk space is above the specified amount."}; + Setting<Strings> hashedMirrors{ + this, {}, "hashed-mirrors", + R"( + A list of web servers used by `builtins.fetchurl` to obtain files by + hash. The default is `http://tarballs.nixos.org/`. Given a hash type + *ht* and a base-16 hash *h*, Nix will try to download the file from + *hashed-mirror*/*ht*/*h*. This allows files to be downloaded even if + they have disappeared from their original URI. For example, given + the default mirror `http://tarballs.nixos.org/`, when building the + derivation + + ```nix + builtins.fetchurl { + url = "https://example.org/foo-1.2.3.tar.xz"; + sha256 = "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"; + } + ``` + + Nix will attempt to download this file from + `http://tarballs.nixos.org/sha256/2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae` + first. If it is not available there, if will try the original URI. + )"}; + + Setting<uint64_t> minFree{ + this, 0, "min-free", + R"( + When free disk space in `/nix/store` drops below `min-free` during a + build, Nix performs a garbage-collection until `max-free` bytes are + available or there is no more garbage. A value of `0` (the default) + disables this feature. + )"}; + + Setting<uint64_t> maxFree{ + this, std::numeric_limits<uint64_t>::max(), "max-free", + R"( + When a garbage collection is triggered by the `min-free` option, it + stops as soon as `max-free` bytes are available. The default is + infinity (i.e. delete all garbage). + )"}; Setting<uint64_t> minFreeCheckInterval{this, 5, "min-free-check-interval", "Number of seconds between checking free disk space."}; - Setting<Paths> pluginFiles{this, {}, "plugin-files", - "Plugins to dynamically load at nix initialization time."}; + Setting<Paths> pluginFiles{ + this, {}, "plugin-files", + R"( + A list of plugin files to be loaded by Nix. Each of these files will + be dlopened by Nix, allowing them to affect execution through static + initialization. In particular, these plugins may construct static + instances of RegisterPrimOp to add new primops or constants to the + expression language, RegisterStoreImplementation to add new store + implementations, RegisterCommand to add new subcommands to the `nix` + command, and RegisterSetting to add new nix config settings. See the + constructors for those types for more details. + + Since these files are loaded into the same address space as Nix + itself, they must be DSOs compatible with the instance of Nix + running at the time (i.e. compiled against the same headers, not + linked to any incompatible libraries). They should not be linked to + any Nix libs directly, as those will be available already at load + time. + + If an entry in the list is a directory, all files in the directory + are loaded as plugins (non-recursively). + )"}; Setting<std::string> githubAccessToken{this, "", "github-access-token", - "GitHub access token to get access to GitHub data through the GitHub API for github:<..> flakes."}; + "GitHub access token to get access to GitHub data through the GitHub API for `github:<..>` flakes."}; Setting<Strings> experimentalFeatures{this, {}, "experimental-features", "Experimental Nix features to enable."}; diff --git a/src/libstore/nar-accessor.cc b/src/libstore/nar-accessor.cc index 59ec164b6..a9efdd0b6 100644 --- a/src/libstore/nar-accessor.cc +++ b/src/libstore/nar-accessor.cc @@ -49,7 +49,8 @@ struct NarAccessor : public FSAccessor : acc(acc), source(source) { } - void createMember(const Path & path, NarMember member) { + void createMember(const Path & path, NarMember member) + { size_t level = std::count(path.begin(), path.end(), '/'); while (parents.size() > level) parents.pop(); diff --git a/src/libutil/args.cc b/src/libutil/args.cc index 986c5d1cd..147602415 100644 --- a/src/libutil/args.cc +++ b/src/libutil/args.cc @@ -3,6 +3,8 @@ #include <glob.h> +#include <nlohmann/json.hpp> + namespace nix { void Args::addFlag(Flag && flag_) @@ -205,6 +207,43 @@ bool Args::processArgs(const Strings & args, bool finish) return res; } +nlohmann::json Args::toJSON() +{ + auto flags = nlohmann::json::object(); + + for (auto & [name, flag] : longFlags) { + auto j = nlohmann::json::object(); + if (flag->shortName) + j["shortName"] = std::string(1, flag->shortName); + if (flag->description != "") + j["description"] = flag->description; + if (flag->category != "") + j["category"] = flag->category; + if (flag->handler.arity != ArityAny) + j["arity"] = flag->handler.arity; + if (!flag->labels.empty()) + j["labels"] = flag->labels; + flags[name] = std::move(j); + } + + auto args = nlohmann::json::array(); + + for (auto & arg : expectedArgs) { + auto j = nlohmann::json::object(); + j["label"] = arg.label; + j["optional"] = arg.optional; + if (arg.handler.arity != ArityAny) + j["arity"] = arg.handler.arity; + args.push_back(std::move(j)); + } + + auto res = nlohmann::json::object(); + res["description"] = description(); + res["flags"] = std::move(flags); + res["args"] = std::move(args); + return res; +} + static void hashTypeCompleter(size_t index, std::string_view prefix) { for (auto & type : hashTypes) @@ -313,11 +352,29 @@ void Command::printHelp(const string & programName, std::ostream & out) } } +nlohmann::json Command::toJSON() +{ + auto exs = nlohmann::json::array(); + + for (auto & example : examples()) { + auto ex = nlohmann::json::object(); + ex["description"] = example.description; + ex["command"] = chomp(stripIndentation(example.command)); + exs.push_back(std::move(ex)); + } + + auto res = Args::toJSON(); + res["examples"] = std::move(exs); + auto s = doc(); + if (s != "") res.emplace("doc", stripIndentation(s)); + return res; +} + MultiCommand::MultiCommand(const Commands & commands) : commands(commands) { expectArgs({ - .label = "command", + .label = "subcommand", .optional = true, .handler = {[=](std::string s) { assert(!command); @@ -387,4 +444,20 @@ bool MultiCommand::processArgs(const Strings & args, bool finish) return Args::processArgs(args, finish); } +nlohmann::json MultiCommand::toJSON() +{ + auto cmds = nlohmann::json::object(); + + for (auto & [name, commandFun] : commands) { + auto command = commandFun(); + auto j = command->toJSON(); + j["category"] = categories[command->category()]; + cmds[name] = std::move(j); + } + + auto res = Args::toJSON(); + res["commands"] = std::move(cmds); + return res; +} + } diff --git a/src/libutil/args.hh b/src/libutil/args.hh index 97a517344..3c1f87f7e 100644 --- a/src/libutil/args.hh +++ b/src/libutil/args.hh @@ -4,6 +4,8 @@ #include <map> #include <memory> +#include <nlohmann/json_fwd.hpp> + #include "util.hh" namespace nix { @@ -20,6 +22,7 @@ public: virtual void printHelp(const string & programName, std::ostream & out); + /* Return a short one-line description of the command. */ virtual std::string description() { return ""; } protected: @@ -203,6 +206,8 @@ public: }); } + virtual nlohmann::json toJSON(); + friend class MultiCommand; }; @@ -217,6 +222,9 @@ struct Command : virtual Args virtual void prepare() { }; virtual void run() = 0; + /* Return documentation about this command, in Markdown format. */ + virtual std::string doc() { return ""; } + struct Example { std::string description; @@ -234,6 +242,8 @@ struct Command : virtual Args virtual Category category() { return catDefault; } void printHelp(const string & programName, std::ostream & out) override; + + nlohmann::json toJSON() override; }; typedef std::map<std::string, std::function<ref<Command>()>> Commands; @@ -259,6 +269,8 @@ public: bool processFlag(Strings::iterator & pos, Strings::iterator end) override; bool processArgs(const Strings & args, bool finish) override; + + nlohmann::json toJSON() override; }; Strings argvToStrings(int argc, char * * argv); diff --git a/src/libutil/config.cc b/src/libutil/config.cc index 8fc700a2b..3cf720bce 100644 --- a/src/libutil/config.cc +++ b/src/libutil/config.cc @@ -1,6 +1,7 @@ #include "config.hh" #include "args.hh" -#include "json.hh" + +#include <nlohmann/json.hpp> namespace nix { @@ -131,15 +132,18 @@ void Config::resetOverriden() s.second.setting->overriden = false; } -void Config::toJSON(JSONObject & out) +nlohmann::json Config::toJSON() { + auto res = nlohmann::json::object(); for (auto & s : _settings) if (!s.second.isAlias) { - JSONObject out2(out.object(s.first)); - out2.attr("description", s.second.setting->description); - JSONPlaceholder out3(out2.placeholder("value")); - s.second.setting->toJSON(out3); + auto obj = nlohmann::json::object(); + obj.emplace("description", s.second.setting->description); + obj.emplace("aliases", s.second.setting->aliases); + obj.emplace("value", s.second.setting->toJSON()); + res.emplace(s.first, obj); } + return res; } void Config::convertToArgs(Args & args, const std::string & category) @@ -153,7 +157,7 @@ AbstractSetting::AbstractSetting( const std::string & name, const std::string & description, const std::set<std::string> & aliases) - : name(name), description(description), aliases(aliases) + : name(name), description(stripIndentation(description)), aliases(aliases) { } @@ -162,9 +166,9 @@ void AbstractSetting::setDefault(const std::string & str) if (!overriden) set(str); } -void AbstractSetting::toJSON(JSONPlaceholder & out) +nlohmann::json AbstractSetting::toJSON() { - out.write(to_string()); + return to_string(); } void AbstractSetting::convertToArg(Args & args, const std::string & category) @@ -172,9 +176,9 @@ void AbstractSetting::convertToArg(Args & args, const std::string & category) } template<typename T> -void BaseSetting<T>::toJSON(JSONPlaceholder & out) +nlohmann::json BaseSetting<T>::toJSON() { - out.write(value); + return value; } template<typename T> @@ -255,11 +259,9 @@ template<> std::string BaseSetting<Strings>::to_string() const return concatStringsSep(" ", value); } -template<> void BaseSetting<Strings>::toJSON(JSONPlaceholder & out) +template<> nlohmann::json BaseSetting<Strings>::toJSON() { - JSONList list(out.list()); - for (auto & s : value) - list.elem(s); + return value; } template<> void BaseSetting<StringSet>::set(const std::string & str) @@ -272,11 +274,9 @@ template<> std::string BaseSetting<StringSet>::to_string() const return concatStringsSep(" ", value); } -template<> void BaseSetting<StringSet>::toJSON(JSONPlaceholder & out) +template<> nlohmann::json BaseSetting<StringSet>::toJSON() { - JSONList list(out.list()); - for (auto & s : value) - list.elem(s); + return value; } template class BaseSetting<int>; @@ -323,10 +323,12 @@ void GlobalConfig::resetOverriden() config->resetOverriden(); } -void GlobalConfig::toJSON(JSONObject & out) +nlohmann::json GlobalConfig::toJSON() { + auto res = nlohmann::json::object(); for (auto & config : *configRegistrations) - config->toJSON(out); + res.update(config->toJSON()); + return res; } void GlobalConfig::convertToArgs(Args & args, const std::string & category) diff --git a/src/libutil/config.hh b/src/libutil/config.hh index 66073546e..2b4265806 100644 --- a/src/libutil/config.hh +++ b/src/libutil/config.hh @@ -4,6 +4,8 @@ #include "types.hh" +#include <nlohmann/json_fwd.hpp> + #pragma once namespace nix { @@ -42,8 +44,6 @@ namespace nix { class Args; class AbstractSetting; -class JSONPlaceholder; -class JSONObject; class AbstractConfig { @@ -97,7 +97,7 @@ public: * Outputs all settings to JSON * - out: JSONObject to write the configuration to */ - virtual void toJSON(JSONObject & out) = 0; + virtual nlohmann::json toJSON() = 0; /** * Converts settings to `Args` to be used on the command line interface @@ -167,7 +167,7 @@ public: void resetOverriden() override; - void toJSON(JSONObject & out) override; + nlohmann::json toJSON() override; void convertToArgs(Args & args, const std::string & category) override; }; @@ -206,7 +206,7 @@ protected: virtual std::string to_string() const = 0; - virtual void toJSON(JSONPlaceholder & out); + virtual nlohmann::json toJSON(); virtual void convertToArg(Args & args, const std::string & category); @@ -251,7 +251,7 @@ public: void convertToArg(Args & args, const std::string & category) override; - void toJSON(JSONPlaceholder & out) override; + nlohmann::json toJSON() override; }; template<typename T> @@ -319,7 +319,7 @@ struct GlobalConfig : public AbstractConfig void resetOverriden() override; - void toJSON(JSONObject & out) override; + nlohmann::json toJSON() override; void convertToArgs(Args & args, const std::string & category) override; diff --git a/src/libutil/logging.hh b/src/libutil/logging.hh index 09619aac6..63cb2b268 100644 --- a/src/libutil/logging.hh +++ b/src/libutil/logging.hh @@ -37,10 +37,12 @@ typedef uint64_t ActivityId; struct LoggerSettings : Config { - Setting<bool> showTrace{this, - false, - "show-trace", - "Whether to show a stack trace on evaluation errors."}; + Setting<bool> showTrace{ + this, false, "show-trace", + R"( + Where Nix should print out a stack trace in case of Nix + expression evaluation errors. + )"}; }; extern LoggerSettings loggerSettings; diff --git a/src/libutil/tests/config.cc b/src/libutil/tests/config.cc index 74c59fd31..c5abefe11 100644 --- a/src/libutil/tests/config.cc +++ b/src/libutil/tests/config.cc @@ -1,9 +1,9 @@ -#include "json.hh" #include "config.hh" #include "args.hh" #include <sstream> #include <gtest/gtest.h> +#include <nlohmann/json.hpp> namespace nix { @@ -33,7 +33,7 @@ namespace nix { const auto iter = settings.find("name-of-the-setting"); ASSERT_NE(iter, settings.end()); ASSERT_EQ(iter->second.value, ""); - ASSERT_EQ(iter->second.description, "description"); + ASSERT_EQ(iter->second.description, "description\n"); } TEST(Config, getDefinedOverridenSettingNotSet) { @@ -59,7 +59,7 @@ namespace nix { const auto iter = settings.find("name-of-the-setting"); ASSERT_NE(iter, settings.end()); ASSERT_EQ(iter->second.value, "value"); - ASSERT_EQ(iter->second.description, "description"); + ASSERT_EQ(iter->second.description, "description\n"); } TEST(Config, getDefinedSettingSet2) { @@ -73,7 +73,7 @@ namespace nix { const auto e = settings.find("name-of-the-setting"); ASSERT_NE(e, settings.end()); ASSERT_EQ(e->second.value, "value"); - ASSERT_EQ(e->second.description, "description"); + ASSERT_EQ(e->second.description, "description\n"); } TEST(Config, addSetting) { @@ -152,29 +152,16 @@ namespace nix { } TEST(Config, toJSONOnEmptyConfig) { - std::stringstream out; - { // Scoped to force the destructor of JSONObject to write the final `}` - JSONObject obj(out); - Config config; - config.toJSON(obj); - } - - ASSERT_EQ(out.str(), "{}"); + ASSERT_EQ(Config().toJSON().dump(), "{}"); } TEST(Config, toJSONOnNonEmptyConfig) { - std::stringstream out; - { // Scoped to force the destructor of JSONObject to write the final `}` - JSONObject obj(out); - - Config config; - std::map<std::string, Config::SettingInfo> settings; - Setting<std::string> setting{&config, "", "name-of-the-setting", "description"}; - setting.assign("value"); + Config config; + std::map<std::string, Config::SettingInfo> settings; + Setting<std::string> setting{&config, "", "name-of-the-setting", "description"}; + setting.assign("value"); - config.toJSON(obj); - } - ASSERT_EQ(out.str(), R"#({"name-of-the-setting":{"description":"description","value":"value"}})#"); + ASSERT_EQ(config.toJSON().dump(), R"#({"name-of-the-setting":{"aliases":[],"description":"description\n","value":"value"}})#"); } TEST(Config, setSettingAlias) { diff --git a/src/libutil/util.cc b/src/libutil/util.cc index c0b9698ee..9e7142e01 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -1464,6 +1464,47 @@ string base64Decode(std::string_view s) } +std::string stripIndentation(std::string_view s) +{ + size_t minIndent = 10000; + size_t curIndent = 0; + bool atStartOfLine = true; + + for (auto & c : s) { + if (atStartOfLine && c == ' ') + curIndent++; + else if (c == '\n') { + if (atStartOfLine) + minIndent = std::max(minIndent, curIndent); + curIndent = 0; + atStartOfLine = true; + } else { + if (atStartOfLine) { + minIndent = std::min(minIndent, curIndent); + atStartOfLine = false; + } + } + } + + std::string res; + + size_t pos = 0; + while (pos < s.size()) { + auto eol = s.find('\n', pos); + if (eol == s.npos) eol = s.size(); + if (eol - pos > minIndent) + res.append(s.substr(pos + minIndent, eol - pos - minIndent)); + res.push_back('\n'); + pos = eol + 1; + } + + return res; +} + + +////////////////////////////////////////////////////////////////////// + + static Sync<std::pair<unsigned short, unsigned short>> windowSize{{0, 0}}; diff --git a/src/libutil/util.hh b/src/libutil/util.hh index 3a20679a8..082e26375 100644 --- a/src/libutil/util.hh +++ b/src/libutil/util.hh @@ -464,6 +464,12 @@ string base64Encode(std::string_view s); string base64Decode(std::string_view s); +/* Remove common leading whitespace from the lines in the string + 's'. For example, if every line is indented by at least 3 spaces, + then we remove 3 spaces from the start of every line. */ +std::string stripIndentation(std::string_view s); + + /* Get a value for the specified key from an associate container. */ template <class T> std::optional<typename T::mapped_type> get(const T & map, const typename T::key_type & key) diff --git a/src/nix/add-to-store.cc b/src/nix/add-to-store.cc index 713155840..023ffa4ed 100644 --- a/src/nix/add-to-store.cc +++ b/src/nix/add-to-store.cc @@ -36,6 +36,14 @@ struct CmdAddToStore : MixDryRun, StoreCommand return "add a path to the Nix store"; } + std::string doc() override + { + return R"( + Copy the file or directory *path* to the Nix store, and + print the resulting store path on standard output. + )"; + } + Examples examples() override { return { diff --git a/src/nix/command.cc b/src/nix/command.cc index 4a93d8e73..fefd72f45 100644 --- a/src/nix/command.cc +++ b/src/nix/command.cc @@ -4,12 +4,25 @@ #include "nixexpr.hh" #include "profiles.hh" +#include <nlohmann/json.hpp> + extern char * * environ __attribute__((weak)); namespace nix { Commands * RegisterCommand::commands = nullptr; +void NixMultiCommand::printHelp(const string & programName, std::ostream & out) +{ + MultiCommand::printHelp(programName, out); +} + +nlohmann::json NixMultiCommand::toJSON() +{ + // FIXME: use Command::toJSON() as well. + return MultiCommand::toJSON(); +} + StoreCommand::StoreCommand() { } diff --git a/src/nix/command.hh b/src/nix/command.hh index bc46a2028..d60c8aeb6 100644 --- a/src/nix/command.hh +++ b/src/nix/command.hh @@ -21,6 +21,13 @@ static constexpr Command::Category catSecondary = 100; static constexpr Command::Category catUtility = 101; static constexpr Command::Category catNixInstallation = 102; +struct NixMultiCommand : virtual MultiCommand, virtual Command +{ + void printHelp(const string & programName, std::ostream & out) override; + + nlohmann::json toJSON() override; +}; + /* A command that requires a Nix store. */ struct StoreCommand : virtual Command { diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 653f8db1b..ae6f4c5f9 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -933,7 +933,7 @@ struct CmdFlakeShow : FlakeCommand } }; -struct CmdFlake : virtual MultiCommand, virtual Command +struct CmdFlake : NixMultiCommand { CmdFlake() : MultiCommand({ @@ -963,11 +963,6 @@ struct CmdFlake : virtual MultiCommand, virtual Command command->second->prepare(); command->second->run(); } - - void printHelp(const string & programName, std::ostream & out) override - { - MultiCommand::printHelp(programName, out); - } }; static auto r1 = registerCommand<CmdFlake>("flake"); diff --git a/src/nix/installables.cc b/src/nix/installables.cc index c3c3b9a12..9bf6b7caa 100644 --- a/src/nix/installables.cc +++ b/src/nix/installables.cc @@ -76,7 +76,7 @@ MixFlakeOptions::MixFlakeOptions() addFlag({ .longName = "override-input", - .description = "override a specific flake input (e.g. 'dwarffs/nixpkgs')", + .description = "override a specific flake input (e.g. `dwarffs/nixpkgs`)", .labels = {"input-path", "flake-url"}, .handler = {[&](std::string inputPath, std::string flakeRef) { lockFlags.inputOverrides.insert_or_assign( @@ -116,7 +116,7 @@ SourceExprCommand::SourceExprCommand() addFlag({ .longName = "file", .shortName = 'f', - .description = "evaluate FILE rather than the default", + .description = "evaluate *file* rather than the default", .labels = {"file"}, .handler = {&file}, .completer = completePath @@ -124,7 +124,7 @@ SourceExprCommand::SourceExprCommand() addFlag({ .longName ="expr", - .description = "evaluate attributes from EXPR", + .description = "evaluate attributes from *expr*", .labels = {"expr"}, .handler = {&expr} }); diff --git a/src/nix/local.mk b/src/nix/local.mk index b057b7cc6..e96200685 100644 --- a/src/nix/local.mk +++ b/src/nix/local.mk @@ -19,7 +19,7 @@ nix_CXXFLAGS += -I src/libutil -I src/libstore -I src/libfetchers -I src/libexpr nix_LIBS = libexpr libmain libfetchers libstore libutil -nix_LDFLAGS = -pthread $(SODIUM_LIBS) $(EDITLINE_LIBS) $(BOOST_LDFLAGS) -lboost_context -lboost_thread -lboost_system +nix_LDFLAGS = -pthread $(SODIUM_LIBS) $(EDITLINE_LIBS) $(BOOST_LDFLAGS) -lboost_context -lboost_thread -lboost_system -llowdown $(foreach name, \ nix-build nix-channel nix-collect-garbage nix-copy-closure nix-daemon nix-env nix-hash nix-instantiate nix-prefetch-url nix-shell nix-store, \ diff --git a/src/nix/main.cc b/src/nix/main.cc index e62657e95..e9479f564 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -17,6 +17,8 @@ #include <netdb.h> #include <netinet/in.h> +#include <nlohmann/json.hpp> + extern std::string chrootHelperName; void chrootHelper(int argc, char * * argv); @@ -140,6 +142,11 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs printHelp(programName, std::cout); throw Exit(); } + + std::string description() override + { + return "a tool for reproducible and declarative configuration management"; + } }; void mainWrapped(int argc, char * * argv) @@ -172,6 +179,29 @@ void mainWrapped(int argc, char * * argv) NixArgs args; + if (argc == 2 && std::string(argv[1]) == "__dump-args") { + std::cout << args.toJSON().dump() << "\n"; + return; + } + + if (argc == 2 && std::string(argv[1]) == "__dump-builtins") { + EvalState state({}, openStore("dummy://")); + auto res = nlohmann::json::object(); + auto builtins = state.baseEnv.values[0]->attrs; + for (auto & builtin : *builtins) { + auto b = nlohmann::json::object(); + if (builtin.value->type != tPrimOp) continue; + auto primOp = builtin.value->primOp; + if (!primOp->doc) continue; + b["arity"] = primOp->arity; + b["args"] = primOp->args; + b["doc"] = trim(stripIndentation(primOp->doc)); + res[(std::string) builtin.name] = std::move(b); + } + std::cout << res.dump() << "\n"; + return; + } + Finally printCompletions([&]() { if (completions) { diff --git a/src/nix/markdown.cc b/src/nix/markdown.cc new file mode 100644 index 000000000..40788a42f --- /dev/null +++ b/src/nix/markdown.cc @@ -0,0 +1,50 @@ +#include "markdown.hh" +#include "util.hh" +#include "finally.hh" + +#include <sys/queue.h> +extern "C" { +#include <lowdown.h> +} + +namespace nix { + +std::string renderMarkdownToTerminal(std::string_view markdown) +{ + struct lowdown_opts opts { + .type = LOWDOWN_TERM, + .maxdepth = 20, + .cols = std::min(getWindowSize().second, (unsigned short) 80), + .hmargin = 0, + .vmargin = 0, + .feat = LOWDOWN_COMMONMARK | LOWDOWN_FENCED | LOWDOWN_DEFLIST | LOWDOWN_TABLES, + .oflags = 0, + }; + + auto doc = lowdown_doc_new(&opts); + if (!doc) + throw Error("cannot allocate Markdown document"); + Finally freeDoc([&]() { lowdown_doc_free(doc); }); + + size_t maxn = 0; + auto node = lowdown_doc_parse(doc, &maxn, markdown.data(), markdown.size()); + if (!node) + throw Error("cannot parse Markdown document"); + Finally freeNode([&]() { lowdown_node_free(node); }); + + auto renderer = lowdown_term_new(&opts); + if (!renderer) + throw Error("cannot allocate Markdown renderer"); + Finally freeRenderer([&]() { lowdown_term_free(renderer); }); + + auto buf = lowdown_buf_new(16384); + if (!buf) + throw Error("cannot allocate Markdown output buffer"); + Finally freeBuffer([&]() { lowdown_buf_free(buf); }); + + lowdown_term_rndr(buf, nullptr, renderer, node); + + return std::string(buf->data, buf->size); +} + +} diff --git a/src/nix/markdown.hh b/src/nix/markdown.hh new file mode 100644 index 000000000..78320fcf5 --- /dev/null +++ b/src/nix/markdown.hh @@ -0,0 +1,7 @@ +#include "types.hh" + +namespace nix { + +std::string renderMarkdownToTerminal(std::string_view markdown); + +} diff --git a/src/nix/profile.cc b/src/nix/profile.cc index 9172fd34c..7ce4dfe4c 100644 --- a/src/nix/profile.cc +++ b/src/nix/profile.cc @@ -441,7 +441,7 @@ struct CmdProfileDiffClosures : virtual StoreCommand, MixDefaultProfile } }; -struct CmdProfile : virtual MultiCommand, virtual Command +struct CmdProfile : NixMultiCommand { CmdProfile() : MultiCommand({ @@ -465,11 +465,6 @@ struct CmdProfile : virtual MultiCommand, virtual Command command->second->prepare(); command->second->run(); } - - void printHelp(const string & programName, std::ostream & out) override - { - MultiCommand::printHelp(programName, out); - } }; static auto r1 = registerCommand<CmdProfile>("profile"); diff --git a/src/nix/registry.cc b/src/nix/registry.cc index ebee4545c..367268683 100644 --- a/src/nix/registry.cc +++ b/src/nix/registry.cc @@ -115,7 +115,7 @@ struct CmdRegistryPin : virtual Args, EvalCommand } }; -struct CmdRegistry : virtual MultiCommand, virtual Command +struct CmdRegistry : virtual NixMultiCommand { CmdRegistry() : MultiCommand({ @@ -141,11 +141,6 @@ struct CmdRegistry : virtual MultiCommand, virtual Command command->second->prepare(); command->second->run(); } - - void printHelp(const string & programName, std::ostream & out) override - { - MultiCommand::printHelp(programName, out); - } }; static auto r1 = registerCommand<CmdRegistry>("registry"); diff --git a/src/nix/repl.cc b/src/nix/repl.cc index bb9578a11..329999475 100644 --- a/src/nix/repl.cc +++ b/src/nix/repl.cc @@ -32,6 +32,7 @@ extern "C" { #include "globals.hh" #include "command.hh" #include "finally.hh" +#include "markdown.hh" #if HAVE_BOEHMGC #define GC_INCLUDE_NEW @@ -419,7 +420,8 @@ bool NixRepl::processLine(string line) << " :r Reload all files\n" << " :s <expr> Build dependencies of derivation, then start nix-shell\n" << " :t <expr> Describe result of evaluation\n" - << " :u <expr> Build derivation, then start nix-shell\n"; + << " :u <expr> Build derivation, then start nix-shell\n" + << " :doc <expr> Show documentation of a builtin function\n"; } else if (command == ":a" || command == ":add") { @@ -513,6 +515,29 @@ bool NixRepl::processLine(string line) else if (command == ":q" || command == ":quit") return false; + else if (command == ":doc") { + Value v; + evalString(arg, v); + if (auto doc = state->getDoc(v)) { + std::string markdown; + + if (!doc->args.empty() && doc->name) { + auto args = doc->args; + for (auto & arg : args) + arg = "*" + arg + "*"; + + markdown += + "**Synopsis:** `builtins." + (std::string) (*doc->name) + "` " + + concatStringsSep(" ", args) + "\n\n"; + } + + markdown += trim(stripIndentation(doc->doc)); + + std::cout << renderMarkdownToTerminal(markdown); + } else + throw Error("value does not have documentation"); + } + else if (command != "") throw Error("unknown command '%1%'", command); @@ -786,7 +811,7 @@ struct CmdRepl : StoreCommand, MixEvalArgs return { Example{ "Display all special commands within the REPL:", - "nix repl\n nix-repl> :?" + "nix repl\nnix-repl> :?" } }; } diff --git a/src/nix/show-config.cc b/src/nix/show-config.cc index 4fd8886de..3ed1ad2aa 100644 --- a/src/nix/show-config.cc +++ b/src/nix/show-config.cc @@ -2,7 +2,8 @@ #include "common-args.hh" #include "shared.hh" #include "store-api.hh" -#include "json.hh" + +#include <nlohmann/json.hpp> using namespace nix; @@ -19,8 +20,7 @@ struct CmdShowConfig : Command, MixJSON { if (json) { // FIXME: use appropriate JSON types (bool, ints, etc). - JSONObject jsonObj(std::cout); - globalConfig.toJSON(jsonObj); + logger->stdout("%s", globalConfig.toJSON().dump()); } else { std::map<std::string, Config::SettingInfo> settings; globalConfig.getSettings(settings); |