use crate::{
errors::{FromMessagesError, RustBuildMetaParseError, WriteTestListError},
helpers::convert_rel_path_to_forward_slash,
list::{BinaryListState, OutputFormat, RustBuildMeta, Styles},
platform::BuildPlatforms,
write_str::WriteStr,
};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::{Artifact, BuildScript, Message, PackageId};
use guppy::graph::PackageGraph;
use nextest_metadata::{
BinaryListSummary, BuildPlatform, RustBinaryId, RustNonTestBinaryKind,
RustNonTestBinarySummary, RustTestBinaryKind, RustTestBinarySummary,
};
use owo_colors::OwoColorize;
use std::{collections::HashSet, io};
use tracing::warn;
#[derive(Clone, Debug)]
pub struct RustTestBinary {
pub id: RustBinaryId,
pub path: Utf8PathBuf,
pub package_id: String,
pub kind: RustTestBinaryKind,
pub name: String,
pub build_platform: BuildPlatform,
}
#[derive(Clone, Debug)]
pub struct BinaryList {
pub rust_build_meta: RustBuildMeta<BinaryListState>,
pub rust_binaries: Vec<RustTestBinary>,
}
impl BinaryList {
pub fn from_messages(
reader: impl io::BufRead,
graph: &PackageGraph,
build_platforms: BuildPlatforms,
) -> Result<Self, FromMessagesError> {
let mut state = BinaryListBuildState::new(graph, build_platforms);
for message in Message::parse_stream(reader) {
let message = message.map_err(FromMessagesError::ReadMessages)?;
state.process_message(message)?;
}
Ok(state.finish())
}
pub fn from_summary(summary: BinaryListSummary) -> Result<Self, RustBuildMetaParseError> {
let rust_binaries = summary
.rust_binaries
.into_values()
.map(|bin| RustTestBinary {
name: bin.binary_name,
path: bin.binary_path,
package_id: bin.package_id,
kind: bin.kind,
id: bin.binary_id,
build_platform: bin.build_platform,
})
.collect();
Ok(Self {
rust_build_meta: RustBuildMeta::from_summary(summary.rust_build_meta)?,
rust_binaries,
})
}
pub fn write(
&self,
output_format: OutputFormat,
writer: &mut dyn WriteStr,
colorize: bool,
) -> Result<(), WriteTestListError> {
match output_format {
OutputFormat::Human { verbose } => self
.write_human(writer, verbose, colorize)
.map_err(WriteTestListError::Io),
OutputFormat::Serializable(format) => format.to_writer(&self.to_summary(), writer),
}
}
fn to_summary(&self) -> BinaryListSummary {
let rust_binaries = self
.rust_binaries
.iter()
.map(|bin| {
let summary = RustTestBinarySummary {
binary_name: bin.name.clone(),
package_id: bin.package_id.clone(),
kind: bin.kind.clone(),
binary_path: bin.path.clone(),
binary_id: bin.id.clone(),
build_platform: bin.build_platform,
};
(bin.id.clone(), summary)
})
.collect();
BinaryListSummary {
rust_build_meta: self.rust_build_meta.to_summary(),
rust_binaries,
}
}
fn write_human(
&self,
writer: &mut dyn WriteStr,
verbose: bool,
colorize: bool,
) -> io::Result<()> {
let mut styles = Styles::default();
if colorize {
styles.colorize();
}
for bin in &self.rust_binaries {
if verbose {
writeln!(writer, "{}:", bin.id.style(styles.binary_id))?;
writeln!(writer, " {} {}", "bin:".style(styles.field), bin.path)?;
writeln!(
writer,
" {} {}",
"build platform:".style(styles.field),
bin.build_platform,
)?;
} else {
writeln!(writer, "{}", bin.id.style(styles.binary_id))?;
}
}
Ok(())
}
pub fn to_string(&self, output_format: OutputFormat) -> Result<String, WriteTestListError> {
let mut s = String::with_capacity(1024);
self.write(output_format, &mut s, false)?;
Ok(s)
}
}
#[derive(Debug)]
struct BinaryListBuildState<'g> {
graph: &'g PackageGraph,
rust_binaries: Vec<RustTestBinary>,
rust_build_meta: RustBuildMeta<BinaryListState>,
}
impl<'g> BinaryListBuildState<'g> {
fn new(graph: &'g PackageGraph, build_platforms: BuildPlatforms) -> Self {
let rust_target_dir = graph.workspace().target_directory().to_path_buf();
Self {
graph,
rust_binaries: vec![],
rust_build_meta: RustBuildMeta::new(rust_target_dir, build_platforms),
}
}
fn process_message(&mut self, message: Message) -> Result<(), FromMessagesError> {
match message {
Message::CompilerArtifact(artifact) => {
self.process_artifact(artifact)?;
}
Message::BuildScriptExecuted(build_script) => {
self.process_build_script(build_script)?;
}
_ => {
}
}
Ok(())
}
fn process_artifact(&mut self, artifact: Artifact) -> Result<(), FromMessagesError> {
if let Some(path) = artifact.executable {
self.detect_base_output_dir(&path);
if artifact.profile.test {
let package_id = artifact.package_id.repr;
let name = artifact.target.name;
let package = self
.graph
.metadata(&guppy::PackageId::new(package_id.clone()))
.map_err(FromMessagesError::PackageGraph)?;
let kind = artifact.target.kind;
if kind.is_empty() {
return Err(FromMessagesError::MissingTargetKind {
package_name: package.name().to_owned(),
binary_name: name.clone(),
});
}
let (computed_kind, platform) = if kind.iter().any(|k| {
k == "lib" || k == "rlib" || k == "dylib" || k == "cdylib" || k == "staticlib"
}) {
(RustTestBinaryKind::LIB, BuildPlatform::Target)
} else if kind.first().map(String::as_str) == Some("proc-macro") {
(RustTestBinaryKind::PROC_MACRO, BuildPlatform::Host)
} else {
(
RustTestBinaryKind::new(
kind.into_iter()
.next()
.expect("already checked that kind is non-empty"),
),
BuildPlatform::Target,
)
};
let id = RustBinaryId::from_parts(package.name(), &computed_kind, &name);
self.rust_binaries.push(RustTestBinary {
path,
package_id,
kind: computed_kind,
name,
id,
build_platform: platform,
});
} else if artifact.target.kind.iter().any(|x| x == "bin") {
if let Ok(rel_path) = path.strip_prefix(&self.rust_build_meta.target_directory) {
let non_test_binary = RustNonTestBinarySummary {
name: artifact.target.name,
kind: RustNonTestBinaryKind::BIN_EXE,
path: convert_rel_path_to_forward_slash(rel_path),
};
self.rust_build_meta
.non_test_binaries
.entry(artifact.package_id.repr)
.or_default()
.insert(non_test_binary);
};
}
} else if artifact.target.kind.iter().any(|x| x.contains("dylib")) {
for filename in artifact.filenames {
if let Ok(rel_path) = filename.strip_prefix(&self.rust_build_meta.target_directory)
{
let non_test_binary = RustNonTestBinarySummary {
name: artifact.target.name.clone(),
kind: RustNonTestBinaryKind::DYLIB,
path: convert_rel_path_to_forward_slash(rel_path),
};
self.rust_build_meta
.non_test_binaries
.entry(artifact.package_id.repr.clone())
.or_default()
.insert(non_test_binary);
}
}
}
Ok(())
}
fn detect_base_output_dir(&mut self, artifact_path: &Utf8Path) -> Option<()> {
let rel_path = artifact_path
.strip_prefix(&self.rust_build_meta.target_directory)
.ok()?;
let parent = rel_path.parent()?;
if parent.file_name() == Some("deps") {
let base = parent.parent()?;
if !self.rust_build_meta.base_output_directories.contains(base) {
self.rust_build_meta
.base_output_directories
.insert(convert_rel_path_to_forward_slash(base));
}
}
Some(())
}
fn process_build_script(&mut self, build_script: BuildScript) -> Result<(), FromMessagesError> {
for path in build_script.linked_paths {
self.detect_linked_path(&build_script.package_id, &path);
}
let package_id = guppy::PackageId::new(build_script.package_id.repr);
let in_workspace = self.graph.metadata(&package_id).map_or_else(
|_| {
warn!(
target: "nextest-runner::list",
"warning: saw package ID `{}` which wasn't produced by cargo metadata",
package_id
);
false
},
|p| p.in_workspace(),
);
if in_workspace {
if let Ok(rel_out_dir) = build_script
.out_dir
.strip_prefix(&self.rust_build_meta.target_directory)
{
self.rust_build_meta.build_script_out_dirs.insert(
package_id.repr().to_owned(),
convert_rel_path_to_forward_slash(rel_out_dir),
);
}
}
Ok(())
}
fn detect_linked_path(&mut self, package_id: &PackageId, path: &Utf8Path) -> Option<()> {
let actual_path = match path.as_str().split_once('=') {
Some((_, p)) => p.into(),
None => path,
};
let rel_path = actual_path
.strip_prefix(&self.rust_build_meta.target_directory)
.ok()?;
self.rust_build_meta
.linked_paths
.entry(convert_rel_path_to_forward_slash(rel_path))
.or_default()
.insert(package_id.repr.clone());
Some(())
}
fn finish(mut self) -> BinaryList {
self.rust_binaries.sort_by(|b1, b2| b1.id.cmp(&b2.id));
let relevant_package_ids = self
.rust_binaries
.iter()
.map(|bin| bin.package_id.clone())
.collect::<HashSet<_>>();
self.rust_build_meta
.build_script_out_dirs
.retain(|package_id, _| relevant_package_ids.contains(package_id));
BinaryList {
rust_build_meta: self.rust_build_meta,
rust_binaries: self.rust_binaries,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cargo_config::{TargetDefinitionLocation, TargetTriple, TargetTripleSource},
list::SerializableFormat,
platform::{HostPlatform, PlatformLibdir, TargetPlatform},
};
use indoc::indoc;
use maplit::btreeset;
use nextest_metadata::PlatformLibdirUnavailable;
use pretty_assertions::assert_eq;
use target_spec::{Platform, TargetFeatures};
#[test]
fn test_parse_binary_list() {
let fake_bin_test = RustTestBinary {
id: "fake-package::bin/fake-binary".into(),
path: "/fake/binary".into(),
package_id: "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)"
.to_owned(),
kind: RustTestBinaryKind::LIB,
name: "fake-binary".to_owned(),
build_platform: BuildPlatform::Target,
};
let fake_macro_test = RustTestBinary {
id: "fake-macro::proc-macro/fake-macro".into(),
path: "/fake/macro".into(),
package_id: "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)"
.to_owned(),
kind: RustTestBinaryKind::PROC_MACRO,
name: "fake-macro".to_owned(),
build_platform: BuildPlatform::Host,
};
let fake_triple = TargetTriple {
platform: Platform::new("aarch64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
source: TargetTripleSource::CliOption,
location: TargetDefinitionLocation::Builtin,
};
let fake_host_libdir = "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib";
let build_platforms = BuildPlatforms {
host: HostPlatform {
platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
libdir: PlatformLibdir::Available(Utf8PathBuf::from(fake_host_libdir)),
},
target: Some(TargetPlatform {
triple: fake_triple,
libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
}),
};
let mut rust_build_meta = RustBuildMeta::new("/fake/target", build_platforms);
rust_build_meta
.base_output_directories
.insert("my-profile".into());
rust_build_meta.non_test_binaries.insert(
"my-package-id".into(),
btreeset! {
RustNonTestBinarySummary {
name: "my-name".into(),
kind: RustNonTestBinaryKind::BIN_EXE,
path: "my-profile/my-name".into(),
},
RustNonTestBinarySummary {
name: "your-name".into(),
kind: RustNonTestBinaryKind::DYLIB,
path: "my-profile/your-name.dll".into(),
},
RustNonTestBinarySummary {
name: "your-name".into(),
kind: RustNonTestBinaryKind::DYLIB,
path: "my-profile/your-name.exp".into(),
},
},
);
let binary_list = BinaryList {
rust_build_meta,
rust_binaries: vec![fake_bin_test, fake_macro_test],
};
static EXPECTED_HUMAN: &str = indoc! {"
fake-package::bin/fake-binary
fake-macro::proc-macro/fake-macro
"};
static EXPECTED_HUMAN_VERBOSE: &str = indoc! {r#"
fake-package::bin/fake-binary:
bin: /fake/binary
build platform: target
fake-macro::proc-macro/fake-macro:
bin: /fake/macro
build platform: host
"#};
static EXPECTED_JSON_PRETTY: &str = indoc! {r#"
{
"rust-build-meta": {
"target-directory": "/fake/target",
"base-output-directories": [
"my-profile"
],
"non-test-binaries": {
"my-package-id": [
{
"name": "my-name",
"kind": "bin-exe",
"path": "my-profile/my-name"
},
{
"name": "your-name",
"kind": "dylib",
"path": "my-profile/your-name.dll"
},
{
"name": "your-name",
"kind": "dylib",
"path": "my-profile/your-name.exp"
}
]
},
"build-script-out-dirs": {},
"linked-paths": [],
"platforms": {
"host": {
"platform": {
"triple": "x86_64-unknown-linux-gnu",
"target-features": "unknown"
},
"libdir": {
"status": "available",
"path": "/home/fake/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib"
}
},
"targets": [
{
"platform": {
"triple": "aarch64-unknown-linux-gnu",
"target-features": "unknown"
},
"libdir": {
"status": "unavailable",
"reason": "rustc-output-error"
}
}
]
},
"target-platforms": [
{
"triple": "aarch64-unknown-linux-gnu",
"target-features": "unknown"
}
],
"target-platform": "aarch64-unknown-linux-gnu"
},
"rust-binaries": {
"fake-macro::proc-macro/fake-macro": {
"binary-id": "fake-macro::proc-macro/fake-macro",
"binary-name": "fake-macro",
"package-id": "fake-macro 0.1.0 (path+file:///Users/fakeuser/project/fake-macro)",
"kind": "proc-macro",
"binary-path": "/fake/macro",
"build-platform": "host"
},
"fake-package::bin/fake-binary": {
"binary-id": "fake-package::bin/fake-binary",
"binary-name": "fake-binary",
"package-id": "fake-package 0.1.0 (path+file:///Users/fakeuser/project/fake-package)",
"kind": "lib",
"binary-path": "/fake/binary",
"build-platform": "target"
}
}
}"#};
assert_eq!(
binary_list
.to_string(OutputFormat::Human { verbose: false })
.expect("human succeeded"),
EXPECTED_HUMAN
);
assert_eq!(
binary_list
.to_string(OutputFormat::Human { verbose: true })
.expect("human succeeded"),
EXPECTED_HUMAN_VERBOSE
);
assert_eq!(
binary_list
.to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
.expect("json-pretty succeeded"),
EXPECTED_JSON_PRETTY
);
}
}