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