use cargo_metadata::{camino::Utf8PathBuf, DependencyKind};
use cargo_platform::Cfg;
use color_eyre::eyre::{bail, Result};
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
process::Command,
str::FromStr,
};
use crate::{Config, Mode, OutputConflictHandling};
#[derive(Default, Debug)]
pub struct Dependencies {
pub import_paths: Vec<PathBuf>,
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)
}
pub fn build_dependencies(config: &mut 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;
config.fill_host_and_target()?;
eprintln!(" Building test dependencies...");
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}"));
}
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}");
}
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;
}
for filename in &artifact.filenames {
import_paths.insert(filename.parent().unwrap().into());
}
let package_id = artifact.package_id;
if artifacts
.insert(package_id.clone(), artifact.filenames)
.is_some()
{
bail!("`ui_test` does not support crates that appear as both build-dependencies and core dependencies: {package_id}")
}
}
}
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)?;
let root = metadata
.packages
.iter()
.find(|package| {
package.manifest_path.as_std_path().canonicalize().unwrap()
== manifest_path.canonicalize().unwrap()
})
.unwrap();
let dependencies = root
.dependencies
.iter()
.filter(|dep| matches!(dep.kind, DependencyKind::Normal))
.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()),
)
})
.chain(std::iter::once((root, root.name.clone())))
.filter_map(|(package, name)| {
let id = &package.id;
match artifacts.remove(id) {
Some(artifacts) => Some((name.replace('-', "_"), artifacts)),
None => {
if name == root.name {
None
} else {
panic!("no artifact found for `{name}`(`{id}`):`\n{artifact_output}")
}
}
}
})
.collect();
let import_paths = import_paths.into_iter().collect();
return Ok(Dependencies {
dependencies,
import_paths,
});
}
bail!("no json found in cargo-metadata output")
}