Skip to main content

sbpf_coverage/
toolchain.rs

1use crate::{DebugPath, util::execute_cmd};
2use std::path::PathBuf;
3
4/// Resolves the rustc sysroot for the toolchain that compiled the binary.
5/// Uses the DW_AT_producer DWARF attribute to identify the toolchain,
6/// then calls `rustc +<toolchain> --print sysroot` to get the local path.
7/// The sysroot contains stdlib sources needed to remap DWARF file paths.
8pub fn get_toolchain_sysroot(debug_path: &DebugPath) -> Option<String> {
9    if debug_path.lang == Some("DW_LANG_Rust".into())
10        && let Some(producer) = debug_path.producer.as_ref()
11    {
12        let (toolchain, platform_tools) =
13            rustc_toolchain_from_producer(producer).or_else(|| {
14                eprintln!("Failed to extract toolchain from DW_AT_producer");
15                None
16            })?;
17        let file_name = debug_path
18            .path
19            .file_name()
20            .map(|n| n.to_string_lossy())
21            .unwrap_or_default();
22        let pt = platform_tools
23            .as_ref()
24            .map(|v| format!(", platform-tools {v}"))
25            .unwrap_or_default();
26        eprintln!("{file_name} likely: compiler {producer}{pt}, toolchain {toolchain}");
27        let sysroot = execute_cmd(
28            &PathBuf::from("rustc"),
29            [&format!("+{toolchain}"), "--print", "sysroot"],
30        )
31        .or_else(|| {
32            eprintln!("Failed to extract sysroot for toolchain {toolchain}");
33            None
34        });
35
36        // Stdlib sources live under <sysroot>/lib/rustlib/src/rust,
37        // append that so the returned path is directly usable for remapping.
38        return sysroot.map(|s| format!("{}/lib/rustlib/src/rust", s.trim()));
39    }
40
41    None
42}
43
44/// Extracts the rustc toolchain specifier from the DW_AT_producer string.
45/// Returns (toolchain, optional platform-tools version),
46/// e.g. ("1.89.0-sbpf-solana-v1.53", Some("v1.53")) or ("nightly-2026-03-01", None).
47pub fn rustc_toolchain_from_producer(producer: &str) -> Option<(String, Option<String>)> {
48    let after = producer.split("rustc version ").nth(1)?;
49
50    // Till now there are two scenarios:
51    // - the toolchain used is the Solana's fork
52    // - for upstream eBPF it's nightly that's used
53    if !after.contains("-dev") {
54        // Handle upstream eBPF here
55        // "1.96.0-nightly (80381278a 2026-03-01))" -> "nightly-2026-03-01"
56        // TODO: Unfortunately the date may differ from the commit hash and be on the next day.
57        let date = after
58            .split('(')
59            .nth(1)?
60            .split(')')
61            .next()?
62            .split_whitespace()
63            .nth(1)?;
64        Some((format!("nightly-{date}"), None))
65    } else {
66        // Handle Solana's fork here, two possible cases:
67        // - DW_AT_producer ("clang LLVM (rustc version 1.89.0-dev)")
68        // - DW_AT_producer ("clang LLVM (rustc version 1.89.0-dev (daa3af4 2026-03-03))")
69        //   as https://github.com/anza-xyz/rust/pull/148 got merged
70        // "1.89.0-dev)" or "1.89.0-dev (daa3af4 2026-03-03))"
71        let version_dev = after.split(')').next()?;
72        let rustc_version = version_dev.split('-').next()?;
73        let producer_rustc_commit_hash = version_dev
74            .split('(')
75            .nth(1)
76            .and_then(|inner| inner.split_whitespace().next())
77            .map(String::from);
78        let platform_tools_version =
79            get_platform_tools_version(rustc_version, producer_rustc_commit_hash.as_deref())?;
80        Some((
81            // "1.89.0-sbpf-solana-v1.53"
82            format!("{rustc_version}-sbpf-solana-{platform_tools_version}"),
83            Some(platform_tools_version),
84        ))
85    }
86}
87
88/// Returns the Cargo home directory, where registry sources and caches are stored.
89/// Respects the CARGO_HOME environment variable, defaulting to ~/.cargo.
90pub fn cargo_home() -> String {
91    std::env::var("CARGO_HOME")
92        .unwrap_or_else(|_| format!("{}/.cargo", std::env::var("HOME").unwrap()))
93}
94
95/// Scans locally installed platform-tools in ~/.cache/solana/ to find which version
96/// contains a rustc matching the given version string (e.g. "1.89.0").
97/// Returns the version directory name (e.g. "v1.53") if found, starting from the latest.
98pub fn get_platform_tools_version(
99    binary_rustc_version: &str,
100    producer_rustc_commit_hash: Option<&str>,
101) -> Option<String> {
102    let home_dir = std::env::var("HOME").ok()?;
103    let base_line = format!("{}/.cache/solana", home_dir);
104    let paths = std::fs::read_dir(&base_line).ok()?;
105
106    let mut platform_tools_dirs = Vec::new();
107    for path in paths {
108        let Ok(path) = path else { continue };
109        // Filter only directories
110        let Ok(file_type) = path.file_type() else {
111            continue;
112        };
113        if !file_type.is_dir() {
114            continue;
115        }
116        let dir_name = path.file_name();
117        // Typically installed versions start with vX.Z
118        if !dir_name.to_string_lossy().starts_with("v") {
119            continue;
120        }
121        platform_tools_dirs.push(dir_name.to_string_lossy().to_string());
122    }
123
124    platform_tools_dirs.sort();
125
126    // Iterate backwards to start with the latest toolchain.
127    // If the producer includes a rustc commit hash, use it to narrow the match;
128    // otherwise fall back to version-string matching.
129    platform_tools_dirs
130        .iter()
131        .rev()
132        .map(|ver| {
133            (
134                ver.clone(),
135                format!("{}/{}/platform-tools/rust/bin/rustc", base_line, ver),
136            )
137        })
138        .filter(|(ver, rustc_path)| {
139            if !PathBuf::from(&rustc_path).is_file() {
140                return false;
141            }
142            let Some(platform_tools_rustc_version) =
143                execute_cmd(&PathBuf::from(rustc_path), ["--version"])
144            else {
145                return false;
146            };
147            let rustc_commit_hash =
148                std::fs::read_to_string(format!("{base_line}/{ver}/platform-tools/version.md"))
149                    .ok()
150                    .and_then(|version_file_content| {
151                        version_file_content
152                            .lines()
153                            .find(|line| line.contains("rust.git"))
154                            .and_then(|line| line.split(' ').next())
155                            .map(String::from)
156                    });
157            if let (Some(rch), Some(ch)) = (rustc_commit_hash, producer_rustc_commit_hash)
158                && !rch.starts_with(ch)
159            {
160                return false;
161            }
162
163            platform_tools_rustc_version.contains(binary_rustc_version)
164        })
165        .map(|(ver, _)| ver)
166        .next()
167}
168
169/// Maps a DWARF-recorded source path to a local filesystem path.
170/// DWARF paths from CI/build environments use absolute paths (e.g. /home/runner/...).
171/// If a rust source root is available, paths containing `/library/` are remapped to the local toolchain sysroot.
172pub fn map_dwarf_path(dwarf_path: &str, rust_src_root: Option<&str>, cargo_root: &str) -> String {
173    if let (Some(rust_src_root), Some(pos)) = (rust_src_root, dwarf_path.find("/library/")) {
174        let suffix = &dwarf_path[pos..];
175        format!("{}/{}", rust_src_root, suffix)
176    } else if let Some(pos) = dwarf_path
177        .find(".cargo/registry/")
178        .or_else(|| dwarf_path.find(".cargo/git/"))
179    {
180        let suffix = &dwarf_path[pos + ".cargo/".len()..];
181        format!("{}/{}", cargo_root, suffix)
182    } else {
183        // fallback: path as-is
184        dwarf_path.to_string()
185    }
186}