about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile3
-rw-r--r--Makefile.config.in1
-rw-r--r--configure.ac16
-rw-r--r--doc/manual/command-ref/conf-file.xml38
-rw-r--r--doc/manual/expressions/builtins.xml11
-rw-r--r--doc/manual/release-notes/rl-2.0.xml7
-rw-r--r--mk/libraries.mk7
-rw-r--r--scripts/install-darwin-multi-user.sh2
-rw-r--r--src/build-remote/build-remote.cc4
-rw-r--r--src/libexpr/eval.cc17
-rw-r--r--src/libexpr/eval.hh2
-rw-r--r--src/libexpr/names.cc2
-rw-r--r--src/libexpr/names.hh2
-rw-r--r--src/libexpr/primops.cc21
-rw-r--r--src/libexpr/primops.hh3
-rw-r--r--src/libmain/shared.cc1
-rw-r--r--src/libmain/shared.hh1
-rw-r--r--src/libstore/build.cc34
-rw-r--r--src/libstore/globals.cc28
-rw-r--r--src/libstore/globals.hh10
-rw-r--r--src/libstore/local-store.cc3
-rw-r--r--src/libstore/local-store.hh3
-rw-r--r--src/libstore/local.mk6
-rw-r--r--src/libstore/pathlocks.cc6
-rw-r--r--src/libstore/pathlocks.hh6
-rw-r--r--src/libutil/config.cc26
-rw-r--r--src/libutil/monitor-fd.hh30
-rwxr-xr-xsrc/nix-build/nix-build.cc2
-rwxr-xr-xsrc/nix-channel/nix-channel.cc3
-rw-r--r--src/nix-collect-garbage/nix-collect-garbage.cc2
-rwxr-xr-xsrc/nix-copy-closure/nix-copy-closure.cc2
-rw-r--r--src/nix-daemon/nix-daemon.cc2
-rw-r--r--src/nix-env/nix-env.cc2
-rw-r--r--src/nix-instantiate/nix-instantiate.cc2
-rw-r--r--src/nix-prefetch-url/nix-prefetch-url.cc2
-rw-r--r--src/nix-store/nix-store.cc2
-rw-r--r--src/nix/build.cc2
-rw-r--r--src/nix/command.hh2
-rw-r--r--src/nix/installables.cc4
-rw-r--r--src/nix/main.cc2
-rw-r--r--tests/init.sh5
-rw-r--r--tests/lang/eval-okay-splitversion.exp1
-rw-r--r--tests/lang/eval-okay-splitversion.nix1
-rw-r--r--tests/local.mk5
-rw-r--r--tests/plugins.sh7
-rw-r--r--tests/plugins/local.mk9
-rw-r--r--tests/plugins/plugintest.cc10
48 files changed, 308 insertions, 50 deletions
diff --git a/.gitignore b/.gitignore
index ce22fa007dc7..0a9599378567 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,7 @@ perl/Makefile.config
 /scripts/nix-copy-closure
 /scripts/nix-reduce-build
 /scripts/nix-http-export.cgi
+/scripts/nix-profile-daemon.sh
 
 # /src/libexpr/
 /src/libexpr/lexer-tab.cc
diff --git a/Makefile b/Makefile
index 5d8e990cc5c0..c867823fc485 100644
--- a/Makefile
+++ b/Makefile
@@ -24,7 +24,8 @@ makefiles = \
   misc/launchd/local.mk \
   misc/upstart/local.mk \
   doc/manual/local.mk \
-  tests/local.mk
+  tests/local.mk \
+  tests/plugins/local.mk
 
 GLOBAL_CXXFLAGS += -std=c++14 -g -Wall -include config.h
 
diff --git a/Makefile.config.in b/Makefile.config.in
index fab82194656e..a9785dc73955 100644
--- a/Makefile.config.in
+++ b/Makefile.config.in
@@ -7,6 +7,7 @@ ENABLE_S3 = @ENABLE_S3@
 HAVE_SODIUM = @HAVE_SODIUM@
 HAVE_READLINE = @HAVE_READLINE@
 HAVE_BROTLI = @HAVE_BROTLI@
+HAVE_SECCOMP = @HAVE_SECCOMP@
 LIBCURL_LIBS = @LIBCURL_LIBS@
 OPENSSL_LIBS = @OPENSSL_LIBS@
 PACKAGE_NAME = @PACKAGE_NAME@
diff --git a/configure.ac b/configure.ac
index c7498fb5240f..4102f32166f5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -188,9 +188,21 @@ AC_SUBST(HAVE_BROTLI, [$have_brotli])
 
 # Look for libseccomp, required for Linux sandboxing.
 if test "$sys_name" = linux; then
-  PKG_CHECK_MODULES([LIBSECCOMP], [libseccomp],
-                    [CXXFLAGS="$LIBSECCOMP_CFLAGS $CXXFLAGS"])
+  AC_ARG_ENABLE([seccomp-sandboxing],
+                AC_HELP_STRING([--disable-seccomp-sandboxing],
+                               [Don't build support for seccomp sandboxing (only recommended if your arch doesn't support libseccomp yet!)]
+                              ))
+  if test "x$enable_seccomp_sandboxing" != "xno"; then
+    PKG_CHECK_MODULES([LIBSECCOMP], [libseccomp],
+                      [CXXFLAGS="$LIBSECCOMP_CFLAGS $CXXFLAGS"])
+    have_seccomp=1
+  else
+    have_seccomp=
+  fi
+else
+  have_seccomp=
 fi
+AC_SUBST(HAVE_SECCOMP, [$have_seccomp])
 
 
 # Look for aws-cpp-sdk-s3.
diff --git a/doc/manual/command-ref/conf-file.xml b/doc/manual/command-ref/conf-file.xml
index fff7994f28df..42906ddff5e2 100644
--- a/doc/manual/command-ref/conf-file.xml
+++ b/doc/manual/command-ref/conf-file.xml
@@ -40,7 +40,12 @@
 
 <para>The configuration files consist of
 <literal><replaceable>name</replaceable> =
-<replaceable>value</replaceable></literal> pairs, one per line.
+<replaceable>value</replaceable></literal> pairs, one per line. Other
+files can be included with a line like <literal>include
+<replaceable>path</replaceable></literal>, where
+<replaceable>path</replaceable> is interpreted relative to the current
+conf file and a missing file is an error unless
+<literal>!include</literal> is used instead.
 Comments start with a <literal>#</literal> character.  Here is an
 example configuration file:</para>
 
@@ -742,6 +747,37 @@ builtins.fetchurl {
   </varlistentry>
 
 
+  <varlistentry xml:id="conf-plugin-files">
+    <term><literal>plugin-files</literal></term>
+    <listitem>
+      <para>
+        A list of plugin files to be loaded by Nix. Each of these
+        files will be dlopened by Nix, allowing them to affect
+        execution through static initialization. In particular, these
+        plugins may construct static instances of RegisterPrimOp to
+        add new primops or constants to the expression language,
+        RegisterStoreImplementation to add new store implementations,
+        and RegisterCommand to add new subcommands to the
+        <literal>nix</literal> command. See the constructors for those
+        types for more details.
+      </para>
+      <para>
+	Since these files are loaded into the same address space as
+        Nix itself, they must be DSOs compatible with the instance of
+        Nix running at the time (i.e. compiled against the same
+        headers, not linked to any incompatible libraries). They
+        should not be linked to any Nix libs directly, as those will
+        be available already at load time.
+      </para>
+      <para>
+	If an entry in the list is a directory, all files in the
+	directory are loaded as plugins (non-recursively).
+      </para>
+    </listitem>
+
+  </varlistentry>
+
+
 </variablelist>
 
 </para>
diff --git a/doc/manual/expressions/builtins.xml b/doc/manual/expressions/builtins.xml
index 81770bcf6292..8a32ed8b5c99 100644
--- a/doc/manual/expressions/builtins.xml
+++ b/doc/manual/expressions/builtins.xml
@@ -126,6 +126,17 @@ if builtins ? getEnv then builtins.getEnv "PATH" else ""</programlisting>
   </varlistentry>
 
 
+  <varlistentry><term><function>builtins.splitVersion</function>
+  <replaceable>s</replaceable></term>
+
+    <listitem><para>Split a string representing a version into its
+    components, by the same version splitting logic underlying the
+    version comparison in <link linkend="ssec-version-comparisons">
+    <command>nix-env -u</command></link>.</para></listitem>
+
+  </varlistentry>
+
+
   <varlistentry><term><function>builtins.concatLists</function>
   <replaceable>lists</replaceable></term>
 
diff --git a/doc/manual/release-notes/rl-2.0.xml b/doc/manual/release-notes/rl-2.0.xml
index 32cdb1d0cefc..effd2e39d309 100644
--- a/doc/manual/release-notes/rl-2.0.xml
+++ b/doc/manual/release-notes/rl-2.0.xml
@@ -389,6 +389,13 @@ configureFlags = "--prefix=${placeholder "out"} --includedir=${placeholder "dev"
     </para>
   </listitem>
 
+  <listitem>
+    <para>
+      Nix can now be extended with plugins. See the documentation of
+      the 'plugin-files' option for more details.
+    </para>
+  </listitem>
+
 </itemizedlist>
 
 <para>Some features were removed:</para>
diff --git a/mk/libraries.mk b/mk/libraries.mk
index 3cd7a53107bd..14c95fa91cf6 100644
--- a/mk/libraries.mk
+++ b/mk/libraries.mk
@@ -45,6 +45,11 @@ endif
 # - $(1)_INSTALL_DIR: the directory where the library will be
 #   installed.  Defaults to $(libdir).
 #
+# - $(1)_EXCLUDE_FROM_LIBRARY_LIST: if defined, the library will not
+#   be automatically marked as a dependency of the top-level all
+#   target andwill not be listed in the make help output. This is
+#   useful for libraries built solely for testing, for example.
+#
 # - BUILD_SHARED_LIBS: if equal to ‘1’, a dynamic library will be
 #   built, otherwise a static library.
 define build-library
@@ -149,7 +154,9 @@ define build-library
   $(1)_DEPS := $$(foreach fn, $$($(1)_OBJS), $$(call filename-to-dep, $$(fn)))
   -include $$($(1)_DEPS)
 
+  ifndef $(1)_EXCLUDE_FROM_LIBRARY_LIST
   libs-list += $$($(1)_PATH)
+  endif
   clean-files += $$(_d)/*.a $$(_d)/*.$(SO_EXT) $$(_d)/*.o $$(_d)/.*.dep $$($(1)_DEPS) $$($(1)_OBJS)
   dist-files += $$(_srcs)
 endef
diff --git a/scripts/install-darwin-multi-user.sh b/scripts/install-darwin-multi-user.sh
index 8d59c1c2b9aa..716b6e9bc9a3 100644
--- a/scripts/install-darwin-multi-user.sh
+++ b/scripts/install-darwin-multi-user.sh
@@ -24,7 +24,7 @@ readonly YELLOW='\033[38;33m'
 readonly YELLOW_UL='\033[38;4;33m'
 
 readonly CORES=$(sysctl -n hw.ncpu)
-readonly NIX_USER_COUNT="$CORES"
+readonly NIX_USER_COUNT="32"
 readonly NIX_BUILD_GROUP_ID="30000"
 readonly NIX_BUILD_GROUP_NAME="nixbld"
 readonly NIX_FIRST_BUILD_UID="30001"
diff --git a/src/build-remote/build-remote.cc b/src/build-remote/build-remote.cc
index df579729af29..dbf8fe1b8f8a 100644
--- a/src/build-remote/build-remote.cc
+++ b/src/build-remote/build-remote.cc
@@ -64,6 +64,8 @@ int main (int argc, char * * argv)
 
         settings.maxBuildJobs.set("1"); // hack to make tests with local?root= work
 
+        initPlugins();
+
         auto store = openStore().cast<LocalStore>();
 
         /* It would be more appropriate to use $XDG_RUNTIME_DIR, since
@@ -241,7 +243,7 @@ connected:
 
         if (!missing.empty()) {
             Activity act(*logger, lvlTalkative, actUnknown, fmt("copying outputs from '%s'", storeUri));
-            setenv("NIX_HELD_LOCKS", concatStringsSep(" ", missing).c_str(), 1); /* FIXME: ugly */
+            store->locksHeld.insert(missing.begin(), missing.end()); /* FIXME: ugly */
             copyPaths(ref<Store>(sshStore), store, missing, NoRepair, NoCheckSigs, substitute);
         }
 
diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index 0b0a0f7b1790..b94bc597b013 100644
--- a/src/libexpr/eval.cc
+++ b/src/libexpr/eval.cc
@@ -404,7 +404,7 @@ Path EvalState::toRealPath(const Path & path, const PathSet & context)
 };
 
 
-void EvalState::addConstant(const string & name, Value & v)
+Value * EvalState::addConstant(const string & name, Value & v)
 {
     Value * v2 = allocValue();
     *v2 = v;
@@ -412,12 +412,18 @@ void EvalState::addConstant(const string & name, Value & v)
     baseEnv.values[baseEnvDispl++] = v2;
     string name2 = string(name, 0, 2) == "__" ? string(name, 2) : name;
     baseEnv.values[0]->attrs->push_back(Attr(symbols.create(name2), v2));
+    return v2;
 }
 
 
 Value * EvalState::addPrimOp(const string & name,
     unsigned int arity, PrimOpFun primOp)
 {
+    if (arity == 0) {
+        Value v;
+        primOp(*this, noPos, nullptr, v);
+        return addConstant(name, v);
+    }
     Value * v = allocValue();
     string name2 = string(name, 0, 2) == "__" ? string(name, 2) : name;
     Symbol sym = symbols.create(name2);
@@ -1700,10 +1706,13 @@ void EvalState::printStats()
     printMsg(v, format("  time elapsed: %1%") % cpuTime);
     printMsg(v, format("  size of a value: %1%") % sizeof(Value));
     printMsg(v, format("  size of an attr: %1%") % sizeof(Attr));
-    printMsg(v, format("  environments allocated: %1% (%2% bytes)") % nrEnvs % bEnvs);
-    printMsg(v, format("  list elements: %1% (%2% bytes)") % nrListElems % bLists);
+    printMsg(v, format("  environments allocated count: %1%") % nrEnvs);
+    printMsg(v, format("  environments allocated bytes: %1%") % bEnvs);
+    printMsg(v, format("  list elements count: %1%") % nrListElems);
+    printMsg(v, format("  list elements bytes: %1%") % bLists);
     printMsg(v, format("  list concatenations: %1%") % nrListConcats);
-    printMsg(v, format("  values allocated: %1% (%2% bytes)") % nrValues % bValues);
+    printMsg(v, format("  values allocated count: %1%") % nrValues);
+    printMsg(v, format("  values allocated bytes: %1%") % bValues);
     printMsg(v, format("  sets allocated: %1% (%2% bytes)") % nrAttrsets % bAttrsets);
     printMsg(v, format("  right-biased unions: %1%") % nrOpUpdates);
     printMsg(v, format("  values copied in right-biased unions: %1%") % nrOpUpdateValuesCopied);
diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh
index 9e3d30d95f49..51905d7e1c62 100644
--- a/src/libexpr/eval.hh
+++ b/src/libexpr/eval.hh
@@ -210,7 +210,7 @@ private:
 
     void createBaseEnv();
 
-    void addConstant(const string & name, Value & v);
+    Value * addConstant(const string & name, Value & v);
 
     Value * addPrimOp(const string & name,
         unsigned int arity, PrimOpFun primOp);
diff --git a/src/libexpr/names.cc b/src/libexpr/names.cc
index 6d78d2116121..382088c78872 100644
--- a/src/libexpr/names.cc
+++ b/src/libexpr/names.cc
@@ -41,7 +41,7 @@ bool DrvName::matches(DrvName & n)
 }
 
 
-static string nextComponent(string::const_iterator & p,
+string nextComponent(string::const_iterator & p,
     const string::const_iterator end)
 {
     /* Skip any dots and dashes (component separators). */
diff --git a/src/libexpr/names.hh b/src/libexpr/names.hh
index 9667fc96fd0f..13c3093e77b0 100644
--- a/src/libexpr/names.hh
+++ b/src/libexpr/names.hh
@@ -24,6 +24,8 @@ private:
 
 typedef list<DrvName> DrvNames;
 
+string nextComponent(string::const_iterator & p,
+    const string::const_iterator end);
 int compareVersions(const string & v1, const string & v2);
 DrvNames drvNamesFromArgs(const Strings & opArgs);
 
diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc
index 466fd13e8698..ca97b2b28bb6 100644
--- a/src/libexpr/primops.cc
+++ b/src/libexpr/primops.cc
@@ -1961,6 +1961,26 @@ static void prim_compareVersions(EvalState & state, const Pos & pos, Value * * a
 }
 
 
+static void prim_splitVersion(EvalState & state, const Pos & pos, Value * * args, Value & v)
+{
+    string version = state.forceStringNoCtx(*args[0], pos);
+    auto iter = version.cbegin();
+    Strings components;
+    while (iter != version.cend()) {
+        auto component = nextComponent(iter, version.cend());
+        if (component.empty())
+            break;
+        components.emplace_back(std::move(component));
+    }
+    state.mkList(v, components.size());
+    unsigned int n = 0;
+    for (auto & component : components) {
+        auto listElem = v.listElems()[n++] = state.allocValue();
+        mkString(*listElem, std::move(component));
+    }
+}
+
+
 /*************************************************************
  * Networking
  *************************************************************/
@@ -2196,6 +2216,7 @@ void EvalState::createBaseEnv()
     // Versions
     addPrimOp("__parseDrvName", 1, prim_parseDrvName);
     addPrimOp("__compareVersions", 2, prim_compareVersions);
+    addPrimOp("__splitVersion", 1, prim_splitVersion);
 
     // Derivations
     addPrimOp("derivationStrict", 1, prim_derivationStrict);
diff --git a/src/libexpr/primops.hh b/src/libexpr/primops.hh
index 39d23b04a5ce..31bf3f84f6c7 100644
--- a/src/libexpr/primops.hh
+++ b/src/libexpr/primops.hh
@@ -9,6 +9,9 @@ struct RegisterPrimOp
 {
     typedef std::vector<std::tuple<std::string, size_t, PrimOpFun>> PrimOps;
     static PrimOps * primOps;
+    /* You can register a constant by passing an arity of 0. fun
+       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);
 };
 
diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc
index 90a4867163df..7d888202bbf1 100644
--- a/src/libmain/shared.cc
+++ b/src/libmain/shared.cc
@@ -262,6 +262,7 @@ void printVersion(const string & programName)
 void showManPage(const string & name)
 {
     restoreSignals();
+    setenv("MANPATH", settings.nixManDir.c_str(), 1);
     execlp("man", "man", name.c_str(), NULL);
     throw SysError(format("command 'man %1%' failed") % name.c_str());
 }
diff --git a/src/libmain/shared.hh b/src/libmain/shared.hh
index 1dcc4f0ac942..8e4861232db5 100644
--- a/src/libmain/shared.hh
+++ b/src/libmain/shared.hh
@@ -22,6 +22,7 @@ public:
 
 int handleExceptions(const string & programName, std::function<void()> fun);
 
+/* Don't forget to call initPlugins() after settings are initialized! */
 void initNix();
 
 void parseCmdLine(int argc, char * * argv,
diff --git a/src/libstore/build.cc b/src/libstore/build.cc
index 392b494e65eb..9b7abaa3d1ee 100644
--- a/src/libstore/build.cc
+++ b/src/libstore/build.cc
@@ -49,7 +49,9 @@
 #include <sys/param.h>
 #include <sys/mount.h>
 #include <sys/syscall.h>
+#if HAVE_SECCOMP
 #include <seccomp.h>
+#endif
 #define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old))
 #endif
 
@@ -1335,19 +1337,6 @@ void DerivationGoal::tryToBuild()
 {
     trace("trying to build");
 
-    /* Check for the possibility that some other goal in this process
-       has locked the output since we checked in haveDerivation().
-       (It can't happen between here and the lockPaths() call below
-       because we're not allowing multi-threading.)  If so, put this
-       goal to sleep until another goal finishes, then try again. */
-    for (auto & i : drv->outputs)
-        if (pathIsLockedByMe(worker.store.toRealPath(i.second.path))) {
-            debug(format("putting derivation '%1%' to sleep because '%2%' is locked by another goal")
-                % drvPath % i.second.path);
-            worker.waitForAnyGoal(shared_from_this());
-            return;
-        }
-
     /* Obtain locks on all output paths.  The locks are automatically
        released when we exit this function or Nix crashes.  If we
        can't acquire the lock, then continue; hopefully some other
@@ -2482,7 +2471,7 @@ void DerivationGoal::chownToBuilder(const Path & path)
 
 void setupSeccomp()
 {
-#if __linux__
+#if __linux__ && HAVE_SECCOMP
     if (!settings.filterSyscalls) return;
 
     scmp_filter_ctx ctx;
@@ -3739,6 +3728,17 @@ void SubstitutionGoal::tryToRun()
         return;
     }
 
+    /* If the store path is already locked (probably by a
+       DerivationGoal), then put this goal to sleep. Note: we don't
+       acquire a lock here since that breaks addToStore(), so below we
+       handle an AlreadyLocked exception from addToStore(). The check
+       here is just an optimisation to prevent having to redo a
+       download due to a locked path. */
+    if (pathIsLockedByMe(worker.store.toRealPath(storePath))) {
+        worker.waitForAWhile(shared_from_this());
+        return;
+    }
+
     maintainRunningSubstitutions = std::make_unique<MaintainCount<uint64_t>>(worker.runningSubstitutions);
     worker.updateProgress();
 
@@ -3778,6 +3778,12 @@ void SubstitutionGoal::finished()
 
     try {
         promise.get_future().get();
+    } catch (AlreadyLocked & e) {
+        /* Probably a DerivationGoal is already building this store
+           path. Sleep for a while and try again. */
+        state = &SubstitutionGoal::init;
+        worker.waitForAWhile(shared_from_this());
+        return;
     } catch (Error & e) {
         printError(e.msg());
 
diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc
index d3c96ddd6e61..247040606fb7 100644
--- a/src/libstore/globals.cc
+++ b/src/libstore/globals.cc
@@ -6,6 +6,7 @@
 #include <algorithm>
 #include <map>
 #include <thread>
+#include <dlfcn.h>
 
 
 namespace nix {
@@ -37,6 +38,7 @@ Settings::Settings()
     , nixConfDir(canonPath(getEnv("NIX_CONF_DIR", NIX_CONF_DIR)))
     , nixLibexecDir(canonPath(getEnv("NIX_LIBEXEC_DIR", NIX_LIBEXEC_DIR)))
     , nixBinDir(canonPath(getEnv("NIX_BIN_DIR", NIX_BIN_DIR)))
+    , nixManDir(canonPath(NIX_MAN_DIR))
     , nixDaemonSocketFile(canonPath(nixStateDir + DEFAULT_SOCKET_PATH))
 {
     buildUsersGroup = getuid() == 0 ? "nixbld" : "";
@@ -137,4 +139,30 @@ void MaxBuildJobsSetting::set(const std::string & str)
         throw UsageError("configuration setting '%s' should be 'auto' or an integer", name);
 }
 
+
+void initPlugins()
+{
+    for (const auto & pluginFile : settings.pluginFiles.get()) {
+        Paths pluginFiles;
+        try {
+            auto ents = readDirectory(pluginFile);
+            for (const auto & ent : ents)
+                pluginFiles.emplace_back(pluginFile + "/" + ent.name);
+        } catch (SysError & e) {
+            if (e.errNo != ENOTDIR)
+                throw;
+            pluginFiles.emplace_back(pluginFile);
+        }
+        for (const auto & file : pluginFiles) {
+            /* handle is purposefully leaked as there may be state in the
+               DSO needed by the action of the plugin. */
+            void *handle =
+                dlopen(file.c_str(), RTLD_LAZY | RTLD_LOCAL);
+            if (!handle)
+                throw Error("could not dynamically open plugin file '%s%': %s%", file, dlerror());
+        }
+    }
+}
+
+
 }
diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh
index 20ac8fe4e9ae..fd3fbec9f017 100644
--- a/src/libstore/globals.hh
+++ b/src/libstore/globals.hh
@@ -82,6 +82,9 @@ public:
     /* The directory where the main programs are stored. */
     Path nixBinDir;
 
+    /* The directory where the man pages are stored. */
+    Path nixManDir;
+
     /* File name of the socket the daemon listens to.  */
     Path nixDaemonSocketFile;
 
@@ -367,12 +370,19 @@ public:
 
     Setting<Strings> allowedUris{this, {}, "allowed-uris",
         "Prefixes of URIs that builtin functions such as fetchurl and fetchGit are allowed to fetch."};
+
+    Setting<Paths> pluginFiles{this, {}, "plugin-files",
+        "Plugins to dynamically load at nix initialization time."};
 };
 
 
 // FIXME: don't use a global variable.
 extern Settings settings;
 
+/* This should be called after settings are initialized, but before
+   anything else */
+void initPlugins();
+
 
 extern const string nixVersion;
 
diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc
index 7afecc1cfc62..4afe51ea91ec 100644
--- a/src/libstore/local-store.cc
+++ b/src/libstore/local-store.cc
@@ -992,8 +992,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, const ref<std::string> &
         /* Lock the output path.  But don't lock if we're being called
            from a build hook (whose parent process already acquired a
            lock on this path). */
-        Strings locksHeld = tokenizeString<Strings>(getEnv("NIX_HELD_LOCKS"));
-        if (find(locksHeld.begin(), locksHeld.end(), info.path) == locksHeld.end())
+        if (!locksHeld.count(info.path))
             outputLock.lockPaths({realPath});
 
         if (repair || !isValidPath(info.path)) {
diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh
index 30bef3a799d4..bbd50e1c1451 100644
--- a/src/libstore/local-store.hh
+++ b/src/libstore/local-store.hh
@@ -104,6 +104,9 @@ private:
 
 public:
 
+    // Hack for build-remote.cc.
+    PathSet locksHeld = tokenizeString<PathSet>(getEnv("NIX_HELD_LOCKS"));
+
     /* Initialise the local store, upgrading the schema if
        necessary. */
     LocalStore(const Params & params);
diff --git a/src/libstore/local.mk b/src/libstore/local.mk
index 50c46ce6fe99..e11efa5c2b54 100644
--- a/src/libstore/local.mk
+++ b/src/libstore/local.mk
@@ -9,6 +9,9 @@ libstore_SOURCES := $(wildcard $(d)/*.cc)
 libstore_LIBS = libutil libformat
 
 libstore_LDFLAGS = $(SQLITE3_LIBS) -lbz2 $(LIBCURL_LIBS) $(SODIUM_LIBS) -pthread
+ifneq ($(OS), FreeBSD)
+ libstore_LDFLAGS += -ldl
+endif
 
 libstore_FILES = sandbox-defaults.sb sandbox-minimal.sb sandbox-network.sb
 
@@ -22,7 +25,7 @@ ifeq ($(OS), SunOS)
 	libstore_LDFLAGS += -lsocket
 endif
 
-ifeq ($(OS), Linux)
+ifeq ($(HAVE_SECCOMP), 1)
 	libstore_LDFLAGS += -lseccomp
 endif
 
@@ -35,6 +38,7 @@ libstore_CXXFLAGS = \
  -DNIX_CONF_DIR=\"$(sysconfdir)/nix\" \
  -DNIX_LIBEXEC_DIR=\"$(libexecdir)\" \
  -DNIX_BIN_DIR=\"$(bindir)\" \
+ -DNIX_MAN_DIR=\"$(mandir)\" \
  -DSANDBOX_SHELL="\"$(sandbox_shell)\"" \
  -DLSOF=\"$(lsof)\"
 
diff --git a/src/libstore/pathlocks.cc b/src/libstore/pathlocks.cc
index 587f29598851..08d1efdbeb01 100644
--- a/src/libstore/pathlocks.cc
+++ b/src/libstore/pathlocks.cc
@@ -113,8 +113,10 @@ bool PathLocks::lockPaths(const PathSet & _paths,
 
         {
             auto lockedPaths(lockedPaths_.lock());
-            if (lockedPaths->count(lockPath))
-                throw Error("deadlock: trying to re-acquire self-held lock '%s'", lockPath);
+            if (lockedPaths->count(lockPath)) {
+                if (!wait) return false;
+                throw AlreadyLocked("deadlock: trying to re-acquire self-held lock '%s'", lockPath);
+            }
             lockedPaths->insert(lockPath);
         }
 
diff --git a/src/libstore/pathlocks.hh b/src/libstore/pathlocks.hh
index 2a7de611446e..db51f950a320 100644
--- a/src/libstore/pathlocks.hh
+++ b/src/libstore/pathlocks.hh
@@ -2,10 +2,8 @@
 
 #include "util.hh"
 
-
 namespace nix {
 
-
 /* Open (possibly create) a lock file and return the file descriptor.
    -1 is returned if create is false and the lock could not be opened
    because it doesn't exist.  Any other error throws an exception. */
@@ -18,6 +16,7 @@ enum LockType { ltRead, ltWrite, ltNone };
 
 bool lockFile(int fd, LockType lockType, bool wait);
 
+MakeError(AlreadyLocked, Error);
 
 class PathLocks
 {
@@ -38,9 +37,6 @@ public:
     void setDeletion(bool deletePaths);
 };
 
-
-// FIXME: not thread-safe!
 bool pathIsLockedByMe(const Path & path);
 
-
 }
diff --git a/src/libutil/config.cc b/src/libutil/config.cc
index d46ca65a3863..0e502769edf8 100644
--- a/src/libutil/config.cc
+++ b/src/libutil/config.cc
@@ -80,7 +80,31 @@ void Config::applyConfigFile(const Path & path, bool fatal)
             vector<string> tokens = tokenizeString<vector<string> >(line);
             if (tokens.empty()) continue;
 
-            if (tokens.size() < 2 || tokens[1] != "=")
+            if (tokens.size() < 2)
+                throw UsageError("illegal configuration line '%1%' in '%2%'", line, path);
+
+            auto include = false;
+            auto ignoreMissing = false;
+            if (tokens[0] == "include")
+                include = true;
+            else if (tokens[0] == "!include") {
+                include = true;
+                ignoreMissing = true;
+            }
+
+            if (include) {
+                if (tokens.size() != 2)
+                    throw UsageError("illegal configuration line '%1%' in '%2%'", line, path);
+                auto p = absPath(tokens[1], dirOf(path));
+                if (pathExists(p)) {
+                    applyConfigFile(p, fatal);
+                } else if (!ignoreMissing) {
+                    throw Error("file '%1%' included from '%2%' not found", p, path);
+                }
+                continue;
+            }
+
+            if (tokens[1] != "=")
                 throw UsageError("illegal configuration line '%1%' in '%2%'", line, path);
 
             string name = tokens[0];
diff --git a/src/libutil/monitor-fd.hh b/src/libutil/monitor-fd.hh
index e0ec66c01803..5ee0b88ef50f 100644
--- a/src/libutil/monitor-fd.hh
+++ b/src/libutil/monitor-fd.hh
@@ -21,13 +21,29 @@ public:
     MonitorFdHup(int fd)
     {
         thread = std::thread([fd]() {
-            /* Wait indefinitely until a POLLHUP occurs. */
-            struct pollfd fds[1];
-            fds[0].fd = fd;
-            fds[0].events = 0;
-            if (poll(fds, 1, -1) == -1) abort(); // can't happen
-            assert(fds[0].revents & POLLHUP);
-            triggerInterrupt();
+            while (true) {
+              /* Wait indefinitely until a POLLHUP occurs. */
+              struct pollfd fds[1];
+              fds[0].fd = fd;
+              /* This shouldn't be necessary, but macOS doesn't seem to
+                 like a zeroed out events field.
+                 See rdar://37537852.
+              */
+              fds[0].events = POLLHUP;
+              auto count = poll(fds, 1, -1);
+              if (count == -1) abort(); // can't happen
+              /* This shouldn't happen, but can on macOS due to a bug.
+                 See rdar://37550628.
+
+                 This may eventually need a delay or further
+                 coordination with the main thread if spinning proves
+                 too harmful.
+               */
+              if (count == 0) continue;
+              assert(fds[0].revents & POLLHUP);
+              triggerInterrupt();
+              break;
+            }
         });
     };
 
diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc
index 1581c282c75c..99f773451ffe 100755
--- a/src/nix-build/nix-build.cc
+++ b/src/nix-build/nix-build.cc
@@ -232,6 +232,8 @@ void mainWrapped(int argc, char * * argv)
 
     myArgs.parseCmdline(args);
 
+    initPlugins();
+
     if (packages && fromArgs)
         throw UsageError("'-p' and '-E' are mutually exclusive");
 
diff --git a/src/nix-channel/nix-channel.cc b/src/nix-channel/nix-channel.cc
index 370f216abccd..ec9a7174ecb9 100755
--- a/src/nix-channel/nix-channel.cc
+++ b/src/nix-channel/nix-channel.cc
@@ -213,6 +213,9 @@ int main(int argc, char ** argv)
             }
             return true;
         });
+
+        initPlugins();
+
         switch (cmd) {
             case cNone:
                 throw UsageError("no command specified");
diff --git a/src/nix-collect-garbage/nix-collect-garbage.cc b/src/nix-collect-garbage/nix-collect-garbage.cc
index cc663a96924d..37fe22f48134 100644
--- a/src/nix-collect-garbage/nix-collect-garbage.cc
+++ b/src/nix-collect-garbage/nix-collect-garbage.cc
@@ -77,6 +77,8 @@ int main(int argc, char * * argv)
             return true;
         });
 
+        initPlugins();
+
         auto profilesDir = settings.nixStateDir + "/profiles";
         if (removeOld) removeOldGenerations(profilesDir);
 
diff --git a/src/nix-copy-closure/nix-copy-closure.cc b/src/nix-copy-closure/nix-copy-closure.cc
index 861fc2e5cd64..dfb1b8fc5dc4 100755
--- a/src/nix-copy-closure/nix-copy-closure.cc
+++ b/src/nix-copy-closure/nix-copy-closure.cc
@@ -44,6 +44,8 @@ int main(int argc, char ** argv)
             return true;
         });
 
+        initPlugins();
+
         if (sshHost.empty())
             throw UsageError("no host name specified");
 
diff --git a/src/nix-daemon/nix-daemon.cc b/src/nix-daemon/nix-daemon.cc
index d3a8ebbdda50..890bffa19aa5 100644
--- a/src/nix-daemon/nix-daemon.cc
+++ b/src/nix-daemon/nix-daemon.cc
@@ -1060,6 +1060,8 @@ int main(int argc, char * * argv)
             return true;
         });
 
+        initPlugins();
+
         if (stdio) {
             if (getStoreType() == tDaemon) {
                 /* Forward on this connection to the real daemon */
diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc
index 016caf6d2346..97e66cbd937e 100644
--- a/src/nix-env/nix-env.cc
+++ b/src/nix-env/nix-env.cc
@@ -1393,6 +1393,8 @@ int main(int argc, char * * argv)
 
         myArgs.parseCmdline(argvToStrings(argc, argv));
 
+        initPlugins();
+
         if (!op) throw UsageError("no operation specified");
 
         auto store = openStore();
diff --git a/src/nix-instantiate/nix-instantiate.cc b/src/nix-instantiate/nix-instantiate.cc
index e05040a42deb..dd262bea0918 100644
--- a/src/nix-instantiate/nix-instantiate.cc
+++ b/src/nix-instantiate/nix-instantiate.cc
@@ -151,6 +151,8 @@ int main(int argc, char * * argv)
 
         myArgs.parseCmdline(argvToStrings(argc, argv));
 
+        initPlugins();
+
         if (evalOnly && !wantsReadWrite)
             settings.readOnlyMode = true;
 
diff --git a/src/nix-prefetch-url/nix-prefetch-url.cc b/src/nix-prefetch-url/nix-prefetch-url.cc
index fef3eaa45538..fa7ee254500c 100644
--- a/src/nix-prefetch-url/nix-prefetch-url.cc
+++ b/src/nix-prefetch-url/nix-prefetch-url.cc
@@ -89,6 +89,8 @@ int main(int argc, char * * argv)
 
         myArgs.parseCmdline(argvToStrings(argc, argv));
 
+        initPlugins();
+
         if (args.size() > 2)
             throw UsageError("too many arguments");
 
diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc
index 4fc3421c0dde..efef7f15c094 100644
--- a/src/nix-store/nix-store.cc
+++ b/src/nix-store/nix-store.cc
@@ -1052,6 +1052,8 @@ int main(int argc, char * * argv)
             return true;
         });
 
+        initPlugins();
+
         if (!op) throw UsageError("no operation specified");
 
         if (op != opDump && op != opRestore) /* !!! hack */
diff --git a/src/nix/build.cc b/src/nix/build.cc
index f7c99f12dbbf..b4f21b32d78f 100644
--- a/src/nix/build.cc
+++ b/src/nix/build.cc
@@ -50,7 +50,7 @@ struct CmdBuild : MixDryRun, InstallablesCommand
 
     void run(ref<Store> store) override
     {
-        auto buildables = toBuildables(store, dryRun ? DryRun : Build, installables);
+        auto buildables = build(store, dryRun ? DryRun : Build, installables);
 
         for (size_t i = 0; i < buildables.size(); ++i) {
             auto & b(buildables[i]);
diff --git a/src/nix/command.hh b/src/nix/command.hh
index a7863c49f37a..97a6fee7fd27 100644
--- a/src/nix/command.hh
+++ b/src/nix/command.hh
@@ -198,7 +198,7 @@ std::shared_ptr<Installable> parseInstallable(
     SourceExprCommand & cmd, ref<Store> store, const std::string & installable,
     bool useDefaultInstallables);
 
-Buildables toBuildables(ref<Store> store, RealiseMode mode,
+Buildables build(ref<Store> store, RealiseMode mode,
     std::vector<std::shared_ptr<Installable>> installables);
 
 PathSet toStorePaths(ref<Store> store, RealiseMode mode,
diff --git a/src/nix/installables.cc b/src/nix/installables.cc
index c3b06c22eba8..a3fdd8a2808d 100644
--- a/src/nix/installables.cc
+++ b/src/nix/installables.cc
@@ -253,7 +253,7 @@ std::shared_ptr<Installable> parseInstallable(
     return installables.front();
 }
 
-Buildables toBuildables(ref<Store> store, RealiseMode mode,
+Buildables build(ref<Store> store, RealiseMode mode,
     std::vector<std::shared_ptr<Installable>> installables)
 {
     if (mode != Build)
@@ -291,7 +291,7 @@ PathSet toStorePaths(ref<Store> store, RealiseMode mode,
 {
     PathSet outPaths;
 
-    for (auto & b : toBuildables(store, mode, installables))
+    for (auto & b : build(store, mode, installables))
         for (auto & output : b.outputs)
             outPaths.insert(output.second);
 
diff --git a/src/nix/main.cc b/src/nix/main.cc
index 8f6bbe8f51ae..bb107ec7d3f6 100644
--- a/src/nix/main.cc
+++ b/src/nix/main.cc
@@ -92,6 +92,8 @@ void mainWrapped(int argc, char * * argv)
 
     args.parseCmdline(argvToStrings(argc, argv));
 
+    initPlugins();
+
     if (!args.command) args.showHelpAndExit();
 
     Finally f([]() { stopProgressBar(); });
diff --git a/tests/init.sh b/tests/init.sh
index 41cca047d8fb..e5353598bcc4 100644
--- a/tests/init.sh
+++ b/tests/init.sh
@@ -16,7 +16,12 @@ mkdir "$NIX_CONF_DIR"
 cat > "$NIX_CONF_DIR"/nix.conf <<EOF
 build-users-group =
 keep-derivations = false
+include nix.conf.extra
+EOF
+
+cat > "$NIX_CONF_DIR"/nix.conf.extra <<EOF
 fsync-metadata = false
+!include nix.conf.extra.not-there
 EOF
 
 # Initialise the database.
diff --git a/tests/lang/eval-okay-splitversion.exp b/tests/lang/eval-okay-splitversion.exp
new file mode 100644
index 000000000000..153ceb8186a0
--- /dev/null
+++ b/tests/lang/eval-okay-splitversion.exp
@@ -0,0 +1 @@
+[ "1" "2" "3" ]
diff --git a/tests/lang/eval-okay-splitversion.nix b/tests/lang/eval-okay-splitversion.nix
new file mode 100644
index 000000000000..9e5c99d2e7f6
--- /dev/null
+++ b/tests/lang/eval-okay-splitversion.nix
@@ -0,0 +1 @@
+builtins.splitVersion "1.2.3"
diff --git a/tests/local.mk b/tests/local.mk
index e90b9f7da4ad..2a4832e1304e 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -22,7 +22,8 @@ nix_tests = \
   run.sh \
   brotli.sh \
   pure-eval.sh \
-  check.sh
+  check.sh \
+  plugins.sh
   # parallel.sh
 
 install-tests += $(foreach x, $(nix_tests), tests/$(x))
@@ -31,4 +32,4 @@ tests-environment = NIX_REMOTE= $(bash) -e
 
 clean-files += $(d)/common.sh
 
-installcheck: $(d)/common.sh
+installcheck: $(d)/common.sh $(d)/plugins/libplugintest.$(SO_EXT)
diff --git a/tests/plugins.sh b/tests/plugins.sh
new file mode 100644
index 000000000000..0fad4f773a28
--- /dev/null
+++ b/tests/plugins.sh
@@ -0,0 +1,7 @@
+source common.sh
+
+set -o pipefail
+
+res=$(nix eval '(builtins.anotherNull)' --option plugin-files $PWD/plugins/libplugintest*)
+
+[ "$res"x = "nullx" ]
diff --git a/tests/plugins/local.mk b/tests/plugins/local.mk
new file mode 100644
index 000000000000..1d2bac052fd2
--- /dev/null
+++ b/tests/plugins/local.mk
@@ -0,0 +1,9 @@
+libraries += libplugintest
+
+libplugintest_DIR := $(d)
+
+libplugintest_SOURCES := $(d)/plugintest.cc
+
+libplugintest_ALLOW_UNDEFINED := 1
+
+libplugintest_EXCLUDE_FROM_LIBRARY_LIST := 1
diff --git a/tests/plugins/plugintest.cc b/tests/plugins/plugintest.cc
new file mode 100644
index 000000000000..6b5e6d7cde21
--- /dev/null
+++ b/tests/plugins/plugintest.cc
@@ -0,0 +1,10 @@
+#include "primops.hh"
+
+using namespace nix;
+
+static void prim_anotherNull (EvalState & state, const Pos & pos, Value ** args, Value & v)
+{
+    mkNull(v);
+}
+
+static RegisterPrimOp r("anotherNull", 0, prim_anotherNull);