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#![cfg_attr(feature = "docs", doc = "\n\nSee the [changelog][changelog] for a full release history.")]
10#![cfg_attr(feature = "docs", doc = "## Feature flags")]
11#![cfg_attr(feature = "docs", doc = document_features::document_features!())]
12//! ## Usage
13//!
14//! ```rust
15//! # macro_rules! assert_snapshot {
16//! #     ($expr:expr) => { $expr };
17//! # }
18//! #[test]
19//! # fn some_cool_test_() {}
20//! fn some_cool_test() {
21//!     assert_snapshot!(postcompile::compile!({
22//!         #![allow(unused)]
23//!
24//!         #[derive(Debug, Clone)]
25//!         struct Test {
26//!             a: u32,
27//!             b: i32,
28//!         }
29//!
30//!         const TEST: Test = Test { a: 1, b: 3 };
31//!     }));
32//! }
33//!
34//! #[test]
35//! # fn some_cool_test_extern_() {}
36//! fn some_cool_test_extern() {
37//!     assert_snapshot!(postcompile::compile_str!(include_str!("some_file.rs")));
38//! }
39//!
40//! #[test]
41//! # fn test_inside_test_() {}
42//! fn test_inside_test() {
43//!     assert_snapshot!(postcompile::compile!(
44//!         postcompile::config! {
45//!             test: true,
46//!         },
47//!         {
48//!             fn add(a: i32, b: i32) -> i32 {
49//!                 a + b
50//!             }
51//!
52//!             #[test]
53//!             fn test_add() {
54//!                 assert_eq!(add(1, 2), 3);
55//!             }
56//!         },
57//!     ));
58//! }
59//!
60//! #[test]
61//! # fn test_inside_test_with_tokio() {}
62//! fn test_inside_test_with_tokio() {
63//!     assert_snapshot!(postcompile::compile!(
64//!         postcompile::config! {
65//!             test: true,
66//!             dependencies: vec![
67//!                 postcompile::Dependency::version("tokio", "1").feature("full")
68//!             ]
69//!         },
70//!         {
71//!             async fn async_add(a: i32, b: i32) -> i32 {
72//!                 a + b
73//!             }
74//!
75//!             #[tokio::test]
76//!             async fn test_add() {
77//!                 assert_eq!(async_add(1, 2).await, 3);
78//!             }
79//!         },
80//!     ));
81//! }
82//! ```
83//!
84//! ## Features
85//!
86//! - Cached builds: This crate reuses the cargo build cache of the original
87//!   crate so that only the contents of the macro are compiled & not any
88//!   additional dependencies.
89//! - Coverage: This crate works with [`cargo-llvm-cov`](https://crates.io/crates/cargo-llvm-cov)
90//!   out of the box, which allows you to instrument the proc-macro expansion.
91//! - Testing: You can define tests with the `#[test]` macro and the tests will run on the generated code.
92//!
93//! ## Alternatives
94//!
95//! - [`compiletest_rs`](https://crates.io/crates/compiletest_rs): This crate is
96//!   used by the Rust compiler team to test the compiler itself. Not really
97//!   useful for proc-macros.
98//! - [`trybuild`](https://crates.io/crates/trybuild): This crate is an
99//!   all-in-one solution for testing proc-macros, with built in snapshot
100//!   testing.
101//! - [`ui_test`](https://crates.io/crates/ui_test): Similar to `trybuild` with
102//!   a slightly different API & used by the Rust compiler team to test the
103//!   compiler itself.
104//!
105//! ### Differences
106//!
107//! The other libraries are focused on testing & have built in test harnesses.
108//! This crate takes a step back and allows you to compile without a testing
109//! harness. This has the advantage of being more flexible, and allows you to
110//! use whatever testing framework you want.
111//!
112//! In the examples above I showcase how to use this crate with the `insta`
113//! crate for snapshot testing.
114//!
115//! ## Limitations
116//!
117//! Please note that this crate does not work inside a running compiler process
118//! (inside a proc-macro) without hacky workarounds and complete build-cache
119//! invalidation.
120//!
121//! This is because `cargo` holds a lock on the build directory and that if we
122//! were to compile inside a proc-macro we would recursively invoke the
123//! compiler.
124//!
125//! ## License
126//!
127//! This project is licensed under the MIT or Apache-2.0 license.
128//! You can choose between one of them if you use this work.
129//!
130//! `SPDX-License-Identifier: MIT OR Apache-2.0`
131#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
132#![cfg_attr(docsrs, feature(doc_auto_cfg))]
133#![deny(missing_docs)]
134#![deny(unsafe_code)]
135#![deny(unreachable_pub)]
136
137use std::borrow::Cow;
138use std::collections::BTreeMap;
139use std::path::Path;
140use std::process::Command;
141
142use cargo_manifest::DependencyDetail;
143
144/// The return status of the compilation.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum ExitStatus {
147    /// If the compiler returned a 0 exit code.
148    Success,
149    /// If the compiler returned a non-0 exit code.
150    Failure(i32),
151}
152
153impl std::fmt::Display for ExitStatus {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            ExitStatus::Success => write!(f, "0"),
157            ExitStatus::Failure(code) => write!(f, "{code}"),
158        }
159    }
160}
161
162/// The output of the compilation.
163#[derive(Debug)]
164pub struct CompileOutput {
165    /// The status of the compilation.
166    pub status: ExitStatus,
167    /// The stdout of the compilation.
168    /// This will contain the expanded code.
169    pub expanded: String,
170    /// The stderr of the compilation.
171    /// This will contain any errors or warnings from the compiler.
172    pub expand_stderr: String,
173    /// The stderr of the compilation.
174    /// This will contain any errors or warnings from the compiler.
175    pub test_stderr: String,
176    /// The stdout of the test results.
177    pub test_stdout: String,
178}
179
180impl std::fmt::Display for CompileOutput {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        writeln!(f, "exit status: {}", self.status)?;
183        if !self.expand_stderr.is_empty() {
184            write!(f, "--- expand_stderr\n{}\n", self.expand_stderr)?;
185        }
186        if !self.test_stderr.is_empty() {
187            write!(f, "--- test_stderr\n{}\n", self.test_stderr)?;
188        }
189        if !self.test_stdout.is_empty() {
190            write!(f, "--- test_stdout\n{}\n", self.test_stdout)?;
191        }
192        if !self.expanded.is_empty() {
193            write!(f, "--- expanded\n{}\n", self.expanded)?;
194        }
195        Ok(())
196    }
197}
198
199fn cargo(config: &Config, manifest_path: &Path, subcommand: &str) -> Command {
200    let mut program = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()));
201    program.arg(subcommand);
202    program.current_dir(manifest_path.parent().unwrap());
203
204    program.env_clear();
205    program.envs(std::env::vars().filter(|(k, _)| !k.starts_with("CARGO_") && k != "OUT_DIR"));
206    program.env("CARGO_TERM_COLOR", "never");
207    program.stderr(std::process::Stdio::piped());
208    program.stdout(std::process::Stdio::piped());
209
210    let target_dir = if config.target_dir.ends_with(target_triple::TARGET) {
211        config.target_dir.parent().unwrap()
212    } else {
213        config.target_dir.as_ref()
214    };
215
216    program.arg("--quiet");
217    program.arg("--manifest-path").arg(manifest_path);
218    program.arg("--target-dir").arg(target_dir);
219
220    if !cfg!(trybuild_no_target) && !cfg!(postcompile_no_target) && config.target_dir.ends_with(target_triple::TARGET) {
221        program.arg("--target").arg(target_triple::TARGET);
222    }
223
224    program
225}
226
227fn write_tmp_file(tokens: &str, tmp_file: &Path) {
228    std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
229
230    let tokens = if let Ok(file) = syn::parse_file(tokens) {
231        prettyplease::unparse(&file)
232    } else {
233        tokens.to_owned()
234    };
235
236    std::fs::write(tmp_file, tokens).unwrap();
237}
238
239fn generate_cargo_toml(config: &Config, crate_name: &str) -> std::io::Result<(String, String)> {
240    let metadata = cargo_metadata::MetadataCommand::new()
241        .manifest_path(config.manifest.as_ref())
242        .exec()
243        .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
244
245    let workspace_manifest = cargo_manifest::Manifest::from_path(metadata.workspace_root.join("Cargo.toml"))
246        .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
247
248    let manifest = cargo_manifest::Manifest::<cargo_manifest::Value, cargo_manifest::Value> {
249        package: Some(cargo_manifest::Package {
250            publish: Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Publish::Flag(false))),
251            edition: match config.edition.as_str() {
252                "2024" => Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2024)),
253                "2021" => Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2021)),
254                "2018" => Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2018)),
255                "2015" => Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2015)),
256                _ => match metadata
257                    .packages
258                    .iter()
259                    .find(|p| p.name == config.package_name)
260                    .map(|p| p.edition)
261                {
262                    Some(cargo_metadata::Edition::E2015) => {
263                        Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2015))
264                    }
265                    Some(cargo_metadata::Edition::E2018) => {
266                        Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2018))
267                    }
268                    Some(cargo_metadata::Edition::E2021) => {
269                        Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2021))
270                    }
271                    Some(cargo_metadata::Edition::E2024) => {
272                        Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2024))
273                    }
274                    _ => None,
275                },
276            },
277            ..cargo_manifest::Package::<cargo_manifest::Value>::new(crate_name.to_owned(), "0.1.0".into())
278        }),
279        workspace: Some(cargo_manifest::Workspace {
280            default_members: None,
281            dependencies: None,
282            exclude: None,
283            members: Vec::new(),
284            metadata: None,
285            package: None,
286            resolver: None,
287        }),
288        dependencies: Some({
289            let mut deps = BTreeMap::new();
290
291            for dep in &config.dependencies {
292                let mut detail = if dep.workspace {
293                    let Some(dep) = workspace_manifest
294                        .workspace
295                        .as_ref()
296                        .and_then(|workspace| workspace.dependencies.as_ref())
297                        .or(workspace_manifest.dependencies.as_ref())
298                        .and_then(|deps| deps.get(&dep.name))
299                    else {
300                        return Err(std::io::Error::new(
301                            std::io::ErrorKind::InvalidInput,
302                            format!("workspace has no dep: {}", dep.name),
303                        ));
304                    };
305
306                    let mut dep = match dep {
307                        cargo_manifest::Dependency::Detailed(d) => d.clone(),
308                        cargo_manifest::Dependency::Simple(version) => DependencyDetail {
309                            version: Some(version.clone()),
310                            ..Default::default()
311                        },
312                        cargo_manifest::Dependency::Inherited(_) => panic!("workspace deps cannot be inherited"),
313                    };
314
315                    if let Some(path) = dep.path.as_mut() {
316                        if std::path::Path::new(path.as_str()).is_relative() {
317                            *path = metadata.workspace_root.join(path.as_str()).to_string()
318                        }
319                    }
320
321                    dep
322                } else {
323                    Default::default()
324                };
325
326                if !dep.default_features {
327                    detail.features = None;
328                }
329
330                detail.default_features = Some(dep.default_features);
331                if let Some(mut path) = dep.path.clone() {
332                    if std::path::Path::new(path.as_str()).is_relative() {
333                        path = config.manifest.parent().unwrap().join(path).to_string_lossy().to_string();
334                    }
335                    detail.path = Some(path);
336                }
337                if let Some(version) = dep.version.clone() {
338                    detail.version = Some(version);
339                }
340
341                detail.features.get_or_insert_default().extend(dep.features.iter().cloned());
342
343                deps.insert(dep.name.clone(), cargo_manifest::Dependency::Detailed(detail));
344            }
345
346            deps
347        }),
348        patch: workspace_manifest.patch.clone().map(|mut patch| {
349            patch.values_mut().for_each(|deps| {
350                deps.values_mut().for_each(|dep| {
351                    if let cargo_manifest::Dependency::Detailed(dep) = dep {
352                        if let Some(path) = &mut dep.path {
353                            if std::path::Path::new(path.as_str()).is_relative() {
354                                *path = metadata.workspace_root.join(path.as_str()).to_string()
355                            }
356                        }
357                    }
358                });
359            });
360
361            patch
362        }),
363        ..Default::default()
364    };
365
366    Ok((
367        toml::to_string(&manifest).map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?,
368        std::fs::read_to_string(metadata.workspace_root.join("Cargo.lock"))?,
369    ))
370}
371
372static TEST_TIME_RE: std::sync::LazyLock<regex::Regex> =
373    std::sync::LazyLock::new(|| regex::Regex::new(r"\d+\.\d+s").expect("failed to compile regex"));
374
375/// Compiles the given tokens and returns the output.
376pub fn compile_custom(tokens: impl std::fmt::Display, config: &Config) -> std::io::Result<CompileOutput> {
377    let tokens = tokens.to_string();
378
379    let crate_name = config.function_name.replace("::", "__");
380    let tmp_crate_path = Path::new(config.tmp_dir.as_ref()).join(&crate_name);
381    std::fs::create_dir_all(&tmp_crate_path)?;
382
383    let manifest_path = tmp_crate_path.join("Cargo.toml");
384    let (cargo_toml, cargo_lock) = generate_cargo_toml(config, &crate_name)?;
385
386    std::fs::write(&manifest_path, cargo_toml)?;
387    std::fs::write(tmp_crate_path.join("Cargo.lock"), cargo_lock)?;
388
389    let main_path = tmp_crate_path.join("src").join("main.rs");
390
391    write_tmp_file(&tokens, &main_path);
392
393    let mut program = cargo(config, &manifest_path, "rustc");
394
395    // The first invoke is used to get the macro expanded code.
396    // We set this env variable so that this compiler can accept nightly options.)
397    program.env("RUSTC_BOOTSTRAP", "1");
398    program.arg("--").arg("-Zunpretty=expanded");
399
400    let output = program.output().unwrap();
401
402    let stdout = String::from_utf8(output.stdout).unwrap();
403    let syn_file = syn::parse_file(&stdout);
404    let stdout = syn_file.as_ref().map(prettyplease::unparse).unwrap_or(stdout);
405
406    let cleanup_output = |out: &[u8]| {
407        let out = String::from_utf8_lossy(out);
408        let tmp_dir = config.tmp_dir.display().to_string();
409        let main_relative = main_path.strip_prefix(&tmp_crate_path).unwrap().display().to_string();
410        let main_path = main_path.display().to_string();
411        TEST_TIME_RE
412            .replace_all(out.as_ref(), "[ELAPSED]s")
413            .trim()
414            .replace(&main_relative, "[POST_COMPILE]")
415            .replace(&main_path, "[POST_COMPILE]")
416            .replace(&tmp_dir, "[BUILD_DIR]")
417    };
418
419    let mut result = CompileOutput {
420        status: if output.status.success() {
421            ExitStatus::Success
422        } else {
423            ExitStatus::Failure(output.status.code().unwrap_or(-1))
424        },
425        expand_stderr: cleanup_output(&output.stderr),
426        expanded: stdout,
427        test_stderr: String::new(),
428        test_stdout: String::new(),
429    };
430
431    if result.status == ExitStatus::Success {
432        let mut program = cargo(config, &manifest_path, "test");
433
434        if !config.test {
435            program.arg("--no-run");
436        }
437
438        let comp_output = program.output().unwrap();
439        result.status = if comp_output.status.success() {
440            ExitStatus::Success
441        } else {
442            ExitStatus::Failure(comp_output.status.code().unwrap_or(-1))
443        };
444
445        result.test_stderr = cleanup_output(&comp_output.stderr);
446        result.test_stdout = cleanup_output(&comp_output.stdout);
447    };
448
449    Ok(result)
450}
451
452/// The configuration for the compilation.
453#[derive(Clone, Debug, Default)]
454pub struct Config {
455    /// The path to the cargo manifest file of the library being tested.
456    /// This is so that we can include the `dependencies` & `dev-dependencies`
457    /// making them available in the code provided.
458    pub manifest: Cow<'static, Path>,
459    /// The path to the target directory, used to cache builds & find
460    /// dependencies.
461    pub target_dir: Cow<'static, Path>,
462    /// A temporary directory to write the expanded code to.
463    pub tmp_dir: Cow<'static, Path>,
464    /// The name of the function to compile.
465    pub function_name: Cow<'static, str>,
466    /// The path to the file being compiled.
467    pub file_path: Cow<'static, Path>,
468    /// The name of the package being compiled.
469    pub package_name: Cow<'static, str>,
470    /// The dependencies to add to the temporary crate.
471    pub dependencies: Vec<Dependency>,
472    /// Run any unit tests in the package.
473    pub test: bool,
474    /// The rust edition to use.
475    pub edition: String,
476}
477
478/// A dependency to apply to the code
479#[derive(Debug, Clone)]
480pub struct Dependency {
481    name: String,
482    path: Option<String>,
483    version: Option<String>,
484    workspace: bool,
485    features: Vec<String>,
486    default_features: bool,
487}
488
489impl Dependency {
490    fn new(name: String) -> Self {
491        Self {
492            name,
493            workspace: false,
494            default_features: true,
495            features: Vec::new(),
496            path: None,
497            version: None,
498        }
499    }
500
501    /// Create a dependency using the workspace dependency
502    pub fn workspace(name: impl std::fmt::Display) -> Self {
503        Self {
504            workspace: true,
505            ..Self::new(name.to_string())
506        }
507    }
508
509    /// Create a dependency using a path to the crate root, relative to the root of the current package.
510    pub fn path(name: impl std::fmt::Display, path: impl std::fmt::Display) -> Self {
511        Self {
512            path: Some(path.to_string()),
513            ..Self::new(name.to_string())
514        }
515    }
516
517    /// Create a dependency using a name and version from crates.io
518    pub fn version(name: impl std::fmt::Display, version: impl std::fmt::Display) -> Self {
519        Self {
520            version: Some(version.to_string()),
521            ..Self::new(name.to_string())
522        }
523    }
524
525    /// Add a feature to the dependency
526    pub fn feature(mut self, feature: impl std::fmt::Display) -> Self {
527        self.features.push(feature.to_string());
528        self
529    }
530
531    /// Toggle the default features flag
532    pub fn default_features(self, default_features: bool) -> Self {
533        Self {
534            default_features,
535            ..self
536        }
537    }
538}
539
540#[macro_export]
541#[doc(hidden)]
542macro_rules! _function_name {
543    () => {{
544        fn f() {}
545        fn type_name_of_val<T>(_: T) -> &'static str {
546            std::any::type_name::<T>()
547        }
548        let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
549        while let Some(rest) = name.strip_suffix("::{{closure}}") {
550            name = rest;
551        }
552        name
553    }};
554}
555
556#[doc(hidden)]
557pub fn build_dir() -> &'static Path {
558    Path::new(env!("OUT_DIR"))
559}
560
561#[doc(hidden)]
562pub fn target_dir() -> &'static Path {
563    build_dir()
564        .parent()
565        .unwrap()
566        .parent()
567        .unwrap()
568        .parent()
569        .unwrap()
570        .parent()
571        .unwrap()
572}
573
574/// Define a config to use when compiling crates.
575/// This macro is allows you to provide values for the config items.
576/// ```rust
577/// let config = postcompile::config! {
578///     edition: "2021".into(),
579///     dependencies: Vec::new()
580/// };
581/// ```
582///
583/// By default the current crate is included as the only dependency. You can undo this by
584/// setting the Dependencies field to an empty vector.
585///
586/// By default the edition is set to whatever the current edition is set to.
587#[macro_export]
588macro_rules! config {
589    (
590        $($item:ident: $value:expr),*$(,)?
591    ) => {{
592        #[allow(unused_mut)]
593        let mut config = $crate::Config {
594            manifest: ::std::borrow::Cow::Borrowed(::std::path::Path::new(env!("CARGO_MANIFEST_PATH"))),
595            tmp_dir: ::std::borrow::Cow::Borrowed($crate::build_dir()),
596            target_dir: ::std::borrow::Cow::Borrowed($crate::target_dir()),
597            function_name: ::std::borrow::Cow::Borrowed($crate::_function_name!()),
598            file_path: ::std::borrow::Cow::Borrowed(::std::path::Path::new(file!())),
599            package_name: ::std::borrow::Cow::Borrowed(env!("CARGO_PKG_NAME")),
600            dependencies: vec![
601                $crate::Dependency::path(env!("CARGO_PKG_NAME"), ".")
602            ],
603            ..::core::default::Default::default()
604        };
605
606        $(
607            config.$item = $value;
608        )*
609
610        config
611    }};
612}
613
614/// Compiles the given tokens and returns the output.
615///
616/// This macro will panic if we fail to invoke the compiler.
617///
618/// ```rust
619/// // Dummy macro to assert the snapshot.
620/// # macro_rules! assert_snapshot {
621/// #     ($expr:expr) => { $expr };
622/// # }
623/// let output = postcompile::compile!({
624///     const TEST: u32 = 1;
625/// });
626///
627/// assert_eq!(output.status, postcompile::ExitStatus::Success);
628/// // We dont have an assert_snapshot! macro in this crate, but you get the idea.
629/// assert_snapshot!(output);
630/// ```
631///
632/// You can provide a custom config using the [`config!`] macro. If not provided the default config is used.
633///
634/// In this example we enable the `test` flag which will run the tests inside the provided source code.
635///
636/// ```rust
637/// // Dummy macro to assert the snapshot.
638/// # macro_rules! assert_snapshot {
639/// #     ($expr:expr) => { $expr };
640/// # }
641/// let output = postcompile::compile!(
642///     postcompile::config! {
643///         test: true
644///     },
645///     {
646///         const TEST: u32 = 1;
647///
648///         #[test]
649///         fn test() {
650///             assert_eq!(TEST, 1);
651///         }
652///     }
653/// );
654///
655/// assert_eq!(output.status, postcompile::ExitStatus::Success);
656/// // We dont have an assert_snapshot! macro in this crate, but you get the idea.
657/// assert_snapshot!(output);
658/// ```
659#[macro_export]
660macro_rules! compile {
661    (
662        $config:expr,
663        { $($tokens:tt)* }$(,)?
664    ) => {
665        $crate::compile_str!($config, stringify!($($tokens)*))
666    };
667    (
668        { $($tokens:tt)* }$(,)?
669    ) => {
670        $crate::compile_str!(stringify!($($tokens)*))
671    };
672}
673
674/// Compiles the given string of tokens and returns the output.
675///
676/// This macro will panic if we fail to invoke the compiler.
677///
678/// Same as the [`compile!`] macro, but for strings. This allows you to do:
679///
680/// ```rust
681/// let output = postcompile::compile_str!(include_str!("some_file.rs"));
682///
683/// // ... do something with the output
684/// ```
685#[macro_export]
686macro_rules! compile_str {
687    ($config:expr, $expr:expr $(,)?) => {
688        $crate::try_compile_str!($config, $expr).expect("failed to compile")
689    };
690    ($expr:expr $(,)?) => {
691        $crate::try_compile_str!($crate::config!(), $expr).expect("failed to compile")
692    };
693}
694
695/// Compiles the given string of tokens and returns the output.
696///
697/// This macro will return an error if we fail to invoke the compiler. Unlike
698/// the [`compile!`] macro, this will not panic.
699///
700/// ```rust
701/// let output = postcompile::try_compile!({
702///     const TEST: u32 = 1;
703/// });
704///
705/// assert!(output.is_ok());
706/// assert_eq!(output.unwrap().status, postcompile::ExitStatus::Success);
707/// ```
708#[macro_export]
709macro_rules! try_compile {
710    ($config:expr, { $($tokens:tt)* }$(,)?) => {
711        $crate::try_compile_str!($crate::config!(), stringify!($($tokens)*))
712    };
713    ({ $($tokens:tt)* }$(,)?) => {
714        $crate::try_compile_str!($crate::config!(), stringify!($($tokens)*))
715    };
716}
717
718/// Compiles the given string of tokens and returns the output.
719///
720/// This macro will return an error if we fail to invoke the compiler.
721///
722/// Same as the [`try_compile!`] macro, but for strings similar usage to
723/// [`compile_str!`].
724#[macro_export]
725macro_rules! try_compile_str {
726    ($config:expr, $expr:expr $(,)?) => {
727        $crate::compile_custom($expr, &$config)
728    };
729    ($expr:expr $(,)?) => {
730        $crate::compile_custom($expr, &$crate::config!())
731    };
732}
733
734/// Changelogs generated by [scuffle_changelog]
735#[cfg(feature = "docs")]
736#[scuffle_changelog::changelog]
737pub mod changelog {}
738
739#[cfg(test)]
740#[cfg_attr(all(test, coverage_nightly), coverage(off))]
741mod tests {
742    use insta::assert_snapshot;
743
744    use crate::Dependency;
745
746    #[test]
747    fn compile_success() {
748        let out = compile!({
749            #[allow(unused)]
750            fn main() {
751                let a = 1;
752                let b = 2;
753                let c = a + b;
754            }
755        });
756
757        assert_snapshot!(out);
758    }
759
760    #[test]
761    fn compile_failure() {
762        let out = compile!({ invalid_rust_code });
763
764        assert_snapshot!(out);
765    }
766
767    #[cfg(not(valgrind))]
768    #[test]
769    fn compile_tests() {
770        let out = compile!(
771            config! {
772                test: true,
773                dependencies: vec![
774                    Dependency::version("tokio", "1").feature("full"),
775                ]
776            },
777            {
778                #[allow(unused)]
779                fn fib(n: i32) -> i32 {
780                    match n {
781                        i32::MIN..=0 => 0,
782                        1 => 1,
783                        n => fib(n - 1) + fib(n - 2),
784                    }
785                }
786
787                #[tokio::test]
788                async fn test_fib() {
789                    assert_eq!(fib(0), 0);
790                    assert_eq!(fib(1), 1);
791                    assert_eq!(fib(2), 1);
792                    assert_eq!(fib(3), 2);
793                    assert_eq!(fib(10), 55);
794                }
795            }
796        );
797
798        assert_snapshot!(out)
799    }
800}