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))]
132#![cfg_attr(docsrs, feature(doc_auto_cfg))]
133#![deny(missing_docs)]
134#![deny(unsafe_code)]
135#![deny(unreachable_pub)]
136
137use std::borrow::Cow;
138use std::collections::BTreeMap;
139use std::path::Path;
140use std::process::Command;
141
142use cargo_manifest::DependencyDetail;
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum ExitStatus {
147 Success,
149 Failure(i32),
151}
152
153impl std::fmt::Display for ExitStatus {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 ExitStatus::Success => write!(f, "0"),
157 ExitStatus::Failure(code) => write!(f, "{code}"),
158 }
159 }
160}
161
162#[derive(Debug)]
164pub struct CompileOutput {
165 pub status: ExitStatus,
167 pub expanded: String,
170 pub expand_stderr: String,
173 pub test_stderr: String,
176 pub test_stdout: String,
178}
179
180impl std::fmt::Display for CompileOutput {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 writeln!(f, "exit status: {}", self.status)?;
183 if !self.expand_stderr.is_empty() {
184 write!(f, "--- expand_stderr\n{}\n", self.expand_stderr)?;
185 }
186 if !self.test_stderr.is_empty() {
187 write!(f, "--- test_stderr\n{}\n", self.test_stderr)?;
188 }
189 if !self.test_stdout.is_empty() {
190 write!(f, "--- test_stdout\n{}\n", self.test_stdout)?;
191 }
192 if !self.expanded.is_empty() {
193 write!(f, "--- expanded\n{}\n", self.expanded)?;
194 }
195 Ok(())
196 }
197}
198
199fn cargo(config: &Config, manifest_path: &Path, subcommand: &str) -> Command {
200 let mut program = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()));
201 program.arg(subcommand);
202 program.current_dir(manifest_path.parent().unwrap());
203
204 program.env_clear();
205 program.envs(std::env::vars().filter(|(k, _)| !k.starts_with("CARGO_") && k != "OUT_DIR"));
206 program.env("CARGO_TERM_COLOR", "never");
207 program.stderr(std::process::Stdio::piped());
208 program.stdout(std::process::Stdio::piped());
209
210 let target_dir = if config.target_dir.ends_with(target_triple::TARGET) {
211 config.target_dir.parent().unwrap()
212 } else {
213 config.target_dir.as_ref()
214 };
215
216 program.arg("--quiet");
217 program.arg("--manifest-path").arg(manifest_path);
218 program.arg("--target-dir").arg(target_dir);
219
220 if !cfg!(trybuild_no_target) && !cfg!(postcompile_no_target) && config.target_dir.ends_with(target_triple::TARGET) {
221 program.arg("--target").arg(target_triple::TARGET);
222 }
223
224 program
225}
226
227fn write_tmp_file(tokens: &str, tmp_file: &Path) {
228 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
229
230 let tokens = if let Ok(file) = syn::parse_file(tokens) {
231 prettyplease::unparse(&file)
232 } else {
233 tokens.to_owned()
234 };
235
236 std::fs::write(tmp_file, tokens).unwrap();
237}
238
239fn generate_cargo_toml(config: &Config, crate_name: &str) -> std::io::Result<(String, String)> {
240 let metadata = cargo_metadata::MetadataCommand::new()
241 .manifest_path(config.manifest.as_ref())
242 .exec()
243 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
244
245 let workspace_manifest = cargo_manifest::Manifest::from_path(metadata.workspace_root.join("Cargo.toml"))
246 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
247
248 let manifest = cargo_manifest::Manifest::<cargo_manifest::Value, cargo_manifest::Value> {
249 package: Some(cargo_manifest::Package {
250 publish: Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Publish::Flag(false))),
251 edition: match config.edition.as_str() {
252 "2024" => Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2024)),
253 "2021" => Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2021)),
254 "2018" => Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2018)),
255 "2015" => Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2015)),
256 _ => match metadata
257 .packages
258 .iter()
259 .find(|p| p.name == config.package_name)
260 .map(|p| p.edition)
261 {
262 Some(cargo_metadata::Edition::E2015) => {
263 Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2015))
264 }
265 Some(cargo_metadata::Edition::E2018) => {
266 Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2018))
267 }
268 Some(cargo_metadata::Edition::E2021) => {
269 Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2021))
270 }
271 Some(cargo_metadata::Edition::E2024) => {
272 Some(cargo_manifest::MaybeInherited::Local(cargo_manifest::Edition::E2024))
273 }
274 _ => None,
275 },
276 },
277 ..cargo_manifest::Package::<cargo_manifest::Value>::new(crate_name.to_owned(), "0.1.0".into())
278 }),
279 workspace: Some(cargo_manifest::Workspace {
280 default_members: None,
281 dependencies: None,
282 exclude: None,
283 members: Vec::new(),
284 metadata: None,
285 package: None,
286 resolver: None,
287 }),
288 dependencies: Some({
289 let mut deps = BTreeMap::new();
290
291 for dep in &config.dependencies {
292 let mut detail = if dep.workspace {
293 let Some(dep) = workspace_manifest
294 .workspace
295 .as_ref()
296 .and_then(|workspace| workspace.dependencies.as_ref())
297 .or(workspace_manifest.dependencies.as_ref())
298 .and_then(|deps| deps.get(&dep.name))
299 else {
300 return Err(std::io::Error::new(
301 std::io::ErrorKind::InvalidInput,
302 format!("workspace has no dep: {}", dep.name),
303 ));
304 };
305
306 let mut dep = match dep {
307 cargo_manifest::Dependency::Detailed(d) => d.clone(),
308 cargo_manifest::Dependency::Simple(version) => DependencyDetail {
309 version: Some(version.clone()),
310 ..Default::default()
311 },
312 cargo_manifest::Dependency::Inherited(_) => panic!("workspace deps cannot be inherited"),
313 };
314
315 if let Some(path) = dep.path.as_mut() {
316 if std::path::Path::new(path.as_str()).is_relative() {
317 *path = metadata.workspace_root.join(path.as_str()).to_string()
318 }
319 }
320
321 dep
322 } else {
323 Default::default()
324 };
325
326 if !dep.default_features {
327 detail.features = None;
328 }
329
330 detail.default_features = Some(dep.default_features);
331 if let Some(mut path) = dep.path.clone() {
332 if std::path::Path::new(path.as_str()).is_relative() {
333 path = config.manifest.parent().unwrap().join(path).to_string_lossy().to_string();
334 }
335 detail.path = Some(path);
336 }
337 if let Some(version) = dep.version.clone() {
338 detail.version = Some(version);
339 }
340
341 detail.features.get_or_insert_default().extend(dep.features.iter().cloned());
342
343 deps.insert(dep.name.clone(), cargo_manifest::Dependency::Detailed(detail));
344 }
345
346 deps
347 }),
348 patch: workspace_manifest.patch.clone().map(|mut patch| {
349 patch.values_mut().for_each(|deps| {
350 deps.values_mut().for_each(|dep| {
351 if let cargo_manifest::Dependency::Detailed(dep) = dep {
352 if let Some(path) = &mut dep.path {
353 if std::path::Path::new(path.as_str()).is_relative() {
354 *path = metadata.workspace_root.join(path.as_str()).to_string()
355 }
356 }
357 }
358 });
359 });
360
361 patch
362 }),
363 ..Default::default()
364 };
365
366 Ok((
367 toml::to_string(&manifest).map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?,
368 std::fs::read_to_string(metadata.workspace_root.join("Cargo.lock"))?,
369 ))
370}
371
372static TEST_TIME_RE: std::sync::LazyLock<regex::Regex> =
373 std::sync::LazyLock::new(|| regex::Regex::new(r"\d+\.\d+s").expect("failed to compile regex"));
374
375pub fn compile_custom(tokens: impl std::fmt::Display, config: &Config) -> std::io::Result<CompileOutput> {
377 let tokens = tokens.to_string();
378
379 let crate_name = config.function_name.replace("::", "__");
380 let tmp_crate_path = Path::new(config.tmp_dir.as_ref()).join(&crate_name);
381 std::fs::create_dir_all(&tmp_crate_path)?;
382
383 let manifest_path = tmp_crate_path.join("Cargo.toml");
384 let (cargo_toml, cargo_lock) = generate_cargo_toml(config, &crate_name)?;
385
386 std::fs::write(&manifest_path, cargo_toml)?;
387 std::fs::write(tmp_crate_path.join("Cargo.lock"), cargo_lock)?;
388
389 let main_path = tmp_crate_path.join("src").join("main.rs");
390
391 write_tmp_file(&tokens, &main_path);
392
393 let mut program = cargo(config, &manifest_path, "rustc");
394
395 program.env("RUSTC_BOOTSTRAP", "1");
398 program.arg("--").arg("-Zunpretty=expanded");
399
400 let output = program.output().unwrap();
401
402 let stdout = String::from_utf8(output.stdout).unwrap();
403 let syn_file = syn::parse_file(&stdout);
404 let stdout = syn_file.as_ref().map(prettyplease::unparse).unwrap_or(stdout);
405
406 let cleanup_output = |out: &[u8]| {
407 let out = String::from_utf8_lossy(out);
408 let tmp_dir = config.tmp_dir.display().to_string();
409 let main_relative = main_path.strip_prefix(&tmp_crate_path).unwrap().display().to_string();
410 let main_path = main_path.display().to_string();
411 TEST_TIME_RE
412 .replace_all(out.as_ref(), "[ELAPSED]s")
413 .trim()
414 .replace(&main_relative, "[POST_COMPILE]")
415 .replace(&main_path, "[POST_COMPILE]")
416 .replace(&tmp_dir, "[BUILD_DIR]")
417 };
418
419 let mut result = CompileOutput {
420 status: if output.status.success() {
421 ExitStatus::Success
422 } else {
423 ExitStatus::Failure(output.status.code().unwrap_or(-1))
424 },
425 expand_stderr: cleanup_output(&output.stderr),
426 expanded: stdout,
427 test_stderr: String::new(),
428 test_stdout: String::new(),
429 };
430
431 if result.status == ExitStatus::Success {
432 let mut program = cargo(config, &manifest_path, "test");
433
434 if !config.test {
435 program.arg("--no-run");
436 }
437
438 let comp_output = program.output().unwrap();
439 result.status = if comp_output.status.success() {
440 ExitStatus::Success
441 } else {
442 ExitStatus::Failure(comp_output.status.code().unwrap_or(-1))
443 };
444
445 result.test_stderr = cleanup_output(&comp_output.stderr);
446 result.test_stdout = cleanup_output(&comp_output.stdout);
447 };
448
449 Ok(result)
450}
451
452#[derive(Clone, Debug, Default)]
454pub struct Config {
455 pub manifest: Cow<'static, Path>,
459 pub target_dir: Cow<'static, Path>,
462 pub tmp_dir: Cow<'static, Path>,
464 pub function_name: Cow<'static, str>,
466 pub file_path: Cow<'static, Path>,
468 pub package_name: Cow<'static, str>,
470 pub dependencies: Vec<Dependency>,
472 pub test: bool,
474 pub edition: String,
476}
477
478#[derive(Debug, Clone)]
480pub struct Dependency {
481 name: String,
482 path: Option<String>,
483 version: Option<String>,
484 workspace: bool,
485 features: Vec<String>,
486 default_features: bool,
487}
488
489impl Dependency {
490 fn new(name: String) -> Self {
491 Self {
492 name,
493 workspace: false,
494 default_features: true,
495 features: Vec::new(),
496 path: None,
497 version: None,
498 }
499 }
500
501 pub fn workspace(name: impl std::fmt::Display) -> Self {
503 Self {
504 workspace: true,
505 ..Self::new(name.to_string())
506 }
507 }
508
509 pub fn path(name: impl std::fmt::Display, path: impl std::fmt::Display) -> Self {
511 Self {
512 path: Some(path.to_string()),
513 ..Self::new(name.to_string())
514 }
515 }
516
517 pub fn version(name: impl std::fmt::Display, version: impl std::fmt::Display) -> Self {
519 Self {
520 version: Some(version.to_string()),
521 ..Self::new(name.to_string())
522 }
523 }
524
525 pub fn feature(mut self, feature: impl std::fmt::Display) -> Self {
527 self.features.push(feature.to_string());
528 self
529 }
530
531 pub fn default_features(self, default_features: bool) -> Self {
533 Self {
534 default_features,
535 ..self
536 }
537 }
538}
539
540#[macro_export]
541#[doc(hidden)]
542macro_rules! _function_name {
543 () => {{
544 fn f() {}
545 fn type_name_of_val<T>(_: T) -> &'static str {
546 std::any::type_name::<T>()
547 }
548 let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
549 while let Some(rest) = name.strip_suffix("::{{closure}}") {
550 name = rest;
551 }
552 name
553 }};
554}
555
556#[doc(hidden)]
557pub fn build_dir() -> &'static Path {
558 Path::new(env!("OUT_DIR"))
559}
560
561#[doc(hidden)]
562pub fn target_dir() -> &'static Path {
563 build_dir()
564 .parent()
565 .unwrap()
566 .parent()
567 .unwrap()
568 .parent()
569 .unwrap()
570 .parent()
571 .unwrap()
572}
573
574#[macro_export]
588macro_rules! config {
589 (
590 $($item:ident: $value:expr),*$(,)?
591 ) => {{
592 #[allow(unused_mut)]
593 let mut config = $crate::Config {
594 manifest: ::std::borrow::Cow::Borrowed(::std::path::Path::new(env!("CARGO_MANIFEST_PATH"))),
595 tmp_dir: ::std::borrow::Cow::Borrowed($crate::build_dir()),
596 target_dir: ::std::borrow::Cow::Borrowed($crate::target_dir()),
597 function_name: ::std::borrow::Cow::Borrowed($crate::_function_name!()),
598 file_path: ::std::borrow::Cow::Borrowed(::std::path::Path::new(file!())),
599 package_name: ::std::borrow::Cow::Borrowed(env!("CARGO_PKG_NAME")),
600 dependencies: vec![
601 $crate::Dependency::path(env!("CARGO_PKG_NAME"), ".")
602 ],
603 ..::core::default::Default::default()
604 };
605
606 $(
607 config.$item = $value;
608 )*
609
610 config
611 }};
612}
613
614#[macro_export]
660macro_rules! compile {
661 (
662 $config:expr,
663 { $($tokens:tt)* }$(,)?
664 ) => {
665 $crate::compile_str!($config, stringify!($($tokens)*))
666 };
667 (
668 { $($tokens:tt)* }$(,)?
669 ) => {
670 $crate::compile_str!(stringify!($($tokens)*))
671 };
672}
673
674#[macro_export]
686macro_rules! compile_str {
687 ($config:expr, $expr:expr $(,)?) => {
688 $crate::try_compile_str!($config, $expr).expect("failed to compile")
689 };
690 ($expr:expr $(,)?) => {
691 $crate::try_compile_str!($crate::config!(), $expr).expect("failed to compile")
692 };
693}
694
695#[macro_export]
709macro_rules! try_compile {
710 ($config:expr, { $($tokens:tt)* }$(,)?) => {
711 $crate::try_compile_str!($crate::config!(), stringify!($($tokens)*))
712 };
713 ({ $($tokens:tt)* }$(,)?) => {
714 $crate::try_compile_str!($crate::config!(), stringify!($($tokens)*))
715 };
716}
717
718#[macro_export]
725macro_rules! try_compile_str {
726 ($config:expr, $expr:expr $(,)?) => {
727 $crate::compile_custom($expr, &$config)
728 };
729 ($expr:expr $(,)?) => {
730 $crate::compile_custom($expr, &$crate::config!())
731 };
732}
733
734#[cfg(feature = "docs")]
736#[scuffle_changelog::changelog]
737pub mod changelog {}
738
739#[cfg(test)]
740#[cfg_attr(all(test, coverage_nightly), coverage(off))]
741mod tests {
742 use insta::assert_snapshot;
743
744 use crate::Dependency;
745
746 #[test]
747 fn compile_success() {
748 let out = compile!({
749 #[allow(unused)]
750 fn main() {
751 let a = 1;
752 let b = 2;
753 let c = a + b;
754 }
755 });
756
757 assert_snapshot!(out);
758 }
759
760 #[test]
761 fn compile_failure() {
762 let out = compile!({ invalid_rust_code });
763
764 assert_snapshot!(out);
765 }
766
767 #[cfg(not(valgrind))]
768 #[test]
769 fn compile_tests() {
770 let out = compile!(
771 config! {
772 test: true,
773 dependencies: vec![
774 Dependency::version("tokio", "1").feature("full"),
775 ]
776 },
777 {
778 #[allow(unused)]
779 fn fib(n: i32) -> i32 {
780 match n {
781 i32::MIN..=0 => 0,
782 1 => 1,
783 n => fib(n - 1) + fib(n - 2),
784 }
785 }
786
787 #[tokio::test]
788 async fn test_fib() {
789 assert_eq!(fib(0), 0);
790 assert_eq!(fib(1), 1);
791 assert_eq!(fib(2), 1);
792 assert_eq!(fib(3), 2);
793 assert_eq!(fib(10), 55);
794 }
795 }
796 );
797
798 assert_snapshot!(out)
799 }
800}