#
cargo/util/
logger.rs

    1//! Build analysis logging infrastructure.
    2
    3use std::hash::Hash;
    4use std::io::{BufWriter, Write};
    5use std::mem::ManuallyDrop;
    6use std::path::Path;
    7use std::sync::mpsc;
    8use std::sync::mpsc::Sender;
    9use std::thread::JoinHandle;
    10
    11use anyhow::Context as _;
    12use cargo_util::paths;
    13
    14use crate::CargoResult;
    15use crate::core::Workspace;
    16use crate::util::log_message::LogMessage;
    17use crate::util::short_hash;
    18
    19/// Logger for `-Zbuild-analysis`.
    20pub struct BuildLogger {
    21    tx: ManuallyDrop<Sender<LogMessage>>,
    22    run_id: RunId,
    23    handle: Option<JoinHandle<()>>,
    24}
    25
    26impl BuildLogger {
    27    /// Creates a logger if `-Zbuild-analysis` is enabled.
    28    pub fn maybe_new(ws: &Workspace<'_>) -> CargoResult<Option<Self>> {
    29        let analysis = ws.gctx().build_config()?.analysis.as_ref();
    30        match (analysis, ws.gctx().cli_unstable().build_analysis) {
    31            (Some(analysis), true) if analysis.enabled => Ok(Some(Self::new(ws)?)),
    32            (Some(_), false) => {
    33                ws.gctx().shell().warn(
    34                    "ignoring 'build.analysis' config, pass `-Zbuild-analysis` to enable it",
    35                )?;
    36                Ok(None)
    37            }
    38            _ => Ok(None),
    39        }
    40    }
    41
    42    fn new(ws: &Workspace<'_>) -> CargoResult<Self> {
    43        let run_id = Self::generate_run_id(ws);
    44
    45        let log_dir = ws.gctx().home().join("log");
    46        paths::create_dir_all(log_dir.as_path_unlocked())?;
    47
    48        let filename = format!("{run_id}.jsonl");
    49        let log_file = log_dir.open_rw_exclusive_create(
    50            Path::new(&filename),
    51            ws.gctx(),
    52            "build analysis log",
    53        )?;
    54
    55        let (tx, rx) = mpsc::channel::<LogMessage>();
    56
    57        let run_id_str = run_id.to_string();
    58        let handle = std::thread::spawn(move || {
    59            let mut writer = BufWriter::new(log_file);
    60            for msg in rx {
    61                let _ = msg.write_json_log(&mut writer, &run_id_str);
    62            }
    63            let _ = writer.flush();
    64        });
    65
    66        Ok(Self {
    67            tx: ManuallyDrop::new(tx),
    68            run_id,
    69            handle: Some(handle),
    70        })
    71    }
    72
    73    /// Generates a unique run ID.
    74    pub fn generate_run_id(ws: &Workspace<'_>) -> RunId {
    75        RunId::new(&ws.root())
    76    }
    77
    78    /// Returns the run ID for this build session.
    79    pub fn run_id(&self) -> &RunId {
    80        &self.run_id
    81    }
    82
    83    /// Logs a message.
    84    pub fn log(&self, msg: LogMessage) {
    85        let _ = self.tx.send(msg);
    86    }
    87}
    88
    89impl Drop for BuildLogger {
    90    fn drop(&mut self) {
    91        // SAFETY: tx is dropped exactly once here to signal thread shutdown.
    92        // ManuallyDrop prevents automatic drop after this impl runs.
    93        unsafe {
    94            ManuallyDrop::drop(&mut self.tx);
    95        }
    96
    97        if let Some(handle) = self.handle.take() {
    98            let _ = handle.join();
    99        }
    100    }
    101}
    102
    103/// A unique identifier for a Cargo invocation.
    104#[derive(Clone)]
    105pub struct RunId {
    106    timestamp: jiff::Timestamp,
    107    hash: String,
    108}
    109
    110impl RunId {
    111    const FORMAT: &str = "%Y%m%dT%H%M%S%3fZ";
    112
    113    pub fn new<H: Hash>(h: &H) -> RunId {
    114        RunId {
    115            timestamp: jiff::Timestamp::now(),
    116            hash: short_hash(h),
    117        }
    118    }
    119
    120    pub fn timestamp(&self) -> &jiff::Timestamp {
    121        &self.timestamp
    122    }
    123
    124    /// Checks whether ID was generated from the same workspace.
    125    pub fn same_workspace(&self, other: &RunId) -> bool {
    126        self.hash == other.hash
    127    }
    128}
    129
    130impl std::fmt::Display for RunId {
    131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    132        let hash = &self.hash;
    133        let timestamp = self.timestamp.strftime(Self::FORMAT);
    134        write!(f, "{timestamp}-{hash}")
    135    }
    136}
    137
    138impl std::str::FromStr for RunId {
    139    type Err = anyhow::Error;
    140
    141    fn from_str(s: &str) -> Result<Self, Self::Err> {
    142        let msg =
    143            || format!("expect run ID in format `20060724T012128000Z-<16-char-hex>`, got `{s}`");
    144        let Some((timestamp, hash)) = s.rsplit_once('-') else {
    145            anyhow::bail!(msg());
    146        };
    147
    148        if hash.len() != 16 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
    149            anyhow::bail!(msg());
    150        }
    151        let timestamp = jiff::civil::DateTime::strptime(Self::FORMAT, timestamp)
    152            .and_then(|dt| dt.to_zoned(jiff::tz::TimeZone::UTC))
    153            .map(|zoned| zoned.timestamp())
    154            .with_context(msg)?;
    155
    156        Ok(RunId {
    157            timestamp,
    158            hash: hash.into(),
    159        })
    160    }
    161}
    162
    163#[cfg(test)]
    164mod tests {
    165    use super::*;
    166
    167    #[test]
    168    fn run_id_round_trip() {
    169        let id = "20060724T012128000Z-b0fd440798ab3cfb";
    170        assert_eq!(id, &id.parse::<RunId>().unwrap().to_string());
    171    }
    172}