From 318d10e60875ef69132651668a249de75730d2ba Mon Sep 17 00:00:00 2001 From: sterni Date: Thu, 9 Sep 2021 18:26:03 +0200 Subject: chore(nint): move from //users/sterni to //nix Since //web/bubblegum depends on nint, we need to move it to a non user directory to conform with the policy established via cl/3434. Note that this likely doesn't mean greater stability (which isn't really implied in depot anyways), since I still would like to use a more elaborate calling convention to allow for additional useful features. Change-Id: I616f905d8df13e3363674aab69a797b0d39fdd79 Reviewed-on: https://cl.tvl.fyi/c/depot/+/3506 Tested-by: BuildkiteCI Reviewed-by: tazjin --- nix/nint/OWNERS | 3 ++ nix/nint/README.md | 85 +++++++++++++++++++++++++++++++++++++++ nix/nint/default.nix | 14 +++++++ nix/nint/nint.rs | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 nix/nint/OWNERS create mode 100644 nix/nint/README.md create mode 100644 nix/nint/default.nix create mode 100644 nix/nint/nint.rs (limited to 'nix/nint') diff --git a/nix/nint/OWNERS b/nix/nint/OWNERS new file mode 100644 index 000000000000..f16dd105d761 --- /dev/null +++ b/nix/nint/OWNERS @@ -0,0 +1,3 @@ +inherited: true +owners: + - sterni diff --git a/nix/nint/README.md b/nix/nint/README.md new file mode 100644 index 000000000000..ddd8045f7378 --- /dev/null +++ b/nix/nint/README.md @@ -0,0 +1,85 @@ +# nint — Nix INTerpreter + +`nint` is a shebang compatible interpreter for nix. It is currently +implemented as a fairly trivial wrapper around `nix-instantiate --eval`. +It allows to run nix expressions as command line tools if they conform +to the following calling convention: + +* Every nix script needs to evaluate to a function which takes an + attribute set as its single argument. Ideally a set pattern with + an ellipsis should be used. By default `nint` passes the following + arguments: + + * `currentDir`: the current working directory as a nix path + * `argv`: a list of arguments to the invokation including the + program name at `builtins.head argv`. + * Extra arguments can be manually passed as described below. + +* The return value should always be a string (throwing is also okay) + which is printed to stdout by `nint`. + +## Usage + +``` +nint [ --arg ARG VALUE … ] script.nix [ ARGS … ] +``` + +Instead of `--arg`, `--argstr` can also be used. They both work +like the flags of the same name for `nix-instantiate` and may +be specified any number of times as long as they are passed +*before* the nix expression to run. + +Below is a shebang which also passes `depot` as an argument +(note the usage of `env -S` to get around the shebang limitation +to two arguments). + +```nix +#!/usr/bin/env -S nint --arg depot /path/to/depot +``` + +## Limitations + +* No side effects except for writing to `stdout`. + +* Output is not streaming, i. e. even if the output is incrementally + calculated, nothing will be printed until the full output is available. + With plain nix strings we can't do better anyways. + +* Limited error handling for the script, no way to set the exit code etc. + +Some of these limitations may be possible to address in the future by using +an alternative nix interpreter and a more elaborate calling convention. + +## Example + +Below is a (very simple) implementation of a `ls(1)`-like program in nix: + +```nix +#!/usr/bin/env nint +{ currentDir, argv, ... }: + +let + lib = import ; + + dirs = + let + args = builtins.tail argv; + in + if args == [] + then [ currentDir ] + else args; + + makeAbsolute = p: + if builtins.isPath p + then p + else if builtins.match "^/.*" p != null + then p + else "${toString currentDir}/${p}"; +in + + lib.concatStringsSep "\n" + (lib.flatten + (builtins.map + (d: (builtins.attrNames (builtins.readDir (makeAbsolute d)))) + dirs)) + "\n" +``` diff --git a/nix/nint/default.nix b/nix/nint/default.nix new file mode 100644 index 000000000000..5cf83d15d6f3 --- /dev/null +++ b/nix/nint/default.nix @@ -0,0 +1,14 @@ +{ depot, pkgs, ... }: + +let + inherit (depot.nix.writers) + rustSimpleBin + ; +in + + rustSimpleBin { + name = "nint"; + dependencies = [ + depot.third_party.rust-crates.serde_json + ]; + } (builtins.readFile ./nint.rs) diff --git a/nix/nint/nint.rs b/nix/nint/nint.rs new file mode 100644 index 000000000000..3d430612851a --- /dev/null +++ b/nix/nint/nint.rs @@ -0,0 +1,110 @@ +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 = 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")) + }, + } + } +} -- cgit 1.4.1