diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index fc51590b20..1c2aafd91f 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -149,6 +149,103 @@ ATermList evalList(EvalState & state, Expr e) } +/* String concatenation and context nodes: in order to allow users to + write things like + + "--with-freetype2-library=" + freetype + "/lib" + + where `freetype' is a derivation, we automatically coerce + derivations into their output path (e.g., + /nix/store/hashcode-freetype) in concatenations. However, if we do + this naively, we could introduce an undeclared dependency: when the + string is used in another derivation, that derivation would not + have an explicitly dependency on `freetype' in its inputDrvs + field. Thus `freetype' would not necessarily be built. + + To prevent this, we wrap the string resulting from the + concatenation in a *context node*, like this: + + Context([freetype], + Str("--with-freetype2-library=/nix/store/hashcode-freetype/lib")) + + Thus the context is the list of all derivations used in the + computation of a value. These contexts are propagated through + further concatenations. In processBinding() in primops.cc, context + nodes are unwrapped and added to inputDrvs. + + !!! Should the ordering of the context list have a canonical form? + + !!! Contexts are not currently recognised in most places in the + evaluator. */ + + +/* Coerce a value to a string, keeping track of contexts. */ +string coerceToStringWithContext(EvalState & state, + ATermList & context, Expr e, bool & isPath) +{ + isPath = false; + + e = evalExpr(state, e); + + ATermList es; + ATerm e2; + if (matchContext(e, es, e2)) { + e = e2; + context = ATconcat(es, context); + } + + ATerm s; + if (matchStr(e, s) || matchUri(e, s)) + return aterm2String(s); + + if (matchPath(e, s)) { + isPath = true; + return aterm2String(s); + } + + if (matchAttrs(e, es)) { + ATermMap attrs; + queryAllAttrs(e, attrs, false); + + Expr a = attrs.get("type"); + if (a && evalString(state, a) == "derivation") { + a = attrs.get("outPath"); + if (!a) throw Error("output path missing from derivation"); + context = ATinsert(context, e); + return evalPath(state, a); + } + } + + throw Error("cannot coerce value to string"); +} + + +/* Wrap an expression in a context if the context is not empty. */ +Expr wrapInContext(ATermList context, Expr e) +{ + return context == ATempty ? e : makeContext(context, e); +} + + +static ATerm concatStrings(EvalState & state, const ATermVector & args) +{ + ATermList context = ATempty; + ostringstream s; + bool isPath; + + for (ATermVector::const_iterator i = args.begin(); i != args.end(); ++i) { + bool isPath2; + s << coerceToStringWithContext(state, context, *i, isPath2); + if (i == args.begin()) isPath = isPath2; + } + + Expr result = isPath + ? makePath(toATerm(canonPath(s.str()))) + : makeStr(toATerm(s.str())); + return wrapInContext(context, result); +} + + Expr evalExpr2(EvalState & state, Expr e) { Expr e1, e2, e3, e4; @@ -167,7 +264,8 @@ Expr evalExpr2(EvalState & state, Expr e) sym == symFunction1 || sym == symAttrs || sym == symList || - sym == symPrimOp) + sym == symPrimOp || + sym == symContext) return e; /* The `Closed' constructor is just a way to prevent substitutions @@ -338,16 +436,10 @@ Expr evalExpr2(EvalState & state, Expr e) /* String or path concatenation. */ if (matchOpPlus(e, e1, e2)) { - e1 = evalExpr(state, e1); - e2 = evalExpr(state, e2); - ATerm s1, s2; - if (matchStr(e1, s1) && matchStr(e2, s2)) - return makeStr(toATerm( - (string) aterm2String(s1) + (string) aterm2String(s2))); - else if (matchPath(e1, s1) && matchPath(e2, s2)) - return makePath(toATerm(canonPath( - (string) aterm2String(s1) + "/" + (string) aterm2String(s2)))); - else throw Error("wrong argument types in `+' operator"); + ATermVector args; + args.push_back(e1); + args.push_back(e2); + return concatStrings(state, args); } /* List concatenation. */ diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 8070f4884c..54a612b367 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -59,6 +59,11 @@ bool evalBool(EvalState & state, Expr e); ATermList evalList(EvalState & state, Expr e); ATerm coerceToString(Expr e); +/* Contexts. */ +string coerceToStringWithContext(EvalState & state, + ATermList & context, Expr e, bool & isPath); +Expr wrapInContext(ATermList context, Expr e); + /* Print statistics. */ void printEvalStats(EvalState & state); diff --git a/src/libexpr/get-drvs.cc b/src/libexpr/get-drvs.cc index 78edbd392d..b101f2da30 100644 --- a/src/libexpr/get-drvs.cc +++ b/src/libexpr/get-drvs.cc @@ -45,6 +45,10 @@ MetaInfo DrvInfo::queryMetaInfo(EvalState & state) const } +/* Cache for already evaluated derivations. Usually putting ATerms in + a STL container is unsafe (they're not scanning for GC roots), but + here it doesn't matter; everything in this set is reachable from + the stack as well. */ typedef set Exprs; diff --git a/src/libexpr/nixexpr-ast.def b/src/libexpr/nixexpr-ast.def index b384ff7c08..fab560c99f 100644 --- a/src/libexpr/nixexpr-ast.def +++ b/src/libexpr/nixexpr-ast.def @@ -35,6 +35,7 @@ Closed | Expr | Expr | Rec | ATermList ATermList | Expr | Bool | ATerm | Expr | Null | | Expr | +Context | ATermList Expr | Expr | Bind | string Expr Pos | ATerm | Bind | string Expr | ATerm | Bind2 diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 3a291e007f..8935b147e5 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -109,6 +109,14 @@ static void processBinding(EvalState & state, Expr e, Derivation & drv, int n; Expr e1, e2; + if (matchContext(e, es, e2)) { + e = e2; + for (ATermIterator i(es); i; ++i) { + Strings dummy; + processBinding(state, *i, drv, dummy); + } + } + if (matchStr(e, s)) ss.push_back(aterm2String(s)); else if (matchUri(e, s)) ss.push_back(aterm2String(s)); else if (e == eTrue) ss.push_back("1"); @@ -408,9 +416,10 @@ ATerm coerceToString(Expr e) /* Convert the argument (which can be a path or a uri) to a string. */ static Expr primToString(EvalState & state, const ATermVector & args) { - ATerm s = coerceToString(evalExpr(state, args[0])); - if (!s) throw Error("cannot coerce value to string"); - return makeStr(s); + ATermList context = ATempty; + bool dummy; + string s = coerceToStringWithContext(state, context, args[0], dummy); + return wrapInContext(context, makeStr(toATerm(s))); } diff --git a/tests/dependencies.nix.in b/tests/dependencies.nix.in index 920564955a..aec9ec5b5f 100644 --- a/tests/dependencies.nix.in +++ b/tests/dependencies.nix.in @@ -18,8 +18,9 @@ let { name = "dependencies"; system = "@system@"; builder = "@shell@"; - args = ["-e" "-x" ./dependencies.builder0.sh]; - inherit input1 input2; + args = ["-e" "-x" (./dependencies.builder0.sh + "/FOOBAR/../.")]; + input1 = input1 + "/."; + inherit input2; }; } \ No newline at end of file diff --git a/tests/lang/eval-okay-string.exp b/tests/lang/eval-okay-string.exp index 07741d1605..9c3f56c3f3 100644 --- a/tests/lang/eval-okay-string.exp +++ b/tests/lang/eval-okay-string.exp @@ -1 +1 @@ -Str("foobar/a/b/c/d") +Str("foobar/a/b/c/d/foo/xyzzy/foo.txt/../foo/x/y") diff --git a/tests/lang/eval-okay-string.nix b/tests/lang/eval-okay-string.nix index 19b60497a5..b5280a0cd1 100644 --- a/tests/lang/eval-okay-string.nix +++ b/tests/lang/eval-okay-string.nix @@ -1 +1 @@ -"foo" + "bar" + toString (/a/b + /c/d) +"foo" + "bar" + toString (/a/b + /c/d) + (/foo/bar + "/../xyzzy/." + "/foo.txt") + ("/../foo" + /x/y)