1use std::fs::{self, File};
2use std::io::{Read, Seek, SeekFrom, Write};
3use std::path::{Path, PathBuf};
4
5#[cfg(any(feature = "deploy", feature = "maelstrom"))]
6use dfir_lang::diagnostic::Diagnostics;
7#[cfg(any(feature = "deploy", feature = "maelstrom"))]
8use dfir_lang::graph::DfirGraph;
9#[cfg(any(feature = "sim", feature = "maelstrom"))]
10use hydro_concurrent_cargo::final_rustc;
11use sha2::{Digest, Sha256};
12#[cfg(any(feature = "deploy", feature = "maelstrom"))]
13use stageleft::internal::quote;
14use trybuild_internals_api::cargo::{self, Metadata};
15use trybuild_internals_api::env::Update;
16use trybuild_internals_api::run::{PathDependency, Project};
17use trybuild_internals_api::{Runner, dependencies, features, path};
18
19pub const HYDRO_RUNTIME_FEATURES: &[&str] = &[
20 "deploy_integration",
21 "runtime_measure",
22 "docker_runtime",
23 "ecs_runtime",
24 "maelstrom_runtime",
25 "sim_runtime",
26];
27
28#[cfg(any(feature = "deploy", feature = "maelstrom"))]
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum LinkingMode {
34 #[cfg_attr(
37 not(feature = "deploy"),
38 expect(
39 dead_code,
40 reason = "only constructed by the deploy backends; Maelstrom-only builds use Dynamic"
41 )
42 )]
43 Static,
44 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
45 Dynamic,
46}
47
48#[cfg(any(feature = "deploy", feature = "maelstrom"))]
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum DeployMode {
52 #[cfg(feature = "deploy")]
53 HydroDeploy,
55 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
56 Containerized,
58 #[cfg(feature = "maelstrom")]
59 Maelstrom,
61}
62
63pub(crate) static IS_TEST: std::sync::atomic::AtomicBool =
64 std::sync::atomic::AtomicBool::new(false);
65
66pub(crate) static CONCURRENT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
67
68pub fn init_test() {
82 IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
83}
84
85#[cfg(any(feature = "deploy", feature = "maelstrom"))]
86fn clean_bin_name_prefix(bin_name_prefix: &str) -> String {
87 bin_name_prefix
88 .replace("::", "__")
89 .replace(" ", "_")
90 .replace(",", "_")
91 .replace("<", "_")
92 .replace(">", "")
93 .replace("(", "")
94 .replace(")", "")
95 .replace("{", "_")
96 .replace("}", "_")
97}
98
99#[derive(Debug, Clone)]
100pub struct TrybuildConfig {
101 pub project_dir: PathBuf,
102 pub target_dir: PathBuf,
103 pub features: Option<Vec<String>>,
104 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
105 #[cfg_attr(
108 not(feature = "deploy"),
109 expect(dead_code, reason = "only read by the deploy backends")
110 )]
111 pub linking_mode: LinkingMode,
115}
116
117#[cfg(any(feature = "deploy", feature = "maelstrom"))]
118pub fn create_graph_trybuild(
119 graph: DfirGraph,
120 extra_stmts: &[syn::Stmt],
121 sidecars: &[syn::Expr],
122 bin_name_prefix: Option<&str>,
123 deploy_mode: DeployMode,
124 linking_mode: LinkingMode,
125) -> (String, TrybuildConfig) {
126 let source_dir = cargo::manifest_dir().unwrap();
127 let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
128 let crate_name = source_manifest.package.name.replace("-", "_");
129
130 let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
131
132 let generated_code = {
133 let _span = tracing::debug_span!(target: "hydro_build", "graph_codegen").entered();
134 compile_graph_trybuild(graph, extra_stmts, sidecars, &crate_name, deploy_mode)
135 };
136
137 let source = {
138 let _span = tracing::debug_span!(target: "hydro_build", "unparse_source").entered();
139 prettyplease::unparse(&generated_code)
140 };
141
142 let hash = format!("{:X}", Sha256::digest(&source))
143 .chars()
144 .take(8)
145 .collect::<String>();
146
147 let bin_name = if let Some(bin_name_prefix) = &bin_name_prefix {
148 format!("{}_{}", clean_bin_name_prefix(bin_name_prefix), &hash)
149 } else {
150 hash
151 };
152
153 let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
154
155 let examples_dir = match linking_mode {
157 LinkingMode::Static => path!(project_dir / "examples"),
158 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
159 LinkingMode::Dynamic => path!(project_dir / "dylib-examples" / "examples"),
160 };
161
162 fs::create_dir_all(&examples_dir).unwrap();
164
165 let out_path = path!(examples_dir / format!("{bin_name}.rs"));
166 {
167 let _span =
168 tracing::debug_span!(target: "hydro_build", "write_generated_sources").entered();
169 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
170 write_atomic(source.as_ref(), &out_path).unwrap();
171 }
172
173 if is_test {
174 write_staged_source_cached(source_dir.as_ref(), &crate_name, &project_dir);
175 }
176
177 if is_test {
178 if cur_bin_enabled_features.is_none() {
179 cur_bin_enabled_features = Some(vec![]);
180 }
181
182 cur_bin_enabled_features
183 .as_mut()
184 .unwrap()
185 .push("hydro___test".to_owned());
186 }
187
188 (
189 bin_name,
190 TrybuildConfig {
191 project_dir,
192 target_dir,
193 features: cur_bin_enabled_features,
194 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
195 linking_mode,
196 },
197 )
198}
199
200#[cfg(any(feature = "deploy", feature = "maelstrom"))]
201pub fn compile_graph_trybuild(
202 partitioned_graph: DfirGraph,
203 extra_stmts: &[syn::Stmt],
204 sidecars: &[syn::Expr],
205 crate_name: &str,
206 deploy_mode: DeployMode,
207) -> syn::File {
208 use crate::staging_util::get_this_crate;
209
210 let mut diagnostics = Diagnostics::new();
211 let dfir_expr: syn::Expr = syn::parse2(
212 partitioned_graph
213 .as_code("e! { __root_dfir_rs }, true, quote!(), &mut diagnostics)
214 .expect("DFIR code generation failed with diagnostics."),
215 )
216 .unwrap();
217
218 let orig_crate_name = quote::format_ident!("{}", crate_name);
219 let trybuild_crate_name_ident = quote::format_ident!("{}_hydro_trybuild", crate_name);
220 let root = get_this_crate();
221 let tokio_main_ident = format!("{}::runtime_support::tokio", root);
222 let dfir_ident = quote::format_ident!("{}", crate::compile::DFIR_IDENT);
223
224 let source_ast: syn::File = match deploy_mode {
225 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
226 DeployMode::Containerized => {
227 syn::parse_quote! {
228 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
229 use #trybuild_crate_name_ident::__root as #orig_crate_name;
230 use #orig_crate_name::*;
231 use #orig_crate_name::__staged::__deps::*;
232 use #root::prelude::*;
233 use #root::runtime_support::dfir_rs as __root_dfir_rs;
234 pub use #orig_crate_name::__staged;
235
236 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
237 async fn main() {
238 #root::telemetry::initialize_tracing();
239
240 #( #extra_stmts )*
241
242 let mut #dfir_ident = #dfir_expr;
243
244 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
245 #(
246 let _ = local_set.spawn_local( #sidecars ); )*
248
249 let _ = local_set.run_until(#dfir_ident.run()).await;
250 }
251 }
252 }
253 #[cfg(feature = "deploy")]
254 DeployMode::HydroDeploy => {
255 syn::parse_quote! {
256 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
257 use #trybuild_crate_name_ident::__root as #orig_crate_name;
258 use #orig_crate_name::*;
259 use #orig_crate_name::__staged::__deps::*;
260 use #root::prelude::*;
261 use #root::runtime_support::dfir_rs as __root_dfir_rs;
262 pub use #orig_crate_name::__staged;
263
264 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
265 async fn main() {
266 let __hydro_lang_trybuild_cli_owned: #root::runtime_support::hydro_deploy_integration::DeployPorts<#root::__staged::deploy::deploy_runtime::HydroMeta> = #root::runtime_support::launch::init_no_ack_start().await;
267 let __hydro_lang_trybuild_cli = &__hydro_lang_trybuild_cli_owned;
268
269 #( #extra_stmts )*
270
271 let mut #dfir_ident = #dfir_expr;
272 println!("ack start");
273
274 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
278 #(
279 let _ = local_set.spawn_local( #sidecars ); )*
281
282 let _ = local_set.run_until(#root::runtime_support::launch::run_stdin_commands(
283 async move {
284 #dfir_ident.run().await
285 }
286 )).await;
287 }
288 }
289 }
290 #[cfg(feature = "maelstrom")]
291 DeployMode::Maelstrom => {
292 syn::parse_quote! {
293 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
294 use #trybuild_crate_name_ident::__root as #orig_crate_name;
295 use #orig_crate_name::*;
296 use #orig_crate_name::__staged::__deps::*;
297 use #root::prelude::*;
298 use #root::runtime_support::dfir_rs as __root_dfir_rs;
299 pub use #orig_crate_name::__staged;
300
301 #[allow(unused)]
302 fn __hydro_runtime<'a>(
303 __hydro_lang_maelstrom_meta: &'a #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::MaelstromMeta
304 )
305 -> #root::runtime_support::dfir_rs::scheduled::context::Dfir<impl #root::runtime_support::dfir_rs::scheduled::context::TickClosure + 'a>
306 {
307 #( #extra_stmts )*
308
309 #dfir_expr
310 }
311
312 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
313 async fn main() {
314 #root::telemetry::initialize_tracing();
315
316 let __hydro_lang_maelstrom_meta = #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::maelstrom_init();
318
319 let mut #dfir_ident = __hydro_runtime(&__hydro_lang_maelstrom_meta);
320
321 __hydro_lang_maelstrom_meta.start_receiving(); let local_set = #root::runtime_support::tokio::task::LocalSet::new();
324 #(
325 let _ = local_set.spawn_local( #sidecars ); )*
327
328 let _ = local_set.run_until(#dfir_ident.run()).await;
329 }
330 }
331 }
332 };
333 source_ast
334}
335
336#[cfg(any(feature = "sim", feature = "maelstrom"))]
339pub struct ExampleBuildConfig<'a> {
340 pub trybuild: TrybuildConfig,
342 pub bin_name: String,
346 pub runtime_feature: &'a str,
349 pub example_name: String,
352 pub crate_type: Option<&'a str>,
355 pub set_trybuild_lib_name: bool,
358 pub allow_fuzz: bool,
361}
362
363#[cfg(any(feature = "sim", feature = "maelstrom"))]
365fn rustc_target_libdir() -> Option<String> {
366 static LIBDIR: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
367 LIBDIR
368 .get_or_init(|| {
369 let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
370 std::process::Command::new(rustc)
371 .args(["--print", "target-libdir"])
372 .output()
373 .ok()
374 .filter(|out| out.status.success())
375 .map(|out| String::from_utf8(out.stdout).unwrap().trim().to_owned())
376 })
377 .clone()
378}
379
380#[cfg(any(feature = "sim", feature = "maelstrom"))]
394pub fn compile_trybuild_example(config: ExampleBuildConfig<'_>) -> Result<tempfile::TempPath, ()> {
395 use std::process::{Command, Stdio};
396
397 let ExampleBuildConfig {
398 trybuild,
399 bin_name,
400 runtime_feature,
401 example_name,
402 crate_type,
403 set_trybuild_lib_name,
404 allow_fuzz,
405 } = config;
406
407 let is_fuzz = allow_fuzz && std::env::var("BOLERO_FUZZER").is_ok();
408 let has_custom_rustflags = std::env::var("RUSTFLAGS").is_ok();
411
412 let crate_to_compile = if is_fuzz {
414 trybuild.project_dir.clone()
415 } else {
416 path!(trybuild.project_dir / "dylib-examples")
417 };
418
419 let (mut prebuild_guard, _cargo_lock) = if !has_custom_rustflags {
420 let prebuild_span =
421 tracing::debug_span!(target: "hydro_build", "prebuild", bin_name = %bin_name).entered();
422
423 let mut features: Vec<String> = trybuild.features.clone().unwrap_or_default();
424 features.push(runtime_feature.to_owned());
425
426 let staged_paths = vec![
427 path!(trybuild.project_dir / "src" / "__staged.rs"),
428 path!(trybuild.project_dir / "Cargo.lock"),
429 std::env::current_exe().unwrap(),
430 ];
431
432 let project_dir = trybuild.project_dir.clone();
433 let features_for_closure = features.clone();
434 let is_fuzz_for_closure = is_fuzz;
435
436 let (guard, cargo_lock) = hydro_concurrent_cargo::run_prebuild(
437 &trybuild.target_dir,
438 trybuild.project_dir.file_name().unwrap().to_str().unwrap(),
439 &features,
440 &staged_paths,
441 |prebuild_target| {
442 let features_str = features_for_closure.join(",");
443
444 let prebuild_crate = if is_fuzz_for_closure {
456 project_dir.clone()
457 } else {
458 path!(project_dir / "dylib-examples")
459 };
460 let mut lib_cmd = Command::new("cargo");
461 lib_cmd.current_dir(&prebuild_crate);
462 lib_cmd.args(["build", "--locked", "--lib"]);
463 lib_cmd.args(["--target-dir", prebuild_target.to_str().unwrap()]);
464 lib_cmd.arg("--no-default-features");
465 lib_cmd.args(["--features", &features_str]);
466 lib_cmd.args(["--config", "build.incremental = false"]);
467 lib_cmd.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
468 let status = lib_cmd.stdin(Stdio::null()).status().unwrap();
469 if !status.success() {
470 panic!("dep prebuild failed");
471 }
472 },
473 );
474
475 drop(prebuild_span);
479 (Some(guard), Some(cargo_lock))
480 } else {
481 (None, None)
482 };
483
484 let example_crate_name = example_name.replace('-', "_");
488 let example_source_path = format!(
489 "{}/examples/{}.rs",
490 crate_to_compile
491 .strip_prefix(&trybuild.project_dir)
492 .ok()
493 .and_then(|p| p.to_str())
494 .unwrap_or_default(),
495 example_name
496 )
497 .trim_start_matches('/')
498 .to_owned();
499
500 let (prebuild_stamp, template_path) = match (&mut prebuild_guard, is_fuzz) {
505 (Some(guard), false) => (
506 Some(guard.read_stamp()),
507 Some(final_rustc::template_path(guard.lock_path())),
508 ),
509 _ => (None, None),
510 };
511
512 if let (Some(stamp), Some(template_path)) = (&prebuild_stamp, &template_path)
513 && let Some(template) = final_rustc::load(template_path, stamp)
514 {
515 let jobs_dir = trybuild.target_dir.join("jobs");
518 let _job_guard = hydro_concurrent_cargo::lock_job_dir(&jobs_dir, &bin_name);
519
520 let final_rustc_span =
521 tracing::debug_span!(target: "hydro_build", "final_rustc", bin_name = %bin_name)
522 .entered();
523
524 let metadata = format!(
527 "{:X}",
528 Sha256::digest(format!("{bin_name}\t{runtime_feature}"))
529 )
530 .chars()
531 .take(16)
532 .collect::<String>();
533
534 let mut envs = vec![(
535 "STAGELEFT_TRYBUILD_BUILD_STAGED".to_owned(),
536 std::ffi::OsString::from("1"),
537 )];
538 envs.push((
541 "CARGO_MANIFEST_DIR".to_owned(),
542 crate_to_compile.clone().into_os_string(),
543 ));
544 envs.push((
545 "CARGO_CRATE_NAME".to_owned(),
546 std::ffi::OsString::from(&example_crate_name),
547 ));
548 if set_trybuild_lib_name {
549 envs.push((
550 "TRYBUILD_LIB_NAME".to_owned(),
551 std::ffi::OsString::from(&bin_name),
552 ));
553 }
554
555 let replayed = final_rustc::replay(
556 &template,
557 final_rustc::ReplayParams {
558 crate_name: &example_crate_name,
559 source_path: &example_source_path,
560 out_dir: &path!(jobs_dir / bin_name / "out"),
561 metadata: &metadata,
562 envs,
563 },
564 );
565 drop(final_rustc_span);
566
567 match replayed {
568 Ok(artifact) => {
569 let out_file = tempfile::NamedTempFile::new().unwrap().into_temp_path();
570 fs::copy(&artifact, &out_file).unwrap();
571 return Ok(out_file);
572 }
573 Err(message) => {
574 tracing::debug!(
575 target: "hydro_build",
576 "final rustc replay failed, falling back to cargo: {message}"
577 );
578 let _ = fs::remove_file(template_path);
579 }
580 }
581 }
582
583 let final_target_dir = if !has_custom_rustflags {
584 let shared_debug = trybuild.target_dir.join("debug");
585 let jobs_dir = trybuild.target_dir.join("jobs");
586 hydro_concurrent_cargo::setup_job_dir(&jobs_dir, &bin_name, &shared_debug)
587 } else {
588 trybuild.target_dir.clone()
589 };
590
591 let _job_build_guard = if !has_custom_rustflags {
593 let populate_span =
594 tracing::debug_span!(target: "hydro_build", "populate_job_dir", bin_name = %bin_name)
595 .entered();
596 let shared_debug = trybuild.target_dir.join("debug");
597 let guard = hydro_concurrent_cargo::populate_job_build_dir(
598 &final_target_dir.join("debug"),
599 &shared_debug,
600 );
601 drop(populate_span);
604 Some(guard)
605 } else {
606 None
607 };
608
609 let final_build_span =
610 tracing::debug_span!(target: "hydro_build", "final_build", bin_name = %bin_name).entered();
611 let mut command = Command::new("cargo");
612 command.current_dir(&crate_to_compile);
613 command.args([
614 "rustc",
615 if has_custom_rustflags {
616 "--locked"
617 } else {
618 "--frozen"
619 },
620 "-v",
623 ]);
624 command.args(["--example", &example_name]);
625 command.args(["--target-dir", final_target_dir.to_str().unwrap()]);
626 command.arg("--no-default-features");
633 command.args([
634 "--features",
635 &trybuild
636 .features
637 .clone()
638 .into_iter()
639 .flatten()
640 .chain([runtime_feature.to_owned()])
641 .collect::<Vec<_>>()
642 .join(","),
643 ]);
644 command.args(["--config", "build.incremental = false"]);
645 if let Some(crate_type) = crate_type {
646 command.args(["--crate-type", crate_type]);
647 }
648 command.arg("--message-format=json-diagnostic-rendered-ansi");
649 command.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
650 if set_trybuild_lib_name {
651 command.env("TRYBUILD_LIB_NAME", &bin_name);
652 }
653
654 command.arg("--");
655
656 if cfg!(any(target_os = "linux", target_os = "macos")) {
657 let debug_path = if let Ok(target) = std::env::var("CARGO_BUILD_TARGET") {
658 path!(final_target_dir / target / "debug")
659 } else {
660 path!(final_target_dir / "debug")
661 };
662
663 let mut rpaths = vec![debug_path.clone(), path!(debug_path / "deps")];
667 if let Some(libdir) = rustc_target_libdir() {
668 rpaths.push(PathBuf::from(libdir));
669 }
670 for rpath in rpaths {
671 if cfg!(target_os = "macos") {
672 command.args([
676 "-Clink-arg=-rpath".to_owned(),
677 format!("-Clink-arg={}", rpath.to_str().unwrap()),
678 ]);
679 } else {
680 command.args([format!("-Clink-arg=-Wl,-rpath,{}", rpath.to_str().unwrap())]);
681 }
682 }
683
684 if cfg!(all(target_os = "linux", target_env = "gnu")) {
685 command.arg(
686 "-Clink-args=-Wl,-z,nodelete",
688 );
689 }
690 }
691
692 if allow_fuzz && let Ok(fuzzer) = std::env::var("BOLERO_FUZZER") {
693 command.env_remove("BOLERO_FUZZER");
694
695 if fuzzer == "libfuzzer" {
696 #[cfg(target_os = "macos")]
697 {
698 command.args(["-Clink-arg=-undefined", "-Clink-arg=dynamic_lookup"]);
699 }
700
701 #[cfg(target_os = "linux")]
702 {
703 command.args(["-Clink-arg=-Wl,--unresolved-symbols=ignore-all"]);
704 }
705 }
706 }
707
708 tracing::debug!(
709 target: "hydro_build",
710 "final build command (cwd={}): {:?}",
711 crate_to_compile.display(),
712 command
713 );
714
715 let mut spawned = command
716 .stdout(Stdio::piped())
717 .stderr(Stdio::piped())
718 .stdin(Stdio::null())
719 .spawn()
720 .unwrap();
721 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
722 let stderr_handle = spawned.stderr.take().unwrap();
723 let stderr_thread = std::thread::spawn(move || {
724 use std::io::Read;
725 let mut buf = String::new();
726 std::io::BufReader::new(stderr_handle)
727 .read_to_string(&mut buf)
728 .unwrap();
729 buf
730 });
731
732 let mut out = Err(());
733 for message in cargo_metadata::Message::parse_stream(reader) {
734 match message.unwrap() {
735 cargo_metadata::Message::CompilerArtifact(artifact) => {
736 let is_output = artifact.target.is_example();
738
739 if is_output {
740 let path = artifact.filenames.first().unwrap();
741 let path_buf: PathBuf = path.clone().into();
742 out = Ok(path_buf);
743 }
744 }
745 cargo_metadata::Message::CompilerMessage(mut msg) => {
746 if let Some(rendered) = msg.message.rendered.as_mut() {
749 let file_names = msg
750 .message
751 .spans
752 .iter()
753 .map(|s| &s.file_name)
754 .collect::<std::collections::BTreeSet<_>>();
755 for file_name in file_names {
756 *rendered = rendered.replace(
757 file_name,
758 &format!("(full path) {}/{file_name}", trybuild.project_dir.display()),
759 )
760 }
761 }
762 eprintln!("{}", msg.message);
763 }
764 cargo_metadata::Message::TextLine(line) => {
765 eprintln!("{}", line);
766 }
767 cargo_metadata::Message::BuildFinished(_) => {}
768 cargo_metadata::Message::BuildScriptExecuted(_) => {}
769 msg => panic!("Unexpected message type: {:?}", msg),
770 }
771 }
772
773 spawned.wait().unwrap();
774 let stderr_output = stderr_thread.join().unwrap();
775 drop(final_build_span);
776
777 if !has_custom_rustflags {
780 for line in stderr_output.lines() {
781 if line.contains("Compiling") && !line.contains("dylib-examples") {
782 panic!(
783 "unexpected recompilation in final build: {line}\nfull stderr:\n{stderr_output}"
784 );
785 }
786 }
787 }
788
789 if out.is_err() {
790 panic!("final build failed to produce binary.\nstderr:\n{stderr_output}");
791 }
792
793 if let (Some(stamp), Some(template_path)) = (&prebuild_stamp, &template_path)
796 && let Some(argv) = final_rustc::extract_rustc_command(&stderr_output, &example_crate_name)
797 && let Some(argv) = final_rustc::build_template(
798 argv,
799 &example_crate_name,
800 &example_name,
801 &final_target_dir,
802 &trybuild.target_dir,
803 )
804 {
805 let _ = final_rustc::store(
806 template_path,
807 &final_rustc::RustcTemplate {
808 stamp: stamp.clone(),
809 cwd: trybuild.project_dir.clone(),
810 argv,
811 },
812 );
813 }
814
815 let out_file = tempfile::NamedTempFile::new().unwrap().into_temp_path();
816 fs::copy(out.as_ref().unwrap(), &out_file).unwrap();
817 Ok(out_file)
818}
819
820pub(crate) fn write_staged_source_cached(source_dir: &Path, crate_name: &str, project_dir: &Path) {
833 let _span = tracing::debug_span!(target: "hydro_build", "gen_staged").entered();
834
835 let staged_path = path!(project_dir / "src" / "__staged.rs");
836 let stamp_path = path!(project_dir / ".hydro-staged-stamp");
837
838 let exe_stamp = std::env::current_exe().ok().and_then(|exe| {
839 let mtime = fs::metadata(&exe)
840 .ok()?
841 .modified()
842 .ok()?
843 .duration_since(std::time::SystemTime::UNIX_EPOCH)
844 .ok()?
845 .as_nanos();
846 Some(format!("{}\t{}", exe.display(), mtime))
847 });
848
849 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
850
851 fs::create_dir_all(path!(project_dir / "src")).unwrap();
852
853 let mut stamp_file = File::options()
857 .read(true)
858 .write(true)
859 .create(true)
860 .truncate(false)
861 .open(&stamp_path)
862 .unwrap();
863 stamp_file.lock().unwrap();
864
865 let mut existing_stamp = String::new();
866 if stamp_file.read_to_string(&mut existing_stamp).is_err() {
867 existing_stamp.clear();
868 }
869
870 if let Some(stamp) = &exe_stamp
871 && existing_stamp == *stamp
872 && staged_path.exists()
873 {
874 return;
875 }
876
877 let raw_toml_manifest = toml::from_str::<toml::Value>(
878 &fs::read_to_string(path!(source_dir / "Cargo.toml")).unwrap(),
879 )
880 .unwrap();
881
882 let maybe_custom_lib_path = raw_toml_manifest
883 .get("lib")
884 .and_then(|lib| lib.get("path"))
885 .and_then(|path| path.as_str());
886
887 let mut gen_staged = stageleft_tool::gen_staged_trybuild(
888 &maybe_custom_lib_path
889 .map(|s| path!(source_dir / s))
890 .unwrap_or_else(|| path!(source_dir / "src" / "lib.rs")),
891 &path!(source_dir / "Cargo.toml"),
892 crate_name,
893 Some("hydro___test".to_owned()),
894 );
895
896 gen_staged.attrs.insert(
897 0,
898 syn::parse_quote! {
899 #![allow(
900 unused,
901 ambiguous_glob_reexports,
902 clippy::suspicious_else_formatting,
903 unexpected_cfgs,
904 reason = "generated code"
905 )]
906 },
907 );
908
909 let inlined_staged = prettyplease::unparse(&gen_staged);
910
911 write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
912
913 if let Some(stamp) = exe_stamp {
914 stamp_file.set_len(0).unwrap();
915 stamp_file.seek(SeekFrom::Start(0)).unwrap();
916 stamp_file.write_all(stamp.as_bytes()).unwrap();
917 }
918}
919
920pub fn create_trybuild()
921-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
922 let _span = tracing::debug_span!(target: "hydro_build", "create_trybuild").entered();
923 let Metadata {
924 target_directory: target_dir,
925 workspace_root: workspace,
926 packages,
927 } = {
928 let _span = tracing::debug_span!(target: "hydro_build", "cargo_metadata").entered();
929 cargo::metadata()?
930 };
931
932 let source_dir = cargo::manifest_dir()?;
933 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
934
935 let mut dev_dependency_features = vec![];
936 source_manifest.dev_dependencies.retain(|k, v| {
937 if source_manifest.dependencies.contains_key(k) {
938 for feat in &v.features {
940 dev_dependency_features.push(format!("{}/{}", k, feat));
941 }
942
943 false
944 } else {
945 dev_dependency_features.push(format!("dep:{k}"));
947
948 v.optional = true;
949 true
950 }
951 });
952
953 let mut features = if std::env::var("RUNNING_AS_EXAMPLE_TEST").is_ok_and(|v| v == "1") {
959 None
960 } else {
961 features::find()
962 };
963
964 let path_dependencies = source_manifest
965 .dependencies
966 .iter()
967 .filter_map(|(name, dep)| {
968 let path = dep.path.as_ref()?;
969 if packages.iter().any(|p| &p.name == name) {
970 None
972 } else {
973 Some(PathDependency {
974 name: name.clone(),
975 normalized_path: path.canonicalize().ok()?,
976 })
977 }
978 })
979 .collect();
980
981 let crate_name = source_manifest.package.name.clone();
982 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
983 fs::create_dir_all(&project_dir)?;
984
985 let project_name = format!("{}-hydro-trybuild", crate_name);
986 let mut manifest = Runner::make_manifest(
987 &workspace,
988 &project_name,
989 &source_dir,
990 &packages,
991 &[],
992 source_manifest,
993 )?;
994
995 if let Some(enabled_features) = &mut features {
996 enabled_features
997 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
998 }
999
1000 for runtime_feature in HYDRO_RUNTIME_FEATURES {
1001 manifest.features.insert(
1002 format!("hydro___feature_{runtime_feature}"),
1003 vec![format!("hydro_lang/{runtime_feature}")],
1004 );
1005 }
1006
1007 manifest
1008 .dependencies
1009 .get_mut("hydro_lang")
1010 .unwrap()
1011 .features
1012 .push("runtime_support".to_owned());
1013
1014 manifest
1015 .features
1016 .insert("hydro___test".to_owned(), dev_dependency_features);
1017
1018 if manifest
1019 .workspace
1020 .as_ref()
1021 .is_some_and(|w| w.dependencies.is_empty())
1022 {
1023 manifest.workspace = None;
1024 }
1025
1026 let project = Project {
1027 dir: project_dir,
1028 source_dir,
1029 target_dir,
1030 name: project_name.clone(),
1031 update: Update::env()?,
1032 has_pass: false,
1033 has_compile_fail: false,
1034 features,
1035 workspace,
1036 path_dependencies,
1037 manifest,
1038 keep_going: false,
1039 };
1040
1041 {
1042 let _span = tracing::debug_span!(target: "hydro_build", "write_project_files").entered();
1043 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
1044
1045 let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
1046 project_lock.lock()?;
1047
1048 fs::create_dir_all(path!(project.dir / "src"))?;
1049 fs::create_dir_all(path!(project.dir / "examples"))?;
1050
1051 let crate_name_ident = syn::Ident::new(
1052 &crate_name.replace("-", "_"),
1053 proc_macro2::Span::call_site(),
1054 );
1055
1056 write_atomic(
1057 prettyplease::unparse(&syn::parse_quote! {
1058 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1059
1060 pub mod __root {
1061 pub use #crate_name_ident::*;
1062 #[cfg(feature = "hydro___test")]
1063 pub use super::__staged;
1064 }
1065
1066 #[cfg(feature = "hydro___test")]
1067 pub mod __staged;
1068 })
1069 .as_bytes(),
1070 &path!(project.dir / "src" / "lib.rs"),
1071 )
1072 .unwrap();
1073
1074 let base_manifest = toml::to_string(&project.manifest)?;
1075
1076 let feature_names: Vec<_> = project.manifest.features.keys().cloned().collect();
1078
1079 let dylib_dir = path!(project.dir / "dylib");
1081 fs::create_dir_all(path!(dylib_dir / "src"))?;
1082
1083 let trybuild_crate_name_ident = syn::Ident::new(
1084 &project_name.replace("-", "_"),
1085 proc_macro2::Span::call_site(),
1086 );
1087 write_atomic(
1088 [
1094 "// v2: dylib must be built as a dependency (prefer-dynamic).\n".as_bytes(),
1095 prettyplease::unparse(&syn::parse_quote! {
1096 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1097 pub use #trybuild_crate_name_ident::*;
1098 })
1099 .as_bytes(),
1100 ]
1101 .concat()
1102 .as_slice(),
1103 &path!(dylib_dir / "src" / "lib.rs"),
1104 )?;
1105
1106 let serialized_edition = toml::to_string(
1107 &vec![("edition", &project.manifest.package.edition)]
1108 .into_iter()
1109 .collect::<std::collections::HashMap<_, _>>(),
1110 )
1111 .unwrap();
1112
1113 let dylib_features_section = feature_names
1116 .iter()
1117 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
1118 .collect::<Vec<_>>()
1119 .join("\n");
1120
1121 let dylib_manifest = format!(
1122 r#"[package]
1123name = "{project_name}-dylib"
1124version = "0.0.0"
1125{}
1126
1127[lib]
1128crate-type = ["{}"]
1129
1130[dependencies]
1131{project_name} = {{ path = "..", default-features = false }}
1132
1133[features]
1134{dylib_features_section}
1135"#,
1136 serialized_edition,
1137 if cfg!(target_os = "windows") {
1138 "rlib"
1139 } else {
1140 "dylib"
1141 }
1142 );
1143 write_atomic(dylib_manifest.as_ref(), &path!(dylib_dir / "Cargo.toml"))?;
1144
1145 let dylib_examples_dir = path!(project.dir / "dylib-examples");
1146 fs::create_dir_all(path!(dylib_examples_dir / "src"))?;
1147 fs::create_dir_all(path!(dylib_examples_dir / "examples"))?;
1148
1149 write_atomic(
1150 b"#![allow(unused_crate_dependencies)]\n",
1151 &path!(dylib_examples_dir / "src" / "lib.rs"),
1152 )?;
1153
1154 let features_section = feature_names
1156 .iter()
1157 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
1158 .collect::<Vec<_>>()
1159 .join("\n");
1160
1161 let dylib_examples_manifest = format!(
1173 r#"[package]
1174name = "{project_name}-dylib-examples"
1175version = "0.0.0"
1176{}
1177
1178[dependencies]
1179{project_name} = {{ package = "{project_name}-dylib", path = "../dylib", default-features = false }}
1180
1181[features]
1182{features_section}
1183
1184[[example]]
1185name = "sim-dylib"
1186crate-type = ["cdylib"]
1187"#,
1188 serialized_edition
1189 );
1190 write_atomic(
1191 dylib_examples_manifest.as_ref(),
1192 &path!(dylib_examples_dir / "Cargo.toml"),
1193 )?;
1194
1195 let sim_dylib_contents = prettyplease::unparse(&syn::parse_quote! {
1197 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1198 include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
1199 });
1200 write_atomic(
1201 sim_dylib_contents.as_bytes(),
1202 &path!(project.dir / "examples" / "sim-dylib.rs"),
1203 )?;
1204 write_atomic(
1205 sim_dylib_contents.as_bytes(),
1206 &path!(dylib_examples_dir / "examples" / "sim-dylib.rs"),
1207 )?;
1208
1209 let workspace_manifest = format!(
1210 r#"{}
1211[[example]]
1212name = "sim-dylib"
1213crate-type = ["cdylib"]
1214
1215[workspace]
1216members = ["dylib", "dylib-examples"]
1217"#,
1218 base_manifest,
1219 );
1220
1221 write_atomic(
1222 workspace_manifest.as_ref(),
1223 &path!(project.dir / "Cargo.toml"),
1224 )?;
1225
1226 let manifest_hash = {
1229 let mut hasher = Sha256::new();
1230 hasher.update(&workspace_manifest);
1231 hasher.update(&dylib_manifest);
1232 hasher.update(&dylib_examples_manifest);
1233 format!("{:X}", hasher.finalize())
1234 .chars()
1235 .take(8)
1236 .collect::<String>()
1237 };
1238
1239 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
1240 let workspace_cargo_lock_contents_and_hash = if workspace_cargo_lock.exists() {
1241 let cargo_lock_contents = fs::read_to_string(&workspace_cargo_lock)?;
1242
1243 let hash = format!("{:X}", Sha256::digest(&cargo_lock_contents))
1244 .chars()
1245 .take(8)
1246 .collect::<String>();
1247
1248 Some((cargo_lock_contents, hash))
1249 } else {
1250 None
1251 };
1252
1253 let trybuild_hash = format!(
1254 "{}-{}",
1255 manifest_hash,
1256 workspace_cargo_lock_contents_and_hash
1257 .as_ref()
1258 .map(|(_contents, hash)| &**hash)
1259 .unwrap_or_default()
1260 );
1261
1262 if !check_contents(
1263 trybuild_hash.as_bytes(),
1264 &path!(project.dir / ".hydro-trybuild-manifest"),
1265 )
1266 .is_ok_and(|b| b)
1267 {
1268 let _span = tracing::debug_span!(target: "hydro_build", "update_lockfile").entered();
1269 if let Some((cargo_lock_contents, _)) = workspace_cargo_lock_contents_and_hash {
1271 write_atomic(
1274 cargo_lock_contents.as_ref(),
1275 &path!(project.dir / "Cargo.lock"),
1276 )?;
1277 } else {
1278 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
1279 }
1280
1281 std::process::Command::new("cargo")
1283 .current_dir(&project.dir)
1284 .args(["update", "-w"]) .stdout(std::process::Stdio::null())
1286 .stderr(std::process::Stdio::null())
1287 .status()
1288 .unwrap();
1289
1290 write_atomic(
1291 trybuild_hash.as_bytes(),
1292 &path!(project.dir / ".hydro-trybuild-manifest"),
1293 )?;
1294 }
1295
1296 let examples_folder = path!(project.dir / "examples");
1298 fs::create_dir_all(&examples_folder)?;
1299
1300 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
1301 if workspace_dot_cargo_config_toml.exists() {
1302 let dot_cargo_folder = path!(project.dir / ".cargo");
1303 fs::create_dir_all(&dot_cargo_folder)?;
1304
1305 write_atomic(
1306 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
1307 &path!(dot_cargo_folder / "config.toml"),
1308 )?;
1309 }
1310
1311 let vscode_folder = path!(project.dir / ".vscode");
1312 fs::create_dir_all(&vscode_folder)?;
1313 write_atomic(
1314 include_bytes!("./vscode-trybuild.json"),
1315 &path!(vscode_folder / "settings.json"),
1316 )?;
1317 }
1318
1319 Ok((
1320 project.dir.as_ref().into(),
1321 project.target_dir.as_ref().into(),
1322 project.features,
1323 ))
1324}
1325
1326fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
1327 let mut file = File::options()
1328 .read(true)
1329 .write(false)
1330 .create(false)
1331 .truncate(false)
1332 .open(path)?;
1333 file.lock()?;
1334
1335 let mut existing_contents = Vec::new();
1336 file.read_to_end(&mut existing_contents)?;
1337 Ok(existing_contents == contents)
1338}
1339
1340pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
1341 let mut file = File::options()
1342 .read(true)
1343 .write(true)
1344 .create(true)
1345 .truncate(false)
1346 .open(path)?;
1347
1348 let mut existing_contents = Vec::new();
1349 file.read_to_end(&mut existing_contents)?;
1350 if existing_contents != contents {
1351 file.lock()?;
1352 file.seek(SeekFrom::Start(0))?;
1353 file.set_len(0)?;
1354 file.write_all(contents)?;
1355 }
1356
1357 Ok(())
1358}