Skip to main content

cuenv_core/
runtime.rs

1use crate::Result;
2use crate::manifest::{DevenvRuntime, NixRuntime, Runtime};
3use cuenv_hooks::{Hook, capture_source_environment};
4use std::collections::HashMap;
5use std::path::Path;
6
7const RUNTIME_ENV_TIMEOUT_SECONDS: u64 = 600;
8
9/// Resolve environment variables provided by the configured runtime.
10///
11/// Supports `Runtime::Nix` (runs `nix print-dev-env`) and `Runtime::Devenv`
12/// (runs `devenv print-dev-env`). Other runtime types return an empty map.
13///
14/// # Errors
15///
16/// Returns an error if the configured runtime environment cannot be acquired.
17pub async fn resolve_runtime_environment(
18    project_root: &Path,
19    runtime: Option<&Runtime>,
20) -> Result<HashMap<String, String>> {
21    match runtime {
22        Some(Runtime::Nix(nix_runtime)) => {
23            resolve_nix_runtime_environment(project_root, nix_runtime).await
24        }
25        Some(Runtime::Devenv(devenv_runtime)) => {
26            resolve_devenv_runtime_environment(project_root, devenv_runtime).await
27        }
28        _ => Ok(HashMap::new()),
29    }
30}
31
32async fn resolve_nix_runtime_environment(
33    project_root: &Path,
34    runtime: &NixRuntime,
35) -> Result<HashMap<String, String>> {
36    let hook = Hook {
37        order: 10,
38        propagate: false,
39        command: "nix".to_string(),
40        args: nix_print_dev_env_args(runtime),
41        dir: Some(project_root.to_string_lossy().to_string()),
42        inputs: vec!["flake.nix".to_string(), "flake.lock".to_string()],
43        source: Some(true),
44    };
45
46    capture_source_environment(hook, &HashMap::new(), RUNTIME_ENV_TIMEOUT_SECONDS)
47        .await
48        .map_err(|e| {
49            crate::Error::configuration(format!("Failed to acquire Nix runtime environment: {e}"))
50        })
51}
52
53async fn resolve_devenv_runtime_environment(
54    project_root: &Path,
55    runtime: &DevenvRuntime,
56) -> Result<HashMap<String, String>> {
57    let devenv_dir = if runtime.path.is_empty() || runtime.path == "." {
58        project_root.to_path_buf()
59    } else {
60        project_root.join(&runtime.path)
61    };
62
63    let devenv_cmd = resolve_devenv_command().await?;
64
65    let hook = Hook {
66        order: 10,
67        propagate: false,
68        command: devenv_cmd,
69        args: vec!["print-dev-env".to_string()],
70        dir: Some(devenv_dir.to_string_lossy().to_string()),
71        inputs: vec!["devenv.nix".to_string(), "devenv.lock".to_string()],
72        source: Some(true),
73    };
74
75    capture_source_environment(hook, &HashMap::new(), RUNTIME_ENV_TIMEOUT_SECONDS)
76        .await
77        .map_err(|e| {
78            crate::Error::configuration(format!(
79                "Failed to acquire devenv runtime environment: {e}"
80            ))
81        })
82}
83
84/// Resolve devenv command path, installing via `nix profile install` if needed.
85///
86/// Returns the command string to invoke devenv — either `"devenv"` if already
87/// on PATH, or the absolute path to the nix profile binary after installation.
88async fn resolve_devenv_command() -> Result<String> {
89    if tokio::process::Command::new("devenv")
90        .arg("version")
91        .output()
92        .await
93        .map(|o| o.status.success())
94        .unwrap_or(false)
95    {
96        return Ok("devenv".to_string());
97    }
98
99    tracing::info!("devenv not found, installing via nix profile install");
100    let output = tokio::process::Command::new("nix")
101        .args([
102            "--extra-experimental-features",
103            "nix-command flakes",
104            "profile",
105            "install",
106            "nixpkgs#devenv",
107        ])
108        .output()
109        .await
110        .map_err(|e| crate::Error::configuration(format!("Failed to install devenv: {e}")))?;
111
112    if !output.status.success() {
113        let stderr = String::from_utf8_lossy(&output.stderr);
114        return Err(crate::Error::configuration(format!(
115            "Failed to install devenv: {stderr}"
116        )));
117    }
118
119    // Return absolute path so we don't need to mutate the process PATH
120    if let Ok(home) = std::env::var("HOME") {
121        let devenv_path = format!("{home}/.nix-profile/bin/devenv");
122        if std::path::Path::new(&devenv_path).exists() {
123            return Ok(devenv_path);
124        }
125    }
126
127    Ok("devenv".to_string())
128}
129
130fn nix_print_dev_env_args(runtime: &NixRuntime) -> Vec<String> {
131    let mut args = vec![
132        "--extra-experimental-features".to_string(),
133        "nix-command flakes".to_string(),
134        "print-dev-env".to_string(),
135    ];
136    args.push(nix_runtime_target(runtime));
137    args
138}
139
140fn nix_runtime_target(runtime: &NixRuntime) -> String {
141    match &runtime.output {
142        Some(output) => format!("{}#{}", runtime.flake, output),
143        None => runtime.flake.clone(),
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn nix_runtime_defaults_to_local_flake() {
153        let runtime = NixRuntime::default();
154
155        assert_eq!(
156            nix_print_dev_env_args(&runtime),
157            vec![
158                "--extra-experimental-features",
159                "nix-command flakes",
160                "print-dev-env",
161                ".",
162            ]
163        );
164    }
165
166    #[test]
167    fn devenv_runtime_from_cue_defaults_to_current_dir() {
168        // When deserialized from CUE/JSON via the Runtime enum, serde default gives "."
169        let runtime: Runtime = serde_json::from_str(r#"{"type":"devenv"}"#).unwrap();
170        match runtime {
171            Runtime::Devenv(devenv) => assert_eq!(devenv.path, "."),
172            _ => panic!("Expected Devenv runtime"),
173        }
174    }
175
176    #[test]
177    fn nix_runtime_uses_explicit_output_target() {
178        let runtime = NixRuntime {
179            flake: "github:example/project".to_string(),
180            output: Some("devShells.x86_64-linux.ci".to_string()),
181        };
182
183        assert_eq!(
184            nix_print_dev_env_args(&runtime),
185            vec![
186                "--extra-experimental-features",
187                "nix-command flakes",
188                "print-dev-env",
189                "github:example/project#devShells.x86_64-linux.ci",
190            ]
191        );
192    }
193}