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