#![warn(clippy::all, clippy::pedantic, clippy::cargo)]
use std::{
io::BufReader,
process::{Command, Stdio},
};
use anyhow::{anyhow, Context};
use camino::Utf8PathBuf;
use cargo_metadata::{camino::Utf8Path, Artifact, Metadata};
use clap::AppSettings;
use dialoguer::Select;
use structopt::StructOpt;
#[allow(unused)]
use tracing::{debug, error, info, warn};
#[derive(StructOpt, Debug)]
#[structopt(bin_name = "cargo", about, author)]
enum OptWrapper {
#[structopt(name = "rr")]
Opt(Opt),
}
#[derive(StructOpt, Debug)]
#[structopt(about, author)]
enum Opt {
#[structopt(
about = "Record a binary or example",
setting(AppSettings::TrailingVarArg),
setting(AppSettings::AllowLeadingHyphen)
)]
Run {
#[structopt(name = "OPTIONS", help = "Options to pass to `cargo run`")]
opts: Vec<String>,
#[structopt(
last = true,
help = "Options to pass to `rr record`. See `rr record -h`"
)]
rr_opts: Vec<String>,
},
#[structopt(
about = "Record a test",
setting(AppSettings::TrailingVarArg),
setting(AppSettings::AllowLeadingHyphen)
)]
Test {
#[structopt(name = "OPTIONS", help = "Options to pass to `cargo test`")]
opts: Vec<String>,
#[structopt(
last = true,
help = "Options to pass to `rr record`. See `rr record -h`"
)]
rr_opts: Vec<String>,
},
#[structopt(
about = "Replay a trace",
setting(AppSettings::TrailingVarArg),
setting(AppSettings::AllowLeadingHyphen)
)]
Replay {
#[structopt(help = "Leave blank to replay the last trace recorded")]
trace: Option<String>,
#[structopt(
last = true,
help = "Options to pass to `rr replay`. See `rr replay -h`"
)]
rr_opts: Vec<String>,
},
#[structopt(about = "List traces", setting(AppSettings::AllowLeadingHyphen))]
Ls {
#[structopt(last = true, help = "Options to pass to `rr ls`. See `rr ls -h`")]
rr_opts: Vec<String>,
},
}
fn main() -> anyhow::Result<()> {
if let Err(err) = run() {
println!(); return Err(err);
}
Ok(())
}
fn run() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let OptWrapper::Opt(opt) = OptWrapper::from_args();
debug!(?opt, "Parsed options");
match opt {
Opt::Run { opts, rr_opts } => {
let bin = build_and_select(false, &opts, |kind| kind == "bin" || kind == "example")?;
record(&bin, rr_opts, Vec::new())?;
}
Opt::Test { mut opts, rr_opts } => {
let bin = build_and_select(true, &opts, |kind| kind == "test")?;
let bin_args = if let Some(last) = opts.last() {
if last.starts_with('-') {
Vec::new()
} else {
let test_name = opts.pop().unwrap();
vec![test_name]
}
} else {
Vec::new()
};
record(&bin, rr_opts, bin_args)?;
}
Opt::Replay { rr_opts, trace } => replay(trace, rr_opts)?,
Opt::Ls { rr_opts } => ls(&rr_opts)?,
}
Ok(())
}
fn meta() -> anyhow::Result<Metadata> {
let meta = cargo_metadata::MetadataCommand::new().exec()?;
Ok(meta)
}
fn build_and_select<F>(
is_test: bool,
opts: &[String],
kind_filter: F,
) -> anyhow::Result<Utf8PathBuf>
where
F: Fn(&str) -> bool,
{
use cargo_metadata::Message;
let meta = meta()?;
let workspace_members = &meta.workspace_members;
let mut cmd = Command::new("cargo");
if is_test {
cmd.arg("test").arg("--no-run");
} else {
cmd.arg("build");
}
let mut cmd = cmd
.arg("--message-format=json-render-diagnostics")
.args(opts)
.stdout(Stdio::piped())
.spawn()?;
let reader = BufReader::new(cmd.stdout.take().expect("Piped stdout"));
let mut artifacts = Vec::new();
for msg in Message::parse_stream(reader) {
if let Message::CompilerArtifact(artifact) = msg? {
if artifact.executable.is_some()
&& workspace_members.iter().any(|w| w == &artifact.package_id)
&& artifact.target.kind.iter().any(|s| kind_filter(s))
{
debug!(?artifact, "Artifact passed filters");
artifacts.push(artifact);
}
}
}
artifacts.sort_by(|a, b| a.target.src_path.cmp(&b.target.src_path));
artifacts.dedup_by(|a, b| a.target.src_path == b.target.src_path);
let artifact = match artifacts.len() {
0 => return Err(anyhow!("cargo-rr: No test artifacts built.")),
1 => &artifacts[0],
_ => select_artifact(&meta, &artifacts)?,
};
let bin = artifact
.executable
.as_ref()
.context("Artifact has no executable")?;
debug!("Selected bin {:?}", bin);
Ok(bin.to_path_buf())
}
fn record(bin: &Utf8Path, mut args: Vec<String>, bin_args: Vec<String>) -> anyhow::Result<()> {
println!();
insert_trailing_args(&mut args, bin_args);
let mut cmd = Command::new("rr")
.arg("record")
.arg(bin)
.args(args)
.spawn()
.context("Failed to run rr")?;
let status = cmd.wait()?;
if !status.success() {
println!("cargo-rr: `rr record` exited with status {}", status);
}
println!("cargo-rr: Run `cargo rr replay` to replay your trace");
Ok(())
}
fn replay(trace: Option<String>, mut args: Vec<String>) -> anyhow::Result<()> {
ctrlc::set_handler(|| {})?;
insert_trailing_args(&mut args, vec!["--quiet".to_string()]);
if let Some(trace) = trace {
args.push(trace);
}
let mut cmd = Command::new("rr")
.arg("replay")
.args(&["-d", "rust-gdb"])
.args(args)
.spawn()
.context("Failed to run rr")?;
let status = cmd.wait()?;
if !status.success() {
return Err(anyhow!(
"cargo-rr: `rr replay` exited with status {}",
status
));
}
Ok(())
}
fn ls(args: &[String]) -> anyhow::Result<()> {
let mut cmd = Command::new("rr")
.arg("ls")
.arg("-t") .args(args)
.spawn()
.context("Failed to run rr")?;
let status = cmd.wait()?;
if !status.success() {
return Err(anyhow!("cargo-rr: `rr ls` exited with status {}", status));
}
Ok(())
}
fn insert_trailing_args(args: &mut Vec<String>, trailing: Vec<String>) {
if let Some(i) = args.iter().position(|a| a == "--") {
for (n, arg) in trailing.into_iter().enumerate() {
args.insert(i + 1 + n, arg);
}
} else {
args.push("--".to_string());
for arg in trailing {
args.push(arg);
}
}
}
fn select_artifact<'a>(meta: &Metadata, artifacts: &'a [Artifact]) -> anyhow::Result<&'a Artifact> {
let names: Vec<_> = artifacts.iter().map(|a| artifact_name(meta, a)).collect();
let selected = Select::new()
.with_prompt("Pick an artifact to run")
.items(&names)
.default(0)
.interact()?;
Ok(&artifacts[selected])
}
fn artifact_name(meta: &Metadata, artifact: &Artifact) -> String {
let src = &artifact.target.src_path;
let src = src.strip_prefix(&meta.workspace_root).unwrap_or(src);
src.to_string()
}