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