1#![forbid(unsafe_code)]
4use anyhow::Result;
43use serde::Serialize;
44
45#[cfg(target_os = "linux")]
46mod linux;
47#[cfg(target_os = "macos")]
48mod macos;
49#[cfg(target_os = "windows")]
50mod windows;
51
52pub fn initialize_platform() -> Result<()> {
60 #[cfg(target_os = "windows")]
61 {
62 windows::configure_console()?;
63 }
64
65 #[cfg(target_os = "linux")]
66 {
67 linux::detect_sandbox();
68 }
69
70 #[cfg(target_os = "macos")]
71 {
72 macos::initialize();
73 }
74
75 let env = detect_runtime();
77 tracing::debug!(
78 os = env.os,
79 arch = env.arch,
80 wsl = env.is_wsl,
81 container = env.is_container,
82 ci = env.is_ci,
83 termux = env.is_termux,
84 sandbox = env.sandbox.unwrap_or("none"),
85 "runtime environment detected"
86 );
87
88 Ok(())
89}
90
91#[must_use]
96pub fn normalize_stdin_line(line: &str) -> &str {
97 line.trim_end_matches(['\r', '\n'])
98}
99
100#[must_use]
105pub fn is_tty() -> bool {
106 std::io::IsTerminal::is_terminal(&std::io::stdout())
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
114pub struct RuntimeEnvironment {
115 pub os: &'static str,
117 pub arch: &'static str,
119 pub is_wsl: bool,
121 pub is_container: bool,
123 pub is_ci: bool,
125 pub is_termux: bool,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub sandbox: Option<&'static str>,
130}
131
132#[must_use]
134pub fn detect_runtime() -> RuntimeEnvironment {
135 RuntimeEnvironment {
136 os: std::env::consts::OS,
137 arch: std::env::consts::ARCH,
138 is_wsl: detect_wsl(),
139 is_container: detect_container(),
140 is_ci: detect_ci(),
141 is_termux: detect_termux(),
142 sandbox: detect_sandbox_kind(),
143 }
144}
145
146fn detect_wsl() -> bool {
147 if std::env::var_os("WSL_DISTRO_NAME").is_some()
148 || std::env::var_os("WSL_INTEROP").is_some()
149 || std::env::var_os("WSLENV").is_some()
150 {
151 return true;
152 }
153 #[cfg(target_os = "linux")]
155 {
156 if let Ok(v) = std::fs::read_to_string("/proc/version") {
157 let lower = v.to_ascii_lowercase();
158 if lower.contains("microsoft") || lower.contains("wsl") {
159 return true;
160 }
161 }
162 }
163 false
164}
165
166fn detect_container() -> bool {
167 if std::env::var_os("KUBERNETES_SERVICE_HOST").is_some()
168 || std::env::var_os("container").is_some()
169 {
170 return true;
171 }
172 if std::path::Path::new("/.dockerenv").exists()
174 || std::path::Path::new("/run/.containerenv").exists()
175 {
176 return true;
177 }
178 #[cfg(target_os = "linux")]
180 {
181 if let Ok(cg) = std::fs::read_to_string("/proc/1/cgroup") {
182 let lower = cg.to_ascii_lowercase();
183 if lower.contains("docker")
184 || lower.contains("containerd")
185 || lower.contains("kubepods")
186 || lower.contains("libpod")
187 || lower.contains("/lxc/")
188 {
189 return true;
190 }
191 }
192 }
193 false
194}
195
196fn detect_ci() -> bool {
197 if std::env::var("CI").map(|v| !v.is_empty() && v != "0" && v != "false") == Ok(true) {
199 return true;
200 }
201 const VENDOR_VARS: &[&str] = &[
202 "GITHUB_ACTIONS",
203 "GITLAB_CI",
204 "TF_BUILD",
205 "CIRCLECI",
206 "BUILDKITE",
207 "TRAVIS",
208 "JENKINS_URL",
209 "APPVEYOR",
210 "TEAMCITY_VERSION",
211 "BITBUCKET_BUILD_NUMBER",
212 ];
213 VENDOR_VARS.iter().any(|k| std::env::var_os(k).is_some())
214}
215
216fn detect_termux() -> bool {
217 std::env::var_os("TERMUX_VERSION").is_some()
218 || std::env::var_os("TERMUX_APK_RELEASE").is_some()
219 || std::env::var("PREFIX")
220 .map(|p| p.contains("com.termux"))
221 .unwrap_or(false)
222}
223
224fn detect_sandbox_kind() -> Option<&'static str> {
225 if std::env::var_os("FLATPAK_ID").is_some() {
226 return Some("flatpak");
227 }
228 if std::env::var_os("SNAP").is_some() {
229 return Some("snap");
230 }
231 None
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use serial_test::serial;
238
239 #[test]
240 fn normalize_strips_trailing_cr() {
241 assert_eq!(normalize_stdin_line("test\r"), "test");
242 assert_eq!(normalize_stdin_line("test\r\n"), "test");
243 assert_eq!(normalize_stdin_line("test\n"), "test");
244 assert_eq!(normalize_stdin_line("test"), "test");
245 }
246
247 #[test]
248 fn normalize_empty_string() {
249 assert_eq!(normalize_stdin_line(""), "");
250 }
251
252 #[test]
253 fn normalize_newlines_only() {
254 assert_eq!(normalize_stdin_line("\n\n\n"), "");
255 }
256
257 #[test]
258 fn normalize_mixed_crlf_lf_keeps_interior() {
259 assert_eq!(
260 normalize_stdin_line("line1\r\nline2\r\nline3"),
261 "line1\r\nline2\r\nline3"
262 );
263 }
264
265 #[test]
266 fn normalize_with_spaces() {
267 assert_eq!(
268 normalize_stdin_line("text with spaces \r\n"),
269 "text with spaces "
270 );
271 }
272
273 #[test]
274 fn is_tty_returns_bool() {
275 let _ = is_tty();
276 }
277
278 #[test]
279 fn detect_runtime_has_os_and_arch() {
280 let env = detect_runtime();
281 assert!(!env.os.is_empty());
282 assert!(!env.arch.is_empty());
283 }
284
285 #[test]
286 #[serial]
287 fn detect_ci_honors_ci_env() {
288 let prev = std::env::var("CI").ok();
289 crate::test_util::env::set_var("CI", "true");
290 assert!(detect_ci());
291 match prev {
292 Some(v) => crate::test_util::env::set_var("CI", v),
293 None => crate::test_util::env::remove_var("CI"),
294 }
295 }
296
297 #[test]
298 #[serial]
299 fn detect_sandbox_flatpak() {
300 let prev_f = std::env::var("FLATPAK_ID").ok();
301 let prev_s = std::env::var("SNAP").ok();
302 crate::test_util::env::set_var("FLATPAK_ID", "org.example.App");
303 crate::test_util::env::remove_var("SNAP");
304 assert_eq!(detect_sandbox_kind(), Some("flatpak"));
305 match prev_f {
306 Some(v) => crate::test_util::env::set_var("FLATPAK_ID", v),
307 None => crate::test_util::env::remove_var("FLATPAK_ID"),
308 }
309 match prev_s {
310 Some(v) => crate::test_util::env::set_var("SNAP", v),
311 None => crate::test_util::env::remove_var("SNAP"),
312 }
313 }
314
315 #[test]
316 #[serial]
317 fn detect_sandbox_snap() {
318 let prev_f = std::env::var("FLATPAK_ID").ok();
319 let prev_s = std::env::var("SNAP").ok();
320 crate::test_util::env::remove_var("FLATPAK_ID");
321 crate::test_util::env::set_var("SNAP", "/snap/app");
322 assert_eq!(detect_sandbox_kind(), Some("snap"));
323 match prev_f {
324 Some(v) => crate::test_util::env::set_var("FLATPAK_ID", v),
325 None => crate::test_util::env::remove_var("FLATPAK_ID"),
326 }
327 match prev_s {
328 Some(v) => crate::test_util::env::set_var("SNAP", v),
329 None => crate::test_util::env::remove_var("SNAP"),
330 }
331 }
332
333 #[test]
334 #[serial]
335 fn detect_termux_via_version() {
336 let prev = std::env::var("TERMUX_VERSION").ok();
337 crate::test_util::env::set_var("TERMUX_VERSION", "0.118");
338 assert!(detect_termux());
339 match prev {
340 Some(v) => crate::test_util::env::set_var("TERMUX_VERSION", v),
341 None => crate::test_util::env::remove_var("TERMUX_VERSION"),
342 }
343 }
344
345 #[test]
346 #[serial]
347 fn detect_wsl_via_distro_name() {
348 let prev = std::env::var("WSL_DISTRO_NAME").ok();
349 crate::test_util::env::set_var("WSL_DISTRO_NAME", "Ubuntu");
350 assert!(detect_wsl());
351 match prev {
352 Some(v) => crate::test_util::env::set_var("WSL_DISTRO_NAME", v),
353 None => crate::test_util::env::remove_var("WSL_DISTRO_NAME"),
354 }
355 }
356
357 #[test]
358 fn runtime_environment_serializes_json_keys() {
359 let env = RuntimeEnvironment {
360 os: "linux",
361 arch: "x86_64",
362 is_wsl: false,
363 is_container: true,
364 is_ci: true,
365 is_termux: false,
366 sandbox: Some("flatpak"),
367 };
368 let v = serde_json::to_value(env).unwrap();
369 assert_eq!(v["os"], "linux");
370 assert_eq!(v["is_container"], true);
371 assert_eq!(v["sandbox"], "flatpak");
372 }
373}