1use 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
19pub struct BuildLogger {
21 tx: ManuallyDrop<Sender<LogMessage>>,
22 run_id: RunId,
23 handle: Option<JoinHandle<()>>,
24}
25
26impl BuildLogger {
27 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 pub fn generate_run_id(ws: &Workspace<'_>) -> RunId {
75 RunId::new(&ws.root())
76 }
77
78 pub fn run_id(&self) -> &RunId {
80 &self.run_id
81 }
82
83 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 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#[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 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}