multiversx_sc_meta_lib/tools/
rustc_version.rs

1use std::{process::Command, str::FromStr};
2
3use multiversx_sc::abi::RustcAbi;
4use rustc_version::{LlvmVersion, VersionMeta};
5use semver::Version;
6
7/// Contains a representation of a Rust compiler (toolchain) version.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct RustcVersion {
10    pub version_meta: VersionMeta,
11    pub short_string: String,
12}
13
14impl RustcVersion {
15    /// Parses the rustc version from sc-config.toml.
16    pub fn from_opt_sc_config_serde(opt_serde_version: &Option<String>) -> Self {
17        if let Some(serde_version) = opt_serde_version {
18            Self::from_sc_config_serde(serde_version)
19        } else {
20            Self::current_version()
21        }
22    }
23
24    pub fn from_sc_config_serde(serde_version: &str) -> Self {
25        let version_meta = get_version_meta_for_toolchain(serde_version);
26        RustcVersion {
27            version_meta,
28            short_string: serde_version.to_owned(),
29        }
30    }
31
32    /// Retrieves the current rustc version from crate `rustc_version`.
33    ///
34    /// The value is embedded into the binary at compile time.
35    pub fn current_version() -> RustcVersion {
36        let version_meta =
37            rustc_version::version_meta().expect("failed to get rustc version metadata");
38        let short_string = rustc_version_to_string(&version_meta);
39        RustcVersion {
40            version_meta,
41            short_string,
42        }
43    }
44
45    /// Formats as a CLI for cargo or rustup, e.g. `cargo +1.88 build`.
46    pub fn to_cli_arg(&self) -> String {
47        format!("+{}", self.short_string)
48    }
49
50    pub fn to_abi(&self) -> RustcAbi {
51        RustcAbi {
52            version: self.version_meta.semver.to_string(),
53            commit_hash: self.version_meta.commit_hash.clone().unwrap_or_default(),
54            commit_date: self.version_meta.commit_date.clone().unwrap_or_default(),
55            build_date: self.version_meta.build_date.clone(),
56            channel: format!("{:?}", self.version_meta.channel),
57            host: self.version_meta.host.clone(),
58            short: self.version_meta.short_version_string.clone(),
59            llvm_version: self
60                .version_meta
61                .llvm_version
62                .clone()
63                .map(|llvm_version| llvm_version.to_string()),
64        }
65    }
66
67    pub fn from_abi(abi: &RustcAbi) -> Self {
68        let semver = Version::parse(&abi.version).expect("failed to parse version");
69        let channel = match abi.channel.as_str() {
70            "Stable" => rustc_version::Channel::Stable,
71            "Nightly" => rustc_version::Channel::Nightly,
72            _ => panic!("unsupported channel: {}", abi.channel),
73        };
74
75        RustcVersion {
76            version_meta: VersionMeta {
77                semver,
78                channel,
79                commit_hash: if abi.commit_hash.is_empty() {
80                    None
81                } else {
82                    Some(abi.commit_hash.clone())
83                },
84                commit_date: if abi.commit_date.is_empty() {
85                    None
86                } else {
87                    Some(abi.commit_date.clone())
88                },
89                build_date: abi.build_date.clone(),
90                host: abi.host.clone(),
91                short_version_string: abi.short.clone(),
92                llvm_version: abi.llvm_version.clone().map(|llvm_version_string| {
93                    LlvmVersion::from_str(&llvm_version_string)
94                        .expect("failed to parse LLVM version")
95                }),
96            },
97            short_string: abi.short.clone(),
98        }
99    }
100}
101
102impl std::fmt::Display for RustcVersion {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(f, "{}", self.short_string)
105    }
106}
107
108fn rustc_version_to_string(version_meta: &VersionMeta) -> String {
109    match version_meta.channel {
110        rustc_version::Channel::Stable => format!(
111            "{}-{}",
112            version_to_string(&version_meta.semver),
113            version_meta.host
114        ),
115        rustc_version::Channel::Nightly => {
116            if let Some(build_date) = &version_meta.build_date {
117                format!("nightly-{}-{}", build_date, version_meta.host)
118            } else {
119                "nightly".to_owned()
120            }
121        }
122        _ => panic!("only stable and nightly supported"),
123    }
124}
125
126/// Outputs major.minor if the other fields are zero or missing. Outputs the full string otherwise.
127fn version_to_string(version: &Version) -> String {
128    if version.patch == 0 && version.pre.is_empty() && version.build.is_empty() {
129        format!("{}.{}", version.major, version.minor)
130    } else {
131        version.to_string()
132    }
133}
134
135/// Gets the VersionMeta for a specific toolchain identifier, by calling `rustc -vV` with that toolchain.
136fn get_version_meta_for_toolchain(toolchain: &str) -> VersionMeta {
137    // Run rustc with the specific toolchain
138    let output = Command::new("rustc")
139        .arg(format!("+{}", toolchain))
140        .arg("-vV")
141        .output()
142        .expect("failed to call rustc to get full toolchain info");
143
144    if !output.status.success() {
145        panic!(
146            "rustc -vV failed: {}",
147            String::from_utf8_lossy(&output.stderr)
148        );
149    }
150
151    // Parse the version string into VersionMeta
152    let version_string =
153        String::from_utf8(output.stdout).expect("failed to parse rustc -vV output as UTF-8");
154    rustc_version::version_meta_for(&version_string)
155        .unwrap_or_else(|_| panic!("failed to parse rustc -vV output: {version_string}"))
156}