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