Skip to main content

nex_pkg/
pkl.rs

1use std::path::{Path, PathBuf};
2use std::process::{Command, Stdio};
3
4use anyhow::{bail, Context, Result};
5
6pub fn evaluate_json(path: &Path) -> Result<serde_json::Value> {
7    let output = run_eval_json(path)?;
8    serde_json::from_slice(&output)
9        .with_context(|| format!("Pkl evaluator did not emit JSON for {}", path.display()))
10}
11
12fn run_eval_json(path: &Path) -> Result<Vec<u8>> {
13    let path_arg = path.to_string_lossy().to_string();
14    let mut attempts = Vec::new();
15
16    if let Ok(bin) = std::env::var("NEX_PKL") {
17        attempts.push(PklCommand::eval_json(bin, path_arg.clone()));
18    }
19    if let Some(bin) = bundled_pkl_path() {
20        attempts.push(PklCommand::eval_json(
21            bin.display().to_string(),
22            path_arg.clone(),
23        ));
24    }
25    attempts.push(PklCommand::eval_json("pkl".to_string(), path_arg.clone()));
26    attempts.push(PklCommand {
27        program: "nix".into(),
28        args: vec![
29            "shell".into(),
30            "nixpkgs#pkl".into(),
31            "-c".into(),
32            "pkl".into(),
33            "eval".into(),
34            "--format".into(),
35            "json".into(),
36            path_arg,
37        ],
38    });
39
40    let mut missing = Vec::new();
41    for attempt in attempts {
42        let output = Command::new(&attempt.program)
43            .args(&attempt.args)
44            .stdin(Stdio::null())
45            .output();
46        match output {
47            Ok(output) if output.status.success() => return Ok(output.stdout),
48            Ok(output) => {
49                let stderr = crate::exec::captured_text(&output.stderr);
50                bail!(
51                    "Pkl evaluator command failed: {} {}\n{}",
52                    attempt.program,
53                    attempt.args.join(" "),
54                    stderr.trim()
55                );
56            }
57            Err(err) if err.kind() == std::io::ErrorKind::NotFound => missing.push(attempt.program),
58            Err(err) => {
59                return Err(err).with_context(|| {
60                    format!(
61                        "starting Pkl evaluator command: {} {}",
62                        attempt.program,
63                        attempt.args.join(" ")
64                    )
65                });
66            }
67        }
68    }
69
70    bail!(
71        "Pkl evaluator unavailable; install `pkl`, set NEX_PKL, or provide `nix` so Nex can run nixpkgs#pkl (tried: {})",
72        missing.join(", ")
73    )
74}
75
76struct PklCommand {
77    program: String,
78    args: Vec<String>,
79}
80
81impl PklCommand {
82    fn eval_json(program: String, path_arg: String) -> Self {
83        Self {
84            program,
85            args: vec!["eval".into(), "--format".into(), "json".into(), path_arg],
86        }
87    }
88}
89
90fn bundled_pkl_path() -> Option<PathBuf> {
91    let exe = std::env::current_exe().ok()?;
92    bundled_pkl_path_for_exe(&exe).filter(|path| path.is_file())
93}
94
95fn bundled_pkl_path_for_exe(exe: &Path) -> Option<PathBuf> {
96    let bin_dir = exe.parent()?;
97    let root = bin_dir.parent()?;
98    Some(root.join("libexec").join("nex").join("pkl"))
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::fs;
105
106    #[cfg(unix)]
107    use std::os::unix::fs::PermissionsExt;
108
109    #[test]
110    fn resolves_bundled_pkl_next_to_release_binary() {
111        let path = bundled_pkl_path_for_exe(Path::new("/opt/nex/bin/nex")).expect("bundled path");
112        assert_eq!(path, Path::new("/opt/nex/libexec/nex/pkl"));
113    }
114
115    #[test]
116    #[cfg(unix)]
117    fn evaluates_json_with_nex_pkl_override() {
118        let temp = tempfile::tempdir().expect("tempdir");
119        let fake_pkl = temp.path().join("fake-pkl");
120        fs::write(
121            &fake_pkl,
122            r#"#!/usr/bin/env bash
123cat <<'JSON'
124{"schema":"ok"}
125JSON
126"#,
127        )
128        .expect("write fake pkl");
129        let mut perms = fs::metadata(&fake_pkl).expect("metadata").permissions();
130        perms.set_mode(0o755);
131        fs::set_permissions(&fake_pkl, perms).expect("chmod");
132
133        let old = std::env::var_os("NEX_PKL");
134        std::env::set_var("NEX_PKL", &fake_pkl);
135        let json = evaluate_json(&temp.path().join("source.pkl")).expect("evaluate pkl");
136        if let Some(old) = old {
137            std::env::set_var("NEX_PKL", old);
138        } else {
139            std::env::remove_var("NEX_PKL");
140        }
141
142        assert_eq!(json["schema"], "ok");
143    }
144}