Skip to main content

ready_set/
env.rs

1//! Export of the dispatcher → plugin environment contract.
2//!
3//! See `docs/contracts/env-vars.md`. The dispatcher SHOULD clear unknown
4//! `READY_SET_*` variables from the parent environment before exec to avoid
5//! injection from the calling shell.
6
7use std::path::Path;
8use std::process::Command;
9
10use ready_set_sdk::OutputMode;
11use ready_set_sdk::context::{ColorMode, LogLevel};
12
13/// Settings exported as the dispatcher → plugin env contract.
14#[derive(Debug, Clone)]
15pub struct EnvContract {
16    /// Dispatcher version (this binary's `CARGO_PKG_VERSION`).
17    pub dispatcher_version: semver::Version,
18    /// Resolved project root, if any.
19    pub project_root: Option<std::path::PathBuf>,
20    /// Resolved `.ready-set.toml`, if any.
21    pub config_path: Option<std::path::PathBuf>,
22    /// Output mode.
23    pub output: OutputMode,
24    /// Log level.
25    pub log: LogLevel,
26    /// Color preference.
27    pub color: ColorMode,
28}
29
30/// Names of the env vars the dispatcher owns.
31const KNOWN_VARS: &[&str] = &[
32    "READY_SET_DISPATCHER_VERSION",
33    "READY_SET_PROJECT_ROOT",
34    "READY_SET_CONFIG_PATH",
35    "READY_SET_OUTPUT",
36    "READY_SET_LOG",
37    "READY_SET_COLOR",
38];
39
40/// Apply the env contract to `cmd`. Clears any pre-existing `READY_SET_*`
41/// variable from the child env that this function does not explicitly set,
42/// so plugins never see stale values from the calling shell.
43pub fn export_contract(cmd: &mut Command, contract: &EnvContract) {
44    // Clear unknown READY_SET_* vars from the inherited parent env.
45    for (key, _) in std::env::vars_os().filter(|(k, _)| {
46        k.to_string_lossy().starts_with("READY_SET_")
47            && !KNOWN_VARS.contains(&k.to_string_lossy().as_ref())
48    }) {
49        cmd.env_remove(&key);
50    }
51
52    cmd.env(
53        "READY_SET_DISPATCHER_VERSION",
54        contract.dispatcher_version.to_string(),
55    );
56    if let Some(root) = &contract.project_root {
57        cmd.env("READY_SET_PROJECT_ROOT", absolute_or_self(root));
58    } else {
59        cmd.env_remove("READY_SET_PROJECT_ROOT");
60    }
61    if let Some(path) = &contract.config_path {
62        cmd.env("READY_SET_CONFIG_PATH", absolute_or_self(path));
63    } else {
64        cmd.env_remove("READY_SET_CONFIG_PATH");
65    }
66    cmd.env(
67        "READY_SET_OUTPUT",
68        match contract.output {
69            OutputMode::Human => "human",
70            OutputMode::Json => "json",
71        },
72    );
73    cmd.env(
74        "READY_SET_LOG",
75        match contract.log {
76            LogLevel::Quiet => "quiet",
77            LogLevel::Normal => "normal",
78            LogLevel::Verbose => "verbose",
79        },
80    );
81    cmd.env(
82        "READY_SET_COLOR",
83        match contract.color {
84            ColorMode::Auto => "auto",
85            ColorMode::Always => "always",
86            ColorMode::Never => "never",
87        },
88    );
89}
90
91fn absolute_or_self(path: &Path) -> std::path::PathBuf {
92    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
93}