expander/
lib.rs

1use fs_err as fs;
2use proc_macro2::TokenStream;
3use quote::quote;
4use std::env;
5use std::io::Write;
6use std::path::Path;
7
8/// Rust edition to format for.
9#[derive(Debug, Clone, Copy)]
10pub enum Edition {
11    Unspecified,
12    _2015,
13    _2018,
14    _2021,
15}
16
17impl std::default::Default for Edition {
18    fn default() -> Self {
19        Self::Unspecified
20    }
21}
22
23impl std::fmt::Display for Edition {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        let s = match self {
26            Self::_2015 => "2015",
27            Self::_2018 => "2018",
28            Self::_2021 => "2021",
29            Self::Unspecified => "",
30        };
31        write!(f, "{}", s)
32    }
33}
34
35/// The channel to use for formatting.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum Channel {
38    #[default]
39    Default,
40    Stable,
41    Beta,
42    Nightly,
43}
44
45impl std::fmt::Display for Channel {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        let s = match self {
48            Self::Stable => "+stable",
49            Self::Beta => "+beta",
50            Self::Nightly => "+nightly",
51            Self::Default => return Ok(()),
52        };
53        write!(f, "{}", s)
54    }
55}
56
57#[derive(Debug, Clone)]
58enum RustFmt {
59    Yes {
60        edition: Edition,
61        channel: Channel,
62        allow_failure: bool,
63    },
64    No,
65}
66
67impl std::default::Default for RustFmt {
68    fn default() -> Self {
69        RustFmt::No
70    }
71}
72
73impl From<Edition> for RustFmt {
74    fn from(edition: Edition) -> Self {
75        RustFmt::Yes {
76            edition,
77            channel: Channel::Default,
78            allow_failure: false,
79        }
80    }
81}
82
83/// Expander to replace a tokenstream by a include to a file
84#[derive(Default, Debug)]
85pub struct Expander {
86    /// Determines if the whole file `include!` should be done (`false`) or not (`true`).
87    dry: bool,
88    /// If `true`, print the generated destination file to terminal.
89    verbose: bool,
90    /// Filename for the generated indirection file to be used.
91    filename_base: String,
92    /// Additional comment to be added.
93    comment: Option<String>,
94    /// Format using `rustfmt` in your path.
95    rustfmt: RustFmt,
96}
97
98impl Expander {
99    /// Create a new expander.
100    ///
101    /// The `filename_base` will be expanded to `{filename_base}-{digest}.rs` in order to dismabiguate
102    /// .
103    pub fn new(filename_base: impl AsRef<str>) -> Self {
104        Self {
105            dry: false,
106            verbose: false,
107            filename_base: filename_base.as_ref().to_owned(),
108            comment: None,
109            rustfmt: RustFmt::No,
110        }
111    }
112
113    /// Add a header comment.
114    pub fn add_comment(mut self, comment: impl Into<Option<String>>) -> Self {
115        self.comment = comment.into().map(|comment| format!("/* {} */\n", comment));
116        self
117    }
118
119    /// Format the resulting file, for readability.
120    pub fn fmt(mut self, edition: impl Into<Edition>) -> Self {
121        self.rustfmt = RustFmt::Yes {
122            edition: edition.into(),
123            channel: Channel::Default,
124            allow_failure: false,
125        };
126        self
127    }
128
129    /// Format the resulting file, for readability.
130    ///
131    /// Allows to specify `channel` and if a failure is fatal in addition.
132    ///
133    /// Note: Calling [`fn fmt(..)`] afterwards will override settings given.
134    pub fn fmt_full(
135        mut self,
136        channel: impl Into<Channel>,
137        edition: impl Into<Edition>,
138        allow_failure: bool,
139    ) -> Self {
140        self.rustfmt = RustFmt::Yes {
141            edition: edition.into(),
142            channel: channel.into(),
143            allow_failure,
144        };
145        self
146    }
147
148    /// Do not modify the provided tokenstream.
149    pub fn dry(mut self, dry: bool) -> Self {
150        self.dry = dry;
151        self
152    }
153
154    /// Print the path of the generated file to `stderr` during the proc-macro invocation.
155    pub fn verbose(mut self, verbose: bool) -> Self {
156        self.verbose = verbose;
157        self
158    }
159
160    #[cfg(any(feature = "syndicate", test))]
161    /// Create a file with `filename` under `env!("OUT_DIR")` if it's not an `Err(_)`.
162    pub fn maybe_write_to_out_dir(
163        self,
164        tokens: impl Into<Result<TokenStream, syn::Error>>,
165    ) -> Result<syn::Result<TokenStream>, std::io::Error> {
166        self.maybe_write_to(tokens, std::path::PathBuf::from(env!("OUT_DIR")).as_path())
167    }
168
169    /// Create a file with `filename` under `env!("OUT_DIR")`.
170    pub fn write_to_out_dir(self, tokens: TokenStream) -> Result<TokenStream, std::io::Error> {
171        let out = std::path::PathBuf::from(env!("OUT_DIR"));
172        self.write_to(tokens, out.as_path())
173    }
174
175    #[cfg(any(feature = "syndicate", test))]
176    /// Create a file with `filename` at `dest` if it's not an `Err(_)`.
177    pub fn maybe_write_to(
178        self,
179        maybe_tokens: impl Into<Result<TokenStream, syn::Error>>,
180        dest_dir: &Path,
181    ) -> Result<syn::Result<TokenStream>, std::io::Error> {
182        match maybe_tokens.into() {
183            Ok(tokens) => Ok(Ok(self.write_to(tokens, dest_dir)?)),
184            err => Ok(err),
185        }
186    }
187
188    /// Create a file with `self.filename` in  `dest_dir`.
189    pub fn write_to(
190        self,
191        tokens: TokenStream,
192        dest_dir: &Path,
193    ) -> Result<TokenStream, std::io::Error> {
194        if self.dry {
195            Ok(tokens)
196        } else {
197            expand_to_file(
198                tokens,
199                dest_dir.join(self.filename_base).as_path(),
200                dest_dir,
201                self.rustfmt,
202                self.comment,
203                self.verbose,
204            )
205        }
206    }
207}
208
209/// Take the leading 6 bytes and convert them to 12 hex ascii characters.
210fn make_suffix(digest: &[u8; 32]) -> String {
211    let mut shortened_hex = String::with_capacity(12);
212    const TABLE: &[u8] = b"0123456789abcdef";
213    for &byte in digest.iter().take(6) {
214        shortened_hex.push(TABLE[((byte >> 4) & 0x0F) as usize] as char);
215        shortened_hex.push(TABLE[((byte >> 0) & 0x0F) as usize] as char);
216    }
217    shortened_hex
218}
219
220/// Expand a proc-macro to file.
221///
222/// The current working directory `cwd` is only used for the `rustfmt` invocation
223/// and hence influences where the config files would be pulled in from.
224fn expand_to_file(
225    tokens: TokenStream,
226    dest: &Path,
227    cwd: &Path,
228    rustfmt: RustFmt,
229    comment: impl Into<Option<String>>,
230    verbose: bool,
231) -> Result<TokenStream, std::io::Error> {
232    let token_str = tokens.to_string();
233    #[cfg(feature = "pretty")]
234    let token_str = match syn::parse_file(&token_str) {
235        Err(e) => {
236            eprintln!("expander: failed to prettify {}: {:?}", dest.display(), e);
237            token_str
238        }
239        Ok(sf) => prettyplease::unparse(&sf),
240    };
241
242    // we need to disambiguate for transitive dependencies, that might create different output to not override one another
243    let mut bytes = token_str.as_bytes();
244    let hash = <blake2::Blake2s256 as blake2::Digest>::digest(bytes);
245    let shortened_hex = make_suffix(hash.as_ref());
246
247    let dest =
248        std::path::PathBuf::from(dest.display().to_string() + "-" + shortened_hex.as_str() + ".rs");
249
250    let mut f = fs::OpenOptions::new()
251        .write(true)
252        .create(true)
253        .truncate(true)
254        .open(dest.as_path())?;
255
256    let Ok(mut f) = file_guard::try_lock(f.file_mut(), file_guard::Lock::Exclusive, 0, 64) else {
257        // the digest of the file will not match if the content to be written differed, hence any existing lock
258        // means we are already writing the same content to the file
259        if verbose {
260            eprintln!("expander: already in progress of writing identical content to {} by a different crate", dest.display());
261        }
262        // now actually wait until the write is complete
263        let _lock = file_guard::lock(f.file_mut(), file_guard::Lock::Exclusive, 0, 64)
264            .expect("File Lock never fails us. qed");
265
266        if verbose {
267            eprintln!("expander: lock was release, referencing");
268        }
269
270        let dest = dest.display().to_string();
271        return Ok(quote! {
272            include!( #dest );
273        });
274    };
275
276    if verbose {
277        eprintln!("expander: writing {}", dest.display());
278    }
279
280    if let Some(comment) = comment.into() {
281        f.write_all(&mut comment.as_bytes())?;
282    }
283
284    f.write_all(&mut bytes)?;
285
286    if let RustFmt::Yes {
287        channel,
288        edition,
289        allow_failure,
290    } = rustfmt
291    {
292        let mut process = std::process::Command::new("rustfmt");
293        if Channel::Default != channel {
294            process.arg(channel.to_string());
295        }
296        process
297            .arg(format!("--edition={}", edition))
298            .arg(&dest)
299            .current_dir(cwd);
300
301        let res = process.status();
302        if allow_failure {
303            if let Err(err) = res {
304                eprintln!(
305                    "expander: failed to format file {} due to {}",
306                    dest.display(),
307                    err
308                );
309            }
310        } else {
311            res?;
312        }
313    }
314
315    let dest = dest.display().to_string();
316    Ok(quote! {
317        include!( #dest );
318    })
319}
320
321#[cfg(test)]
322mod tests;