aboutsummaryrefslogtreecommitdiff
path: root/src/libexpr
diff options
context:
space:
mode:
Diffstat (limited to 'src/libexpr')
-rw-r--r--src/libexpr/eval.cc111
-rw-r--r--src/libexpr/eval.hh127
-rw-r--r--src/libexpr/flake/flake.cc3
-rw-r--r--src/libexpr/lexer.l2
-rw-r--r--src/libexpr/parser.y92
-rw-r--r--src/libexpr/primops.cc358
-rw-r--r--src/libexpr/primops.hh19
-rw-r--r--src/libexpr/primops/context.cc54
-rw-r--r--src/libexpr/primops/fetchClosure.cc251
-rw-r--r--src/libexpr/primops/fetchMercurial.cc6
-rw-r--r--src/libexpr/primops/fetchTree.cc33
-rw-r--r--src/libexpr/primops/fromTOML.cc36
-rw-r--r--src/libexpr/search-path.cc56
-rw-r--r--src/libexpr/search-path.hh108
-rw-r--r--src/libexpr/tests/error_traces.cc2
-rw-r--r--src/libexpr/tests/search-path.cc90
-rw-r--r--src/libexpr/tests/value/print.cc236
-rw-r--r--src/libexpr/value.hh15
18 files changed, 1291 insertions, 308 deletions
diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index 740a5e677..be1bdb806 100644
--- a/src/libexpr/eval.cc
+++ b/src/libexpr/eval.cc
@@ -4,6 +4,7 @@
#include "util.hh"
#include "store-api.hh"
#include "derivations.hh"
+#include "downstream-placeholder.hh"
#include "globals.hh"
#include "eval-inline.hh"
#include "filetransfer.hh"
@@ -94,11 +95,16 @@ RootValue allocRootValue(Value * v)
#endif
}
-void Value::print(const SymbolTable & symbols, std::ostream & str,
- std::set<const void *> * seen) const
+void Value::print(const SymbolTable &symbols, std::ostream &str,
+ std::set<const void *> *seen, int depth) const
+
{
checkInterrupt();
+ if (depth <= 0) {
+ str << "«too deep»";
+ return;
+ }
switch (internalType) {
case tInt:
str << integer;
@@ -122,7 +128,7 @@ void Value::print(const SymbolTable & symbols, std::ostream & str,
str << "{ ";
for (auto & i : attrs->lexicographicOrder(symbols)) {
str << symbols[i->name] << " = ";
- i->value->print(symbols, str, seen);
+ i->value->print(symbols, str, seen, depth - 1);
str << "; ";
}
str << "}";
@@ -138,7 +144,7 @@ void Value::print(const SymbolTable & symbols, std::ostream & str,
str << "[ ";
for (auto v2 : listItems()) {
if (v2)
- v2->print(symbols, str, seen);
+ v2->print(symbols, str, seen, depth - 1);
else
str << "(nullptr)";
str << " ";
@@ -180,11 +186,10 @@ void Value::print(const SymbolTable & symbols, std::ostream & str,
}
}
-
-void Value::print(const SymbolTable & symbols, std::ostream & str, bool showRepeated) const
-{
+void Value::print(const SymbolTable &symbols, std::ostream &str,
+ bool showRepeated, int depth) const {
std::set<const void *> seen;
- print(symbols, str, showRepeated ? nullptr : &seen);
+ print(symbols, str, showRepeated ? nullptr : &seen, depth);
}
// Pretty print types for assertion errors
@@ -210,20 +215,21 @@ const Value * getPrimOp(const Value &v) {
return primOp;
}
-std::string_view showType(ValueType type)
+std::string_view showType(ValueType type, bool withArticle)
{
+ #define WA(a, w) withArticle ? a " " w : w
switch (type) {
- case nInt: return "an integer";
- case nBool: return "a Boolean";
- case nString: return "a string";
- case nPath: return "a path";
+ case nInt: return WA("an", "integer");
+ case nBool: return WA("a", "Boolean");
+ case nString: return WA("a", "string");
+ case nPath: return WA("a", "path");
case nNull: return "null";
- case nAttrs: return "a set";
- case nList: return "a list";
- case nFunction: return "a function";
- case nExternal: return "an external value";
- case nFloat: return "a float";
- case nThunk: return "a thunk";
+ case nAttrs: return WA("a", "set");
+ case nList: return WA("a", "list");
+ case nFunction: return WA("a", "function");
+ case nExternal: return WA("an", "external value");
+ case nFloat: return WA("a", "float");
+ case nThunk: return WA("a", "thunk");
}
abort();
}
@@ -492,7 +498,7 @@ ErrorBuilder & ErrorBuilder::withFrame(const Env & env, const Expr & expr)
EvalState::EvalState(
- const Strings & _searchPath,
+ const SearchPath & _searchPath,
ref<Store> store,
std::shared_ptr<Store> buildStore)
: sWith(symbols.create("<with>"))
@@ -557,30 +563,32 @@ EvalState::EvalState(
/* Initialise the Nix expression search path. */
if (!evalSettings.pureEval) {
- for (auto & i : _searchPath) addToSearchPath(i);
- for (auto & i : evalSettings.nixPath.get()) addToSearchPath(i);
+ for (auto & i : _searchPath.elements)
+ addToSearchPath(SearchPath::Elem {i});
+ for (auto & i : evalSettings.nixPath.get())
+ addToSearchPath(SearchPath::Elem::parse(i));
}
if (evalSettings.restrictEval || evalSettings.pureEval) {
allowedPaths = PathSet();
- for (auto & i : searchPath) {
- auto r = resolveSearchPathElem(i);
- if (!r.first) continue;
+ for (auto & i : searchPath.elements) {
+ auto r = resolveSearchPathPath(i.path);
+ if (!r) continue;
- auto path = r.second;
+ auto path = *std::move(r);
- if (store->isInStore(r.second)) {
+ if (store->isInStore(path)) {
try {
StorePathSet closure;
- store->computeFSClosure(store->toStorePath(r.second).first, closure);
+ store->computeFSClosure(store->toStorePath(path).first, closure);
for (auto & path : closure)
allowPath(path);
} catch (InvalidPath &) {
- allowPath(r.second);
+ allowPath(path);
}
} else
- allowPath(r.second);
+ allowPath(path);
}
}
@@ -701,28 +709,34 @@ Path EvalState::toRealPath(const Path & path, const NixStringContext & context)
}
-Value * EvalState::addConstant(const std::string & name, Value & v)
+Value * EvalState::addConstant(const std::string & name, Value & v, Constant info)
{
Value * v2 = allocValue();
*v2 = v;
- addConstant(name, v2);
+ addConstant(name, v2, info);
return v2;
}
-void EvalState::addConstant(const std::string & name, Value * v)
+void EvalState::addConstant(const std::string & name, Value * v, Constant info)
{
- staticBaseEnv->vars.emplace_back(symbols.create(name), baseEnvDispl);
- baseEnv.values[baseEnvDispl++] = v;
auto name2 = name.substr(0, 2) == "__" ? name.substr(2) : name;
- baseEnv.values[0]->attrs->push_back(Attr(symbols.create(name2), v));
-}
+ constantInfos.push_back({name2, info});
-Value * EvalState::addPrimOp(const std::string & name,
- size_t arity, PrimOpFun primOp)
-{
- return addPrimOp(PrimOp { .fun = primOp, .arity = arity, .name = name });
+ if (!(evalSettings.pureEval && info.impureOnly)) {
+ /* Check the type, if possible.
+
+ We might know the type of a thunk in advance, so be allowed
+ to just write it down in that case. */
+ if (auto gotType = v->type(true); gotType != nThunk)
+ assert(info.type == gotType);
+
+ /* Install value the base environment. */
+ staticBaseEnv->vars.emplace_back(symbols.create(name), baseEnvDispl);
+ baseEnv.values[baseEnvDispl++] = v;
+ baseEnv.values[0]->attrs->push_back(Attr(symbols.create(name2), v));
+ }
}
@@ -736,7 +750,10 @@ Value * EvalState::addPrimOp(PrimOp && primOp)
vPrimOp->mkPrimOp(new PrimOp(primOp));
Value v;
v.mkApp(vPrimOp, vPrimOp);
- return addConstant(primOp.name, v);
+ return addConstant(primOp.name, v, {
+ .type = nThunk, // FIXME
+ .doc = primOp.doc,
+ });
}
auto envName = symbols.create(primOp.name);
@@ -762,13 +779,13 @@ std::optional<EvalState::Doc> EvalState::getDoc(Value & v)
{
if (v.isPrimOp()) {
auto v2 = &v;
- if (v2->primOp->doc)
+ if (auto * doc = v2->primOp->doc)
return Doc {
.pos = {},
.name = v2->primOp->name,
.arity = v2->primOp->arity,
.args = v2->primOp->args,
- .doc = v2->primOp->doc,
+ .doc = doc,
};
}
return {};
@@ -1058,7 +1075,7 @@ void EvalState::mkOutputString(
? store->printStorePath(*std::move(optOutputPath))
/* Downstream we would substitute this for an actual path once
we build the floating CA derivation */
- : downstreamPlaceholder(*store, drvPath, outputName),
+ : DownstreamPlaceholder::unknownCaOutput(drvPath, outputName).render(),
NixStringContext {
NixStringContextElem::Built {
.drvPath = drvPath,
@@ -2380,7 +2397,7 @@ DerivedPath EvalState::coerceToDerivedPath(const PosIdx pos, Value & v, std::str
// This is testing for the case of CA derivations
auto sExpected = optOutputPath
? store->printStorePath(*optOutputPath)
- : downstreamPlaceholder(*store, b.drvPath, output);
+ : DownstreamPlaceholder::unknownCaOutput(b.drvPath, output).render();
if (s != sExpected)
error(
"string '%s' has context with the output '%s' from derivation '%s', but the string is not the right placeholder for this derivation output. It should be '%s'",
@@ -2619,7 +2636,7 @@ Strings EvalSettings::getDefaultNixPath()
{
Strings res;
auto add = [&](const Path & p, const std::string & s = std::string()) {
- if (pathExists(p)) {
+ if (pathAccessible(p)) {
if (s.empty()) {
res.push_back(p);
} else {
diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh
index a90ff34c0..277e77ad5 100644
--- a/src/libexpr/eval.hh
+++ b/src/libexpr/eval.hh
@@ -9,6 +9,7 @@
#include "config.hh"
#include "experimental-features.hh"
#include "input-accessor.hh"
+#include "search-path.hh"
#include <map>
#include <optional>
@@ -25,15 +26,72 @@ struct DerivedPath;
enum RepairFlag : bool;
+/**
+ * Function that implements a primop.
+ */
typedef void (* PrimOpFun) (EvalState & state, const PosIdx pos, Value * * args, Value & v);
+/**
+ * Info about a primitive operation, and its implementation
+ */
struct PrimOp
{
- PrimOpFun fun;
- size_t arity;
+ /**
+ * Name of the primop. `__` prefix is treated specially.
+ */
std::string name;
+
+ /**
+ * Names of the parameters of a primop, for primops that take a
+ * fixed number of arguments to be substituted for these parameters.
+ */
std::vector<std::string> args;
+
+ /**
+ * Aritiy of the primop.
+ *
+ * If `args` is not empty, this field will be computed from that
+ * field instead, so it doesn't need to be manually set.
+ */
+ size_t arity = 0;
+
+ /**
+ * Optional free-form documentation about the primop.
+ */
const char * doc = nullptr;
+
+ /**
+ * Implementation of the primop.
+ */
+ PrimOpFun fun;
+
+ /**
+ * Optional experimental for this to be gated on.
+ */
+ std::optional<ExperimentalFeature> experimentalFeature;
+};
+
+/**
+ * Info about a constant
+ */
+struct Constant
+{
+ /**
+ * Optional type of the constant (known since it is a fixed value).
+ *
+ * @todo we should use an enum for this.
+ */
+ ValueType type = nThunk;
+
+ /**
+ * Optional free-form documentation about the constant.
+ */
+ const char * doc = nullptr;
+
+ /**
+ * Whether the constant is impure, and not available in pure mode.
+ */
+ bool impureOnly = false;
};
#if HAVE_BOEHMGC
@@ -65,11 +123,6 @@ std::string printValue(const EvalState & state, const Value & v);
std::ostream & operator << (std::ostream & os, const ValueType t);
-// FIXME: maybe change this to an std::variant<SourcePath, URL>.
-typedef std::pair<std::string, std::string> SearchPathElem;
-typedef std::list<SearchPathElem> SearchPath;
-
-
/**
* Initialise the Boehm GC, if applicable.
*/
@@ -256,7 +309,7 @@ private:
SearchPath searchPath;
- std::map<std::string, std::pair<bool, std::string>> searchPathResolved;
+ std::map<std::string, std::optional<std::string>> searchPathResolved;
/**
* Cache used by checkSourcePath().
@@ -283,12 +336,12 @@ private:
public:
EvalState(
- const Strings & _searchPath,
+ const SearchPath & _searchPath,
ref<Store> store,
std::shared_ptr<Store> buildStore = nullptr);
~EvalState();
- void addToSearchPath(const std::string & s);
+ void addToSearchPath(SearchPath::Elem && elem);
SearchPath getSearchPath() { return searchPath; }
@@ -370,12 +423,16 @@ public:
* Look up a file in the search path.
*/
SourcePath findFile(const std::string_view path);
- SourcePath findFile(SearchPath & searchPath, const std::string_view path, const PosIdx pos = noPos);
+ SourcePath findFile(const SearchPath & searchPath, const std::string_view path, const PosIdx pos = noPos);
/**
+ * Try to resolve a search path value (not the optinal key part)
+ *
* If the specified search path element is a URI, download it.
+ *
+ * If it is not found, return `std::nullopt`
*/
- std::pair<bool, std::string> resolveSearchPathElem(const SearchPathElem & elem);
+ std::optional<std::string> resolveSearchPathPath(const SearchPath::Path & path);
/**
* Evaluate an expression to normal form
@@ -483,7 +540,7 @@ public:
* Coerce to `DerivedPath`.
*
* Must be a string which is either a literal store path or a
- * "placeholder (see `downstreamPlaceholder()`).
+ * "placeholder (see `DownstreamPlaceholder`).
*
* Even more importantly, the string context must be exactly one
* element, which is either a `NixStringContextElem::Opaque` or
@@ -509,18 +566,23 @@ public:
*/
std::shared_ptr<StaticEnv> staticBaseEnv; // !!! should be private
+ /**
+ * Name and documentation about every constant.
+ *
+ * Constants from primops are hard to crawl, and their docs will go
+ * here too.
+ */
+ std::vector<std::pair<std::string, Constant>> constantInfos;
+
private:
unsigned int baseEnvDispl = 0;
void createBaseEnv();
- Value * addConstant(const std::string & name, Value & v);
+ Value * addConstant(const std::string & name, Value & v, Constant info);
- void addConstant(const std::string & name, Value * v);
-
- Value * addPrimOp(const std::string & name,
- size_t arity, PrimOpFun primOp);
+ void addConstant(const std::string & name, Value * v, Constant info);
Value * addPrimOp(PrimOp && primOp);
@@ -534,6 +596,10 @@ public:
std::optional<std::string> name;
size_t arity;
std::vector<std::string> args;
+ /**
+ * Unlike the other `doc` fields in this file, this one should never be
+ * `null`.
+ */
const char * doc;
};
@@ -622,7 +688,7 @@ public:
* @param optOutputPath Optional output path for that string. Must
* be passed if and only if output store object is input-addressed.
* Will be printed to form string if passed, otherwise a placeholder
- * will be used (see `downstreamPlaceholder()`).
+ * will be used (see `DownstreamPlaceholder`).
*/
void mkOutputString(
Value & value,
@@ -700,8 +766,11 @@ struct DebugTraceStacker {
/**
* @return A string representing the type of the value `v`.
+ *
+ * @param withArticle Whether to begin with an english article, e.g. "an
+ * integer" vs "integer".
*/
-std::string_view showType(ValueType type);
+std::string_view showType(ValueType type, bool withArticle = true);
std::string showType(const Value & v);
/**
@@ -733,7 +802,12 @@ struct EvalSettings : Config
Setting<Strings> nixPath{
this, getDefaultNixPath(), "nix-path",
- "List of directories to be searched for `<...>` file references."};
+ R"(
+ List of directories to be searched for `<...>` file references
+
+ In particular, outside of [pure evaluation mode](#conf-pure-evaluation), this determines the value of
+ [`builtins.nixPath`](@docroot@/language/builtin-constants.md#builtin-constants-nixPath).
+ )"};
Setting<bool> restrictEval{
this, false, "restrict-eval",
@@ -741,11 +815,18 @@ struct EvalSettings : Config
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`.
+ [`allowed-uris`](../command-ref/conf-file.md#conf-allowed-uris).
+ 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."};
+ R"(
+ Pure evaluation mode ensures that the result of Nix expressions is fully determined by explicitly declared inputs, and not influenced by external state:
+
+ - Restrict file system and network access to files specified by cryptographic hash
+ - Disable [`bultins.currentSystem`](@docroot@/language/builtin-constants.md#builtins-currentSystem) and [`builtins.currentTime`](@docroot@/language/builtin-constants.md#builtins-currentTime)
+ )"
+ };
Setting<bool> enableImportFromDerivation{
this, true, "allow-import-from-derivation",
diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc
index 60bb6a71e..5aa44d6a1 100644
--- a/src/libexpr/flake/flake.cc
+++ b/src/libexpr/flake/flake.cc
@@ -788,9 +788,6 @@ static RegisterPrimOp r2({
```nix
(builtins.getFlake "github:edolstra/dwarffs").rev
```
-
- This function is only available if you enable the experimental feature
- `flakes`.
)",
.fun = prim_getFlake,
.experimentalFeature = Xp::Flakes,
diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l
index 462b3b602..a3a8608d9 100644
--- a/src/libexpr/lexer.l
+++ b/src/libexpr/lexer.l
@@ -36,7 +36,7 @@ static inline PosIdx makeCurPos(const YYLTYPE & loc, ParseData * data)
#define CUR_POS makeCurPos(*yylloc, data)
// backup to recover from yyless(0)
-YYLTYPE prev_yylloc;
+thread_local YYLTYPE prev_yylloc;
static void initLoc(YYLTYPE * loc)
{
diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y
index 4d981712a..0a1ad9967 100644
--- a/src/libexpr/parser.y
+++ b/src/libexpr/parser.y
@@ -275,7 +275,12 @@ static Expr * stripIndentation(const PosIdx pos, SymbolTable & symbols,
}
/* If this is a single string, then don't do a concatenation. */
- return es2->size() == 1 && dynamic_cast<ExprString *>((*es2)[0].second) ? (*es2)[0].second : new ExprConcatStrings(pos, true, es2);
+ if (es2->size() == 1 && dynamic_cast<ExprString *>((*es2)[0].second)) {
+ auto *const result = (*es2)[0].second;
+ delete es2;
+ return result;
+ }
+ return new ExprConcatStrings(pos, true, es2);
}
@@ -330,7 +335,7 @@ void yyerror(YYLTYPE * loc, yyscan_t scanner, ParseData * data, const char * err
%type <ind_string_parts> ind_string_parts
%type <e> path_start string_parts string_attr
%type <id> attr
-%token <id> ID ATTRPATH
+%token <id> ID
%token <str> STR IND_STR
%token <n> INT
%token <nf> FLOAT
@@ -658,7 +663,7 @@ Expr * EvalState::parse(
ParseData data {
.state = *this,
.symbols = symbols,
- .basePath = std::move(basePath),
+ .basePath = basePath,
.origin = {origin},
};
@@ -729,19 +734,9 @@ Expr * EvalState::parseStdin()
}
-void EvalState::addToSearchPath(const std::string & s)
+void EvalState::addToSearchPath(SearchPath::Elem && elem)
{
- size_t pos = s.find('=');
- std::string prefix;
- Path path;
- if (pos == std::string::npos) {
- path = s;
- } else {
- prefix = std::string(s, 0, pos);
- path = std::string(s, pos + 1);
- }
-
- searchPath.emplace_back(prefix, path);
+ searchPath.elements.emplace_back(std::move(elem));
}
@@ -751,22 +746,19 @@ SourcePath EvalState::findFile(const std::string_view path)
}
-SourcePath EvalState::findFile(SearchPath & searchPath, const std::string_view path, const PosIdx pos)
+SourcePath EvalState::findFile(const SearchPath & searchPath, const std::string_view path, const PosIdx pos)
{
- for (auto & i : searchPath) {
- std::string suffix;
- if (i.first.empty())
- suffix = concatStrings("/", path);
- else {
- auto s = i.first.size();
- if (path.compare(0, s, i.first) != 0 ||
- (path.size() > s && path[s] != '/'))
- continue;
- suffix = path.size() == s ? "" : concatStrings("/", path.substr(s));
- }
- auto r = resolveSearchPathElem(i);
- if (!r.first) continue;
- Path res = r.second + suffix;
+ for (auto & i : searchPath.elements) {
+ auto suffixOpt = i.prefix.suffixIfPotentialMatch(path);
+
+ if (!suffixOpt) continue;
+ auto suffix = *suffixOpt;
+
+ auto rOpt = resolveSearchPathPath(i.path);
+ if (!rOpt) continue;
+ auto r = *rOpt;
+
+ Path res = suffix == "" ? r : concatStrings(r, "/", suffix);
if (pathExists(res)) return CanonPath(canonPath(res));
}
@@ -783,49 +775,53 @@ SourcePath EvalState::findFile(SearchPath & searchPath, const std::string_view p
}
-std::pair<bool, std::string> EvalState::resolveSearchPathElem(const SearchPathElem & elem)
+std::optional<std::string> EvalState::resolveSearchPathPath(const SearchPath::Path & value0)
{
- auto i = searchPathResolved.find(elem.second);
+ auto & value = value0.s;
+ auto i = searchPathResolved.find(value);
if (i != searchPathResolved.end()) return i->second;
- std::pair<bool, std::string> res;
+ std::optional<std::string> res;
- if (EvalSettings::isPseudoUrl(elem.second)) {
+ if (EvalSettings::isPseudoUrl(value)) {
try {
auto storePath = fetchers::downloadTarball(
- store, EvalSettings::resolvePseudoUrl(elem.second), "source", false).first.storePath;
- res = { true, store->toRealPath(storePath) };
+ store, EvalSettings::resolvePseudoUrl(value), "source", false).tree.storePath;
+ res = { store->toRealPath(storePath) };
} catch (FileTransferError & e) {
logWarning({
- .msg = hintfmt("Nix search path entry '%1%' cannot be downloaded, ignoring", elem.second)
+ .msg = hintfmt("Nix search path entry '%1%' cannot be downloaded, ignoring", value)
});
- res = { false, "" };
+ res = std::nullopt;
}
}
- else if (hasPrefix(elem.second, "flake:")) {
+ else if (hasPrefix(value, "flake:")) {
experimentalFeatureSettings.require(Xp::Flakes);
- auto flakeRef = parseFlakeRef(elem.second.substr(6), {}, true, false);
- debug("fetching flake search path element '%s''", elem.second);
+ auto flakeRef = parseFlakeRef(value.substr(6), {}, true, false);
+ debug("fetching flake search path element '%s''", value);
auto storePath = flakeRef.resolve(store).fetchTree(store).first.storePath;
- res = { true, store->toRealPath(storePath) };
+ res = { store->toRealPath(storePath) };
}
else {
- auto path = absPath(elem.second);
+ auto path = absPath(value);
if (pathExists(path))
- res = { true, path };
+ res = { path };
else {
logWarning({
- .msg = hintfmt("Nix search path entry '%1%' does not exist, ignoring", elem.second)
+ .msg = hintfmt("Nix search path entry '%1%' does not exist, ignoring", value)
});
- res = { false, "" };
+ res = std::nullopt;
}
}
- debug("resolved search path element '%s' to '%s'", elem.second, res.second);
+ if (res)
+ debug("resolved search path element '%s' to '%s'", value, *res);
+ else
+ debug("failed to resolve search path element '%s'", value);
- searchPathResolved[elem.second] = res;
+ searchPathResolved[value] = res;
return res;
}
diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc
index 6fbd66389..8a61e57cc 100644
--- a/src/libexpr/primops.cc
+++ b/src/libexpr/primops.cc
@@ -1,11 +1,12 @@
#include "archive.hh"
#include "derivations.hh"
+#include "downstream-placeholder.hh"
#include "eval-inline.hh"
#include "eval.hh"
#include "globals.hh"
#include "json-to-value.hh"
#include "names.hh"
-#include "references.hh"
+#include "path-references.hh"
#include "store-api.hh"
#include "util.hh"
#include "value-to-json.hh"
@@ -87,7 +88,7 @@ StringMap EvalState::realiseContext(const NixStringContext & context)
auto outputs = resolveDerivedPath(*store, drv);
for (auto & [outputName, outputPath] : outputs) {
res.insert_or_assign(
- downstreamPlaceholder(*store, drv.drvPath, outputName),
+ DownstreamPlaceholder::unknownCaOutput(drv.drvPath, outputName).render(),
store->printStorePath(outputPath)
);
}
@@ -237,7 +238,7 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v
}
}
-static RegisterPrimOp primop_scopedImport(RegisterPrimOp::Info {
+static RegisterPrimOp primop_scopedImport(PrimOp {
.name = "scopedImport",
.arity = 2,
.fun = [](EvalState & state, const PosIdx pos, Value * * args, Value & v)
@@ -691,7 +692,7 @@ static void prim_genericClosure(EvalState & state, const PosIdx pos, Value * * a
v.listElems()[n++] = i;
}
-static RegisterPrimOp primop_genericClosure(RegisterPrimOp::Info {
+static RegisterPrimOp primop_genericClosure(PrimOp {
.name = "__genericClosure",
.args = {"attrset"},
.arity = 1,
@@ -808,7 +809,7 @@ static void prim_addErrorContext(EvalState & state, const PosIdx pos, Value * *
}
}
-static RegisterPrimOp primop_addErrorContext(RegisterPrimOp::Info {
+static RegisterPrimOp primop_addErrorContext(PrimOp {
.name = "__addErrorContext",
.arity = 2,
.fun = prim_addErrorContext,
@@ -1151,16 +1152,14 @@ drvName, Bindings * attrs, Value & v)
if (i->value->type() == nNull) continue;
}
- if (i->name == state.sContentAddressed) {
- contentAddressed = state.forceBool(*i->value, noPos, context_below);
- if (contentAddressed)
- experimentalFeatureSettings.require(Xp::CaDerivations);
+ if (i->name == state.sContentAddressed && state.forceBool(*i->value, noPos, context_below)) {
+ contentAddressed = true;
+ experimentalFeatureSettings.require(Xp::CaDerivations);
}
- else if (i->name == state.sImpure) {
- isImpure = state.forceBool(*i->value, noPos, context_below);
- if (isImpure)
- experimentalFeatureSettings.require(Xp::ImpureDerivations);
+ else if (i->name == state.sImpure && state.forceBool(*i->value, noPos, context_below)) {
+ isImpure = true;
+ experimentalFeatureSettings.require(Xp::ImpureDerivations);
}
/* The `args' attribute is special: it supplies the
@@ -1401,7 +1400,7 @@ drvName, Bindings * attrs, Value & v)
v.mkAttrs(result);
}
-static RegisterPrimOp primop_derivationStrict(RegisterPrimOp::Info {
+static RegisterPrimOp primop_derivationStrict(PrimOp {
.name = "derivationStrict",
.arity = 1,
.fun = prim_derivationStrict,
@@ -1502,7 +1501,9 @@ static RegisterPrimOp primop_storePath({
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.
+ Not available in [pure evaluation mode](@docroot@/command-ref/conf-file.md#conf-pure-eval).
+
+ See also [`builtins.fetchClosure`](#builtins-fetchClosure).
)",
.fun = prim_storePath,
});
@@ -1657,7 +1658,10 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V
}));
}
- searchPath.emplace_back(prefix, path);
+ searchPath.elements.emplace_back(SearchPath::Elem {
+ .prefix = SearchPath::Prefix { .s = prefix },
+ .path = SearchPath::Path { .s = path },
+ });
}
auto path = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.findFile");
@@ -1665,9 +1669,52 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V
v.mkPath(state.checkSourcePath(state.findFile(searchPath, path, pos)));
}
-static RegisterPrimOp primop_findFile(RegisterPrimOp::Info {
+static RegisterPrimOp primop_findFile(PrimOp {
.name = "__findFile",
- .arity = 2,
+ .args = {"search path", "lookup path"},
+ .doc = R"(
+ Look up the given path with the given search path.
+
+ A search path is represented list of [attribute sets](./values.md#attribute-set) with two attributes, `prefix`, and `path`.
+ `prefix` is a relative path.
+ `path` denotes a file system location; the exact syntax depends on the command line interface.
+
+ Examples of search path attribute sets:
+
+ - ```
+ {
+ prefix = "nixos-config";
+ path = "/etc/nixos/configuration.nix";
+ }
+ ```
+
+ - ```
+ {
+ prefix = "";
+ path = "/nix/var/nix/profiles/per-user/root/channels";
+ }
+ ```
+
+ The lookup algorithm checks each entry until a match is found, returning a [path value](@docroot@/language/values.html#type-path) of the match.
+
+ This is the process for each entry:
+ If the lookup path matches `prefix`, then the remainder of the lookup path (the "suffix") is searched for within the directory denoted by `patch`.
+ Note that the `path` may need to be downloaded at this point to look inside.
+ If the suffix is found inside that directory, then the entry is a match;
+ the combined absolute path of the directory (now downloaded if need be) and the suffix is returned.
+
+ The syntax
+
+ ```nix
+ <nixpkgs>
+ ```
+
+ is equivalent to:
+
+ ```nix
+ builtins.findFile builtins.nixPath "nixpkgs"
+ ```
+ )",
.fun = prim_findFile,
});
@@ -2386,7 +2433,7 @@ static void prim_unsafeGetAttrPos(EvalState & state, const PosIdx pos, Value * *
state.mkPos(v, i->pos);
}
-static RegisterPrimOp primop_unsafeGetAttrPos(RegisterPrimOp::Info {
+static RegisterPrimOp primop_unsafeGetAttrPos(PrimOp {
.name = "__unsafeGetAttrPos",
.arity = 2,
.fun = prim_unsafeGetAttrPos,
@@ -3909,13 +3956,8 @@ static void prim_replaceStrings(EvalState & state, const PosIdx pos, Value * * a
for (auto elem : args[0]->listItems())
from.emplace_back(state.forceString(*elem, pos, "while evaluating one of the strings to replace passed to builtins.replaceStrings"));
- std::vector<std::pair<std::string, NixStringContext>> to;
- to.reserve(args[1]->listSize());
- for (auto elem : args[1]->listItems()) {
- NixStringContext ctx;
- auto s = state.forceString(*elem, ctx, pos, "while evaluating one of the replacement strings passed to builtins.replaceStrings");
- to.emplace_back(s, std::move(ctx));
- }
+ std::unordered_map<size_t, std::string> cache;
+ auto to = args[1]->listItems();
NixStringContext context;
auto s = state.forceString(*args[2], context, pos, "while evaluating the third argument passed to builtins.replaceStrings");
@@ -3926,10 +3968,19 @@ static void prim_replaceStrings(EvalState & state, const PosIdx pos, Value * * a
bool found = false;
auto i = from.begin();
auto j = to.begin();
- for (; i != from.end(); ++i, ++j)
+ size_t j_index = 0;
+ for (; i != from.end(); ++i, ++j, ++j_index)
if (s.compare(p, i->size(), *i) == 0) {
found = true;
- res += j->first;
+ auto v = cache.find(j_index);
+ if (v == cache.end()) {
+ NixStringContext ctx;
+ auto ts = state.forceString(**j, ctx, pos, "while evaluating one of the replacement strings passed to builtins.replaceStrings");
+ v = (cache.emplace(j_index, ts)).first;
+ for (auto& path : ctx)
+ context.insert(path);
+ }
+ res += v->second;
if (i->empty()) {
if (p < s.size())
res += s[p];
@@ -3937,9 +3988,6 @@ static void prim_replaceStrings(EvalState & state, const PosIdx pos, Value * * a
} else {
p += i->size();
}
- for (auto& path : j->second)
- context.insert(path);
- j->second.clear();
break;
}
if (!found) {
@@ -3957,7 +4005,11 @@ static RegisterPrimOp primop_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,
+ with the corresponding string in *to*.
+
+ The argument *to* is lazy, that is, it is only evaluated when its corresponding pattern in *from* is matched in the string *s*
+
+ Example:
```nix
builtins.replaceStrings ["oo" "a"] ["a" "i"] "foobar"
@@ -4054,22 +4106,10 @@ static RegisterPrimOp primop_splitVersion({
RegisterPrimOp::PrimOps * RegisterPrimOp::primOps;
-RegisterPrimOp::RegisterPrimOp(std::string name, size_t arity, PrimOpFun fun)
-{
- if (!primOps) primOps = new PrimOps;
- primOps->push_back({
- .name = name,
- .args = {},
- .arity = arity,
- .fun = fun,
- });
-}
-
-
-RegisterPrimOp::RegisterPrimOp(Info && info)
+RegisterPrimOp::RegisterPrimOp(PrimOp && primOp)
{
if (!primOps) primOps = new PrimOps;
- primOps->push_back(std::move(info));
+ primOps->push_back(std::move(primOp));
}
@@ -4082,85 +4122,253 @@ void EvalState::createBaseEnv()
/* `builtins' must be first! */
v.mkAttrs(buildBindings(128).finish());
- addConstant("builtins", v);
+ addConstant("builtins", v, {
+ .type = nAttrs,
+ .doc = R"(
+ Contains all the [built-in functions](@docroot@/language/builtins.md) and values.
+
+ Since built-in functions were added over time, [testing for attributes](./operators.md#has-attribute) in `builtins` can be used for graceful fallback on older Nix installations:
+
+ ```nix
+ # if hasContext is not available, we assume `s` has a context
+ if builtins ? hasContext then builtins.hasContext s else true
+ ```
+ )",
+ });
v.mkBool(true);
- addConstant("true", v);
+ addConstant("true", v, {
+ .type = nBool,
+ .doc = R"(
+ Primitive value.
+
+ It can be returned by
+ [comparison operators](@docroot@/language/operators.md#Comparison)
+ and used in
+ [conditional expressions](@docroot@/language/constructs.md#Conditionals).
+
+ The name `true` is not special, and can be shadowed:
+
+ ```nix-repl
+ nix-repl> let true = 1; in true
+ 1
+ ```
+ )",
+ });
v.mkBool(false);
- addConstant("false", v);
+ addConstant("false", v, {
+ .type = nBool,
+ .doc = R"(
+ Primitive value.
+
+ It can be returned by
+ [comparison operators](@docroot@/language/operators.md#Comparison)
+ and used in
+ [conditional expressions](@docroot@/language/constructs.md#Conditionals).
+
+ The name `false` is not special, and can be shadowed:
+
+ ```nix-repl
+ nix-repl> let false = 1; in false
+ 1
+ ```
+ )",
+ });
v.mkNull();
- addConstant("null", v);
+ addConstant("null", v, {
+ .type = nNull,
+ .doc = R"(
+ Primitive value.
+
+ The name `null` is not special, and can be shadowed:
+
+ ```nix-repl
+ nix-repl> let null = 1; in null
+ 1
+ ```
+ )",
+ });
if (!evalSettings.pureEval) {
v.mkInt(time(0));
- addConstant("__currentTime", v);
+ }
+ addConstant("__currentTime", v, {
+ .type = nInt,
+ .doc = R"(
+ Return the [Unix time](https://en.wikipedia.org/wiki/Unix_time) at first evaluation.
+ Repeated references to that name will re-use the initially obtained value.
+
+ Example:
+
+ ```console
+ $ nix repl
+ Welcome to Nix 2.15.1 Type :? for help.
+ nix-repl> builtins.currentTime
+ 1683705525
+
+ nix-repl> builtins.currentTime
+ 1683705525
+ ```
+
+ The [store path](@docroot@/glossary.md#gloss-store-path) of a derivation depending on `currentTime` will differ for each evaluation, unless both evaluate `builtins.currentTime` in the same second.
+ )",
+ .impureOnly = true,
+ });
+
+ if (!evalSettings.pureEval) {
v.mkString(settings.thisSystem.get());
- addConstant("__currentSystem", v);
}
+ addConstant("__currentSystem", v, {
+ .type = nString,
+ .doc = R"(
+ The value of the [`system` configuration option](@docroot@/command-ref/conf-file.md#conf-pure-eval).
+
+ It can be used to set the `system` attribute for [`builtins.derivation`](@docroot@/language/derivations.md) such that the resulting derivation can be built on the same system that evaluates the Nix expression:
+
+ ```nix
+ builtins.derivation {
+ # ...
+ system = builtins.currentSystem;
+ }
+ ```
+
+ It can be overridden in order to create derivations for different system than the current one:
+
+ ```console
+ $ nix-instantiate --system "mips64-linux" --eval --expr 'builtins.currentSystem'
+ "mips64-linux"
+ ```
+ )",
+ .impureOnly = true,
+ });
v.mkString(nixVersion);
- addConstant("__nixVersion", v);
+ addConstant("__nixVersion", v, {
+ .type = nString,
+ .doc = R"(
+ The version of Nix.
+
+ For example, where the command line returns the current Nix version,
+
+ ```shell-session
+ $ nix --version
+ nix (Nix) 2.16.0
+ ```
+
+ the Nix language evaluator returns the same value:
+
+ ```nix-repl
+ nix-repl> builtins.nixVersion
+ "2.16.0"
+ ```
+ )",
+ });
v.mkString(store->storeDir);
- addConstant("__storeDir", v);
+ addConstant("__storeDir", v, {
+ .type = nString,
+ .doc = R"(
+ Logical file system location of the [Nix store](@docroot@/glossary.md#gloss-store) currently in use.
+
+ This value is determined by the `store` parameter in [Store URLs](@docroot@/command-ref/new-cli/nix3-help-stores.md):
+
+ ```shell-session
+ $ nix-instantiate --store 'dummy://?store=/blah' --eval --expr builtins.storeDir
+ "/blah"
+ ```
+ )",
+ });
/* Language version. This should be increased every time a new
language feature gets added. It's not necessary to increase it
when primops get added, because you can just use `builtins ?
primOp' to check. */
v.mkInt(6);
- addConstant("__langVersion", v);
+ addConstant("__langVersion", v, {
+ .type = nInt,
+ .doc = R"(
+ The current version of the Nix language.
+ )",
+ });
// Miscellaneous
if (evalSettings.enableNativeCode) {
- addPrimOp("__importNative", 2, prim_importNative);
- addPrimOp("__exec", 1, prim_exec);
+ addPrimOp({
+ .name = "__importNative",
+ .arity = 2,
+ .fun = prim_importNative,
+ });
+ addPrimOp({
+ .name = "__exec",
+ .arity = 1,
+ .fun = prim_exec,
+ });
}
addPrimOp({
- .fun = evalSettings.traceVerbose ? prim_trace : prim_second,
- .arity = 2,
.name = "__traceVerbose",
.args = { "e1", "e2" },
+ .arity = 2,
.doc = R"(
Evaluate *e1* and print its abstract syntax representation on standard
error if `--trace-verbose` is enabled. Then return *e2*. This function
is useful for debugging.
)",
+ .fun = evalSettings.traceVerbose ? prim_trace : prim_second,
});
/* Add a value containing the current Nix expression search path. */
- mkList(v, searchPath.size());
+ mkList(v, searchPath.elements.size());
int n = 0;
- for (auto & i : searchPath) {
+ for (auto & i : searchPath.elements) {
auto attrs = buildBindings(2);
- attrs.alloc("path").mkString(i.second);
- attrs.alloc("prefix").mkString(i.first);
+ attrs.alloc("path").mkString(i.path.s);
+ attrs.alloc("prefix").mkString(i.prefix.s);
(v.listElems()[n++] = allocValue())->mkAttrs(attrs);
}
- addConstant("__nixPath", v);
+ addConstant("__nixPath", v, {
+ .type = nList,
+ .doc = R"(
+ The search path used to resolve angle bracket path lookups.
+
+ Angle bracket expressions can be
+ [desugared](https://en.wikipedia.org/wiki/Syntactic_sugar)
+ using this and
+ [`builtins.findFile`](./builtins.html#builtins-findFile):
+
+ ```nix
+ <nixpkgs>
+ ```
+
+ is equivalent to:
+
+ ```nix
+ builtins.findFile builtins.nixPath "nixpkgs"
+ ```
+ )",
+ });
if (RegisterPrimOp::primOps)
for (auto & primOp : *RegisterPrimOp::primOps)
- if (!primOp.experimentalFeature
- || experimentalFeatureSettings.isEnabled(*primOp.experimentalFeature))
+ if (experimentalFeatureSettings.isEnabled(primOp.experimentalFeature))
{
- addPrimOp({
- .fun = primOp.fun,
- .arity = std::max(primOp.args.size(), primOp.arity),
- .name = primOp.name,
- .args = primOp.args,
- .doc = primOp.doc,
- });
+ auto primOpAdjusted = primOp;
+ primOpAdjusted.arity = std::max(primOp.args.size(), primOp.arity);
+ addPrimOp(std::move(primOpAdjusted));
}
/* Add a wrapper around the derivation primop that computes the
- `drvPath' and `outPath' attributes lazily. */
+ `drvPath' and `outPath' attributes lazily.
+
+ Null docs because it is documented separately.
+ */
auto vDerivation = allocValue();
- addConstant("derivation", vDerivation);
+ addConstant("derivation", vDerivation, {
+ .type = nFunction,
+ });
/* 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 4ae73fe1f..930e7f32a 100644
--- a/src/libexpr/primops.hh
+++ b/src/libexpr/primops.hh
@@ -10,17 +10,7 @@ namespace nix {
struct RegisterPrimOp
{
- struct Info
- {
- std::string name;
- std::vector<std::string> args;
- size_t arity = 0;
- const char * doc;
- PrimOpFun fun;
- std::optional<ExperimentalFeature> experimentalFeature;
- };
-
- typedef std::vector<Info> PrimOps;
+ typedef std::vector<PrimOp> PrimOps;
static PrimOps * primOps;
/**
@@ -28,12 +18,7 @@ struct RegisterPrimOp
* will get called during EvalState initialization, so there
* may be primops not yet added and builtins is not yet sorted.
*/
- RegisterPrimOp(
- std::string name,
- size_t arity,
- PrimOpFun fun);
-
- RegisterPrimOp(Info && info);
+ RegisterPrimOp(PrimOp && primOp);
};
/* These primops are disabled without enableNativeCode, but plugins
diff --git a/src/libexpr/primops/context.cc b/src/libexpr/primops/context.cc
index 07bf400cf..8b3468009 100644
--- a/src/libexpr/primops/context.cc
+++ b/src/libexpr/primops/context.cc
@@ -12,7 +12,11 @@ static void prim_unsafeDiscardStringContext(EvalState & state, const PosIdx pos,
v.mkString(*s);
}
-static RegisterPrimOp primop_unsafeDiscardStringContext("__unsafeDiscardStringContext", 1, prim_unsafeDiscardStringContext);
+static RegisterPrimOp primop_unsafeDiscardStringContext({
+ .name = "__unsafeDiscardStringContext",
+ .arity = 1,
+ .fun = prim_unsafeDiscardStringContext
+});
static void prim_hasContext(EvalState & state, const PosIdx pos, Value * * args, Value & v)
@@ -22,7 +26,16 @@ static void prim_hasContext(EvalState & state, const PosIdx pos, Value * * args,
v.mkBool(!context.empty());
}
-static RegisterPrimOp primop_hasContext("__hasContext", 1, prim_hasContext);
+static RegisterPrimOp primop_hasContext({
+ .name = "__hasContext",
+ .args = {"s"},
+ .doc = R"(
+ Return `true` if string *s* has a non-empty context. The
+ context can be obtained with
+ [`getContext`](#builtins-getContext).
+ )",
+ .fun = prim_hasContext
+});
/* Sometimes we want to pass a derivation path (i.e. pkg.drvPath) to a
@@ -51,7 +64,11 @@ static void prim_unsafeDiscardOutputDependency(EvalState & state, const PosIdx p
v.mkString(*s, context2);
}
-static RegisterPrimOp primop_unsafeDiscardOutputDependency("__unsafeDiscardOutputDependency", 1, prim_unsafeDiscardOutputDependency);
+static RegisterPrimOp primop_unsafeDiscardOutputDependency({
+ .name = "__unsafeDiscardOutputDependency",
+ .arity = 1,
+ .fun = prim_unsafeDiscardOutputDependency
+});
/* Extract the context of a string as a structured Nix value.
@@ -119,7 +136,30 @@ static void prim_getContext(EvalState & state, const PosIdx pos, Value * * args,
v.mkAttrs(attrs);
}
-static RegisterPrimOp primop_getContext("__getContext", 1, prim_getContext);
+static RegisterPrimOp primop_getContext({
+ .name = "__getContext",
+ .args = {"s"},
+ .doc = R"(
+ Return the string context of *s*.
+
+ The string context tracks references to derivations within a string.
+ It is represented as an attribute set of [store derivation](@docroot@/glossary.md#gloss-store-derivation) paths mapping to output names.
+
+ Using [string interpolation](@docroot@/language/string-interpolation.md) on a derivation will add that derivation to the string context.
+ For example,
+
+ ```nix
+ builtins.getContext "${derivation { name = "a"; builder = "b"; system = "c"; }}"
+ ```
+
+ evaluates to
+
+ ```
+ { "/nix/store/arhvjaf6zmlyn8vh8fgn55rpwnxq0n7l-a.drv" = { outputs = [ "out" ]; }; }
+ ```
+ )",
+ .fun = prim_getContext
+});
/* Append the given context to a given string.
@@ -192,6 +232,10 @@ static void prim_appendContext(EvalState & state, const PosIdx pos, Value * * ar
v.mkString(orig, context);
}
-static RegisterPrimOp primop_appendContext("__appendContext", 2, prim_appendContext);
+static RegisterPrimOp primop_appendContext({
+ .name = "__appendContext",
+ .arity = 2,
+ .fun = prim_appendContext
+});
}
diff --git a/src/libexpr/primops/fetchClosure.cc b/src/libexpr/primops/fetchClosure.cc
index 4cf1f1e0b..7fe8203f4 100644
--- a/src/libexpr/primops/fetchClosure.cc
+++ b/src/libexpr/primops/fetchClosure.cc
@@ -5,37 +5,150 @@
namespace nix {
+/**
+ * Handler for the content addressed case.
+ *
+ * @param state Evaluator state and store to write to.
+ * @param fromStore Store containing the path to rewrite.
+ * @param fromPath Source path to be rewritten.
+ * @param toPathMaybe Path to write the rewritten path to. If empty, the error shows the actual path.
+ * @param v Return `Value`
+ */
+static void runFetchClosureWithRewrite(EvalState & state, const PosIdx pos, Store & fromStore, const StorePath & fromPath, const std::optional<StorePath> & toPathMaybe, Value &v) {
+
+ // establish toPath or throw
+
+ if (!toPathMaybe || !state.store->isValidPath(*toPathMaybe)) {
+ auto rewrittenPath = makeContentAddressed(fromStore, *state.store, fromPath);
+ if (toPathMaybe && *toPathMaybe != rewrittenPath)
+ throw Error({
+ .msg = hintfmt("rewriting '%s' to content-addressed form yielded '%s', while '%s' was expected",
+ state.store->printStorePath(fromPath),
+ state.store->printStorePath(rewrittenPath),
+ state.store->printStorePath(*toPathMaybe)),
+ .errPos = state.positions[pos]
+ });
+ if (!toPathMaybe)
+ throw Error({
+ .msg = hintfmt(
+ "rewriting '%s' to content-addressed form yielded '%s'\n"
+ "Use this value for the 'toPath' attribute passed to 'fetchClosure'",
+ state.store->printStorePath(fromPath),
+ state.store->printStorePath(rewrittenPath)),
+ .errPos = state.positions[pos]
+ });
+ }
+
+ auto toPath = *toPathMaybe;
+
+ // check and return
+
+ auto resultInfo = state.store->queryPathInfo(toPath);
+
+ if (!resultInfo->isContentAddressed(*state.store)) {
+ // We don't perform the rewriting when outPath already exists, as an optimisation.
+ // However, we can quickly detect a mistake if the toPath is input addressed.
+ throw Error({
+ .msg = hintfmt(
+ "The 'toPath' value '%s' is input-addressed, so it can't possibly be the result of rewriting to a content-addressed path.\n\n"
+ "Set 'toPath' to an empty string to make Nix report the correct content-addressed path.",
+ state.store->printStorePath(toPath)),
+ .errPos = state.positions[pos]
+ });
+ }
+
+ state.mkStorePathString(toPath, v);
+}
+
+/**
+ * Fetch the closure and make sure it's content addressed.
+ */
+static void runFetchClosureWithContentAddressedPath(EvalState & state, const PosIdx pos, Store & fromStore, const StorePath & fromPath, Value & v) {
+
+ if (!state.store->isValidPath(fromPath))
+ copyClosure(fromStore, *state.store, RealisedPath::Set { fromPath });
+
+ auto info = state.store->queryPathInfo(fromPath);
+
+ if (!info->isContentAddressed(*state.store)) {
+ throw Error({
+ .msg = hintfmt(
+ "The 'fromPath' value '%s' is input-addressed, but 'inputAddressed' is set to 'false' (default).\n\n"
+ "If you do intend to fetch an input-addressed store path, add\n\n"
+ " inputAddressed = true;\n\n"
+ "to the 'fetchClosure' arguments.\n\n"
+ "Note that to ensure authenticity input-addressed store paths, users must configure a trusted binary cache public key on their systems. This is not needed for content-addressed paths.",
+ state.store->printStorePath(fromPath)),
+ .errPos = state.positions[pos]
+ });
+ }
+
+ state.mkStorePathString(fromPath, v);
+}
+
+/**
+ * Fetch the closure and make sure it's input addressed.
+ */
+static void runFetchClosureWithInputAddressedPath(EvalState & state, const PosIdx pos, Store & fromStore, const StorePath & fromPath, Value & v) {
+
+ if (!state.store->isValidPath(fromPath))
+ copyClosure(fromStore, *state.store, RealisedPath::Set { fromPath });
+
+ auto info = state.store->queryPathInfo(fromPath);
+
+ if (info->isContentAddressed(*state.store)) {
+ throw Error({
+ .msg = hintfmt(
+ "The store object referred to by 'fromPath' at '%s' is not input-addressed, but 'inputAddressed' is set to 'true'.\n\n"
+ "Remove the 'inputAddressed' attribute (it defaults to 'false') to expect 'fromPath' to be content-addressed",
+ state.store->printStorePath(fromPath)),
+ .errPos = state.positions[pos]
+ });
+ }
+
+ state.mkStorePathString(fromPath, v);
+}
+
+typedef std::optional<StorePath> StorePathOrGap;
+
static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.fetchClosure");
std::optional<std::string> fromStoreUrl;
std::optional<StorePath> fromPath;
- bool toCA = false;
- std::optional<StorePath> toPath;
+ std::optional<StorePathOrGap> toPath;
+ std::optional<bool> inputAddressedMaybe;
for (auto & attr : *args[0]->attrs) {
const auto & attrName = state.symbols[attr.name];
+ auto attrHint = [&]() -> std::string {
+ return "while evaluating the '" + attrName + "' attribute passed to builtins.fetchClosure";
+ };
if (attrName == "fromPath") {
NixStringContext context;
- fromPath = state.coerceToStorePath(attr.pos, *attr.value, context,
- "while evaluating the 'fromPath' attribute passed to builtins.fetchClosure");
+ fromPath = state.coerceToStorePath(attr.pos, *attr.value, context, attrHint());
}
else if (attrName == "toPath") {
state.forceValue(*attr.value, attr.pos);
- toCA = true;
- if (attr.value->type() != nString || attr.value->string.s != std::string("")) {
+ bool isEmptyString = attr.value->type() == nString && attr.value->string.s == std::string("");
+ if (isEmptyString) {
+ toPath = StorePathOrGap {};
+ }
+ else {
NixStringContext context;
- toPath = state.coerceToStorePath(attr.pos, *attr.value, context,
- "while evaluating the 'toPath' attribute passed to builtins.fetchClosure");
+ toPath = state.coerceToStorePath(attr.pos, *attr.value, context, attrHint());
}
}
else if (attrName == "fromStore")
fromStoreUrl = state.forceStringNoCtx(*attr.value, attr.pos,
- "while evaluating the 'fromStore' attribute passed to builtins.fetchClosure");
+ attrHint());
+
+ else if (attrName == "inputAddressed")
+ inputAddressedMaybe = state.forceBool(*attr.value, attr.pos, attrHint());
else
throw Error({
@@ -50,6 +163,18 @@ static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * arg
.errPos = state.positions[pos]
});
+ bool inputAddressed = inputAddressedMaybe.value_or(false);
+
+ if (inputAddressed) {
+ if (toPath)
+ throw Error({
+ .msg = hintfmt("attribute '%s' is set to true, but '%s' is also set. Please remove one of them",
+ "inputAddressed",
+ "toPath"),
+ .errPos = state.positions[pos]
+ });
+ }
+
if (!fromStoreUrl)
throw Error({
.msg = hintfmt("attribute '%s' is missing in call to 'fetchClosure'", "fromStore"),
@@ -74,55 +199,40 @@ static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * arg
auto fromStore = openStore(parsedURL.to_string());
- if (toCA) {
- if (!toPath || !state.store->isValidPath(*toPath)) {
- auto remappings = makeContentAddressed(*fromStore, *state.store, { *fromPath });
- auto i = remappings.find(*fromPath);
- assert(i != remappings.end());
- if (toPath && *toPath != i->second)
- throw Error({
- .msg = hintfmt("rewriting '%s' to content-addressed form yielded '%s', while '%s' was expected",
- state.store->printStorePath(*fromPath),
- state.store->printStorePath(i->second),
- state.store->printStorePath(*toPath)),
- .errPos = state.positions[pos]
- });
- if (!toPath)
- throw Error({
- .msg = hintfmt(
- "rewriting '%s' to content-addressed form yielded '%s'; "
- "please set this in the 'toPath' attribute passed to 'fetchClosure'",
- state.store->printStorePath(*fromPath),
- state.store->printStorePath(i->second)),
- .errPos = state.positions[pos]
- });
- }
- } else {
- if (!state.store->isValidPath(*fromPath))
- copyClosure(*fromStore, *state.store, RealisedPath::Set { *fromPath });
- toPath = fromPath;
- }
-
- /* In pure mode, require a CA path. */
- if (evalSettings.pureEval) {
- auto info = state.store->queryPathInfo(*toPath);
- if (!info->isContentAddressed(*state.store))
- throw Error({
- .msg = hintfmt("in pure mode, 'fetchClosure' requires a content-addressed path, which '%s' isn't",
- state.store->printStorePath(*toPath)),
- .errPos = state.positions[pos]
- });
- }
-
- state.mkStorePathString(*toPath, v);
+ if (toPath)
+ runFetchClosureWithRewrite(state, pos, *fromStore, *fromPath, *toPath, v);
+ else if (inputAddressed)
+ runFetchClosureWithInputAddressedPath(state, pos, *fromStore, *fromPath, v);
+ else
+ runFetchClosureWithContentAddressedPath(state, pos, *fromStore, *fromPath, v);
}
static RegisterPrimOp primop_fetchClosure({
.name = "__fetchClosure",
.args = {"args"},
.doc = R"(
- Fetch a Nix store closure from a binary cache, rewriting it into
- content-addressed form. For example,
+ Fetch a store path [closure](@docroot@/glossary.md#gloss-closure) from a binary cache, and return the store path as a string with context.
+
+ This function can be invoked in three ways, that we will discuss in order of preference.
+
+ **Fetch a content-addressed store path**
+
+ Example:
+
+ ```nix
+ builtins.fetchClosure {
+ fromStore = "https://cache.nixos.org";
+ fromPath = /nix/store/ldbhlwhh39wha58rm61bkiiwm6j7211j-git-2.33.1;
+ }
+ ```
+
+ This is the simplest invocation, and it does not require the user of the expression to configure [`trusted-public-keys`](@docroot@/command-ref/conf-file.md#conf-trusted-public-keys) to ensure their authenticity.
+
+ If your store path is [input addressed](@docroot@/glossary.md#gloss-input-addressed-store-object) instead of content addressed, consider the other two invocations.
+
+ **Fetch any store path and rewrite it to a fully content-addressed store path**
+
+ Example:
```nix
builtins.fetchClosure {
@@ -132,31 +242,42 @@ static RegisterPrimOp primop_fetchClosure({
}
```
- fetches `/nix/store/r2jd...` from the specified binary cache,
+ This example fetches `/nix/store/r2jd...` from the specified binary cache,
and rewrites it into the content-addressed store path
`/nix/store/ldbh...`.
- If `fromPath` is already content-addressed, or if you are
- allowing impure evaluation (`--impure`), then `toPath` may be
- omitted.
+ Like the previous example, no extra configuration or privileges are required.
To find out the correct value for `toPath` given a `fromPath`,
- you can use `nix store make-content-addressed`:
+ use [`nix store make-content-addressed`](@docroot@/command-ref/new-cli/nix3-store-make-content-addressed.md):
```console
# nix store make-content-addressed --from https://cache.nixos.org /nix/store/r2jd6ygnmirm2g803mksqqjm4y39yi6i-git-2.33.1
rewrote '/nix/store/r2jd6ygnmirm2g803mksqqjm4y39yi6i-git-2.33.1' to '/nix/store/ldbhlwhh39wha58rm61bkiiwm6j7211j-git-2.33.1'
```
- This function is similar to `builtins.storePath` in that it
- allows you to use a previously built store path in a Nix
- expression. However, it is more reproducible because it requires
- specifying a binary cache from which the path can be fetched.
- Also, requiring a content-addressed final store path avoids the
- need for users to configure binary cache public keys.
+ Alternatively, set `toPath = ""` and find the correct `toPath` in the error message.
+
+ **Fetch an input-addressed store path as is**
+
+ Example:
+
+ ```nix
+ builtins.fetchClosure {
+ fromStore = "https://cache.nixos.org";
+ fromPath = /nix/store/r2jd6ygnmirm2g803mksqqjm4y39yi6i-git-2.33.1;
+ inputAddressed = true;
+ }
+ ```
+
+ It is possible to fetch an [input-addressed store path](@docroot@/glossary.md#gloss-input-addressed-store-object) and return it as is.
+ However, this is the least preferred way of invoking `fetchClosure`, because it requires that the input-addressed paths are trusted by the Nix configuration.
+
+ **`builtins.storePath`**
- This function is only available if you enable the experimental
- feature `fetch-closure`.
+ `fetchClosure` is similar to [`builtins.storePath`](#builtins-storePath) in that it allows you to use a previously built store path in a Nix expression.
+ However, `fetchClosure` is more reproducible because it specifies a binary cache from which the path can be fetched.
+ Also, using content-addressed store paths does not require users to configure [`trusted-public-keys`](@docroot@/command-ref/conf-file.md#conf-trusted-public-keys) to ensure their authenticity.
)",
.fun = prim_fetchClosure,
.experimentalFeature = Xp::FetchClosure,
diff --git a/src/libexpr/primops/fetchMercurial.cc b/src/libexpr/primops/fetchMercurial.cc
index 2c0d98e74..322692b52 100644
--- a/src/libexpr/primops/fetchMercurial.cc
+++ b/src/libexpr/primops/fetchMercurial.cc
@@ -88,6 +88,10 @@ static void prim_fetchMercurial(EvalState & state, const PosIdx pos, Value * * a
state.allowPath(tree.storePath);
}
-static RegisterPrimOp r_fetchMercurial("fetchMercurial", 1, prim_fetchMercurial);
+static RegisterPrimOp r_fetchMercurial({
+ .name = "fetchMercurial",
+ .arity = 1,
+ .fun = prim_fetchMercurial
+});
}
diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc
index cd7039025..579a45f92 100644
--- a/src/libexpr/primops/fetchTree.cc
+++ b/src/libexpr/primops/fetchTree.cc
@@ -22,7 +22,7 @@ void emitTreeAttrs(
{
assert(input.isLocked());
- auto attrs = state.buildBindings(8);
+ auto attrs = state.buildBindings(10);
state.mkStorePathString(tree.storePath, attrs.alloc(state.sOutPath));
@@ -56,6 +56,11 @@ void emitTreeAttrs(
}
+ if (auto dirtyRev = fetchers::maybeGetStrAttr(input.attrs, "dirtyRev")) {
+ attrs.alloc("dirtyRev").mkString(*dirtyRev);
+ attrs.alloc("dirtyShortRev").mkString(*fetchers::maybeGetStrAttr(input.attrs, "dirtyShortRev"));
+ }
+
if (auto lastModified = input.getLastModified()) {
attrs.alloc("lastModified").mkInt(*lastModified);
attrs.alloc("lastModifiedDate").mkString(
@@ -194,7 +199,11 @@ static void prim_fetchTree(EvalState & state, const PosIdx pos, Value * * args,
}
// FIXME: document
-static RegisterPrimOp primop_fetchTree("fetchTree", 1, prim_fetchTree);
+static RegisterPrimOp primop_fetchTree({
+ .name = "fetchTree",
+ .arity = 1,
+ .fun = prim_fetchTree
+});
static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v,
const std::string & who, bool unpack, std::string name)
@@ -262,7 +271,7 @@ static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v
// https://github.com/NixOS/nix/issues/4313
auto storePath =
unpack
- ? fetchers::downloadTarball(state.store, *url, name, (bool) expectedHash).first.storePath
+ ? fetchers::downloadTarball(state.store, *url, name, (bool) expectedHash).tree.storePath
: fetchers::downloadFile(state.store, *url, name, (bool) expectedHash).storePath;
if (expectedHash) {
@@ -286,9 +295,9 @@ 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.
+ Download the specified URL and return the path of the downloaded file.
+
+ Not available in [restricted evaluation mode](@docroot@/command-ref/conf-file.md#conf-restrict-eval).
)",
.fun = prim_fetchurl,
});
@@ -338,8 +347,7 @@ static RegisterPrimOp primop_fetchTarball({
stdenv.mkDerivation { … }
```
- This function is not available if [restricted evaluation
- mode](../command-ref/conf-file.md) is enabled.
+ Not available in [restricted evaluation mode](@docroot@/command-ref/conf-file.md#conf-restrict-eval).
)",
.fun = prim_fetchTarball,
});
@@ -470,14 +478,9 @@ static RegisterPrimOp primop_fetchGit({
}
```
- > **Note**
- >
- > Nix will refetch the branch in accordance with
- > the option `tarball-ttl`.
+ Nix will refetch the branch according to the [`tarball-ttl`](@docroot@/command-ref/conf-file.md#conf-tarball-ttl) setting.
- > **Note**
- >
- > This behavior is disabled in *Pure evaluation mode*.
+ This behavior is disabled in [pure evaluation mode](@docroot@/command-ref/conf-file.md#conf-pure-eval).
- To fetch the content of a checked-out work directory:
diff --git a/src/libexpr/primops/fromTOML.cc b/src/libexpr/primops/fromTOML.cc
index 8a5231781..2f4d4022e 100644
--- a/src/libexpr/primops/fromTOML.cc
+++ b/src/libexpr/primops/fromTOML.cc
@@ -3,6 +3,8 @@
#include "../../toml11/toml.hpp"
+#include <sstream>
+
namespace nix {
static void prim_fromTOML(EvalState & state, const PosIdx pos, Value * * args, Value & val)
@@ -58,8 +60,18 @@ static void prim_fromTOML(EvalState & state, const PosIdx pos, Value * * args, V
case toml::value_t::offset_datetime:
case toml::value_t::local_date:
case toml::value_t::local_time:
- // We fail since Nix doesn't have date and time types
- throw std::runtime_error("Dates and times are not supported");
+ {
+ if (experimentalFeatureSettings.isEnabled(Xp::ParseTomlTimestamps)) {
+ auto attrs = state.buildBindings(2);
+ attrs.alloc("_type").mkString("timestamp");
+ std::ostringstream s;
+ s << t;
+ attrs.alloc("value").mkString(s.str());
+ v.mkAttrs(attrs);
+ } else {
+ throw std::runtime_error("Dates and times are not supported");
+ }
+ }
break;;
case toml::value_t::empty:
v.mkNull();
@@ -78,6 +90,24 @@ static void prim_fromTOML(EvalState & state, const PosIdx pos, Value * * args, V
}
}
-static RegisterPrimOp primop_fromTOML("fromTOML", 1, prim_fromTOML);
+static RegisterPrimOp primop_fromTOML({
+ .name = "fromTOML",
+ .args = {"e"},
+ .doc = R"(
+ Convert a TOML string to a Nix value. For example,
+
+ ```nix
+ builtins.fromTOML ''
+ x=1
+ s="a"
+ [table]
+ y=2
+ ''
+ ```
+
+ returns the value `{ s = "a"; table = { y = 2; }; x = 1; }`.
+ )",
+ .fun = prim_fromTOML
+});
}
diff --git a/src/libexpr/search-path.cc b/src/libexpr/search-path.cc
new file mode 100644
index 000000000..36bb4c3a5
--- /dev/null
+++ b/src/libexpr/search-path.cc
@@ -0,0 +1,56 @@
+#include "search-path.hh"
+#include "util.hh"
+
+namespace nix {
+
+std::optional<std::string_view> SearchPath::Prefix::suffixIfPotentialMatch(
+ std::string_view path) const
+{
+ auto n = s.size();
+
+ /* Non-empty prefix and suffix must be separated by a /, or the
+ prefix is not a valid path prefix. */
+ bool needSeparator = n > 0 && (path.size() - n) > 0;
+
+ if (needSeparator && path[n] != '/') {
+ return std::nullopt;
+ }
+
+ /* Prefix must be prefix of this path. */
+ if (path.compare(0, n, s) != 0) {
+ return std::nullopt;
+ }
+
+ /* Skip next path separator. */
+ return {
+ path.substr(needSeparator ? n + 1 : n)
+ };
+}
+
+
+SearchPath::Elem SearchPath::Elem::parse(std::string_view rawElem)
+{
+ size_t pos = rawElem.find('=');
+
+ return SearchPath::Elem {
+ .prefix = Prefix {
+ .s = pos == std::string::npos
+ ? std::string { "" }
+ : std::string { rawElem.substr(0, pos) },
+ },
+ .path = Path {
+ .s = std::string { rawElem.substr(pos + 1) },
+ },
+ };
+}
+
+
+SearchPath parseSearchPath(const Strings & rawElems)
+{
+ SearchPath res;
+ for (auto & rawElem : rawElems)
+ res.elements.emplace_back(SearchPath::Elem::parse(rawElem));
+ return res;
+}
+
+}
diff --git a/src/libexpr/search-path.hh b/src/libexpr/search-path.hh
new file mode 100644
index 000000000..ce78135b5
--- /dev/null
+++ b/src/libexpr/search-path.hh
@@ -0,0 +1,108 @@
+#pragma once
+///@file
+
+#include <optional>
+
+#include "types.hh"
+#include "comparator.hh"
+
+namespace nix {
+
+/**
+ * A "search path" is a list of ways look for something, used with
+ * `builtins.findFile` and `< >` lookup expressions.
+ */
+struct SearchPath
+{
+ /**
+ * A single element of a `SearchPath`.
+ *
+ * Each element is tried in succession when looking up a path. The first
+ * element to completely match wins.
+ */
+ struct Elem;
+
+ /**
+ * The first part of a `SearchPath::Elem` pair.
+ *
+ * Called a "prefix" because it takes the form of a prefix of a file
+ * path (first `n` path components). When looking up a path, to use
+ * a `SearchPath::Elem`, its `Prefix` must match the path.
+ */
+ struct Prefix;
+
+ /**
+ * The second part of a `SearchPath::Elem` pair.
+ *
+ * It is either a path or a URL (with certain restrictions / extra
+ * structure).
+ *
+ * If the prefix of the path we are looking up matches, we then
+ * check if the rest of the path points to something that exists
+ * within the directory denoted by this. If so, the
+ * `SearchPath::Elem` as a whole matches, and that *something* being
+ * pointed to by the rest of the path we are looking up is the
+ * result.
+ */
+ struct Path;
+
+ /**
+ * The list of search path elements. Each one is checked for a path
+ * when looking up. (The actual lookup entry point is in `EvalState`
+ * not in this class.)
+ */
+ std::list<SearchPath::Elem> elements;
+
+ /**
+ * Parse a string into a `SearchPath`
+ */
+ static SearchPath parse(const Strings & rawElems);
+};
+
+struct SearchPath::Prefix
+{
+ /**
+ * Underlying string
+ *
+ * @todo Should we normalize this when constructing a `SearchPath::Prefix`?
+ */
+ std::string s;
+
+ GENERATE_CMP(SearchPath::Prefix, me->s);
+
+ /**
+ * If the path possibly matches this search path element, return the
+ * suffix that we should look for inside the resolved value of the
+ * element
+ * Note the double optionality in the name. While we might have a matching prefix, the suffix may not exist.
+ */
+ std::optional<std::string_view> suffixIfPotentialMatch(std::string_view path) const;
+};
+
+struct SearchPath::Path
+{
+ /**
+ * The location of a search path item, as a path or URL.
+ *
+ * @todo Maybe change this to `std::variant<SourcePath, URL>`.
+ */
+ std::string s;
+
+ GENERATE_CMP(SearchPath::Path, me->s);
+};
+
+struct SearchPath::Elem
+{
+
+ Prefix prefix;
+ Path path;
+
+ GENERATE_CMP(SearchPath::Elem, me->prefix, me->path);
+
+ /**
+ * Parse a string into a `SearchPath::Elem`
+ */
+ static SearchPath::Elem parse(std::string_view rawElem);
+};
+
+}
diff --git a/src/libexpr/tests/error_traces.cc b/src/libexpr/tests/error_traces.cc
index 24e95ac39..285651256 100644
--- a/src/libexpr/tests/error_traces.cc
+++ b/src/libexpr/tests/error_traces.cc
@@ -171,7 +171,7 @@ namespace nix {
hintfmt("value is %s while a string was expected", "an integer"),
hintfmt("while evaluating one of the strings to replace passed to builtins.replaceStrings"));
- ASSERT_TRACE2("replaceStrings [ \"old\" ] [ true ] {}",
+ ASSERT_TRACE2("replaceStrings [ \"oo\" ] [ true ] \"foo\"",
TypeError,
hintfmt("value is %s while a string was expected", "a Boolean"),
hintfmt("while evaluating one of the replacement strings passed to builtins.replaceStrings"));
diff --git a/src/libexpr/tests/search-path.cc b/src/libexpr/tests/search-path.cc
new file mode 100644
index 000000000..dbe7ab95f
--- /dev/null
+++ b/src/libexpr/tests/search-path.cc
@@ -0,0 +1,90 @@
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+#include "search-path.hh"
+
+namespace nix {
+
+TEST(SearchPathElem, parse_justPath) {
+ ASSERT_EQ(
+ SearchPath::Elem::parse("foo"),
+ (SearchPath::Elem {
+ .prefix = SearchPath::Prefix { .s = "" },
+ .path = SearchPath::Path { .s = "foo" },
+ }));
+}
+
+TEST(SearchPathElem, parse_emptyPrefix) {
+ ASSERT_EQ(
+ SearchPath::Elem::parse("=foo"),
+ (SearchPath::Elem {
+ .prefix = SearchPath::Prefix { .s = "" },
+ .path = SearchPath::Path { .s = "foo" },
+ }));
+}
+
+TEST(SearchPathElem, parse_oneEq) {
+ ASSERT_EQ(
+ SearchPath::Elem::parse("foo=bar"),
+ (SearchPath::Elem {
+ .prefix = SearchPath::Prefix { .s = "foo" },
+ .path = SearchPath::Path { .s = "bar" },
+ }));
+}
+
+TEST(SearchPathElem, parse_twoEqs) {
+ ASSERT_EQ(
+ SearchPath::Elem::parse("foo=bar=baz"),
+ (SearchPath::Elem {
+ .prefix = SearchPath::Prefix { .s = "foo" },
+ .path = SearchPath::Path { .s = "bar=baz" },
+ }));
+}
+
+
+TEST(SearchPathElem, suffixIfPotentialMatch_justPath) {
+ SearchPath::Prefix prefix { .s = "" };
+ ASSERT_EQ(prefix.suffixIfPotentialMatch("any/thing"), std::optional { "any/thing" });
+}
+
+TEST(SearchPathElem, suffixIfPotentialMatch_misleadingPrefix1) {
+ SearchPath::Prefix prefix { .s = "foo" };
+ ASSERT_EQ(prefix.suffixIfPotentialMatch("fooX"), std::nullopt);
+}
+
+TEST(SearchPathElem, suffixIfPotentialMatch_misleadingPrefix2) {
+ SearchPath::Prefix prefix { .s = "foo" };
+ ASSERT_EQ(prefix.suffixIfPotentialMatch("fooX/bar"), std::nullopt);
+}
+
+TEST(SearchPathElem, suffixIfPotentialMatch_partialPrefix) {
+ SearchPath::Prefix prefix { .s = "fooX" };
+ ASSERT_EQ(prefix.suffixIfPotentialMatch("foo"), std::nullopt);
+}
+
+TEST(SearchPathElem, suffixIfPotentialMatch_exactPrefix) {
+ SearchPath::Prefix prefix { .s = "foo" };
+ ASSERT_EQ(prefix.suffixIfPotentialMatch("foo"), std::optional { "" });
+}
+
+TEST(SearchPathElem, suffixIfPotentialMatch_multiKey) {
+ SearchPath::Prefix prefix { .s = "foo/bar" };
+ ASSERT_EQ(prefix.suffixIfPotentialMatch("foo/bar/baz"), std::optional { "baz" });
+}
+
+TEST(SearchPathElem, suffixIfPotentialMatch_trailingSlash) {
+ SearchPath::Prefix prefix { .s = "foo" };
+ ASSERT_EQ(prefix.suffixIfPotentialMatch("foo/"), std::optional { "" });
+}
+
+TEST(SearchPathElem, suffixIfPotentialMatch_trailingDoubleSlash) {
+ SearchPath::Prefix prefix { .s = "foo" };
+ ASSERT_EQ(prefix.suffixIfPotentialMatch("foo//"), std::optional { "/" });
+}
+
+TEST(SearchPathElem, suffixIfPotentialMatch_trailingPath) {
+ SearchPath::Prefix prefix { .s = "foo" };
+ ASSERT_EQ(prefix.suffixIfPotentialMatch("foo/bar/baz"), std::optional { "bar/baz" });
+}
+
+}
diff --git a/src/libexpr/tests/value/print.cc b/src/libexpr/tests/value/print.cc
new file mode 100644
index 000000000..5e96e12ec
--- /dev/null
+++ b/src/libexpr/tests/value/print.cc
@@ -0,0 +1,236 @@
+#include "tests/libexpr.hh"
+
+#include "value.hh"
+
+namespace nix {
+
+using namespace testing;
+
+struct ValuePrintingTests : LibExprTest
+{
+ template<class... A>
+ void test(Value v, std::string_view expected, A... args)
+ {
+ std::stringstream out;
+ v.print(state.symbols, out, args...);
+ ASSERT_EQ(out.str(), expected);
+ }
+};
+
+TEST_F(ValuePrintingTests, tInt)
+{
+ Value vInt;
+ vInt.mkInt(10);
+ test(vInt, "10");
+}
+
+TEST_F(ValuePrintingTests, tBool)
+{
+ Value vBool;
+ vBool.mkBool(true);
+ test(vBool, "true");
+}
+
+TEST_F(ValuePrintingTests, tString)
+{
+ Value vString;
+ vString.mkString("some-string");
+ test(vString, "\"some-string\"");
+}
+
+TEST_F(ValuePrintingTests, tPath)
+{
+ Value vPath;
+ vPath.mkString("/foo");
+ test(vPath, "\"/foo\"");
+}
+
+TEST_F(ValuePrintingTests, tNull)
+{
+ Value vNull;
+ vNull.mkNull();
+ test(vNull, "null");
+}
+
+TEST_F(ValuePrintingTests, tAttrs)
+{
+ Value vOne;
+ vOne.mkInt(1);
+
+ Value vTwo;
+ vTwo.mkInt(2);
+
+ BindingsBuilder builder(state, state.allocBindings(10));
+ builder.insert(state.symbols.create("one"), &vOne);
+ builder.insert(state.symbols.create("two"), &vTwo);
+
+ Value vAttrs;
+ vAttrs.mkAttrs(builder.finish());
+
+ test(vAttrs, "{ one = 1; two = 2; }");
+}
+
+TEST_F(ValuePrintingTests, tList)
+{
+ Value vOne;
+ vOne.mkInt(1);
+
+ Value vTwo;
+ vTwo.mkInt(2);
+
+ Value vList;
+ state.mkList(vList, 5);
+ vList.bigList.elems[0] = &vOne;
+ vList.bigList.elems[1] = &vTwo;
+ vList.bigList.size = 3;
+
+ test(vList, "[ 1 2 (nullptr) ]");
+}
+
+TEST_F(ValuePrintingTests, vThunk)
+{
+ Value vThunk;
+ vThunk.mkThunk(nullptr, nullptr);
+
+ test(vThunk, "<CODE>");
+}
+
+TEST_F(ValuePrintingTests, vApp)
+{
+ Value vApp;
+ vApp.mkApp(nullptr, nullptr);
+
+ test(vApp, "<CODE>");
+}
+
+TEST_F(ValuePrintingTests, vLambda)
+{
+ Value vLambda;
+ vLambda.mkLambda(nullptr, nullptr);
+
+ test(vLambda, "<LAMBDA>");
+}
+
+TEST_F(ValuePrintingTests, vPrimOp)
+{
+ Value vPrimOp;
+ vPrimOp.mkPrimOp(nullptr);
+
+ test(vPrimOp, "<PRIMOP>");
+}
+
+TEST_F(ValuePrintingTests, vPrimOpApp)
+{
+ Value vPrimOpApp;
+ vPrimOpApp.mkPrimOpApp(nullptr, nullptr);
+
+ test(vPrimOpApp, "<PRIMOP-APP>");
+}
+
+TEST_F(ValuePrintingTests, vExternal)
+{
+ struct MyExternal : ExternalValueBase
+ {
+ public:
+ std::string showType() const override
+ {
+ return "";
+ }
+ std::string typeOf() const override
+ {
+ return "";
+ }
+ virtual std::ostream & print(std::ostream & str) const override
+ {
+ str << "testing-external!";
+ return str;
+ }
+ } myExternal;
+ Value vExternal;
+ vExternal.mkExternal(&myExternal);
+
+ test(vExternal, "testing-external!");
+}
+
+TEST_F(ValuePrintingTests, vFloat)
+{
+ Value vFloat;
+ vFloat.mkFloat(2.0);
+
+ test(vFloat, "2");
+}
+
+TEST_F(ValuePrintingTests, vBlackhole)
+{
+ Value vBlackhole;
+ vBlackhole.mkBlackhole();
+ test(vBlackhole, "«potential infinite recursion»");
+}
+
+TEST_F(ValuePrintingTests, depthAttrs)
+{
+ Value vOne;
+ vOne.mkInt(1);
+
+ Value vTwo;
+ vTwo.mkInt(2);
+
+ BindingsBuilder builder(state, state.allocBindings(10));
+ builder.insert(state.symbols.create("one"), &vOne);
+ builder.insert(state.symbols.create("two"), &vTwo);
+
+ Value vAttrs;
+ vAttrs.mkAttrs(builder.finish());
+
+ BindingsBuilder builder2(state, state.allocBindings(10));
+ builder2.insert(state.symbols.create("one"), &vOne);
+ builder2.insert(state.symbols.create("two"), &vTwo);
+ builder2.insert(state.symbols.create("nested"), &vAttrs);
+
+ Value vNested;
+ vNested.mkAttrs(builder2.finish());
+
+ test(vNested, "{ nested = «too deep»; one = «too deep»; two = «too deep»; }", false, 1);
+ test(vNested, "{ nested = { one = «too deep»; two = «too deep»; }; one = 1; two = 2; }", false, 2);
+ test(vNested, "{ nested = { one = 1; two = 2; }; one = 1; two = 2; }", false, 3);
+ test(vNested, "{ nested = { one = 1; two = 2; }; one = 1; two = 2; }", false, 4);
+}
+
+TEST_F(ValuePrintingTests, depthList)
+{
+ Value vOne;
+ vOne.mkInt(1);
+
+ Value vTwo;
+ vTwo.mkInt(2);
+
+ BindingsBuilder builder(state, state.allocBindings(10));
+ builder.insert(state.symbols.create("one"), &vOne);
+ builder.insert(state.symbols.create("two"), &vTwo);
+
+ Value vAttrs;
+ vAttrs.mkAttrs(builder.finish());
+
+ BindingsBuilder builder2(state, state.allocBindings(10));
+ builder2.insert(state.symbols.create("one"), &vOne);
+ builder2.insert(state.symbols.create("two"), &vTwo);
+ builder2.insert(state.symbols.create("nested"), &vAttrs);
+
+ Value vNested;
+ vNested.mkAttrs(builder2.finish());
+
+ Value vList;
+ state.mkList(vList, 5);
+ vList.bigList.elems[0] = &vOne;
+ vList.bigList.elems[1] = &vTwo;
+ vList.bigList.elems[2] = &vNested;
+ vList.bigList.size = 3;
+
+ test(vList, "[ «too deep» «too deep» «too deep» ]", false, 1);
+ test(vList, "[ 1 2 { nested = «too deep»; one = «too deep»; two = «too deep»; } ]", false, 2);
+ test(vList, "[ 1 2 { nested = { one = «too deep»; two = «too deep»; }; one = 1; two = 2; } ]", false, 3);
+ test(vList, "[ 1 2 { nested = { one = 1; two = 2; }; one = 1; two = 2; } ]", false, 4);
+ test(vList, "[ 1 2 { nested = { one = 1; two = 2; }; one = 1; two = 2; } ]", false, 5);
+}
+
+} // namespace nix
diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh
index 89c0c36fd..c44683e50 100644
--- a/src/libexpr/value.hh
+++ b/src/libexpr/value.hh
@@ -2,6 +2,7 @@
///@file
#include <cassert>
+#include <climits>
#include "symbol-table.hh"
#include "value/context.hh"
@@ -137,11 +138,11 @@ private:
friend std::string showType(const Value & v);
- void print(const SymbolTable & symbols, std::ostream & str, std::set<const void *> * seen) const;
+ void print(const SymbolTable &symbols, std::ostream &str, std::set<const void *> *seen, int depth) const;
public:
- void print(const SymbolTable & symbols, std::ostream & str, bool showRepeated = false) const;
+ void print(const SymbolTable &symbols, std::ostream &str, bool showRepeated = false, int depth = INT_MAX) const;
// Functions needed to distinguish the type
// These should be removed eventually, by putting the functionality that's
@@ -218,8 +219,11 @@ public:
/**
* Returns the normal type of a Value. This only returns nThunk if
* the Value hasn't been forceValue'd
+ *
+ * @param invalidIsThunk Instead of aborting an an invalid (probably
+ * 0, so uninitialized) internal type, return `nThunk`.
*/
- inline ValueType type() const
+ inline ValueType type(bool invalidIsThunk = false) const
{
switch (internalType) {
case tInt: return nInt;
@@ -234,7 +238,10 @@ public:
case tFloat: return nFloat;
case tThunk: case tApp: case tBlackhole: return nThunk;
}
- abort();
+ if (invalidIsThunk)
+ return nThunk;
+ else
+ abort();
}
/**