Skip to main content

cabin_env/
lib.rs

1//! `CABIN_*` environment variable name constants and the typed
2//! builder for the `cabin run` / `cabin test` package-execution
3//! overlay.
4//!
5//! Cabin is Cargo-inspired (not Cargo-compatible): the env vars
6//! it reads on the *input* side and the env vars it sets on the
7//! *output* side both follow Cargo's naming conventions where
8//! the semantics line up, and diverge with `CABIN_*` names where
9//! Cabin's C/C++ semantics differ. This crate is the single
10//! source of truth for both halves so the rest of the codebase
11//! agrees on names.
12//!
13//! Crate boundaries:
14//! - this crate must not run processes, read configuration
15//!   files, or touch the filesystem;
16//! - it must not depend on `cabin`, `cabin-build`, or other
17//!   higher-level crates that would create cyclic dependencies;
18//! - it consumes typed inputs and produces typed outputs (the
19//!   orchestration layer is responsible for mapping resolved
20//!   values into [`PackageEnvInputs`]).
21//!
22//! ## Read-side env vars
23//!
24//! Constants for every `CABIN_*` variable Cabin's CLI reads
25//! live as `pub const ... : &str = "..."` in this crate. The
26//! orchestration layer reads each one through `std::env::var`
27//! (or an injected `env_fn` for tests) and threads the value
28//! through to the right resolver.
29//!
30//! ## Run / test overlay
31//!
32//! `cabin run` and `cabin test` inject exactly the same small,
33//! stable set of package-execution variables built by
34//! [`package_env`]. The overlay is layered on top of the
35//! inherited environment; it never clears the user's `PATH`,
36//! `LANG`, etc.
37
38pub mod build_flags;
39
40pub use build_flags::{
41    CFLAGS, CPPFLAGS, CXXFLAGS, EnvBuildFlags, EnvBuildFlagsError, LDFLAGS, parse_env_build_flags,
42};
43
44use std::collections::BTreeMap;
45use std::ffi::OsString;
46
47use thiserror::Error;
48
49// ---------------------------------------------------------------------------
50// Read-side env var name constants
51// ---------------------------------------------------------------------------
52
53/// Path to a single explicit Cabin config file. When set, no
54/// other config files are loaded.
55pub const CABIN_CONFIG: &str = "CABIN_CONFIG";
56
57/// Override for the per-user config home (the directory under
58/// which Cabin looks for `config.toml`). Honored by the
59/// `cabin-config` crate's discovery layer.
60pub const CABIN_CONFIG_HOME: &str = "CABIN_CONFIG_HOME";
61
62/// When truthy, Cabin loads no config files at all. Used by the
63/// integration test harness so a developer's
64/// `~/.config/cabin/config.toml` cannot leak into tests.
65pub const CABIN_NO_CONFIG: &str = "CABIN_NO_CONFIG";
66
67/// Build output directory. Honored by commands that write to,
68/// read from, or deliberately exclude the build directory:
69/// `cabin build`, `cabin clean`, `cabin run`, `cabin test`,
70/// `cabin fmt`, and `cabin tidy`.
71///
72/// Precedence: CLI flag (`--build-dir`) > env var > config
73/// (`[paths] build-dir`) > built-in default (`build`).
74pub const CABIN_BUILD_DIR: &str = "CABIN_BUILD_DIR";
75
76/// Override for the artifact cache directory for a single
77/// invocation. Honored by every command that resolves an
78/// artifact cache. Wins over `CABIN_CACHE_HOME` and the XDG
79/// fallbacks below it.
80pub const CABIN_CACHE_DIR: &str = "CABIN_CACHE_DIR";
81
82/// Override for the per-user cache home — the directory cabin's
83/// global cache lives under. Defaults to `$XDG_CACHE_HOME/cabin`,
84/// falling back to `$HOME/.cache/cabin`, per XDG Base Directory
85/// conventions. Mirrors the precedence shape `CABIN_CONFIG_HOME`
86/// uses for the per-user config home.
87///
88/// Distinct from `CABIN_CACHE_DIR`: use `CABIN_CACHE_HOME` to
89/// relocate the *user* cache home (every project's cache moves
90/// together); use `CABIN_CACHE_DIR` to point a single invocation
91/// at a specific cache directory.
92pub const CABIN_CACHE_HOME: &str = "CABIN_CACHE_HOME";
93
94/// Forbid network access for this invocation. Equivalent to
95/// passing `--offline` on the CLI. The CLI flag still takes
96/// precedence; the env var only sets the default.
97pub const CABIN_NET_OFFLINE: &str = "CABIN_NET_OFFLINE";
98
99/// Compiler-cache wrapper selector (`ccache`, `sccache`,
100/// `none`). Honored by `cabin-toolchain`'s wrapper resolver.
101pub const CABIN_COMPILER_WRAPPER: &str = "CABIN_COMPILER_WRAPPER";
102
103/// Override for the `clang-format` executable Cabin spawns
104/// from `cabin fmt`.  When set and non-empty, the value is
105/// used verbatim (typically an absolute path) and the `PATH`
106/// lookup is skipped.  When unset, Cabin spawns `clang-format`
107/// from `PATH`.
108pub const CABIN_FMT: &str = "CABIN_FMT";
109
110/// Override for the `run-clang-tidy` executable Cabin spawns
111/// from `cabin tidy`.  Same shape as [`CABIN_FMT`]: when set and
112/// non-empty the value is used verbatim and the `PATH` lookup is
113/// skipped, otherwise Cabin resolves `run-clang-tidy` against
114/// `PATH`.
115pub const CABIN_TIDY: &str = "CABIN_TIDY";
116
117/// Override for the `pkg-config` executable Cabin spawns when
118/// probing `system = true` dependencies. Same shape as
119/// [`CABIN_FMT`] and the other Cabin tool overrides: when set
120/// and non-empty the value is used verbatim (typically an
121/// absolute path) and the `PATH` lookup is skipped; when unset,
122/// Cabin spawns `pkg-config` from `PATH`. Cabin only invokes
123/// `pkg-config` when the workspace declares at least one
124/// `system = true` entry.
125pub const CABIN_PKG_CONFIG: &str = "CABIN_PKG_CONFIG";
126
127/// Number of parallel jobs the build backend should use.
128/// Cargo-style: positive integer, `0` is rejected.  Cabin
129/// reads this env var when `--jobs` is not on the command
130/// line.
131///
132/// Precedence: CLI `--jobs` flag > env var > `[build] jobs`
133/// config setting > backend default.
134pub const CABIN_BUILD_JOBS: &str = "CABIN_BUILD_JOBS";
135
136/// Terminal-color selector (`auto`, `always`, or `never`).
137/// Honored by the CLI when `--color` is not present.
138pub const CABIN_TERM_COLOR: &str = "CABIN_TERM_COLOR";
139
140/// Enable verbose Cabin-owned status output when no `-v` /
141/// `--verbose` CLI flag is present.
142pub const CABIN_TERM_VERBOSE: &str = "CABIN_TERM_VERBOSE";
143
144/// Suppress Cabin-owned status output when no `-q` /
145/// `--quiet` CLI flag is present.
146pub const CABIN_TERM_QUIET: &str = "CABIN_TERM_QUIET";
147
148// ---------------------------------------------------------------------------
149// Package-execution env var name constants
150// ---------------------------------------------------------------------------
151//
152// The fixed names Cabin sets on the `cabin run` / `cabin test`
153// child processes. This is the entire injected contract.
154
155/// Absolute path to the package's manifest directory.
156pub const CABIN_MANIFEST_DIR: &str = "CABIN_MANIFEST_DIR";
157
158/// Absolute path to the package's `cabin.toml` manifest.
159pub const CABIN_MANIFEST_PATH: &str = "CABIN_MANIFEST_PATH";
160
161/// Package name in the form the manifest declares.
162pub const CABIN_PACKAGE_NAME: &str = "CABIN_PACKAGE_NAME";
163
164/// Resolved package version (`<major>.<minor>.<patch>` plus any
165/// pre-release / build suffix exactly as the manifest declares).
166pub const CABIN_PACKAGE_VERSION: &str = "CABIN_PACKAGE_VERSION";
167
168/// Active profile name (`dev`, `release`, or any custom
169/// profile).
170pub const CABIN_PROFILE: &str = "CABIN_PROFILE";
171
172// ---------------------------------------------------------------------------
173// Package-execution env builder
174// ---------------------------------------------------------------------------
175
176/// Inputs for [`package_env`]. The orchestration layer fills
177/// this in from already-resolved typed values. `cabin run` and
178/// `cabin test` use the same shape — the injected overlay does
179/// not depend on whether the target is a binary or a test.
180#[derive(Debug, Clone)]
181pub struct PackageEnvInputs<'a> {
182    /// Manifest directory of the package owning the target.
183    pub manifest_dir: &'a std::path::Path,
184    /// `cabin.toml` path of the package owning the target.
185    pub manifest_path: &'a std::path::Path,
186    /// Package name as the manifest declared it.
187    pub package_name: &'a str,
188    /// Resolved package version.
189    pub package_version: &'a str,
190    /// Resolved profile name (`dev`, `release`, …).
191    pub profile: &'a str,
192    /// Resolved build directory.
193    pub build_dir: &'a std::path::Path,
194}
195
196/// Build the `CABIN_*` overlay surfaced to a `cabin run` /
197/// `cabin test` child process. Returns a deterministic
198/// `BTreeMap` so two calls with the same inputs are byte-equal.
199/// Infallible: every value is copied straight from the typed
200/// inputs.
201#[must_use]
202pub fn package_env(inputs: &PackageEnvInputs<'_>) -> BTreeMap<String, OsString> {
203    let mut out = BTreeMap::new();
204    out.insert(
205        CABIN_MANIFEST_DIR.to_owned(),
206        inputs.manifest_dir.as_os_str().to_owned(),
207    );
208    out.insert(
209        CABIN_MANIFEST_PATH.to_owned(),
210        inputs.manifest_path.as_os_str().to_owned(),
211    );
212    out.insert(
213        CABIN_PACKAGE_NAME.to_owned(),
214        OsString::from(inputs.package_name),
215    );
216    out.insert(
217        CABIN_PACKAGE_VERSION.to_owned(),
218        OsString::from(inputs.package_version),
219    );
220    out.insert(CABIN_PROFILE.to_owned(), OsString::from(inputs.profile));
221    out.insert(
222        CABIN_BUILD_DIR.to_owned(),
223        inputs.build_dir.as_os_str().to_owned(),
224    );
225    out
226}
227
228// ---------------------------------------------------------------------------
229// Truthy / boolean parsing for read-side env vars
230// ---------------------------------------------------------------------------
231
232/// Whether a raw env-var value should be treated as truthy.
233/// Mirrors Cargo: any of `1`, `true`, `yes`, `on` (case-
234/// insensitive) is truthy; an empty string is falsy; anything
235/// else is rejected via [`BoolError::Invalid`].
236///
237/// # Errors
238/// Returns [`BoolError::Invalid`] when `value` is non-empty and
239/// matches none of the recognized truthy or falsy spellings,
240/// carrying the offending input string.
241pub fn parse_bool(value: &str) -> Result<bool, BoolError> {
242    if value.is_empty() {
243        return Ok(false);
244    }
245    let lower = value.to_ascii_lowercase();
246    match lower.as_str() {
247        "1" | "true" | "yes" | "on" => Ok(true),
248        "0" | "false" | "no" | "off" => Ok(false),
249        _ => Err(BoolError::Invalid(value.to_owned())),
250    }
251}
252
253/// Errors produced by [`parse_bool`].
254#[derive(Debug, Error, PartialEq, Eq)]
255pub enum BoolError {
256    /// The string did not match any documented truthy / falsy
257    /// spelling.
258    #[error("expected one of `1`, `0`, `true`, `false`, `yes`, `no`, `on`, `off`; got `{0}`")]
259    Invalid(String),
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn parse_bool_recognizes_documented_truthy_and_falsy_spellings() {
268        for v in ["1", "true", "TRUE", "yes", "On"] {
269            assert!(parse_bool(v).unwrap(), "expected truthy: {v:?}");
270        }
271        for v in ["0", "false", "no", "off"] {
272            assert!(!parse_bool(v).unwrap(), "expected falsy: {v:?}");
273        }
274        assert!(!parse_bool("").unwrap());
275    }
276
277    #[test]
278    fn parse_bool_rejects_unknown_spellings() {
279        assert!(matches!(parse_bool("maybe"), Err(BoolError::Invalid(_))));
280    }
281
282    #[test]
283    fn parse_bool_error_wording_includes_raw_value() {
284        let err = parse_bool("perhaps").unwrap_err();
285        let rendered = err.to_string();
286        assert!(
287            rendered.contains("perhaps"),
288            "error should echo the input: {rendered}"
289        );
290        // The wording must list every recognized spelling so
291        // users see how to fix it.
292        assert!(
293            rendered.contains("true") && rendered.contains("false"),
294            "{rendered}"
295        );
296    }
297
298    #[test]
299    fn package_env_emits_exactly_the_six_strict_keys() {
300        use std::path::PathBuf;
301        let manifest_dir = PathBuf::from("/abs/app");
302        let manifest_path = PathBuf::from("/abs/app/cabin.toml");
303        let build_dir = PathBuf::from("/abs/app/build");
304        let env = package_env(&PackageEnvInputs {
305            manifest_dir: &manifest_dir,
306            manifest_path: &manifest_path,
307            package_name: "my-pkg",
308            package_version: "0.1.0",
309            profile: "dev",
310            build_dir: &build_dir,
311        });
312        let names: Vec<&str> = env.keys().map(String::as_str).collect();
313        assert_eq!(
314            names,
315            vec![
316                CABIN_BUILD_DIR,
317                CABIN_MANIFEST_DIR,
318                CABIN_MANIFEST_PATH,
319                CABIN_PACKAGE_NAME,
320                CABIN_PACKAGE_VERSION,
321                CABIN_PROFILE,
322            ],
323            "package_env must emit exactly the six strict keys in BTreeMap order"
324        );
325        assert_eq!(env.get(CABIN_PACKAGE_NAME).unwrap(), "my-pkg");
326        assert_eq!(env.get(CABIN_PACKAGE_VERSION).unwrap(), "0.1.0");
327        assert_eq!(env.get(CABIN_PROFILE).unwrap(), "dev");
328        assert_eq!(env.get(CABIN_BUILD_DIR).unwrap(), "/abs/app/build");
329    }
330
331    #[test]
332    fn package_env_does_not_emit_any_removed_variable() {
333        use std::path::PathBuf;
334        let dir = PathBuf::from("/abs/app");
335        let path = PathBuf::from("/abs/app/cabin.toml");
336        let env = package_env(&PackageEnvInputs {
337            manifest_dir: &dir,
338            manifest_path: &path,
339            package_name: "demo",
340            package_version: "0.1.0",
341            profile: "release",
342            build_dir: &dir,
343        });
344        for removed in [
345            "CABIN",
346            "CABIN_PACKAGE_NAME_CANONICAL",
347            "CABIN_BIN_NAME",
348            "CABIN_BIN_NAME_CANONICAL",
349            "CABIN_TEST_NAME",
350            "CABIN_TEST_NAME_CANONICAL",
351            "CABIN_TARGET_KIND",
352            "CABIN_TARGET_TRIPLE",
353            "CABIN_HOST_TRIPLE",
354            "CABIN_BUILD_CONFIGURATION_FINGERPRINT",
355        ] {
356            assert!(
357                !env.contains_key(removed),
358                "removed variable `{removed}` must not be injected"
359            );
360        }
361    }
362
363    #[test]
364    fn package_env_is_byte_stable_for_equal_inputs() {
365        use std::path::PathBuf;
366        let dir = PathBuf::from("/abs/app");
367        let path = PathBuf::from("/abs/app/cabin.toml");
368        let mk = || {
369            package_env(&PackageEnvInputs {
370                manifest_dir: &dir,
371                manifest_path: &path,
372                package_name: "demo",
373                package_version: "0.1.0",
374                profile: "dev",
375                build_dir: &dir,
376            })
377        };
378        assert_eq!(mk(), mk());
379    }
380}