diff --git a/src/libexpr/meson.build b/src/libexpr/meson.build index 520ec2d83..654a4e87b 100644 --- a/src/libexpr/meson.build +++ b/src/libexpr/meson.build @@ -97,6 +97,7 @@ libexpr_sources = files( 'primops/fromTOML.cc', 'primops/import.cc', 'primops/list.cc', + 'primops/path.cc', 'primops/string.cc', 'primops/types.cc', 'value/context.cc', diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 093c3862a..a01eb9de4 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -115,25 +115,6 @@ StringMap EvalState::realiseContext(const NixStringContext & context) return res; } -SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, const RealisePathFlags flags) -{ - NixStringContext context; - - auto path = state.coerceToPath(noPos, v, context, "while realising the context of a path"); - - try { - StringMap rewrites = state.realiseContext(context); - - auto realPath = state.rootPath(CanonPath(state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context))); - - return flags.checkForPureEval - ? state.checkSourcePath(realPath) - : realPath; - } catch (Error & e) { - e.addTrace(state.positions[pos], "while realising the context of path '%s'", path); - throw; - } -} /* Execute a program and parse its output */ @@ -812,437 +793,12 @@ static RegisterPrimOp primop_derivationStrict(PrimOp { .fun = prim_derivationStrict, }); -/* Return a placeholder string for the specified output that will be - substituted by the corresponding output path at build time. For - example, 'placeholder "out"' returns the string - /1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9. At build - time, any occurrence of this string in an derivation attribute will - be replaced with the concrete path in the Nix store of the output - ‘out’. */ -static void prim_placeholder(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - v.mkString(hashPlaceholder(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.placeholder"))); -} - -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 *************************************************************/ - -/* Convert the argument to a path. !!! obsolete? */ -static void prim_toPath(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - NixStringContext context; - auto path = state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.toPath"); - v.mkString(path.path.abs(), 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 - generates a call to a function with an already existing store path - as argument. You don't want to use `toPath' here because it copies - the path to the Nix store, which yields a copy like - /nix/store/newhash-oldhash-oldname. In the past, `toPath' had - special case behaviour for store paths, but that created weird - corner cases. */ -static void prim_storePath(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - if (evalSettings.pureEval) - state.error( - "'%s' is not allowed in pure evaluation mode", - "builtins.storePath" - ).atPos(pos).debugThrow(); - - NixStringContext context; - auto path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.storePath")).path; - /* Resolve symlinks in ‘path’, unless ‘path’ itself is a symlink - directly in the store. The latter condition is necessary so - e.g. nix-push does the right thing. */ - if (!state.store->isStorePath(path.abs())) - path = CanonPath(canonPath(path.abs(), true)); - if (!state.store->isInStore(path.abs())) - state.error("path '%1%' is not in the Nix store", path) - .atPos(pos).debugThrow(); - auto path2 = state.store->toStorePath(path.abs()).first; - if (!settings.readOnlyMode) - state.store->ensurePath(path2); - context.insert(NixStringContextElem::Opaque { .path = path2 }); - v.mkString(path.abs(), 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`). - - Not available in [pure evaluation mode](@docroot@/command-ref/conf-file.md#conf-pure-eval). - - See also [`builtins.fetchClosure`](#builtins-fetchClosure). - )", - .fun = prim_storePath, -}); - -static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - auto & arg = *args[0]; - - /* We don’t check the path right now, because we don’t want to - throw if the path isn’t allowed, but just return false (and we - can’t just catch the exception here because we still want to - throw if something in the evaluation of `arg` tries to - access an unauthorized path). */ - auto path = realisePath(state, pos, arg, { .checkForPureEval = false }); - - /* SourcePath doesn't know about trailing slash. */ - auto mustBeDir = arg.type() == nString - && (arg.str().ends_with("/") - || arg.str().ends_with("/.")); - - try { - auto checked = state - .checkSourcePath(path) - .resolveSymlinks(mustBeDir ? SymlinkResolution::Full : SymlinkResolution::Ancestors); - - auto st = checked.maybeLstat(); - auto exists = st && (!mustBeDir || st->type == InputAccessor::tDirectory); - v.mkBool(exists); - } catch (SysError & e) { - /* Don't give away info from errors while canonicalising - ‘path’ in restricted mode. */ - v.mkBool(false); - } catch (RestrictedPathError & e) { - v.mkBool(false); - } -} - -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 directory of the given path, i.e., everything before the - last slash. Return either a path or a string depending on the type - of the argument. */ -static void prim_dirOf(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceValue(*args[0], pos); - if (args[0]->type() == nPath) { - auto path = args[0]->path(); - v.mkPath(path.path.isRoot() ? path : path.parent()); - } else { - NixStringContext context; - auto path = state.coerceToString(pos, *args[0], context, - "while evaluating the first argument passed to 'builtins.dirOf'", - false, false); - auto dir = dirOf(*path); - v.mkString(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 PosIdx pos, Value * * args, Value & v) -{ - auto path = realisePath(state, pos, *args[0]); - auto s = path.readFile(); - if (s.find((char) 0) != std::string::npos) - state.error( - "the contents of the file '%1%' cannot be represented as a Nix string", - path - ).atPos(pos).debugThrow(); - StorePathSet refs; - if (state.store->isInStore(path.path.abs())) { - try { - refs = state.store->queryPathInfo(state.store->toStorePath(path.path.abs()).first)->references; - } catch (Error &) { // FIXME: should be InvalidPathError - } - // Re-scan references to filter down to just the ones that actually occur in the file. - auto refsSink = PathRefScanSink::fromPaths(refs); - refsSink << s; - refs = refsSink.getResultPaths(); - } - NixStringContext context; - for (auto && p : std::move(refs)) { - context.insert(NixStringContextElem::Opaque { - .path = std::move((StorePath &&)p), - }); - } - v.mkString(s, context); -} - -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 paths, - which are desugared to 'findFile __nixPath "x"'. */ -static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.findFile"); - - SearchPath searchPath; - - for (auto v2 : args[0]->listItems()) { - state.forceAttrs(*v2, pos, "while evaluating an element of the list passed to builtins.findFile"); - - std::string prefix; - Bindings::iterator i = v2->attrs->find(state.sPrefix); - if (i != v2->attrs->end()) - prefix = state.forceStringNoCtx(*i->value, pos, "while evaluating the `prefix` attribute of an element of the list passed to builtins.findFile"); - - i = getAttr(state, state.sPath, v2->attrs, "in an element of the __nixPath"); - - NixStringContext context; - auto path = state.coerceToString(pos, *i->value, context, - "while evaluating the `path` attribute of an element of the list passed to builtins.findFile", - false, false).toOwned(); - - try { - auto rewrites = state.realiseContext(context); - path = rewriteStrings(path, rewrites); - } catch (InvalidPathError & e) { - state.error( - "cannot find '%1%', since path '%2%' is not valid", - path, - e.path - ).atPos(pos).debugThrow(); - } - - 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"); - - v.mkPath(state.checkSourcePath(state.findFile(searchPath, path, pos))); -} - -static RegisterPrimOp primop_findFile(PrimOp { - .name = "__findFile", - .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 - - ``` - - is equivalent to: - - ```nix - builtins.findFile builtins.nixPath "nixpkgs" - ``` - )", - .fun = prim_findFile, -}); - -static std::string_view fileTypeToString(InputAccessor::Type type) -{ - return - type == InputAccessor::Type::tRegular ? "regular" : - type == InputAccessor::Type::tDirectory ? "directory" : - type == InputAccessor::Type::tSymlink ? "symlink" : - "unknown"; -} - -static void prim_readFileType(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - auto path = realisePath(state, pos, *args[0]); - /* Retrieve the directory entry type and stringize it. */ - v.mkString(fileTypeToString(path.lstat().type)); -} - -static RegisterPrimOp primop_readFileType({ - .name = "__readFileType", - .args = {"p"}, - .doc = R"( - Determine the directory entry type of a filesystem node, being - one of "directory", "regular", "symlink", or "unknown". - )", - .fun = prim_readFileType, -}); - -/* Read a directory (without . or ..) */ -static void prim_readDir(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - auto path = realisePath(state, pos, *args[0]); - - // Retrieve directory entries for all nodes in a directory. - // This is similar to `getFileType` but is optimized to reduce system calls - // on many systems. - auto entries = path.readDirectory(); - auto attrs = state.buildBindings(entries.size()); - - // If we hit unknown directory entry types we may need to fallback to - // using `getFileType` on some systems. - // In order to reduce system calls we make each lookup lazy by using - // `builtins.readFileType` application. - Value * readFileType = nullptr; - - for (auto & [name, type] : entries) { - auto & attr = attrs.alloc(name); - if (!type) { - // Some filesystems or operating systems may not be able to return - // detailed node info quickly in this case we produce a thunk to - // query the file type lazily. - auto epath = state.allocValue(); - epath->mkPath(path + name); - if (!readFileType) - readFileType = &state.getBuiltin("readFileType"); - attr.mkApp(readFileType, epath); - } else { - // This branch of the conditional is much more likely. - // Here we just stringize the directory entry type. - attr.mkString(fileTypeToString(*type)); - } - } - - v.mkAttrs(attrs); -} - -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, -}); - -/* Extend single element string context with another output. */ -static void prim_outputOf(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - SingleDerivedPath drvPath = state.coerceToSingleDerivedPath(pos, *args[0], "while evaluating the first argument to builtins.outputOf"); - - OutputNameView outputName = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument to builtins.outputOf"); - - state.mkSingleDerivedPathString( - SingleDerivedPath::Built { - .drvPath = make_ref(drvPath), - .output = std::string { outputName }, - }, - v); -} - -static RegisterPrimOp primop_outputOf({ - .name = "__outputOf", - .args = {"derivation-reference", "output-name"}, - .doc = R"( - Return the output path of a derivation, literally or using a placeholder if needed. - - If the derivation has a statically-known output path (i.e. the derivation output is input-addressed, or fixed content-addresed), the output path will just be returned. - But if the derivation is content-addressed or if the derivation is itself not-statically produced (i.e. is the output of another derivation), a placeholder will be returned instead. - - *`derivation reference`* must be a string that may contain a regular store path to a derivation, or may be a placeholder reference. If the derivation is produced by a derivation, you must explicitly select `drv.outPath`. - This primop can be chained arbitrarily deeply. - For instance, - - ```nix - builtins.outputOf - (builtins.outputOf myDrv "out) - "out" - ``` - - will return a placeholder for the output of the output of `myDrv`. - - This primop corresponds to the `^` sigil for derivable paths, e.g. as part of installable syntax on the command line. - )", - .fun = prim_outputOf, - .experimentalFeature = Xp::DynamicDerivations, -}); - /************************************************************* * Creating files *************************************************************/ @@ -1410,333 +966,7 @@ static RegisterPrimOp primop_fromJSON({ /* Store a string in the Nix store as a source file that can be used as an input by derivations. */ -static void prim_toFile(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - NixStringContext context; - std::string name(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.toFile")); - std::string contents(state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.toFile")); - StorePathSet refs; - - for (auto c : context) { - if (auto p = std::get_if(&c.raw)) - refs.insert(p->path); - else - state.error( - "files created by %1% may not reference derivations, but %2% references %3%", - "builtins.toFile", - name, - c.to_string() - ).atPos(pos).debugThrow(); - } - - auto storePath = settings.readOnlyMode - ? state.store->computeStorePathForText(name, contents, refs) - : state.store->addTextToStore(name, contents, refs, state.repair); - - /* Note: we don't need to add `context' to the context of the - result, since `storePath' itself has references to the paths - used in args[1]. */ - - /* Add the output of this to the allowed paths. */ - state.allowAndSetStorePathString(storePath, v); -} - -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 and its build script 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 a - [string interpolation](@docroot@/language/values.md#type-string), 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 PosIdx pos, - std::string_view name, - Path path, - Value * filterFun, - FileIngestionMethod method, - const std::optional expectedHash, - Value & v, - const NixStringContext & context) -{ - try { - // FIXME: handle CA derivation outputs (where path needs to - // be rewritten to the actual output). - auto rewrites = state.realiseContext(context); - path = state.toRealPath(rewriteStrings(path, rewrites), context); - - StorePathSet refs; - - if (state.store->isInStore(path)) { - try { - auto [storePath, subPath] = state.store->toStorePath(path); - // FIXME: we should scanForReferences on the path before adding it - refs = state.store->queryPathInfo(storePath)->references; - path = state.store->toRealPath(storePath) + subPath; - } catch (Error &) { // FIXME: should be InvalidPathError - } - } - - path = evalSettings.pureEval && expectedHash - ? path - : state.checkSourcePath(CanonPath(path)).path.abs(); - - PathFilter filter = filterFun ? ([&](const Path & path) { - auto st = lstat(path); - - /* Call the filter function. The first argument is the path, - the second is a string indicating the type of the file. */ - Value arg1; - arg1.mkString(path); - - Value arg2; - arg2.mkString( - S_ISREG(st.st_mode) ? "regular" : - S_ISDIR(st.st_mode) ? "directory" : - S_ISLNK(st.st_mode) ? "symlink" : - "unknown" /* not supported, will fail! */); - - Value * args []{&arg1, &arg2}; - Value res; - state.callFunction(*filterFun, 2, args, res, pos); - - return state.forceBool(res, pos, "while evaluating the return value of the path filter function"); - }) : defaultPathFilter; - - std::optional expectedStorePath; - if (expectedHash) - expectedStorePath = state.store->makeFixedOutputPath(name, FixedOutputInfo { - .method = method, - .hash = *expectedHash, - .references = {}, - }); - - if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { - auto dstPath = fetchToStore( - *state.store, state.rootPath(CanonPath(path)), name, method, &filter, state.repair); - if (expectedHash && expectedStorePath != dstPath) - state.error( - "store path mismatch in (possibly filtered) path added from '%s'", - path - ).atPos(pos).debugThrow(); - state.allowAndSetStorePathString(dstPath, v); - } else - state.allowAndSetStorePathString(*expectedStorePath, v); - } catch (Error & e) { - e.addTrace(state.positions[pos], "while adding path '%s'", path); - throw; - } -} - - -static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - NixStringContext context; - auto path = state.coerceToPath(pos, *args[1], context, - "while evaluating the second argument (the path to filter) passed to builtins.filterSource"); - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterSource"); - addPath(state, pos, path.baseName(), path.path.abs(), args[0], FileIngestionMethod::Recursive, std::nullopt, v, context); -} - -static RegisterPrimOp primop_filterSource({ - .name = "__filterSource", - .args = {"e1", "e2"}, - .doc = R"( - > **Warning** - > - > `filterSource` should not be used to filter store paths. Since - > `filterSource` uses the name of the input directory while naming - > the output directory, doing so will produce a directory name in - > the form of `--`, where `-` is - > the name of the input directory. Since `` depends on the - > unfiltered directory, the name of the output directory will - > indirectly depend on files that are filtered out by the - > function. This will trigger a rebuild even when a filtered out - > file is changed. Use `builtins.path` instead, which allows - > specifying the name of the output directory. - - 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 PosIdx pos, Value * * args, Value & v) -{ - std::optional path; - std::string name; - Value * filterFun = nullptr; - auto method = FileIngestionMethod::Recursive; - std::optional expectedHash; - NixStringContext context; - - state.forceAttrs(*args[0], pos, "while evaluating the argument passed to 'builtins.path'"); - - for (auto & attr : *args[0]->attrs) { - auto n = state.symbols[attr.name]; - if (n == "path") - path.emplace(state.coerceToPath(attr.pos, *attr.value, context, "while evaluating the 'path' attribute passed to 'builtins.path'")); - else if (attr.name == state.sName) - name = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `name` attribute passed to builtins.path"); - else if (n == "filter") - state.forceFunction(*(filterFun = attr.value), attr.pos, "while evaluating the `filter` parameter passed to builtins.path"); - else if (n == "recursive") - method = FileIngestionMethod { state.forceBool(*attr.value, attr.pos, "while evaluating the `recursive` attribute passed to builtins.path") }; - else if (n == "sha256") - expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `sha256` attribute passed to builtins.path"), htSHA256); - else - state.error( - "unsupported argument '%1%' to 'addPath'", - state.symbols[attr.name] - ).atPos(attr.pos).debugThrow(); - } - if (!path) - state.error( - "missing required 'path' attribute in the first argument to builtins.path" - ).atPos(pos).debugThrow(); - if (name.empty()) - name = path->baseName(); - - addPath(state, pos, name, path->path.abs(), filterFun, method, expectedHash, v, context); -} - -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, -}); /************************************************************* diff --git a/src/libexpr/primops/path.cc b/src/libexpr/primops/path.cc new file mode 100644 index 000000000..5402b5812 --- /dev/null +++ b/src/libexpr/primops/path.cc @@ -0,0 +1,932 @@ +#include "eval-settings.hh" +#include "fetch-to-store.hh" +#include "path-references.hh" +#include "primops.hh" +#include "store-api.hh" + +namespace nix { + +// TODO: Use a const map instead ? +static std::string_view fileTypeToString(InputAccessor::Type type) +{ + return type == InputAccessor::Type::tRegular ? "regular" + : type == InputAccessor::Type::tDirectory ? "directory" + : type == InputAccessor::Type::tSymlink ? "symlink" + : "unknown"; +} + +static void addPath( + EvalState & state, + const PosIdx pos, + std::string_view name, + Path path, + Value * filterFun, + FileIngestionMethod method, + const std::optional expectedHash, + Value & v, + const NixStringContext & context +) +{ + try { + // FIXME: handle CA derivation outputs (where path needs to + // be rewritten to the actual output). + auto rewrites = state.realiseContext(context); + path = state.toRealPath(rewriteStrings(path, rewrites), context); + + StorePathSet refs; + + if (state.store->isInStore(path)) { + try { + auto [storePath, subPath] = state.store->toStorePath(path); + // FIXME: we should scanForReferences on the path before adding it + refs = state.store->queryPathInfo(storePath)->references; + path = state.store->toRealPath(storePath) + subPath; + } catch (Error &) { // FIXME: should be InvalidPathError + } + } + + path = evalSettings.pureEval && expectedHash + ? path + : state.checkSourcePath(CanonPath(path)).path.abs(); + + PathFilter filter = filterFun ? ([&](const Path & path) { + auto st = lstat(path); + + /* Call the filter function. The first argument is the path, + the second is a string indicating the type of the file. */ + Value arg1; + arg1.mkString(path); + + Value arg2; + arg2.mkString( + S_ISREG(st.st_mode) ? "regular" + : S_ISDIR(st.st_mode) ? "directory" + : S_ISLNK(st.st_mode) ? "symlink" + : "unknown" /* not supported, will fail! */ + ); + + Value * args[]{&arg1, &arg2}; + Value res; + state.callFunction(*filterFun, 2, args, res, pos); + + return state.forceBool( + res, pos, "while evaluating the return value of the path filter function" + ); + }) + : defaultPathFilter; + + std::optional expectedStorePath; + if (expectedHash) { + expectedStorePath = state.store->makeFixedOutputPath( + name, + FixedOutputInfo{ + .method = method, + .hash = *expectedHash, + .references = {}, + } + ); + } + + if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { + auto dstPath = fetchToStore( + *state.store, state.rootPath(CanonPath(path)), name, method, &filter, state.repair + ); + if (expectedHash && expectedStorePath != dstPath) { + state + .error( + "store path mismatch in (possibly filtered) path added from '%s'", path + ) + .atPos(pos) + .debugThrow(); + } + state.allowAndSetStorePathString(dstPath, v); + } else { + state.allowAndSetStorePathString(*expectedStorePath, v); + } + } catch (Error & e) { + e.addTrace(state.positions[pos], "while adding path '%s'", path); + throw; + } +} + +SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, const RealisePathFlags flags) +{ + NixStringContext context; + + auto path = state.coerceToPath(noPos, v, context, "while realising the context of a path"); + + try { + StringMap rewrites = state.realiseContext(context); + + auto realPath = state.rootPath(CanonPath(state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context))); + + return flags.checkForPureEval + ? state.checkSourcePath(realPath) + : realPath; + } catch (Error & e) { + e.addTrace(state.positions[pos], "while realising the context of path '%s'", path); + throw; + } +} + +/** + * builtins.dirOf + */ + +static void prim_dirOf(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceValue(*args[0], pos); + if (args[0]->type() == nPath) { + auto path = args[0]->path(); + v.mkPath(path.path.isRoot() ? path : path.parent()); + } else { + NixStringContext context; + auto path = state.coerceToString( + pos, + *args[0], + context, + "while evaluating the first argument passed to 'builtins.dirOf'", + false, + false + ); + auto dir = dirOf(*path); + v.mkString(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, +}); + +/** + * builtins.filterSource + */ + +static void prim_filterSource(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + NixStringContext context; + auto path = state.coerceToPath( + pos, + *args[1], + context, + "while evaluating the second argument (the path to filter) passed to builtins.filterSource" + ); + state.forceFunction( + *args[0], pos, "while evaluating the first argument passed to builtins.filterSource" + ); + addPath( + state, + pos, + path.baseName(), + path.path.abs(), + args[0], + FileIngestionMethod::Recursive, + std::nullopt, + v, + context + ); +} + +static RegisterPrimOp primop_filterSource({ + .name = "__filterSource", + .args = {"e1", "e2"}, + .doc = R"( + > **Warning** + > + > `filterSource` should not be used to filter store paths. Since + > `filterSource` uses the name of the input directory while naming + > the output directory, doing so will produce a directory name in + > the form of `--`, where `-` is + > the name of the input directory. Since `` depends on the + > unfiltered directory, the name of the output directory will + > indirectly depend on files that are filtered out by the + > function. This will trigger a rebuild even when a filtered out + > file is changed. Use `builtins.path` instead, which allows + > specifying the name of the output directory. + + 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, +}); + +/** + * builtins.findFile + */ + +static void prim_findFile(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceList( + *args[0], pos, "while evaluating the first argument passed to builtins.findFile" + ); + + SearchPath searchPath; + + for (auto v2 : args[0]->listItems()) { + state.forceAttrs( + *v2, pos, "while evaluating an element of the list passed to builtins.findFile" + ); + + std::string prefix; + Bindings::iterator i = v2->attrs->find(state.sPrefix); + if (i != v2->attrs->end()) { + prefix = state.forceStringNoCtx( + *i->value, + pos, + "while evaluating the `prefix` attribute of an element of the list passed to " + "builtins.findFile" + ); + } + + i = getAttr(state, state.sPath, v2->attrs, "in an element of the __nixPath"); + + NixStringContext context; + auto path = state + .coerceToString( + pos, + *i->value, + context, + "while evaluating the `path` attribute of an element of the list " + "passed to builtins.findFile", + false, + false + ) + .toOwned(); + + try { + auto rewrites = state.realiseContext(context); + path = rewriteStrings(path, rewrites); + } catch (InvalidPathError & e) { + state.error("cannot find '%1%', since path '%2%' is not valid", path, e.path) + .atPos(pos) + .debugThrow(); + } + + 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" + ); + + v.mkPath(state.checkSourcePath(state.findFile(searchPath, path, pos))); +} + +static RegisterPrimOp primop_findFile(PrimOp{ + .name = "__findFile", + .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 + + ``` + + is equivalent to: + + ```nix + builtins.findFile builtins.nixPath "nixpkgs" + ``` + )", + .fun = prim_findFile, +}); + +/** + * builtins.outputOf + */ + +static void prim_outputOf(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + SingleDerivedPath drvPath = state.coerceToSingleDerivedPath( + pos, *args[0], "while evaluating the first argument to builtins.outputOf" + ); + + OutputNameView outputName = state.forceStringNoCtx( + *args[1], pos, "while evaluating the second argument to builtins.outputOf" + ); + + state.mkSingleDerivedPathString( + SingleDerivedPath::Built{ + .drvPath = make_ref(drvPath), + .output = std::string{outputName}, + }, + v + ); +} + +static RegisterPrimOp primop_outputOf({ + .name = "__outputOf", + .args = {"derivation-reference", "output-name"}, + .doc = R"( + Return the output path of a derivation, literally or using a placeholder if needed. + + If the derivation has a statically-known output path (i.e. the derivation output is input-addressed, or fixed content-addresed), the output path will just be returned. + But if the derivation is content-addressed or if the derivation is itself not-statically produced (i.e. is the output of another derivation), a placeholder will be returned instead. + + *`derivation reference`* must be a string that may contain a regular store path to a derivation, or may be a placeholder reference. If the derivation is produced by a derivation, you must explicitly select `drv.outPath`. + This primop can be chained arbitrarily deeply. + For instance, + + ```nix + builtins.outputOf + (builtins.outputOf myDrv "out) + "out" + ``` + + will return a placeholder for the output of the output of `myDrv`. + + This primop corresponds to the `^` sigil for derivable paths, e.g. as part of installable syntax on the command line. + )", + .fun = prim_outputOf, + .experimentalFeature = Xp::DynamicDerivations, +}); + +/** + * builtins.path + */ + +static void prim_path(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + std::optional path; + std::string name; + Value * filterFun = nullptr; + auto method = FileIngestionMethod::Recursive; + std::optional expectedHash; + NixStringContext context; + + state.forceAttrs(*args[0], pos, "while evaluating the argument passed to 'builtins.path'"); + + for (auto & attr : *args[0]->attrs) { + auto n = state.symbols[attr.name]; + if (n == "path") { + path.emplace(state.coerceToPath( + attr.pos, + *attr.value, + context, + "while evaluating the 'path' attribute passed to 'builtins.path'" + )); + } else if (attr.name == state.sName) { + name = state.forceStringNoCtx( + *attr.value, + attr.pos, + "while evaluating the `name` attribute passed to builtins.path" + ); + } else if (n == "filter") { + state.forceFunction( + *(filterFun = attr.value), + attr.pos, + "while evaluating the `filter` parameter passed to builtins.path" + ); + } else if (n == "recursive") { + method = FileIngestionMethod{state.forceBool( + *attr.value, + attr.pos, + "while evaluating the `recursive` attribute passed to builtins.path" + )}; + } else if (n == "sha256") { + expectedHash = newHashAllowEmpty( + state.forceStringNoCtx( + *attr.value, + attr.pos, + "while evaluating the `sha256` attribute passed to builtins.path" + ), + htSHA256 + ); + } else { + state + .error( + "unsupported argument '%1%' to 'addPath'", state.symbols[attr.name] + ) + .atPos(attr.pos) + .debugThrow(); + } + } + if (!path) { + state + .error( + "missing required 'path' attribute in the first argument to builtins.path" + ) + .atPos(pos) + .debugThrow(); + } + if (name.empty()) { + name = path->baseName(); + } + + addPath(state, pos, name, path->path.abs(), filterFun, method, expectedHash, v, context); +} + +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, +}); + +/** + * builtins.pathExists + */ + +static void prim_pathExists(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + auto & arg = *args[0]; + + /* We don’t check the path right now, because we don’t want to + throw if the path isn’t allowed, but just return false (and we + can’t just catch the exception here because we still want to + throw if something in the evaluation of `arg` tries to + access an unauthorized path). */ + auto path = realisePath(state, pos, arg, {.checkForPureEval = false}); + + /* SourcePath doesn't know about trailing slash. */ + auto mustBeDir = + arg.type() == nString && (arg.str().ends_with("/") || arg.str().ends_with("/.")); + + try { + auto checked = state.checkSourcePath(path).resolveSymlinks( + mustBeDir ? SymlinkResolution::Full : SymlinkResolution::Ancestors + ); + + auto st = checked.maybeLstat(); + auto exists = st && (!mustBeDir || st->type == InputAccessor::tDirectory); + v.mkBool(exists); + } catch (SysError & e) { + /* Don't give away info from errors while canonicalising + ‘path’ in restricted mode. */ + v.mkBool(false); + } catch (RestrictedPathError & e) { + v.mkBool(false); + } +} + +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, +}); + +/** + * builtins.palceholder + */ + +/* Return a placeholder string for the specified output that will be + substituted by the corresponding output path at build time. For + example, 'placeholder "out"' returns the string + /1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9. At build + time, any occurrence of this string in an derivation attribute will + be replaced with the concrete path in the Nix store of the output + ‘out’. */ +static void prim_placeholder(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + v.mkString(hashPlaceholder(state.forceStringNoCtx( + *args[0], pos, "while evaluating the first argument passed to builtins.placeholder" + ))); +} + +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, +}); + +/** + * builtins.toPath + * WARNING: deprecated + */ + +static void prim_toPath(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + NixStringContext context; + auto path = state.coerceToPath( + pos, *args[0], context, "while evaluating the first argument passed to builtins.toPath" + ); + v.mkString(path.path.abs(), 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, +}); + +/** + * builtins.readDir + */ + +static void prim_readDir(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + auto path = realisePath(state, pos, *args[0]); + + // Retrieve directory entries for all nodes in a directory. + // This is similar to `getFileType` but is optimized to reduce system calls + // on many systems. + auto entries = path.readDirectory(); + auto attrs = state.buildBindings(entries.size()); + + // If we hit unknown directory entry types we may need to fallback to + // using `getFileType` on some systems. + // In order to reduce system calls we make each lookup lazy by using + // `builtins.readFileType` application. + Value * readFileType = nullptr; + + for (auto & [name, type] : entries) { + auto & attr = attrs.alloc(name); + if (!type) { + // Some filesystems or operating systems may not be able to return + // detailed node info quickly in this case we produce a thunk to + // query the file type lazily. + auto epath = state.allocValue(); + epath->mkPath(path + name); + if (!readFileType) { + readFileType = &state.getBuiltin("readFileType"); + } + attr.mkApp(readFileType, epath); + } else { + // This branch of the conditional is much more likely. + // Here we just stringize the directory entry type. + attr.mkString(fileTypeToString(*type)); + } + } + + v.mkAttrs(attrs); +} + +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, +}); + +/** + * builtins.readFile + */ + +static void prim_readFile(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + auto path = realisePath(state, pos, *args[0]); + auto s = path.readFile(); + if (s.find((char) 0) != std::string::npos) { + state + .error( + "the contents of the file '%1%' cannot be represented as a Nix string", path + ) + .atPos(pos) + .debugThrow(); + } + StorePathSet refs; + if (state.store->isInStore(path.path.abs())) { + try { + refs = state.store->queryPathInfo(state.store->toStorePath(path.path.abs()).first) + ->references; + } catch (Error &) { // FIXME: should be InvalidPathError + } + // Re-scan references to filter down to just the ones that actually occur in the file. + auto refsSink = PathRefScanSink::fromPaths(refs); + refsSink << s; + refs = refsSink.getResultPaths(); + } + NixStringContext context; + for (auto && p : std::move(refs)) { + context.insert(NixStringContextElem::Opaque{ + .path = std::move((StorePath &&) p), + }); + } + v.mkString(s, context); +} + +static RegisterPrimOp primop_readFile({ + .name = "__readFile", + .args = {"path"}, + .doc = R"( + Return the contents of the file *path* as a string. + )", + .fun = prim_readFile, +}); + +/** + * builtins.readFileType + */ + +static void prim_readFileType(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + auto path = realisePath(state, pos, *args[0]); + /* Retrieve the directory entry type and stringize it. */ + v.mkString(fileTypeToString(path.lstat().type)); +} + +static RegisterPrimOp primop_readFileType({ + .name = "__readFileType", + .args = {"p"}, + .doc = R"( + Determine the directory entry type of a filesystem node, being + one of "directory", "regular", "symlink", or "unknown". + )", + .fun = prim_readFileType, +}); + +/** + * builtins.storePath + */ + +static void prim_storePath(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + if (evalSettings.pureEval) { + state.error("'%s' is not allowed in pure evaluation mode", "builtins.storePath") + .atPos(pos) + .debugThrow(); + } + + NixStringContext context; + auto path = state + .checkSourcePath(state.coerceToPath( + pos, + *args[0], + context, + "while evaluating the first argument passed to builtins.storePath" + )) + .path; + /* Resolve symlinks in ‘path’, unless ‘path’ itself is a symlink + directly in the store. The latter condition is necessary so + e.g. nix-push does the right thing. */ + if (!state.store->isStorePath(path.abs())) { + path = CanonPath(canonPath(path.abs(), true)); + } + if (!state.store->isInStore(path.abs())) { + state.error("path '%1%' is not in the Nix store", path).atPos(pos).debugThrow(); + } + auto path2 = state.store->toStorePath(path.abs()).first; + if (!settings.readOnlyMode) { + state.store->ensurePath(path2); + } + context.insert(NixStringContextElem::Opaque{.path = path2}); + v.mkString(path.abs(), 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`). + + Not available in [pure evaluation mode](@docroot@/command-ref/conf-file.md#conf-pure-eval). + + See also [`builtins.fetchClosure`](#builtins-fetchClosure). + )", + .fun = prim_storePath, +}); + +/** + * builtins.toFile + */ + +static void prim_toFile(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + NixStringContext context; + std::string name(state.forceStringNoCtx( + *args[0], pos, "while evaluating the first argument passed to builtins.toFile" + )); + std::string contents(state.forceString( + *args[1], context, pos, "while evaluating the second argument passed to builtins.toFile" + )); + + StorePathSet refs; + + for (auto c : context) { + if (auto p = std::get_if(&c.raw)) { + refs.insert(p->path); + } else { + state + .error( + "files created by %1% may not reference derivations, but %2% references %3%", + "builtins.toFile", + name, + c.to_string() + ) + .atPos(pos) + .debugThrow(); + } + } + + auto storePath = settings.readOnlyMode + ? state.store->computeStorePathForText(name, contents, refs) + : state.store->addTextToStore(name, contents, refs, state.repair); + + /* Note: we don't need to add `context' to the context of the + result, since `storePath' itself has references to the paths + used in args[1]. */ + + /* Add the output of this to the allowed paths. */ + state.allowAndSetStorePathString(storePath, v); +} + +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 and its build script 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 a + [string interpolation](@docroot@/language/values.md#type-string), 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, +}); + +}