use std::ffi::OsStr;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use crate::error::CliError;
#[derive(Debug, Clone, Copy)]
#[allow(
clippy::struct_excessive_bools,
reason = "independent yes/no facts about the environment"
)]
pub struct Env {
pub ci: bool,
pub no_color: bool,
pub stdin_tty: bool,
pub stdout_tty: bool,
pub github_actions: bool,
}
impl Env {
pub fn detect() -> Self {
let ci = std::env::var_os("CI").map(|value| value.to_string_lossy().into_owned());
let no_color = std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty());
Self {
ci: is_truthy(ci.as_deref()),
no_color,
stdin_tty: std::io::stdin().is_terminal(),
stdout_tty: std::io::stdout().is_terminal(),
github_actions: std::env::var_os("GITHUB_ACTIONS").is_some_and(|value| value == "true"),
}
}
}
const TIMINGS_VAR: &str = "SKIF_TIMINGS";
pub fn enable_timings() {
if std::env::var_os(TIMINGS_VAR).is_some_and(|value| !value.is_empty() && value != "0") {
let _ = skif_core::timing::enable_stderr();
}
}
pub fn github_workspace() -> Option<PathBuf> {
std::env::var_os("GITHUB_WORKSPACE")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
pub fn enter_bazel_run_directory() -> Result<(), CliError> {
let Some(dir) = bazel_run_directory(
std::env::var_os("BUILD_WORKSPACE_DIRECTORY").as_deref(),
std::env::var_os("BUILD_WORKING_DIRECTORY").as_deref(),
) else {
return Ok(());
};
std::env::set_current_dir(&dir)
.map_err(|error| CliError::io("cannot enter the `bazel run` directory", &dir, &error))
}
pub fn bazel_run_directory(workspace: Option<&OsStr>, working: Option<&OsStr>) -> Option<PathBuf> {
let workspace = Path::new(workspace.filter(|value| !value.is_empty())?);
Some(
working
.filter(|value| !value.is_empty())
.map_or_else(|| workspace.to_path_buf(), |working| workspace.join(working)),
)
}
pub fn bazel_test_tmpdir() -> Option<PathBuf> {
test_tmpdir(std::env::var_os("TEST_TMPDIR").as_deref())
}
pub fn test_tmpdir(value: Option<&OsStr>) -> Option<PathBuf> {
value.filter(|value| !value.is_empty()).map(PathBuf::from)
}
pub fn is_truthy(value: Option<&str>) -> bool {
match value.map(str::trim) {
None | Some("") => false,
Some(text) => !["0", "false", "no", "off"]
.iter()
.any(|falsy| text.eq_ignore_ascii_case(falsy)),
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use std::path::PathBuf;
use super::{bazel_run_directory, is_truthy, test_tmpdir};
#[test]
fn an_empty_test_tmpdir_is_unset() {
let dir = std::env::temp_dir().join("test-tmp");
assert_eq!(test_tmpdir(None), None);
assert_eq!(test_tmpdir(Some(OsStr::new(""))), None);
assert_eq!(test_tmpdir(Some(dir.as_os_str())), Some(dir.clone()));
}
#[test]
fn the_bazel_run_directory_is_the_working_directory_else_the_workspace() {
let workspace = std::env::temp_dir().join("workspace");
let packages = workspace.join("packages");
let elsewhere = std::env::temp_dir().join("elsewhere");
let root = Some(workspace.as_os_str());
let cases: [(Option<&OsStr>, Option<&OsStr>, Option<PathBuf>); 8] = [
(None, None, None),
(None, Some(elsewhere.as_os_str()), None),
(Some(OsStr::new("")), Some(elsewhere.as_os_str()), None),
(root, None, Some(workspace.clone())),
(root, Some(OsStr::new("")), Some(workspace.clone())),
(root, Some(packages.as_os_str()), Some(packages.clone())),
(root, Some(elsewhere.as_os_str()), Some(elsewhere.clone())),
(root, Some(OsStr::new("packages")), Some(packages.clone())),
];
for (workspace, working, expected) in cases {
assert_eq!(
bazel_run_directory(workspace, working),
expected,
"{workspace:?} {working:?}"
);
}
}
#[test]
fn ci_truthiness_table() {
let cases = [
(None, false),
(Some(""), false),
(Some(" "), false),
(Some("0"), false),
(Some("false"), false),
(Some("FALSE"), false),
(Some("No"), false),
(Some("off"), false),
(Some("1"), true),
(Some("true"), true),
(Some("yes"), true),
(Some("anything"), true),
];
for (value, expected) in cases {
assert_eq!(is_truthy(value), expected, "{value:?}");
}
}
}