shlevy / nix-exec

Run programs defined in nix expressions
MIT License
50 stars 6 forks source link

nix-exec

nix-exec is a tool to run programs defined in nix expressions. It has two major goals:

nix-exec is designed to have a minimal interface to keep it usable in as wide of a context as possible.

Invocation

$ 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.

Expression entry point

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.

IO monad

nix-exec provides a monad for defining programs which it executes. The lib argument contains the monad functions:

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.

dlopen

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.

Configuration settings

The configuration attribute in the nix-exec lib argument is a set containing the following information about the compile-time configuration of nix-exec:

unsafe-perform-io

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.

fetchgit

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:

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.

reexec

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.

Global symbols

nix-exec defines a number of external variables in the C header <nix-exec.h> to introspect the execution environment:

In addition, symbols defined in libnixmain, libnixexpr, and libnixstore are all available.

unsafe-lib.nix

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.

API stability

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.

Examples

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)