Skip to main content

ctcb_core/
lib.rs

1//! Core types and utilities for the clang-tool-chain-bins workspace.
2//!
3//! Provides platform/architecture detection, formatting helpers, and shared types
4//! used across all ctcb crates.
5
6use std::fmt;
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11/// Supported operating system platforms.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum Platform {
15    Win,
16    Linux,
17    Darwin,
18}
19
20/// Supported CPU architectures.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum Arch {
24    X86_64,
25    Arm64,
26}
27
28/// A (platform, architecture) pair identifying a build target.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub struct Target {
31    pub platform: Platform,
32    pub arch: Arch,
33}
34
35// ---------------------------------------------------------------------------
36// Platform
37// ---------------------------------------------------------------------------
38
39impl Platform {
40    /// Parse a platform string, accepting common aliases.
41    ///
42    /// Recognised inputs (case-insensitive): `win`, `windows`, `linux`,
43    /// `darwin`, `macos`, `mac`.
44    pub fn from_str_loose(s: &str) -> anyhow::Result<Self> {
45        match s.to_ascii_lowercase().as_str() {
46            "win" | "windows" => Ok(Self::Win),
47            "linux" => Ok(Self::Linux),
48            "darwin" | "macos" | "mac" => Ok(Self::Darwin),
49            other => anyhow::bail!("unknown platform: {other:?}"),
50        }
51    }
52}
53
54impl fmt::Display for Platform {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            Self::Win => write!(f, "win"),
58            Self::Linux => write!(f, "linux"),
59            Self::Darwin => write!(f, "darwin"),
60        }
61    }
62}
63
64// ---------------------------------------------------------------------------
65// Arch
66// ---------------------------------------------------------------------------
67
68impl Arch {
69    /// Parse an architecture string, accepting common aliases.
70    ///
71    /// Recognised inputs (case-insensitive): `x86_64`, `x64`, `amd64`,
72    /// `arm64`, `aarch64`.
73    pub fn from_str_loose(s: &str) -> anyhow::Result<Self> {
74        match s.to_ascii_lowercase().as_str() {
75            "x86_64" | "x64" | "amd64" => Ok(Self::X86_64),
76            "arm64" | "aarch64" => Ok(Self::Arm64),
77            other => anyhow::bail!("unknown architecture: {other:?}"),
78        }
79    }
80}
81
82impl fmt::Display for Arch {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::X86_64 => write!(f, "x86_64"),
86            Self::Arm64 => write!(f, "arm64"),
87        }
88    }
89}
90
91// ---------------------------------------------------------------------------
92// Target
93// ---------------------------------------------------------------------------
94
95impl Target {
96    /// Create a new target from explicit platform and architecture.
97    pub fn new(platform: Platform, arch: Arch) -> Self {
98        Self { platform, arch }
99    }
100
101    /// Detect the current host platform and architecture at runtime.
102    pub fn current() -> anyhow::Result<Self> {
103        let platform = if cfg!(target_os = "windows") {
104            Platform::Win
105        } else if cfg!(target_os = "linux") {
106            Platform::Linux
107        } else if cfg!(target_os = "macos") {
108            Platform::Darwin
109        } else {
110            anyhow::bail!("unsupported operating system");
111        };
112
113        let arch = match std::env::consts::ARCH {
114            "x86_64" => Arch::X86_64,
115            "aarch64" => Arch::Arm64,
116            other => anyhow::bail!("unsupported architecture: {other}"),
117        };
118
119        Ok(Self { platform, arch })
120    }
121}
122
123impl fmt::Display for Target {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "{}-{}", self.platform, self.arch)
126    }
127}
128
129// ---------------------------------------------------------------------------
130// Formatting helpers
131// ---------------------------------------------------------------------------
132
133/// Format a byte count as a human-readable string (e.g. `"52.3 MB"`).
134pub fn format_size(bytes: u64) -> String {
135    const KB: f64 = 1024.0;
136    const MB: f64 = KB * 1024.0;
137    const GB: f64 = MB * 1024.0;
138    const TB: f64 = GB * 1024.0;
139
140    let b = bytes as f64;
141    if b < KB {
142        format!("{bytes} B")
143    } else if b < MB {
144        format!("{:.1} KB", b / KB)
145    } else if b < GB {
146        format!("{:.1} MB", b / MB)
147    } else if b < TB {
148        format!("{:.1} GB", b / GB)
149    } else {
150        format!("{:.1} TB", b / TB)
151    }
152}
153
154/// Format a [`Duration`] as a human-readable string.
155///
156/// Examples: `"3m 24s"`, `"1.5s"`, `"250ms"`.
157pub fn format_duration(duration: Duration) -> String {
158    let total_secs = duration.as_secs();
159    let millis = duration.subsec_millis();
160
161    if total_secs == 0 {
162        if millis == 0 {
163            return "0ms".to_string();
164        }
165        return format!("{millis}ms");
166    }
167
168    let minutes = total_secs / 60;
169    let secs = total_secs % 60;
170
171    if minutes > 0 {
172        if secs > 0 {
173            format!("{minutes}m {secs}s")
174        } else {
175            format!("{minutes}m")
176        }
177    } else if millis > 0 {
178        // Show fractional seconds when under one minute and there are millis
179        let frac = total_secs as f64 + millis as f64 / 1000.0;
180        format!("{frac:.1}s")
181    } else {
182        format!("{secs}s")
183    }
184}
185
186/// Print a section header surrounded by a 70-character separator line.
187pub fn print_section(title: &str) {
188    let sep = "=".repeat(70);
189    println!("{sep}");
190    println!("{title}");
191    println!("{sep}");
192}
193
194// ===========================================================================
195// Tests
196// ===========================================================================
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    // -- Platform round-trip -------------------------------------------------
203
204    #[test]
205    fn platform_display_roundtrip() {
206        for plat in [Platform::Win, Platform::Linux, Platform::Darwin] {
207            let s = plat.to_string();
208            let parsed = Platform::from_str_loose(&s).unwrap();
209            assert_eq!(plat, parsed, "round-trip failed for {s}");
210        }
211    }
212
213    #[test]
214    fn platform_from_str_loose_aliases() {
215        assert_eq!(Platform::from_str_loose("windows").unwrap(), Platform::Win);
216        assert_eq!(Platform::from_str_loose("WIN").unwrap(), Platform::Win);
217        assert_eq!(Platform::from_str_loose("macos").unwrap(), Platform::Darwin);
218        assert_eq!(Platform::from_str_loose("mac").unwrap(), Platform::Darwin);
219        assert_eq!(Platform::from_str_loose("LINUX").unwrap(), Platform::Linux);
220    }
221
222    #[test]
223    fn platform_from_str_loose_unknown() {
224        assert!(Platform::from_str_loose("freebsd").is_err());
225    }
226
227    // -- Arch round-trip -----------------------------------------------------
228
229    #[test]
230    fn arch_display_roundtrip() {
231        for arch in [Arch::X86_64, Arch::Arm64] {
232            let s = arch.to_string();
233            let parsed = Arch::from_str_loose(&s).unwrap();
234            assert_eq!(arch, parsed, "round-trip failed for {s}");
235        }
236    }
237
238    #[test]
239    fn arch_from_str_loose_aliases() {
240        assert_eq!(Arch::from_str_loose("x64").unwrap(), Arch::X86_64);
241        assert_eq!(Arch::from_str_loose("amd64").unwrap(), Arch::X86_64);
242        assert_eq!(Arch::from_str_loose("AMD64").unwrap(), Arch::X86_64);
243        assert_eq!(Arch::from_str_loose("aarch64").unwrap(), Arch::Arm64);
244        assert_eq!(Arch::from_str_loose("ARM64").unwrap(), Arch::Arm64);
245    }
246
247    #[test]
248    fn arch_from_str_loose_unknown() {
249        assert!(Arch::from_str_loose("mips").is_err());
250    }
251
252    // -- Target --------------------------------------------------------------
253
254    #[test]
255    fn target_current_succeeds() {
256        let t = Target::current().unwrap();
257        // We're running on a known platform, so the result must be valid.
258        let _ = t.platform;
259        let _ = t.arch;
260    }
261
262    #[test]
263    fn target_display() {
264        let t = Target::new(Platform::Linux, Arch::X86_64);
265        assert_eq!(t.to_string(), "linux-x86_64");
266
267        let t2 = Target::new(Platform::Darwin, Arch::Arm64);
268        assert_eq!(t2.to_string(), "darwin-arm64");
269    }
270
271    // -- format_size ---------------------------------------------------------
272
273    #[test]
274    fn format_size_bytes() {
275        assert_eq!(format_size(0), "0 B");
276        assert_eq!(format_size(512), "512 B");
277    }
278
279    #[test]
280    fn format_size_kilobytes() {
281        assert_eq!(format_size(1024), "1.0 KB");
282        assert_eq!(format_size(1536), "1.5 KB");
283    }
284
285    #[test]
286    fn format_size_megabytes() {
287        assert_eq!(format_size(52_428_800), "50.0 MB");
288        // 52.3 MB = 52.3 * 1024 * 1024 = 54_843_597 (approx)
289        let mb_52_3 = (52.3 * 1024.0 * 1024.0) as u64;
290        assert_eq!(format_size(mb_52_3), "52.3 MB");
291    }
292
293    #[test]
294    fn format_size_gigabytes() {
295        assert_eq!(format_size(1_073_741_824), "1.0 GB");
296    }
297
298    // -- format_duration -----------------------------------------------------
299
300    #[test]
301    fn format_duration_zero() {
302        assert_eq!(format_duration(Duration::ZERO), "0ms");
303    }
304
305    #[test]
306    fn format_duration_millis_only() {
307        assert_eq!(format_duration(Duration::from_millis(250)), "250ms");
308    }
309
310    #[test]
311    fn format_duration_fractional_seconds() {
312        assert_eq!(format_duration(Duration::from_millis(1500)), "1.5s");
313    }
314
315    #[test]
316    fn format_duration_whole_seconds() {
317        assert_eq!(format_duration(Duration::from_secs(5)), "5s");
318    }
319
320    #[test]
321    fn format_duration_minutes_and_seconds() {
322        assert_eq!(format_duration(Duration::from_secs(204)), "3m 24s");
323    }
324
325    #[test]
326    fn format_duration_exact_minutes() {
327        assert_eq!(format_duration(Duration::from_secs(120)), "2m");
328    }
329
330    // -- Serde ---------------------------------------------------------------
331
332    #[test]
333    fn platform_serde_json_roundtrip() {
334        let p = Platform::Darwin;
335        let json = serde_json::to_string(&p).unwrap();
336        assert_eq!(json, "\"darwin\"");
337        let p2: Platform = serde_json::from_str(&json).unwrap();
338        assert_eq!(p, p2);
339    }
340
341    #[test]
342    fn arch_serde_json_roundtrip() {
343        let a = Arch::X86_64;
344        let json = serde_json::to_string(&a).unwrap();
345        assert_eq!(json, "\"x86_64\"");
346        let a2: Arch = serde_json::from_str(&json).unwrap();
347        assert_eq!(a, a2);
348    }
349}