Skip to main content

soothfast_sdk/
target.rs

1//! Rust target triples → the platform metadata each packaging format needs.
2//!
3//! npm and Python describe the same machine in different vocabularies —
4//! `os`/`cpu`/`libc` triples on one side, wheel platform tags on the other.
5//! Both are derived here so the emitters and the build command cannot
6//! disagree about what `aarch64-apple-darwin` means.
7
8/// One cross-compilation target and how each ecosystem names it.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Target {
11    /// Rust target triple, as `cargo build --target` takes it.
12    pub triple: &'static str,
13    /// npm `os` field.
14    pub os: &'static str,
15    /// npm `cpu` field.
16    pub cpu: &'static str,
17    /// npm `libc` field; only Linux distinguishes.
18    pub libc: Option<&'static str>,
19    /// Python wheel platform tag.
20    ///
21    /// A `manylinux_*` tag is a claim about the *build* environment, not
22    /// about this table: it holds only when the binary was linked against
23    /// a glibc at least that old. `sdk build` warns when it cannot tell
24    /// that it was.
25    pub wheel_platform: &'static str,
26    /// Executable suffix on this target.
27    pub exe_suffix: &'static str,
28}
29
30impl Target {
31    /// Every target the table knows how to describe.
32    pub const ALL: &'static [Target] = &[
33        Target {
34            triple: "aarch64-apple-darwin",
35            os: "darwin",
36            cpu: "arm64",
37            libc: None,
38            wheel_platform: "macosx_11_0_arm64",
39            exe_suffix: "",
40        },
41        Target {
42            triple: "aarch64-pc-windows-msvc",
43            os: "win32",
44            cpu: "arm64",
45            libc: None,
46            wheel_platform: "win_arm64",
47            exe_suffix: ".exe",
48        },
49        Target {
50            triple: "aarch64-unknown-linux-gnu",
51            os: "linux",
52            cpu: "arm64",
53            libc: Some("glibc"),
54            wheel_platform: "manylinux_2_17_aarch64",
55            exe_suffix: "",
56        },
57        Target {
58            triple: "aarch64-unknown-linux-musl",
59            os: "linux",
60            cpu: "arm64",
61            libc: Some("musl"),
62            wheel_platform: "musllinux_1_2_aarch64",
63            exe_suffix: "",
64        },
65        Target {
66            triple: "x86_64-apple-darwin",
67            os: "darwin",
68            cpu: "x64",
69            libc: None,
70            wheel_platform: "macosx_10_12_x86_64",
71            exe_suffix: "",
72        },
73        Target {
74            triple: "x86_64-pc-windows-msvc",
75            os: "win32",
76            cpu: "x64",
77            libc: None,
78            wheel_platform: "win_amd64",
79            exe_suffix: ".exe",
80        },
81        Target {
82            triple: "x86_64-unknown-linux-gnu",
83            os: "linux",
84            cpu: "x64",
85            libc: Some("glibc"),
86            wheel_platform: "manylinux_2_17_x86_64",
87            exe_suffix: "",
88        },
89        Target {
90            triple: "x86_64-unknown-linux-musl",
91            os: "linux",
92            cpu: "x64",
93            libc: Some("musl"),
94            wheel_platform: "musllinux_1_2_x86_64",
95            exe_suffix: "",
96        },
97    ];
98
99    /// The matrix used when an `[[sdk]]` entry names no `targets`: the
100    /// platforms a published package is expected to cover. musl is not in
101    /// it — it needs its own toolchain and most consumers never ask.
102    pub const DEFAULT: &'static [&'static str] = &[
103        "aarch64-apple-darwin",
104        "aarch64-unknown-linux-gnu",
105        "x86_64-apple-darwin",
106        "x86_64-pc-windows-msvc",
107        "x86_64-unknown-linux-gnu",
108    ];
109
110    /// Look a triple up in the table.
111    pub fn find(triple: &str) -> Option<&'static Target> {
112        Target::ALL.iter().find(|t| t.triple == triple)
113    }
114
115    /// The configured targets, or the default matrix when none are named.
116    /// Sorted and deduplicated, so emission stays deterministic.
117    pub fn matrix(configured: &[String]) -> Result<Vec<&'static Target>, String> {
118        let names: Vec<&str> = if configured.is_empty() {
119            Target::DEFAULT.to_vec()
120        } else {
121            configured.iter().map(String::as_str).collect()
122        };
123        let mut out = Vec::with_capacity(names.len());
124        for name in names {
125            let target = Target::find(name).ok_or_else(|| {
126                let known: Vec<&str> = Target::ALL.iter().map(|t| t.triple).collect();
127                format!(
128                    "unknown target {name:?} (expected one of {})",
129                    known.join(", ")
130                )
131            })?;
132            if !out.contains(&target) {
133                out.push(target);
134            }
135        }
136        out.sort_by_key(|t| t.triple);
137        Ok(out)
138    }
139
140    /// The npm sub-package suffix: `linux-x64`, `darwin-arm64`,
141    /// `linux-x64-musl`. Mirrors what esbuild and swc publish under.
142    pub fn npm_suffix(&self) -> String {
143        match self.libc {
144            Some("musl") => format!("{}-{}-musl", self.os, self.cpu),
145            _ => format!("{}-{}", self.os, self.cpu),
146        }
147    }
148
149    /// The full wheel tag hatchling wants.
150    pub fn wheel_tag(&self) -> String {
151        format!("py3-none-{}", self.wheel_platform)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn the_table_is_sorted_and_every_triple_is_distinct() {
161        let triples: Vec<&str> = Target::ALL.iter().map(|t| t.triple).collect();
162        let mut sorted = triples.clone();
163        sorted.sort_unstable();
164        sorted.dedup();
165        assert_eq!(triples, sorted, "keep ALL sorted and unique");
166    }
167
168    #[test]
169    fn the_default_matrix_is_all_resolvable() {
170        for triple in Target::DEFAULT {
171            assert!(Target::find(triple).is_some(), "{triple}");
172        }
173    }
174
175    #[test]
176    fn npm_suffixes_match_what_the_ecosystem_publishes() {
177        let suffix = |t: &str| Target::find(t).expect("known").npm_suffix();
178        assert_eq!(suffix("x86_64-unknown-linux-gnu"), "linux-x64");
179        assert_eq!(suffix("aarch64-apple-darwin"), "darwin-arm64");
180        assert_eq!(suffix("x86_64-pc-windows-msvc"), "win32-x64");
181        assert_eq!(suffix("x86_64-unknown-linux-musl"), "linux-x64-musl");
182    }
183
184    #[test]
185    fn glibc_and_musl_linux_never_collide() {
186        let gnu = Target::find("x86_64-unknown-linux-gnu").expect("known");
187        let musl = Target::find("x86_64-unknown-linux-musl").expect("known");
188        assert_ne!(gnu.npm_suffix(), musl.npm_suffix());
189        assert_ne!(gnu.wheel_tag(), musl.wheel_tag());
190    }
191
192    #[test]
193    fn an_empty_matrix_falls_back_to_the_default() {
194        let matrix = Target::matrix(&[]).expect("defaults resolve");
195        assert_eq!(matrix.len(), Target::DEFAULT.len());
196    }
197
198    #[test]
199    fn a_repeated_target_is_only_built_once() {
200        let matrix = Target::matrix(&[
201            "x86_64-unknown-linux-gnu".into(),
202            "x86_64-unknown-linux-gnu".into(),
203        ])
204        .expect("resolves");
205        assert_eq!(matrix.len(), 1);
206    }
207
208    #[test]
209    fn an_unknown_target_lists_the_ones_that_exist() {
210        let err = Target::matrix(&["sparc-unknown-none".into()]).expect_err("rejected");
211        assert!(err.contains("sparc"), "{err}");
212        assert!(err.contains("x86_64-unknown-linux-gnu"), "{err}");
213    }
214}