Skip to main content

cargo/util/
rustc.rs

1use crate::util::data_structures::HashMap;
2use std::env;
3use std::hash::{Hash, Hasher};
4use std::path::{Path, PathBuf};
5use std::sync::Mutex;
6
7use anyhow::{Context as _, bail};
8use cargo_util::{ProcessBuilder, ProcessError, paths};
9use filetime::FileTime;
10use serde::{Deserialize, Serialize};
11use tracing::{debug, info, warn};
12
13use crate::compiler::apply_env_config;
14use crate::util::interning::InternedString;
15use crate::util::{CargoResult, GlobalContext, StableHasher};
16
17/// Information on the `rustc` executable
18#[derive(Debug)]
19pub struct Rustc {
20    /// The location of the exe
21    pub path: PathBuf,
22    /// An optional program that will be passed the path of the rust exe as its first argument, and
23    /// rustc args following this.
24    pub wrapper: Option<PathBuf>,
25    /// An optional wrapper to be used in addition to `rustc.wrapper` for workspace crates
26    pub workspace_wrapper: Option<PathBuf>,
27    /// Verbose version information (the output of `rustc -vV`)
28    pub verbose_version: String,
29    /// The rustc version (`1.23.4-beta.2`), this comes from `verbose_version`.
30    pub version: semver::Version,
31    /// The host triple (arch-platform-OS), this comes from `verbose_version`.
32    pub host: InternedString,
33    /// The rustc full commit hash, this comes from `verbose_version`.
34    pub commit_hash: Option<String>,
35    cache: Mutex<Cache>,
36}
37
38impl Rustc {
39    /// Runs the compiler at `path` to learn various pieces of information about
40    /// it, with an optional wrapper.
41    ///
42    /// If successful this function returns a description of the compiler along
43    /// with a list of its capabilities.
44    #[tracing::instrument(skip(gctx))]
45    pub fn new(
46        path: PathBuf,
47        wrapper: Option<PathBuf>,
48        workspace_wrapper: Option<PathBuf>,
49        rustup_rustc: &Path,
50        cache_location: Option<PathBuf>,
51        gctx: &GlobalContext,
52    ) -> CargoResult<Rustc> {
53        let mut cache = Cache::load(
54            wrapper.as_deref(),
55            workspace_wrapper.as_deref(),
56            &path,
57            rustup_rustc,
58            cache_location,
59            gctx,
60        );
61
62        let mut cmd = ProcessBuilder::new(&path)
63            .wrapped(workspace_wrapper.as_ref())
64            .wrapped(wrapper.as_deref());
65        apply_env_config(gctx, &mut cmd)?;
66        cmd.env(crate::CARGO_ENV, gctx.cargo_exe()?);
67
68        cmd.arg("-vV");
69        let verbose_version = cache.cached_output(&cmd, 0)?.0;
70
71        let extract = |field: &str| -> CargoResult<&str> {
72            verbose_version
73                .lines()
74                .find_map(|l| l.strip_prefix(field))
75                .ok_or_else(|| {
76                    anyhow::format_err!(
77                        "`rustc -vV` didn't have a line for `{}`, got:\n{}",
78                        field.trim(),
79                        verbose_version
80                    )
81                })
82        };
83
84        let host = extract("host: ")?.into();
85        let version = semver::Version::parse(extract("release: ")?).with_context(|| {
86            format!(
87                "rustc version does not appear to be a valid semver version, from:\n{}",
88                verbose_version
89            )
90        })?;
91        let commit_hash = extract("commit-hash: ").ok().map(|hash| {
92            // Possible commit-hash values from rustc are SHA hex string and "unknown". See:
93            // * https://github.com/rust-lang/rust/blob/531cb83fc/src/bootstrap/src/utils/channel.rs#L73
94            // * https://github.com/rust-lang/rust/blob/531cb83fc/compiler/rustc_driver_impl/src/lib.rs#L911-L913
95            #[cfg(debug_assertions)]
96            if hash != "unknown" {
97                debug_assert!(
98                    hash.chars().all(|ch| ch.is_ascii_hexdigit()),
99                    "commit hash must be a hex string, got: {hash:?}"
100                );
101                debug_assert!(
102                    hash.len() == 40 || hash.len() == 64,
103                    "hex string must be generated from sha1 or sha256 (i.e., it must be 40 or 64 characters long)\ngot: {hash:?}"
104                );
105            }
106            hash.to_string()
107        });
108
109        Ok(Rustc {
110            path,
111            wrapper,
112            workspace_wrapper,
113            verbose_version,
114            version,
115            host,
116            commit_hash,
117            cache: Mutex::new(cache),
118        })
119    }
120
121    /// Gets a process builder set up to use the found rustc version, with a wrapper if `Some`.
122    pub fn process(&self) -> ProcessBuilder {
123        let mut cmd = ProcessBuilder::new(self.path.as_path()).wrapped(self.wrapper.as_ref());
124        cmd.retry_with_argfile(true);
125        cmd
126    }
127
128    /// Gets a process builder set up to use the found rustc version, with a wrapper if `Some`.
129    pub fn workspace_process(&self) -> ProcessBuilder {
130        let mut cmd = ProcessBuilder::new(self.path.as_path())
131            .wrapped(self.workspace_wrapper.as_ref())
132            .wrapped(self.wrapper.as_ref());
133        cmd.retry_with_argfile(true);
134        cmd
135    }
136
137    pub fn process_no_wrapper(&self) -> ProcessBuilder {
138        let mut cmd = ProcessBuilder::new(&self.path);
139        cmd.retry_with_argfile(true);
140        cmd
141    }
142
143    /// Gets the output for the given command.
144    ///
145    /// This will return the cached value if available, otherwise it will run
146    /// the command and cache the output.
147    ///
148    /// `extra_fingerprint` is extra data to include in the cache fingerprint.
149    /// Use this if there is other information about the environment that may
150    /// affect the output that is not part of `cmd`.
151    ///
152    /// Returns a tuple of strings `(stdout, stderr)`.
153    pub fn cached_output(
154        &self,
155        cmd: &ProcessBuilder,
156        extra_fingerprint: u64,
157    ) -> CargoResult<(String, String)> {
158        self.cache
159            .lock()
160            .unwrap()
161            .cached_output(cmd, extra_fingerprint)
162    }
163
164    /// Use the rustc executable to fetch the sysroot path.
165    pub fn sysroot(&self, gctx: &GlobalContext) -> CargoResult<PathBuf> {
166        let mut cmd = self.workspace_process();
167        apply_env_config(gctx, &mut cmd)?;
168        cmd.env(crate::CARGO_ENV, gctx.cargo_exe()?);
169        cmd.arg("--print=sysroot");
170
171        let (stdout, _) = self.cached_output(&cmd, 0)?;
172        let path: PathBuf = stdout.trim().into();
173        if !path.exists() {
174            bail!("sysroot path \"{}\" does not exist", path.display());
175        }
176        Ok(path)
177    }
178}
179
180/// It is a well known fact that `rustc` is not the fastest compiler in the
181/// world.  What is less known is that even `rustc --version --verbose` takes
182/// about a hundred milliseconds! Because we need compiler version info even
183/// for no-op builds, we cache it here, based on compiler's mtime and rustup's
184/// current toolchain.
185///
186/// <https://github.com/rust-lang/cargo/issues/5315>
187/// <https://github.com/rust-lang/rust/issues/49761>
188#[derive(Debug)]
189struct Cache {
190    cache_location: Option<PathBuf>,
191    dirty: bool,
192    data: CacheData,
193}
194
195#[derive(Serialize, Deserialize, Debug, Default)]
196struct CacheData {
197    rustc_fingerprint: u64,
198    outputs: HashMap<u64, Output>,
199    successes: HashMap<u64, bool>,
200}
201
202#[derive(Serialize, Deserialize, Debug)]
203struct Output {
204    success: bool,
205    status: String,
206    code: Option<i32>,
207    stdout: String,
208    stderr: String,
209}
210
211impl Cache {
212    fn load(
213        wrapper: Option<&Path>,
214        workspace_wrapper: Option<&Path>,
215        rustc: &Path,
216        rustup_rustc: &Path,
217        cache_location: Option<PathBuf>,
218        gctx: &GlobalContext,
219    ) -> Cache {
220        match (
221            cache_location,
222            rustc_fingerprint(wrapper, workspace_wrapper, rustc, rustup_rustc, gctx),
223        ) {
224            (Some(cache_location), Ok(rustc_fingerprint)) => {
225                let empty = CacheData {
226                    rustc_fingerprint,
227                    outputs: HashMap::default(),
228                    successes: HashMap::default(),
229                };
230                let mut dirty = true;
231                let data = match read(&cache_location) {
232                    Ok(data) => {
233                        if data.rustc_fingerprint == rustc_fingerprint {
234                            debug!("reusing existing rustc info cache");
235                            dirty = false;
236                            data
237                        } else {
238                            debug!("different compiler, creating new rustc info cache");
239                            empty
240                        }
241                    }
242                    Err(e) => {
243                        debug!("failed to read rustc info cache: {}", e);
244                        empty
245                    }
246                };
247                return Cache {
248                    cache_location: Some(cache_location),
249                    dirty,
250                    data,
251                };
252
253                fn read(path: &Path) -> CargoResult<CacheData> {
254                    let json = paths::read(path)?;
255                    Ok(serde_json::from_str(&json)?)
256                }
257            }
258            (_, fingerprint) => {
259                if let Err(e) = fingerprint {
260                    warn!("failed to calculate rustc fingerprint: {}", e);
261                }
262                debug!("rustc info cache disabled");
263                Cache {
264                    cache_location: None,
265                    dirty: false,
266                    data: CacheData::default(),
267                }
268            }
269        }
270    }
271
272    fn cached_output(
273        &mut self,
274        cmd: &ProcessBuilder,
275        extra_fingerprint: u64,
276    ) -> CargoResult<(String, String)> {
277        let key = process_fingerprint(cmd, extra_fingerprint);
278        if let std::collections::hash_map::Entry::Vacant(e) = self.data.outputs.entry(key) {
279            debug!("rustc info cache miss");
280            debug!("running {}", cmd);
281            let output = cmd.output()?;
282            let stdout = String::from_utf8(output.stdout)
283                .map_err(|e| anyhow::anyhow!("{}: {:?}", e, e.as_bytes()))
284                .with_context(|| format!("`{}` didn't return utf8 output", cmd))?;
285            let stderr = String::from_utf8(output.stderr)
286                .map_err(|e| anyhow::anyhow!("{}: {:?}", e, e.as_bytes()))
287                .with_context(|| format!("`{}` didn't return utf8 output", cmd))?;
288            e.insert(Output {
289                success: output.status.success(),
290                status: if output.status.success() {
291                    String::new()
292                } else {
293                    cargo_util::exit_status_to_string(output.status)
294                },
295                code: output.status.code(),
296                stdout,
297                stderr,
298            });
299            self.dirty = true;
300        } else {
301            debug!("rustc info cache hit");
302        }
303        let output = &self.data.outputs[&key];
304        if output.success {
305            Ok((output.stdout.clone(), output.stderr.clone()))
306        } else {
307            Err(ProcessError::new_raw(
308                &format!("process didn't exit successfully: {}", cmd),
309                output.code,
310                &output.status,
311                Some(output.stdout.as_ref()),
312                Some(output.stderr.as_ref()),
313            )
314            .into())
315        }
316    }
317}
318
319impl Drop for Cache {
320    fn drop(&mut self) {
321        if !self.dirty {
322            return;
323        }
324        if let Some(ref path) = self.cache_location {
325            let json = serde_json::to_string(&self.data).unwrap();
326            match paths::write(path, json.as_bytes()) {
327                Ok(()) => info!("updated rustc info cache"),
328                Err(e) => warn!("failed to update rustc info cache: {}", e),
329            }
330        }
331    }
332}
333
334fn rustc_fingerprint(
335    wrapper: Option<&Path>,
336    workspace_wrapper: Option<&Path>,
337    rustc: &Path,
338    rustup_rustc: &Path,
339    gctx: &GlobalContext,
340) -> CargoResult<u64> {
341    let mut hasher = StableHasher::new();
342
343    let hash_exe = |hasher: &mut _, path| -> CargoResult<()> {
344        let path = paths::resolve_executable(path)?;
345        path.hash(hasher);
346
347        let meta = paths::metadata(&path)?;
348        meta.len().hash(hasher);
349
350        // Often created and modified are the same, but not all filesystems support the former,
351        // and distro reproducible builds may clamp the latter, so we try to use both.
352        FileTime::from_creation_time(&meta).hash(hasher);
353        FileTime::from_last_modification_time(&meta).hash(hasher);
354        Ok(())
355    };
356
357    hash_exe(&mut hasher, rustc)?;
358    if let Some(wrapper) = wrapper {
359        hash_exe(&mut hasher, wrapper)?;
360    }
361    if let Some(workspace_wrapper) = workspace_wrapper {
362        hash_exe(&mut hasher, workspace_wrapper)?;
363    }
364
365    // Rustup can change the effective compiler without touching
366    // the `rustc` binary, so we try to account for this here.
367    // If we see rustup's env vars, we mix them into the fingerprint,
368    // but we also mix in the mtime of the actual compiler (and not
369    // the rustup shim at `~/.cargo/bin/rustup`), because `RUSTUP_TOOLCHAIN`
370    // could be just `stable-x86_64-unknown-linux-gnu`, i.e, it could
371    // not mention the version of Rust at all, which changes after
372    // `rustup update`.
373    //
374    // If we don't see rustup env vars, but it looks like the compiler
375    // is managed by rustup, we conservatively bail out.
376    let maybe_rustup = rustup_rustc == rustc;
377    match (
378        maybe_rustup,
379        gctx.get_env("RUSTUP_HOME"),
380        gctx.get_env("RUSTUP_TOOLCHAIN"),
381    ) {
382        (_, Ok(rustup_home), Ok(rustup_toolchain)) => {
383            debug!("adding rustup info to rustc fingerprint");
384            rustup_toolchain.hash(&mut hasher);
385            rustup_home.hash(&mut hasher);
386            let real_rustc = Path::new(&rustup_home)
387                .join("toolchains")
388                .join(rustup_toolchain)
389                .join("bin")
390                .join("rustc")
391                .with_extension(env::consts::EXE_EXTENSION);
392            paths::mtime(&real_rustc)?.hash(&mut hasher);
393        }
394        (true, _, _) => anyhow::bail!("probably rustup rustc, but without rustup's env vars"),
395        _ => (),
396    }
397
398    Ok(Hasher::finish(&hasher))
399}
400
401fn process_fingerprint(cmd: &ProcessBuilder, extra_fingerprint: u64) -> u64 {
402    let mut hasher = StableHasher::new();
403    extra_fingerprint.hash(&mut hasher);
404    cmd.get_args().for_each(|arg| arg.hash(&mut hasher));
405    let mut env = cmd.get_envs().iter().collect::<Vec<_>>();
406    env.sort_unstable();
407    env.hash(&mut hasher);
408    Hasher::finish(&hasher)
409}