1#![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#![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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum ExitStatus {
151 Success,
153 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#[derive(Debug)]
168pub struct CompileOutput {
169 pub status: ExitStatus,
171 pub expanded: String,
174 pub expand_stderr: String,
177 pub test_stderr: String,
180 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
396pub 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 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#[derive(Clone, Debug, Default)]
585pub struct Config {
586 pub manifest: Option<Cow<'static, Path>>,
590 pub target_dir: Option<Cow<'static, Path>>,
593 pub tmp_dir: Option<Cow<'static, Path>>,
595 pub function_name: Cow<'static, str>,
597 pub file_path: Cow<'static, Path>,
599 pub package_name: Cow<'static, str>,
601 pub dependencies: Vec<Dependency>,
603 pub test: bool,
605 pub edition: String,
607}
608
609#[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 pub fn workspace(name: impl std::fmt::Display) -> Self {
634 Self {
635 workspace: true,
636 ..Self::new(name.to_string())
637 }
638 }
639
640 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 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 pub fn feature(mut self, feature: impl std::fmt::Display) -> Self {
658 self.features.push(feature.to_string());
659 self
660 }
661
662 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#[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#[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#[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#[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#[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#[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}