hydro_lang/compile/trybuild/
generate.rs1use 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", "runtime_measure", "docker_runtime"];
22
23pub(crate) static IS_TEST: std::sync::atomic::AtomicBool =
24 std::sync::atomic::AtomicBool::new(false);
25
26pub(crate) static CONCURRENT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
27
28pub fn init_test() {
42 IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
43}
44
45#[cfg(feature = "deploy")]
46fn clean_name_hint(name_hint: &str) -> String {
47 name_hint
48 .replace("::", "__")
49 .replace(" ", "_")
50 .replace(",", "_")
51 .replace("<", "_")
52 .replace(">", "")
53 .replace("(", "")
54 .replace(")", "")
55}
56
57#[derive(Debug, Clone)]
58pub struct TrybuildConfig {
59 pub project_dir: PathBuf,
60 pub target_dir: PathBuf,
61 pub features: Option<Vec<String>>,
62}
63
64#[cfg(feature = "deploy")]
65pub fn create_graph_trybuild(
66 graph: DfirGraph,
67 extra_stmts: Vec<syn::Stmt>,
68 name_hint: &Option<String>,
69 is_containerized: bool,
70) -> (String, TrybuildConfig) {
71 let source_dir = cargo::manifest_dir().unwrap();
72 let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
73 let crate_name = &source_manifest.package.name.to_string().replace("-", "_");
74
75 let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
76
77 let generated_code = compile_graph_trybuild(
78 graph,
79 extra_stmts,
80 crate_name.clone(),
81 is_test,
82 is_containerized,
83 );
84
85 let inlined_staged = if is_test {
86 let gen_staged = stageleft_tool::gen_staged_trybuild(
87 &path!(source_dir / "src" / "lib.rs"),
88 &path!(source_dir / "Cargo.toml"),
89 crate_name.clone(),
90 Some("hydro___test".to_string()),
91 );
92
93 Some(prettyplease::unparse(&syn::parse_quote! {
94 #![allow(
95 unused,
96 ambiguous_glob_reexports,
97 clippy::suspicious_else_formatting,
98 unexpected_cfgs,
99 reason = "generated code"
100 )]
101
102 #gen_staged
103 }))
104 } else {
105 None
106 };
107
108 let source = prettyplease::unparse(&generated_code);
109
110 let hash = format!("{:X}", Sha256::digest(&source))
111 .chars()
112 .take(8)
113 .collect::<String>();
114
115 let bin_name = if let Some(name_hint) = &name_hint {
116 format!("{}_{}", clean_name_hint(name_hint), &hash)
117 } else {
118 hash
119 };
120
121 let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
122
123 fs::create_dir_all(path!(project_dir / "examples")).unwrap();
125
126 let out_path = path!(project_dir / "examples" / format!("{bin_name}.rs"));
127 {
128 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
129 write_atomic(source.as_ref(), &out_path).unwrap();
130 }
131
132 if let Some(inlined_staged) = inlined_staged {
133 let staged_path = path!(project_dir / "src" / "__staged.rs");
134 {
135 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
136 write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
137 }
138 }
139
140 if is_test {
141 if cur_bin_enabled_features.is_none() {
142 cur_bin_enabled_features = Some(vec![]);
143 }
144
145 cur_bin_enabled_features
146 .as_mut()
147 .unwrap()
148 .push("hydro___test".to_string());
149 }
150
151 (
152 bin_name,
153 TrybuildConfig {
154 project_dir,
155 target_dir,
156 features: cur_bin_enabled_features,
157 },
158 )
159}
160
161#[cfg(feature = "deploy")]
162pub fn compile_graph_trybuild(
163 partitioned_graph: DfirGraph,
164 extra_stmts: Vec<syn::Stmt>,
165 crate_name: String,
166 is_test: bool,
167 is_containerized: bool,
168) -> syn::File {
169 let mut diagnostics = Vec::new();
170 let mut dfir_expr: syn::Expr = syn::parse2(partitioned_graph.as_code(
171 "e! { __root_dfir_rs },
172 true,
173 quote!(),
174 &mut diagnostics,
175 ))
176 .unwrap();
177
178 if is_test {
179 UseTestModeStaged {
180 crate_name: crate_name.clone(),
181 }
182 .visit_expr_mut(&mut dfir_expr);
183 }
184
185 let trybuild_crate_name_ident = quote::format_ident!("{}_hydro_trybuild", crate_name);
186
187 let source_ast: syn::File = if is_containerized {
188 syn::parse_quote! {
189 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
190 use hydro_lang::prelude::*;
191 use hydro_lang::runtime_support::dfir_rs as __root_dfir_rs;
192 pub use #trybuild_crate_name_ident::__staged;
193
194 #[allow(unused)]
195 async fn __hydro_runtime<'a>() -> hydro_lang::runtime_support::dfir_rs::scheduled::graph::Dfir<'a> {
196 #(#extra_stmts)*
198
199 #dfir_expr
201 }
202
203 #[hydro_lang::runtime_support::tokio::main(crate = "hydro_lang::runtime_support::tokio", flavor = "current_thread")]
204 async fn main() {
205 hydro_lang::telemetry::initialize_tracing();
206
207 let flow = __hydro_runtime().await;
208
209 hydro_lang::runtime_support::resource_measurement::run_containerized(flow).await;
210 }
211 }
212 } else {
213 syn::parse_quote! {
214 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
215 use hydro_lang::prelude::*;
216 use hydro_lang::runtime_support::dfir_rs as __root_dfir_rs;
217 pub use #trybuild_crate_name_ident::__staged;
218
219 #[allow(unused)]
220 fn __hydro_runtime<'a>(
221 __hydro_lang_trybuild_cli: &'a hydro_lang::runtime_support::dfir_rs::util::deploy::DeployPorts<hydro_lang::__staged::deploy::deploy_runtime::HydroMeta>
222 )
223 -> hydro_lang::runtime_support::dfir_rs::scheduled::graph::Dfir<'a>
224 {
225 #(#extra_stmts)*
226 #dfir_expr
227 }
228
229 #[hydro_lang::runtime_support::tokio::main(crate = "hydro_lang::runtime_support::tokio", flavor = "current_thread")]
230 async fn main() {
231 let ports = hydro_lang::runtime_support::dfir_rs::util::deploy::init_no_ack_start().await;
232 let flow = __hydro_runtime(&ports);
233 println!("ack start");
234
235 hydro_lang::runtime_support::resource_measurement::run(flow).await;
236 }
237 }
238 };
239 source_ast
240}
241
242pub fn create_trybuild()
243-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
244 let Metadata {
245 target_directory: target_dir,
246 workspace_root: workspace,
247 packages,
248 } = cargo::metadata()?;
249
250 let source_dir = cargo::manifest_dir()?;
251 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
252
253 let mut dev_dependency_features = vec![];
254 source_manifest.dev_dependencies.retain(|k, v| {
255 if source_manifest.dependencies.contains_key(k) {
256 for feat in &v.features {
258 dev_dependency_features.push(format!("{}/{}", k, feat));
259 }
260
261 false
262 } else {
263 dev_dependency_features.push(format!("dep:{k}"));
265
266 v.optional = true;
267 true
268 }
269 });
270
271 let mut features = features::find();
272
273 let path_dependencies = source_manifest
274 .dependencies
275 .iter()
276 .filter_map(|(name, dep)| {
277 let path = dep.path.as_ref()?;
278 if packages.iter().any(|p| &p.name == name) {
279 None
281 } else {
282 Some(PathDependency {
283 name: name.clone(),
284 normalized_path: path.canonicalize().ok()?,
285 })
286 }
287 })
288 .collect();
289
290 let crate_name = source_manifest.package.name.clone();
291 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
292 fs::create_dir_all(&project_dir)?;
293
294 let project_name = format!("{}-hydro-trybuild", crate_name);
295 let mut manifest = Runner::make_manifest(
296 &workspace,
297 &project_name,
298 &source_dir,
299 &packages,
300 &[],
301 source_manifest,
302 )?;
303
304 if let Some(enabled_features) = &mut features {
305 enabled_features
306 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
307 }
308
309 for runtime_feature in HYDRO_RUNTIME_FEATURES {
310 manifest.features.insert(
311 format!("hydro___feature_{runtime_feature}"),
312 vec![format!("hydro_lang/{runtime_feature}")],
313 );
314 }
315
316 manifest
317 .dependencies
318 .get_mut("hydro_lang")
319 .unwrap()
320 .features
321 .push("runtime_support".to_string());
322
323 manifest
324 .features
325 .insert("hydro___test".to_string(), dev_dependency_features);
326
327 let project = Project {
328 dir: project_dir,
329 source_dir,
330 target_dir,
331 name: project_name,
332 update: Update::env()?,
333 has_pass: false,
334 has_compile_fail: false,
335 features,
336 workspace,
337 path_dependencies,
338 manifest,
339 keep_going: false,
340 };
341
342 {
343 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
344
345 let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
346 project_lock.lock()?;
347
348 fs::create_dir_all(path!(project.dir / "src"))?;
349
350 let crate_name_ident = syn::Ident::new(
351 &crate_name.replace("-", "_"),
352 proc_macro2::Span::call_site(),
353 );
354 write_atomic(
355 prettyplease::unparse(&syn::parse_quote! {
356 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
357
358 #[cfg(feature = "hydro___test")]
359 pub mod __staged;
360
361 #[cfg(not(feature = "hydro___test"))]
362 pub use #crate_name_ident::__staged;
363 })
364 .as_bytes(),
365 &path!(project.dir / "src" / "lib.rs"),
366 )
367 .unwrap();
368
369 let manifest_toml = toml::to_string(&project.manifest)?;
370 let manifest_with_example = format!(
371 r#"{}
372
373[lib]
374crate-type = [{}]
375
376[[example]]
377name = "sim-dylib"
378crate-type = ["cdylib"]"#,
379 manifest_toml,
380 if cfg!(target_os = "windows") {
381 r#""rlib""# } else {
383 r#""rlib", "dylib""#
384 },
385 );
386
387 write_atomic(
388 manifest_with_example.as_ref(),
389 &path!(project.dir / "Cargo.toml"),
390 )?;
391
392 let manifest_hash = format!("{:X}", Sha256::digest(&manifest_with_example))
393 .chars()
394 .take(8)
395 .collect::<String>();
396
397 if !check_contents(
398 manifest_hash.as_bytes(),
399 &path!(project.dir / ".hydro-trybuild-manifest"),
400 )
401 .is_ok_and(|b| b)
402 {
403 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
405 if workspace_cargo_lock.exists() {
406 write_atomic(
407 fs::read_to_string(&workspace_cargo_lock)?.as_ref(),
408 &path!(project.dir / "Cargo.lock"),
409 )?;
410 } else {
411 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
412 }
413
414 std::process::Command::new("cargo")
416 .current_dir(&project.dir)
417 .args(["update", "-w"]) .stdout(std::process::Stdio::null())
419 .stderr(std::process::Stdio::null())
420 .status()
421 .unwrap();
422
423 write_atomic(
424 manifest_hash.as_bytes(),
425 &path!(project.dir / ".hydro-trybuild-manifest"),
426 )?;
427 }
428
429 let examples_folder = path!(project.dir / "examples");
430 fs::create_dir_all(&examples_folder)?;
431 write_atomic(
432 prettyplease::unparse(&syn::parse_quote! {
433 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
434 include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
435 })
436 .as_bytes(),
437 &path!(project.dir / "examples" / "sim-dylib.rs"),
438 )?;
439
440 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
441 if workspace_dot_cargo_config_toml.exists() {
442 let dot_cargo_folder = path!(project.dir / ".cargo");
443 fs::create_dir_all(&dot_cargo_folder)?;
444
445 write_atomic(
446 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
447 &path!(dot_cargo_folder / "config.toml"),
448 )?;
449 }
450
451 let vscode_folder = path!(project.dir / ".vscode");
452 fs::create_dir_all(&vscode_folder)?;
453 write_atomic(
454 include_bytes!("./vscode-trybuild.json"),
455 &path!(vscode_folder / "settings.json"),
456 )?;
457 }
458
459 Ok((
460 project.dir.as_ref().into(),
461 path!(project.target_dir / "hydro_trybuild"),
462 project.features,
463 ))
464}
465
466fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
467 let mut file = File::options()
468 .read(true)
469 .write(false)
470 .create(false)
471 .truncate(false)
472 .open(path)?;
473 file.lock()?;
474
475 let mut existing_contents = Vec::new();
476 file.read_to_end(&mut existing_contents)?;
477 Ok(existing_contents == contents)
478}
479
480pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
481 let mut file = File::options()
482 .read(true)
483 .write(true)
484 .create(true)
485 .truncate(false)
486 .open(path)?;
487
488 let mut existing_contents = Vec::new();
489 file.read_to_end(&mut existing_contents)?;
490 if existing_contents != contents {
491 file.lock()?;
492 file.seek(SeekFrom::Start(0))?;
493 file.set_len(0)?;
494 file.write_all(contents)?;
495 }
496
497 Ok(())
498}