tracing_build_script/lib.rs
1use std::{io, io::Write};
2use tracing::{Level, Metadata};
3use tracing_subscriber::fmt::MakeWriter;
4
5enum ErrorAndWarnState {
6 /// Initial state, no output has been written yet
7 /// cargo::warning= needs to be written next
8 Init,
9 /// Normal operation, cargo::warning= was already written
10 /// and user input did not end with newline
11 Normal,
12 /// Normal operation, cargo::warning= was already written
13 /// but user input ended with a newline or another char that needs escaping.
14 /// This either means the log message is done, or there just happens to be a newline at the end of this write
15 /// request
16 LastCharWasSpecial(u8),
17}
18
19fn char_is_special(ch: u8) -> bool {
20 ch == b'\n' || ch == b'\r'
21}
22
23fn escape_special(ch: u8) -> &'static [u8] {
24 match ch {
25 b'\n' => b"\\n",
26 b'\r' => b"\\r",
27 _ => unreachable!(),
28 }
29}
30
31enum BuildScriptWriterInner {
32 Informational(io::Stderr),
33 ErrorsAndWarnings {
34 state: ErrorAndWarnState,
35 writer: io::Stdout,
36 },
37}
38
39/// A writer intended to support the [output capturing of build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script).
40/// `BuildScriptWriter` can be used by [`tracing_subscriber::fmt::Subscriber`](tracing_subscriber::fmt::Subscriber) or [`tracing_subscriber::fmt::Layer`](tracing_subscriber::fmt::Layer)
41/// to enable capturing output in build scripts.
42pub struct BuildScriptWriter(BuildScriptWriterInner);
43
44impl BuildScriptWriter {
45 /// Create a writer for informational events.
46 /// Events will be written to stderr.
47 pub fn informational() -> Self {
48 Self(BuildScriptWriterInner::Informational(io::stderr()))
49 }
50
51 /// Create a writer for warning and error events.
52 /// Events will be written to stdout after having `cargo::warning=` prepended.
53 pub fn errors_and_warnings() -> Self {
54 Self(BuildScriptWriterInner::ErrorsAndWarnings { state: ErrorAndWarnState::Init, writer: io::stdout() })
55 }
56}
57
58impl Drop for BuildScriptWriter {
59 fn drop(&mut self) {
60 if let BuildScriptWriterInner::ErrorsAndWarnings { state: ErrorAndWarnState::LastCharWasSpecial(ch), writer } =
61 &mut self.0
62 {
63 let _ = writer.write(&[*ch]);
64 }
65 }
66}
67
68impl Write for BuildScriptWriter {
69 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
70 self.write_all(buf)?;
71 Ok(buf.len())
72 }
73
74 fn flush(&mut self) -> io::Result<()> {
75 match &mut self.0 {
76 BuildScriptWriterInner::Informational(writer) => writer.flush(),
77 BuildScriptWriterInner::ErrorsAndWarnings { writer, state: _ } => writer.flush(),
78 }
79 }
80
81 fn write_all(&mut self, mut buf: &[u8]) -> io::Result<()> {
82 match &mut self.0 {
83 BuildScriptWriterInner::Informational(writer) => writer.write_all(buf),
84 BuildScriptWriterInner::ErrorsAndWarnings { state, writer } => {
85 // We will need to issue multiple write calls to the writer (to avoid heap allocation)
86 // so we need to lock it to prevent other threads from clobbering our output.
87 let mut writer = writer.lock();
88
89 // depending on the current state we may need to prefix the output
90 match *state {
91 ErrorAndWarnState::Init => {
92 writer.write_all(b"cargo::warning=")?;
93 },
94 ErrorAndWarnState::LastCharWasSpecial(ch) => {
95 writer.write_all(escape_special(ch))?;
96 },
97 ErrorAndWarnState::Normal => {},
98 }
99
100 // If the last char is a newline we need to remember that but cannot immediately
101 // write it out. This is because we cannot know yet if its needs to be escaped, there are two cases:
102 //
103 // 1. this call to write is not actually the last call to write that will happen it just happens to end with a newline
104 // => we need to escape the newline
105 //
106 // 2. this call to write is actually the last call to write that will happen, and it ends with a newline
107 // => we need to keep the newline as is, because it is the newline terminator of the log message
108 // (tracing automatically appends a newline at the end of each message, like println!)
109 //
110 // Since we cannot decide which of these cases we are in at the moment, we need to delay writing the last character (if it is a newline) until we know that.
111 // We know which case we are in
112 // either when we enter this function the next time (case 1)
113 // or the next time or when we enter the destructor (case 2).
114 match buf.last().copied() {
115 Some(ch) if char_is_special(ch) => {
116 buf = &buf[..buf.len() - 1];
117 *state = ErrorAndWarnState::LastCharWasSpecial(ch);
118 },
119 _ => {
120 *state = ErrorAndWarnState::Normal;
121 },
122 }
123
124 let mut last_special_char = match buf.iter().position(|ch| char_is_special(*ch)) {
125 Some(pos) => {
126 writer.write_all(&buf[..pos])?;
127
128 let ret = buf[pos];
129 buf = &buf[pos + 1..];
130 ret
131 },
132 None => {
133 // fast path for messages without any special chars
134 writer.write_all(buf)?;
135 return Ok(());
136 },
137 };
138
139 loop {
140 writer.write_all(escape_special(last_special_char))?;
141
142 match buf.iter().position(|ch| char_is_special(*ch)) {
143 Some(pos) => {
144 writer.write_all(&buf[..pos])?;
145
146 last_special_char = buf[pos];
147 buf = &buf[pos + 1..];
148 },
149 None => {
150 writer.write_all(buf)?;
151 break;
152 },
153 }
154 }
155
156 Ok(())
157 },
158 }
159 }
160}
161
162/// [`MakeWriter`](tracing_subscriber::fmt::MakeWriter) implementation for [`BuildScriptWriter`](BuildScriptWriter)
163///
164/// # Behaviour
165/// Events for Levels Error and Warn are printed to stdout with [`cargo::warning=`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning) prepended.
166/// All other levels are sent to stderr, where they are only visible when running with verbose build output (`cargo build -vv`).
167///
168/// Note: this writer explicitly does **not** use the [`cargo::error=`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-error) instruction
169/// because it aborts the build with an error, which is not always desired.
170///
171/// # Example
172/// ```
173/// tracing_subscriber::fmt()
174/// .with_writer(tracing_build_script::BuildScriptMakeWriter)
175/// .init();
176/// ```
177pub struct BuildScriptMakeWriter;
178
179impl<'a> MakeWriter<'a> for BuildScriptMakeWriter {
180 type Writer = BuildScriptWriter;
181
182 fn make_writer(&'a self) -> Self::Writer {
183 BuildScriptWriter::informational()
184 }
185
186 fn make_writer_for(&'a self, meta: &Metadata) -> Self::Writer {
187 if meta.level() == &Level::ERROR || meta.level() == &Level::WARN {
188 BuildScriptWriter::errors_and_warnings()
189 } else {
190 BuildScriptWriter::informational()
191 }
192 }
193}