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