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}