tvl-depot/nix/nint/nint.rs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

111 lines
2.9 KiB
Rust
Raw Normal View History

feat(sterni/nint): shebang interpreter for nix scripts nint (short for nix interpreter) is a tiny wrapper around nix-instantiate which allows to run nix scripts, i. e. nix expressions that conform to a certain calling convention. A nix script runnable using nint must conform to the following constraints: * It must evaluate to a function which has a set pattern with an ellipsis as the single argument. * It must produce a string as a return value or fail. When invoked, a the expression receives the following arguments: * `currentDir`: the current working directory as a nix path * `argv`: a list of strings containing `argv` including `argv[0]` * extra arguments which are manually specified which allows for passing along dependencies or libraries, for example: nint --arg depot '(import /depot {})' my-prog.nix [ argv[1] … ] would pass along depot to be used in `my-prog.nix`. Such nix scripts are purely functional in a sense: The way inputs can be taken is very limited and causing effects is also only possible in a very limited sense (using builtins.fetchurl if TARBALL_TTL is 0, adding files and directories to the nix store, realising derivations). As an approximation, a program executed using nint can be thought of as a function with the following signature: λ :: environment → working directory → argv → stdout where environment includes: * the time at the start of the program (`builtins.currentTime`) * other information about the machine (`builtins.currentSystem` …) * environment variables (`builtins.getEnv`) * the file system (`builtins.readDir`, `builtins.readFile`, …) which is the biggest input impurity as it may change during evaluation Additionally import from derivation and builtin fetchers are available which introduce further impurities to be utilized. Future work: * Streaming I/O via lazy lists. This would allow usage of stdin and output before the program terminates. However this would require using libexpr directly or writing a custom nix interpreter. A description of how this would work can be found on the website of the esoteric programming language Lazy K: https://tromp.github.io/cl/lazy-k.html * An effect system beyond stdin / stdout. * Better error handling, support setting exit codes etc. These features would require either using an alternative or custom interpreter for nix (tvix or hnix) or to link against libexpr directly to have more control over evaluation. Change-Id: I61528516eb418740df355852f23425acc4d0656a Reviewed-on: https://cl.tvl.fyi/c/depot/+/2745 Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI
2021-02-20 18:02:02 +01:00
extern crate serde_json;
use serde_json::Value;
use std::ffi::OsString;
use std::os::unix::ffi::{OsStringExt, OsStrExt};
use std::io::{Error, ErrorKind, Write};
use std::process::Command;
fn render_nix_string(s: &OsString) -> OsString {
let mut rendered = Vec::new();
rendered.extend(b"\"");
for b in s.as_os_str().as_bytes() {
match char::from(*b) {
'\"' => rendered.extend(b"\\\""),
'\\' => rendered.extend(b"\\\\"),
'$' => rendered.extend(b"\\$"),
_ => rendered.push(*b),
}
}
rendered.extend(b"\"");
OsString::from_vec(rendered)
}
fn render_nix_list(arr: &[OsString]) -> OsString {
let mut rendered = Vec::new();
rendered.extend(b"[ ");
for el in arr {
rendered.extend(render_nix_string(el).as_os_str().as_bytes());
rendered.extend(b" ");
}
rendered.extend(b"]");
OsString::from_vec(rendered)
}
fn main() -> std::io::Result<()> {
let mut nix_args = Vec::new();
let mut args = std::env::args_os().into_iter();
let mut in_args = true;
let mut argv: Vec<OsString> = Vec::new();
// skip argv[0]
args.next();
loop {
let arg = match args.next() {
Some(a) => a,
None => break,
};
if !arg.to_str().map(|s| s.starts_with("-")).unwrap_or(false) {
in_args = false;
}
if in_args {
match(arg.to_str()) {
Some("--arg") | Some("--argstr") => {
nix_args.push(arg);
nix_args.push(args.next().unwrap());
nix_args.push(args.next().unwrap());
Ok(())
}
_ => Err(Error::new(ErrorKind::Other, "unknown argument")),
}?
} else {
argv.push(arg);
}
}
if argv.len() < 1 {
Err(Error::new(ErrorKind::Other, "missing argv"))
} else {
let cd = std::env::current_dir()?.into_os_string();
nix_args.push(OsString::from("--arg"));
nix_args.push(OsString::from("currentDir"));
nix_args.push(cd);
nix_args.push(OsString::from("--arg"));
nix_args.push(OsString::from("argv"));
nix_args.push(render_nix_list(&argv[..]));
nix_args.push(OsString::from("--eval"));
nix_args.push(OsString::from("--json"));
nix_args.push(argv[0].clone());
let run = Command::new("nix-instantiate")
.args(nix_args)
.output()?;
match serde_json::from_slice(&run.stdout[..]) {
Ok(Value::String(s)) => Ok(print!("{}", s)),
Ok(_) => Err(Error::new(ErrorKind::Other, "output must be a string")),
_ => {
std::io::stderr().write_all(&run.stderr[..]);
Err(Error::new(ErrorKind::Other, "internal nix error"))
},
}
}
}