direnv.rs

 1use crate::environment::EnvironmentErrorMessage;
 2use std::process::ExitStatus;
 3
 4#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
 5use {collections::HashMap, std::path::Path, util::ResultExt};
 6
 7#[derive(Clone)]
 8pub enum DirenvError {
 9    NotFound,
10    FailedRun,
11    NonZeroExit(ExitStatus, Vec<u8>),
12    InvalidJson,
13}
14
15impl From<DirenvError> for Option<EnvironmentErrorMessage> {
16    fn from(value: DirenvError) -> Self {
17        match value {
18            DirenvError::NotFound => None,
19            DirenvError::FailedRun | DirenvError::NonZeroExit(_, _) => {
20                Some(EnvironmentErrorMessage(String::from(
21                    "Failed to run direnv. See logs for more info",
22                )))
23            }
24            DirenvError::InvalidJson => Some(EnvironmentErrorMessage(String::from(
25                "Direnv returned invalid json. See logs for more info",
26            ))),
27        }
28    }
29}
30
31#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
32pub async fn load_direnv_environment(
33    env: &HashMap<String, String>,
34    dir: &Path,
35) -> Result<HashMap<String, Option<String>>, DirenvError> {
36    let Ok(direnv_path) = which::which("direnv") else {
37        return Err(DirenvError::NotFound);
38    };
39
40    let args = &["export", "json"];
41    let Some(direnv_output) = smol::process::Command::new(&direnv_path)
42        .args(args)
43        .envs(env)
44        .env("TERM", "dumb")
45        .current_dir(dir)
46        .output()
47        .await
48        .log_err()
49    else {
50        return Err(DirenvError::FailedRun);
51    };
52
53    if !direnv_output.status.success() {
54        log::error!(
55            "Loading direnv environment failed ({}), stderr: {}",
56            direnv_output.status,
57            String::from_utf8_lossy(&direnv_output.stderr)
58        );
59        return Err(DirenvError::NonZeroExit(
60            direnv_output.status,
61            direnv_output.stderr,
62        ));
63    }
64
65    let output = String::from_utf8_lossy(&direnv_output.stdout);
66    if output.is_empty() {
67        // direnv outputs nothing when it has no changes to apply to environment variables
68        return Ok(HashMap::new());
69    }
70
71    match serde_json::from_str(&output) {
72        Ok(env) => Ok(env),
73        Err(err) => {
74            log::error!(
75                "json parse error {}, while parsing output of `{} {}`:\n{}",
76                err,
77                direnv_path.display(),
78                args.join(" "),
79                output
80            );
81            Err(DirenvError::InvalidJson)
82        }
83    }
84}