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