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 .map_err(RustBuildMetaParseError::PlatformDeserializeError)?
387 .expect("the input is not None, so the output must not be None"),
388 libdir: PlatformLibdir::from_summary(summary.libdir),
389 })
390 }
391
392 fn map_libdir(&self, mapper: &PlatformLibdirMapper) -> Self {
393 Self {
394 triple: self.triple.clone(),
395 libdir: mapper.map(&self.libdir),
396 }
397 }
398}
399
400#[derive(Clone, Debug, Eq, PartialEq)]
402pub enum PlatformLibdir {
403 Available(Utf8PathBuf),
405
406 Unavailable(PlatformLibdirUnavailable),
408}
409
410impl PlatformLibdir {
411 pub fn from_path(path: Utf8PathBuf) -> Self {
413 Self::Available(path)
414 }
415
416 pub fn from_rustc_stdout(rustc_output: Option<Vec<u8>>) -> Self {
420 fn inner(v: Option<Vec<u8>>) -> Result<Utf8PathBuf, PlatformLibdirUnavailable> {
421 let v = v.ok_or(PlatformLibdirUnavailable::RUSTC_FAILED)?;
422
423 let s = String::from_utf8(v).map_err(|e| {
424 debug!("failed to convert the output to a string: {e}");
425 PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR
426 })?;
427
428 let mut lines = s.lines();
429 let Some(out) = lines.next() else {
430 debug!("empty output");
431 return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
432 };
433
434 let trimmed = out.trim();
435 if trimmed.is_empty() {
436 debug!("empty output");
437 return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
438 }
439
440 for line in lines {
442 if !line.trim().is_empty() {
443 debug!("unexpected additional output: {line}");
444 return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
445 }
446 }
447
448 Ok(Utf8PathBuf::from(trimmed))
449 }
450
451 match inner(rustc_output) {
452 Ok(path) => Self::Available(path),
453 Err(error) => Self::Unavailable(error),
454 }
455 }
456
457 pub fn from_unavailable(error: PlatformLibdirUnavailable) -> Self {
459 Self::Unavailable(error)
460 }
461
462 pub fn as_path(&self) -> Option<&Utf8Path> {
464 match self {
465 Self::Available(path) => Some(path),
466 Self::Unavailable(_) => None,
467 }
468 }
469
470 pub fn to_summary(&self) -> PlatformLibdirSummary {
472 match self {
473 Self::Available(path) => PlatformLibdirSummary::Available { path: path.clone() },
474 Self::Unavailable(reason) => PlatformLibdirSummary::Unavailable {
475 reason: reason.clone(),
476 },
477 }
478 }
479
480 pub fn from_summary(summary: PlatformLibdirSummary) -> Self {
482 match summary {
483 PlatformLibdirSummary::Available { path: libdir } => Self::Available(libdir),
484 PlatformLibdirSummary::Unavailable { reason } => Self::Unavailable(reason),
485 }
486 }
487}
488
489#[cfg(test)]
494pub(crate) fn detect_host_platform_for_tests() -> Platform {
495 use crate::RustcCli;
496
497 HostPlatform::detect(PlatformLibdir::from_rustc_stdout(
498 RustcCli::print_host_libdir().read(),
499 ))
500 .expect("host platform detection should succeed in tests")
501 .platform
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507 use test_case::test_case;
508
509 #[test]
510 fn test_from_rustc_output_invalid() {
511 assert_eq!(
513 PlatformLibdir::from_rustc_stdout(None),
514 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_FAILED),
515 );
516
517 assert_eq!(
519 PlatformLibdir::from_rustc_stdout(Some(Vec::new())),
520 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
521 );
522
523 assert_eq!(
525 PlatformLibdir::from_rustc_stdout(Some(b"\n".to_vec())),
526 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
527 );
528
529 assert_eq!(
531 PlatformLibdir::from_rustc_stdout(Some(b"/fake/libdir/1\n/fake/libdir/2\n".to_vec())),
532 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
533 );
534 }
535
536 #[test_case(b"/fake/libdir/22548", "/fake/libdir/22548"; "single line")]
537 #[test_case(
538 b"\t /fake/libdir\t \n\r",
539 "/fake/libdir";
540 "with leading or trailing whitespace"
541 )]
542 #[test_case(
543 b"/fake/libdir/1\n\n",
544 "/fake/libdir/1";
545 "trailing newlines"
546 )]
547 fn test_read_from_rustc_output_valid(input: &[u8], actual: &str) {
548 assert_eq!(
549 PlatformLibdir::from_rustc_stdout(Some(input.to_vec())),
550 PlatformLibdir::Available(actual.into()),
551 );
552 }
553}