tvl-depot/nix/nint/nint.rs
sterni 852d059e2f feat(nix/nint): accept attribute set with stdout, stderr and exit
This extends the calling convention for nint in a non-breaking way: If
the called script returns an attribute set instead of a string the
following is done:

* If the attributes `stdout` and/or `stderr` exist, their content (which
  must be a string currently) is written to the respective output.

* If the attribute `exit` exists, nint will exit with the given exit
  code. Must be a number that can be converted to an `i32`. If it's
  missing, nint will exit without indicating an error.

Change-Id: I209cf178fee3d970fdea3b26e4049e944af47457
Reviewed-on: https://cl.tvl.fyi/c/depot/+/3547
Tested-by: BuildkiteCI
Reviewed-by: tazjin <mail@tazj.in>
2021-09-18 11:56:22 +00:00

156 lines
4.6 KiB
Rust

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, stdout, stderr};
use std::process::Command;
use std::convert::{TryFrom};
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)
}
/// Slightly overkill helper macro which takes a `Map<String, Value>` obtained
/// from `Value::Object` and an output name (`stderr` or `stdout`) as an
/// identifier. If a value exists for the given output in the object it gets
/// written to the appropriate output.
macro_rules! handle_set_output {
($map_name:ident, $output_name:ident) => {
match $map_name.get(stringify!($output_name)) {
Some(Value::String(s)) =>
$output_name().write_all(s.as_bytes()),
Some(_) => Err(
Error::new(
ErrorKind::Other,
format!("Attribute {} must be a string!", stringify!($output_name)),
)
),
None => Ok(()),
}
}
}
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("--strict"));
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)) => stdout().write_all(s.as_bytes()),
Ok(Value::Object(m)) => {
handle_set_output!(m, stdout)?;
handle_set_output!(m, stderr)?;
match m.get("exit") {
Some(Value::Number(n)) => {
let code = n.as_i64().and_then(|v| i32::try_from(v).ok());
match code {
Some(i) => std::process::exit(i),
None => Err(
Error::new(
ErrorKind::Other,
"Attribute exit is not an i32"
)
),
}
},
Some(_) => Err(
Error::new(ErrorKind::Other, "exit must be a number")
),
None => Ok(()),
}
},
Ok(_) => Err(Error::new(ErrorKind::Other, "output must be a string or an object")),
_ => {
stderr().write_all(&run.stderr[..]);
Err(Error::new(ErrorKind::Other, "internal nix error"))
},
}
}
}