use crate::{
errors::RustBuildMetaParseError,
helpers::convert_rel_path_to_main_sep,
list::{BinaryListState, TestListState},
platform::{BuildPlatforms, TargetPlatform},
reuse_build::PathMapper,
};
use camino::Utf8PathBuf;
use itertools::Itertools;
use nextest_metadata::{
BuildPlatformsSummary, BuildScriptInfoSummary, RustBuildMetaSummary, RustNonTestBinarySummary,
};
use std::{
collections::{BTreeMap, BTreeSet},
marker::PhantomData,
};
use tracing::warn;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RustBuildMeta<State> {
pub target_directory: Utf8PathBuf,
pub build_directory: Utf8PathBuf,
pub base_output_directories: BTreeSet<Utf8PathBuf>,
pub non_test_binaries: BTreeMap<String, BTreeSet<RustNonTestBinarySummary>>,
pub build_script_out_dirs: BTreeMap<String, Utf8PathBuf>,
pub build_script_info: Option<BTreeMap<String, BuildScriptInfo>>,
pub linked_paths: BTreeMap<Utf8PathBuf, BTreeSet<String>>,
pub build_platforms: BuildPlatforms,
pub state: PhantomData<State>,
}
impl RustBuildMeta<BinaryListState> {
pub fn new(
target_directory: impl Into<Utf8PathBuf>,
build_directory: impl Into<Utf8PathBuf>,
build_platforms: BuildPlatforms,
) -> Self {
Self {
target_directory: target_directory.into(),
build_directory: build_directory.into(),
base_output_directories: BTreeSet::new(),
non_test_binaries: BTreeMap::new(),
build_script_out_dirs: BTreeMap::new(),
build_script_info: Some(BTreeMap::new()),
linked_paths: BTreeMap::new(),
state: PhantomData,
build_platforms,
}
}
pub fn map_paths(&self, path_mapper: &PathMapper) -> RustBuildMeta<TestListState> {
let new_target_directory = path_mapper
.new_target_dir()
.unwrap_or(&self.target_directory)
.to_path_buf();
let new_build_directory = path_mapper
.new_build_dir()
.or(path_mapper.new_target_dir())
.unwrap_or(&self.build_directory)
.to_path_buf();
RustBuildMeta {
target_directory: new_target_directory,
build_directory: new_build_directory,
base_output_directories: self.base_output_directories.clone(),
non_test_binaries: self.non_test_binaries.clone(),
build_script_out_dirs: self.build_script_out_dirs.clone(),
build_script_info: self.build_script_info.clone(),
linked_paths: self.linked_paths.clone(),
state: PhantomData,
build_platforms: self.build_platforms.map_libdir(path_mapper.libdir_mapper()),
}
}
}
impl RustBuildMeta<TestListState> {
pub fn empty() -> Self {
Self {
target_directory: Utf8PathBuf::new(),
build_directory: Utf8PathBuf::new(),
base_output_directories: BTreeSet::new(),
non_test_binaries: BTreeMap::new(),
build_script_out_dirs: BTreeMap::new(),
build_script_info: Some(BTreeMap::new()),
linked_paths: BTreeMap::new(),
state: PhantomData,
build_platforms: BuildPlatforms::new_with_no_target().unwrap(),
}
}
pub fn dylib_paths(&self) -> Vec<Utf8PathBuf> {
let libdirs = self
.build_platforms
.host
.libdir
.as_path()
.into_iter()
.chain(
self.build_platforms
.target
.as_ref()
.and_then(|target| target.libdir.as_path()),
)
.map(|libdir| libdir.to_path_buf())
.collect::<Vec<_>>();
if libdirs.is_empty() {
warn!("failed to detect the rustc libdir, may fail to list or run tests");
}
self.linked_paths
.keys()
.filter_map(|rel_path| {
let join_path = self
.build_directory
.join(convert_rel_path_to_main_sep(rel_path));
join_path.exists().then_some(join_path)
})
.chain(self.base_output_directories.iter().flat_map(|base_output| {
let abs_base = self
.build_directory
.join(convert_rel_path_to_main_sep(base_output));
let with_deps = abs_base.join("deps");
[with_deps, abs_base]
}))
.chain(libdirs)
.unique()
.collect()
}
}
impl<State> RustBuildMeta<State> {
pub fn from_summary(summary: RustBuildMetaSummary) -> Result<Self, RustBuildMetaParseError> {
let build_platforms = if let Some(summary) = summary.platforms {
BuildPlatforms::from_summary(summary.clone())?
} else if let Some(summary) = summary.target_platforms.first() {
BuildPlatforms::from_target_summary(summary.clone())?
} else {
BuildPlatforms::from_summary_str(summary.target_platform.clone())?
};
let build_directory = summary
.build_directory
.unwrap_or_else(|| summary.target_directory.clone());
Ok(Self {
target_directory: summary.target_directory,
build_directory,
base_output_directories: summary.base_output_directories,
build_script_out_dirs: summary.build_script_out_dirs,
build_script_info: summary.build_script_info.map(|info| {
info.into_iter()
.map(|(k, v)| (k, BuildScriptInfo::from_summary(v)))
.collect()
}),
non_test_binaries: summary.non_test_binaries,
linked_paths: summary
.linked_paths
.into_iter()
.map(|linked_path| (linked_path, BTreeSet::new()))
.collect(),
state: PhantomData,
build_platforms,
})
}
pub fn to_summary(&self) -> RustBuildMetaSummary {
RustBuildMetaSummary {
target_directory: self.target_directory.clone(),
build_directory: Some(self.build_directory.clone()),
base_output_directories: self.base_output_directories.clone(),
non_test_binaries: self.non_test_binaries.clone(),
build_script_out_dirs: self.build_script_out_dirs.clone(),
build_script_info: self.build_script_info.as_ref().map(|info| {
info.iter()
.map(|(k, v)| (k.clone(), v.to_summary()))
.collect()
}),
linked_paths: self.linked_paths.keys().cloned().collect(),
target_platform: self.build_platforms.to_summary_str(),
target_platforms: vec![self.build_platforms.to_target_or_host_summary()],
platforms: Some(BuildPlatformsSummary {
host: self.build_platforms.host.to_summary(),
targets: self
.build_platforms
.target
.as_ref()
.into_iter()
.map(TargetPlatform::to_summary)
.collect(),
}),
}
}
pub fn to_archive_summary(&self) -> RustBuildMetaSummary {
let mut summary = self.to_summary();
summary.build_directory = None;
summary
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct BuildScriptInfo {
pub envs: BTreeMap<String, String>,
}
impl BuildScriptInfo {
fn from_summary(summary: BuildScriptInfoSummary) -> Self {
Self { envs: summary.envs }
}
fn to_summary(&self) -> BuildScriptInfoSummary {
BuildScriptInfoSummary {
envs: self.envs.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cargo_config::TargetTriple,
platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform},
};
use nextest_metadata::{
BuildPlatformsSummary, HostPlatformSummary, PlatformLibdirSummary,
PlatformLibdirUnavailable,
};
use target_spec::{Platform, summaries::PlatformSummary};
use test_case::test_case;
impl Default for RustBuildMeta<BinaryListState> {
fn default() -> Self {
RustBuildMeta::<BinaryListState>::new(
Utf8PathBuf::default(),
Utf8PathBuf::default(),
BuildPlatforms::new_with_no_target()
.expect("creating BuildPlatforms without target triple should succeed"),
)
}
}
fn x86_64_pc_windows_msvc_triple() -> TargetTriple {
TargetTriple::deserialize_str(Some("x86_64-pc-windows-msvc".to_owned()))
.expect("creating TargetTriple should succeed")
.expect("the output of deserialize_str shouldn't be None")
}
fn host_current() -> HostPlatform {
HostPlatform {
platform: Platform::build_target()
.expect("should detect the build target successfully"),
libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
}
}
fn host_current_with_libdir(libdir: &str) -> HostPlatform {
HostPlatform {
platform: Platform::build_target()
.expect("should detect the build target successfully"),
libdir: PlatformLibdir::Available(libdir.into()),
}
}
fn host_not_current_with_libdir(libdir: &str) -> HostPlatform {
cfg_if::cfg_if! {
if #[cfg(windows)] {
let triple = TargetTriple::x86_64_unknown_linux_gnu();
} else {
let triple = x86_64_pc_windows_msvc_triple();
}
};
HostPlatform {
platform: triple.platform,
libdir: PlatformLibdir::Available(libdir.into()),
}
}
fn target_linux() -> TargetPlatform {
TargetPlatform::new(
TargetTriple::x86_64_unknown_linux_gnu(),
PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
)
}
fn target_linux_with_libdir(libdir: &str) -> TargetPlatform {
TargetPlatform::new(
TargetTriple::x86_64_unknown_linux_gnu(),
PlatformLibdir::Available(libdir.into()),
)
}
fn target_windows() -> TargetPlatform {
TargetPlatform::new(
x86_64_pc_windows_msvc_triple(),
PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
)
}
#[test_case(RustBuildMetaSummary {
..Default::default()
}, RustBuildMeta::<BinaryListState> {
build_platforms: BuildPlatforms {
host: host_current(),
target: None,
},
// Summary has no build_script_info field, so from_summary produces None.
build_script_info: None,
..Default::default()
}; "no target platforms")]
#[test_case(RustBuildMetaSummary {
target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
..Default::default()
}, RustBuildMeta::<BinaryListState> {
build_platforms: BuildPlatforms {
host: host_current(),
target: Some(target_linux()),
},
build_script_info: None,
..Default::default()
}; "only target platform field")]
#[test_case(RustBuildMetaSummary {
target_platform: Some("x86_64-unknown-linux-gnu".to_owned()),
// target_platforms should be preferred over target_platform
target_platforms: vec![PlatformSummary::new("x86_64-pc-windows-msvc")],
..Default::default()
}, RustBuildMeta::<BinaryListState> {
build_platforms: BuildPlatforms {
host: host_current(),
target: Some(target_windows()),
},
build_script_info: None,
..Default::default()
}; "target platform and target platforms field")]
#[test_case(RustBuildMetaSummary {
target_platform: Some("aarch64-unknown-linux-gnu".to_owned()),
target_platforms: vec![PlatformSummary::new("x86_64-pc-windows-msvc")],
// platforms should be preferred over both target_platform and target_platforms
platforms: Some(BuildPlatformsSummary {
host: host_not_current_with_libdir("/fake/test/libdir/281").to_summary(),
targets: vec![target_linux_with_libdir("/fake/test/libdir/837").to_summary()],
}),
..Default::default()
}, RustBuildMeta::<BinaryListState> {
build_platforms: BuildPlatforms {
host: host_not_current_with_libdir("/fake/test/libdir/281"),
target: Some(target_linux_with_libdir("/fake/test/libdir/837")),
},
build_script_info: None,
..Default::default()
}; "target platform and target platforms and platforms field")]
#[test_case(RustBuildMetaSummary {
platforms: Some(BuildPlatformsSummary {
host: host_current().to_summary(),
targets: vec![],
}),
..Default::default()
}, RustBuildMeta::<BinaryListState> {
build_platforms: BuildPlatforms {
host: host_current(),
target: None,
},
build_script_info: None,
..Default::default()
}; "platforms with zero targets")]
#[test_case(RustBuildMetaSummary {
target_directory: "/fake/target".into(),
build_directory: Some("/fake/build".into()),
platforms: Some(BuildPlatformsSummary {
host: host_current().to_summary(),
targets: vec![],
}),
..Default::default()
}, RustBuildMeta::<BinaryListState> {
target_directory: "/fake/target".into(),
build_directory: "/fake/build".into(),
build_platforms: BuildPlatforms {
host: host_current(),
target: None,
},
build_script_info: None,
..Default::default()
}; "build directory differs from target directory")]
#[test_case(RustBuildMetaSummary {
target_directory: "/fake/target".into(),
build_directory: None,
platforms: Some(BuildPlatformsSummary {
host: host_current().to_summary(),
targets: vec![],
}),
..Default::default()
}, RustBuildMeta::<BinaryListState> {
target_directory: "/fake/target".into(),
// When build_directory is absent, it defaults to target_directory.
build_directory: "/fake/target".into(),
build_platforms: BuildPlatforms {
host: host_current(),
target: None,
},
build_script_info: None,
..Default::default()
}; "build directory absent defaults to target directory")]
fn test_from_summary(summary: RustBuildMetaSummary, expected: RustBuildMeta<BinaryListState>) {
let actual = RustBuildMeta::<BinaryListState>::from_summary(summary)
.expect("RustBuildMeta should deserialize from summary with success.");
assert_eq!(actual, expected);
}
#[test]
fn test_from_summary_error_multiple_targets() {
let summary = RustBuildMetaSummary {
platforms: Some(BuildPlatformsSummary {
host: host_current().to_summary(),
targets: vec![target_linux().to_summary(), target_windows().to_summary()],
}),
..Default::default()
};
let actual = RustBuildMeta::<BinaryListState>::from_summary(summary);
assert!(
matches!(actual, Err(RustBuildMetaParseError::Unsupported { .. })),
"Expect the parse result to be an error of RustBuildMetaParseError::Unsupported, actual {actual:?}"
);
}
#[test]
fn test_from_summary_error_invalid_host_platform_summary() {
let summary = RustBuildMetaSummary {
platforms: Some(BuildPlatformsSummary {
host: HostPlatformSummary {
platform: PlatformSummary::new("invalid-platform-triple"),
libdir: PlatformLibdirSummary::Unavailable {
reason: PlatformLibdirUnavailable::RUSTC_FAILED,
},
},
targets: vec![],
}),
..Default::default()
};
let actual = RustBuildMeta::<BinaryListState>::from_summary(summary);
actual.expect_err("parse result should be an error");
}
#[test_case(RustBuildMeta::<BinaryListState> {
build_platforms: BuildPlatforms {
host: host_current(),
target: None,
},
..Default::default()
}, RustBuildMetaSummary {
target_platform: None,
target_platforms: vec![host_current().to_summary().platform],
platforms: Some(BuildPlatformsSummary {
host: host_current().to_summary(),
targets: vec![],
}),
build_script_info: Some(BTreeMap::new()),
build_directory: Some(Utf8PathBuf::new()),
..Default::default()
}; "build platforms without target")]
#[test_case(RustBuildMeta::<BinaryListState> {
build_platforms: BuildPlatforms {
host: host_current_with_libdir("/fake/test/libdir/736"),
target: Some(target_linux_with_libdir("/fake/test/libdir/873")),
},
..Default::default()
}, RustBuildMetaSummary {
target_platform: Some(
target_linux_with_libdir("/fake/test/libdir/873")
.triple
.platform
.triple_str()
.to_owned(),
),
target_platforms: vec![target_linux_with_libdir("/fake/test/libdir/873").triple.platform.to_summary()],
platforms: Some(BuildPlatformsSummary {
host: host_current_with_libdir("/fake/test/libdir/736").to_summary(),
targets: vec![target_linux_with_libdir("/fake/test/libdir/873").to_summary()],
}),
build_script_info: Some(BTreeMap::new()),
build_directory: Some(Utf8PathBuf::new()),
..Default::default()
}; "build platforms with target")]
#[test_case(RustBuildMeta::<BinaryListState> {
target_directory: "/fake/target".into(),
build_directory: "/fake/build".into(),
build_platforms: BuildPlatforms {
host: host_current(),
target: None,
},
..Default::default()
}, RustBuildMetaSummary {
target_directory: "/fake/target".into(),
// build_directory is emitted when it differs from target_directory.
build_directory: Some("/fake/build".into()),
target_platform: None,
target_platforms: vec![host_current().to_summary().platform],
platforms: Some(BuildPlatformsSummary {
host: host_current().to_summary(),
targets: vec![],
}),
build_script_info: Some(BTreeMap::new()),
..Default::default()
}; "build directory differs from target directory")]
#[test_case(RustBuildMeta::<BinaryListState> {
target_directory: "/fake/target".into(),
build_directory: "/fake/target".into(),
build_platforms: BuildPlatforms {
host: host_current(),
target: None,
},
..Default::default()
}, RustBuildMetaSummary {
target_directory: "/fake/target".into(),
// build_directory is always emitted by to_summary().
build_directory: Some("/fake/target".into()),
target_platform: None,
target_platforms: vec![host_current().to_summary().platform],
platforms: Some(BuildPlatformsSummary {
host: host_current().to_summary(),
targets: vec![],
}),
build_script_info: Some(BTreeMap::new()),
..Default::default()
}; "build directory equals target directory")]
fn test_to_summary(meta: RustBuildMeta<BinaryListState>, expected: RustBuildMetaSummary) {
let actual = meta.to_summary();
assert_eq!(actual, expected);
}
#[test]
fn test_to_archive_summary_omits_build_directory() {
let meta = RustBuildMeta::<BinaryListState> {
target_directory: "/fake/target".into(),
build_directory: "/fake/build".into(),
build_platforms: BuildPlatforms {
host: host_current(),
target: None,
},
..Default::default()
};
let archive_summary = meta.to_archive_summary();
assert_eq!(
archive_summary.build_directory, None,
"to_archive_summary should always set build_directory to None"
);
assert_eq!(archive_summary.target_directory, meta.target_directory);
let round_tripped = RustBuildMeta::<BinaryListState>::from_summary(archive_summary)
.expect("from_summary should succeed");
assert_eq!(
round_tripped.build_directory, round_tripped.target_directory,
"after round-trip through archive summary, \
build_directory should equal target_directory"
);
}
#[test]
fn test_dylib_paths_should_include_rustc_dir() {
let host_libdir = Utf8PathBuf::from("/fake/rustc/host/libdir");
let target_libdir = Utf8PathBuf::from("/fake/rustc/target/libdir");
let rust_build_meta = RustBuildMeta {
build_platforms: BuildPlatforms {
host: host_current_with_libdir(host_libdir.as_ref()),
target: Some(TargetPlatform::new(
TargetTriple::x86_64_unknown_linux_gnu(),
PlatformLibdir::Available(target_libdir.clone()),
)),
},
..RustBuildMeta::empty()
};
let dylib_paths = rust_build_meta.dylib_paths();
assert!(
dylib_paths.contains(&host_libdir),
"{dylib_paths:?} should contain {host_libdir}"
);
assert!(
dylib_paths.contains(&target_libdir),
"{dylib_paths:?} should contain {target_libdir}"
);
}
#[test]
fn test_dylib_paths_should_not_contain_duplicate_paths() {
let tmpdir = camino_tempfile::tempdir().expect("should create temp dir successfully");
let host_libdir = tmpdir.path().to_path_buf();
let target_libdir = host_libdir.clone();
let fake_target_dir = tmpdir
.path()
.parent()
.expect("tmp directory should have a parent");
let tmpdir_dirname = tmpdir
.path()
.file_name()
.expect("tmp directory should have a file name");
let rust_build_meta = RustBuildMeta {
target_directory: fake_target_dir.to_path_buf(),
build_directory: fake_target_dir.to_path_buf(),
linked_paths: [(Utf8PathBuf::from(tmpdir_dirname), Default::default())].into(),
base_output_directories: [Utf8PathBuf::from(tmpdir_dirname)].into(),
build_platforms: BuildPlatforms {
host: host_current_with_libdir(host_libdir.as_ref()),
target: Some(TargetPlatform::new(
TargetTriple::x86_64_unknown_linux_gnu(),
PlatformLibdir::Available(target_libdir.clone()),
)),
},
..RustBuildMeta::empty()
};
let dylib_paths = rust_build_meta.dylib_paths();
let expected_abs = fake_target_dir.join(tmpdir_dirname);
assert!(
dylib_paths.contains(&expected_abs),
"{dylib_paths:?} should contain {expected_abs}"
);
assert!(
dylib_paths.clone().into_iter().all_unique(),
"{dylib_paths:?} should not contain duplicate paths"
);
}
}