xct2cli 0.1.0

Library and CLI for transforming Xcode Instruments .trace bundles (hotspots, callgraphs, annotated disassembly, PMI counters, heap allocations). Apple Silicon.
Documentation
//! Subprocess wrapper around the `xctrace` CLI.

use std::ffi::OsStr;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;

use camino::Utf8Path;

use crate::error::Error;
use crate::error::Result;

pub const DEFAULT_XCTRACE: &str = "/usr/bin/xctrace";

#[derive(Debug, Clone)]
pub struct Xctrace {
    binary: PathBuf,
}

impl Default for Xctrace {
    fn default() -> Self {
        Self {
            binary: PathBuf::from(DEFAULT_XCTRACE),
        }
    }
}

impl Xctrace {
    pub fn at(path: impl Into<PathBuf>) -> Self {
        Self {
            binary: path.into(),
        }
    }

    /// Resolve via `$XCTRACE_BIN`, falling back to `/usr/bin/xctrace`.
    pub fn discover() -> Self {
        if let Ok(env_path) = std::env::var("XCTRACE_BIN") {
            return Self::at(env_path);
        }
        Self::default()
    }

    pub fn export_toc(&self, trace: &Path) -> Result<Vec<u8>> {
        self.run(
            "export",
            &[
                OsStr::new("--input"),
                trace.as_os_str(),
                OsStr::new("--toc"),
            ],
        )
    }

    pub fn export_xpath(&self, trace: &Path, xpath: &str) -> Result<Vec<u8>> {
        self.run(
            "export",
            &[
                OsStr::new("--input"),
                trace.as_os_str(),
                OsStr::new("--xpath"),
                OsStr::new(xpath),
            ],
        )
    }

    pub fn record_launch(
        &self,
        template: &str,
        output_trace: &Utf8Path,
        target: &Path,
        target_args: &[OsString],
        extra_env: &[(String, String)],
    ) -> Result<()> {
        let mut args: Vec<OsString> = vec![
            "record".into(),
            "--template".into(),
            template.into(),
            "--output".into(),
            output_trace.as_os_str().into(),
            "--no-prompt".into(),
        ];
        for (k, v) in extra_env {
            args.push("--env".into());
            args.push(format!("{k}={v}").into());
        }
        args.push("--launch".into());
        args.push("--".into());
        args.push(target.as_os_str().into());
        for a in target_args {
            args.push(a.clone());
        }
        let _ = self.run_args("record", &args)?;
        Ok(())
    }

    fn run(&self, sub: &'static str, tail: &[&OsStr]) -> Result<Vec<u8>> {
        let mut args: Vec<OsString> = Vec::with_capacity(tail.len() + 1);
        args.push(sub.into());
        for t in tail {
            args.push((*t).to_owned());
        }
        self.run_args(sub, &args)
    }

    fn run_args(&self, sub: &'static str, args: &[OsString]) -> Result<Vec<u8>> {
        let mut cmd = Command::new(&self.binary);
        cmd.args(args);
        cmd.stdout(Stdio::piped());
        cmd.stderr(Stdio::piped());
        tracing::debug!(?args, binary = ?self.binary, "spawning xctrace");
        let out = cmd.output().map_err(|e| match e.kind() {
            std::io::ErrorKind::NotFound => Error::XctraceMissing(self.binary.clone()),
            _ => Error::Io(e),
        })?;
        if !out.status.success() {
            return Err(Error::XctraceFailed {
                subcommand: sub,
                status: out.status,
                stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
            });
        }
        Ok(out.stdout)
    }
}