1use std::collections::hash_map::HashMap;
2use std::env;
3use std::hash::{Hash, Hasher};
4use std::path::{Path, PathBuf};
5use std::sync::Mutex;
6
7use anyhow::Context as _;
8use cargo_util::{ProcessBuilder, ProcessError, paths};
9use filetime::FileTime;
10use serde::{Deserialize, Serialize};
11use tracing::{debug, info, warn};
12
13use crate::core::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 cmd.arg("-vV");
68 let verbose_version = cache.cached_output(&cmd, 0)?.0;
69
70 let extract = |field: &str| -> CargoResult<&str> {
71 verbose_version
72 .lines()
73 .find_map(|l| l.strip_prefix(field))
74 .ok_or_else(|| {
75 anyhow::format_err!(
76 "`rustc -vV` didn't have a line for `{}`, got:\n{}",
77 field.trim(),
78 verbose_version
79 )
80 })
81 };
82
83 let host = extract("host: ")?.into();
84 let version = semver::Version::parse(extract("release: ")?).with_context(|| {
85 format!(
86 "rustc version does not appear to be a valid semver version, from:\n{}",
87 verbose_version
88 )
89 })?;
90 let commit_hash = extract("commit-hash: ").ok().map(|hash| {
91 #[cfg(debug_assertions)]
95 if hash != "unknown" {
96 debug_assert!(
97 hash.chars().all(|ch| ch.is_ascii_hexdigit()),
98 "commit hash must be a hex string, got: {hash:?}"
99 );
100 debug_assert!(
101 hash.len() == 40 || hash.len() == 64,
102 "hex string must be generated from sha1 or sha256 (i.e., it must be 40 or 64 characters long)\ngot: {hash:?}"
103 );
104 }
105 hash.to_string()
106 });
107
108 Ok(Rustc {
109 path,
110 wrapper,
111 workspace_wrapper,
112 verbose_version,
113 version,
114 host,
115 commit_hash,
116 cache: Mutex::new(cache),
117 })
118 }
119
120 pub fn process(&self) -> ProcessBuilder {
122 let mut cmd = ProcessBuilder::new(self.path.as_path()).wrapped(self.wrapper.as_ref());
123 cmd.retry_with_argfile(true);
124 cmd
125 }
126
127 pub fn workspace_process(&self) -> ProcessBuilder {
129 let mut cmd = ProcessBuilder::new(self.path.as_path())
130 .wrapped(self.workspace_wrapper.as_ref())
131 .wrapped(self.wrapper.as_ref());
132 cmd.retry_with_argfile(true);
133 cmd
134 }
135
136 pub fn process_no_wrapper(&self) -> ProcessBuilder {
137 let mut cmd = ProcessBuilder::new(&self.path);
138 cmd.retry_with_argfile(true);
139 cmd
140 }
141
142 pub fn cached_output(
153 &self,
154 cmd: &ProcessBuilder,
155 extra_fingerprint: u64,
156 ) -> CargoResult<(String, String)> {
157 self.cache
158 .lock()
159 .unwrap()
160 .cached_output(cmd, extra_fingerprint)
161 }
162}
163
164#[derive(Debug)]
173struct Cache {
174 cache_location: Option<PathBuf>,
175 dirty: bool,
176 data: CacheData,
177}
178
179#[derive(Serialize, Deserialize, Debug, Default)]
180struct CacheData {
181 rustc_fingerprint: u64,
182 outputs: HashMap<u64, Output>,
183 successes: HashMap<u64, bool>,
184}
185
186#[derive(Serialize, Deserialize, Debug)]
187struct Output {
188 success: bool,
189 status: String,
190 code: Option<i32>,
191 stdout: String,
192 stderr: String,
193}
194
195impl Cache {
196 fn load(
197 wrapper: Option<&Path>,
198 workspace_wrapper: Option<&Path>,
199 rustc: &Path,
200 rustup_rustc: &Path,
201 cache_location: Option<PathBuf>,
202 gctx: &GlobalContext,
203 ) -> Cache {
204 match (
205 cache_location,
206 rustc_fingerprint(wrapper, workspace_wrapper, rustc, rustup_rustc, gctx),
207 ) {
208 (Some(cache_location), Ok(rustc_fingerprint)) => {
209 let empty = CacheData {
210 rustc_fingerprint,
211 outputs: HashMap::new(),
212 successes: HashMap::new(),
213 };
214 let mut dirty = true;
215 let data = match read(&cache_location) {
216 Ok(data) => {
217 if data.rustc_fingerprint == rustc_fingerprint {
218 debug!("reusing existing rustc info cache");
219 dirty = false;
220 data
221 } else {
222 debug!("different compiler, creating new rustc info cache");
223 empty
224 }
225 }
226 Err(e) => {
227 debug!("failed to read rustc info cache: {}", e);
228 empty
229 }
230 };
231 return Cache {
232 cache_location: Some(cache_location),
233 dirty,
234 data,
235 };
236
237 fn read(path: &Path) -> CargoResult<CacheData> {
238 let json = paths::read(path)?;
239 Ok(serde_json::from_str(&json)?)
240 }
241 }
242 (_, fingerprint) => {
243 if let Err(e) = fingerprint {
244 warn!("failed to calculate rustc fingerprint: {}", e);
245 }
246 debug!("rustc info cache disabled");
247 Cache {
248 cache_location: None,
249 dirty: false,
250 data: CacheData::default(),
251 }
252 }
253 }
254 }
255
256 fn cached_output(
257 &mut self,
258 cmd: &ProcessBuilder,
259 extra_fingerprint: u64,
260 ) -> CargoResult<(String, String)> {
261 let key = process_fingerprint(cmd, extra_fingerprint);
262 if let std::collections::hash_map::Entry::Vacant(e) = self.data.outputs.entry(key) {
263 debug!("rustc info cache miss");
264 debug!("running {}", cmd);
265 let output = cmd.output()?;
266 let stdout = String::from_utf8(output.stdout)
267 .map_err(|e| anyhow::anyhow!("{}: {:?}", e, e.as_bytes()))
268 .with_context(|| format!("`{}` didn't return utf8 output", cmd))?;
269 let stderr = String::from_utf8(output.stderr)
270 .map_err(|e| anyhow::anyhow!("{}: {:?}", e, e.as_bytes()))
271 .with_context(|| format!("`{}` didn't return utf8 output", cmd))?;
272 e.insert(Output {
273 success: output.status.success(),
274 status: if output.status.success() {
275 String::new()
276 } else {
277 cargo_util::exit_status_to_string(output.status)
278 },
279 code: output.status.code(),
280 stdout,
281 stderr,
282 });
283 self.dirty = true;
284 } else {
285 debug!("rustc info cache hit");
286 }
287 let output = &self.data.outputs[&key];
288 if output.success {
289 Ok((output.stdout.clone(), output.stderr.clone()))
290 } else {
291 Err(ProcessError::new_raw(
292 &format!("process didn't exit successfully: {}", cmd),
293 output.code,
294 &output.status,
295 Some(output.stdout.as_ref()),
296 Some(output.stderr.as_ref()),
297 )
298 .into())
299 }
300 }
301}
302
303impl Drop for Cache {
304 fn drop(&mut self) {
305 if !self.dirty {
306 return;
307 }
308 if let Some(ref path) = self.cache_location {
309 let json = serde_json::to_string(&self.data).unwrap();
310 match paths::write(path, json.as_bytes()) {
311 Ok(()) => info!("updated rustc info cache"),
312 Err(e) => warn!("failed to update rustc info cache: {}", e),
313 }
314 }
315 }
316}
317
318fn rustc_fingerprint(
319 wrapper: Option<&Path>,
320 workspace_wrapper: Option<&Path>,
321 rustc: &Path,
322 rustup_rustc: &Path,
323 gctx: &GlobalContext,
324) -> CargoResult<u64> {
325 let mut hasher = StableHasher::new();
326
327 let hash_exe = |hasher: &mut _, path| -> CargoResult<()> {
328 let path = paths::resolve_executable(path)?;
329 path.hash(hasher);
330
331 let meta = paths::metadata(&path)?;
332 meta.len().hash(hasher);
333
334 FileTime::from_creation_time(&meta).hash(hasher);
337 FileTime::from_last_modification_time(&meta).hash(hasher);
338 Ok(())
339 };
340
341 hash_exe(&mut hasher, rustc)?;
342 if let Some(wrapper) = wrapper {
343 hash_exe(&mut hasher, wrapper)?;
344 }
345 if let Some(workspace_wrapper) = workspace_wrapper {
346 hash_exe(&mut hasher, workspace_wrapper)?;
347 }
348
349 let maybe_rustup = rustup_rustc == rustc;
361 match (
362 maybe_rustup,
363 gctx.get_env("RUSTUP_HOME"),
364 gctx.get_env("RUSTUP_TOOLCHAIN"),
365 ) {
366 (_, Ok(rustup_home), Ok(rustup_toolchain)) => {
367 debug!("adding rustup info to rustc fingerprint");
368 rustup_toolchain.hash(&mut hasher);
369 rustup_home.hash(&mut hasher);
370 let real_rustc = Path::new(&rustup_home)
371 .join("toolchains")
372 .join(rustup_toolchain)
373 .join("bin")
374 .join("rustc")
375 .with_extension(env::consts::EXE_EXTENSION);
376 paths::mtime(&real_rustc)?.hash(&mut hasher);
377 }
378 (true, _, _) => anyhow::bail!("probably rustup rustc, but without rustup's env vars"),
379 _ => (),
380 }
381
382 Ok(Hasher::finish(&hasher))
383}
384
385fn process_fingerprint(cmd: &ProcessBuilder, extra_fingerprint: u64) -> u64 {
386 let mut hasher = StableHasher::new();
387 extra_fingerprint.hash(&mut hasher);
388 cmd.get_args().for_each(|arg| arg.hash(&mut hasher));
389 let mut env = cmd.get_envs().iter().collect::<Vec<_>>();
390 env.sort_unstable();
391 env.hash(&mut hasher);
392 Hasher::finish(&hasher)
393}