use crate::{
RustcCli,
cargo_config::{CargoTargetArg, TargetTriple},
errors::{
DisplayErrorChain, HostPlatformDetectError, RustBuildMetaParseError, TargetTripleError,
},
indenter::DisplayIndented,
reuse_build::{LibdirMapper, PlatformLibdirMapper},
};
use camino::{Utf8Path, Utf8PathBuf};
use nextest_metadata::{
BuildPlatformsSummary, HostPlatformSummary, PlatformLibdirSummary, PlatformLibdirUnavailable,
TargetPlatformSummary,
};
pub use target_spec::Platform;
use target_spec::{
TargetFeatures, errors::RustcVersionVerboseParseError, summaries::PlatformSummary,
};
use tracing::{debug, warn};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BuildPlatforms {
pub host: HostPlatform,
pub target: Option<TargetPlatform>,
}
impl BuildPlatforms {
pub fn new_with_no_target() -> Result<Self, HostPlatformDetectError> {
Ok(Self {
host: HostPlatform {
platform: Platform::build_target().map_err(|build_target_error| {
HostPlatformDetectError::BuildTargetError {
build_target_error: Box::new(build_target_error),
}
})?,
libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
},
target: None,
})
}
pub fn map_libdir(&self, mapper: &LibdirMapper) -> Self {
Self {
host: self.host.map_libdir(&mapper.host),
target: self
.target
.as_ref()
.map(|target| target.map_libdir(&mapper.target)),
}
}
pub fn to_cargo_target_arg(&self) -> Result<CargoTargetArg, TargetTripleError> {
match &self.target {
Some(target) => target.triple.to_cargo_target_arg(),
None => {
Ok(CargoTargetArg::Builtin(
self.host.platform.triple_str().to_owned(),
))
}
}
}
pub fn to_summary(&self) -> BuildPlatformsSummary {
BuildPlatformsSummary {
host: self.host.to_summary(),
targets: self
.target
.as_ref()
.map(|target| vec![target.to_summary()])
.unwrap_or_default(),
}
}
pub fn to_target_or_host_summary(&self) -> PlatformSummary {
if let Some(target) = &self.target {
target.triple.platform.to_summary()
} else {
self.host.platform.to_summary()
}
}
pub fn to_summary_str(&self) -> Option<String> {
self.target
.as_ref()
.map(|triple| triple.triple.platform.triple_str().to_owned())
}
pub fn from_summary(summary: BuildPlatformsSummary) -> Result<Self, RustBuildMetaParseError> {
Ok(BuildPlatforms {
host: HostPlatform::from_summary(summary.host)?,
target: {
if summary.targets.len() > 1 {
return Err(RustBuildMetaParseError::Unsupported {
message: "multiple build targets is not supported".to_owned(),
});
}
summary
.targets
.first()
.map(|target| TargetPlatform::from_summary(target.clone()))
.transpose()?
},
})
}
pub fn from_target_summary(summary: PlatformSummary) -> Result<Self, RustBuildMetaParseError> {
let host = HostPlatform {
platform: Platform::build_target()
.map_err(RustBuildMetaParseError::DetectBuildTargetError)?,
libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
};
let target = TargetTriple::deserialize(Some(summary))?.map(|triple| {
TargetPlatform::new(
triple,
PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
)
});
Ok(Self { host, target })
}
pub fn from_summary_str(summary: Option<String>) -> Result<Self, RustBuildMetaParseError> {
let host = HostPlatform {
platform: Platform::build_target()
.map_err(RustBuildMetaParseError::DetectBuildTargetError)?,
libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
};
let target = TargetTriple::deserialize_str(summary)?.map(|triple| {
TargetPlatform::new(
triple,
PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
)
});
Ok(Self { host, target })
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HostPlatform {
pub platform: Platform,
pub libdir: PlatformLibdir,
}
impl HostPlatform {
pub fn detect(libdir: PlatformLibdir) -> Result<Self, HostPlatformDetectError> {
let platform = detect_host_platform()?;
Ok(Self { platform, libdir })
}
pub fn to_summary(&self) -> HostPlatformSummary {
HostPlatformSummary {
platform: self.platform.to_summary(),
libdir: self.libdir.to_summary(),
}
}
pub fn from_summary(summary: HostPlatformSummary) -> Result<Self, RustBuildMetaParseError> {
let platform = summary
.platform
.to_platform()
.map_err(RustBuildMetaParseError::PlatformDeserializeError)?;
Ok(Self {
platform,
libdir: PlatformLibdir::from_summary(summary.libdir),
})
}
fn map_libdir(&self, mapper: &PlatformLibdirMapper) -> Self {
Self {
platform: self.platform.clone(),
libdir: mapper.map(&self.libdir),
}
}
}
fn detect_host_platform() -> Result<Platform, HostPlatformDetectError> {
const FORCE_BUILD_TARGET_VAR: &str = "__NEXTEST_FORCE_BUILD_TARGET";
let build_target = match std::env::var(FORCE_BUILD_TARGET_VAR).as_deref() {
Ok("error") => Err(target_spec::Error::RustcVersionVerboseParse(
RustcVersionVerboseParseError::MissingHostLine {
output: format!(
"({FORCE_BUILD_TARGET_VAR} set to \"error\", forcibly failing build target detection)\n"
),
},
)),
Ok(triple) => Platform::new(triple.to_owned(), TargetFeatures::Unknown),
Err(_) => Platform::build_target(),
};
let rustc_vv = RustcCli::version_verbose()
.to_expression()
.stdout_capture()
.stderr_capture()
.unchecked();
match rustc_vv.run() {
Ok(output) => {
if output.status.success() {
match Platform::from_rustc_version_verbose(output.stdout, TargetFeatures::Unknown) {
Ok(platform) => Ok(platform),
Err(host_platform_error) => {
match build_target {
Ok(build_target) => {
warn!(
"for host platform, parsing `rustc -vV` failed; \
falling back to build target `{}`\n\
- host platform error:\n{}",
build_target.triple().as_str(),
DisplayErrorChain::new_with_initial_indent(
" ",
host_platform_error
),
);
Ok(build_target)
}
Err(build_target_error) => {
Err(HostPlatformDetectError::HostPlatformParseError {
host_platform_error: Box::new(host_platform_error),
build_target_error: Box::new(build_target_error),
})
}
}
}
}
} else {
match build_target {
Ok(build_target) => {
warn!(
"for host platform, `rustc -vV` failed with {}; \
falling back to build target `{}`\n\
- `rustc -vV` stdout:\n{}\n\
- `rustc -vV` stderr:\n{}",
output.status,
build_target.triple().as_str(),
DisplayIndented {
item: String::from_utf8_lossy(&output.stdout),
indent: " "
},
DisplayIndented {
item: String::from_utf8_lossy(&output.stderr),
indent: " "
},
);
Ok(build_target)
}
Err(build_target_error) => {
Err(HostPlatformDetectError::RustcVvFailed {
status: output.status,
stdout: output.stdout,
stderr: output.stderr,
build_target_error: Box::new(build_target_error),
})
}
}
}
}
Err(error) => {
match build_target {
Ok(build_target) => {
warn!(
"for host platform, failed to spawn `rustc -vV`; \
falling back to build target `{}`\n\
- host platform error:\n{}",
build_target.triple().as_str(),
DisplayErrorChain::new_with_initial_indent(" ", error),
);
Ok(build_target)
}
Err(build_target_error) => {
Err(HostPlatformDetectError::RustcVvSpawnError {
error,
build_target_error: Box::new(build_target_error),
})
}
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TargetPlatform {
pub triple: TargetTriple,
pub libdir: PlatformLibdir,
}
impl TargetPlatform {
pub fn new(triple: TargetTriple, libdir: PlatformLibdir) -> Self {
Self { triple, libdir }
}
pub fn to_summary(&self) -> TargetPlatformSummary {
TargetPlatformSummary {
platform: self.triple.platform.to_summary(),
libdir: self.libdir.to_summary(),
}
}
pub fn from_summary(summary: TargetPlatformSummary) -> Result<Self, RustBuildMetaParseError> {
Ok(Self {
triple: TargetTriple::deserialize(Some(summary.platform))?
.expect("the input is not None, so the output must not be None"),
libdir: PlatformLibdir::from_summary(summary.libdir),
})
}
fn map_libdir(&self, mapper: &PlatformLibdirMapper) -> Self {
Self {
triple: self.triple.clone(),
libdir: mapper.map(&self.libdir),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PlatformLibdir {
Available(Utf8PathBuf),
Unavailable(PlatformLibdirUnavailable),
}
impl PlatformLibdir {
pub fn from_path(path: Utf8PathBuf) -> Self {
Self::Available(path)
}
pub fn from_rustc_stdout(rustc_output: Option<Vec<u8>>) -> Self {
fn inner(v: Option<Vec<u8>>) -> Result<Utf8PathBuf, PlatformLibdirUnavailable> {
let v = v.ok_or(PlatformLibdirUnavailable::RUSTC_FAILED)?;
let s = String::from_utf8(v).map_err(|e| {
debug!("failed to convert the output to a string: {e}");
PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR
})?;
let mut lines = s.lines();
let Some(out) = lines.next() else {
debug!("empty output");
return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
};
let trimmed = out.trim();
if trimmed.is_empty() {
debug!("empty output");
return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
}
for line in lines {
if !line.trim().is_empty() {
debug!("unexpected additional output: {line}");
return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
}
}
Ok(Utf8PathBuf::from(trimmed))
}
match inner(rustc_output) {
Ok(path) => Self::Available(path),
Err(error) => Self::Unavailable(error),
}
}
pub fn from_unavailable(error: PlatformLibdirUnavailable) -> Self {
Self::Unavailable(error)
}
pub fn as_path(&self) -> Option<&Utf8Path> {
match self {
Self::Available(path) => Some(path),
Self::Unavailable(_) => None,
}
}
pub fn to_summary(&self) -> PlatformLibdirSummary {
match self {
Self::Available(path) => PlatformLibdirSummary::Available { path: path.clone() },
Self::Unavailable(reason) => PlatformLibdirSummary::Unavailable {
reason: reason.clone(),
},
}
}
pub fn from_summary(summary: PlatformLibdirSummary) -> Self {
match summary {
PlatformLibdirSummary::Available { path: libdir } => Self::Available(libdir),
PlatformLibdirSummary::Unavailable { reason } => Self::Unavailable(reason),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cargo_config::{TargetDefinitionLocation, TargetTripleSource};
use indoc::indoc;
use test_case::test_case;
#[test]
fn test_build_platforms_custom_cfg_roundtrip() {
let cfg_text = indoc! {r#"
panic="unwind"
target_arch="x86_64"
target_endian="little"
target_env="gnu"
target_family="unix"
target_os="linux"
target_pointer_width="64"
target_vendor="unknown"
unix
"#};
let target_platform =
Platform::new_custom_cfg("x86_64-custom-linux-gnu", cfg_text, TargetFeatures::Unknown)
.expect("cfg text is valid");
let target_libdir = PlatformLibdir::Available(Utf8PathBuf::from("/target/lib"));
let build_platforms = BuildPlatforms {
host: HostPlatform {
platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
libdir: PlatformLibdir::Available(Utf8PathBuf::from("/host/lib")),
},
target: Some(TargetPlatform::new(
TargetTriple {
platform: target_platform.clone(),
source: TargetTripleSource::CliOption,
location: TargetDefinitionLocation::RustcCfgCustom,
},
target_libdir.clone(),
)),
};
let json = serde_json::to_string(&build_platforms.to_summary())
.expect("serialized build platforms");
let summary: BuildPlatformsSummary =
serde_json::from_str(&json).expect("deserialized build platforms");
let roundtripped =
BuildPlatforms::from_summary(summary).expect("build platforms round-trip");
let expected = BuildPlatforms {
target: Some(TargetPlatform::new(
TargetTriple {
platform: target_platform,
source: TargetTripleSource::Metadata,
location: TargetDefinitionLocation::RustcCfgCustom,
},
target_libdir,
)),
..build_platforms
};
assert_eq!(roundtripped, expected, "build platforms round-trip");
assert_eq!(
roundtripped
.to_cargo_target_arg()
.expect("cargo target arg")
.to_string(),
"x86_64-custom-linux-gnu",
"cargo target arg is the triple"
);
}
#[test]
fn test_from_rustc_output_invalid() {
assert_eq!(
PlatformLibdir::from_rustc_stdout(None),
PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_FAILED),
);
assert_eq!(
PlatformLibdir::from_rustc_stdout(Some(Vec::new())),
PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
);
assert_eq!(
PlatformLibdir::from_rustc_stdout(Some(b"\n".to_vec())),
PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
);
assert_eq!(
PlatformLibdir::from_rustc_stdout(Some(b"/fake/libdir/1\n/fake/libdir/2\n".to_vec())),
PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
);
}
#[test_case(b"/fake/libdir/22548", "/fake/libdir/22548"; "single line")]
#[test_case(
b"\t /fake/libdir\t \n\r",
"/fake/libdir";
"with leading or trailing whitespace"
)]
#[test_case(
b"/fake/libdir/1\n\n",
"/fake/libdir/1";
"trailing newlines"
)]
fn test_read_from_rustc_output_valid(input: &[u8], actual: &str) {
assert_eq!(
PlatformLibdir::from_rustc_stdout(Some(input.to_vec())),
PlatformLibdir::Available(actual.into()),
);
}
}