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#[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#[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#[derive(Default, Debug)]
85pub struct Expander {
86 dry: bool,
88 verbose: bool,
90 filename_base: String,
92 comment: Option<String>,
94 rustfmt: RustFmt,
96}
97
98impl Expander {
99 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 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 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 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 pub fn dry(mut self, dry: bool) -> Self {
150 self.dry = dry;
151 self
152 }
153
154 pub fn verbose(mut self, verbose: bool) -> Self {
156 self.verbose = verbose;
157 self
158 }
159
160 #[cfg(any(feature = "syndicate", test))]
161 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 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 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 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
209fn 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
220fn 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 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 if verbose {
260 eprintln!("expander: already in progress of writing identical content to {} by a different crate", dest.display());
261 }
262 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;