hydro_lang/deploy/
trybuild.rs

1use std::fs::{self, File};
2use std::io::{Read, Seek, SeekFrom, Write};
3use std::path::{Path, PathBuf};
4
5use dfir_lang::graph::DfirGraph;
6use sha2::{Digest, Sha256};
7use stageleft::internal::quote;
8use syn::visit_mut::VisitMut;
9use trybuild_internals_api::cargo::{self, Metadata};
10use trybuild_internals_api::env::Update;
11use trybuild_internals_api::run::{PathDependency, Project};
12use trybuild_internals_api::{Runner, dependencies, features, path};
13
14use super::trybuild_rewriters::UseTestModeStaged;
15
16pub const HYDRO_RUNTIME_FEATURES: &[&str] = &["deploy_integration", "runtime_measure"];
17
18static IS_TEST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
19
20static CONCURRENT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
21
22/// Enables "test mode" for Hydro, which makes it possible to compile Hydro programs written
23/// inside a `#[cfg(test)]` module. This should be enabled in a global [`ctor`] hook.
24///
25/// # Example
26/// ```ignore
27/// #[cfg(test)]
28/// mod test_init {
29///    #[ctor::ctor]
30///    fn init() {
31///        hydro_lang::deploy::init_test();
32///    }
33/// }
34/// ```
35pub fn init_test() {
36    IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
37}
38
39fn clean_name_hint(name_hint: &str) -> String {
40    name_hint
41        .replace("::", "__")
42        .replace(" ", "_")
43        .replace(",", "_")
44        .replace("<", "_")
45        .replace(">", "")
46        .replace("(", "")
47        .replace(")", "")
48}
49
50pub struct TrybuildConfig {
51    pub project_dir: PathBuf,
52    pub target_dir: PathBuf,
53    pub features: Option<Vec<String>>,
54}
55
56pub fn create_graph_trybuild(
57    graph: DfirGraph,
58    extra_stmts: Vec<syn::Stmt>,
59    name_hint: &Option<String>,
60) -> (String, TrybuildConfig) {
61    let source_dir = cargo::manifest_dir().unwrap();
62    let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
63    let crate_name = &source_manifest.package.name.to_string().replace("-", "_");
64
65    let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
66
67    let generated_code = compile_graph_trybuild(graph, extra_stmts, crate_name.clone(), is_test);
68
69    let inlined_staged: syn::File = if is_test {
70        let gen_staged = stageleft_tool::gen_staged_trybuild(
71            &path!(source_dir / "src" / "lib.rs"),
72            &path!(source_dir / "Cargo.toml"),
73            crate_name.clone(),
74            is_test,
75        );
76
77        syn::parse_quote! {
78            #[allow(
79                unused,
80                ambiguous_glob_reexports,
81                clippy::suspicious_else_formatting,
82                unexpected_cfgs,
83                reason = "generated code"
84            )]
85            pub mod __staged {
86                #gen_staged
87            }
88        }
89    } else {
90        let crate_name_ident = syn::Ident::new(crate_name, proc_macro2::Span::call_site());
91        syn::parse_quote!(
92            pub use #crate_name_ident::__staged;
93        )
94    };
95
96    let source = prettyplease::unparse(&syn::parse_quote! {
97        #generated_code
98
99        #inlined_staged
100    });
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    // TODO(shadaj): garbage collect this directory occasionally
116    fs::create_dir_all(path!(project_dir / "src" / "bin")).unwrap();
117
118    let out_path = path!(project_dir / "src" / "bin" / 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 is_test {
125        if cur_bin_enabled_features.is_none() {
126            cur_bin_enabled_features = Some(vec![]);
127        }
128
129        cur_bin_enabled_features
130            .as_mut()
131            .unwrap()
132            .push("hydro___test".to_string());
133    }
134
135    (
136        bin_name,
137        TrybuildConfig {
138            project_dir,
139            target_dir,
140            features: cur_bin_enabled_features,
141        },
142    )
143}
144
145pub fn compile_graph_trybuild(
146    partitioned_graph: DfirGraph,
147    extra_stmts: Vec<syn::Stmt>,
148    crate_name: String,
149    is_test: bool,
150) -> syn::File {
151    let mut diagnostics = Vec::new();
152    let mut dfir_expr: syn::Expr = syn::parse2(partitioned_graph.as_code(
153        &quote! { __root_dfir_rs },
154        true,
155        quote!(),
156        &mut diagnostics,
157    ))
158    .unwrap();
159
160    if is_test {
161        UseTestModeStaged {
162            crate_name: crate_name.clone(),
163        }
164        .visit_expr_mut(&mut dfir_expr);
165    }
166
167    let source_ast: syn::File = syn::parse_quote! {
168        #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
169        use hydro_lang::prelude::*;
170        use hydro_lang::runtime_support::dfir_rs as __root_dfir_rs;
171
172        #[allow(unused)]
173        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> {
174            #(#extra_stmts)*
175            #dfir_expr
176        }
177
178        #[hydro_lang::runtime_support::tokio::main(crate = "hydro_lang::runtime_support::tokio", flavor = "current_thread")]
179        async fn main() {
180            let ports = hydro_lang::runtime_support::dfir_rs::util::deploy::init_no_ack_start().await;
181            let flow = __hydro_runtime(&ports);
182            println!("ack start");
183
184            hydro_lang::runtime_support::resource_measurement::run(flow).await;
185        }
186    };
187    source_ast
188}
189
190pub fn create_trybuild()
191-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
192    let Metadata {
193        target_directory: target_dir,
194        workspace_root: workspace,
195        packages,
196    } = cargo::metadata()?;
197
198    let source_dir = cargo::manifest_dir()?;
199    let mut source_manifest = dependencies::get_manifest(&source_dir)?;
200
201    let mut dev_dependency_features = vec![];
202    source_manifest.dev_dependencies.retain(|k, v| {
203        if source_manifest.dependencies.contains_key(k) {
204            // already a non-dev dependency, so drop the dep and put the features under the test flag
205            for feat in &v.features {
206                dev_dependency_features.push(format!("{}/{}", k, feat));
207            }
208
209            false
210        } else {
211            // only enable this in test mode, so make it optional otherwise
212            dev_dependency_features.push(format!("dep:{k}"));
213
214            v.optional = true;
215            true
216        }
217    });
218
219    let mut features = features::find();
220
221    let path_dependencies = source_manifest
222        .dependencies
223        .iter()
224        .filter_map(|(name, dep)| {
225            let path = dep.path.as_ref()?;
226            if packages.iter().any(|p| &p.name == name) {
227                // Skip path dependencies coming from the workspace itself
228                None
229            } else {
230                Some(PathDependency {
231                    name: name.clone(),
232                    normalized_path: path.canonicalize().ok()?,
233                })
234            }
235        })
236        .collect();
237
238    let crate_name = source_manifest.package.name.clone();
239    let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
240    fs::create_dir_all(&project_dir)?;
241
242    let project_name = format!("{}-hydro-trybuild", crate_name);
243    let mut manifest = Runner::make_manifest(
244        &workspace,
245        &project_name,
246        &source_dir,
247        &packages,
248        &[],
249        source_manifest,
250    )?;
251
252    if let Some(enabled_features) = &mut features {
253        enabled_features
254            .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
255    }
256
257    for runtime_feature in HYDRO_RUNTIME_FEATURES {
258        manifest.features.insert(
259            format!("hydro___feature_{runtime_feature}"),
260            vec![format!("hydro_lang/{runtime_feature}")],
261        );
262    }
263
264    manifest
265        .dependencies
266        .get_mut("hydro_lang")
267        .unwrap()
268        .features
269        .push("runtime_support".to_string());
270
271    manifest
272        .features
273        .insert("hydro___test".to_string(), dev_dependency_features);
274
275    let project = Project {
276        dir: project_dir,
277        source_dir,
278        target_dir,
279        name: project_name,
280        update: Update::env()?,
281        has_pass: false,
282        has_compile_fail: false,
283        features,
284        workspace,
285        path_dependencies,
286        manifest,
287        keep_going: false,
288    };
289
290    {
291        let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
292
293        let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
294        project_lock.lock()?;
295
296        let manifest_toml = toml::to_string(&project.manifest)?;
297        write_atomic(manifest_toml.as_ref(), &path!(project.dir / "Cargo.toml"))?;
298
299        let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
300        if workspace_cargo_lock.exists() {
301            write_atomic(
302                fs::read_to_string(&workspace_cargo_lock)?.as_ref(),
303                &path!(project.dir / "Cargo.lock"),
304            )?;
305        } else {
306            let _ = cargo::cargo(&project).arg("generate-lockfile").status();
307        }
308
309        let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
310        if workspace_dot_cargo_config_toml.exists() {
311            let dot_cargo_folder = path!(project.dir / ".cargo");
312            fs::create_dir_all(&dot_cargo_folder)?;
313
314            write_atomic(
315                fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
316                &path!(dot_cargo_folder / "config.toml"),
317            )?;
318        }
319
320        let vscode_folder = path!(project.dir / ".vscode");
321        fs::create_dir_all(&vscode_folder)?;
322        write_atomic(
323            include_bytes!("./vscode-trybuild.json"),
324            &path!(vscode_folder / "settings.json"),
325        )?;
326    }
327
328    Ok((
329        project.dir.as_ref().into(),
330        path!(project.target_dir / "hydro_trybuild"),
331        project.features,
332    ))
333}
334
335fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
336    let mut file = File::options()
337        .read(true)
338        .write(true)
339        .create(true)
340        .truncate(false)
341        .open(path)?;
342    file.lock()?;
343
344    let mut existing_contents = Vec::new();
345    file.read_to_end(&mut existing_contents)?;
346    if existing_contents != contents {
347        file.seek(SeekFrom::Start(0))?;
348        file.set_len(0)?;
349        file.write_all(contents)?;
350    }
351
352    Ok(())
353}