1use std::cell::RefCell;
4use std::hash::Hash;
5use std::io::{BufWriter, Write};
6use std::mem::ManuallyDrop;
7use std::path::Path;
8use std::sync::mpsc::{self, Sender};
9use std::thread::JoinHandle;
10
11use anyhow::Context as _;
12use cargo_util::paths;
13
14use crate::CargoResult;
15use crate::core::Workspace;
16use crate::core::compiler::BuildConfig;
17use crate::util::log_message::LogMessage;
18use crate::util::short_hash;
19
20struct FileLogger {
22 tx: ManuallyDrop<Sender<LogMessage>>,
23 handle: Option<JoinHandle<()>>,
24}
25
26impl FileLogger {
27 fn maybe_new(ws: &Workspace<'_>, run_id: &RunId) -> CargoResult<Option<FileLogger>> {
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 => {
32 let log_dir = ws.gctx().home().join("log");
33 paths::create_dir_all(log_dir.as_path_unlocked())?;
34
35 let filename = format!("{run_id}.jsonl");
36 let log_file = log_dir.open_rw_exclusive_create(
37 Path::new(&filename),
38 ws.gctx(),
39 "build analysis log",
40 )?;
41
42 let (tx, rx) = mpsc::channel::<LogMessage>();
43
44 let run_id_str = run_id.to_string();
45 let handle = std::thread::spawn(move || {
46 let mut writer = BufWriter::new(log_file);
47 for msg in rx {
48 let _ = msg.write_json_log(&mut writer, &run_id_str);
49 }
50 let _ = writer.flush();
51 });
52
53 Ok(Some(Self {
54 tx: ManuallyDrop::new(tx),
55 handle: Some(handle),
56 }))
57 }
58 (Some(_), false) => {
59 ws.gctx().shell().warn(
60 "ignoring 'build.analysis' config, pass `-Zbuild-analysis` to enable it",
61 )?;
62 Ok(None)
63 }
64 _ => Ok(None),
65 }
66 }
67}
68
69impl Drop for FileLogger {
70 fn drop(&mut self) {
71 unsafe {
74 ManuallyDrop::drop(&mut self.tx);
75 }
76
77 if let Some(handle) = self.handle.take() {
78 let _ = handle.join();
79 }
80 }
81}
82
83struct InMemoryLogger {
85 logs: RefCell<Vec<LogMessage>>,
87}
88
89impl InMemoryLogger {
90 fn maybe_new(options: &BuildConfig) -> Option<Self> {
91 if options.timing_report {
92 Some(Self {
93 logs: RefCell::new(Vec::new()),
94 })
95 } else {
96 None
97 }
98 }
99}
100
101pub struct BuildLogger {
103 run_id: RunId,
104 file_logger: Option<FileLogger>,
105 in_memory_logger: Option<InMemoryLogger>,
106}
107
108impl BuildLogger {
109 pub fn maybe_new(ws: &Workspace<'_>, options: &BuildConfig) -> CargoResult<Option<Self>> {
110 let run_id = Self::generate_run_id(ws);
111 let file_logger = FileLogger::maybe_new(ws, &run_id)?;
112 let in_memory_logger = InMemoryLogger::maybe_new(options);
113
114 if file_logger.is_none() && in_memory_logger.is_none() {
115 return Ok(None);
116 }
117
118 Ok(Some(Self {
119 run_id,
120 file_logger,
121 in_memory_logger,
122 }))
123 }
124
125 pub fn generate_run_id(ws: &Workspace<'_>) -> RunId {
127 RunId::new(&ws.root())
128 }
129
130 pub fn run_id(&self) -> &RunId {
132 &self.run_id
133 }
134
135 pub fn log(&self, msg: LogMessage) {
137 if let Some(ref logger) = self.in_memory_logger {
138 let mut borrowed = logger.logs.try_borrow_mut().expect(
139 "Unable to get a mutable reference to in-memory logger; please file a bug report",
140 );
141 borrowed.push(msg.clone());
142 };
143
144 if let Some(ref logger) = self.file_logger {
145 let _ = logger.tx.send(msg);
146 };
147 }
148
149 pub fn get_logs(&self) -> Option<Vec<LogMessage>> {
150 self.in_memory_logger.as_ref().map(|l| {
151 l.logs
152 .try_borrow()
153 .expect("Unable to get a reference to in-memory logger; please file a bug report")
154 .clone()
155 })
156 }
157}
158
159#[derive(Clone)]
161pub struct RunId {
162 timestamp: jiff::Timestamp,
163 hash: String,
164}
165
166impl RunId {
167 const FORMAT: &str = "%Y%m%dT%H%M%S%3fZ";
168
169 pub fn new<H: Hash>(h: &H) -> RunId {
170 RunId {
171 timestamp: jiff::Timestamp::now(),
172 hash: short_hash(h),
173 }
174 }
175
176 pub fn timestamp(&self) -> &jiff::Timestamp {
177 &self.timestamp
178 }
179
180 pub fn same_workspace(&self, other: &RunId) -> bool {
182 self.hash == other.hash
183 }
184}
185
186impl std::fmt::Display for RunId {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 let hash = &self.hash;
189 let timestamp = self.timestamp.strftime(Self::FORMAT);
190 write!(f, "{timestamp}-{hash}")
191 }
192}
193
194impl std::str::FromStr for RunId {
195 type Err = anyhow::Error;
196
197 fn from_str(s: &str) -> Result<Self, Self::Err> {
198 let msg =
199 || format!("expect run ID in format `20060724T012128000Z-<16-char-hex>`, got `{s}`");
200 let Some((timestamp, hash)) = s.rsplit_once('-') else {
201 anyhow::bail!(msg());
202 };
203
204 if hash.len() != 16 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
205 anyhow::bail!(msg());
206 }
207 let timestamp = jiff::civil::DateTime::strptime(Self::FORMAT, timestamp)
208 .and_then(|dt| dt.to_zoned(jiff::tz::TimeZone::UTC))
209 .map(|zoned| zoned.timestamp())
210 .with_context(msg)?;
211
212 Ok(RunId {
213 timestamp,
214 hash: hash.into(),
215 })
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn run_id_round_trip() {
225 let id = "20060724T012128000Z-b0fd440798ab3cfb";
226 assert_eq!(id, &id.parse::<RunId>().unwrap().to_string());
227 }
228}