cargo/util/
rustc.rs

1#![allow(deprecated)] // for SipHasher
2
3use std::collections::hash_map::{Entry, HashMap};
4use std::env;
5use std::hash::{Hash, Hasher, SipHasher};
6use std::path::{Path, PathBuf};
7use std::sync::Mutex;
8
9use log::{debug, info, warn};
10use serde::{Deserialize, Serialize};
11
12use crate::core::InternedString;
13use crate::util::paths;
14use crate::util::{self, profile, CargoResult, CargoResultExt, ProcessBuilder};
15
16/// Information on the `rustc` executable
17#[derive(Debug)]
18pub struct Rustc {
19    /// The location of the exe
20    pub path: PathBuf,
21    /// An optional program that will be passed the path of the rust exe as its first argument, and
22    /// rustc args following this.
23    pub wrapper: Option<PathBuf>,
24    /// An optional wrapper to be used in addition to `rustc.wrapper` for workspace crates
25    pub workspace_wrapper: Option<PathBuf>,
26    /// Verbose version information (the output of `rustc -vV`)
27    pub verbose_version: String,
28    /// The host triple (arch-platform-OS), this comes from verbose_version.
29    pub host: InternedString,
30    cache: Mutex<Cache>,
31}
32
33impl Rustc {
34    /// Runs the compiler at `path` to learn various pieces of information about
35    /// it, with an optional wrapper.
36    ///
37    /// If successful this function returns a description of the compiler along
38    /// with a list of its capabilities.
39    pub fn new(
40        path: PathBuf,
41        wrapper: Option<PathBuf>,
42        workspace_wrapper: Option<PathBuf>,
43        rustup_rustc: &Path,
44        cache_location: Option<PathBuf>,
45    ) -> CargoResult<Rustc> {
46        let _p = profile::start("Rustc::new");
47
48        let mut cache = Cache::load(&path, rustup_rustc, cache_location);
49
50        let mut cmd = util::process(&path);
51        cmd.arg("-vV");
52        let verbose_version = cache.cached_output(&cmd)?.0;
53
54        let host = {
55            let triple = verbose_version
56                .lines()
57                .find(|l| l.starts_with("host: "))
58                .map(|l| &l[6..])
59                .ok_or_else(|| {
60                    anyhow::format_err!(
61                        "`rustc -vV` didn't have a line for `host:`, got:\n{}",
62                        verbose_version
63                    )
64                })?;
65            InternedString::new(triple)
66        };
67
68        Ok(Rustc {
69            path,
70            wrapper,
71            workspace_wrapper,
72            verbose_version,
73            host,
74            cache: Mutex::new(cache),
75        })
76    }
77
78    /// Gets a process builder set up to use the found rustc version, with a wrapper if `Some`.
79    pub fn process(&self) -> ProcessBuilder {
80        util::process(self.path.as_path()).wrapped(self.wrapper.as_ref())
81    }
82
83    /// Gets a process builder set up to use the found rustc version, with a wrapper if `Some`.
84    pub fn workspace_process(&self) -> ProcessBuilder {
85        util::process(self.path.as_path())
86            .wrapped(self.workspace_wrapper.as_ref())
87            .wrapped(self.wrapper.as_ref())
88    }
89
90    pub fn process_no_wrapper(&self) -> ProcessBuilder {
91        util::process(&self.path)
92    }
93
94    pub fn cached_output(&self, cmd: &ProcessBuilder) -> CargoResult<(String, String)> {
95        self.cache.lock().unwrap().cached_output(cmd)
96    }
97}
98
99/// It is a well known fact that `rustc` is not the fastest compiler in the
100/// world.  What is less known is that even `rustc --version --verbose` takes
101/// about a hundred milliseconds! Because we need compiler version info even
102/// for no-op builds, we cache it here, based on compiler's mtime and rustup's
103/// current toolchain.
104///
105/// https://github.com/rust-lang/cargo/issues/5315
106/// https://github.com/rust-lang/rust/issues/49761
107#[derive(Debug)]
108struct Cache {
109    cache_location: Option<PathBuf>,
110    dirty: bool,
111    data: CacheData,
112}
113
114#[derive(Serialize, Deserialize, Debug, Default)]
115struct CacheData {
116    rustc_fingerprint: u64,
117    outputs: HashMap<u64, (String, String)>,
118    successes: HashMap<u64, bool>,
119}
120
121impl Cache {
122    fn load(rustc: &Path, rustup_rustc: &Path, cache_location: Option<PathBuf>) -> Cache {
123        match (cache_location, rustc_fingerprint(rustc, rustup_rustc)) {
124            (Some(cache_location), Ok(rustc_fingerprint)) => {
125                let empty = CacheData {
126                    rustc_fingerprint,
127                    outputs: HashMap::new(),
128                    successes: HashMap::new(),
129                };
130                let mut dirty = true;
131                let data = match read(&cache_location) {
132                    Ok(data) => {
133                        if data.rustc_fingerprint == rustc_fingerprint {
134                            debug!("reusing existing rustc info cache");
135                            dirty = false;
136                            data
137                        } else {
138                            debug!("different compiler, creating new rustc info cache");
139                            empty
140                        }
141                    }
142                    Err(e) => {
143                        debug!("failed to read rustc info cache: {}", e);
144                        empty
145                    }
146                };
147                return Cache {
148                    cache_location: Some(cache_location),
149                    dirty,
150                    data,
151                };
152
153                fn read(path: &Path) -> CargoResult<CacheData> {
154                    let json = paths::read(path)?;
155                    Ok(serde_json::from_str(&json)?)
156                }
157            }
158            (_, fingerprint) => {
159                if let Err(e) = fingerprint {
160                    warn!("failed to calculate rustc fingerprint: {}", e);
161                }
162                debug!("rustc info cache disabled");
163                Cache {
164                    cache_location: None,
165                    dirty: false,
166                    data: CacheData::default(),
167                }
168            }
169        }
170    }
171
172    fn cached_output(&mut self, cmd: &ProcessBuilder) -> CargoResult<(String, String)> {
173        let key = process_fingerprint(cmd);
174        match self.data.outputs.entry(key) {
175            Entry::Occupied(entry) => {
176                debug!("rustc info cache hit");
177                Ok(entry.get().clone())
178            }
179            Entry::Vacant(entry) => {
180                debug!("rustc info cache miss");
181                debug!("running {}", cmd);
182                let output = cmd.exec_with_output()?;
183                let stdout = String::from_utf8(output.stdout)
184                    .map_err(|e| anyhow::anyhow!("{}: {:?}", e, e.as_bytes()))
185                    .chain_err(|| anyhow::anyhow!("`{}` didn't return utf8 output", cmd))?;
186                let stderr = String::from_utf8(output.stderr)
187                    .map_err(|e| anyhow::anyhow!("{}: {:?}", e, e.as_bytes()))
188                    .chain_err(|| anyhow::anyhow!("`{}` didn't return utf8 output", cmd))?;
189                let output = (stdout, stderr);
190                entry.insert(output.clone());
191                self.dirty = true;
192                Ok(output)
193            }
194        }
195    }
196}
197
198impl Drop for Cache {
199    fn drop(&mut self) {
200        if !self.dirty {
201            return;
202        }
203        if let Some(ref path) = self.cache_location {
204            let json = serde_json::to_string(&self.data).unwrap();
205            match paths::write(path, json.as_bytes()) {
206                Ok(()) => info!("updated rustc info cache"),
207                Err(e) => warn!("failed to update rustc info cache: {}", e),
208            }
209        }
210    }
211}
212
213fn rustc_fingerprint(path: &Path, rustup_rustc: &Path) -> CargoResult<u64> {
214    let mut hasher = SipHasher::new();
215
216    let path = paths::resolve_executable(path)?;
217    path.hash(&mut hasher);
218
219    paths::mtime(&path)?.hash(&mut hasher);
220
221    // Rustup can change the effective compiler without touching
222    // the `rustc` binary, so we try to account for this here.
223    // If we see rustup's env vars, we mix them into the fingerprint,
224    // but we also mix in the mtime of the actual compiler (and not
225    // the rustup shim at `~/.cargo/bin/rustup`), because `RUSTUP_TOOLCHAIN`
226    // could be just `stable-x86_64-unknown-linux-gnu`, i.e, it could
227    // not mention the version of Rust at all, which changes after
228    // `rustup update`.
229    //
230    // If we don't see rustup env vars, but it looks like the compiler
231    // is managed by rustup, we conservatively bail out.
232    let maybe_rustup = rustup_rustc == path;
233    match (
234        maybe_rustup,
235        env::var("RUSTUP_HOME"),
236        env::var("RUSTUP_TOOLCHAIN"),
237    ) {
238        (_, Ok(rustup_home), Ok(rustup_toolchain)) => {
239            debug!("adding rustup info to rustc fingerprint");
240            rustup_toolchain.hash(&mut hasher);
241            rustup_home.hash(&mut hasher);
242            let real_rustc = Path::new(&rustup_home)
243                .join("toolchains")
244                .join(rustup_toolchain)
245                .join("bin")
246                .join("rustc")
247                .with_extension(env::consts::EXE_EXTENSION);
248            paths::mtime(&real_rustc)?.hash(&mut hasher);
249        }
250        (true, _, _) => anyhow::bail!("probably rustup rustc, but without rustup's env vars"),
251        _ => (),
252    }
253
254    Ok(hasher.finish())
255}
256
257fn process_fingerprint(cmd: &ProcessBuilder) -> u64 {
258    let mut hasher = SipHasher::new();
259    cmd.get_args().hash(&mut hasher);
260    let mut env = cmd.get_envs().iter().collect::<Vec<_>>();
261    env.sort_unstable();
262    env.hash(&mut hasher);
263    hasher.finish()
264}