use anyhow::{anyhow, Context};
use buildline::adapters::{cargo::Cargo, ninja::Ninja, Adapter};
use buildline::appender::append_events;
use buildline::chrome_trace::{process_name_event, span_event};
use buildline::session::Session;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
if let Err(e) = run() {
eprintln!("buildline: {e:#}");
std::process::exit(2);
}
}
fn run() -> anyhow::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let dash_dash = args.iter().position(|a| a == "--").ok_or_else(|| {
anyhow!("usage: buildline -- <tool> [args...] (BUILDLINE_SESSION must be set)")
})?;
let mut tool_argv: Vec<String> = args[dash_dash + 1..].to_vec();
if tool_argv.is_empty() {
return Err(anyhow!("no command given after --"));
}
let session_env = std::env::var("BUILDLINE_SESSION")
.context("BUILDLINE_SESSION must be set to a trace file path, e.g. ./build.trace")?;
let trace_path = PathBuf::from(session_env);
let track = Path::new(&tool_argv[0])
.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| anyhow!("could not determine tool name from '{}'", tool_argv[0]))?
.to_string();
if let Some(extra) = maybe_inject_timings(&track, &tool_argv[1..]) {
tool_argv.extend(extra);
}
let mut session = Session::load_or_new(&trace_path)?;
let wall_clock_start_unix_us = now_unix_us();
let status = Command::new(&tool_argv[0])
.args(&tool_argv[1..])
.status()
.with_context(|| format!("launching '{}'", tool_argv[0]))?;
match capture(&track, &trace_path, &mut session, wall_clock_start_unix_us) {
Ok(n) => eprintln!(
"buildline: appended {n} span(s) for '{track}' to {}",
trace_path.display()
),
Err(e) => eprintln!("buildline: warning: could not capture timing for '{track}': {e:#}"),
}
std::process::exit(status.code().unwrap_or(1));
}
fn maybe_inject_timings(tool: &str, rest_args: &[String]) -> Option<Vec<String>> {
if tool != "cargo" {
return None;
}
let sub = rest_args.first()?;
let supports_timings = ["build", "check", "test", "bench"].contains(&sub.as_str());
let already_requested = rest_args
.iter()
.any(|a| a == "--timings" || a.starts_with("--timings="));
if supports_timings && !already_requested {
Some(vec!["--timings".to_string()])
} else {
None
}
}
fn capture(
track: &str,
trace_path: &Path,
session: &mut Session,
wall_clock_start_unix_us: i64,
) -> anyhow::Result<usize> {
let (adapter, artifact_path): (Box<dyn Adapter>, PathBuf) = match track {
"ninja" => (Box::new(Ninja), PathBuf::from(".ninja_log")),
"cargo" => (
Box::new(Cargo),
PathBuf::from("target/cargo-timings/cargo-timing.html"),
),
other => return Err(anyhow!("no adapter for tool '{other}' yet")),
};
let bytes = std::fs::read(&artifact_path).with_context(|| {
format!(
"reading {} (did the build actually run {track}?)",
artifact_path.display()
)
})?;
let mut spans = adapter.parse(&bytes)?;
session.record_start(track, wall_clock_start_unix_us);
let is_new_track = session.is_new_track(track);
let pid = session.pid_for(track);
session.place(&mut spans);
let mut events = Vec::with_capacity(spans.len() + 1);
if is_new_track {
events.push(process_name_event(track, pid));
}
events.extend(spans.iter().map(|s| span_event(s, pid)));
append_events(trace_path, &events)?;
session.save(trace_path)?;
Ok(spans.len())
}
fn now_unix_us() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before Unix epoch")
.as_micros() as i64
}