1use crate::{
7 RustcCli,
8 cargo_config::{CargoTargetArg, TargetTriple},
9 errors::{
10 DisplayErrorChain, HostPlatformDetectError, RustBuildMetaParseError, TargetTripleError,
11 },
12 indenter::DisplayIndented,
13 reuse_build::{LibdirMapper, PlatformLibdirMapper},
14};
15use camino::{Utf8Path, Utf8PathBuf};
16use nextest_metadata::{
17 BuildPlatformsSummary, HostPlatformSummary, PlatformLibdirSummary, PlatformLibdirUnavailable,
18 TargetPlatformSummary,
19};
20pub use target_spec::Platform;
21use target_spec::{
22 TargetFeatures, errors::RustcVersionVerboseParseError, summaries::PlatformSummary,
23};
24use tracing::{debug, warn};
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct BuildPlatforms {
29 pub host: HostPlatform,
31
32 pub target: Option<TargetPlatform>,
36}
37
38impl BuildPlatforms {
39 pub fn new_with_no_target() -> Result<Self, HostPlatformDetectError> {
43 Ok(Self {
44 host: HostPlatform {
45 platform: Platform::build_target().map_err(|build_target_error| {
48 HostPlatformDetectError::BuildTargetError {
49 build_target_error: Box::new(build_target_error),
50 }
51 })?,
52 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::new_const("test")),
53 },
54 target: None,
55 })
56 }
57
58 pub fn map_libdir(&self, mapper: &LibdirMapper) -> Self {
60 Self {
61 host: self.host.map_libdir(&mapper.host),
62 target: self
63 .target
64 .as_ref()
65 .map(|target| target.map_libdir(&mapper.target)),
66 }
67 }
68
69 pub fn to_cargo_target_arg(&self) -> Result<CargoTargetArg, TargetTripleError> {
71 match &self.target {
72 Some(target) => target.triple.to_cargo_target_arg(),
73 None => {
74 Ok(CargoTargetArg::Builtin(
76 self.host.platform.triple_str().to_owned(),
77 ))
78 }
79 }
80 }
81
82 pub fn to_summary(&self) -> BuildPlatformsSummary {
84 BuildPlatformsSummary {
85 host: self.host.to_summary(),
86 targets: self
87 .target
88 .as_ref()
89 .map(|target| vec![target.to_summary()])
90 .unwrap_or_default(),
91 }
92 }
93
94 pub fn to_target_or_host_summary(&self) -> PlatformSummary {
98 if let Some(target) = &self.target {
99 target.triple.platform.to_summary()
100 } else {
101 self.host.platform.to_summary()
102 }
103 }
104
105 pub fn to_summary_str(&self) -> Option<String> {
109 self.target
110 .as_ref()
111 .map(|triple| triple.triple.platform.triple_str().to_owned())
112 }
113
114 pub fn from_summary(summary: BuildPlatformsSummary) -> Result<Self, RustBuildMetaParseError> {
116 Ok(BuildPlatforms {
117 host: HostPlatform::from_summary(summary.host)?,
118 target: {
119 if summary.targets.len() > 1 {
120 return Err(RustBuildMetaParseError::Unsupported {
121 message: "multiple build targets is not supported".to_owned(),
122 });
123 }
124 summary
125 .targets
126 .first()
127 .map(|target| TargetPlatform::from_summary(target.clone()))
128 .transpose()?
129 },
130 })
131 }
132
133 pub fn from_target_summary(summary: PlatformSummary) -> Result<Self, RustBuildMetaParseError> {
137 let host = HostPlatform {
144 platform: Platform::build_target()
147 .map_err(RustBuildMetaParseError::DetectBuildTargetError)?,
148 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
149 };
150
151 let target = TargetTriple::deserialize(Some(summary))?.map(|triple| {
152 TargetPlatform::new(
153 triple,
154 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
155 )
156 });
157
158 Ok(Self { host, target })
159 }
160
161 pub fn from_summary_str(summary: Option<String>) -> Result<Self, RustBuildMetaParseError> {
165 let host = HostPlatform {
173 platform: Platform::build_target()
176 .map_err(RustBuildMetaParseError::DetectBuildTargetError)?,
177 libdir: PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
178 };
179
180 let target = TargetTriple::deserialize_str(summary)?.map(|triple| {
181 TargetPlatform::new(
182 triple,
183 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::OLD_SUMMARY),
184 )
185 });
186
187 Ok(Self { host, target })
188 }
189}
190
191#[derive(Clone, Debug, Eq, PartialEq)]
193pub struct HostPlatform {
194 pub platform: Platform,
196
197 pub libdir: PlatformLibdir,
199}
200
201impl HostPlatform {
202 pub fn detect(libdir: PlatformLibdir) -> Result<Self, HostPlatformDetectError> {
207 let platform = detect_host_platform()?;
208 Ok(Self { platform, libdir })
209 }
210
211 pub fn to_summary(&self) -> HostPlatformSummary {
213 HostPlatformSummary {
214 platform: self.platform.to_summary(),
215 libdir: self.libdir.to_summary(),
216 }
217 }
218
219 pub fn from_summary(summary: HostPlatformSummary) -> Result<Self, RustBuildMetaParseError> {
221 let platform = summary
222 .platform
223 .to_platform()
224 .map_err(RustBuildMetaParseError::PlatformDeserializeError)?;
225 Ok(Self {
226 platform,
227 libdir: PlatformLibdir::from_summary(summary.libdir),
228 })
229 }
230
231 fn map_libdir(&self, mapper: &PlatformLibdirMapper) -> Self {
232 Self {
233 platform: self.platform.clone(),
234 libdir: mapper.map(&self.libdir),
235 }
236 }
237}
238
239fn detect_host_platform() -> Result<Platform, HostPlatformDetectError> {
245 const FORCE_BUILD_TARGET_VAR: &str = "__NEXTEST_FORCE_BUILD_TARGET";
248
249 let build_target = match std::env::var(FORCE_BUILD_TARGET_VAR).as_deref() {
250 Ok("error") => Err(target_spec::Error::RustcVersionVerboseParse(
251 RustcVersionVerboseParseError::MissingHostLine {
252 output: format!(
253 "({FORCE_BUILD_TARGET_VAR} set to \"error\", forcibly failing build target detection)\n"
254 ),
255 },
256 )),
257 Ok(triple) => Platform::new(triple.to_owned(), TargetFeatures::Unknown),
258 Err(_) => Platform::build_target(),
259 };
260
261 let rustc_vv = RustcCli::version_verbose()
262 .to_expression()
263 .stdout_capture()
264 .stderr_capture()
265 .unchecked();
266 match rustc_vv.run() {
267 Ok(output) => {
268 if output.status.success() {
269 match Platform::from_rustc_version_verbose(output.stdout, TargetFeatures::Unknown) {
273 Ok(platform) => Ok(platform),
274 Err(host_platform_error) => {
275 match build_target {
276 Ok(build_target) => {
277 warn!(
278 "for host platform, parsing `rustc -vV` failed; \
279 falling back to build target `{}`\n\
280 - host platform error:\n{}",
281 build_target.triple().as_str(),
282 DisplayErrorChain::new_with_initial_indent(
283 " ",
284 host_platform_error
285 ),
286 );
287 Ok(build_target)
288 }
289 Err(build_target_error) => {
290 Err(HostPlatformDetectError::HostPlatformParseError {
292 host_platform_error: Box::new(host_platform_error),
293 build_target_error: Box::new(build_target_error),
294 })
295 }
296 }
297 }
298 }
299 } else {
300 match build_target {
301 Ok(build_target) => {
302 warn!(
303 "for host platform, `rustc -vV` failed with {}; \
304 falling back to build target `{}`\n\
305 - `rustc -vV` stdout:\n{}\n\
306 - `rustc -vV` stderr:\n{}",
307 output.status,
308 build_target.triple().as_str(),
309 DisplayIndented {
310 item: String::from_utf8_lossy(&output.stdout),
311 indent: " "
312 },
313 DisplayIndented {
314 item: String::from_utf8_lossy(&output.stderr),
315 indent: " "
316 },
317 );
318 Ok(build_target)
319 }
320 Err(build_target_error) => {
321 Err(HostPlatformDetectError::RustcVvFailed {
324 status: output.status,
325 stdout: output.stdout,
326 stderr: output.stderr,
327 build_target_error: Box::new(build_target_error),
328 })
329 }
330 }
331 }
332 }
333 Err(error) => {
334 match build_target {
335 Ok(build_target) => {
336 warn!(
337 "for host platform, failed to spawn `rustc -vV`; \
338 falling back to build target `{}`\n\
339 - host platform error:\n{}",
340 build_target.triple().as_str(),
341 DisplayErrorChain::new_with_initial_indent(" ", error),
342 );
343 Ok(build_target)
344 }
345 Err(build_target_error) => {
346 Err(HostPlatformDetectError::RustcVvSpawnError {
349 error,
350 build_target_error: Box::new(build_target_error),
351 })
352 }
353 }
354 }
355 }
356}
357
358#[derive(Clone, Debug, Eq, PartialEq)]
360pub struct TargetPlatform {
361 pub triple: TargetTriple,
363
364 pub libdir: PlatformLibdir,
366}
367
368impl TargetPlatform {
369 pub fn new(triple: TargetTriple, libdir: PlatformLibdir) -> Self {
371 Self { triple, libdir }
372 }
373
374 pub fn to_summary(&self) -> TargetPlatformSummary {
376 TargetPlatformSummary {
377 platform: self.triple.platform.to_summary(),
378 libdir: self.libdir.to_summary(),
379 }
380 }
381
382 pub fn from_summary(summary: TargetPlatformSummary) -> Result<Self, RustBuildMetaParseError> {
384 Ok(Self {
385 triple: TargetTriple::deserialize(Some(summary.platform))?
386 .expect("the input is not None, so the output must not be None"),
387 libdir: PlatformLibdir::from_summary(summary.libdir),
388 })
389 }
390
391 fn map_libdir(&self, mapper: &PlatformLibdirMapper) -> Self {
392 Self {
393 triple: self.triple.clone(),
394 libdir: mapper.map(&self.libdir),
395 }
396 }
397}
398
399#[derive(Clone, Debug, Eq, PartialEq)]
401pub enum PlatformLibdir {
402 Available(Utf8PathBuf),
404
405 Unavailable(PlatformLibdirUnavailable),
407}
408
409impl PlatformLibdir {
410 pub fn from_path(path: Utf8PathBuf) -> Self {
412 Self::Available(path)
413 }
414
415 pub fn from_rustc_stdout(rustc_output: Option<Vec<u8>>) -> Self {
419 fn inner(v: Option<Vec<u8>>) -> Result<Utf8PathBuf, PlatformLibdirUnavailable> {
420 let v = v.ok_or(PlatformLibdirUnavailable::RUSTC_FAILED)?;
421
422 let s = String::from_utf8(v).map_err(|e| {
423 debug!("failed to convert the output to a string: {e}");
424 PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR
425 })?;
426
427 let mut lines = s.lines();
428 let Some(out) = lines.next() else {
429 debug!("empty output");
430 return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
431 };
432
433 let trimmed = out.trim();
434 if trimmed.is_empty() {
435 debug!("empty output");
436 return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
437 }
438
439 for line in lines {
441 if !line.trim().is_empty() {
442 debug!("unexpected additional output: {line}");
443 return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
444 }
445 }
446
447 Ok(Utf8PathBuf::from(trimmed))
448 }
449
450 match inner(rustc_output) {
451 Ok(path) => Self::Available(path),
452 Err(error) => Self::Unavailable(error),
453 }
454 }
455
456 pub fn from_unavailable(error: PlatformLibdirUnavailable) -> Self {
458 Self::Unavailable(error)
459 }
460
461 pub fn as_path(&self) -> Option<&Utf8Path> {
463 match self {
464 Self::Available(path) => Some(path),
465 Self::Unavailable(_) => None,
466 }
467 }
468
469 pub fn to_summary(&self) -> PlatformLibdirSummary {
471 match self {
472 Self::Available(path) => PlatformLibdirSummary::Available { path: path.clone() },
473 Self::Unavailable(reason) => PlatformLibdirSummary::Unavailable {
474 reason: reason.clone(),
475 },
476 }
477 }
478
479 pub fn from_summary(summary: PlatformLibdirSummary) -> Self {
481 match summary {
482 PlatformLibdirSummary::Available { path: libdir } => Self::Available(libdir),
483 PlatformLibdirSummary::Unavailable { reason } => Self::Unavailable(reason),
484 }
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use crate::cargo_config::{TargetDefinitionLocation, TargetTripleSource};
492 use indoc::indoc;
493 use test_case::test_case;
494
495 #[test]
496 fn test_build_platforms_custom_cfg_roundtrip() {
497 let cfg_text = indoc! {r#"
498 panic="unwind"
499 target_arch="x86_64"
500 target_endian="little"
501 target_env="gnu"
502 target_family="unix"
503 target_os="linux"
504 target_pointer_width="64"
505 target_vendor="unknown"
506 unix
507 "#};
508 let target_platform =
509 Platform::new_custom_cfg("x86_64-custom-linux-gnu", cfg_text, TargetFeatures::Unknown)
510 .expect("cfg text is valid");
511 let target_libdir = PlatformLibdir::Available(Utf8PathBuf::from("/target/lib"));
512 let build_platforms = BuildPlatforms {
513 host: HostPlatform {
514 platform: TargetTriple::x86_64_unknown_linux_gnu().platform,
515 libdir: PlatformLibdir::Available(Utf8PathBuf::from("/host/lib")),
516 },
517 target: Some(TargetPlatform::new(
518 TargetTriple {
519 platform: target_platform.clone(),
520 source: TargetTripleSource::CliOption,
521 location: TargetDefinitionLocation::RustcCfgCustom,
522 },
523 target_libdir.clone(),
524 )),
525 };
526
527 let json = serde_json::to_string(&build_platforms.to_summary())
528 .expect("serialized build platforms");
529 let summary: BuildPlatformsSummary =
530 serde_json::from_str(&json).expect("deserialized build platforms");
531 let roundtripped =
532 BuildPlatforms::from_summary(summary).expect("build platforms round-trip");
533
534 let expected = BuildPlatforms {
536 target: Some(TargetPlatform::new(
537 TargetTriple {
538 platform: target_platform,
539 source: TargetTripleSource::Metadata,
540 location: TargetDefinitionLocation::RustcCfgCustom,
541 },
542 target_libdir,
543 )),
544 ..build_platforms
545 };
546 assert_eq!(roundtripped, expected, "build platforms round-trip");
547 assert_eq!(
548 roundtripped
549 .to_cargo_target_arg()
550 .expect("cargo target arg")
551 .to_string(),
552 "x86_64-custom-linux-gnu",
553 "cargo target arg is the triple"
554 );
555 }
556
557 #[test]
558 fn test_from_rustc_output_invalid() {
559 assert_eq!(
561 PlatformLibdir::from_rustc_stdout(None),
562 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_FAILED),
563 );
564
565 assert_eq!(
567 PlatformLibdir::from_rustc_stdout(Some(Vec::new())),
568 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
569 );
570
571 assert_eq!(
573 PlatformLibdir::from_rustc_stdout(Some(b"\n".to_vec())),
574 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
575 );
576
577 assert_eq!(
579 PlatformLibdir::from_rustc_stdout(Some(b"/fake/libdir/1\n/fake/libdir/2\n".to_vec())),
580 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
581 );
582 }
583
584 #[test_case(b"/fake/libdir/22548", "/fake/libdir/22548"; "single line")]
585 #[test_case(
586 b"\t /fake/libdir\t \n\r",
587 "/fake/libdir";
588 "with leading or trailing whitespace"
589 )]
590 #[test_case(
591 b"/fake/libdir/1\n\n",
592 "/fake/libdir/1";
593 "trailing newlines"
594 )]
595 fn test_read_from_rustc_output_valid(input: &[u8], actual: &str) {
596 assert_eq!(
597 PlatformLibdir::from_rustc_stdout(Some(input.to_vec())),
598 PlatformLibdir::Available(actual.into()),
599 );
600 }
601}