Skip to main content

npmgen_core/target/
mod.rs

1//! Build targets: the resolved [`Target`] domain type and the [`TargetResolver`]
2//! that derives the target set by precedence.
3
4mod defaults;
5mod resolver;
6
7pub use resolver::TargetResolver;
8
9use std::path::{Path, PathBuf};
10
11use crate::config::TargetSpec;
12
13/// Cargo profile directory the release build lands in; must agree with the
14/// `--release` flag the build driver passes.
15pub(crate) const RELEASE_PROFILE_DIR: &str = "release";
16
17/// A resolved platform: the npm key, its npm `os`/`cpu` install filters, the
18/// Rust target triple to build, and whether the binary carries a `.exe`.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Target {
21    pub key: String,
22    pub os: String,
23    pub cpu: String,
24    pub triple: String,
25    pub windows: bool,
26}
27
28impl Target {
29    /// Build a target by decomposing a Rust target triple into npm os/cpu/key.
30    pub fn from_triple(triple: &str) -> Result<Self, TargetError> {
31        let segments: Vec<&str> = triple.split('-').collect();
32        let arch = segments.first().copied().unwrap_or_default();
33        let cpu = Self::cpu_for_arch(arch).ok_or_else(|| TargetError::UnknownTriple {
34            triple: triple.to_owned(),
35        })?;
36        // Match the right-most system token: Rust triples place the OS after the
37        // kernel, so `aarch64-linux-android` is android, not linux.
38        let (system, os) = segments
39            .iter()
40            .rev()
41            .find_map(|segment| Self::os_for_system(segment).map(|os| (*segment, os)))
42            .ok_or_else(|| TargetError::UnknownTriple {
43                triple: triple.to_owned(),
44            })?;
45
46        Ok(Self {
47            key: format!("{os}-{cpu}"),
48            os: os.to_owned(),
49            cpu: cpu.to_owned(),
50            triple: triple.to_owned(),
51            windows: Self::is_windows_system(system),
52        })
53    }
54
55    /// Build a target from a user spec: the triple defaults from the key, and the
56    /// os/cpu default from the key's two segments.
57    pub fn from_spec(spec: &TargetSpec) -> Result<Self, TargetError> {
58        let triple = spec
59            .triple
60            .clone()
61            .or_else(|| Self::default_triple(&spec.key).map(str::to_owned))
62            .ok_or_else(|| TargetError::UnknownTargetKey {
63                key: spec.key.clone(),
64            })?;
65
66        let (key_os, key_cpu) =
67            spec.key
68                .split_once('-')
69                .ok_or_else(|| TargetError::InvalidKey {
70                    key: spec.key.clone(),
71                })?;
72        let os = spec.os.clone().unwrap_or_else(|| key_os.to_owned());
73        let cpu = spec.cpu.clone().unwrap_or_else(|| key_cpu.to_owned());
74
75        Ok(Self {
76            windows: Self::is_windows_os(&os),
77            key: spec.key.clone(),
78            os,
79            cpu,
80            triple,
81        })
82    }
83
84    /// The default platform set: every entry of the key/triple table.
85    pub(super) fn defaults() -> Vec<Self> {
86        defaults::KEY_TRIPLES
87            .iter()
88            .map(|(_, triple)| Self::from_triple(triple).expect("default triples decode"))
89            .collect()
90    }
91
92    /// File name of `stem` for this platform (appends `.exe` on Windows).
93    pub fn binary_filename(&self, stem: &str) -> String {
94        if self.windows {
95            format!("{stem}.exe")
96        } else {
97            stem.to_owned()
98        }
99    }
100
101    /// Path of the compiled `bin` under cargo's target directory:
102    /// `<target_dir>/<triple>/release/<bin>[.exe]`.
103    pub fn binary_path(&self, target_directory: &Path, bin: &str) -> PathBuf {
104        target_directory
105            .join(&self.triple)
106            .join(RELEASE_PROFILE_DIR)
107            .join(self.binary_filename(bin))
108    }
109
110    fn default_triple(key: &str) -> Option<&'static str> {
111        Self::lookup(defaults::KEY_TRIPLES, key)
112    }
113
114    fn cpu_for_arch(arch: &str) -> Option<&'static str> {
115        Self::lookup(defaults::ARCH_CPU, arch)
116    }
117
118    fn os_for_system(system: &str) -> Option<&'static str> {
119        Self::lookup(defaults::SYSTEM_OS, system)
120    }
121
122    fn is_windows_system(system: &str) -> bool {
123        system == defaults::WINDOWS_SYSTEM
124    }
125
126    fn is_windows_os(os: &str) -> bool {
127        os == defaults::WINDOWS_OS
128    }
129
130    fn lookup(table: &[(&str, &'static str)], needle: &str) -> Option<&'static str> {
131        table
132            .iter()
133            .find(|(key, _)| *key == needle)
134            .map(|(_, value)| *value)
135    }
136}
137
138/// Failures resolving the target set.
139#[derive(Debug, thiserror::Error)]
140pub enum TargetError {
141    #[error("target key {key:?} is not of the form <os>-<cpu>")]
142    InvalidKey { key: String },
143
144    #[error("no default triple for target key {key:?}; declare `triple` explicitly")]
145    UnknownTargetKey { key: String },
146
147    #[error("cannot derive os/cpu from target triple {triple:?}")]
148    UnknownTriple { triple: String },
149
150    #[error("--target {key:?} matches none of the resolved targets")]
151    UnknownFilterKey { key: String },
152
153    #[error("reading cargo config {}", path.display())]
154    CargoConfig {
155        path: PathBuf,
156        #[source]
157        source: std::io::Error,
158    },
159
160    #[error("parsing cargo config {}", path.display())]
161    CargoConfigParse {
162        path: PathBuf,
163        #[source]
164        source: toml::de::Error,
165    },
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use std::path::Path;
172
173    #[test]
174    fn decomposes_known_triples() {
175        let windows = Target::from_triple("x86_64-pc-windows-msvc").unwrap();
176        assert_eq!(windows.key, "win32-x64");
177        assert_eq!(windows.os, "win32");
178        assert_eq!(windows.cpu, "x64");
179        assert!(windows.windows);
180
181        let mac = Target::from_triple("aarch64-apple-darwin").unwrap();
182        assert_eq!(mac.key, "darwin-arm64");
183        assert!(!mac.windows);
184
185        let linux = Target::from_triple("x86_64-unknown-linux-gnu").unwrap();
186        assert_eq!(linux.key, "linux-x64");
187
188        // Two system tokens: the rightmost (android) wins over linux.
189        let android = Target::from_triple("aarch64-linux-android").unwrap();
190        assert_eq!(android.key, "android-arm64");
191        assert_eq!(android.os, "android");
192        assert!(!android.windows);
193    }
194
195    #[test]
196    fn rejects_undecodable_triple() {
197        assert!(Target::from_triple("sparc-unknown-haiku").is_err());
198    }
199
200    #[test]
201    fn spec_defaults_triple_from_key() {
202        let spec = TargetSpec {
203            key: "win32-arm64".to_owned(),
204            triple: None,
205            os: None,
206            cpu: None,
207        };
208        let target = Target::from_spec(&spec).unwrap();
209        assert_eq!(target.triple, "aarch64-pc-windows-msvc");
210        assert_eq!(target.os, "win32");
211        assert_eq!(target.cpu, "arm64");
212        assert!(target.windows);
213    }
214
215    #[test]
216    fn spec_without_default_triple_needs_explicit_one() {
217        let spec = TargetSpec {
218            key: "haiku-x64".to_owned(),
219            triple: None,
220            os: None,
221            cpu: None,
222        };
223        assert!(Target::from_spec(&spec).is_err());
224    }
225
226    #[test]
227    fn binary_filename_appends_exe_on_windows_only() {
228        let windows = Target::from_triple("x86_64-pc-windows-msvc").unwrap();
229        let linux = Target::from_triple("x86_64-unknown-linux-gnu").unwrap();
230        assert_eq!(windows.binary_filename("tool"), "tool.exe");
231        assert_eq!(linux.binary_filename("tool"), "tool");
232        assert_eq!(
233            linux.binary_path(Path::new("/t"), "tool"),
234            Path::new("/t/x86_64-unknown-linux-gnu/release/tool")
235        );
236    }
237}