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}