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#![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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum ExitStatus {
152 Success,
154 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#[derive(Debug)]
169pub struct CompileOutput {
170 pub status: ExitStatus,
172 pub expanded: String,
175 pub expand_stderr: String,
178 pub test_stderr: String,
181 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
397pub 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 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#[derive(Clone, Debug, Default)]
586pub struct Config {
587 pub manifest: Option<Cow<'static, Path>>,
591 pub target_dir: Option<Cow<'static, Path>>,
594 pub tmp_dir: Option<Cow<'static, Path>>,
596 pub function_name: Cow<'static, str>,
598 pub file_path: Cow<'static, Path>,
600 pub package_name: Cow<'static, str>,
602 pub dependencies: Vec<Dependency>,
604 pub test: bool,
606 pub edition: String,
608}
609
610#[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 pub fn workspace(name: impl std::fmt::Display) -> Self {
635 Self {
636 workspace: true,
637 ..Self::new(name.to_string())
638 }
639 }
640
641 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 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 pub fn feature(mut self, feature: impl std::fmt::Display) -> Self {
659 self.features.push(feature.to_string());
660 self
661 }
662
663 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#[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#[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#[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#[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#[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#[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}