use std::{
env,
ffi::OsString,
fs,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{Context, Result, bail, ensure};
use crate::{Artifact, StdMode, args::Args};
const TOOLCHAIN: Option<&str> = option_env!("RUSTUP_TOOLCHAIN");
const BUILT_AGAINST: Option<&str> = option_env!("PANICGRAPH_RUSTC");
struct Layout {
out: PathBuf,
target: PathBuf,
}
pub fn collect(args: &Args) -> Result<Vec<Artifact>> {
let driver = driver_path()?;
check_toolchain()?;
let root = match &args.manifest_dir {
Some(dir) => dir.clone(),
None => env::current_dir()
.context("could not determine the current directory")?,
};
let layout = prepare(&root, &driver, args)?;
build(args, &driver, &root, &layout)?;
let mut artifacts = load(&layout.out)?;
if artifacts.is_empty() {
if layout.target.exists() {
fs::remove_dir_all(&layout.target).with_context(|| {
format!("could not clear {}", layout.target.display())
})?;
}
build(args, &driver, &root, &layout)?;
artifacts = load(&layout.out)?;
}
if artifacts.is_empty() {
bail!("the build produced no analysis artifacts");
}
Ok(artifacts)
}
fn check_toolchain() -> Result<()> {
let (Some(built), Some(current)) = (BUILT_AGAINST, rustc_version()?) else {
return Ok(());
};
ensure!(
current == built,
"this tool was built against {built}, but the toolchain now offers \
{current}. The analysis driver links that compiler's own libraries \
and cannot run against another build of it, so reinstall with \
`cargo install --path .`"
);
Ok(())
}
fn rustc_version() -> Result<Option<String>> {
let out = rustc()
.arg("--version")
.output()
.context("could not run rustc")?;
let text =
String::from_utf8(out.stdout).context("rustc printed invalid utf-8")?;
let line = text.lines().next().unwrap_or_default().trim().to_owned();
Ok((!line.is_empty()).then_some(line))
}
fn build(
args: &Args,
driver: &Path,
root: &Path,
layout: &Layout,
) -> Result<()> {
let mut cmd = Command::new("cargo");
cmd.current_dir(root)
.arg("build")
.arg("--profile")
.arg(cargo_profile(&args.profile))
.arg("--target-dir")
.arg(&layout.target);
if let Some(pkg) = &args.package {
cmd.arg("--package").arg(pkg);
}
if args.std_mode == StdMode::Full {
cmd.arg("-Z")
.arg("build-std=core,alloc,std")
.arg("--target")
.arg(host_triple()?);
}
cmd.env("RUSTC_WRAPPER", driver)
.env("PANICGRAPH_OUT", &layout.out)
.env("PANICGRAPH_PROFILE", &args.profile)
.env("PANICGRAPH_STD_MODE", args.std_mode.name());
if let Some(toolchain) = TOOLCHAIN {
cmd.env("RUSTUP_TOOLCHAIN", toolchain);
}
if let Some(path) = library_path()? {
cmd.env("LD_LIBRARY_PATH", path);
}
let status = cmd
.status()
.context("could not run cargo; is it on the PATH?")?;
if !status.success() {
bail!("the analysis build failed; the errors above are from cargo");
}
Ok(())
}
const SLOT_MARKER: &str = ".panicgraph-slot";
fn prepare(root: &Path, driver: &Path, args: &Args) -> Result<Layout> {
let base = root.join("target").join("panicgraph");
let slot = format!(
"{}-{}-{}",
args.profile,
args.std_mode.name(),
args.package.as_deref().unwrap_or("all"),
);
let layout = Layout {
out: base.join(&slot),
target: base.join("build"),
};
let marker = format!("{slot}\n{}\n", driver_stamp(driver));
discard_if_stale(&layout, &marker)?;
fs::create_dir_all(&layout.out).with_context(|| {
format!("could not create {}", layout.out.display())
})?;
fs::write(layout.out.join(SLOT_MARKER), marker.as_bytes()).with_context(
|| format!("could not write the marker in {}", layout.out.display()),
)?;
prune_stale(&base, &layout);
Ok(layout)
}
fn driver_stamp(driver: &Path) -> String {
let Ok(meta) = fs::metadata(driver) else {
return "unknown".to_owned();
};
let modified = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map_or(0, |d| d.as_nanos());
format!("{}-{modified}", meta.len())
}
fn discard_if_stale(layout: &Layout, marker: &str) -> Result<()> {
let path = layout.out.join(SLOT_MARKER);
if !layout.out.exists() {
return Ok(());
}
if fs::read_to_string(&path).is_ok_and(|found| found == marker) {
return Ok(());
}
for dir in [&layout.out, &layout.target] {
if dir.exists() {
fs::remove_dir_all(dir).with_context(|| {
format!("could not clear {}", dir.display())
})?;
}
}
Ok(())
}
fn prune_stale(base: &Path, layout: &Layout) {
let Ok(entries) = fs::read_dir(base) else {
return;
};
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
if !path.is_dir() || path == layout.out || path == layout.target {
continue;
}
if path.join(SLOT_MARKER).exists() {
continue;
}
if fs::remove_dir_all(&path).is_ok() {
println!("removed stale results in {}", path.display());
}
}
}
fn load(dir: &Path) -> Result<Vec<Artifact>> {
let mut out = Vec::new();
for entry in fs::read_dir(dir)
.with_context(|| format!("could not read {}", dir.display()))?
{
let path = entry?.path();
if path.extension().is_none_or(|e| e != "json") {
continue;
}
let text = fs::read(&path)
.with_context(|| format!("could not read {}", path.display()))?;
let artifact: Artifact = serde_json::from_slice(&text)
.with_context(|| format!("could not parse {}", path.display()))?;
out.push(artifact);
}
Ok(out)
}
fn driver_path() -> Result<PathBuf> {
let exe = env::current_exe().context("could not locate this program")?;
let dir = exe
.parent()
.context("this program has no containing directory")?;
let driver = dir.join("panicgraph-driver");
if !driver.exists() {
bail!(
"the analysis driver is missing from {}; it is installed \
alongside this program, so reinstall with `cargo install \
--path .`",
dir.display()
);
}
Ok(driver)
}
fn library_path() -> Result<Option<OsString>> {
let Some(sysroot) = sysroot()? else {
return Ok(None);
};
let lib = PathBuf::from(sysroot).join("lib");
let mut value = lib.into_os_string();
if let Some(existing) = env::var_os("LD_LIBRARY_PATH") {
value.push(":");
value.push(existing);
}
Ok(Some(value))
}
fn sysroot() -> Result<Option<String>> {
let out = rustc()
.arg("--print")
.arg("sysroot")
.output()
.context("could not run rustc")?;
let text =
String::from_utf8(out.stdout).context("rustc printed invalid utf-8")?;
let text = text.trim();
Ok((!text.is_empty()).then(|| text.to_owned()))
}
fn host_triple() -> Result<String> {
rustc_field("host: ")?.context("rustc did not report a host triple")
}
fn rustc_field(label: &str) -> Result<Option<String>> {
let out = rustc()
.arg("--version")
.arg("--verbose")
.output()
.context("could not run rustc")?;
let text =
String::from_utf8(out.stdout).context("rustc printed invalid utf-8")?;
Ok(text
.lines()
.find_map(|l| l.strip_prefix(label))
.map(str::trim)
.map(str::to_owned))
}
fn rustc() -> Command {
let mut cmd = Command::new("rustc");
if let Some(toolchain) = TOOLCHAIN {
cmd.env("RUSTUP_TOOLCHAIN", toolchain);
}
cmd
}
fn cargo_profile(profile: &str) -> &str {
if profile == "debug" { "dev" } else { profile }
}