mirror of https://github.com/rust-lang/cargo
389 lines
13 KiB
Rust
389 lines
13 KiB
Rust
use std::collections::hash_map::HashMap;
|
|
use std::env;
|
|
use std::hash::{Hash, Hasher};
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Mutex;
|
|
|
|
use anyhow::Context as _;
|
|
use cargo_util::{paths, ProcessBuilder, ProcessError};
|
|
use serde::{Deserialize, Serialize};
|
|
use tracing::{debug, info, warn};
|
|
|
|
use crate::core::compiler::apply_env_config;
|
|
use crate::util::interning::InternedString;
|
|
use crate::util::{CargoResult, GlobalContext, StableHasher};
|
|
|
|
/// Information on the `rustc` executable
|
|
#[derive(Debug)]
|
|
pub struct Rustc {
|
|
/// The location of the exe
|
|
pub path: PathBuf,
|
|
/// An optional program that will be passed the path of the rust exe as its first argument, and
|
|
/// rustc args following this.
|
|
pub wrapper: Option<PathBuf>,
|
|
/// An optional wrapper to be used in addition to `rustc.wrapper` for workspace crates
|
|
pub workspace_wrapper: Option<PathBuf>,
|
|
/// Verbose version information (the output of `rustc -vV`)
|
|
pub verbose_version: String,
|
|
/// The rustc version (`1.23.4-beta.2`), this comes from verbose_version.
|
|
pub version: semver::Version,
|
|
/// The host triple (arch-platform-OS), this comes from verbose_version.
|
|
pub host: InternedString,
|
|
/// The rustc full commit hash, this comes from `verbose_version`.
|
|
pub commit_hash: Option<String>,
|
|
cache: Mutex<Cache>,
|
|
}
|
|
|
|
impl Rustc {
|
|
/// Runs the compiler at `path` to learn various pieces of information about
|
|
/// it, with an optional wrapper.
|
|
///
|
|
/// If successful this function returns a description of the compiler along
|
|
/// with a list of its capabilities.
|
|
#[tracing::instrument(skip(gctx))]
|
|
pub fn new(
|
|
path: PathBuf,
|
|
wrapper: Option<PathBuf>,
|
|
workspace_wrapper: Option<PathBuf>,
|
|
rustup_rustc: &Path,
|
|
cache_location: Option<PathBuf>,
|
|
gctx: &GlobalContext,
|
|
) -> CargoResult<Rustc> {
|
|
let mut cache = Cache::load(
|
|
wrapper.as_deref(),
|
|
workspace_wrapper.as_deref(),
|
|
&path,
|
|
rustup_rustc,
|
|
cache_location,
|
|
gctx,
|
|
);
|
|
|
|
let mut cmd = ProcessBuilder::new(&path)
|
|
.wrapped(workspace_wrapper.as_ref())
|
|
.wrapped(wrapper.as_deref());
|
|
apply_env_config(gctx, &mut cmd)?;
|
|
cmd.arg("-vV");
|
|
let verbose_version = cache.cached_output(&cmd, 0)?.0;
|
|
|
|
let extract = |field: &str| -> CargoResult<&str> {
|
|
verbose_version
|
|
.lines()
|
|
.find_map(|l| l.strip_prefix(field))
|
|
.ok_or_else(|| {
|
|
anyhow::format_err!(
|
|
"`rustc -vV` didn't have a line for `{}`, got:\n{}",
|
|
field.trim(),
|
|
verbose_version
|
|
)
|
|
})
|
|
};
|
|
|
|
let host = InternedString::new(extract("host: ")?);
|
|
let version = semver::Version::parse(extract("release: ")?).with_context(|| {
|
|
format!(
|
|
"rustc version does not appear to be a valid semver version, from:\n{}",
|
|
verbose_version
|
|
)
|
|
})?;
|
|
let commit_hash = extract("commit-hash: ").ok().map(|hash| {
|
|
// Possible commit-hash values from rustc are SHA hex string and "unknown". See:
|
|
// * https://github.com/rust-lang/rust/blob/531cb83fc/src/bootstrap/src/utils/channel.rs#L73
|
|
// * https://github.com/rust-lang/rust/blob/531cb83fc/compiler/rustc_driver_impl/src/lib.rs#L911-L913
|
|
#[cfg(debug_assertions)]
|
|
if hash != "unknown" {
|
|
debug_assert!(
|
|
hash.chars().all(|ch| ch.is_ascii_hexdigit()),
|
|
"commit hash must be a hex string, got: {hash:?}"
|
|
);
|
|
debug_assert!(
|
|
hash.len() == 40 || hash.len() == 64,
|
|
"hex string must be generated from sha1 or sha256 (i.e., it must be 40 or 64 characters long)\ngot: {hash:?}"
|
|
);
|
|
}
|
|
hash.to_string()
|
|
});
|
|
|
|
Ok(Rustc {
|
|
path,
|
|
wrapper,
|
|
workspace_wrapper,
|
|
verbose_version,
|
|
version,
|
|
host,
|
|
commit_hash,
|
|
cache: Mutex::new(cache),
|
|
})
|
|
}
|
|
|
|
/// Gets a process builder set up to use the found rustc version, with a wrapper if `Some`.
|
|
pub fn process(&self) -> ProcessBuilder {
|
|
let mut cmd = ProcessBuilder::new(self.path.as_path()).wrapped(self.wrapper.as_ref());
|
|
cmd.retry_with_argfile(true);
|
|
cmd
|
|
}
|
|
|
|
/// Gets a process builder set up to use the found rustc version, with a wrapper if `Some`.
|
|
pub fn workspace_process(&self) -> ProcessBuilder {
|
|
let mut cmd = ProcessBuilder::new(self.path.as_path())
|
|
.wrapped(self.workspace_wrapper.as_ref())
|
|
.wrapped(self.wrapper.as_ref());
|
|
cmd.retry_with_argfile(true);
|
|
cmd
|
|
}
|
|
|
|
pub fn process_no_wrapper(&self) -> ProcessBuilder {
|
|
let mut cmd = ProcessBuilder::new(&self.path);
|
|
cmd.retry_with_argfile(true);
|
|
cmd
|
|
}
|
|
|
|
/// Gets the output for the given command.
|
|
///
|
|
/// This will return the cached value if available, otherwise it will run
|
|
/// the command and cache the output.
|
|
///
|
|
/// `extra_fingerprint` is extra data to include in the cache fingerprint.
|
|
/// Use this if there is other information about the environment that may
|
|
/// affect the output that is not part of `cmd`.
|
|
///
|
|
/// Returns a tuple of strings `(stdout, stderr)`.
|
|
pub fn cached_output(
|
|
&self,
|
|
cmd: &ProcessBuilder,
|
|
extra_fingerprint: u64,
|
|
) -> CargoResult<(String, String)> {
|
|
self.cache
|
|
.lock()
|
|
.unwrap()
|
|
.cached_output(cmd, extra_fingerprint)
|
|
}
|
|
}
|
|
|
|
/// It is a well known fact that `rustc` is not the fastest compiler in the
|
|
/// world. What is less known is that even `rustc --version --verbose` takes
|
|
/// about a hundred milliseconds! Because we need compiler version info even
|
|
/// for no-op builds, we cache it here, based on compiler's mtime and rustup's
|
|
/// current toolchain.
|
|
///
|
|
/// <https://github.com/rust-lang/cargo/issues/5315>
|
|
/// <https://github.com/rust-lang/rust/issues/49761>
|
|
#[derive(Debug)]
|
|
struct Cache {
|
|
cache_location: Option<PathBuf>,
|
|
dirty: bool,
|
|
data: CacheData,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
|
struct CacheData {
|
|
rustc_fingerprint: u64,
|
|
outputs: HashMap<u64, Output>,
|
|
successes: HashMap<u64, bool>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
struct Output {
|
|
success: bool,
|
|
status: String,
|
|
code: Option<i32>,
|
|
stdout: String,
|
|
stderr: String,
|
|
}
|
|
|
|
impl Cache {
|
|
fn load(
|
|
wrapper: Option<&Path>,
|
|
workspace_wrapper: Option<&Path>,
|
|
rustc: &Path,
|
|
rustup_rustc: &Path,
|
|
cache_location: Option<PathBuf>,
|
|
gctx: &GlobalContext,
|
|
) -> Cache {
|
|
match (
|
|
cache_location,
|
|
rustc_fingerprint(wrapper, workspace_wrapper, rustc, rustup_rustc, gctx),
|
|
) {
|
|
(Some(cache_location), Ok(rustc_fingerprint)) => {
|
|
let empty = CacheData {
|
|
rustc_fingerprint,
|
|
outputs: HashMap::new(),
|
|
successes: HashMap::new(),
|
|
};
|
|
let mut dirty = true;
|
|
let data = match read(&cache_location) {
|
|
Ok(data) => {
|
|
if data.rustc_fingerprint == rustc_fingerprint {
|
|
debug!("reusing existing rustc info cache");
|
|
dirty = false;
|
|
data
|
|
} else {
|
|
debug!("different compiler, creating new rustc info cache");
|
|
empty
|
|
}
|
|
}
|
|
Err(e) => {
|
|
debug!("failed to read rustc info cache: {}", e);
|
|
empty
|
|
}
|
|
};
|
|
return Cache {
|
|
cache_location: Some(cache_location),
|
|
dirty,
|
|
data,
|
|
};
|
|
|
|
fn read(path: &Path) -> CargoResult<CacheData> {
|
|
let json = paths::read(path)?;
|
|
Ok(serde_json::from_str(&json)?)
|
|
}
|
|
}
|
|
(_, fingerprint) => {
|
|
if let Err(e) = fingerprint {
|
|
warn!("failed to calculate rustc fingerprint: {}", e);
|
|
}
|
|
debug!("rustc info cache disabled");
|
|
Cache {
|
|
cache_location: None,
|
|
dirty: false,
|
|
data: CacheData::default(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cached_output(
|
|
&mut self,
|
|
cmd: &ProcessBuilder,
|
|
extra_fingerprint: u64,
|
|
) -> CargoResult<(String, String)> {
|
|
let key = process_fingerprint(cmd, extra_fingerprint);
|
|
if self.data.outputs.contains_key(&key) {
|
|
debug!("rustc info cache hit");
|
|
} else {
|
|
debug!("rustc info cache miss");
|
|
debug!("running {}", cmd);
|
|
let output = cmd.output()?;
|
|
let stdout = String::from_utf8(output.stdout)
|
|
.map_err(|e| anyhow::anyhow!("{}: {:?}", e, e.as_bytes()))
|
|
.with_context(|| format!("`{}` didn't return utf8 output", cmd))?;
|
|
let stderr = String::from_utf8(output.stderr)
|
|
.map_err(|e| anyhow::anyhow!("{}: {:?}", e, e.as_bytes()))
|
|
.with_context(|| format!("`{}` didn't return utf8 output", cmd))?;
|
|
self.data.outputs.insert(
|
|
key,
|
|
Output {
|
|
success: output.status.success(),
|
|
status: if output.status.success() {
|
|
String::new()
|
|
} else {
|
|
cargo_util::exit_status_to_string(output.status)
|
|
},
|
|
code: output.status.code(),
|
|
stdout,
|
|
stderr,
|
|
},
|
|
);
|
|
self.dirty = true;
|
|
}
|
|
let output = &self.data.outputs[&key];
|
|
if output.success {
|
|
Ok((output.stdout.clone(), output.stderr.clone()))
|
|
} else {
|
|
Err(ProcessError::new_raw(
|
|
&format!("process didn't exit successfully: {}", cmd),
|
|
output.code,
|
|
&output.status,
|
|
Some(output.stdout.as_ref()),
|
|
Some(output.stderr.as_ref()),
|
|
)
|
|
.into())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for Cache {
|
|
fn drop(&mut self) {
|
|
if !self.dirty {
|
|
return;
|
|
}
|
|
if let Some(ref path) = self.cache_location {
|
|
let json = serde_json::to_string(&self.data).unwrap();
|
|
match paths::write(path, json.as_bytes()) {
|
|
Ok(()) => info!("updated rustc info cache"),
|
|
Err(e) => warn!("failed to update rustc info cache: {}", e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn rustc_fingerprint(
|
|
wrapper: Option<&Path>,
|
|
workspace_wrapper: Option<&Path>,
|
|
rustc: &Path,
|
|
rustup_rustc: &Path,
|
|
gctx: &GlobalContext,
|
|
) -> CargoResult<u64> {
|
|
let mut hasher = StableHasher::new();
|
|
|
|
let hash_exe = |hasher: &mut _, path| -> CargoResult<()> {
|
|
let path = paths::resolve_executable(path)?;
|
|
path.hash(hasher);
|
|
|
|
paths::mtime(&path)?.hash(hasher);
|
|
Ok(())
|
|
};
|
|
|
|
hash_exe(&mut hasher, rustc)?;
|
|
if let Some(wrapper) = wrapper {
|
|
hash_exe(&mut hasher, wrapper)?;
|
|
}
|
|
if let Some(workspace_wrapper) = workspace_wrapper {
|
|
hash_exe(&mut hasher, workspace_wrapper)?;
|
|
}
|
|
|
|
// Rustup can change the effective compiler without touching
|
|
// the `rustc` binary, so we try to account for this here.
|
|
// If we see rustup's env vars, we mix them into the fingerprint,
|
|
// but we also mix in the mtime of the actual compiler (and not
|
|
// the rustup shim at `~/.cargo/bin/rustup`), because `RUSTUP_TOOLCHAIN`
|
|
// could be just `stable-x86_64-unknown-linux-gnu`, i.e, it could
|
|
// not mention the version of Rust at all, which changes after
|
|
// `rustup update`.
|
|
//
|
|
// If we don't see rustup env vars, but it looks like the compiler
|
|
// is managed by rustup, we conservatively bail out.
|
|
let maybe_rustup = rustup_rustc == rustc;
|
|
match (
|
|
maybe_rustup,
|
|
gctx.get_env("RUSTUP_HOME"),
|
|
gctx.get_env("RUSTUP_TOOLCHAIN"),
|
|
) {
|
|
(_, Ok(rustup_home), Ok(rustup_toolchain)) => {
|
|
debug!("adding rustup info to rustc fingerprint");
|
|
rustup_toolchain.hash(&mut hasher);
|
|
rustup_home.hash(&mut hasher);
|
|
let real_rustc = Path::new(&rustup_home)
|
|
.join("toolchains")
|
|
.join(rustup_toolchain)
|
|
.join("bin")
|
|
.join("rustc")
|
|
.with_extension(env::consts::EXE_EXTENSION);
|
|
paths::mtime(&real_rustc)?.hash(&mut hasher);
|
|
}
|
|
(true, _, _) => anyhow::bail!("probably rustup rustc, but without rustup's env vars"),
|
|
_ => (),
|
|
}
|
|
|
|
Ok(hasher.finish())
|
|
}
|
|
|
|
fn process_fingerprint(cmd: &ProcessBuilder, extra_fingerprint: u64) -> u64 {
|
|
let mut hasher = StableHasher::new();
|
|
extra_fingerprint.hash(&mut hasher);
|
|
cmd.get_args().for_each(|arg| arg.hash(&mut hasher));
|
|
let mut env = cmd.get_envs().iter().collect::<Vec<_>>();
|
|
env.sort_unstable();
|
|
env.hash(&mut hasher);
|
|
hasher.finish()
|
|
}
|