1use crate::{
7 RustcCli,
8 cargo_config::{CargoTargetArg, TargetTriple},
9 errors::{
10 DisplayErrorChain, HostPlatformDetectError, RustBuildMetaParseError, TargetTripleError,
11 },
12 reuse_build::{LibdirMapper, PlatformLibdirMapper},
13};
14use camino::{Utf8Path, Utf8PathBuf};
15use indent_write::indentable::Indented;
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 enum ForceBuildTarget {
250 Triple(String),
251 Error,
252 }
253
254 let force_build_target = match std::env::var(FORCE_BUILD_TARGET_VAR).as_deref() {
255 Ok("error") => Some(ForceBuildTarget::Error),
256 Ok(triple) => Some(ForceBuildTarget::Triple(triple.to_owned())),
257 Err(_) => None,
258 };
259
260 let build_target = match force_build_target {
261 Some(ForceBuildTarget::Triple(triple)) => Platform::new(triple, TargetFeatures::Unknown),
262 Some(ForceBuildTarget::Error) => Err(target_spec::Error::RustcVersionVerboseParse(
263 RustcVersionVerboseParseError::MissingHostLine {
264 output: format!(
265 "({} set to \"error\", forcibly failing build target detection)\n",
266 FORCE_BUILD_TARGET_VAR
267 ),
268 },
269 )),
270 None => Platform::build_target(),
271 };
272
273 let rustc_vv = RustcCli::version_verbose()
274 .to_expression()
275 .stdout_capture()
276 .stderr_capture()
277 .unchecked();
278 match rustc_vv.run() {
279 Ok(output) => {
280 if output.status.success() {
281 match Platform::from_rustc_version_verbose(output.stdout, TargetFeatures::Unknown) {
285 Ok(platform) => Ok(platform),
286 Err(host_platform_error) => {
287 match build_target {
288 Ok(build_target) => {
289 warn!(
290 "for host platform, parsing `rustc -vV` failed; \
291 falling back to build target `{}`\n\
292 - host platform error:\n{}",
293 build_target.triple().as_str(),
294 DisplayErrorChain::new_with_initial_indent(
295 " ",
296 host_platform_error
297 ),
298 );
299 Ok(build_target)
300 }
301 Err(build_target_error) => {
302 Err(HostPlatformDetectError::HostPlatformParseError {
304 host_platform_error: Box::new(host_platform_error),
305 build_target_error: Box::new(build_target_error),
306 })
307 }
308 }
309 }
310 }
311 } else {
312 match build_target {
313 Ok(build_target) => {
314 warn!(
315 "for host platform, `rustc -vV` failed with {}; \
316 falling back to build target `{}`\n\
317 - `rustc -vV` stdout:\n{}\n\
318 - `rustc -vV` stderr:\n{}",
319 output.status,
320 build_target.triple().as_str(),
321 Indented {
322 item: String::from_utf8_lossy(&output.stdout),
323 indent: " "
324 },
325 Indented {
326 item: String::from_utf8_lossy(&output.stderr),
327 indent: " "
328 },
329 );
330 Ok(build_target)
331 }
332 Err(build_target_error) => {
333 Err(HostPlatformDetectError::RustcVvFailed {
336 status: output.status,
337 stdout: output.stdout,
338 stderr: output.stderr,
339 build_target_error: Box::new(build_target_error),
340 })
341 }
342 }
343 }
344 }
345 Err(error) => {
346 match build_target {
347 Ok(build_target) => {
348 warn!(
349 "for host platform, failed to spawn `rustc -vV`; \
350 falling back to build target `{}`\n\
351 - host platform error:\n{}",
352 build_target.triple().as_str(),
353 DisplayErrorChain::new_with_initial_indent(" ", error),
354 );
355 Ok(build_target)
356 }
357 Err(build_target_error) => {
358 Err(HostPlatformDetectError::RustcVvSpawnError {
361 error,
362 build_target_error: Box::new(build_target_error),
363 })
364 }
365 }
366 }
367 }
368}
369
370#[derive(Clone, Debug, Eq, PartialEq)]
372pub struct TargetPlatform {
373 pub triple: TargetTriple,
375
376 pub libdir: PlatformLibdir,
378}
379
380impl TargetPlatform {
381 pub fn new(triple: TargetTriple, libdir: PlatformLibdir) -> Self {
383 Self { triple, libdir }
384 }
385
386 pub fn to_summary(&self) -> TargetPlatformSummary {
388 TargetPlatformSummary {
389 platform: self.triple.platform.to_summary(),
390 libdir: self.libdir.to_summary(),
391 }
392 }
393
394 pub fn from_summary(summary: TargetPlatformSummary) -> Result<Self, RustBuildMetaParseError> {
396 Ok(Self {
397 triple: TargetTriple::deserialize(Some(summary.platform))
398 .map_err(RustBuildMetaParseError::PlatformDeserializeError)?
399 .expect("the input is not None, so the output must not be None"),
400 libdir: PlatformLibdir::from_summary(summary.libdir),
401 })
402 }
403
404 fn map_libdir(&self, mapper: &PlatformLibdirMapper) -> Self {
405 Self {
406 triple: self.triple.clone(),
407 libdir: mapper.map(&self.libdir),
408 }
409 }
410}
411
412#[derive(Clone, Debug, Eq, PartialEq)]
414pub enum PlatformLibdir {
415 Available(Utf8PathBuf),
417
418 Unavailable(PlatformLibdirUnavailable),
420}
421
422impl PlatformLibdir {
423 pub fn from_path(path: Utf8PathBuf) -> Self {
425 Self::Available(path)
426 }
427
428 pub fn from_rustc_stdout(rustc_output: Option<Vec<u8>>) -> Self {
432 fn inner(v: Option<Vec<u8>>) -> Result<Utf8PathBuf, PlatformLibdirUnavailable> {
433 let v = v.ok_or(PlatformLibdirUnavailable::RUSTC_FAILED)?;
434
435 let s = String::from_utf8(v).map_err(|e| {
436 debug!("failed to convert the output to a string: {e}");
437 PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR
438 })?;
439
440 let mut lines = s.lines();
441 let Some(out) = lines.next() else {
442 debug!("empty output");
443 return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
444 };
445
446 let trimmed = out.trim();
447 if trimmed.is_empty() {
448 debug!("empty output");
449 return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
450 }
451
452 for line in lines {
454 if !line.trim().is_empty() {
455 debug!("unexpected additional output: {line}");
456 return Err(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR);
457 }
458 }
459
460 Ok(Utf8PathBuf::from(trimmed))
461 }
462
463 match inner(rustc_output) {
464 Ok(path) => Self::Available(path),
465 Err(error) => Self::Unavailable(error),
466 }
467 }
468
469 pub fn from_unavailable(error: PlatformLibdirUnavailable) -> Self {
471 Self::Unavailable(error)
472 }
473
474 pub fn as_path(&self) -> Option<&Utf8Path> {
476 match self {
477 Self::Available(path) => Some(path),
478 Self::Unavailable(_) => None,
479 }
480 }
481
482 pub fn to_summary(&self) -> PlatformLibdirSummary {
484 match self {
485 Self::Available(path) => PlatformLibdirSummary::Available { path: path.clone() },
486 Self::Unavailable(reason) => PlatformLibdirSummary::Unavailable {
487 reason: reason.clone(),
488 },
489 }
490 }
491
492 pub fn from_summary(summary: PlatformLibdirSummary) -> Self {
494 match summary {
495 PlatformLibdirSummary::Available { path: libdir } => Self::Available(libdir),
496 PlatformLibdirSummary::Unavailable { reason } => Self::Unavailable(reason),
497 }
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504 use test_case::test_case;
505
506 #[test]
507 fn test_from_rustc_output_invalid() {
508 assert_eq!(
510 PlatformLibdir::from_rustc_stdout(None),
511 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_FAILED),
512 );
513
514 assert_eq!(
516 PlatformLibdir::from_rustc_stdout(Some(Vec::new())),
517 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
518 );
519
520 assert_eq!(
522 PlatformLibdir::from_rustc_stdout(Some(b"\n".to_vec())),
523 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
524 );
525
526 assert_eq!(
528 PlatformLibdir::from_rustc_stdout(Some(b"/fake/libdir/1\n/fake/libdir/2\n".to_vec())),
529 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::RUSTC_OUTPUT_ERROR),
530 );
531 }
532
533 #[test_case(b"/fake/libdir/22548", "/fake/libdir/22548"; "single line")]
534 #[test_case(
535 b"\t /fake/libdir\t \n\r",
536 "/fake/libdir";
537 "with leading or trailing whitespace"
538 )]
539 #[test_case(
540 b"/fake/libdir/1\n\n",
541 "/fake/libdir/1";
542 "trailing newlines"
543 )]
544 fn test_read_from_rustc_output_valid(input: &[u8], actual: &str) {
545 assert_eq!(
546 PlatformLibdir::from_rustc_stdout(Some(input.to_vec())),
547 PlatformLibdir::Available(actual.into()),
548 );
549 }
550}