1use std::fs::{self, File};
2use std::io::{Read, Seek, SeekFrom, Write};
3use std::path::{Path, PathBuf};
4
5#[cfg(feature = "deploy")]
6use dfir_lang::graph::DfirGraph;
7use sha2::{Digest, Sha256};
8#[cfg(feature = "deploy")]
9use stageleft::internal::quote;
10#[cfg(feature = "deploy")]
11use syn::visit_mut::VisitMut;
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
17#[cfg(feature = "deploy")]
18use super::rewriters::UseTestModeStaged;
19
20pub const HYDRO_RUNTIME_FEATURES: &[&str] = &[
21 "deploy_integration",
22 "runtime_measure",
23 "docker_runtime",
24 "ecs_runtime",
25];
26
27#[cfg(feature = "deploy")]
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum LinkingMode {
33 Static,
34 Dynamic,
35}
36
37pub(crate) static IS_TEST: std::sync::atomic::AtomicBool =
38 std::sync::atomic::AtomicBool::new(false);
39
40pub(crate) static CONCURRENT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
41
42pub fn init_test() {
56 IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
57}
58
59#[cfg(feature = "deploy")]
60fn clean_name_hint(name_hint: &str) -> String {
61 name_hint
62 .replace("::", "__")
63 .replace(" ", "_")
64 .replace(",", "_")
65 .replace("<", "_")
66 .replace(">", "")
67 .replace("(", "")
68 .replace(")", "")
69}
70
71#[derive(Debug, Clone)]
72pub struct TrybuildConfig {
73 pub project_dir: PathBuf,
74 pub target_dir: PathBuf,
75 pub features: Option<Vec<String>>,
76 #[cfg(feature = "deploy")]
77 pub linking_mode: LinkingMode,
81}
82
83#[cfg(feature = "deploy")]
84pub fn create_graph_trybuild(
85 graph: DfirGraph,
86 extra_stmts: Vec<syn::Stmt>,
87 name_hint: &Option<String>,
88 is_containerized: bool,
89 linking_mode: LinkingMode,
90) -> (String, TrybuildConfig) {
91 let source_dir = cargo::manifest_dir().unwrap();
92 let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
93 let crate_name = &source_manifest.package.name.to_string().replace("-", "_");
94
95 let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
96
97 let generated_code = compile_graph_trybuild(
98 graph,
99 extra_stmts,
100 crate_name.clone(),
101 is_test,
102 is_containerized,
103 );
104
105 let inlined_staged = if is_test {
106 let raw_toml_manifest = toml::from_str::<toml::Value>(
107 &fs::read_to_string(path!(source_dir / "Cargo.toml")).unwrap(),
108 )
109 .unwrap();
110
111 let maybe_custom_lib_path = raw_toml_manifest
112 .get("lib")
113 .and_then(|lib| lib.get("path"))
114 .and_then(|path| path.as_str());
115
116 let mut gen_staged = stageleft_tool::gen_staged_trybuild(
117 &maybe_custom_lib_path
118 .map(|s| path!(source_dir / s))
119 .unwrap_or_else(|| path!(source_dir / "src" / "lib.rs")),
120 &path!(source_dir / "Cargo.toml"),
121 crate_name.clone(),
122 Some("hydro___test".to_string()),
123 );
124
125 gen_staged.attrs.insert(
126 0,
127 syn::parse_quote! {
128 #![allow(
129 unused,
130 ambiguous_glob_reexports,
131 clippy::suspicious_else_formatting,
132 unexpected_cfgs,
133 reason = "generated code"
134 )]
135 },
136 );
137
138 Some(prettyplease::unparse(&gen_staged))
139 } else {
140 None
141 };
142
143 let source = prettyplease::unparse(&generated_code);
144
145 let hash = format!("{:X}", Sha256::digest(&source))
146 .chars()
147 .take(8)
148 .collect::<String>();
149
150 let bin_name = if let Some(name_hint) = &name_hint {
151 format!("{}_{}", clean_name_hint(name_hint), &hash)
152 } else {
153 hash
154 };
155
156 let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
157
158 let examples_dir = match linking_mode {
160 LinkingMode::Static => path!(project_dir / "examples"),
161 LinkingMode::Dynamic => path!(project_dir / "dylib-examples" / "examples"),
162 };
163
164 fs::create_dir_all(&examples_dir).unwrap();
166
167 let out_path = path!(examples_dir / format!("{bin_name}.rs"));
168 {
169 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
170 write_atomic(source.as_ref(), &out_path).unwrap();
171 }
172
173 if let Some(inlined_staged) = inlined_staged {
174 let staged_path = path!(project_dir / "src" / "__staged.rs");
175 {
176 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
177 write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
178 }
179 }
180
181 if is_test {
182 if cur_bin_enabled_features.is_none() {
183 cur_bin_enabled_features = Some(vec![]);
184 }
185
186 cur_bin_enabled_features
187 .as_mut()
188 .unwrap()
189 .push("hydro___test".to_string());
190 }
191
192 (
193 bin_name,
194 TrybuildConfig {
195 project_dir,
196 target_dir,
197 features: cur_bin_enabled_features,
198 linking_mode,
199 },
200 )
201}
202
203#[cfg(feature = "deploy")]
204pub fn compile_graph_trybuild(
205 partitioned_graph: DfirGraph,
206 extra_stmts: Vec<syn::Stmt>,
207 crate_name: String,
208 is_test: bool,
209 is_containerized: bool,
210) -> syn::File {
211 use crate::staging_util::get_this_crate;
212
213 let mut diagnostics = Vec::new();
214 let mut dfir_expr: syn::Expr = syn::parse2(partitioned_graph.as_code(
215 "e! { __root_dfir_rs },
216 true,
217 quote!(),
218 &mut diagnostics,
219 ))
220 .unwrap();
221
222 if is_test {
223 UseTestModeStaged {
224 crate_name: crate_name.clone(),
225 }
226 .visit_expr_mut(&mut dfir_expr);
227 }
228
229 let orig_crate_name = quote::format_ident!("{}", crate_name);
230 let trybuild_crate_name_ident = quote::format_ident!("{}_hydro_trybuild", crate_name);
231 let root = get_this_crate();
232 let tokio_main_ident = format!("{}::runtime_support::tokio", root);
233
234 let source_ast: syn::File = if is_containerized {
235 syn::parse_quote! {
236 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
237 use #trybuild_crate_name_ident::__root as #orig_crate_name;
238 use #trybuild_crate_name_ident::__staged::__deps::*;
239 use #root::prelude::*;
240 use #root::runtime_support::dfir_rs as __root_dfir_rs;
241 pub use #trybuild_crate_name_ident::__staged;
242
243 #[allow(unused)]
244 async fn __hydro_runtime<'a>() -> #root::runtime_support::dfir_rs::scheduled::graph::Dfir<'a> {
245 #(#extra_stmts)*
247
248 #dfir_expr
250 }
251
252 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
253 async fn main() {
254 #root::telemetry::initialize_tracing();
255
256 let flow = __hydro_runtime().await;
257
258 #root::runtime_support::launch::run_containerized(flow).await;
259 }
260 }
261 } else {
262 syn::parse_quote! {
263 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
264 use #trybuild_crate_name_ident::__root as #orig_crate_name;
265 use #trybuild_crate_name_ident::__staged::__deps::*;
266 use #root::prelude::*;
267 use #root::runtime_support::dfir_rs as __root_dfir_rs;
268 pub use #trybuild_crate_name_ident::__staged;
269
270 #[allow(unused)]
271 fn __hydro_runtime<'a>(__hydro_lang_trybuild_cli: &'a #root::runtime_support::hydro_deploy_integration::DeployPorts<#root::__staged::deploy::deploy_runtime::HydroMeta>) -> #root::runtime_support::dfir_rs::scheduled::graph::Dfir<'a> {
272 #(#extra_stmts)*
273 #dfir_expr
274 }
275
276 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
277 async fn main() {
278 let ports = #root::runtime_support::launch::init_no_ack_start().await;
279 let flow = __hydro_runtime(&ports);
280 println!("ack start");
281
282 #root::runtime_support::launch::run(flow).await;
283 }
284 }
285 };
286 source_ast
287}
288
289pub fn create_trybuild()
290-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
291 let Metadata {
292 target_directory: target_dir,
293 workspace_root: workspace,
294 packages,
295 } = cargo::metadata()?;
296
297 let source_dir = cargo::manifest_dir()?;
298 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
299
300 let mut dev_dependency_features = vec![];
301 source_manifest.dev_dependencies.retain(|k, v| {
302 if source_manifest.dependencies.contains_key(k) {
303 for feat in &v.features {
305 dev_dependency_features.push(format!("{}/{}", k, feat));
306 }
307
308 false
309 } else {
310 dev_dependency_features.push(format!("dep:{k}"));
312
313 v.optional = true;
314 true
315 }
316 });
317
318 let mut features = features::find();
319
320 let path_dependencies = source_manifest
321 .dependencies
322 .iter()
323 .filter_map(|(name, dep)| {
324 let path = dep.path.as_ref()?;
325 if packages.iter().any(|p| &p.name == name) {
326 None
328 } else {
329 Some(PathDependency {
330 name: name.clone(),
331 normalized_path: path.canonicalize().ok()?,
332 })
333 }
334 })
335 .collect();
336
337 let crate_name = source_manifest.package.name.clone();
338 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
339 fs::create_dir_all(&project_dir)?;
340
341 let project_name = format!("{}-hydro-trybuild", crate_name);
342 let mut manifest = Runner::make_manifest(
343 &workspace,
344 &project_name,
345 &source_dir,
346 &packages,
347 &[],
348 source_manifest,
349 )?;
350
351 if let Some(enabled_features) = &mut features {
352 enabled_features
353 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
354 }
355
356 for runtime_feature in HYDRO_RUNTIME_FEATURES {
357 manifest.features.insert(
358 format!("hydro___feature_{runtime_feature}"),
359 vec![format!("hydro_lang/{runtime_feature}")],
360 );
361 }
362
363 manifest
364 .dependencies
365 .get_mut("hydro_lang")
366 .unwrap()
367 .features
368 .push("runtime_support".to_string());
369
370 manifest
371 .features
372 .insert("hydro___test".to_string(), dev_dependency_features);
373
374 if manifest
375 .workspace
376 .as_ref()
377 .is_some_and(|w| w.dependencies.is_empty())
378 {
379 manifest.workspace = None;
380 }
381
382 let project = Project {
383 dir: project_dir,
384 source_dir,
385 target_dir,
386 name: project_name.clone(),
387 update: Update::env()?,
388 has_pass: false,
389 has_compile_fail: false,
390 features,
391 workspace,
392 path_dependencies,
393 manifest,
394 keep_going: false,
395 };
396
397 {
398 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
399
400 let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
401 project_lock.lock()?;
402
403 fs::create_dir_all(path!(project.dir / "src"))?;
404
405 let crate_name_ident = syn::Ident::new(
406 &crate_name.replace("-", "_"),
407 proc_macro2::Span::call_site(),
408 );
409
410 write_atomic(
411 prettyplease::unparse(&syn::parse_quote! {
412 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
413
414 pub use #crate_name_ident as __root;
415
416 #[cfg(feature = "hydro___test")]
417 pub mod __staged;
418
419 #[cfg(not(feature = "hydro___test"))]
420 pub use #crate_name_ident::__staged;
421 })
422 .as_bytes(),
423 &path!(project.dir / "src" / "lib.rs"),
424 )
425 .unwrap();
426
427 let manifest_toml = toml::to_string(&project.manifest)?;
428 let base_manifest = manifest_toml.clone();
429
430 let feature_names: Vec<_> = project.manifest.features.keys().cloned().collect();
432
433 let dylib_dir = path!(project.dir / "dylib");
435 fs::create_dir_all(path!(dylib_dir / "src"))?;
436
437 let trybuild_crate_name_ident = syn::Ident::new(
438 &project_name.replace("-", "_"),
439 proc_macro2::Span::call_site(),
440 );
441 write_atomic(
442 prettyplease::unparse(&syn::parse_quote! {
443 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
444 pub use #trybuild_crate_name_ident::*;
445 })
446 .as_bytes(),
447 &path!(dylib_dir / "src" / "lib.rs"),
448 )?;
449
450 let serialized_edition = toml::to_string(
451 &vec![("edition", &project.manifest.package.edition)]
452 .into_iter()
453 .collect::<std::collections::HashMap<_, _>>(),
454 )
455 .unwrap();
456
457 let dylib_manifest = format!(
461 r#"[package]
462name = "{project_name}-dylib"
463version = "0.0.0"
464{}
465
466[lib]
467crate-type = ["{}"]
468
469[dependencies]
470{project_name} = {{ path = "..", default-features = false }}
471"#,
472 serialized_edition,
473 if cfg!(target_os = "windows") {
474 "rlib"
475 } else {
476 "dylib"
477 }
478 );
479 write_atomic(dylib_manifest.as_ref(), &path!(dylib_dir / "Cargo.toml"))?;
480
481 let dylib_examples_dir = path!(project.dir / "dylib-examples");
482 fs::create_dir_all(path!(dylib_examples_dir / "src"))?;
483 fs::create_dir_all(path!(dylib_examples_dir / "examples"))?;
484
485 write_atomic(
486 b"#![allow(unused_crate_dependencies)]\n",
487 &path!(dylib_examples_dir / "src" / "lib.rs"),
488 )?;
489
490 let features_section = feature_names
492 .iter()
493 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
494 .collect::<Vec<_>>()
495 .join("\n");
496
497 let dylib_examples_manifest = format!(
499 r#"[package]
500name = "{project_name}-dylib-examples"
501version = "0.0.0"
502{}
503
504[dev-dependencies]
505{project_name} = {{ path = "..", default-features = false }}
506{project_name}-dylib = {{ path = "../dylib", default-features = false }}
507
508[features]
509{features_section}
510
511[[example]]
512name = "sim-dylib"
513crate-type = ["cdylib"]
514"#,
515 serialized_edition
516 );
517 write_atomic(
518 dylib_examples_manifest.as_ref(),
519 &path!(dylib_examples_dir / "Cargo.toml"),
520 )?;
521
522 write_atomic(
524 prettyplease::unparse(&syn::parse_quote! {
525 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
526 include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
527 })
528 .as_bytes(),
529 &path!(dylib_examples_dir / "examples" / "sim-dylib.rs"),
530 )?;
531
532 let workspace_manifest = format!(
533 r#"{}
534[workspace]
535members = ["dylib", "dylib-examples"]
536"#,
537 base_manifest,
538 );
539
540 write_atomic(
541 workspace_manifest.as_ref(),
542 &path!(project.dir / "Cargo.toml"),
543 )?;
544
545 let manifest_hash = format!("{:X}", Sha256::digest(&workspace_manifest))
547 .chars()
548 .take(8)
549 .collect::<String>();
550
551 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
552 let workspace_cargo_lock_contents_and_hash = if workspace_cargo_lock.exists() {
553 let cargo_lock_contents = fs::read_to_string(&workspace_cargo_lock)?;
554
555 let hash = format!("{:X}", Sha256::digest(&cargo_lock_contents))
556 .chars()
557 .take(8)
558 .collect::<String>();
559
560 Some((cargo_lock_contents, hash))
561 } else {
562 None
563 };
564
565 let trybuild_hash = format!(
566 "{}-{}",
567 manifest_hash,
568 workspace_cargo_lock_contents_and_hash
569 .as_ref()
570 .map(|s| s.1.as_ref())
571 .unwrap_or("")
572 );
573
574 if !check_contents(
575 trybuild_hash.as_bytes(),
576 &path!(project.dir / ".hydro-trybuild-manifest"),
577 )
578 .is_ok_and(|b| b)
579 {
580 if let Some((cargo_lock_contents, _)) = workspace_cargo_lock_contents_and_hash {
582 write_atomic(
585 cargo_lock_contents.as_ref(),
586 &path!(project.dir / "Cargo.lock"),
587 )?;
588 } else {
589 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
590 }
591
592 std::process::Command::new("cargo")
594 .current_dir(&project.dir)
595 .args(["update", "-w"]) .stdout(std::process::Stdio::null())
597 .stderr(std::process::Stdio::null())
598 .status()
599 .unwrap();
600
601 write_atomic(
602 trybuild_hash.as_bytes(),
603 &path!(project.dir / ".hydro-trybuild-manifest"),
604 )?;
605 }
606
607 let examples_folder = path!(project.dir / "examples");
609 fs::create_dir_all(&examples_folder)?;
610
611 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
612 if workspace_dot_cargo_config_toml.exists() {
613 let dot_cargo_folder = path!(project.dir / ".cargo");
614 fs::create_dir_all(&dot_cargo_folder)?;
615
616 write_atomic(
617 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
618 &path!(dot_cargo_folder / "config.toml"),
619 )?;
620 }
621
622 let vscode_folder = path!(project.dir / ".vscode");
623 fs::create_dir_all(&vscode_folder)?;
624 write_atomic(
625 include_bytes!("./vscode-trybuild.json"),
626 &path!(vscode_folder / "settings.json"),
627 )?;
628 }
629
630 Ok((
631 project.dir.as_ref().into(),
632 project.target_dir.as_ref().into(),
633 project.features,
634 ))
635}
636
637fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
638 let mut file = File::options()
639 .read(true)
640 .write(false)
641 .create(false)
642 .truncate(false)
643 .open(path)?;
644 file.lock()?;
645
646 let mut existing_contents = Vec::new();
647 file.read_to_end(&mut existing_contents)?;
648 Ok(existing_contents == contents)
649}
650
651pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
652 let mut file = File::options()
653 .read(true)
654 .write(true)
655 .create(true)
656 .truncate(false)
657 .open(path)?;
658
659 let mut existing_contents = Vec::new();
660 file.read_to_end(&mut existing_contents)?;
661 if existing_contents != contents {
662 file.lock()?;
663 file.seek(SeekFrom::Start(0))?;
664 file.set_len(0)?;
665 file.write_all(contents)?;
666 }
667
668 Ok(())
669}