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