aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Makefile.config.in2
-rw-r--r--configure.ac10
-rw-r--r--doc/manual/command-ref/nix-store.xml8
-rw-r--r--mk/libraries.mk2
-rw-r--r--release.nix1
-rw-r--r--scripts/install-multi-user.sh3
-rw-r--r--scripts/install-nix-from-closure.sh6
-rw-r--r--scripts/nix-profile.sh.in2
-rw-r--r--src/libexpr/eval.hh3
-rw-r--r--src/libexpr/primops.cc56
-rw-r--r--src/libexpr/primops/context.cc187
-rw-r--r--src/libstore/download.cc31
-rw-r--r--src/libstore/store-api.cc16
-rw-r--r--src/libstore/store-api.hh4
-rw-r--r--src/nix-store/nix-store.cc15
-rw-r--r--tests/common.sh.in1
-rw-r--r--tests/init.sh3
-rw-r--r--tests/lang/eval-okay-context-introspection.exp1
-rw-r--r--tests/lang/eval-okay-context-introspection.nix24
20 files changed, 293 insertions, 85 deletions
diff --git a/.gitignore b/.gitignore
index 0f2f3ddee..b75c5d489 100644
--- a/.gitignore
+++ b/.gitignore
@@ -81,6 +81,9 @@ perl/Makefile.config
/tests/common.sh
/tests/dummy
/tests/result*
+/tests/restricted-innocent
+/tests/shell
+/tests/shell.drv
# /tests/lang/
/tests/lang/*.out
diff --git a/Makefile.config.in b/Makefile.config.in
index b01a4afbf..59730b646 100644
--- a/Makefile.config.in
+++ b/Makefile.config.in
@@ -1,4 +1,6 @@
+AR = @AR@
BDW_GC_LIBS = @BDW_GC_LIBS@
+BUILD_SHARED_LIBS = @BUILD_SHARED_LIBS@
CC = @CC@
CFLAGS = @CFLAGS@
CXX = @CXX@
diff --git a/configure.ac b/configure.ac
index 5a2526672..410b20972 100644
--- a/configure.ac
+++ b/configure.ac
@@ -64,6 +64,7 @@ AC_PROG_CXX
AC_PROG_CPP
AX_CXX_COMPILE_STDCXX_14
+AC_CHECK_TOOL([AR], [ar])
# Use 64-bit file system calls so that we can support files > 2 GiB.
AC_SYS_LARGEFILE
@@ -267,6 +268,15 @@ AC_ARG_WITH(sandbox-shell, AC_HELP_STRING([--with-sandbox-shell=PATH],
sandbox_shell=$withval)
AC_SUBST(sandbox_shell)
+AC_ARG_ENABLE(shared, AC_HELP_STRING([--enable-shared],
+ [Build shared libraries for Nix [default=yes]]),
+ shared=$enableval, shared=yes)
+if test "$shared" = yes; then
+ AC_SUBST(BUILD_SHARED_LIBS, 1, [Whether to build shared libraries.])
+else
+ AC_SUBST(BUILD_SHARED_LIBS, 0, [Whether to build shared libraries.])
+fi
+
# Expand all variables in config.status.
test "$prefix" = NONE && prefix=$ac_default_prefix
diff --git a/doc/manual/command-ref/nix-store.xml b/doc/manual/command-ref/nix-store.xml
index 41a04f265..d73cb92ee 100644
--- a/doc/manual/command-ref/nix-store.xml
+++ b/doc/manual/command-ref/nix-store.xml
@@ -1282,6 +1282,7 @@ ktorrent-2.2.1/NEWS
<cmdsynopsis>
<command>nix-store</command>
<arg choice='plain'><option>--dump-db</option></arg>
+ <arg rep='repeat'><replaceable>paths</replaceable></arg>
</cmdsynopsis>
</refsection>
@@ -1292,6 +1293,13 @@ Nix database to standard output. It can be loaded into an empty Nix
store using <option>--load-db</option>. This is useful for making
backups and when migrating to different database schemas.</para>
+<para>By default, <option>--dump-db</option> will dump the entire Nix
+database. When one or more store paths is passed, only the subset of
+the Nix database for those store paths is dumped. As with
+<option>--export</option>, the user is responsible for passing all the
+store paths for a closure. See <option>--export</option> for an
+example.</para>
+
</refsection>
</refsection>
diff --git a/mk/libraries.mk b/mk/libraries.mk
index 14c95fa91..3953446cb 100644
--- a/mk/libraries.mk
+++ b/mk/libraries.mk
@@ -125,7 +125,7 @@ define build-library
$(1)_PATH := $$(_d)/$$($(1)_NAME).a
$$($(1)_PATH): $$($(1)_OBJS) | $$(_d)/
- $(trace-ar) ar crs $$@ $$?
+ $(trace-ar) $(AR) crs $$@ $$?
$(1)_LDFLAGS_USE += $$($(1)_PATH) $$($(1)_LDFLAGS)
diff --git a/release.nix b/release.nix
index 271645067..5c0027301 100644
--- a/release.nix
+++ b/release.nix
@@ -278,7 +278,6 @@ let
pkgs.runCommand "eval-nixos" { buildInputs = [ build.x86_64-linux ]; }
''
export NIX_STATE_DIR=$TMPDIR
- nix-store --init
nix-instantiate ${nixpkgs}/nixos/release-combined.nix -A tested --dry-run \
--arg nixpkgs '{ outPath = ${nixpkgs}; revCount = 123; shortRev = "abcdefgh"; }'
diff --git a/scripts/install-multi-user.sh b/scripts/install-multi-user.sh
index 6ee8dd485..f93be9d3a 100644
--- a/scripts/install-multi-user.sh
+++ b/scripts/install-multi-user.sh
@@ -674,9 +674,6 @@ $NIX_INSTALLED_NIX.
EOF
fi
- _sudo "to initialize the Nix Database" \
- $NIX_INSTALLED_NIX/bin/nix-store --init
-
cat ./.reginfo \
| _sudo "to load data for the first time in to the Nix Database" \
"$NIX_INSTALLED_NIX/bin/nix-store" --load-db
diff --git a/scripts/install-nix-from-closure.sh b/scripts/install-nix-from-closure.sh
index ab20774bb..fc633fa23 100644
--- a/scripts/install-nix-from-closure.sh
+++ b/scripts/install-nix-from-closure.sh
@@ -109,12 +109,6 @@ for i in $(cd "$self/store" >/dev/null && echo ./*); do
done
echo "" >&2
-echo "initialising Nix database..." >&2
-if ! $nix/bin/nix-store --init; then
- echo "$0: failed to initialize the Nix database" >&2
- exit 1
-fi
-
if ! "$nix/bin/nix-store" --load-db < "$self/.reginfo"; then
echo "$0: unable to register valid paths" >&2
exit 1
diff --git a/scripts/nix-profile.sh.in b/scripts/nix-profile.sh.in
index a5f52274f..db03e16ba 100644
--- a/scripts/nix-profile.sh.in
+++ b/scripts/nix-profile.sh.in
@@ -75,7 +75,7 @@ if [ -n "$HOME" ] && [ -n "$USER" ]; then
export NIX_SSL_CERT_FILE="$NIX_LINK/etc/ca-bundle.crt"
fi
- if [ -n "${MANPATH}" ]; then
+ if [ -n "${MANPATH-}" ]; then
export MANPATH="$NIX_LINK/share/man:$MANPATH"
fi
diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh
index d0f298e16..9fe387891 100644
--- a/src/libexpr/eval.hh
+++ b/src/libexpr/eval.hh
@@ -316,6 +316,9 @@ private:
/* Return a string representing the type of the value `v'. */
string showType(const Value & v);
+/* Decode a context string ‘!<name>!<path>’ into a pair <path,
+ name>. */
+std::pair<string, string> decodeContext(const string & s);
/* If `path' refers to a directory, then append "/default.nix". */
Path resolveExprPath(Path path);
diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc
index 60698f740..0da9f702f 100644
--- a/src/libexpr/primops.cc
+++ b/src/libexpr/primops.cc
@@ -687,21 +687,12 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
}
}
- /* See prim_unsafeDiscardOutputDependency. */
- else if (path.at(0) == '~')
- drv.inputSrcs.insert(string(path, 1));
-
/* Handle derivation outputs of the form ‘!<name>!<path>’. */
else if (path.at(0) == '!') {
std::pair<string, string> ctx = decodeContext(path);
drv.inputDrvs[ctx.first].insert(ctx.second);
}
- /* Handle derivation contexts returned by
- ‘builtins.storePath’. */
- else if (isDerivation(path))
- drv.inputDrvs[path] = state.store->queryDerivationOutputNames(path);
-
/* Otherwise it's a source file. */
else
drv.inputSrcs.insert(path);
@@ -1004,13 +995,8 @@ static void prim_toFile(EvalState & state, const Pos & pos, Value * * args, Valu
PathSet refs;
for (auto path : context) {
- if (path.at(0) == '=') path = string(path, 1);
- if (isDerivation(path)) {
- /* See prim_unsafeDiscardOutputDependency. */
- if (path.at(0) != '~')
- throw EvalError(format("in 'toFile': the file '%1%' cannot refer to derivation outputs, at %2%") % name % pos);
- path = string(path, 1);
- }
+ if (path.at(0) != '/')
+ throw EvalError(format("in 'toFile': the file '%1%' cannot refer to derivation outputs, at %2%") % name % pos);
refs.insert(path);
}
@@ -1794,41 +1780,6 @@ static void prim_stringLength(EvalState & state, const Pos & pos, Value * * args
}
-static void prim_unsafeDiscardStringContext(EvalState & state, const Pos & pos, Value * * args, Value & v)
-{
- PathSet context;
- string s = state.coerceToString(pos, *args[0], context);
- mkString(v, s, PathSet());
-}
-
-
-static void prim_hasContext(EvalState & state, const Pos & pos, Value * * args, Value & v)
-{
- PathSet context;
- state.forceString(*args[0], context, pos);
- mkBool(v, !context.empty());
-}
-
-
-/* Sometimes we want to pass a derivation path (i.e. pkg.drvPath) to a
- builder without causing the derivation to be built (for instance,
- in the derivation that builds NARs in nix-push, when doing
- source-only deployment). This primop marks the string context so
- that builtins.derivation adds the path to drv.inputSrcs rather than
- drv.inputDrvs. */
-static void prim_unsafeDiscardOutputDependency(EvalState & state, const Pos & pos, Value * * args, Value & v)
-{
- PathSet context;
- string s = state.coerceToString(pos, *args[0], context);
-
- PathSet context2;
- for (auto & p : context)
- context2.insert(p.at(0) == '=' ? "~" + string(p, 1) : p);
-
- mkString(v, s, context2);
-}
-
-
/* Return the cryptographic hash of a string in base-16. */
static void prim_hashString(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
@@ -2299,9 +2250,6 @@ void EvalState::createBaseEnv()
addPrimOp("toString", 1, prim_toString);
addPrimOp("__substring", 3, prim_substring);
addPrimOp("__stringLength", 1, prim_stringLength);
- addPrimOp("__hasContext", 1, prim_hasContext);
- addPrimOp("__unsafeDiscardStringContext", 1, prim_unsafeDiscardStringContext);
- addPrimOp("__unsafeDiscardOutputDependency", 1, prim_unsafeDiscardOutputDependency);
addPrimOp("__hashString", 2, prim_hashString);
addPrimOp("__match", 2, prim_match);
addPrimOp("__split", 2, prim_split);
diff --git a/src/libexpr/primops/context.cc b/src/libexpr/primops/context.cc
new file mode 100644
index 000000000..2d79739ea
--- /dev/null
+++ b/src/libexpr/primops/context.cc
@@ -0,0 +1,187 @@
+#include "primops.hh"
+#include "eval-inline.hh"
+#include "derivations.hh"
+
+namespace nix {
+
+static void prim_unsafeDiscardStringContext(EvalState & state, const Pos & pos, Value * * args, Value & v)
+{
+ PathSet context;
+ string s = state.coerceToString(pos, *args[0], context);
+ mkString(v, s, PathSet());
+}
+
+static RegisterPrimOp r1("__unsafeDiscardStringContext", 1, prim_unsafeDiscardStringContext);
+
+
+static void prim_hasContext(EvalState & state, const Pos & pos, Value * * args, Value & v)
+{
+ PathSet context;
+ state.forceString(*args[0], context, pos);
+ mkBool(v, !context.empty());
+}
+
+static RegisterPrimOp r2("__hasContext", 1, prim_hasContext);
+
+
+/* Sometimes we want to pass a derivation path (i.e. pkg.drvPath) to a
+ builder without causing the derivation to be built (for instance,
+ in the derivation that builds NARs in nix-push, when doing
+ source-only deployment). This primop marks the string context so
+ that builtins.derivation adds the path to drv.inputSrcs rather than
+ drv.inputDrvs. */
+static void prim_unsafeDiscardOutputDependency(EvalState & state, const Pos & pos, Value * * args, Value & v)
+{
+ PathSet context;
+ string s = state.coerceToString(pos, *args[0], context);
+
+ PathSet context2;
+ for (auto & p : context)
+ context2.insert(p.at(0) == '=' ? string(p, 1) : p);
+
+ mkString(v, s, context2);
+}
+
+static RegisterPrimOp r3("__unsafeDiscardOutputDependency", 1, prim_unsafeDiscardOutputDependency);
+
+
+/* Extract the context of a string as a structured Nix value.
+
+ The context is represented as an attribute set whose keys are the
+ paths in the context set and whose values are attribute sets with
+ the following keys:
+ path: True if the relevant path is in the context as a plain store
+ path (i.e. the kind of context you get when interpolating
+ a Nix path (e.g. ./.) into a string). False if missing.
+ allOutputs: True if the relevant path is a derivation and it is
+ in the context as a drv file with all of its outputs
+ (i.e. the kind of context you get when referencing
+ .drvPath of some derivation). False if missing.
+ outputs: If a non-empty list, the relevant path is a derivation
+ and the provided outputs are referenced in the context
+ (i.e. the kind of context you get when referencing
+ .outPath of some derivation). Empty list if missing.
+ Note that for a given path any combination of the above attributes
+ may be present.
+*/
+static void prim_getContext(EvalState & state, const Pos & pos, Value * * args, Value & v)
+{
+ struct ContextInfo {
+ bool path = false;
+ bool allOutputs = false;
+ Strings outputs;
+ };
+ PathSet context;
+ state.forceString(*args[0], context, pos);
+ auto contextInfos = std::map<Path, ContextInfo>();
+ for (const auto & p : context) {
+ Path drv;
+ string output;
+ const Path * path = &p;
+ if (p.at(0) == '=') {
+ drv = string(p, 1);
+ path = &drv;
+ } else if (p.at(0) == '!') {
+ std::pair<string, string> ctx = decodeContext(p);
+ drv = ctx.first;
+ output = ctx.second;
+ path = &drv;
+ }
+ auto isPath = drv.empty();
+ auto isAllOutputs = (!drv.empty()) && output.empty();
+
+ auto iter = contextInfos.find(*path);
+ if (iter == contextInfos.end()) {
+ contextInfos.emplace(*path, ContextInfo{isPath, isAllOutputs, output.empty() ? Strings{} : Strings{std::move(output)}});
+ } else {
+ if (isPath)
+ iter->second.path = true;
+ else if (isAllOutputs)
+ iter->second.allOutputs = true;
+ else
+ iter->second.outputs.emplace_back(std::move(output));
+ }
+ }
+
+ state.mkAttrs(v, contextInfos.size());
+
+ auto sPath = state.symbols.create("path");
+ auto sAllOutputs = state.symbols.create("allOutputs");
+ for (const auto & info : contextInfos) {
+ auto & infoVal = *state.allocAttr(v, state.symbols.create(info.first));
+ state.mkAttrs(infoVal, 3);
+ if (info.second.path)
+ mkBool(*state.allocAttr(infoVal, sPath), true);
+ if (info.second.allOutputs)
+ mkBool(*state.allocAttr(infoVal, sAllOutputs), true);
+ if (!info.second.outputs.empty()) {
+ auto & outputsVal = *state.allocAttr(infoVal, state.sOutputs);
+ state.mkList(outputsVal, info.second.outputs.size());
+ size_t i = 0;
+ for (const auto & output : info.second.outputs) {
+ mkString(*(outputsVal.listElems()[i++] = state.allocValue()), output);
+ }
+ }
+ infoVal.attrs->sort();
+ }
+ v.attrs->sort();
+}
+
+static RegisterPrimOp r4("__getContext", 1, prim_getContext);
+
+
+/* Append the given context to a given string.
+
+ See the commentary above unsafeGetContext for details of the
+ context representation.
+*/
+static void prim_appendContext(EvalState & state, const Pos & pos, Value * * args, Value & v)
+{
+ PathSet context;
+ auto orig = state.forceString(*args[0], context, pos);
+
+ state.forceAttrs(*args[1], pos);
+
+ auto sPath = state.symbols.create("path");
+ auto sAllOutputs = state.symbols.create("allOutputs");
+ for (auto & i : *args[1]->attrs) {
+ if (!state.store->isStorePath(i.name))
+ throw EvalError("Context key '%s' is not a store path, at %s", i.name, i.pos);
+ if (!settings.readOnlyMode)
+ state.store->ensurePath(i.name);
+ state.forceAttrs(*i.value, *i.pos);
+ auto iter = i.value->attrs->find(sPath);
+ if (iter != i.value->attrs->end()) {
+ if (state.forceBool(*iter->value, *iter->pos))
+ context.insert(i.name);
+ }
+
+ iter = i.value->attrs->find(sAllOutputs);
+ if (iter != i.value->attrs->end()) {
+ if (state.forceBool(*iter->value, *iter->pos)) {
+ if (!isDerivation(i.name)) {
+ throw EvalError("Tried to add all-outputs context of %s, which is not a derivation, to a string, at %s", i.name, i.pos);
+ }
+ context.insert("=" + string(i.name));
+ }
+ }
+
+ iter = i.value->attrs->find(state.sOutputs);
+ if (iter != i.value->attrs->end()) {
+ state.forceList(*iter->value, *iter->pos);
+ if (iter->value->listSize() && !isDerivation(i.name)) {
+ throw EvalError("Tried to add derivation output context of %s, which is not a derivation, to a string, at %s", i.name, i.pos);
+ }
+ for (unsigned int n = 0; n < iter->value->listSize(); ++n) {
+ auto name = state.forceStringNoCtx(*iter->value->listElems()[n], *iter->pos);
+ context.insert("!" + name + "!" + string(i.name));
+ }
+ }
+ }
+
+ mkString(v, orig, context);
+}
+
+static RegisterPrimOp r5("__appendContext", 2, prim_appendContext);
+
+}
diff --git a/src/libstore/download.cc b/src/libstore/download.cc
index 467f570bb..22382ab1d 100644
--- a/src/libstore/download.cc
+++ b/src/libstore/download.cc
@@ -614,6 +614,22 @@ struct CurlDownloader : public Downloader
writeFull(wakeupPipe.writeSide.get(), " ");
}
+#ifdef ENABLE_S3
+ std::tuple<std::string, std::string, Store::Params> parseS3Uri(std::string uri)
+ {
+ auto [path, params] = splitUriAndParams(uri);
+
+ auto slash = path.find('/', 5); // 5 is the length of "s3://" prefix
+ if (slash == std::string::npos)
+ throw nix::Error("bad S3 URI '%s'", path);
+
+ std::string bucketName(path, 5, slash - 5);
+ std::string key(path, slash + 1);
+
+ return {bucketName, key, params};
+ }
+#endif
+
void enqueueDownload(const DownloadRequest & request,
Callback<DownloadResult> callback) override
{
@@ -622,12 +638,15 @@ struct CurlDownloader : public Downloader
// FIXME: do this on a worker thread
try {
#ifdef ENABLE_S3
- S3Helper s3Helper("", Aws::Region::US_EAST_1, "", ""); // FIXME: make configurable
- auto slash = request.uri.find('/', 5);
- if (slash == std::string::npos)
- throw nix::Error("bad S3 URI '%s'", request.uri);
- std::string bucketName(request.uri, 5, slash - 5);
- std::string key(request.uri, slash + 1);
+ auto [bucketName, key, params] = parseS3Uri(request.uri);
+
+ std::string profile = get(params, "profile", "");
+ std::string region = get(params, "region", Aws::Region::US_EAST_1);
+ std::string scheme = get(params, "scheme", "");
+ std::string endpoint = get(params, "endpoint", "");
+
+ S3Helper s3Helper(profile, region, scheme, endpoint);
+
// FIXME: implement ETag
auto s3Res = s3Helper.getObject(bucketName, key);
DownloadResult res;
diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc
index 913a11121..c13ff1156 100644
--- a/src/libstore/store-api.cc
+++ b/src/libstore/store-api.cc
@@ -842,12 +842,11 @@ namespace nix {
RegisterStoreImplementation::Implementations * RegisterStoreImplementation::implementations = 0;
-
-ref<Store> openStore(const std::string & uri_,
- const Store::Params & extraParams)
+/* Split URI into protocol+hierarchy part and its parameter set. */
+std::pair<std::string, Store::Params> splitUriAndParams(const std::string & uri_)
{
auto uri(uri_);
- Store::Params params(extraParams);
+ Store::Params params;
auto q = uri.find('?');
if (q != std::string::npos) {
for (auto s : tokenizeString<Strings>(uri.substr(q + 1), "&")) {
@@ -873,6 +872,15 @@ ref<Store> openStore(const std::string & uri_,
}
uri = uri_.substr(0, q);
}
+ return {uri, params};
+}
+
+ref<Store> openStore(const std::string & uri_,
+ const Store::Params & extraParams)
+{
+ auto [uri, uriParams] = splitUriAndParams(uri_);
+ auto params = extraParams;
+ params.insert(uriParams.begin(), uriParams.end());
for (auto fun : *RegisterStoreImplementation::implementations) {
auto store = fun(uri, params);
diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh
index f504735e0..ad0f8df11 100644
--- a/src/libstore/store-api.hh
+++ b/src/libstore/store-api.hh
@@ -798,4 +798,8 @@ ValidPathInfo decodeValidPathInfo(std::istream & str,
for paths created by makeFixedOutputPath() / addToStore(). */
std::string makeFixedOutputCA(bool recursive, const Hash & hash);
+
+/* Split URI into protocol+hierarchy part and its parameter set. */
+std::pair<std::string, Store::Params> splitUriAndParams(const std::string & uri);
+
}
diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc
index a9ad14762..33138baff 100644
--- a/src/nix-store/nix-store.cc
+++ b/src/nix-store/nix-store.cc
@@ -485,11 +485,16 @@ static void opReadLog(Strings opFlags, Strings opArgs)
static void opDumpDB(Strings opFlags, Strings opArgs)
{
if (!opFlags.empty()) throw UsageError("unknown flag");
- if (!opArgs.empty())
- throw UsageError("no arguments expected");
- PathSet validPaths = store->queryAllValidPaths();
- for (auto & i : validPaths)
- cout << store->makeValidityRegistration({i}, true, true);
+ if (!opArgs.empty()) {
+ for (auto & i : opArgs)
+ i = store->followLinksToStorePath(i);
+ for (auto & i : opArgs)
+ cout << store->makeValidityRegistration({i}, true, true);
+ } else {
+ PathSet validPaths = store->queryAllValidPaths();
+ for (auto & i : validPaths)
+ cout << store->makeValidityRegistration({i}, true, true);
+ }
}
diff --git a/tests/common.sh.in b/tests/common.sh.in
index 6a523ca9d..28d0e8bfd 100644
--- a/tests/common.sh.in
+++ b/tests/common.sh.in
@@ -53,7 +53,6 @@ clearStore() {
mkdir "$NIX_STORE_DIR"
rm -rf "$NIX_STATE_DIR"
mkdir "$NIX_STATE_DIR"
- nix-store --init
clearProfiles
}
diff --git a/tests/init.sh b/tests/init.sh
index 19a12c1e2..6fa4f815b 100644
--- a/tests/init.sh
+++ b/tests/init.sh
@@ -25,9 +25,6 @@ fsync-metadata = false
!include nix.conf.extra.not-there
EOF
-# Initialise the database.
-nix-store --init
-
# Did anything happen?
test -e "$NIX_STATE_DIR"/db/db.sqlite
diff --git a/tests/lang/eval-okay-context-introspection.exp b/tests/lang/eval-okay-context-introspection.exp
new file mode 100644
index 000000000..27ba77dda
--- /dev/null
+++ b/tests/lang/eval-okay-context-introspection.exp
@@ -0,0 +1 @@
+true
diff --git a/tests/lang/eval-okay-context-introspection.nix b/tests/lang/eval-okay-context-introspection.nix
new file mode 100644
index 000000000..43178bd2e
--- /dev/null
+++ b/tests/lang/eval-okay-context-introspection.nix
@@ -0,0 +1,24 @@
+let
+ drv = derivation {
+ name = "fail";
+ builder = "/bin/false";
+ system = "x86_64-linux";
+ outputs = [ "out" "foo" ];
+ };
+
+ path = "${./eval-okay-context-introspection.nix}";
+
+ desired-context = {
+ "${builtins.unsafeDiscardStringContext path}" = {
+ path = true;
+ };
+ "${builtins.unsafeDiscardStringContext drv.drvPath}" = {
+ outputs = [ "foo" "out" ];
+ allOutputs = true;
+ };
+ };
+
+ legit-context = builtins.getContext "${path}${drv.outPath}${drv.foo.outPath}${drv.drvPath}";
+
+ constructed-context = builtins.getContext (builtins.appendContext "" desired-context);
+in legit-context == constructed-context