Skip to main content

weaveffi_core/
platform.rs

1//! The target-platform model that the packaging pipeline shares.
2//!
3//! `weaveffi generate` emits binding *source*; `weaveffi package` goes further
4//! and assembles publishable packages that bundle a prebuilt native library for
5//! each platform. To do that without every backend re-deriving the same facts,
6//! this module is the single source of truth for the supported platforms and
7//! the per-ecosystem identifiers each one maps to:
8//!
9//! * the Rust target triple (`aarch64-apple-darwin`, …) used by `--build`;
10//! * the shared-library file name (`libfoo.dylib`, `foo.dll`, …);
11//! * the NuGet runtime identifier (`osx-arm64`, …);
12//! * the Node.js `process.platform`/`process.arch` tokens;
13//! * the Python wheel platform tag (`macosx_11_0_arm64`, …); and
14//! * the RubyGems platform string (`arm64-darwin`, …).
15//!
16//! A [`BinarySet`] pairs each [`Platform`] with the on-disk path to its
17//! prebuilt library; the [`crate::package`] driver and every packaging backend
18//! consume it.
19
20use camino::Utf8PathBuf;
21
22/// The operating-system family of a [`Platform`].
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub enum Os {
25    /// Apple platforms (macOS); shared libraries are `.dylib`.
26    MacOs,
27    /// Linux with the GNU C library (glibc); shared libraries are `.so`.
28    Linux,
29    /// Windows; shared libraries are `.dll`.
30    Windows,
31}
32
33/// The CPU architecture of a [`Platform`].
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
35pub enum Arch {
36    /// 64-bit x86 (`x86_64` / `amd64`).
37    X64,
38    /// 64-bit ARM (`aarch64` / `arm64`).
39    Arm64,
40}
41
42/// A single native target platform WeaveFFI can build for and bundle a
43/// prebuilt library into a published package.
44///
45/// The v1 matrix is macOS (arm64 and x64), Linux glibc (x64 and arm64), and
46/// Windows (x64). Each variant carries a stable [`id`](Self::id) used both as
47/// the `--platforms` token and as the per-platform subdirectory name in the
48/// `--binaries` input layout (`<dir>/<id>/<library>`).
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
50pub enum Platform {
51    /// macOS on Apple silicon (`aarch64-apple-darwin`).
52    MacosArm64,
53    /// macOS on Intel (`x86_64-apple-darwin`).
54    MacosX64,
55    /// Linux glibc on x86-64 (`x86_64-unknown-linux-gnu`).
56    LinuxX64,
57    /// Linux glibc on ARM64 (`aarch64-unknown-linux-gnu`).
58    LinuxArm64,
59    /// Windows on x86-64 (`x86_64-pc-windows-msvc`).
60    WindowsX64,
61}
62
63impl Platform {
64    /// Every platform in the v1 support matrix, in a stable order.
65    pub const ALL: [Platform; 5] = [
66        Platform::MacosArm64,
67        Platform::MacosX64,
68        Platform::LinuxX64,
69        Platform::LinuxArm64,
70        Platform::WindowsX64,
71    ];
72
73    /// The stable WeaveFFI platform identifier, used as the `--platforms` token
74    /// and the `--binaries` subdirectory name (`darwin-arm64`, `darwin-x64`,
75    /// `linux-x64`, `linux-arm64`, `windows-x64`).
76    pub fn id(self) -> &'static str {
77        match self {
78            Platform::MacosArm64 => "darwin-arm64",
79            Platform::MacosX64 => "darwin-x64",
80            Platform::LinuxX64 => "linux-x64",
81            Platform::LinuxArm64 => "linux-arm64",
82            Platform::WindowsX64 => "windows-x64",
83        }
84    }
85
86    /// Parse a [`Platform`] from its [`id`](Self::id), returning `None` for an
87    /// unrecognized token.
88    pub fn from_id(s: &str) -> Option<Platform> {
89        Platform::ALL.into_iter().find(|p| p.id() == s)
90    }
91
92    /// The operating-system family.
93    pub fn os(self) -> Os {
94        match self {
95            Platform::MacosArm64 | Platform::MacosX64 => Os::MacOs,
96            Platform::LinuxX64 | Platform::LinuxArm64 => Os::Linux,
97            Platform::WindowsX64 => Os::Windows,
98        }
99    }
100
101    /// The CPU architecture.
102    pub fn arch(self) -> Arch {
103        match self {
104            Platform::MacosArm64 | Platform::LinuxArm64 => Arch::Arm64,
105            Platform::MacosX64 | Platform::LinuxX64 | Platform::WindowsX64 => Arch::X64,
106        }
107    }
108
109    /// The Rust target triple used to cross-compile a producer for this
110    /// platform with `weaveffi package --build`.
111    pub fn rust_target(self) -> &'static str {
112        match self {
113            Platform::MacosArm64 => "aarch64-apple-darwin",
114            Platform::MacosX64 => "x86_64-apple-darwin",
115            Platform::LinuxX64 => "x86_64-unknown-linux-gnu",
116            Platform::LinuxArm64 => "aarch64-unknown-linux-gnu",
117            Platform::WindowsX64 => "x86_64-pc-windows-msvc",
118        }
119    }
120
121    /// Resolve a [`Platform`] from a Rust target triple, returning `None` for a
122    /// triple outside the support matrix.
123    pub fn from_rust_target(triple: &str) -> Option<Platform> {
124        Platform::ALL
125            .into_iter()
126            .find(|p| p.rust_target() == triple)
127    }
128
129    /// The shared-library filename prefix: `"lib"` on Unix, empty on Windows.
130    pub fn lib_prefix(self) -> &'static str {
131        match self.os() {
132            Os::MacOs | Os::Linux => "lib",
133            Os::Windows => "",
134        }
135    }
136
137    /// The shared-library filename extension (without the dot): `dylib`, `so`,
138    /// or `dll`.
139    pub fn lib_extension(self) -> &'static str {
140        match self.os() {
141            Os::MacOs => "dylib",
142            Os::Linux => "so",
143            Os::Windows => "dll",
144        }
145    }
146
147    /// The platform-correct shared-library filename for a logical base name.
148    ///
149    /// `Platform::MacosArm64.lib_filename("contacts")` is `libcontacts.dylib`;
150    /// `Platform::WindowsX64.lib_filename("contacts")` is `contacts.dll`.
151    pub fn lib_filename(self, base: &str) -> String {
152        format!("{}{base}.{}", self.lib_prefix(), self.lib_extension())
153    }
154
155    /// The NuGet runtime identifier (RID) for the `runtimes/<rid>/native/`
156    /// layout: `osx-arm64`, `osx-x64`, `linux-x64`, `linux-arm64`, `win-x64`.
157    pub fn nuget_rid(self) -> &'static str {
158        match self {
159            Platform::MacosArm64 => "osx-arm64",
160            Platform::MacosX64 => "osx-x64",
161            Platform::LinuxX64 => "linux-x64",
162            Platform::LinuxArm64 => "linux-arm64",
163            Platform::WindowsX64 => "win-x64",
164        }
165    }
166
167    /// The Node.js `process.platform` value (`darwin`, `linux`, `win32`).
168    pub fn node_os(self) -> &'static str {
169        match self.os() {
170            Os::MacOs => "darwin",
171            Os::Linux => "linux",
172            Os::Windows => "win32",
173        }
174    }
175
176    /// The Node.js `process.arch` value (`arm64`, `x64`).
177    pub fn node_cpu(self) -> &'static str {
178        match self.arch() {
179            Arch::Arm64 => "arm64",
180            Arch::X64 => "x64",
181        }
182    }
183
184    /// The Python wheel platform tag (the final segment of a wheel filename),
185    /// for example `macosx_11_0_arm64` or `manylinux2014_x86_64`.
186    pub fn python_platform_tag(self) -> &'static str {
187        match self {
188            Platform::MacosArm64 => "macosx_11_0_arm64",
189            Platform::MacosX64 => "macosx_10_12_x86_64",
190            Platform::LinuxX64 => "manylinux2014_x86_64",
191            Platform::LinuxArm64 => "manylinux2014_aarch64",
192            Platform::WindowsX64 => "win_amd64",
193        }
194    }
195
196    /// The RubyGems platform string used for a precompiled platform gem, for
197    /// example `arm64-darwin` or `x86_64-linux`.
198    pub fn ruby_platform(self) -> &'static str {
199        match self {
200            Platform::MacosArm64 => "arm64-darwin",
201            Platform::MacosX64 => "x86_64-darwin",
202            Platform::LinuxX64 => "x86_64-linux",
203            Platform::LinuxArm64 => "aarch64-linux",
204            Platform::WindowsX64 => "x64-mingw-ucrt",
205        }
206    }
207
208    /// A short human-readable label (`macOS arm64`, `Linux x64`, …) for
209    /// progress and diagnostic messages.
210    pub fn display_name(self) -> &'static str {
211        match self {
212            Platform::MacosArm64 => "macOS arm64",
213            Platform::MacosX64 => "macOS x64",
214            Platform::LinuxX64 => "Linux x64",
215            Platform::LinuxArm64 => "Linux arm64",
216            Platform::WindowsX64 => "Windows x64",
217        }
218    }
219}
220
221/// One prebuilt native library: the [`Platform`] it targets and the on-disk
222/// path to the shared library file to bundle.
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct NativeBinary {
225    /// The platform this library was built for.
226    pub platform: Platform,
227    /// Absolute or relative path to the shared library file on disk.
228    pub source: Utf8PathBuf,
229}
230
231/// The set of prebuilt native libraries to bundle into a package, keyed by
232/// platform.
233///
234/// `lib_name` is the logical base name every generated loader, import name, and
235/// bundled filename is derived from (for example `contacts`, yielding
236/// `libcontacts.dylib` / `contacts.dll`). It is the resolved package identity,
237/// not the WeaveFFI brand, so the bundled file matches what the producer emits.
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct BinarySet {
240    /// Logical shared-library base name (resolved package identity).
241    pub lib_name: String,
242    /// One entry per platform that has a prebuilt library available.
243    pub binaries: Vec<NativeBinary>,
244}
245
246impl BinarySet {
247    /// Create a set with the given logical library base name and no binaries.
248    pub fn new(lib_name: impl Into<String>) -> Self {
249        Self {
250            lib_name: lib_name.into(),
251            binaries: Vec::new(),
252        }
253    }
254
255    /// Record the prebuilt library `source` for `platform`, replacing any
256    /// previous entry for the same platform.
257    pub fn insert(&mut self, platform: Platform, source: impl Into<Utf8PathBuf>) {
258        let source = source.into();
259        if let Some(existing) = self.binaries.iter_mut().find(|b| b.platform == platform) {
260            existing.source = source;
261        } else {
262            self.binaries.push(NativeBinary { platform, source });
263        }
264    }
265
266    /// The library built for `platform`, if present.
267    pub fn get(&self, platform: Platform) -> Option<&NativeBinary> {
268        self.binaries.iter().find(|b| b.platform == platform)
269    }
270
271    /// Every platform with a bundled library, in insertion order.
272    pub fn platforms(&self) -> impl Iterator<Item = Platform> + '_ {
273        self.binaries.iter().map(|b| b.platform)
274    }
275
276    /// True when no binaries have been recorded.
277    pub fn is_empty(&self) -> bool {
278        self.binaries.is_empty()
279    }
280
281    /// The bundled filename for `platform` under this set's `lib_name`
282    /// (`libcontacts.dylib`, `contacts.dll`, …).
283    pub fn bundled_filename(&self, platform: Platform) -> String {
284        platform.lib_filename(&self.lib_name)
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn ids_round_trip() {
294        for p in Platform::ALL {
295            assert_eq!(Platform::from_id(p.id()), Some(p));
296        }
297        assert_eq!(Platform::from_id("nonsense"), None);
298    }
299
300    #[test]
301    fn rust_targets_round_trip() {
302        for p in Platform::ALL {
303            assert_eq!(Platform::from_rust_target(p.rust_target()), Some(p));
304        }
305        assert_eq!(Platform::from_rust_target("mips-unknown-linux-gnu"), None);
306    }
307
308    #[test]
309    fn lib_filenames_are_platform_correct() {
310        assert_eq!(
311            Platform::MacosArm64.lib_filename("contacts"),
312            "libcontacts.dylib"
313        );
314        assert_eq!(
315            Platform::LinuxX64.lib_filename("contacts"),
316            "libcontacts.so"
317        );
318        assert_eq!(
319            Platform::WindowsX64.lib_filename("contacts"),
320            "contacts.dll"
321        );
322    }
323
324    #[test]
325    fn ecosystem_identifiers() {
326        assert_eq!(Platform::MacosArm64.nuget_rid(), "osx-arm64");
327        assert_eq!(Platform::WindowsX64.nuget_rid(), "win-x64");
328        assert_eq!(Platform::MacosX64.node_os(), "darwin");
329        assert_eq!(Platform::MacosX64.node_cpu(), "x64");
330        assert_eq!(Platform::WindowsX64.node_os(), "win32");
331        assert_eq!(
332            Platform::LinuxArm64.python_platform_tag(),
333            "manylinux2014_aarch64"
334        );
335        assert_eq!(Platform::MacosArm64.ruby_platform(), "arm64-darwin");
336        assert_eq!(Platform::LinuxX64.ruby_platform(), "x86_64-linux");
337    }
338
339    #[test]
340    fn binary_set_insert_get_and_replace() {
341        let mut set = BinarySet::new("contacts");
342        assert!(set.is_empty());
343        set.insert(Platform::MacosArm64, "/tmp/a/libcontacts.dylib");
344        set.insert(Platform::LinuxX64, "/tmp/b/libcontacts.so");
345        assert_eq!(set.binaries.len(), 2);
346
347        // Re-inserting the same platform replaces rather than duplicates.
348        set.insert(Platform::MacosArm64, "/tmp/c/libcontacts.dylib");
349        assert_eq!(set.binaries.len(), 2);
350        assert_eq!(
351            set.get(Platform::MacosArm64).unwrap().source.as_str(),
352            "/tmp/c/libcontacts.dylib"
353        );
354
355        let platforms: Vec<Platform> = set.platforms().collect();
356        assert_eq!(platforms, vec![Platform::MacosArm64, Platform::LinuxX64]);
357        assert_eq!(set.bundled_filename(Platform::WindowsX64), "contacts.dll");
358    }
359}