Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions tools/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions tools/hermes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ syn = { version = "2.0.114", features = ["full", "visit", "extra-traits", "parsi
quote = "1.0"
thiserror = "2.0.18"
walkdir = "2.5.0"
indicatif = { version = "0.18.3", features = ["improved_unicode"] }
console = "0.16.2"

[dev-dependencies]
syn = { version = "2.0.114", features = ["printing", "full", "visit", "extra-traits", "parsing"] }
Expand All @@ -35,6 +37,8 @@ datatest-stable = "0.3.3"
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
which = "6.0"
regex.workspace = true
strip-ansi-escapes = "0.2.1"

[[test]]
name = "integration"
Expand Down
99 changes: 96 additions & 3 deletions tools/hermes/src/charon.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::process::Command;
use std::{
io::{BufRead, BufReader},
process::Command,
};

use anyhow::{bail, Context as _, Result};
use cargo_metadata::{diagnostic::DiagnosticLevel, Message};

use crate::{
resolve::{Args, HermesTargetKind, Roots},
Expand Down Expand Up @@ -39,6 +43,9 @@ pub fn run_charon(args: &Args, roots: &Roots, packages: &[HermesArtifact]) -> Re
// Separator for the underlying cargo command
cmd.arg("--");

// Ensure cargo emits json msgs which charon-driver natively generates
cmd.arg("--message-format=json");

cmd.arg("--manifest-path").arg(&artifact.shadow_manifest_path);

match artifact.target_kind {
Expand Down Expand Up @@ -77,9 +84,95 @@ pub fn run_charon(args: &Args, roots: &Roots, packages: &[HermesArtifact]) -> Re

log::debug!("Command: {:?}", cmd);

let status = cmd.status().context("Failed to execute charon")?;
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn().context("Failed to spawn charon")?;

let mut output_error = false;

let safety_buffer = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let safety_buffer_clone = std::sync::Arc::clone(&safety_buffer);
if let Some(stderr) = child.stderr.take() {
std::thread::spawn(move || {
use std::io::{BufRead, BufReader};
let reader = BufReader::new(stderr);
for line in reader.lines() {
if let Ok(line) = line {
if let Ok(mut buf) = safety_buffer_clone.lock() {
buf.push(line);
}
}
}
});
}

if !status.success() {
let pb = indicatif::ProgressBar::new_spinner();
pb.set_style(
indicatif::ProgressStyle::default_spinner().template("{spinner:.green} {msg}").unwrap(),
);
pb.enable_steady_tick(std::time::Duration::from_millis(100));
pb.set_message("Compiling...");

if let Some(stdout) = child.stdout.take() {
let reader = BufReader::new(stdout);

let mut mapper = crate::diagnostics::DiagnosticMapper::new(
artifact.shadow_manifest_path.parent().unwrap().to_path_buf(),
roots.workspace.clone(),
);

for line in reader.lines() {
if let Ok(line) = line {
if let Ok(msg) = serde_json::from_str::<cargo_metadata::Message>(&line) {
match msg {
Message::CompilerArtifact(a) => {
pb.set_message(format!("Compiling {}", a.target.name));
}
Message::CompilerMessage(msg) => {
pb.suspend(|| {
mapper.render_miette(&msg.message, |s| eprintln!("{}", s));
});
if matches!(
msg.message.level,
DiagnosticLevel::Error | DiagnosticLevel::Ice
) {
output_error = true;
}
}
Message::TextLine(t) => {
if let Ok(mut buf) = safety_buffer.lock() {
buf.push(t);
}
}
_ => {}
}
} else {
if let Ok(mut buf) = safety_buffer.lock() {
buf.push(line);
}
}
}
}
}

pb.finish_and_clear();

let status = child.wait().context("Failed to wait for charon")?;

// FIXME: There's a subtle edge case here – if we get error output AND
// Rustc ICE's, there's a good chance that the JSON error messages we
// print won't include all relevant information – some will be printed
// via stderr. In this case, `output_error = true` and so we bail and
// discard stderr, which will swallow information from the user.
if output_error {
bail!("Diagnostic error in charon");
} else if !status.success() {
// "Silent Death" dump
if let Ok(buf) = safety_buffer.lock() {
for line in buf.iter() {
eprintln!("{}", line);
}
}
bail!("Charon failed with status: {}", status);
}
}
Expand Down
180 changes: 180 additions & 0 deletions tools/hermes/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};

use cargo_metadata::diagnostic::{Diagnostic, DiagnosticLevel, DiagnosticSpan};
use miette::{NamedSource, Report, SourceOffset};
use thiserror::Error;

pub struct DiagnosticMapper {
shadow_root: PathBuf,
user_root: PathBuf,
user_root_canonical: PathBuf,
source_cache: HashMap<PathBuf, String>,
}

#[derive(Error, Debug)]
#[error("{message}")]
struct MappedError {
message: String,
src: NamedSource<String>,
labels: Vec<miette::LabeledSpan>,
help: Option<String>,
related: Vec<MappedError>,
}

impl miette::Diagnostic for MappedError {
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.src)
}

fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
if self.labels.is_empty() {
None
} else {
Some(Box::new(self.labels.iter().cloned()))
}
}

fn help(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
self.help.as_ref().map(|h| Box::new(h.clone()) as Box<dyn std::fmt::Display>)
}

fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
if self.related.is_empty() {
None
} else {
let iter = self.related.iter().map(|e| e as &dyn miette::Diagnostic);
Some(Box::new(iter))
}
}
}

impl DiagnosticMapper {
pub fn new(shadow_root: PathBuf, user_root: PathBuf) -> Self {
let user_root_canonical =
fs::canonicalize(&user_root).unwrap_or_else(|_| user_root.clone());
Self { shadow_root, user_root, user_root_canonical, source_cache: HashMap::new() }
}

pub fn map_path(&self, path: &Path) -> Option<PathBuf> {
let mut p = path.to_path_buf();
if p.is_relative() {
p = self.user_root.join(p);
}

// Strategy A: Starts with shadow_root
if let Ok(suffix) = p.strip_prefix(&self.shadow_root) {
return Some(self.user_root.join(suffix));
}

// Strategy B: Starts with user_root or user_root_canonical
if p.starts_with(&self.user_root) || p.starts_with(&self.user_root_canonical) {
return Some(p);
}

None
}

fn get_source(&mut self, path: &Path) -> Option<String> {
if let Some(src) = self.source_cache.get(path) {
return Some(src.clone());
}
if let Ok(src) = fs::read_to_string(path) {
self.source_cache.insert(path.to_path_buf(), src.clone());
Some(src)
} else {
None
}
}

pub fn render_miette<F>(&mut self, diag: &Diagnostic, mut printer: F)
where
F: FnMut(String),
{
let mut mapped_paths_and_spans: HashMap<PathBuf, Vec<&DiagnosticSpan>> = HashMap::new();

// 1) Group spans by mapped path
for s in &diag.spans {
let p = PathBuf::from(&s.file_name);
if let Some(mapped_path) = self.map_path(&p) {
mapped_paths_and_spans.entry(mapped_path).or_default().push(s);
}
}

// Check children for help messages
let mut help_msg = None;
for child in &diag.children {
if child.level == DiagnosticLevel::Help {
help_msg = Some(child.message.clone());
}
}

if !mapped_paths_and_spans.is_empty() {
// Find the path that contains the primary span, or just take the first one
let primary_path = diag
.spans
.iter()
.find(|s| s.is_primary)
.and_then(|s| self.map_path(&PathBuf::from(&s.file_name)))
.or_else(|| mapped_paths_and_spans.keys().next().cloned());

if let Some(main_path) = primary_path {
let mut all_errors = Vec::new();

// Sort the paths to have the primary path first
let mut paths: Vec<PathBuf> = mapped_paths_and_spans.keys().cloned().collect();
paths.sort_by_key(|p| p != &main_path);

for p in paths {
if let Some(src) = self.get_source(&p) {
let mut labels = Vec::new();
for s in mapped_paths_and_spans.get(&p).unwrap() {
let label_text = s.label.clone().unwrap_or_default();
let start = s.byte_start as usize;
let len = (s.byte_end - s.byte_start) as usize;
if start <= src.len() && start + len <= src.len() {
let offset = SourceOffset::from(start);
labels.push(miette::LabeledSpan::new(
Some(label_text),
offset.offset(),
len,
));
}
}

let err = MappedError {
message: if p == main_path {
diag.message.clone()
} else {
format!("related to: {}", p.display())
},
src: NamedSource::new(p.to_string_lossy(), src),
labels,
help: if p == main_path { help_msg.clone() } else { None },
related: Vec::new(),
};
all_errors.push(err);
}
}

if !all_errors.is_empty() {
let mut main_err = all_errors.remove(0);
main_err.related = all_errors;
printer(format!("{:?}", Report::new(main_err)));
return;
}
}
}

// If we get here, no span was successfully mapped
let prefix = match diag.level {
DiagnosticLevel::Error | DiagnosticLevel::Ice => "[External Error]",
DiagnosticLevel::Warning => "[External Warning]",
_ => "[External Info]",
};
printer(format!("{} {}", prefix, diag.message));
}
}
1 change: 1 addition & 0 deletions tools/hermes/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod charon;
mod diagnostics;
mod errors;
mod parse;
mod resolve;
Expand Down
Loading
Loading