1use addr2line::Loader;
2use anyhow::{anyhow, Result};
3use byteorder::{LittleEndian, ReadBytesExt};
4use cargo_metadata::MetadataCommand;
5use std::{
6 collections::BTreeMap,
7 env::var_os,
8 fs::{metadata, File, OpenOptions},
9 io::Write,
10 path::{Path, PathBuf},
11};
12
13pub const DOCKER_BUILDER_VERSION: &str = "0.0.0";
14
15pub const DEFAULT_RPC_PORT: u16 = 8899;
17
18#[cfg(feature = "__anchor_cli")]
19mod anchor_cli_lib;
20#[cfg(feature = "__anchor_cli")]
21pub use anchor_cli_lib::__build_with_debug;
22#[cfg(feature = "__anchor_cli")]
23pub use anchor_cli_lib::{
24 __get_keypair as get_keypair, __is_hidden as is_hidden, __keys_sync as keys_sync,
25};
26
27#[cfg(feature = "__anchor_cli")]
28mod anchor_cli_config;
29#[cfg(feature = "__anchor_cli")]
30use anchor_cli_config as config;
31#[cfg(feature = "__anchor_cli")]
32pub use anchor_cli_config::{BootstrapMode, ConfigOverride, ProgramArch};
33
34mod insn;
35use insn::Insn;
36
37mod start_address;
38use start_address::start_address;
39
40pub mod util;
41use util::{files_with_extension, StripCurrentDir};
42
43mod vaddr;
44use vaddr::Vaddr;
45
46#[derive(Clone, Debug, Default, Eq, PartialEq)]
47struct Entry<'a> {
48 file: &'a str,
49 line: u32,
50}
51
52struct Dwarf {
53 path: PathBuf,
54 start_address: u64,
55 #[allow(dead_code, reason = "`vaddr` points into `loader`")]
56 loader: &'static Loader,
57 vaddr_entry_map: BTreeMap<u64, Entry<'static>>,
58}
59
60enum Outcome {
61 Lcov(PathBuf),
62 ClosestMatch(PathBuf),
63}
64
65type Vaddrs = Vec<u64>;
66
67type VaddrEntryMap<'a> = BTreeMap<u64, Entry<'a>>;
68
69#[allow(dead_code)]
70#[derive(Debug)]
71struct ClosestMatch<'a, 'b> {
72 pcs_path: &'a Path,
73 debug_path: &'b Path,
74 mismatch: Mismatch,
75}
76
77#[allow(dead_code)]
78#[derive(Clone, Copy, Debug, Default)]
79struct Mismatch {
80 index: usize,
81 vaddr: Vaddr,
82 expected: Insn,
83 actual: Insn,
84}
85
86type FileLineCountMap<'a> = BTreeMap<&'a str, BTreeMap<u32, usize>>;
87
88pub fn run(sbf_trace_dir: impl AsRef<Path>, debug: bool) -> Result<()> {
89 let mut lcov_paths = Vec::new();
90 let mut closest_match_paths = Vec::new();
91
92 let debug_paths = debug_paths()?;
93
94 let dwarfs = debug_paths
95 .into_iter()
96 .map(|path| build_dwarf(&path))
97 .collect::<Result<Vec<_>>>()?;
98
99 if dwarfs.is_empty() {
100 eprintln!("Found no debug files");
101 return Ok(());
102 }
103
104 if debug {
105 for dwarf in dwarfs {
106 dump_vaddr_entry_map(dwarf.vaddr_entry_map);
107 }
108 return Ok(());
109 }
110
111 let pcs_paths = files_with_extension(&sbf_trace_dir, "pcs")?;
112
113 for pcs_path in &pcs_paths {
114 match process_pcs_path(&dwarfs, pcs_path)? {
115 Outcome::Lcov(lcov_path) => {
116 lcov_paths.push(lcov_path.strip_current_dir().to_path_buf());
117 }
118 Outcome::ClosestMatch(closest_match_path) => {
119 closest_match_paths.push(closest_match_path.strip_current_dir().to_path_buf());
120 }
121 }
122 }
123
124 eprintln!(
125 "
126Processed {} of {} program counter files
127
128Lcov files written: {lcov_paths:#?}
129
130Closest match files written: {closest_match_paths:#?}
131
132If you are done generating lcov files, try running:
133
134 genhtml --output-directory coverage {}/*.lcov && open coverage/index.html
135",
136 lcov_paths.len(),
137 pcs_paths.len(),
138 sbf_trace_dir.as_ref().strip_current_dir().display()
139 );
140
141 Ok(())
142}
143
144fn debug_paths() -> Result<Vec<PathBuf>> {
145 let metadata = MetadataCommand::new().no_deps().exec()?;
146 let target_directory = metadata.target_directory;
147 files_with_extension(target_directory.join("deploy"), "debug")
148}
149
150fn build_dwarf(debug_path: &Path) -> Result<Dwarf> {
151 let start_address = start_address(debug_path)?;
152
153 let loader = Loader::new(debug_path).map_err(|error| {
154 anyhow!(
155 "failed to build loader for {}: {}",
156 debug_path.display(),
157 error
158 )
159 })?;
160
161 let loader = Box::leak(Box::new(loader));
162
163 let vaddr_entry_map = build_vaddr_entry_map(loader, debug_path)?;
164
165 Ok(Dwarf {
166 path: debug_path.to_path_buf(),
167 start_address,
168 loader,
169 vaddr_entry_map,
170 })
171}
172
173fn process_pcs_path(dwarfs: &[Dwarf], pcs_path: &Path) -> Result<Outcome> {
174 eprintln!();
175 eprintln!(
176 "Program counters file: {}",
177 pcs_path.strip_current_dir().display()
178 );
179
180 let mut vaddrs = read_vaddrs(pcs_path)?;
181
182 eprintln!("Program counters read: {}", vaddrs.len());
183
184 let (dwarf, mismatch) = find_applicable_dwarf(dwarfs, pcs_path, &mut vaddrs)?;
185
186 if let Some(mismatch) = mismatch {
187 return write_closest_match(pcs_path, dwarf, mismatch).map(Outcome::ClosestMatch);
188 }
189
190 eprintln!(
191 "Applicable dwarf: {}",
192 dwarf.path.strip_current_dir().display()
193 );
194
195 assert!(vaddrs
196 .first()
197 .is_some_and(|&vaddr| vaddr == dwarf.start_address));
198
199 vaddrs.dedup_by_key::<_, Option<&Entry>>(|vaddr| dwarf.vaddr_entry_map.get(vaddr));
202
203 let vaddrs = vaddrs
206 .into_iter()
207 .filter(|vaddr| dwarf.vaddr_entry_map.contains_key(vaddr))
208 .collect::<Vec<_>>();
209
210 eprintln!("Line hits: {}", vaddrs.len());
211
212 let file_line_count_map = build_file_line_count_map(&dwarf.vaddr_entry_map, vaddrs);
213
214 write_lcov_file(pcs_path, file_line_count_map).map(Outcome::Lcov)
215}
216
217static CARGO_HOME: std::sync::LazyLock<PathBuf> = std::sync::LazyLock::new(|| {
218 if let Some(cargo_home) = var_os("CARGO_HOME") {
219 PathBuf::from(cargo_home)
220 } else {
221 #[allow(deprecated)]
222 #[cfg_attr(
223 dylint_lib = "inconsistent_qualification",
224 allow(inconsistent_qualification)
225 )]
226 std::env::home_dir().unwrap().join(".cargo")
227 }
228});
229
230fn build_vaddr_entry_map<'a>(loader: &'a Loader, debug_path: &Path) -> Result<VaddrEntryMap<'a>> {
231 let mut vaddr_entry_map = VaddrEntryMap::new();
232 let metadata = metadata(debug_path)?;
233 for vaddr in (0..metadata.len()).step_by(size_of::<u64>()) {
234 let location = loader
235 .find_location(vaddr)
236 .map_err(|error| anyhow!("failed to find location for address 0x{vaddr:x}: {error}"))?;
237 let Some(location) = location else {
238 continue;
239 };
240 let Some(file) = location.file else {
241 continue;
242 };
243 if !Path::new(file).try_exists()? {
245 continue;
246 }
247 if !include_cargo() && file.starts_with(CARGO_HOME.to_string_lossy().as_ref()) {
248 continue;
249 }
250 let Some(line) = location.line else {
251 continue;
252 };
253 let Some(_column) = location.column else {
255 continue;
256 };
257 let entry = vaddr_entry_map.entry(vaddr).or_default();
258 entry.file = file;
259 entry.line = line;
260 }
261 Ok(vaddr_entry_map)
262}
263
264fn dump_vaddr_entry_map(vaddr_entry_map: BTreeMap<u64, Entry<'_>>) {
265 let mut prev = String::new();
266 for (vaddr, Entry { file, line }) in vaddr_entry_map {
267 let curr = format!("{file}:{line}");
268 if prev != curr {
269 eprintln!("0x{vaddr:x}: {curr}");
270 prev = curr;
271 }
272 }
273}
274
275fn read_vaddrs(pcs_path: &Path) -> Result<Vaddrs> {
276 let mut vaddrs = Vaddrs::new();
277 let mut pcs_file = File::open(pcs_path)?;
278 while let Ok(pc) = pcs_file.read_u64::<LittleEndian>() {
279 let vaddr = pc << 3;
280 vaddrs.push(vaddr);
281 }
282 Ok(vaddrs)
283}
284
285fn find_applicable_dwarf<'a>(
286 dwarfs: &'a [Dwarf],
287 pcs_path: &Path,
288 vaddrs: &mut [u64],
289) -> Result<(&'a Dwarf, Option<Mismatch>)> {
290 let dwarf_mismatches = collect_dwarf_mismatches(dwarfs, pcs_path, vaddrs)?;
291
292 if let Some((dwarf, _)) = dwarf_mismatches
293 .iter()
294 .find(|(_, mismatch)| mismatch.is_none())
295 {
296 let vaddr_first = *vaddrs.first().unwrap();
297
298 assert!(dwarf.start_address >= vaddr_first);
299
300 let shift = dwarf.start_address - vaddr_first;
301
302 for vaddr in vaddrs.iter_mut() {
304 *vaddr += shift;
305 }
306
307 return Ok((dwarf, None));
308 }
309
310 Ok(dwarf_mismatches
311 .into_iter()
312 .max_by_key(|(_, mismatch)| mismatch.as_ref().unwrap().index)
313 .unwrap())
314}
315
316fn collect_dwarf_mismatches<'a>(
317 dwarfs: &'a [Dwarf],
318 pcs_path: &Path,
319 vaddrs: &[u64],
320) -> Result<Vec<(&'a Dwarf, Option<Mismatch>)>> {
321 dwarfs
322 .iter()
323 .map(|dwarf| {
324 let mismatch = dwarf_mismatch(vaddrs, dwarf, pcs_path)?;
325 Ok((dwarf, mismatch))
326 })
327 .collect()
328}
329
330fn dwarf_mismatch(vaddrs: &[u64], dwarf: &Dwarf, pcs_path: &Path) -> Result<Option<Mismatch>> {
331 use std::io::{Seek, SeekFrom};
332
333 let Some(&vaddr_first) = vaddrs.first() else {
334 return Ok(Some(Mismatch::default()));
335 };
336
337 if dwarf.start_address < vaddr_first {
338 return Ok(Some(Mismatch::default()));
339 }
340
341 let shift = dwarf.start_address - vaddr_first;
345
346 let mut so_file = File::open(dwarf.path.with_extension("so"))?;
347 let mut insns_file = File::open(pcs_path.with_extension("insns"))?;
348
349 for (index, &vaddr) in vaddrs.iter().enumerate() {
350 let vaddr = vaddr + shift;
351
352 so_file.seek(SeekFrom::Start(vaddr))?;
353 let expected = so_file.read_u64::<LittleEndian>()?;
354
355 let actual = insns_file.read_u64::<LittleEndian>()?;
356
357 if expected & 0xff == 0x85 {
360 continue;
361 }
362
363 if expected != actual {
364 return Ok(Some(Mismatch {
365 index,
366 vaddr: Vaddr::from(vaddr),
367 expected: Insn::from(expected),
368 actual: Insn::from(actual),
369 }));
370 }
371 }
372
373 Ok(None)
374}
375
376fn write_closest_match(pcs_path: &Path, dwarf: &Dwarf, mismatch: Mismatch) -> Result<PathBuf> {
377 let closest_match_path = pcs_path.with_extension("closest_match");
378 let mut file = OpenOptions::new()
379 .create(true)
380 .truncate(true)
381 .write(true)
382 .open(&closest_match_path)?;
383 writeln!(
384 file,
385 "{:#?}",
386 ClosestMatch {
387 pcs_path,
388 debug_path: &dwarf.path,
389 mismatch
390 }
391 )?;
392 Ok(closest_match_path)
393}
394
395fn build_file_line_count_map<'a>(
396 vaddr_entry_map: &BTreeMap<u64, Entry<'a>>,
397 vaddrs: Vaddrs,
398) -> FileLineCountMap<'a> {
399 let mut file_line_count_map = FileLineCountMap::new();
400 for Entry { file, line } in vaddr_entry_map.values() {
401 let line_count_map = file_line_count_map.entry(file).or_default();
402 line_count_map.insert(*line, 0);
403 }
404
405 for vaddr in vaddrs {
406 let Some(entry) = vaddr_entry_map.get(&vaddr) else {
408 continue;
409 };
410 let line_count_map = file_line_count_map.get_mut(entry.file).unwrap();
411 let count = line_count_map.get_mut(&entry.line).unwrap();
412 *count += 1;
413 }
414
415 file_line_count_map
416}
417
418fn write_lcov_file(pcs_path: &Path, file_line_count_map: FileLineCountMap<'_>) -> Result<PathBuf> {
419 let lcov_path = Path::new(pcs_path).with_extension("lcov");
420
421 let mut file = OpenOptions::new()
422 .create(true)
423 .truncate(true)
424 .write(true)
425 .open(&lcov_path)?;
426
427 for (source_file, line_count_map) in file_line_count_map {
428 writeln!(file, "SF:{source_file}")?;
430 for (line, count) in line_count_map {
431 writeln!(file, "DA:{line},{count}")?;
432 }
433 writeln!(file, "end_of_record")?;
434 }
435
436 Ok(lcov_path)
437}
438
439fn include_cargo() -> bool {
440 var_os("INCLUDE_CARGO").is_some()
441}
442
443#[cfg(test)]
444mod tests;
445
446#[test]
447fn nested_workspace() {
448 nested_workspace::test().unwrap();
449}