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#[derive(Debug)]
19pub struct Rustc {
20 pub path: PathBuf,
22 pub wrapper: Option<PathBuf>,
25 pub workspace_wrapper: Option<PathBuf>,
27 pub verbose_version: String,
29 pub version: semver::Version,
31 pub host: InternedString,
33 pub commit_hash: Option<String>,
35 cache: Mutex<Cache>,
36}
37
38impl Rustc {
39 #[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 #[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 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 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 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 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#[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 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 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}