hydro_lang/compile/trybuild/
generate.rs

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 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
29/// Enables "test mode" for Hydro, which makes it possible to compile Hydro programs written
30/// inside a `#[cfg(test)]` module. This should be enabled in a global [`ctor`] hook.
31///
32/// # Example
33/// ```ignore
34/// #[cfg(test)]
35/// mod test_init {
36///    #[ctor::ctor]
37///    fn init() {
38///        hydro_lang::compile::init_test();
39///    }
40/// }
41/// ```
42pub 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    // TODO(shadaj): garbage collect this directory occasionally
117    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        &quote! { __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            // already a non-dev dependency, so drop the dep and put the features under the test flag
221            for feat in &v.features {
222                dev_dependency_features.push(format!("{}/{}", k, feat));
223            }
224
225            false
226        } else {
227            // only enable this in test mode, so make it optional otherwise
228            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                // Skip path dependencies coming from the workspace itself
244                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""# // see https://github.com/bevyengine/bevy/pull/2016
343            } 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            // this is expensive, so we only do it if the manifest changed
365            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            // not `--offline` because some new runtime features may be enabled
376            std::process::Command::new("cargo")
377                .current_dir(&project.dir)
378                .args(["update", "-w"]) // -w to not actually update any versions
379                .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}