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;
9use sha2::{Digest, Sha256};
10#[cfg(any(feature = "deploy", feature = "maelstrom"))]
11use stageleft::internal::quote;
12use trybuild_internals_api::cargo::{self, Metadata};
13use trybuild_internals_api::env::Update;
14use trybuild_internals_api::run::{PathDependency, Project};
15use trybuild_internals_api::{Runner, dependencies, features, path};
16
17pub const HYDRO_RUNTIME_FEATURES: &[&str] = &[
18 "deploy_integration",
19 "runtime_measure",
20 "docker_runtime",
21 "ecs_runtime",
22 "maelstrom_runtime",
23 "sim_runtime",
24];
25
26#[cfg(any(feature = "deploy", feature = "maelstrom"))]
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum LinkingMode {
32 #[cfg_attr(
35 not(feature = "deploy"),
36 expect(
37 dead_code,
38 reason = "only constructed by the deploy backends; Maelstrom-only builds use Dynamic"
39 )
40 )]
41 Static,
42 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
43 Dynamic,
44}
45
46#[cfg(any(feature = "deploy", feature = "maelstrom"))]
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum DeployMode {
50 #[cfg(feature = "deploy")]
51 HydroDeploy,
53 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
54 Containerized,
56 #[cfg(feature = "maelstrom")]
57 Maelstrom,
59}
60
61pub(crate) static IS_TEST: std::sync::atomic::AtomicBool =
62 std::sync::atomic::AtomicBool::new(false);
63
64pub(crate) static CONCURRENT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
65
66pub fn init_test() {
80 IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
81}
82
83#[cfg(any(feature = "deploy", feature = "maelstrom"))]
84fn clean_bin_name_prefix(bin_name_prefix: &str) -> String {
85 bin_name_prefix
86 .replace("::", "__")
87 .replace(" ", "_")
88 .replace(",", "_")
89 .replace("<", "_")
90 .replace(">", "")
91 .replace("(", "")
92 .replace(")", "")
93 .replace("{", "_")
94 .replace("}", "_")
95}
96
97#[derive(Debug, Clone)]
98pub struct TrybuildConfig {
99 pub project_dir: PathBuf,
100 pub target_dir: PathBuf,
101 pub features: Option<Vec<String>>,
102 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
103 #[cfg_attr(
106 not(feature = "deploy"),
107 expect(dead_code, reason = "only read by the deploy backends")
108 )]
109 pub linking_mode: LinkingMode,
113}
114
115#[cfg(any(feature = "deploy", feature = "maelstrom"))]
116pub fn create_graph_trybuild(
117 graph: DfirGraph,
118 extra_stmts: &[syn::Stmt],
119 sidecars: &[syn::Expr],
120 bin_name_prefix: Option<&str>,
121 deploy_mode: DeployMode,
122 linking_mode: LinkingMode,
123) -> (String, TrybuildConfig) {
124 let source_dir = cargo::manifest_dir().unwrap();
125 let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
126 let crate_name = source_manifest.package.name.replace("-", "_");
127
128 let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
129
130 let generated_code =
131 compile_graph_trybuild(graph, extra_stmts, sidecars, &crate_name, deploy_mode);
132
133 let inlined_staged = if is_test {
134 let raw_toml_manifest = toml::from_str::<toml::Value>(
135 &fs::read_to_string(path!(source_dir / "Cargo.toml")).unwrap(),
136 )
137 .unwrap();
138
139 let maybe_custom_lib_path = raw_toml_manifest
140 .get("lib")
141 .and_then(|lib| lib.get("path"))
142 .and_then(|path| path.as_str());
143
144 let mut gen_staged = stageleft_tool::gen_staged_trybuild(
145 &maybe_custom_lib_path
146 .map(|s| path!(source_dir / s))
147 .unwrap_or_else(|| path!(source_dir / "src" / "lib.rs")),
148 &path!(source_dir / "Cargo.toml"),
149 &crate_name,
150 Some("hydro___test".to_owned()),
151 );
152
153 gen_staged.attrs.insert(
154 0,
155 syn::parse_quote! {
156 #![allow(
157 unused,
158 ambiguous_glob_reexports,
159 clippy::suspicious_else_formatting,
160 unexpected_cfgs,
161 reason = "generated code"
162 )]
163 },
164 );
165
166 Some(prettyplease::unparse(&gen_staged))
167 } else {
168 None
169 };
170
171 let source = prettyplease::unparse(&generated_code);
172
173 let hash = format!("{:X}", Sha256::digest(&source))
174 .chars()
175 .take(8)
176 .collect::<String>();
177
178 let bin_name = if let Some(bin_name_prefix) = &bin_name_prefix {
179 format!("{}_{}", clean_bin_name_prefix(bin_name_prefix), &hash)
180 } else {
181 hash
182 };
183
184 let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
185
186 let examples_dir = match linking_mode {
188 LinkingMode::Static => path!(project_dir / "examples"),
189 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
190 LinkingMode::Dynamic => path!(project_dir / "dylib-examples" / "examples"),
191 };
192
193 fs::create_dir_all(&examples_dir).unwrap();
195
196 let out_path = path!(examples_dir / format!("{bin_name}.rs"));
197 {
198 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
199 write_atomic(source.as_ref(), &out_path).unwrap();
200 }
201
202 if let Some(inlined_staged) = inlined_staged {
203 let staged_path = path!(project_dir / "src" / "__staged.rs");
204 {
205 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
206 write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
207 }
208 }
209
210 if is_test {
211 if cur_bin_enabled_features.is_none() {
212 cur_bin_enabled_features = Some(vec![]);
213 }
214
215 cur_bin_enabled_features
216 .as_mut()
217 .unwrap()
218 .push("hydro___test".to_owned());
219 }
220
221 (
222 bin_name,
223 TrybuildConfig {
224 project_dir,
225 target_dir,
226 features: cur_bin_enabled_features,
227 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
228 linking_mode,
229 },
230 )
231}
232
233#[cfg(any(feature = "deploy", feature = "maelstrom"))]
234pub fn compile_graph_trybuild(
235 partitioned_graph: DfirGraph,
236 extra_stmts: &[syn::Stmt],
237 sidecars: &[syn::Expr],
238 crate_name: &str,
239 deploy_mode: DeployMode,
240) -> syn::File {
241 use crate::staging_util::get_this_crate;
242
243 let mut diagnostics = Diagnostics::new();
244 let dfir_expr: syn::Expr = syn::parse2(
245 partitioned_graph
246 .as_code("e! { __root_dfir_rs }, true, quote!(), &mut diagnostics)
247 .expect("DFIR code generation failed with diagnostics."),
248 )
249 .unwrap();
250
251 let orig_crate_name = quote::format_ident!("{}", crate_name);
252 let trybuild_crate_name_ident = quote::format_ident!("{}_hydro_trybuild", crate_name);
253 let root = get_this_crate();
254 let tokio_main_ident = format!("{}::runtime_support::tokio", root);
255 let dfir_ident = quote::format_ident!("{}", crate::compile::DFIR_IDENT);
256
257 let source_ast: syn::File = match deploy_mode {
258 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
259 DeployMode::Containerized => {
260 syn::parse_quote! {
261 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
262 use #trybuild_crate_name_ident::__root as #orig_crate_name;
263 use #orig_crate_name::*;
264 use #orig_crate_name::__staged::__deps::*;
265 use #root::prelude::*;
266 use #root::runtime_support::dfir_rs as __root_dfir_rs;
267 pub use #orig_crate_name::__staged;
268
269 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
270 async fn main() {
271 #root::telemetry::initialize_tracing();
272
273 #( #extra_stmts )*
274
275 let mut #dfir_ident = #dfir_expr;
276
277 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(#dfir_ident.run()).await;
283 }
284 }
285 }
286 #[cfg(feature = "deploy")]
287 DeployMode::HydroDeploy => {
288 syn::parse_quote! {
289 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
290 use #trybuild_crate_name_ident::__root as #orig_crate_name;
291 use #orig_crate_name::*;
292 use #orig_crate_name::__staged::__deps::*;
293 use #root::prelude::*;
294 use #root::runtime_support::dfir_rs as __root_dfir_rs;
295 pub use #orig_crate_name::__staged;
296
297 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
298 async fn main() {
299 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;
300 let __hydro_lang_trybuild_cli = &__hydro_lang_trybuild_cli_owned;
301
302 #( #extra_stmts )*
303
304 let mut #dfir_ident = #dfir_expr;
305 println!("ack start");
306
307 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
311 #(
312 let _ = local_set.spawn_local( #sidecars ); )*
314
315 let _ = local_set.run_until(#root::runtime_support::launch::run_stdin_commands(
316 async move {
317 #dfir_ident.run().await
318 }
319 )).await;
320 }
321 }
322 }
323 #[cfg(feature = "maelstrom")]
324 DeployMode::Maelstrom => {
325 syn::parse_quote! {
326 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
327 use #trybuild_crate_name_ident::__root as #orig_crate_name;
328 use #orig_crate_name::*;
329 use #orig_crate_name::__staged::__deps::*;
330 use #root::prelude::*;
331 use #root::runtime_support::dfir_rs as __root_dfir_rs;
332 pub use #orig_crate_name::__staged;
333
334 #[allow(unused)]
335 fn __hydro_runtime<'a>(
336 __hydro_lang_maelstrom_meta: &'a #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::MaelstromMeta
337 )
338 -> #root::runtime_support::dfir_rs::scheduled::context::Dfir<impl #root::runtime_support::dfir_rs::scheduled::context::TickClosure + 'a>
339 {
340 #( #extra_stmts )*
341
342 #dfir_expr
343 }
344
345 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
346 async fn main() {
347 #root::telemetry::initialize_tracing();
348
349 let __hydro_lang_maelstrom_meta = #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::maelstrom_init();
351
352 let mut #dfir_ident = __hydro_runtime(&__hydro_lang_maelstrom_meta);
353
354 __hydro_lang_maelstrom_meta.start_receiving(); let local_set = #root::runtime_support::tokio::task::LocalSet::new();
357 #(
358 let _ = local_set.spawn_local( #sidecars ); )*
360
361 let _ = local_set.run_until(#dfir_ident.run()).await;
362 }
363 }
364 }
365 };
366 source_ast
367}
368
369#[cfg(any(feature = "sim", feature = "maelstrom"))]
372pub struct ExampleBuildConfig<'a> {
373 pub trybuild: TrybuildConfig,
375 pub bin_name: String,
379 pub runtime_feature: &'a str,
382 pub example_name: String,
385 pub crate_type: Option<&'a str>,
388 pub set_trybuild_lib_name: bool,
391 pub allow_fuzz: bool,
394}
395
396#[cfg(any(feature = "sim", feature = "maelstrom"))]
404pub fn compile_trybuild_example(config: ExampleBuildConfig<'_>) -> Result<tempfile::TempPath, ()> {
405 use std::process::{Command, Stdio};
406
407 let ExampleBuildConfig {
408 trybuild,
409 bin_name,
410 runtime_feature,
411 example_name,
412 crate_type,
413 set_trybuild_lib_name,
414 allow_fuzz,
415 } = config;
416
417 let is_fuzz = allow_fuzz && std::env::var("BOLERO_FUZZER").is_ok();
418 let has_custom_rustflags = std::env::var("RUSTFLAGS").is_ok();
421
422 let crate_to_compile = if is_fuzz {
424 trybuild.project_dir.clone()
425 } else {
426 path!(trybuild.project_dir / "dylib-examples")
427 };
428
429 let (final_target_dir, _prebuild_guard, _cargo_lock) = if !has_custom_rustflags {
430 let shared_debug = trybuild.target_dir.join("debug");
431 let jobs_dir = trybuild.target_dir.join("jobs");
432 let per_job = hydro_concurrent_cargo::setup_job_dir(&jobs_dir, &bin_name, &shared_debug);
433
434 let mut features: Vec<String> = trybuild.features.clone().unwrap_or_default();
435 features.push(runtime_feature.to_owned());
436
437 let staged_paths = vec![
438 path!(trybuild.project_dir / "src" / "__staged.rs"),
439 path!(trybuild.project_dir / "Cargo.lock"),
440 std::env::current_exe().unwrap(),
441 ];
442
443 let project_dir = trybuild.project_dir.clone();
444 let features_for_closure = features.clone();
445 let is_fuzz_for_closure = is_fuzz;
446
447 let (guard, cargo_lock) = hydro_concurrent_cargo::run_prebuild(
448 &trybuild.target_dir,
449 trybuild.project_dir.file_name().unwrap().to_str().unwrap(),
450 &features,
451 &staged_paths,
452 |prebuild_target| {
453 let features_str = features_for_closure.join(",");
454
455 let dylib_crate = path!(project_dir / "dylib");
456 let mut dep_cmd = Command::new("cargo");
457 dep_cmd.current_dir(&dylib_crate);
458 dep_cmd.args(["build", "--locked"]);
459 dep_cmd.args(["--target-dir", prebuild_target.to_str().unwrap()]);
460 dep_cmd.arg("--no-default-features");
461 dep_cmd.args(["--features", &features_str]);
462 dep_cmd.args(["--config", "build.incremental = false"]);
463 dep_cmd.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
464 eprintln!("[hydro-build] starting prebuild child cargo");
465 let status = dep_cmd.stdin(Stdio::null()).status().unwrap();
466 eprintln!(
467 "[hydro-build] prebuild child cargo finished, success={}",
468 status.success()
469 );
470 if !status.success() {
471 panic!("dep prebuild failed");
472 }
473
474 if !is_fuzz_for_closure {
478 eprintln!("[hydro-build] starting prebuild dylib-examples lib");
479 let dylib_examples_crate = path!(project_dir / "dylib-examples");
480 let mut lib_cmd = Command::new("cargo");
481 lib_cmd.current_dir(&dylib_examples_crate);
482 lib_cmd.args(["build", "--locked", "--lib"]);
483 lib_cmd.args(["--target-dir", prebuild_target.to_str().unwrap()]);
484 lib_cmd.arg("--no-default-features");
485 lib_cmd.args(["--features", &features_str]);
486 lib_cmd.args(["--config", "build.incremental = false"]);
487 lib_cmd.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
488 let lib_status = lib_cmd.stdin(Stdio::null()).status().unwrap();
489 if !lib_status.success() {
490 panic!("dylib-examples lib prebuild failed");
491 }
492 }
493 },
494 );
495
496 (per_job, Some(guard), Some(cargo_lock))
497 } else {
498 (trybuild.target_dir.clone(), None, None)
499 };
500
501 let _job_build_guard = if !has_custom_rustflags {
503 let shared_debug = trybuild.target_dir.join("debug");
504 Some(hydro_concurrent_cargo::populate_job_build_dir(
505 &final_target_dir.join("debug"),
506 &shared_debug,
507 ))
508 } else {
509 None
510 };
511
512 let mut command = Command::new("cargo");
513 command.current_dir(&crate_to_compile);
514 command.args([
515 "rustc",
516 if has_custom_rustflags {
517 "--locked"
518 } else {
519 "--frozen"
520 },
521 ]);
522 command.args(["--example", &example_name]);
523 command.args(["--target-dir", final_target_dir.to_str().unwrap()]);
524 command.arg("--no-default-features");
531 command.args([
532 "--features",
533 &trybuild
534 .features
535 .clone()
536 .into_iter()
537 .flatten()
538 .chain([runtime_feature.to_owned()])
539 .collect::<Vec<_>>()
540 .join(","),
541 ]);
542 command.args(["--config", "build.incremental = false"]);
543 if let Some(crate_type) = crate_type {
544 command.args(["--crate-type", crate_type]);
545 }
546 command.arg("--message-format=json-diagnostic-rendered-ansi");
547 command.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
548 if set_trybuild_lib_name {
549 command.env("TRYBUILD_LIB_NAME", &bin_name);
550 }
551
552 command.arg("--");
553
554 if cfg!(target_os = "linux") {
555 let debug_path = if let Ok(target) = std::env::var("CARGO_BUILD_TARGET") {
556 path!(final_target_dir / target / "debug")
557 } else {
558 path!(final_target_dir / "debug")
559 };
560
561 command.args([&format!(
562 "-Clink-arg=-Wl,-rpath,{}",
563 debug_path.to_str().unwrap()
564 )]);
565
566 if cfg!(target_env = "gnu") {
567 command.arg(
568 "-Clink-args=-Wl,-z,nodelete",
570 );
571 }
572 }
573
574 if allow_fuzz && let Ok(fuzzer) = std::env::var("BOLERO_FUZZER") {
575 command.env_remove("BOLERO_FUZZER");
576
577 if fuzzer == "libfuzzer" {
578 #[cfg(target_os = "macos")]
579 {
580 command.args(["-Clink-arg=-undefined", "-Clink-arg=dynamic_lookup"]);
581 }
582
583 #[cfg(target_os = "linux")]
584 {
585 command.args(["-Clink-arg=-Wl,--unresolved-symbols=ignore-all"]);
586 }
587 }
588 }
589
590 let mut spawned = command
591 .stdout(Stdio::piped())
592 .stderr(Stdio::piped())
593 .stdin(Stdio::null())
594 .spawn()
595 .unwrap();
596 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
597 let stderr_handle = spawned.stderr.take().unwrap();
598 let stderr_thread = std::thread::spawn(move || {
599 use std::io::Read;
600 let mut buf = String::new();
601 std::io::BufReader::new(stderr_handle)
602 .read_to_string(&mut buf)
603 .unwrap();
604 buf
605 });
606
607 let mut out = Err(());
608 for message in cargo_metadata::Message::parse_stream(reader) {
609 match message.unwrap() {
610 cargo_metadata::Message::CompilerArtifact(artifact) => {
611 let is_output = artifact.target.is_example();
613
614 if is_output {
615 let path = artifact.filenames.first().unwrap();
616 let path_buf: PathBuf = path.clone().into();
617 out = Ok(path_buf);
618 }
619 }
620 cargo_metadata::Message::CompilerMessage(mut msg) => {
621 if let Some(rendered) = msg.message.rendered.as_mut() {
624 let file_names = msg
625 .message
626 .spans
627 .iter()
628 .map(|s| &s.file_name)
629 .collect::<std::collections::BTreeSet<_>>();
630 for file_name in file_names {
631 *rendered = rendered.replace(
632 file_name,
633 &format!("(full path) {}/{file_name}", trybuild.project_dir.display()),
634 )
635 }
636 }
637 eprintln!("{}", msg.message);
638 }
639 cargo_metadata::Message::TextLine(line) => {
640 eprintln!("{}", line);
641 }
642 cargo_metadata::Message::BuildFinished(_) => {}
643 cargo_metadata::Message::BuildScriptExecuted(_) => {}
644 msg => panic!("Unexpected message type: {:?}", msg),
645 }
646 }
647
648 spawned.wait().unwrap();
649 let stderr_output = stderr_thread.join().unwrap();
650
651 if !has_custom_rustflags {
654 for line in stderr_output.lines() {
655 if line.contains("Compiling") && !line.contains("dylib-examples") {
656 panic!(
657 "unexpected recompilation in final build: {line}\nfull stderr:\n{stderr_output}"
658 );
659 }
660 }
661 }
662
663 if out.is_err() {
664 panic!("final build failed to produce binary.\nstderr:\n{stderr_output}");
665 }
666
667 let out_file = tempfile::NamedTempFile::new().unwrap().into_temp_path();
668 fs::copy(out.as_ref().unwrap(), &out_file).unwrap();
669 Ok(out_file)
670}
671
672pub fn create_trybuild()
673-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
674 let Metadata {
675 target_directory: target_dir,
676 workspace_root: workspace,
677 packages,
678 } = cargo::metadata()?;
679
680 let source_dir = cargo::manifest_dir()?;
681 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
682
683 let mut dev_dependency_features = vec![];
684 source_manifest.dev_dependencies.retain(|k, v| {
685 if source_manifest.dependencies.contains_key(k) {
686 for feat in &v.features {
688 dev_dependency_features.push(format!("{}/{}", k, feat));
689 }
690
691 false
692 } else {
693 dev_dependency_features.push(format!("dep:{k}"));
695
696 v.optional = true;
697 true
698 }
699 });
700
701 let mut features = if std::env::var("RUNNING_AS_EXAMPLE_TEST").is_ok_and(|v| v == "1") {
707 None
708 } else {
709 features::find()
710 };
711
712 let path_dependencies = source_manifest
713 .dependencies
714 .iter()
715 .filter_map(|(name, dep)| {
716 let path = dep.path.as_ref()?;
717 if packages.iter().any(|p| &p.name == name) {
718 None
720 } else {
721 Some(PathDependency {
722 name: name.clone(),
723 normalized_path: path.canonicalize().ok()?,
724 })
725 }
726 })
727 .collect();
728
729 let crate_name = source_manifest.package.name.clone();
730 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
731 fs::create_dir_all(&project_dir)?;
732
733 let project_name = format!("{}-hydro-trybuild", crate_name);
734 let mut manifest = Runner::make_manifest(
735 &workspace,
736 &project_name,
737 &source_dir,
738 &packages,
739 &[],
740 source_manifest,
741 )?;
742
743 if let Some(enabled_features) = &mut features {
744 enabled_features
745 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
746 }
747
748 for runtime_feature in HYDRO_RUNTIME_FEATURES {
749 manifest.features.insert(
750 format!("hydro___feature_{runtime_feature}"),
751 vec![format!("hydro_lang/{runtime_feature}")],
752 );
753 }
754
755 manifest
756 .dependencies
757 .get_mut("hydro_lang")
758 .unwrap()
759 .features
760 .push("runtime_support".to_owned());
761
762 manifest
763 .features
764 .insert("hydro___test".to_owned(), dev_dependency_features);
765
766 if manifest
767 .workspace
768 .as_ref()
769 .is_some_and(|w| w.dependencies.is_empty())
770 {
771 manifest.workspace = None;
772 }
773
774 let project = Project {
775 dir: project_dir,
776 source_dir,
777 target_dir,
778 name: project_name.clone(),
779 update: Update::env()?,
780 has_pass: false,
781 has_compile_fail: false,
782 features,
783 workspace,
784 path_dependencies,
785 manifest,
786 keep_going: false,
787 };
788
789 {
790 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
791
792 let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
793 project_lock.lock()?;
794
795 fs::create_dir_all(path!(project.dir / "src"))?;
796 fs::create_dir_all(path!(project.dir / "examples"))?;
797
798 let crate_name_ident = syn::Ident::new(
799 &crate_name.replace("-", "_"),
800 proc_macro2::Span::call_site(),
801 );
802
803 write_atomic(
804 prettyplease::unparse(&syn::parse_quote! {
805 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
806
807 pub mod __root {
808 pub use #crate_name_ident::*;
809 #[cfg(feature = "hydro___test")]
810 pub use super::__staged;
811 }
812
813 #[cfg(feature = "hydro___test")]
814 pub mod __staged;
815 })
816 .as_bytes(),
817 &path!(project.dir / "src" / "lib.rs"),
818 )
819 .unwrap();
820
821 let base_manifest = toml::to_string(&project.manifest)?;
822
823 let feature_names: Vec<_> = project.manifest.features.keys().cloned().collect();
825
826 let dylib_dir = path!(project.dir / "dylib");
828 fs::create_dir_all(path!(dylib_dir / "src"))?;
829
830 let trybuild_crate_name_ident = syn::Ident::new(
831 &project_name.replace("-", "_"),
832 proc_macro2::Span::call_site(),
833 );
834 write_atomic(
835 prettyplease::unparse(&syn::parse_quote! {
836 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
837 pub use #trybuild_crate_name_ident::*;
838 })
839 .as_bytes(),
840 &path!(dylib_dir / "src" / "lib.rs"),
841 )?;
842
843 let serialized_edition = toml::to_string(
844 &vec![("edition", &project.manifest.package.edition)]
845 .into_iter()
846 .collect::<std::collections::HashMap<_, _>>(),
847 )
848 .unwrap();
849
850 let dylib_features_section = feature_names
853 .iter()
854 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
855 .collect::<Vec<_>>()
856 .join("\n");
857
858 let dylib_manifest = format!(
859 r#"[package]
860name = "{project_name}-dylib"
861version = "0.0.0"
862{}
863
864[lib]
865crate-type = ["{}"]
866
867[dependencies]
868{project_name} = {{ path = "..", default-features = false }}
869
870[features]
871{dylib_features_section}
872"#,
873 serialized_edition,
874 if cfg!(target_os = "windows") {
875 "rlib"
876 } else {
877 "dylib"
878 }
879 );
880 write_atomic(dylib_manifest.as_ref(), &path!(dylib_dir / "Cargo.toml"))?;
881
882 let dylib_examples_dir = path!(project.dir / "dylib-examples");
883 fs::create_dir_all(path!(dylib_examples_dir / "src"))?;
884 fs::create_dir_all(path!(dylib_examples_dir / "examples"))?;
885
886 write_atomic(
887 b"#![allow(unused_crate_dependencies)]\n",
888 &path!(dylib_examples_dir / "src" / "lib.rs"),
889 )?;
890
891 let features_section = feature_names
893 .iter()
894 .map(|f| format!("{f} = [\"{project_name}/{f}\", \"{project_name}-dylib/{f}\"]"))
895 .collect::<Vec<_>>()
896 .join("\n");
897
898 let dylib_examples_manifest = format!(
900 r#"[package]
901name = "{project_name}-dylib-examples"
902version = "0.0.0"
903{}
904
905[dev-dependencies]
906{project_name} = {{ path = "..", default-features = false }}
907{project_name}-dylib = {{ path = "../dylib", default-features = false }}
908
909[features]
910{features_section}
911
912[[example]]
913name = "sim-dylib"
914crate-type = ["cdylib"]
915"#,
916 serialized_edition
917 );
918 write_atomic(
919 dylib_examples_manifest.as_ref(),
920 &path!(dylib_examples_dir / "Cargo.toml"),
921 )?;
922
923 let sim_dylib_contents = prettyplease::unparse(&syn::parse_quote! {
925 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
926 include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
927 });
928 write_atomic(
929 sim_dylib_contents.as_bytes(),
930 &path!(project.dir / "examples" / "sim-dylib.rs"),
931 )?;
932 write_atomic(
933 sim_dylib_contents.as_bytes(),
934 &path!(dylib_examples_dir / "examples" / "sim-dylib.rs"),
935 )?;
936
937 let workspace_manifest = format!(
938 r#"{}
939[[example]]
940name = "sim-dylib"
941crate-type = ["cdylib"]
942
943[workspace]
944members = ["dylib", "dylib-examples"]
945"#,
946 base_manifest,
947 );
948
949 write_atomic(
950 workspace_manifest.as_ref(),
951 &path!(project.dir / "Cargo.toml"),
952 )?;
953
954 let manifest_hash = format!("{:X}", Sha256::digest(&workspace_manifest))
956 .chars()
957 .take(8)
958 .collect::<String>();
959
960 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
961 let workspace_cargo_lock_contents_and_hash = if workspace_cargo_lock.exists() {
962 let cargo_lock_contents = fs::read_to_string(&workspace_cargo_lock)?;
963
964 let hash = format!("{:X}", Sha256::digest(&cargo_lock_contents))
965 .chars()
966 .take(8)
967 .collect::<String>();
968
969 Some((cargo_lock_contents, hash))
970 } else {
971 None
972 };
973
974 let trybuild_hash = format!(
975 "{}-{}",
976 manifest_hash,
977 workspace_cargo_lock_contents_and_hash
978 .as_ref()
979 .map(|(_contents, hash)| &**hash)
980 .unwrap_or_default()
981 );
982
983 if !check_contents(
984 trybuild_hash.as_bytes(),
985 &path!(project.dir / ".hydro-trybuild-manifest"),
986 )
987 .is_ok_and(|b| b)
988 {
989 if let Some((cargo_lock_contents, _)) = workspace_cargo_lock_contents_and_hash {
991 write_atomic(
994 cargo_lock_contents.as_ref(),
995 &path!(project.dir / "Cargo.lock"),
996 )?;
997 } else {
998 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
999 }
1000
1001 std::process::Command::new("cargo")
1003 .current_dir(&project.dir)
1004 .args(["update", "-w"]) .stdout(std::process::Stdio::null())
1006 .stderr(std::process::Stdio::null())
1007 .status()
1008 .unwrap();
1009
1010 write_atomic(
1011 trybuild_hash.as_bytes(),
1012 &path!(project.dir / ".hydro-trybuild-manifest"),
1013 )?;
1014 }
1015
1016 let examples_folder = path!(project.dir / "examples");
1018 fs::create_dir_all(&examples_folder)?;
1019
1020 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
1021 if workspace_dot_cargo_config_toml.exists() {
1022 let dot_cargo_folder = path!(project.dir / ".cargo");
1023 fs::create_dir_all(&dot_cargo_folder)?;
1024
1025 write_atomic(
1026 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
1027 &path!(dot_cargo_folder / "config.toml"),
1028 )?;
1029 }
1030
1031 let vscode_folder = path!(project.dir / ".vscode");
1032 fs::create_dir_all(&vscode_folder)?;
1033 write_atomic(
1034 include_bytes!("./vscode-trybuild.json"),
1035 &path!(vscode_folder / "settings.json"),
1036 )?;
1037 }
1038
1039 Ok((
1040 project.dir.as_ref().into(),
1041 project.target_dir.as_ref().into(),
1042 project.features,
1043 ))
1044}
1045
1046fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
1047 let mut file = File::options()
1048 .read(true)
1049 .write(false)
1050 .create(false)
1051 .truncate(false)
1052 .open(path)?;
1053 file.lock()?;
1054
1055 let mut existing_contents = Vec::new();
1056 file.read_to_end(&mut existing_contents)?;
1057 Ok(existing_contents == contents)
1058}
1059
1060pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
1061 let mut file = File::options()
1062 .read(true)
1063 .write(true)
1064 .create(true)
1065 .truncate(false)
1066 .open(path)?;
1067
1068 let mut existing_contents = Vec::new();
1069 file.read_to_end(&mut existing_contents)?;
1070 if existing_contents != contents {
1071 file.lock()?;
1072 file.seek(SeekFrom::Start(0))?;
1073 file.set_len(0)?;
1074 file.write_all(contents)?;
1075 }
1076
1077 Ok(())
1078}