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