blob: 0e95e0f8411784fab3aace7cb7e9b61fc1e2d565 [file] [log] [blame]
use cargo_metadata::{camino::Utf8PathBuf, DependencyKind};
use cargo_platform::Cfg;
use color_eyre::eyre::{bail, eyre, Result};
use std::{
collections::{hash_map::Entry, HashMap, HashSet},
ffi::OsString,
path::PathBuf,
process::Command,
str::FromStr,
sync::{Arc, OnceLock, RwLock},
};
use crate::{
build_aux, status_emitter::StatusEmitter, Config, Errored, Mode, OutputConflictHandling,
};
#[derive(Default, Debug)]
pub struct Dependencies {
/// All paths that must be imported with `-L dependency=`. This is for
/// finding proc macros run on the host and dependencies for the target.
pub import_paths: Vec<PathBuf>,
/// The name as chosen in the `Cargo.toml` and its corresponding rmeta file.
pub dependencies: Vec<(String, Vec<Utf8PathBuf>)>,
}
fn cfgs(config: &Config) -> Result<Vec<Cfg>> {
let mut cmd = config.cfgs.build(&config.out_dir);
cmd.arg("--target").arg(config.target.as_ref().unwrap());
let output = cmd.output()?;
let stdout = String::from_utf8(output.stdout)?;
if !output.status.success() {
let stderr = String::from_utf8(output.stderr)?;
bail!(
"failed to obtain `cfg` information from {cmd:?}:\nstderr:\n{stderr}\n\nstdout:{stdout}"
);
}
let mut cfgs = vec![];
for line in stdout.lines() {
cfgs.push(Cfg::from_str(line)?);
}
Ok(cfgs)
}
/// Compiles dependencies and returns the crate names and corresponding rmeta files.
pub(crate) fn build_dependencies(config: &Config) -> Result<Dependencies> {
let manifest_path = match &config.dependencies_crate_manifest_path {
Some(path) => path.to_owned(),
None => return Ok(Default::default()),
};
let manifest_path = &manifest_path;
let mut build = config.dependency_builder.build(&config.out_dir);
build.arg(manifest_path);
if let Some(target) = &config.target {
build.arg(format!("--target={target}"));
}
// Reusable closure for setting up the environment both for artifact generation and `cargo_metadata`
let set_locking = |cmd: &mut Command| match (&config.output_conflict_handling, &config.mode) {
(_, Mode::Yolo { .. }) => {}
(OutputConflictHandling::Error(_), _) => {
cmd.arg("--locked");
}
_ => {}
};
set_locking(&mut build);
build.arg("--message-format=json");
let output = build.output()?;
if !output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
let stderr = String::from_utf8(output.stderr)?;
bail!("failed to compile dependencies:\ncommand: {build:?}\nstderr:\n{stderr}\n\nstdout:{stdout}");
}
// Collect all artifacts generated
let artifact_output = output.stdout;
let artifact_output = String::from_utf8(artifact_output)?;
let mut import_paths: HashSet<PathBuf> = HashSet::new();
let mut artifacts = HashMap::new();
for line in artifact_output.lines() {
let Ok(message) = serde_json::from_str::<cargo_metadata::Message>(line) else {
continue;
};
if let cargo_metadata::Message::CompilerArtifact(artifact) = message {
if artifact
.filenames
.iter()
.any(|f| f.ends_with("build-script-build"))
{
continue;
}
// Check that we only collect rmeta and rlib crates, not build script crates
if artifact
.filenames
.iter()
.any(|f| !matches!(f.extension(), Some("rlib" | "rmeta")))
{
continue;
}
for filename in &artifact.filenames {
import_paths.insert(filename.parent().unwrap().into());
}
let package_id = artifact.package_id;
if let Some(prev) = artifacts.insert(package_id.clone(), Ok(artifact.filenames)) {
artifacts.insert(
package_id.clone(),
Err(format!("{prev:#?} vs {:#?}", artifacts[&package_id])),
);
}
}
}
// Check which crates are mentioned in the crate itself
let mut metadata = cargo_metadata::MetadataCommand::new().cargo_command();
metadata.arg("--manifest-path").arg(manifest_path);
config.dependency_builder.apply_env(&mut metadata);
set_locking(&mut metadata);
let output = metadata.output()?;
if !output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
let stderr = String::from_utf8(output.stderr)?;
bail!("failed to run cargo-metadata:\nstderr:\n{stderr}\n\nstdout:{stdout}");
}
let output = output.stdout;
let output = String::from_utf8(output)?;
let cfg = cfgs(config)?;
for line in output.lines() {
if !line.starts_with('{') {
continue;
}
let metadata: cargo_metadata::Metadata = serde_json::from_str(line)?;
// Only take artifacts that are defined in the Cargo.toml
// First, find the root artifact
let root = metadata
.packages
.iter()
.find(|package| {
package.manifest_path.as_std_path().canonicalize().unwrap()
== manifest_path.canonicalize().unwrap()
})
.unwrap();
// Then go over all of its dependencies
let dependencies = root
.dependencies
.iter()
.filter(|dep| matches!(dep.kind, DependencyKind::Normal))
// Only consider dependencies that are enabled on the current target
.filter(|dep| match &dep.target {
Some(platform) => platform.matches(config.target.as_ref().unwrap(), &cfg),
None => true,
})
.map(|dep| {
let package = metadata
.packages
.iter()
.find(|&p| p.name == dep.name && dep.req.matches(&p.version))
.expect("dependency does not exist");
(
package,
dep.rename.clone().unwrap_or_else(|| package.name.clone()),
)
})
// Also expose the root crate
.chain(std::iter::once((root, root.name.clone())))
.filter_map(|(package, name)| {
// Get the id for the package matching the version requirement of the dep
let id = &package.id;
// Return the name chosen in `Cargo.toml` and the path to the corresponding artifact
match artifacts.remove(id) {
Some(Ok(artifacts)) => Some(Ok((name.replace('-', "_"), artifacts))),
Some(Err(what)) => Some(Err(eyre!("`ui_test` does not support crates that appear as both build-dependencies and core dependencies: {id}: {what}"))),
None => {
if name == root.name {
// If there are no artifacts, this is the root crate and it is being built as a binary/test
// instead of a library. We simply add no artifacts, meaning you can't depend on functions
// and types declared in the root crate.
None
} else {
panic!("no artifact found for `{name}`(`{id}`):`\n{artifact_output}")
}
}
}
})
.collect::<Result<Vec<_>>>()?;
let import_paths = import_paths.into_iter().collect();
return Ok(Dependencies {
dependencies,
import_paths,
});
}
bail!("no json found in cargo-metadata output")
}
#[derive(PartialEq, Eq, Debug, Hash, Clone)]
pub enum Build {
/// Build the dependencies.
Dependencies,
/// Build an aux-build.
Aux { aux_file: PathBuf },
}
impl Build {
fn description(&self) -> String {
match self {
Build::Dependencies => "Building dependencies".into(),
Build::Aux { aux_file } => format!("Building aux file {}", aux_file.display()),
}
}
}
pub struct BuildManager<'a> {
#[allow(clippy::type_complexity)]
cache: RwLock<HashMap<Build, Arc<OnceLock<Result<Vec<OsString>, ()>>>>>,
status_emitter: &'a dyn StatusEmitter,
}
impl<'a> BuildManager<'a> {
pub fn new(status_emitter: &'a dyn StatusEmitter) -> Self {
Self {
cache: Default::default(),
status_emitter,
}
}
/// This function will block until the build is done and then return the arguments
/// that need to be passed in order to build the dependencies.
/// The error is only reported once, all follow up invocations of the same build will
/// have a generic error about a previous build failing.
pub fn build(&self, what: Build, config: &Config) -> Result<Vec<OsString>, Errored> {
// Fast path without much contention.
if let Some(res) = self.cache.read().unwrap().get(&what).and_then(|o| o.get()) {
return res.clone().map_err(|()| Errored {
command: Command::new(format!("{what:?}")),
errors: vec![],
stderr: b"previous build failed".to_vec(),
stdout: vec![],
});
}
let mut lock = self.cache.write().unwrap();
let once = match lock.entry(what.clone()) {
Entry::Occupied(entry) => {
if let Some(res) = entry.get().get() {
return res.clone().map_err(|()| Errored {
command: Command::new(format!("{what:?}")),
errors: vec![],
stderr: b"previous build failed".to_vec(),
stdout: vec![],
});
}
entry.get().clone()
}
Entry::Vacant(entry) => {
let once = Arc::new(OnceLock::new());
entry.insert(once.clone());
once
}
};
drop(lock);
let mut err = None;
once.get_or_init(|| {
let build = self
.status_emitter
.register_test(what.description().into())
.for_revision("");
let res = match &what {
Build::Dependencies => match config.build_dependencies() {
Ok(args) => Ok(args),
Err(e) => {
err = Some(Errored {
command: Command::new(format!("{what:?}")),
errors: vec![],
stderr: format!("{e:?}").into_bytes(),
stdout: vec![],
});
Err(())
}
},
Build::Aux { aux_file } => match build_aux(aux_file, config, self) {
Ok(args) => Ok(args.iter().map(Into::into).collect()),
Err(e) => {
err = Some(e);
Err(())
}
},
};
build.done(
&res.as_ref()
.map(|_| crate::TestOk::Ok)
.map_err(|()| Errored {
command: Command::new(what.description()),
errors: vec![],
stderr: vec![],
stdout: vec![],
}),
);
res
})
.clone()
.map_err(|()| {
err.unwrap_or_else(|| Errored {
command: Command::new(what.description()),
errors: vec![],
stderr: b"previous build failed".to_vec(),
stdout: vec![],
})
})
}
}