use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use anyhow::{bail, Context, Result};
pub fn evaluate_json(path: &Path) -> Result<serde_json::Value> {
let output = run_eval_json(path)?;
serde_json::from_slice(&output)
.with_context(|| format!("Pkl evaluator did not emit JSON for {}", path.display()))
}
fn run_eval_json(path: &Path) -> Result<Vec<u8>> {
let path_arg = path.to_string_lossy().to_string();
let mut attempts = Vec::new();
if let Ok(bin) = std::env::var("NEX_PKL") {
attempts.push(PklCommand::eval_json(bin, path_arg.clone()));
}
if let Some(bin) = bundled_pkl_path() {
attempts.push(PklCommand::eval_json(
bin.display().to_string(),
path_arg.clone(),
));
}
attempts.push(PklCommand::eval_json("pkl".to_string(), path_arg.clone()));
attempts.push(PklCommand {
program: "nix".into(),
args: vec![
"shell".into(),
"nixpkgs#pkl".into(),
"-c".into(),
"pkl".into(),
"eval".into(),
"--format".into(),
"json".into(),
path_arg,
],
});
let mut missing = Vec::new();
for attempt in attempts {
let output = Command::new(&attempt.program)
.args(&attempt.args)
.stdin(Stdio::null())
.output();
match output {
Ok(output) if output.status.success() => return Ok(output.stdout),
Ok(output) => {
let stderr = crate::exec::captured_text(&output.stderr);
bail!(
"Pkl evaluator command failed: {} {}\n{}",
attempt.program,
attempt.args.join(" "),
stderr.trim()
);
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => missing.push(attempt.program),
Err(err) => {
return Err(err).with_context(|| {
format!(
"starting Pkl evaluator command: {} {}",
attempt.program,
attempt.args.join(" ")
)
});
}
}
}
bail!(
"Pkl evaluator unavailable; install `pkl`, set NEX_PKL, or provide `nix` so Nex can run nixpkgs#pkl (tried: {})",
missing.join(", ")
)
}
struct PklCommand {
program: String,
args: Vec<String>,
}
impl PklCommand {
fn eval_json(program: String, path_arg: String) -> Self {
Self {
program,
args: vec!["eval".into(), "--format".into(), "json".into(), path_arg],
}
}
}
fn bundled_pkl_path() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
bundled_pkl_path_for_exe(&exe).filter(|path| path.is_file())
}
fn bundled_pkl_path_for_exe(exe: &Path) -> Option<PathBuf> {
let bin_dir = exe.parent()?;
let root = bin_dir.parent()?;
Some(root.join("libexec").join("nex").join("pkl"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[test]
fn resolves_bundled_pkl_next_to_release_binary() {
let path = bundled_pkl_path_for_exe(Path::new("/opt/nex/bin/nex")).expect("bundled path");
assert_eq!(path, Path::new("/opt/nex/libexec/nex/pkl"));
}
#[test]
#[cfg(unix)]
fn evaluates_json_with_nex_pkl_override() {
let temp = tempfile::tempdir().expect("tempdir");
let fake_pkl = temp.path().join("fake-pkl");
fs::write(
&fake_pkl,
r#"#!/usr/bin/env bash
cat <<'JSON'
{"schema":"ok"}
JSON
"#,
)
.expect("write fake pkl");
let mut perms = fs::metadata(&fake_pkl).expect("metadata").permissions();
perms.set_mode(0o755);
fs::set_permissions(&fake_pkl, perms).expect("chmod");
let old = std::env::var_os("NEX_PKL");
std::env::set_var("NEX_PKL", &fake_pkl);
let json = evaluate_json(&temp.path().join("source.pkl")).expect("evaluate pkl");
if let Some(old) = old {
std::env::set_var("NEX_PKL", old);
} else {
std::env::remove_var("NEX_PKL");
}
assert_eq!(json["schema"], "ok");
}
}