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
89use std::borrow::Cow;
90use std::ffi::{OsStr, OsString};
91use std::os::unix::ffi::OsStrExt;
92use std::path::Path;
93use std::process::Command;
94
95use deps::{Dependencies, Errored};
96
97mod deps;
98mod features;
99
100/// The return status of the compilation.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum ExitStatus {
103    /// If the compiler returned a 0 exit code.
104    Success,
105    /// If the compiler returned a non-0 exit code.
106    Failure(i32),
107}
108
109impl std::fmt::Display for ExitStatus {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            ExitStatus::Success => write!(f, "0"),
113            ExitStatus::Failure(code) => write!(f, "{}", code),
114        }
115    }
116}
117
118/// The output of the compilation.
119#[derive(Debug)]
120pub struct CompileOutput {
121    /// The status of the compilation.
122    pub status: ExitStatus,
123    /// The stdout of the compilation.
124    /// This will contain the expanded code.
125    pub stdout: String,
126    /// The stderr of the compilation.
127    /// This will contain any errors or warnings from the compiler.
128    pub stderr: String,
129}
130
131impl std::fmt::Display for CompileOutput {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        writeln!(f, "exit status: {}", self.status)?;
134        if !self.stderr.is_empty() {
135            write!(f, "--- stderr \n{}\n", self.stderr)?;
136        }
137        if !self.stdout.is_empty() {
138            write!(f, "--- stdout \n{}\n", self.stdout)?;
139        }
140        Ok(())
141    }
142}
143
144fn rustc(config: &Config, tmp_file: &Path) -> Command {
145    let mut program = Command::new(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()));
146    program.env("RUSTC_BOOTSTRAP", "1");
147    let rust_flags = std::env::var_os("RUSTFLAGS");
148
149    if let Some(rust_flags) = &rust_flags {
150        program.args(
151            rust_flags
152                .as_encoded_bytes()
153                .split(|&b| b == b' ')
154                .map(|flag| OsString::from(OsStr::from_bytes(flag))),
155        );
156    }
157
158    program.arg("--crate-name");
159    program.arg(config.function_name.split("::").last().unwrap_or("unnamed"));
160    program.arg(tmp_file);
161    program.envs(std::env::vars());
162
163    program.stderr(std::process::Stdio::piped());
164    program.stdout(std::process::Stdio::piped());
165
166    program
167}
168
169fn write_tmp_file(tokens: &str, tmp_file: &Path) {
170    #[cfg(feature = "prettyplease")]
171    {
172        if let Ok(syn_file) = syn::parse_file(tokens) {
173            let pretty_file = prettyplease::unparse(&syn_file);
174            std::fs::write(tmp_file, pretty_file).unwrap();
175            return;
176        }
177    }
178
179    std::fs::write(tmp_file, tokens).unwrap();
180}
181
182/// Compiles the given tokens and returns the output.
183pub fn compile_custom(tokens: &str, config: &Config) -> Result<CompileOutput, Errored> {
184    let tmp_file = Path::new(config.tmp_dir.as_ref()).join(format!("{}.rs", config.function_name));
185
186    write_tmp_file(tokens, &tmp_file);
187
188    let dependencies = Dependencies::new(config)?;
189
190    let mut program = rustc(config, &tmp_file);
191
192    dependencies.apply(&mut program);
193    // The first invoke is used to get the macro expanded code.
194    program.arg("-Zunpretty=expanded");
195
196    let output = program.output().unwrap();
197
198    let stdout = String::from_utf8(output.stdout).unwrap();
199    let syn_file = syn::parse_file(&stdout);
200    #[cfg(feature = "prettyplease")]
201    let stdout = syn_file.as_ref().map(prettyplease::unparse).unwrap_or(stdout);
202
203    let mut crate_type = "lib";
204
205    if let Ok(file) = syn_file {
206        if file.items.iter().any(|item| {
207            let syn::Item::Fn(func) = item else {
208                return false;
209            };
210
211            func.sig.ident == "main"
212        }) {
213            crate_type = "bin";
214        }
215    };
216
217    let mut status = if output.status.success() {
218        ExitStatus::Success
219    } else {
220        ExitStatus::Failure(output.status.code().unwrap_or(-1))
221    };
222
223    let stderr = if status == ExitStatus::Success {
224        let mut program = rustc(config, &tmp_file);
225        dependencies.apply(&mut program);
226        program.arg("--emit=llvm-ir");
227        program.arg(format!("--crate-type={crate_type}"));
228        program.arg("-o");
229        program.arg("-");
230        let comp_output = program.output().unwrap();
231        status = if comp_output.status.success() {
232            ExitStatus::Success
233        } else {
234            ExitStatus::Failure(comp_output.status.code().unwrap_or(-1))
235        };
236        String::from_utf8(comp_output.stderr).unwrap()
237    } else {
238        String::from_utf8(output.stderr).unwrap()
239    };
240
241    let stderr = stderr.replace(tmp_file.as_os_str().to_string_lossy().as_ref(), "<postcompile>");
242    let stdout = stdout.replace(tmp_file.as_os_str().to_string_lossy().as_ref(), "<postcompile>");
243
244    Ok(CompileOutput { status, stdout, stderr })
245}
246
247/// The configuration for the compilation.
248#[derive(Clone, Debug)]
249pub struct Config {
250    /// The path to the cargo manifest file of the library being tested.
251    /// This is so that we can include the `dependencies` & `dev-dependencies`
252    /// making them available in the code provided.
253    pub manifest: Cow<'static, Path>,
254    /// The path to the target directory, used to cache builds & find
255    /// dependencies.
256    pub target_dir: Cow<'static, Path>,
257    /// A temporary directory to write the expanded code to.
258    pub tmp_dir: Cow<'static, Path>,
259    /// The name of the function to compile.
260    pub function_name: Cow<'static, str>,
261}
262
263#[macro_export]
264#[doc(hidden)]
265macro_rules! _function_name {
266    () => {{
267        fn f() {}
268        fn type_name_of_val<T>(_: T) -> &'static str {
269            std::any::type_name::<T>()
270        }
271        let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
272        while let Some(rest) = name.strip_suffix("::{{closure}}") {
273            name = rest;
274        }
275        name
276    }};
277}
278
279#[doc(hidden)]
280pub fn build_dir() -> &'static Path {
281    Path::new(env!("OUT_DIR"))
282}
283
284#[doc(hidden)]
285pub fn target_dir() -> &'static Path {
286    build_dir()
287        .parent()
288        .unwrap()
289        .parent()
290        .unwrap()
291        .parent()
292        .unwrap()
293        .parent()
294        .unwrap()
295}
296
297#[macro_export]
298#[doc(hidden)]
299macro_rules! _config {
300    () => {{
301        $crate::Config {
302            manifest: ::std::borrow::Cow::Borrowed(::std::path::Path::new(env!("CARGO_MANIFEST_PATH"))),
303            tmp_dir: ::std::borrow::Cow::Borrowed($crate::build_dir()),
304            target_dir: ::std::borrow::Cow::Borrowed($crate::target_dir()),
305            function_name: ::std::borrow::Cow::Borrowed($crate::_function_name!()),
306        }
307    }};
308}
309
310/// Compiles the given tokens and returns the output.
311///
312/// This macro will panic if we fail to invoke the compiler.
313///
314/// ```rs
315/// // Dummy macro to assert the snapshot.
316/// macro_rules! assert_snapshot {
317///     ($expr:expr) => {};
318/// }
319///
320/// let output = postcompile::compile! {
321///     const TEST: u32 = 1;
322/// };
323///
324/// assert_eq!(output.status, postcompile::ExitStatus::Success);
325/// assert!(output.stderr.is_empty());
326/// assert_snapshot!(output.stdout); // We dont have an assert_snapshot! macro in this crate, but you get the idea.
327/// ```
328#[macro_export]
329macro_rules! compile {
330    ($($tokens:tt)*) => {
331        $crate::compile_str!(stringify!($($tokens)*))
332    };
333}
334
335/// Compiles the given string of tokens and returns the output.
336///
337/// This macro will panic if we fail to invoke the compiler.
338///
339/// Same as the [`compile!`] macro, but for strings. This allows you to do:
340///
341/// ```rs
342/// let output = postcompile::compile_str!(include_str!("some_file.rs"));
343///
344/// // ... do something with the output
345/// ```
346#[macro_export]
347macro_rules! compile_str {
348    ($expr:expr) => {
349        $crate::try_compile_str!($expr).expect("failed to compile")
350    };
351}
352
353/// Compiles the given string of tokens and returns the output.
354///
355/// This macro will return an error if we fail to invoke the compiler. Unlike
356/// the [`compile!`] macro, this will not panic.
357///
358/// ```rs
359/// let output = postcompile::try_compile! {
360///     const TEST: u32 = 1;
361/// };
362///
363/// assert!(output.is_ok());
364/// assert_eq!(output.unwrap().status, postcompile::ExitStatus::Success);
365/// ```
366#[macro_export]
367macro_rules! try_compile {
368    ($($tokens:tt)*) => {
369        $crate::try_compile_str!(stringify!($($tokens)*))
370    };
371}
372
373/// Compiles the given string of tokens and returns the output.
374///
375/// This macro will return an error if we fail to invoke the compiler.
376///
377/// Same as the [`try_compile!`] macro, but for strings similar usage to
378/// [`compile_str!`].
379#[macro_export]
380macro_rules! try_compile_str {
381    ($expr:expr) => {
382        $crate::compile_custom($expr, &$crate::_config!())
383    };
384}
385
386#[cfg(test)]
387#[cfg_attr(all(test, coverage_nightly), coverage(off))]
388mod tests {
389    use insta::assert_snapshot;
390
391    use crate::ExitStatus;
392
393    #[test]
394    fn compile_success() {
395        let out = compile! {
396            #[allow(unused)]
397            fn main() {
398                let a = 1;
399                let b = 2;
400                let c = a + b;
401            }
402        };
403
404        assert_eq!(out.status, ExitStatus::Success);
405        assert!(out.stderr.is_empty());
406        assert_snapshot!(out);
407    }
408
409    #[test]
410    fn try_compile_success() {
411        let out = try_compile! {
412            #[allow(unused)]
413            fn main() {
414                let xd = 0xd;
415                let xdd = 0xdd;
416                let xddd = xd + xdd;
417                println!("{}", xddd);
418            }
419        };
420
421        assert!(out.is_ok());
422        let out = out.unwrap();
423        assert_eq!(out.status, ExitStatus::Success);
424        assert!(out.stderr.is_empty());
425        assert!(!out.stdout.is_empty());
426    }
427
428    #[test]
429    fn compile_failure() {
430        let out = compile! {
431            invalid_rust_code
432        };
433
434        assert_eq!(out.status, ExitStatus::Failure(1));
435        assert!(out.stdout.is_empty());
436        assert_snapshot!(out);
437    }
438
439    #[test]
440    fn try_compile_failure() {
441        let out = try_compile! {
442            invalid rust code
443        };
444
445        assert!(out.is_ok());
446        let out = out.unwrap();
447        assert_eq!(out.status, ExitStatus::Failure(1));
448        assert!(out.stdout.is_empty());
449        assert!(!out.stderr.is_empty());
450    }
451}