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}