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 Static,
33 #[cfg(feature = "deploy")]
34 Dynamic,
35}
36
37#[cfg(any(feature = "deploy", feature = "maelstrom"))]
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum DeployMode {
41 #[cfg(feature = "deploy")]
42 HydroDeploy,
44 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
45 Containerized,
47 #[cfg(feature = "maelstrom")]
48 Maelstrom,
50}
51
52pub(crate) static IS_TEST: std::sync::atomic::AtomicBool =
53 std::sync::atomic::AtomicBool::new(false);
54
55pub(crate) static CONCURRENT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
56
57pub fn init_test() {
71 IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
72}
73
74#[cfg(any(feature = "deploy", feature = "maelstrom"))]
75fn clean_bin_name_prefix(bin_name_prefix: &str) -> String {
76 bin_name_prefix
77 .replace("::", "__")
78 .replace(" ", "_")
79 .replace(",", "_")
80 .replace("<", "_")
81 .replace(">", "")
82 .replace("(", "")
83 .replace(")", "")
84 .replace("{", "_")
85 .replace("}", "_")
86}
87
88#[derive(Debug, Clone)]
89pub struct TrybuildConfig {
90 pub project_dir: PathBuf,
91 pub target_dir: PathBuf,
92 pub features: Option<Vec<String>>,
93 #[cfg(feature = "deploy")]
94 pub linking_mode: LinkingMode,
98}
99
100#[cfg(any(feature = "deploy", feature = "maelstrom"))]
101pub fn create_graph_trybuild(
102 graph: DfirGraph,
103 extra_stmts: &[syn::Stmt],
104 sidecars: &[syn::Expr],
105 bin_name_prefix: Option<&str>,
106 deploy_mode: DeployMode,
107 linking_mode: LinkingMode,
108) -> (String, TrybuildConfig) {
109 let source_dir = cargo::manifest_dir().unwrap();
110 let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
111 let crate_name = source_manifest.package.name.replace("-", "_");
112
113 let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
114
115 let generated_code =
116 compile_graph_trybuild(graph, extra_stmts, sidecars, &crate_name, deploy_mode);
117
118 let inlined_staged = if is_test {
119 let raw_toml_manifest = toml::from_str::<toml::Value>(
120 &fs::read_to_string(path!(source_dir / "Cargo.toml")).unwrap(),
121 )
122 .unwrap();
123
124 let maybe_custom_lib_path = raw_toml_manifest
125 .get("lib")
126 .and_then(|lib| lib.get("path"))
127 .and_then(|path| path.as_str());
128
129 let mut gen_staged = stageleft_tool::gen_staged_trybuild(
130 &maybe_custom_lib_path
131 .map(|s| path!(source_dir / s))
132 .unwrap_or_else(|| path!(source_dir / "src" / "lib.rs")),
133 &path!(source_dir / "Cargo.toml"),
134 &crate_name,
135 Some("hydro___test".to_owned()),
136 );
137
138 gen_staged.attrs.insert(
139 0,
140 syn::parse_quote! {
141 #![allow(
142 unused,
143 ambiguous_glob_reexports,
144 clippy::suspicious_else_formatting,
145 unexpected_cfgs,
146 reason = "generated code"
147 )]
148 },
149 );
150
151 Some(prettyplease::unparse(&gen_staged))
152 } else {
153 None
154 };
155
156 let source = prettyplease::unparse(&generated_code);
157
158 let hash = format!("{:X}", Sha256::digest(&source))
159 .chars()
160 .take(8)
161 .collect::<String>();
162
163 let bin_name = if let Some(bin_name_prefix) = &bin_name_prefix {
164 format!("{}_{}", clean_bin_name_prefix(bin_name_prefix), &hash)
165 } else {
166 hash
167 };
168
169 let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
170
171 let examples_dir = match linking_mode {
173 LinkingMode::Static => path!(project_dir / "examples"),
174 #[cfg(feature = "deploy")]
175 LinkingMode::Dynamic => path!(project_dir / "dylib-examples" / "examples"),
176 };
177
178 fs::create_dir_all(&examples_dir).unwrap();
180
181 let out_path = path!(examples_dir / format!("{bin_name}.rs"));
182 {
183 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
184 write_atomic(source.as_ref(), &out_path).unwrap();
185 }
186
187 if let Some(inlined_staged) = inlined_staged {
188 let staged_path = path!(project_dir / "src" / "__staged.rs");
189 {
190 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
191 write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
192 }
193 }
194
195 if is_test {
196 if cur_bin_enabled_features.is_none() {
197 cur_bin_enabled_features = Some(vec![]);
198 }
199
200 cur_bin_enabled_features
201 .as_mut()
202 .unwrap()
203 .push("hydro___test".to_owned());
204 }
205
206 (
207 bin_name,
208 TrybuildConfig {
209 project_dir,
210 target_dir,
211 features: cur_bin_enabled_features,
212 #[cfg(feature = "deploy")]
213 linking_mode,
214 },
215 )
216}
217
218#[cfg(any(feature = "deploy", feature = "maelstrom"))]
219pub fn compile_graph_trybuild(
220 partitioned_graph: DfirGraph,
221 extra_stmts: &[syn::Stmt],
222 sidecars: &[syn::Expr],
223 crate_name: &str,
224 deploy_mode: DeployMode,
225) -> syn::File {
226 use crate::staging_util::get_this_crate;
227
228 let mut diagnostics = Diagnostics::new();
229 let dfir_expr: syn::Expr = syn::parse2(
230 partitioned_graph
231 .as_code("e! { __root_dfir_rs }, true, quote!(), &mut diagnostics)
232 .expect("DFIR code generation failed with diagnostics."),
233 )
234 .unwrap();
235
236 let orig_crate_name = quote::format_ident!("{}", crate_name);
237 let trybuild_crate_name_ident = quote::format_ident!("{}_hydro_trybuild", crate_name);
238 let root = get_this_crate();
239 let tokio_main_ident = format!("{}::runtime_support::tokio", root);
240 let dfir_ident = quote::format_ident!("{}", crate::compile::DFIR_IDENT);
241
242 let source_ast: syn::File = match deploy_mode {
243 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
244 DeployMode::Containerized => {
245 syn::parse_quote! {
246 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
247 use #trybuild_crate_name_ident::__root as #orig_crate_name;
248 use #orig_crate_name::*;
249 use #orig_crate_name::__staged::__deps::*;
250 use #root::prelude::*;
251 use #root::runtime_support::dfir_rs as __root_dfir_rs;
252 pub use #orig_crate_name::__staged;
253
254 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
255 async fn main() {
256 #root::telemetry::initialize_tracing();
257
258 #( #extra_stmts )*
259
260 let mut #dfir_ident = #dfir_expr;
261
262 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
263 #(
264 let _ = local_set.spawn_local( #sidecars ); )*
266
267 let _ = local_set.run_until(#dfir_ident.run()).await;
268 }
269 }
270 }
271 #[cfg(feature = "deploy")]
272 DeployMode::HydroDeploy => {
273 syn::parse_quote! {
274 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
275 use #trybuild_crate_name_ident::__root as #orig_crate_name;
276 use #orig_crate_name::*;
277 use #orig_crate_name::__staged::__deps::*;
278 use #root::prelude::*;
279 use #root::runtime_support::dfir_rs as __root_dfir_rs;
280 pub use #orig_crate_name::__staged;
281
282 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
283 async fn main() {
284 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;
285 let __hydro_lang_trybuild_cli = &__hydro_lang_trybuild_cli_owned;
286
287 #( #extra_stmts )*
288
289 let mut #dfir_ident = #dfir_expr;
290 println!("ack start");
291
292 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
296 #(
297 let _ = local_set.spawn_local( #sidecars ); )*
299
300 let _ = local_set.run_until(#root::runtime_support::launch::run_stdin_commands(
301 async move {
302 #dfir_ident.run().await
303 }
304 )).await;
305 }
306 }
307 }
308 #[cfg(feature = "maelstrom")]
309 DeployMode::Maelstrom => {
310 syn::parse_quote! {
311 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
312 use #trybuild_crate_name_ident::__root as #orig_crate_name;
313 use #orig_crate_name::*;
314 use #orig_crate_name::__staged::__deps::*;
315 use #root::prelude::*;
316 use #root::runtime_support::dfir_rs as __root_dfir_rs;
317 pub use #orig_crate_name::__staged;
318
319 #[allow(unused)]
320 fn __hydro_runtime<'a>(
321 __hydro_lang_maelstrom_meta: &'a #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::MaelstromMeta
322 )
323 -> #root::runtime_support::dfir_rs::scheduled::context::Dfir<impl #root::runtime_support::dfir_rs::scheduled::context::TickClosure + 'a>
324 {
325 #( #extra_stmts )*
326
327 #dfir_expr
328 }
329
330 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
331 async fn main() {
332 #root::telemetry::initialize_tracing();
333
334 let __hydro_lang_maelstrom_meta = #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::maelstrom_init();
336
337 let mut #dfir_ident = __hydro_runtime(&__hydro_lang_maelstrom_meta);
338
339 __hydro_lang_maelstrom_meta.start_receiving(); let local_set = #root::runtime_support::tokio::task::LocalSet::new();
342 #(
343 let _ = local_set.spawn_local( #sidecars ); )*
345
346 let _ = local_set.run_until(#dfir_ident.run()).await;
347 }
348 }
349 }
350 };
351 source_ast
352}
353
354pub fn create_trybuild()
355-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
356 let Metadata {
357 target_directory: target_dir,
358 workspace_root: workspace,
359 packages,
360 } = cargo::metadata()?;
361
362 let source_dir = cargo::manifest_dir()?;
363 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
364
365 let mut dev_dependency_features = vec![];
366 source_manifest.dev_dependencies.retain(|k, v| {
367 if source_manifest.dependencies.contains_key(k) {
368 for feat in &v.features {
370 dev_dependency_features.push(format!("{}/{}", k, feat));
371 }
372
373 false
374 } else {
375 dev_dependency_features.push(format!("dep:{k}"));
377
378 v.optional = true;
379 true
380 }
381 });
382
383 let mut features = features::find();
384
385 let path_dependencies = source_manifest
386 .dependencies
387 .iter()
388 .filter_map(|(name, dep)| {
389 let path = dep.path.as_ref()?;
390 if packages.iter().any(|p| &p.name == name) {
391 None
393 } else {
394 Some(PathDependency {
395 name: name.clone(),
396 normalized_path: path.canonicalize().ok()?,
397 })
398 }
399 })
400 .collect();
401
402 let crate_name = source_manifest.package.name.clone();
403 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
404 fs::create_dir_all(&project_dir)?;
405
406 let project_name = format!("{}-hydro-trybuild", crate_name);
407 let mut manifest = Runner::make_manifest(
408 &workspace,
409 &project_name,
410 &source_dir,
411 &packages,
412 &[],
413 source_manifest,
414 )?;
415
416 if let Some(enabled_features) = &mut features {
417 enabled_features
418 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
419 }
420
421 for runtime_feature in HYDRO_RUNTIME_FEATURES {
422 manifest.features.insert(
423 format!("hydro___feature_{runtime_feature}"),
424 vec![format!("hydro_lang/{runtime_feature}")],
425 );
426 }
427
428 manifest
429 .dependencies
430 .get_mut("hydro_lang")
431 .unwrap()
432 .features
433 .push("runtime_support".to_owned());
434
435 manifest
436 .features
437 .insert("hydro___test".to_owned(), dev_dependency_features);
438
439 if manifest
440 .workspace
441 .as_ref()
442 .is_some_and(|w| w.dependencies.is_empty())
443 {
444 manifest.workspace = None;
445 }
446
447 let project = Project {
448 dir: project_dir,
449 source_dir,
450 target_dir,
451 name: project_name.clone(),
452 update: Update::env()?,
453 has_pass: false,
454 has_compile_fail: false,
455 features,
456 workspace,
457 path_dependencies,
458 manifest,
459 keep_going: false,
460 };
461
462 {
463 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
464
465 let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
466 project_lock.lock()?;
467
468 fs::create_dir_all(path!(project.dir / "src"))?;
469 fs::create_dir_all(path!(project.dir / "examples"))?;
470
471 let crate_name_ident = syn::Ident::new(
472 &crate_name.replace("-", "_"),
473 proc_macro2::Span::call_site(),
474 );
475
476 write_atomic(
477 prettyplease::unparse(&syn::parse_quote! {
478 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
479
480 pub mod __root {
481 pub use #crate_name_ident::*;
482 #[cfg(feature = "hydro___test")]
483 pub use super::__staged;
484 }
485
486 #[cfg(feature = "hydro___test")]
487 pub mod __staged;
488 })
489 .as_bytes(),
490 &path!(project.dir / "src" / "lib.rs"),
491 )
492 .unwrap();
493
494 let base_manifest = toml::to_string(&project.manifest)?;
495
496 let feature_names: Vec<_> = project.manifest.features.keys().cloned().collect();
498
499 let dylib_dir = path!(project.dir / "dylib");
501 fs::create_dir_all(path!(dylib_dir / "src"))?;
502
503 let trybuild_crate_name_ident = syn::Ident::new(
504 &project_name.replace("-", "_"),
505 proc_macro2::Span::call_site(),
506 );
507 write_atomic(
508 prettyplease::unparse(&syn::parse_quote! {
509 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
510 pub use #trybuild_crate_name_ident::*;
511 })
512 .as_bytes(),
513 &path!(dylib_dir / "src" / "lib.rs"),
514 )?;
515
516 let serialized_edition = toml::to_string(
517 &vec![("edition", &project.manifest.package.edition)]
518 .into_iter()
519 .collect::<std::collections::HashMap<_, _>>(),
520 )
521 .unwrap();
522
523 let dylib_manifest = format!(
527 r#"[package]
528name = "{project_name}-dylib"
529version = "0.0.0"
530{}
531
532[lib]
533crate-type = ["{}"]
534
535[dependencies]
536{project_name} = {{ path = "..", default-features = false }}
537"#,
538 serialized_edition,
539 if cfg!(target_os = "windows") {
540 "rlib"
541 } else {
542 "dylib"
543 }
544 );
545 write_atomic(dylib_manifest.as_ref(), &path!(dylib_dir / "Cargo.toml"))?;
546
547 let dylib_examples_dir = path!(project.dir / "dylib-examples");
548 fs::create_dir_all(path!(dylib_examples_dir / "src"))?;
549 fs::create_dir_all(path!(dylib_examples_dir / "examples"))?;
550
551 write_atomic(
552 b"#![allow(unused_crate_dependencies)]\n",
553 &path!(dylib_examples_dir / "src" / "lib.rs"),
554 )?;
555
556 let features_section = feature_names
558 .iter()
559 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
560 .collect::<Vec<_>>()
561 .join("\n");
562
563 let dylib_examples_manifest = format!(
565 r#"[package]
566name = "{project_name}-dylib-examples"
567version = "0.0.0"
568{}
569
570[dev-dependencies]
571{project_name} = {{ path = "..", default-features = false }}
572{project_name}-dylib = {{ path = "../dylib", default-features = false }}
573
574[features]
575{features_section}
576
577[[example]]
578name = "sim-dylib"
579crate-type = ["cdylib"]
580"#,
581 serialized_edition
582 );
583 write_atomic(
584 dylib_examples_manifest.as_ref(),
585 &path!(dylib_examples_dir / "Cargo.toml"),
586 )?;
587
588 let sim_dylib_contents = prettyplease::unparse(&syn::parse_quote! {
590 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
591 include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
592 });
593 write_atomic(
594 sim_dylib_contents.as_bytes(),
595 &path!(project.dir / "examples" / "sim-dylib.rs"),
596 )?;
597 write_atomic(
598 sim_dylib_contents.as_bytes(),
599 &path!(dylib_examples_dir / "examples" / "sim-dylib.rs"),
600 )?;
601
602 let workspace_manifest = format!(
603 r#"{}
604[[example]]
605name = "sim-dylib"
606crate-type = ["cdylib"]
607
608[workspace]
609members = ["dylib", "dylib-examples"]
610"#,
611 base_manifest,
612 );
613
614 write_atomic(
615 workspace_manifest.as_ref(),
616 &path!(project.dir / "Cargo.toml"),
617 )?;
618
619 let manifest_hash = format!("{:X}", Sha256::digest(&workspace_manifest))
621 .chars()
622 .take(8)
623 .collect::<String>();
624
625 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
626 let workspace_cargo_lock_contents_and_hash = if workspace_cargo_lock.exists() {
627 let cargo_lock_contents = fs::read_to_string(&workspace_cargo_lock)?;
628
629 let hash = format!("{:X}", Sha256::digest(&cargo_lock_contents))
630 .chars()
631 .take(8)
632 .collect::<String>();
633
634 Some((cargo_lock_contents, hash))
635 } else {
636 None
637 };
638
639 let trybuild_hash = format!(
640 "{}-{}",
641 manifest_hash,
642 workspace_cargo_lock_contents_and_hash
643 .as_ref()
644 .map(|(_contents, hash)| &**hash)
645 .unwrap_or_default()
646 );
647
648 if !check_contents(
649 trybuild_hash.as_bytes(),
650 &path!(project.dir / ".hydro-trybuild-manifest"),
651 )
652 .is_ok_and(|b| b)
653 {
654 if let Some((cargo_lock_contents, _)) = workspace_cargo_lock_contents_and_hash {
656 write_atomic(
659 cargo_lock_contents.as_ref(),
660 &path!(project.dir / "Cargo.lock"),
661 )?;
662 } else {
663 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
664 }
665
666 std::process::Command::new("cargo")
668 .current_dir(&project.dir)
669 .args(["update", "-w"]) .stdout(std::process::Stdio::null())
671 .stderr(std::process::Stdio::null())
672 .status()
673 .unwrap();
674
675 write_atomic(
676 trybuild_hash.as_bytes(),
677 &path!(project.dir / ".hydro-trybuild-manifest"),
678 )?;
679 }
680
681 let examples_folder = path!(project.dir / "examples");
683 fs::create_dir_all(&examples_folder)?;
684
685 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
686 if workspace_dot_cargo_config_toml.exists() {
687 let dot_cargo_folder = path!(project.dir / ".cargo");
688 fs::create_dir_all(&dot_cargo_folder)?;
689
690 write_atomic(
691 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
692 &path!(dot_cargo_folder / "config.toml"),
693 )?;
694 }
695
696 let vscode_folder = path!(project.dir / ".vscode");
697 fs::create_dir_all(&vscode_folder)?;
698 write_atomic(
699 include_bytes!("./vscode-trybuild.json"),
700 &path!(vscode_folder / "settings.json"),
701 )?;
702 }
703
704 Ok((
705 project.dir.as_ref().into(),
706 project.target_dir.as_ref().into(),
707 project.features,
708 ))
709}
710
711fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
712 let mut file = File::options()
713 .read(true)
714 .write(false)
715 .create(false)
716 .truncate(false)
717 .open(path)?;
718 file.lock()?;
719
720 let mut existing_contents = Vec::new();
721 file.read_to_end(&mut existing_contents)?;
722 Ok(existing_contents == contents)
723}
724
725pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
726 let mut file = File::options()
727 .read(true)
728 .write(true)
729 .create(true)
730 .truncate(false)
731 .open(path)?;
732
733 let mut existing_contents = Vec::new();
734 file.read_to_end(&mut existing_contents)?;
735 if existing_contents != contents {
736 file.lock()?;
737 file.seek(SeekFrom::Start(0))?;
738 file.set_len(0)?;
739 file.write_all(contents)?;
740 }
741
742 Ok(())
743}