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