nix-exec
is a tool to run programs defined in nix expressions. It has
two major goals:
- Provide a common framework for defining libraries for tools that require
complex interactions with
nix
, such ashydra
andnixops
(and arguablynix-env
andnixos-rebuild
), both to increase ease of development and to encourage reusability - Define a basic minimal bootstrapping environment for
nix
-using tools. For example, usingnixops
currently requires you to installpython
and severalpython
libraries, andhydra
requires aperl
web framework. If both were rewritten to benix-exec
programs, a system containing onlynix-exec
and the top-level scripts forhydra
andnixops
could run either without any futher manual installation.
nix-exec
is designed to have a minimal interface to keep it usable in as
wide of a context as possible.
$ nix-exec SCRIPT [ARGS...]
nix-exec
is meant to be invoked on a nix
script, with an optional set of
arguments. Any arguments recognized by nix
passed before the script name
(such as --verbose
) will be used to initialize nix
. If the script name
starts with a -
, the --
argument can be used to signify the end of
arguments that should be possibly passed to nix
.
nix-exec
is designed to be usable in a shebang.
The top-level script should evaluate to a function of a single argument
that returns a nix-exec
IO value (see below). The argument will be an
attribute set containing a list args
of the arguments passed to the script
(including the script name) and an attribute set lib
containing the IO
monad functions and nix-exec
's configuration settings.
nix-exec
provides a monad
for defining programs which it executes. The lib
argument contains the monad functions:
unit
(AKAreturn
) :: a -> m a: Bring a value into the monadmap
(AKAfmap
) :: (a -> b) -> m a -> m b: Apply a function to a monadic valuejoin
:: m m a -> m a: 'Flatten' a nested monadic value
For Haskell programmers, note that this is the
'map and join'
definition of a monad, and that the familiar >>=
can be defined in terms of
map
and join
.
In addition, the nix-exec
lib
argument contains a dlopen
function to
allow native code to be executed when running the IO
value. dlopen
takes
three arguments, filename
, symbol
, and args
.
When running a monadic value resulting from a call dlopen
, nix-exec
will
dynamically load the file at filename
, load a nix::PrimOpFun
from the DSO
at symbol symbol
, and pass the values in the args
list to the PrimOpFun
.
PrimOpFun
is defined in <nix/eval.hh>
.
The filename
argument can be the result of a derivation, in which case
nix-exec
will build the derivation before trying to dynamically load it.
Note that the PrimOpFun
must return a value that is properly forced, i.e.
not a thunk or an un-called function application.
The configuration
attribute in the nix-exec
lib
argument is a set
containing the following information about the compile-time configuration
of nix-exec
:
prefix
: The installation prefixdatadir
: The data directoryversion.major
: The major version numberversion.minor
: The minor version numberversion.patchlevel
: The version patchlevel.version.prelevel
: If present, the version pre-release level
The builtins
attribute in the nix-exec
lib contains contains an
unsafe-perform-io
attribute that is a function that takes an IO value, runs
it, and returns the produced value. It has largely similar pitfalls to Haskell's
unsafePerformIO
function.
For bootstrapping purposes, the builtins
attribute in the nix-exec
lib
contains a fetchgit
attribute that is a function that takes a set with the
following arguments:
url
: The URL of the repositoryrev
: The desired revisionfetchSubmodules
: Whether to fetch submodules (defaulttrue
)cache-dir
: The directory to cache repos and archives in (default$HOME/.cache/fetchgit
).
When called, fetchgit
returns an IO value that, when run, checks out
the given revision of the given git repository into a directory and yields a
path
pointing to that directory.
For bootstrapping purposes, the builtins
attribute in the nix-exec
lib
contains a reexec
attribute that is a function that takes a path to a
nix-exec
binary and returns an IO value that, when run, reexecutes itself with
the passed in path if and only if the path is different than how nix-exec
was
originally executed, and yields null otherwise. This allows the use of a fixed
version of nix-exec
and its dependencies (especially nix
), though of course
the path to nix-exec
itself must be evaluatable with the host version of
nix-exec
.
nix-exec
defines a number of external variables in the C header
<nix-exec.h>
to introspect the execution environment:
nixexec_argc
: The number of arguments passed tonix-exec
nixexec_argv
: A NULL-terminated list of arguments passed tonix-exec
In addition, symbols defined in libnixmain
, libnixexpr
, and libnixstore
are all available.
For cases where the expression author doesn't completely control the invocation
of the evaluator (e.g. nixops
has no way to specify that it should run
nix-exec
), nix-exec
installs unsafe-lib.nix
in $(datadir)/nix
. Importing
this file evaluates to the lib
set passed to normal nix-exec
programs. This
uses builtins.importNative
under the hood, so it requires the
allow-unsafe-native-code-during-evaluation
nix option to be set to true.
Note that when using unsafe-lib.nix
, nixexec_argc
will be 0
and
nixexec_argv
will be NULL
unless called within an actual nix-exec
invocation.
The nix::PrimOpFun
API is not necessarily stable from version to version of
nix
. As such, scripts should inspect builtins.nixVersion
to ensure that
loaded dynamic objects are compatible.
This prints out the arguments passed to it, one per line:
#!/usr/bin/env nix-exec
{ args, lib }: let
pkgs = import <nixpkgs> {};
print-args-src = builtins.toFile "print-args.cc" ''
#include <iostream>
#include <eval.hh>
#include <eval-inline.hh>
extern "C" void print(nix::EvalState & state, const nix::Pos & pos, nix::Value ** args, nix::Value & v) {
state.forceList(*args[0], pos);
for (unsigned int index = 0; index < args[0]->list.length; ++index) {
auto str = state.forceStringNoCtx(*args[0]->list.elems[index], pos);
std::cout << str << std::endl;
}
v.type = nix::tNull;
}
'';
print-args-so = pkgs.runCommand "print-args.so" {} ''
c++ -shared -fPIC -I${pkgs.nixUnstable}/include/nix -I${pkgs.boehmgc}/include -std=c++11 -O3 ${print-args-src} -o $out
strip -S $out
'';
printArgs = args: lib.dlopen print-args-so "print" [ args ];
in printArgs args
Sketch of what nix-exec
-based nixops
might look like:
#!/usr/bin/env nix-exec
{ args, lib }: let
nixops-src = lib.builtins.fetchgit { url = git://github.com/NixOS/nix-exec.git; rev = "v3.0.4"; };
nixops-import = lib.join (lib.map (src: import src lib) nixops-src);
in lib.join (lib.map (nixops: let
lib = nixops.lib.nix-exec;
processed = nixops.process-args args;
info = lib.bind processed (args: nixops.query-db args.uuid);
drv = info: nixops.eval-network info.expr info.args info.nix-path;
build = info: drv: lib.mapM (host: nixops.build-remote drv.${host} host) info.hosts;
activate = info: drv: lib.mapM (host: nixops.activate drv.${host} host) info.hosts;
in lib.bind info (info: lib.bind (drv info) (drv: lib.bind (build info drv) (results:
if lib.all-success results then lib.bind (activate info drv) (results: if lib.all-success results then nixops.exit 0 else nixops.exit 1) else nixops.exit 1
)))) nixops-import)