@@ -443,21 +443,22 @@ fn named_compiler_status_value( |
| 443 | 443 | tools: &ToolchainConfig, |
| 444 | 444 | capture_root: Option<&PathBuf>, |
| 445 | 445 | ) -> String { |
| 446 | | - match named { |
| 447 | | - NamedCompiler::Armfortas => match &tools.armfortas { |
| 448 | | - ArmfortasCliAdapter::Linked => capture_root |
| 449 | | - .map(|root| format!("linked via Cargo to {}", display_path(root))) |
| 450 | | - .unwrap_or_else(|| { |
| 451 | | - "linked adapter requested but unavailable in this build".to_string() |
| 452 | | - }), |
| 453 | | - ArmfortasCliAdapter::External(binary) => tool_probe_status(binary, false), |
| 454 | | - }, |
| 455 | | - _ => tool_probe_status( |
| 456 | | - &tools |
| 457 | | - .named_compiler_binary(named) |
| 458 | | - .unwrap_or_else(|| named.as_str().to_string()), |
| 459 | | - false, |
| 446 | + let probe = named_compiler_probe(named, tools, capture_root); |
| 447 | + match probe.resolved_path { |
| 448 | + Some(path) => format!( |
| 449 | + "configured={} resolved={}", |
| 450 | + probe.configured, |
| 451 | + path.display() |
| 460 | 452 | ), |
| 453 | + None => { |
| 454 | + if probe.status == "linked" { |
| 455 | + probe |
| 456 | + .detail |
| 457 | + .unwrap_or_else(|| "linked via Cargo".to_string()) |
| 458 | + } else { |
| 459 | + format!("configured={} resolved=missing", probe.configured) |
| 460 | + } |
| 461 | + } |
| 461 | 462 | } |
| 462 | 463 | } |
| 463 | 464 | |
@@ -469,6 +470,7 @@ fn append_named_compiler_fields( |
| 469 | 470 | ) { |
| 470 | 471 | let prefix = format!("named_compiler.{}", named.as_str()); |
| 471 | 472 | let capabilities = compiler_capabilities(&CompilerSpec::Named(named), tools); |
| 473 | + let probe = named_compiler_probe(named, tools, capture_root); |
| 472 | 474 | if named == NamedCompiler::Armfortas { |
| 473 | 475 | let armfortas = tools.armfortas_adapters(); |
| 474 | 476 | fields.push(( |
@@ -505,6 +507,23 @@ fn append_named_compiler_fields( |
| 505 | 507 | format!("{}.unavailable_artifacts", prefix), |
| 506 | 508 | capability_unavailable_summary(&capabilities), |
| 507 | 509 | )); |
| 510 | + fields.push((format!("{}.probe_status", prefix), probe.status.clone())); |
| 511 | + fields.push(( |
| 512 | + format!("{}.probe_resolved_path", prefix), |
| 513 | + probe |
| 514 | + .resolved_path |
| 515 | + .as_ref() |
| 516 | + .map(|path| display_path(path)) |
| 517 | + .unwrap_or_else(|| "none".to_string()), |
| 518 | + )); |
| 519 | + fields.push(( |
| 520 | + format!("{}.probe_banner", prefix), |
| 521 | + probe.banner.clone().unwrap_or_else(|| "none".to_string()), |
| 522 | + )); |
| 523 | + fields.push(( |
| 524 | + format!("{}.probe_detail", prefix), |
| 525 | + probe.detail.clone().unwrap_or_else(|| "none".to_string()), |
| 526 | + )); |
| 508 | 527 | } |
| 509 | 528 | |
| 510 | 529 | fn compiler_capability_backend(spec: &CompilerSpec, tools: &ToolchainConfig) -> (String, String) { |
@@ -3446,6 +3465,26 @@ fn render_doctor_capabilities_json(capabilities: &CompilerCapabilities) -> Strin |
| 3446 | 3465 | ) |
| 3447 | 3466 | } |
| 3448 | 3467 | |
| 3468 | +fn render_tool_probe_json(probe: &ToolProbe) -> String { |
| 3469 | + format!( |
| 3470 | + "{{\"configured\":\"{}\",\"status\":\"{}\",\"resolved_path\":{},\"banner\":{},\"detail\":{}}}", |
| 3471 | + json_escape(&probe.configured), |
| 3472 | + json_escape(&probe.status), |
| 3473 | + match probe.resolved_path.as_ref() { |
| 3474 | + Some(path) => format!("\"{}\"", json_escape(&display_path(path))), |
| 3475 | + None => "null".to_string(), |
| 3476 | + }, |
| 3477 | + match probe.banner.as_ref() { |
| 3478 | + Some(banner) => format!("\"{}\"", json_escape(banner)), |
| 3479 | + None => "null".to_string(), |
| 3480 | + }, |
| 3481 | + match probe.detail.as_ref() { |
| 3482 | + Some(detail) => format!("\"{}\"", json_escape(detail)), |
| 3483 | + None => "null".to_string(), |
| 3484 | + }, |
| 3485 | + ) |
| 3486 | +} |
| 3487 | + |
| 3449 | 3488 | fn json_string_vec_map(map: &BTreeMap<String, Vec<String>>) -> String { |
| 3450 | 3489 | let mut rendered = String::from("{"); |
| 3451 | 3490 | for (index, (key, values)) in map.iter().enumerate() { |
@@ -3467,6 +3506,7 @@ fn render_named_compiler_entry_json( |
| 3467 | 3506 | capture_root: Option<&PathBuf>, |
| 3468 | 3507 | ) -> String { |
| 3469 | 3508 | let capabilities = compiler_capabilities(&CompilerSpec::Named(named), tools); |
| 3509 | + let probe = named_compiler_probe(named, tools, capture_root); |
| 3470 | 3510 | let mut fields = vec![ |
| 3471 | 3511 | format!( |
| 3472 | 3512 | "\"accepted_names\": {}", |
@@ -3480,6 +3520,7 @@ fn render_named_compiler_entry_json( |
| 3480 | 3520 | "\"capabilities\": {}", |
| 3481 | 3521 | render_doctor_capabilities_json(&capabilities) |
| 3482 | 3522 | ), |
| 3523 | + format!("\"probe\": {}", render_tool_probe_json(&probe)), |
| 3483 | 3524 | ]; |
| 3484 | 3525 | if named == NamedCompiler::Armfortas { |
| 3485 | 3526 | let armfortas = tools.armfortas_adapters(); |
@@ -3780,8 +3821,17 @@ fn doctor_markdown_cell(value: &str) -> String { |
| 3780 | 3821 | value.replace('|', "\\|").replace('\n', "<br>") |
| 3781 | 3822 | } |
| 3782 | 3823 | |
| 3783 | | -fn tool_probe_status(configured: &str, already_resolved_path: bool) -> String { |
| 3784 | | - let resolved = if already_resolved_path { |
| 3824 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 3825 | +struct ToolProbe { |
| 3826 | + configured: String, |
| 3827 | + resolved_path: Option<PathBuf>, |
| 3828 | + status: String, |
| 3829 | + banner: Option<String>, |
| 3830 | + detail: Option<String>, |
| 3831 | +} |
| 3832 | + |
| 3833 | +fn tool_probe(configured: &str, already_resolved_path: bool) -> ToolProbe { |
| 3834 | + let resolved_path = if already_resolved_path { |
| 3785 | 3835 | let path = PathBuf::from(configured); |
| 3786 | 3836 | if path.exists() { |
| 3787 | 3837 | Some(path) |
@@ -3792,7 +3842,126 @@ fn tool_probe_status(configured: &str, already_resolved_path: bool) -> String { |
| 3792 | 3842 | resolve_tool_path(configured) |
| 3793 | 3843 | }; |
| 3794 | 3844 | |
| 3795 | | - match resolved { |
| 3845 | + match resolved_path { |
| 3846 | + Some(path) => match probe_tool_banner(&path) { |
| 3847 | + Ok((arg, banner)) => ToolProbe { |
| 3848 | + configured: configured.to_string(), |
| 3849 | + resolved_path: Some(path), |
| 3850 | + status: "invokable".into(), |
| 3851 | + banner: Some(banner), |
| 3852 | + detail: Some(format!("probe succeeded with {}", arg)), |
| 3853 | + }, |
| 3854 | + Err(detail) => ToolProbe { |
| 3855 | + configured: configured.to_string(), |
| 3856 | + resolved_path: Some(path), |
| 3857 | + status: "resolved".into(), |
| 3858 | + banner: None, |
| 3859 | + detail: Some(detail), |
| 3860 | + }, |
| 3861 | + }, |
| 3862 | + None => ToolProbe { |
| 3863 | + configured: configured.to_string(), |
| 3864 | + resolved_path: None, |
| 3865 | + status: "missing".into(), |
| 3866 | + banner: None, |
| 3867 | + detail: Some("binary not found on disk or PATH".into()), |
| 3868 | + }, |
| 3869 | + } |
| 3870 | +} |
| 3871 | + |
| 3872 | +fn linked_tool_probe(capture_root: Option<&PathBuf>) -> ToolProbe { |
| 3873 | + match capture_root { |
| 3874 | + Some(root) => ToolProbe { |
| 3875 | + configured: "linked".into(), |
| 3876 | + resolved_path: Some(root.clone()), |
| 3877 | + status: "linked".into(), |
| 3878 | + banner: None, |
| 3879 | + detail: Some(format!("linked via Cargo to {}", display_path(root))), |
| 3880 | + }, |
| 3881 | + None => ToolProbe { |
| 3882 | + configured: "linked".into(), |
| 3883 | + resolved_path: None, |
| 3884 | + status: "unavailable".into(), |
| 3885 | + banner: None, |
| 3886 | + detail: Some("linked adapter requested but unavailable in this build".into()), |
| 3887 | + }, |
| 3888 | + } |
| 3889 | +} |
| 3890 | + |
| 3891 | +fn named_compiler_probe( |
| 3892 | + named: NamedCompiler, |
| 3893 | + tools: &ToolchainConfig, |
| 3894 | + capture_root: Option<&PathBuf>, |
| 3895 | +) -> ToolProbe { |
| 3896 | + match named { |
| 3897 | + NamedCompiler::Armfortas => match &tools.armfortas { |
| 3898 | + ArmfortasCliAdapter::Linked => linked_tool_probe(capture_root), |
| 3899 | + ArmfortasCliAdapter::External(binary) => tool_probe(binary, false), |
| 3900 | + }, |
| 3901 | + _ => tool_probe( |
| 3902 | + &tools |
| 3903 | + .named_compiler_binary(named) |
| 3904 | + .unwrap_or_else(|| named.as_str().to_string()), |
| 3905 | + false, |
| 3906 | + ), |
| 3907 | + } |
| 3908 | +} |
| 3909 | + |
| 3910 | +fn compiler_spec_probe( |
| 3911 | + spec: &CompilerSpec, |
| 3912 | + tools: &ToolchainConfig, |
| 3913 | + capture_root: Option<&PathBuf>, |
| 3914 | +) -> ToolProbe { |
| 3915 | + match spec { |
| 3916 | + CompilerSpec::Named(named) => named_compiler_probe(*named, tools, capture_root), |
| 3917 | + CompilerSpec::Binary(path) => tool_probe(&path.display().to_string(), true), |
| 3918 | + } |
| 3919 | +} |
| 3920 | + |
| 3921 | +fn probe_tool_banner(path: &Path) -> Result<(String, String), String> { |
| 3922 | + let mut last_detail = None; |
| 3923 | + for arg in ["--version", "-V", "-v"] { |
| 3924 | + match Command::new(path).arg(arg).output() { |
| 3925 | + Ok(output) => { |
| 3926 | + let stdout = String::from_utf8_lossy(&output.stdout); |
| 3927 | + let stderr = String::from_utf8_lossy(&output.stderr); |
| 3928 | + let combined = stdout |
| 3929 | + .lines() |
| 3930 | + .chain(stderr.lines()) |
| 3931 | + .map(str::trim) |
| 3932 | + .find(|line| !line.is_empty()) |
| 3933 | + .map(|line| compact_probe_banner(line)); |
| 3934 | + if let Some(line) = combined { |
| 3935 | + return Ok((arg.to_string(), line)); |
| 3936 | + } |
| 3937 | + last_detail = Some(format!( |
| 3938 | + "{} returned no banner output (exit={})", |
| 3939 | + arg, |
| 3940 | + output.status.code().unwrap_or(-1) |
| 3941 | + )); |
| 3942 | + } |
| 3943 | + Err(err) => { |
| 3944 | + last_detail = Some(format!("{} failed: {}", arg, err)); |
| 3945 | + } |
| 3946 | + } |
| 3947 | + } |
| 3948 | + |
| 3949 | + Err(last_detail.unwrap_or_else(|| "probe failed".to_string())) |
| 3950 | +} |
| 3951 | + |
| 3952 | +fn compact_probe_banner(line: &str) -> String { |
| 3953 | + let compact = line.split_whitespace().collect::<Vec<_>>().join(" "); |
| 3954 | + let chars = compact.chars().collect::<Vec<_>>(); |
| 3955 | + if chars.len() > 120 { |
| 3956 | + chars.into_iter().take(117).collect::<String>() + "..." |
| 3957 | + } else { |
| 3958 | + compact |
| 3959 | + } |
| 3960 | +} |
| 3961 | + |
| 3962 | +fn tool_probe_status(configured: &str, already_resolved_path: bool) -> String { |
| 3963 | + let probe = tool_probe(configured, already_resolved_path); |
| 3964 | + match probe.resolved_path { |
| 3796 | 3965 | Some(path) => format!("configured={} resolved={}", configured, path.display()), |
| 3797 | 3966 | None => format!("configured={} resolved=missing", configured), |
| 3798 | 3967 | } |
@@ -4885,7 +5054,21 @@ fn case_discovery_lines(case: &CaseSpec, tools: &ToolchainConfig) -> Vec<String> |
| 4885 | 5054 | } |
| 4886 | 5055 | |
| 4887 | 5056 | if let Some(generic) = &case.generic_introspect { |
| 5057 | + let capture_root = tools.armfortas_adapters().capture_root(); |
| 5058 | + let probe = compiler_spec_probe(&generic.compiler, tools, capture_root.as_ref()); |
| 4888 | 5059 | lines.push(format!("compiler: {}", generic.compiler.display_name())); |
| 5060 | + lines.push(format!("compiler_probe_status: {}", probe.status)); |
| 5061 | + lines.push(format!( |
| 5062 | + "compiler_probe_resolved_path: {}", |
| 5063 | + probe |
| 5064 | + .resolved_path |
| 5065 | + .as_ref() |
| 5066 | + .map(|path| display_path(path)) |
| 5067 | + .unwrap_or_else(|| "none".to_string()) |
| 5068 | + )); |
| 5069 | + if let Some(banner) = &probe.banner { |
| 5070 | + lines.push(format!("compiler_probe_banner: {}", banner)); |
| 5071 | + } |
| 4889 | 5072 | lines.push(format!( |
| 4890 | 5073 | "artifacts: {}", |
| 4891 | 5074 | format_artifact_name_list( |
@@ -4915,11 +5098,38 @@ fn case_discovery_lines(case: &CaseSpec, tools: &ToolchainConfig) -> Vec<String> |
| 4915 | 5098 | } |
| 4916 | 5099 | |
| 4917 | 5100 | if let Some(generic) = &case.generic_compare { |
| 5101 | + let capture_root = tools.armfortas_adapters().capture_root(); |
| 5102 | + let left_probe = compiler_spec_probe(&generic.left, tools, capture_root.as_ref()); |
| 5103 | + let right_probe = compiler_spec_probe(&generic.right, tools, capture_root.as_ref()); |
| 4918 | 5104 | lines.push(format!( |
| 4919 | 5105 | "compare: {} vs {}", |
| 4920 | 5106 | generic.left.display_name(), |
| 4921 | 5107 | generic.right.display_name() |
| 4922 | 5108 | )); |
| 5109 | + lines.push(format!("left_probe_status: {}", left_probe.status)); |
| 5110 | + lines.push(format!( |
| 5111 | + "left_probe_resolved_path: {}", |
| 5112 | + left_probe |
| 5113 | + .resolved_path |
| 5114 | + .as_ref() |
| 5115 | + .map(|path| display_path(path)) |
| 5116 | + .unwrap_or_else(|| "none".to_string()) |
| 5117 | + )); |
| 5118 | + if let Some(banner) = &left_probe.banner { |
| 5119 | + lines.push(format!("left_probe_banner: {}", banner)); |
| 5120 | + } |
| 5121 | + lines.push(format!("right_probe_status: {}", right_probe.status)); |
| 5122 | + lines.push(format!( |
| 5123 | + "right_probe_resolved_path: {}", |
| 5124 | + right_probe |
| 5125 | + .resolved_path |
| 5126 | + .as_ref() |
| 5127 | + .map(|path| display_path(path)) |
| 5128 | + .unwrap_or_else(|| "none".to_string()) |
| 5129 | + )); |
| 5130 | + if let Some(banner) = &right_probe.banner { |
| 5131 | + lines.push(format!("right_probe_banner: {}", banner)); |
| 5132 | + } |
| 4923 | 5133 | lines.push(format!( |
| 4924 | 5134 | "artifacts: {}", |
| 4925 | 5135 | format_artifact_name_list( |
@@ -10992,6 +11202,14 @@ mod tests { |
| 10992 | 11202 | fs::set_permissions(path, perms).unwrap(); |
| 10993 | 11203 | } |
| 10994 | 11204 | |
| 11205 | + #[cfg(unix)] |
| 11206 | + fn write_probe_script(path: &Path, banner: &str) { |
| 11207 | + fs::write(path, format!("#!/bin/sh\nprintf '%s\\n' {:?}\n", banner)).unwrap(); |
| 11208 | + let mut perms = fs::metadata(path).unwrap().permissions(); |
| 11209 | + perms.set_mode(0o755); |
| 11210 | + fs::set_permissions(path, perms).unwrap(); |
| 11211 | + } |
| 11212 | + |
| 10995 | 11213 | #[cfg(unix)] |
| 10996 | 11214 | fn command_is_available(name: &str) -> bool { |
| 10997 | 11215 | Command::new("which") |
@@ -14133,8 +14351,8 @@ end |
| 14133 | 14351 | fs::create_dir_all(&root).unwrap(); |
| 14134 | 14352 | let armfortas_bin = root.join("armfortas"); |
| 14135 | 14353 | let gfortran_bin = root.join("gfortran"); |
| 14136 | | - fs::write(&armfortas_bin, "").unwrap(); |
| 14137 | | - fs::write(&gfortran_bin, "").unwrap(); |
| 14354 | + write_probe_script(&armfortas_bin, "armfortas dev build"); |
| 14355 | + write_probe_script(&gfortran_bin, "GNU Fortran 99.1"); |
| 14138 | 14356 | |
| 14139 | 14357 | let config = DoctorConfig { |
| 14140 | 14358 | tools: ToolchainConfig { |
@@ -14189,6 +14407,10 @@ end |
| 14189 | 14407 | assert!(rendered.contains("named_compiler.lfortran.candidate_binaries: lfortran")); |
| 14190 | 14408 | assert!(rendered.contains("named_compiler.ifx.accepted_names: ifx")); |
| 14191 | 14409 | assert!(rendered.contains("named_compiler.nvfortran.accepted_names: nvfortran, pgfortran")); |
| 14410 | + assert!(rendered.contains("named_compiler.armfortas.probe_status: invokable")); |
| 14411 | + assert!(rendered.contains("named_compiler.armfortas.probe_banner: armfortas dev build")); |
| 14412 | + assert!(rendered.contains("named_compiler.gfortran.probe_status: invokable")); |
| 14413 | + assert!(rendered.contains("named_compiler.gfortran.probe_banner: GNU Fortran 99.1")); |
| 14192 | 14414 | assert!(rendered.contains( |
| 14193 | 14415 | "explicit_compiler_path: any filesystem path passed to compare/introspect uses the generic external-driver adapter" |
| 14194 | 14416 | )); |
@@ -14214,12 +14436,34 @@ end |
| 14214 | 14436 | assert!(rendered_json.contains("\"tools\": {")); |
| 14215 | 14437 | assert!(rendered_json.contains("\"lfortran\": {")); |
| 14216 | 14438 | assert!(rendered_json.contains("\"named_compiler.armfortas.adapter_extras\"")); |
| 14439 | + assert!(rendered_json.contains("\"probe\": {")); |
| 14217 | 14440 | assert!(rendered_markdown.contains("# bencch doctor report")); |
| 14218 | 14441 | assert!(rendered_markdown.contains("| `named_compiler.armfortas` |")); |
| 14219 | 14442 | |
| 14220 | 14443 | let _ = fs::remove_dir_all(&root); |
| 14221 | 14444 | } |
| 14222 | 14445 | |
| 14446 | + #[cfg(unix)] |
| 14447 | + #[test] |
| 14448 | + fn tool_probe_reads_banner_from_executable() { |
| 14449 | + let root = std::env::temp_dir().join("bencch_tool_probe_banner"); |
| 14450 | + let _ = fs::remove_dir_all(&root); |
| 14451 | + fs::create_dir_all(&root).unwrap(); |
| 14452 | + let probe_bin = root.join("fakefortran"); |
| 14453 | + write_probe_script(&probe_bin, "Fake Fortran 1.2.3"); |
| 14454 | + |
| 14455 | + let probe = tool_probe(&probe_bin.display().to_string(), true); |
| 14456 | + assert_eq!(probe.status, "invokable"); |
| 14457 | + assert_eq!(probe.banner.as_deref(), Some("Fake Fortran 1.2.3")); |
| 14458 | + assert!(probe |
| 14459 | + .detail |
| 14460 | + .as_deref() |
| 14461 | + .unwrap_or_default() |
| 14462 | + .contains("--version")); |
| 14463 | + |
| 14464 | + let _ = fs::remove_dir_all(&root); |
| 14465 | + } |
| 14466 | + |
| 14223 | 14467 | #[test] |
| 14224 | 14468 | fn case_discovery_lines_report_capability_block_for_generic_introspect() { |
| 14225 | 14469 | let case = CaseSpec { |
@@ -14280,6 +14524,43 @@ end |
| 14280 | 14524 | )); |
| 14281 | 14525 | } |
| 14282 | 14526 | |
| 14527 | + #[cfg(unix)] |
| 14528 | + #[test] |
| 14529 | + fn case_discovery_lines_include_compiler_probe_banner() { |
| 14530 | + let root = std::env::temp_dir().join("bencch_case_discovery_probe"); |
| 14531 | + let _ = fs::remove_dir_all(&root); |
| 14532 | + fs::create_dir_all(&root).unwrap(); |
| 14533 | + let compiler = root.join("probe-compiler"); |
| 14534 | + write_probe_script(&compiler, "Probe Compiler 7.4"); |
| 14535 | + |
| 14536 | + let case = CaseSpec { |
| 14537 | + name: "probe".into(), |
| 14538 | + source: PathBuf::from("demo.f90"), |
| 14539 | + graph_files: Vec::new(), |
| 14540 | + requested: BTreeSet::new(), |
| 14541 | + generic_introspect: Some(GenericIntrospectCase { |
| 14542 | + compiler: CompilerSpec::Binary(compiler.clone()), |
| 14543 | + artifacts: BTreeSet::from([ArtifactKey::Asm]), |
| 14544 | + }), |
| 14545 | + generic_compare: None, |
| 14546 | + opt_levels: vec![OptLevel::O0], |
| 14547 | + repeat_count: 2, |
| 14548 | + reference_compilers: Vec::new(), |
| 14549 | + consistency_checks: Vec::new(), |
| 14550 | + expectations: Vec::new(), |
| 14551 | + status_rules: Vec::new(), |
| 14552 | + capability_policy: None, |
| 14553 | + }; |
| 14554 | + |
| 14555 | + let lines = case_discovery_lines(&case, &ToolchainConfig::from_env()); |
| 14556 | + assert!(lines.contains(&"compiler_probe_status: invokable".to_string())); |
| 14557 | + assert!(lines |
| 14558 | + .iter() |
| 14559 | + .any(|line| line.contains("compiler_probe_banner: Probe Compiler 7.4"))); |
| 14560 | + |
| 14561 | + let _ = fs::remove_dir_all(&root); |
| 14562 | + } |
| 14563 | + |
| 14283 | 14564 | #[test] |
| 14284 | 14565 | fn case_discovery_lines_distinguish_legacy_surfaces() { |
| 14285 | 14566 | let observable_case = CaseSpec { |