target_spec/
custom.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Parse custom platforms.
5
6use std::borrow::Cow;
7
8use cfg_expr::targets::{
9    Abi, Arch, Env, Families, Family, HasAtomic, HasAtomics, Os, TargetInfo, Triple, Vendor,
10};
11use serde::{Deserialize, Serialize};
12
13#[derive(Clone, Debug, Deserialize, Serialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
14#[serde(rename_all = "kebab-case")]
15pub(crate) struct TargetDefinition {
16    // TODO: it would be nice to use target-spec-json for this, but that has a
17    // few limitations as of v0.1:
18    //
19    // * target-pointer-width is a string before roughly nightly-2025-10-12 (it
20    //   was changed to an integer after that).
21    // * Os and Env deserialized to enums, but we would really like them to be strings.
22    //
23    // ---
24    arch: String,
25    #[serde(rename = "target-pointer-width", with = "target_pointer_width")]
26    pointer_width: u8,
27
28    // These parameters are not used by target-spec but are mandatory in Target, so we require them
29    // here. https://doc.rust-lang.org/nightly/nightly-rustc/rustc_target/spec/struct.Target.html
30    #[allow(dead_code)]
31    llvm_target: String,
32    #[allow(dead_code)]
33    data_layout: String,
34
35    // These are optional parameters used by target-spec.
36    #[serde(default)]
37    os: Option<String>,
38    #[serde(default)]
39    abi: Option<String>,
40    #[serde(default)]
41    env: Option<String>,
42    #[serde(default)]
43    vendor: Option<String>,
44    #[serde(default)]
45    target_family: Vec<String>,
46    #[serde(default)]
47    target_endian: Endian,
48    #[serde(default)]
49    min_atomic_width: Option<u16>,
50    #[serde(default)]
51    max_atomic_width: Option<u16>,
52    #[serde(default)]
53    panic_strategy: PanicStrategy,
54}
55
56impl TargetDefinition {
57    pub(crate) fn into_target_info(self, triple: Cow<'static, str>) -> TargetInfo {
58        // Per https://doc.rust-lang.org/nightly/nightly-rustc/src/rustc_target/spec/mod.rs.html,
59        // the default value for min_atomic_width is 8.
60        let min_atomic_width = self.min_atomic_width.unwrap_or(8);
61        // The default max atomic width is the pointer width.
62        let max_atomic_width = self.max_atomic_width.unwrap_or(self.pointer_width as u16);
63
64        let mut has_atomics = Vec::new();
65        // atomic_width should always be a power of two, but rather than checking that we just
66        // start counting up from 8.
67        let mut atomic_width = 8;
68        while atomic_width <= max_atomic_width {
69            if atomic_width < min_atomic_width {
70                atomic_width *= 2;
71                continue;
72            }
73            has_atomics.push(HasAtomic::IntegerSize(atomic_width));
74            if atomic_width == self.pointer_width as u16 {
75                has_atomics.push(HasAtomic::Pointer);
76            }
77            atomic_width *= 2;
78        }
79
80        TargetInfo {
81            triple: Triple::new(triple),
82            os: self.os.map(Os::new),
83            abi: self.abi.map(Abi::new),
84            arch: Arch::new(self.arch),
85            env: self.env.map(Env::new),
86            vendor: self.vendor.map(Vendor::new),
87            families: Families::new(self.target_family.into_iter().map(Family::new)),
88            pointer_width: self.pointer_width,
89            endian: self.target_endian.to_cfg_expr(),
90            has_atomics: HasAtomics::new(has_atomics),
91            panic: self.panic_strategy.to_cfg_expr(),
92        }
93    }
94}
95
96mod target_pointer_width {
97    use serde::{Deserializer, Serializer};
98
99    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<u8, D::Error>
100    where
101        D: Deserializer<'de>,
102    {
103        use std::fmt;
104
105        struct PointerWidthVisitor;
106
107        impl<'de> serde::de::Visitor<'de> for PointerWidthVisitor {
108            type Value = u8;
109
110            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
111                formatter.write_str("a string or integer representing pointer width")
112            }
113
114            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
115            where
116                E: serde::de::Error,
117            {
118                value
119                    .try_into()
120                    .map_err(|_| E::custom(format!("pointer width {value} out of range for u8")))
121            }
122
123            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
124            where
125                E: serde::de::Error,
126            {
127                value
128                    .parse::<u8>()
129                    .map_err(|error| E::custom(format!("error parsing as integer: {error}")))
130            }
131        }
132
133        deserializer.deserialize_any(PointerWidthVisitor)
134    }
135
136    pub(super) fn serialize<S>(value: &u8, serializer: S) -> Result<S::Ok, S::Error>
137    where
138        S: Serializer,
139    {
140        // Should change this in the future to serialize as an integer?
141        serializer.serialize_str(&value.to_string())
142    }
143}
144
145#[derive(
146    Copy, Clone, Debug, Deserialize, Serialize, Default, Eq, Hash, Ord, PartialEq, PartialOrd,
147)]
148#[serde(rename_all = "kebab-case")]
149enum Endian {
150    #[default]
151    Little,
152    Big,
153}
154
155impl Endian {
156    fn to_cfg_expr(self) -> cfg_expr::targets::Endian {
157        match self {
158            Self::Little => cfg_expr::targets::Endian::little,
159            Self::Big => cfg_expr::targets::Endian::big,
160        }
161    }
162}
163
164#[derive(
165    Copy, Clone, Debug, Deserialize, Serialize, Default, Eq, Hash, Ord, PartialEq, PartialOrd,
166)]
167#[serde(rename_all = "kebab-case")]
168enum PanicStrategy {
169    #[default]
170    Unwind,
171    Abort,
172}
173
174impl PanicStrategy {
175    fn to_cfg_expr(self) -> cfg_expr::targets::Panic {
176        match self {
177            Self::Unwind => cfg_expr::targets::Panic::unwind,
178            Self::Abort => cfg_expr::targets::Panic::abort,
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use std::{collections::BTreeMap, process::Command};
187
188    #[derive(Deserialize)]
189    #[serde(transparent)]
190    struct AllTargets(BTreeMap<String, TargetDefinition>);
191
192    #[test]
193    fn test_all_builtin_specs_recognized() {
194        let rustc_bin: String = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
195        let output = Command::new(rustc_bin)
196            // Used for -Zunstable-options. This is test-only code so it doesn't matter.
197            .env("RUSTC_BOOTSTRAP", "1")
198            .args(["-Z", "unstable-options", "--print", "all-target-specs-json"])
199            .output()
200            .expect("rustc command succeeded");
201        assert!(output.status.success(), "rustc command succeeded");
202
203        let all_targets: AllTargets = serde_json::from_slice(&output.stdout)
204            .expect("deserializing all-target-specs-json succeeded");
205        for (triple, target_def) in all_targets.0 {
206            eprintln!("*** testing {triple}");
207            // Just make sure this doesn't panic. (If this becomes fallible in the future, then this
208            // shouldn't return an error either.)
209            target_def.clone().into_target_info(triple.clone().into());
210            let json =
211                serde_json::to_string(&target_def).expect("target def serialized successfully");
212            eprintln!("* minified json: {json}");
213            let target_def_2 = serde_json::from_str(&json).expect("target def 2 deserialized");
214            assert_eq!(target_def, target_def_2, "matches");
215
216            // Do some spot checks for things like big-endian targets.
217            if triple.starts_with("powerpc-") || triple.starts_with("powerpc64-") {
218                assert_eq!(
219                    target_def.target_endian,
220                    Endian::Big,
221                    "powerpc is big-endian"
222                );
223            }
224            if triple.contains("-linux") {
225                assert!(
226                    target_def.target_family.contains(&"unix".to_owned()),
227                    "linux target_family should contain unix (was {:#?})",
228                    target_def.target_family,
229                );
230            }
231        }
232    }
233}