postcompile/
lib.rs

1//! A crate which allows you to compile Rust code at runtime (hence the name
2//! `postcompile`).
3//!
4//! What that means is that you can provide the input to `rustc` and then get
5//! back the expanded output, compiler errors, warnings, etc.
6//!
7//! This is particularly useful when making snapshot tests of proc-macros, look
8//! below for an example with the `insta` crate.
9//!
10//! ## Usage
11//!
12//! ```rs
13//! #[test]
14//! fn some_cool_test() {
15//!     insta::assert_snapshot!(postcompile::compile! {
16//!         #![allow(unused)]
17//!
18//!         #[derive(Debug, Clone)]
19//!         struct Test {
20//!             a: u32,
21//!             b: i32,
22//!         }
23//!
24//!         const TEST: Test = Test { a: 1, b: 3 };
25//!     });
26//! }
27//!
28//! #[test]
29//! fn some_cool_test_extern() {
30//!     insta::assert_snapshot!(postcompile::compile_str!(include_str!("some_file.rs")));
31//! }
32//! ```
33//!
34//! ## Features
35//!
36//! - Cached builds: This crate reuses the cargo build cache of the original
37//!   crate so that only the contents of the macro are compiled & not any
38//!   additional dependencies.
39//! - Coverage: This crate works with [`cargo-llvm-cov`](https://crates.io/crates/cargo-llvm-cov)
40//!   out of the box, which allows you to instrument the proc-macro expansion.
41//!
42//! ## Alternatives
43//!
44//! - [`compiletest_rs`](https://crates.io/crates/compiletest_rs): This crate is
45//!   used by the Rust compiler team to test the compiler itself. Not really
46//!   useful for proc-macros.
47//! - [`trybuild`](https://crates.io/crates/trybuild): This crate is an
48//!   all-in-one solution for testing proc-macros, with built in snapshot
49//!   testing.
50//! - [`ui_test`](https://crates.io/crates/ui_test): Similar to `trybuild` with
51//!   a slightly different API & used by the Rust compiler team to test the
52//!   compiler itself.
53//!
54//! ### Differences
55//!
56//! The other libraries are focused on testing & have built in test harnesses.
57//! This crate takes a step back and allows you to compile without a testing
58//! harness. This has the advantage of being more flexible, and allows you to
59//! use whatever testing framework you want.
60//!
61//! In the examples above I showcase how to use this crate with the `insta`
62//! crate for snapshot testing.
63//!
64//! ## Status
65//!
66//! This crate is currently under development and is not yet stable.
67//!
68//! Unit tests are not yet fully implemented. Use at your own risk.
69//!
70//! ## Limitations
71//!
72//! Please note that this crate does not work inside a running compiler process
73//! (inside a proc-macro) without hacky workarounds and complete build-cache
74//! invalidation.
75//!
76//! This is because `cargo` holds a lock on the build directory and that if we
77//! were to compile inside a proc-macro we would recursively invoke the
78//! compiler.
79//!
80//! ## License
81//!
82//! This project is licensed under the [MIT](./LICENSE.MIT) or
83//! [Apache-2.0](./LICENSE.Apache-2.0) license. You can choose between one of
84//! them if you use this work.
85//!
86//! `SPDX-License-Identifier: MIT OR Apache-2.0`
87#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
88#![deny(missing_docs)]
89#![deny(unreachable_pub)]
90#![deny(clippy::undocumented_unsafe_blocks)]
91#![deny(clippy::multiple_unsafe_ops_per_block)]
92
93use std::borrow::Cow;
94use std::ffi::OsStr;
95use std::path::Path;
96use std::process::Command;
97
98use deps::{Dependencies, Errored};
99
100mod deps;
101mod features;
102
103/// The return status of the compilation.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum ExitStatus {
106    /// If the compiler returned a 0 exit code.
107    Success,
108    /// If the compiler returned a non-0 exit code.
109    Failure(i32),
110}
111
112impl std::fmt::Display for ExitStatus {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        match self {
115            ExitStatus::Success => write!(f, "0"),
116            ExitStatus::Failure(code) => write!(f, "{code}"),
117        }
118    }
119}
120
121/// The output of the compilation.
122#[derive(Debug)]
123pub struct CompileOutput {
124    /// The status of the compilation.
125    pub status: ExitStatus,
126    /// The stdout of the compilation.
127    /// This will contain the expanded code.
128    pub stdout: String,
129    /// The stderr of the compilation.
130    /// This will contain any errors or warnings from the compiler.
131    pub stderr: String,
132}
133
134impl std::fmt::Display for CompileOutput {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        writeln!(f, "exit status: {}", self.status)?;
137        if !self.stderr.is_empty() {
138            write!(f, "--- stderr \n{}\n", self.stderr)?;
139        }
140        if !self.stdout.is_empty() {
141            write!(f, "--- stdout \n{}\n", self.stdout)?;
142        }
143        Ok(())
144    }
145}
146
147fn rustc(config: &Config, tmp_file: &Path) -> Command {
148    let mut program = Command::new(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()));
149    program.env("RUSTC_BOOTSTRAP", "1");
150    let rust_flags = std::env::var_os("RUSTFLAGS");
151
152    if let Some(rust_flags) = &rust_flags {
153        program.args(
154            rust_flags
155                .as_encoded_bytes()
156                .split(|&b| b == b' ')
157                // Safety: The bytes are already encoded (we call OsString::as_encoded_bytes above)
158                .map(|flag| unsafe { OsStr::from_encoded_bytes_unchecked(flag) }),
159        );
160    }
161
162    program.arg("--crate-name");
163    program.arg(config.function_name.split("::").last().unwrap_or("unnamed"));
164    program.arg(tmp_file);
165    program.envs(std::env::vars());
166
167    program.stderr(std::process::Stdio::piped());
168    program.stdout(std::process::Stdio::piped());
169
170    program
171}
172
173fn write_tmp_file(tokens: &str, tmp_file: &Path) {
174    std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
175
176    #[cfg(feature = "prettyplease")]
177    {
178        if let Ok(syn_file) = syn::parse_file(tokens) {
179            let pretty_file = prettyplease::unparse(&syn_file);
180            std::fs::write(tmp_file, pretty_file).unwrap();
181            return;
182        }
183    }
184
185    std::fs::write(tmp_file, tokens).unwrap();
186}
187
188/// Compiles the given tokens and returns the output.
189pub fn compile_custom(tokens: &str, config: &Config) -> Result<CompileOutput, Errored> {
190    let dependencies = Dependencies::new(config)?;
191
192    let tmp_file = Path::new(config.tmp_dir.as_ref()).join(format!("{}.rs", config.function_name.replace("::", "____")));
193    write_tmp_file(tokens, &tmp_file);
194
195    let mut program = rustc(config, &tmp_file);
196
197    dependencies.apply(&mut program);
198    // The first invoke is used to get the macro expanded code.
199    program.arg("-Zunpretty=expanded");
200
201    let output = program.output().unwrap();
202
203    let stdout = String::from_utf8(output.stdout).unwrap();
204    let syn_file = syn::parse_file(&stdout);
205    #[cfg(feature = "prettyplease")]
206    let stdout = syn_file.as_ref().map(prettyplease::unparse).unwrap_or(stdout);
207
208    let mut crate_type = "lib";
209
210    if let Ok(file) = syn_file {
211        if file.items.iter().any(|item| {
212            let syn::Item::Fn(func) = item else {
213                return false;
214            };
215
216            func.sig.ident == "main"
217        }) {
218            crate_type = "bin";
219        }
220    };
221
222    let mut status = if output.status.success() {
223        ExitStatus::Success
224    } else {
225        ExitStatus::Failure(output.status.code().unwrap_or(-1))
226    };
227
228    let stderr = if status == ExitStatus::Success {
229        let mut program = rustc(config, &tmp_file);
230        dependencies.apply(&mut program);
231        program.arg("--emit=llvm-ir");
232        program.arg(format!("--crate-type={crate_type}"));
233        program.arg("-o");
234        program.arg("-");
235        let comp_output = program.output().unwrap();
236        status = if comp_output.status.success() {
237            ExitStatus::Success
238        } else {
239            ExitStatus::Failure(comp_output.status.code().unwrap_or(-1))
240        };
241        String::from_utf8(comp_output.stderr).unwrap()
242    } else {
243        String::from_utf8(output.stderr).unwrap()
244    };
245
246    let stderr = stderr.replace(tmp_file.as_os_str().to_string_lossy().as_ref(), "<postcompile>");
247    let stdout = stdout.replace(tmp_file.as_os_str().to_string_lossy().as_ref(), "<postcompile>");
248
249    Ok(CompileOutput { status, stdout, stderr })
250}
251
252/// The configuration for the compilation.
253#[derive(Clone, Debug)]
254pub struct Config {
255    /// The path to the cargo manifest file of the library being tested.
256    /// This is so that we can include the `dependencies` & `dev-dependencies`
257    /// making them available in the code provided.
258    pub manifest: Cow<'static, Path>,
259    /// The path to the target directory, used to cache builds & find
260    /// dependencies.
261    pub target_dir: Cow<'static, Path>,
262    /// A temporary directory to write the expanded code to.
263    pub tmp_dir: Cow<'static, Path>,
264    /// The name of the function to compile.
265    pub function_name: Cow<'static, str>,
266    /// The path to the file being compiled.
267    pub file_path: Cow<'static, Path>,
268    /// The name of the package being compiled.
269    pub package_name: Cow<'static, str>,
270}
271
272#[macro_export]
273#[doc(hidden)]
274macro_rules! _function_name {
275    () => {{
276        fn f() {}
277        fn type_name_of_val<T>(_: T) -> &'static str {
278            std::any::type_name::<T>()
279        }
280        let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
281        while let Some(rest) = name.strip_suffix("::{{closure}}") {
282            name = rest;
283        }
284        name
285    }};
286}
287
288#[doc(hidden)]
289pub fn build_dir() -> &'static Path {
290    Path::new(env!("OUT_DIR"))
291}
292
293#[doc(hidden)]
294pub fn target_dir() -> &'static Path {
295    build_dir()
296        .parent()
297        .unwrap()
298        .parent()
299        .unwrap()
300        .parent()
301        .unwrap()
302        .parent()
303        .unwrap()
304}
305
306#[macro_export]
307#[doc(hidden)]
308macro_rules! _config {
309    () => {{
310        $crate::Config {
311            manifest: ::std::borrow::Cow::Borrowed(::std::path::Path::new(env!("CARGO_MANIFEST_PATH"))),
312            tmp_dir: ::std::borrow::Cow::Borrowed($crate::build_dir()),
313            target_dir: ::std::borrow::Cow::Borrowed($crate::target_dir()),
314            function_name: ::std::borrow::Cow::Borrowed($crate::_function_name!()),
315            file_path: ::std::borrow::Cow::Borrowed(::std::path::Path::new(file!())),
316            package_name: ::std::borrow::Cow::Borrowed(env!("CARGO_PKG_NAME")),
317        }
318    }};
319}
320
321/// Compiles the given tokens and returns the output.
322///
323/// This macro will panic if we fail to invoke the compiler.
324///
325/// ```rs
326/// // Dummy macro to assert the snapshot.
327/// macro_rules! assert_snapshot {
328///     ($expr:expr) => {};
329/// }
330///
331/// let output = postcompile::compile! {
332///     const TEST: u32 = 1;
333/// };
334///
335/// assert_eq!(output.status, postcompile::ExitStatus::Success);
336/// assert!(output.stderr.is_empty());
337/// assert_snapshot!(output.stdout); // We dont have an assert_snapshot! macro in this crate, but you get the idea.
338/// ```
339#[macro_export]
340macro_rules! compile {
341    ($($tokens:tt)*) => {
342        $crate::compile_str!(stringify!($($tokens)*))
343    };
344}
345
346/// Compiles the given string of tokens and returns the output.
347///
348/// This macro will panic if we fail to invoke the compiler.
349///
350/// Same as the [`compile!`] macro, but for strings. This allows you to do:
351///
352/// ```rs
353/// let output = postcompile::compile_str!(include_str!("some_file.rs"));
354///
355/// // ... do something with the output
356/// ```
357#[macro_export]
358macro_rules! compile_str {
359    ($expr:expr) => {
360        $crate::try_compile_str!($expr).expect("failed to compile")
361    };
362}
363
364/// Compiles the given string of tokens and returns the output.
365///
366/// This macro will return an error if we fail to invoke the compiler. Unlike
367/// the [`compile!`] macro, this will not panic.
368///
369/// ```rs
370/// let output = postcompile::try_compile! {
371///     const TEST: u32 = 1;
372/// };
373///
374/// assert!(output.is_ok());
375/// assert_eq!(output.unwrap().status, postcompile::ExitStatus::Success);
376/// ```
377#[macro_export]
378macro_rules! try_compile {
379    ($($tokens:tt)*) => {
380        $crate::try_compile_str!(stringify!($($tokens)*))
381    };
382}
383
384/// Compiles the given string of tokens and returns the output.
385///
386/// This macro will return an error if we fail to invoke the compiler.
387///
388/// Same as the [`try_compile!`] macro, but for strings similar usage to
389/// [`compile_str!`].
390#[macro_export]
391macro_rules! try_compile_str {
392    ($expr:expr) => {
393        $crate::compile_custom($expr, &$crate::_config!())
394    };
395}
396
397#[cfg(test)]
398#[cfg_attr(all(test, coverage_nightly), coverage(off))]
399mod tests {
400    use insta::assert_snapshot;
401
402    use crate::ExitStatus;
403
404    #[test]
405    fn compile_success() {
406        let out = compile! {
407            #[allow(unused)]
408            fn main() {
409                let a = 1;
410                let b = 2;
411                let c = a + b;
412            }
413        };
414
415        dbg!(&out);
416
417        assert_eq!(out.status, ExitStatus::Success);
418        assert!(out.stderr.is_empty());
419        assert_snapshot!(out);
420    }
421
422    #[test]
423    fn try_compile_success() {
424        let out = try_compile! {
425            #[allow(unused)]
426            fn main() {
427                let xd = 0xd;
428                let xdd = 0xdd;
429                let xddd = xd + xdd;
430                println!("{}", xddd);
431            }
432        };
433
434        assert!(out.is_ok());
435        let out = out.unwrap();
436        dbg!(&out);
437        assert_eq!(out.status, ExitStatus::Success);
438        assert!(out.stderr.is_empty());
439        assert!(!out.stdout.is_empty());
440    }
441
442    #[test]
443    fn compile_failure() {
444        let out = compile! {
445            invalid_rust_code
446        };
447
448        assert_eq!(out.status, ExitStatus::Failure(1));
449        assert!(out.stdout.is_empty());
450        assert_snapshot!(out);
451    }
452
453    #[test]
454    fn try_compile_failure() {
455        let out = try_compile! {
456            invalid rust code
457        };
458
459        assert!(out.is_ok());
460        let out = out.unwrap();
461        assert_eq!(out.status, ExitStatus::Failure(1));
462        assert!(out.stdout.is_empty());
463        assert!(!out.stderr.is_empty());
464    }
465}