hydro_lang/deploy/
trybuild.rs

1use std::fs::{self, File};
2use std::io::{Read, Write};
3use std::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::ReplaceCrateNameWithStaged;
15
16pub const HYDRO_RUNTIME_FEATURES: [&str; 1] = ["runtime_measure"];
17
18static IS_TEST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
19
20pub fn init_test() {
21    IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
22}
23
24fn clean_name_hint(name_hint: &str) -> String {
25    name_hint
26        .replace("::", "__")
27        .replace(" ", "_")
28        .replace(",", "_")
29        .replace("<", "_")
30        .replace(">", "")
31        .replace("(", "")
32        .replace(")", "")
33}
34
35pub fn create_graph_trybuild(
36    graph: DfirGraph,
37    extra_stmts: Vec<syn::Stmt>,
38    name_hint: &Option<String>,
39) -> (String, (PathBuf, PathBuf, Option<Vec<String>>)) {
40    let source_dir = cargo::manifest_dir().unwrap();
41    let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
42    let crate_name = &source_manifest.package.name.to_string().replace("-", "_");
43
44    let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
45
46    let mut generated_code = compile_graph_trybuild(graph, extra_stmts);
47
48    ReplaceCrateNameWithStaged {
49        crate_name: crate_name.clone(),
50        is_test,
51    }
52    .visit_file_mut(&mut generated_code);
53
54    let inlined_staged = if is_test {
55        stageleft_tool::gen_staged_trybuild(
56            &path!(source_dir / "src" / "lib.rs"),
57            crate_name.clone(),
58            is_test,
59        )
60    } else {
61        syn::parse_quote!()
62    };
63
64    let source = prettyplease::unparse(&syn::parse_quote! {
65        #generated_code
66
67        #[allow(
68            unused,
69            ambiguous_glob_reexports,
70            clippy::suspicious_else_formatting,
71            unexpected_cfgs,
72            reason = "generated code"
73        )]
74        pub mod __staged {
75            #inlined_staged
76        }
77    });
78
79    let hash = format!("{:X}", Sha256::digest(&source))
80        .chars()
81        .take(8)
82        .collect::<String>();
83
84    let bin_name = if let Some(name_hint) = &name_hint {
85        format!("{}_{}", clean_name_hint(name_hint), &hash)
86    } else {
87        hash
88    };
89
90    let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
91
92    // TODO(shadaj): garbage collect this directory occasionally
93    fs::create_dir_all(path!(project_dir / "src" / "bin")).unwrap();
94
95    let out_path = path!(project_dir / "src" / "bin" / format!("{bin_name}.rs"));
96    {
97        let mut out_file = File::options()
98            .read(true)
99            .write(true)
100            .create(true)
101            .truncate(false)
102            .open(&out_path)
103            .unwrap();
104        #[cfg(nightly)]
105        out_file.lock().unwrap();
106
107        let mut existing_contents = String::new();
108        out_file.read_to_string(&mut existing_contents).unwrap();
109        if existing_contents != source {
110            out_file.write_all(source.as_ref()).unwrap()
111        }
112    }
113
114    if is_test {
115        if cur_bin_enabled_features.is_none() {
116            cur_bin_enabled_features = Some(vec![]);
117        }
118
119        cur_bin_enabled_features
120            .as_mut()
121            .unwrap()
122            .push("hydro___test".to_string());
123    }
124
125    (
126        bin_name,
127        (project_dir, target_dir, cur_bin_enabled_features),
128    )
129}
130
131pub fn compile_graph_trybuild(
132    partitioned_graph: DfirGraph,
133    extra_stmts: Vec<syn::Stmt>,
134) -> syn::File {
135    let mut diagnostics = Vec::new();
136    let tokens = partitioned_graph.as_code(
137        &quote! { hydro_lang::dfir_rs },
138        true,
139        quote!(),
140        &mut diagnostics,
141    );
142
143    let source_ast: syn::File = syn::parse_quote! {
144        #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
145        use hydro_lang::*;
146
147        #[allow(unused)]
148        fn __hydro_runtime<'a>(__hydro_lang_trybuild_cli: &'a hydro_lang::dfir_rs::util::deploy::DeployPorts<hydro_lang::deploy_runtime::HydroMeta>) -> hydro_lang::dfir_rs::scheduled::graph::Dfir<'a> {
149            #(#extra_stmts)*
150            #tokens
151        }
152
153        #[tokio::main]
154        async fn main() {
155            let ports = hydro_lang::dfir_rs::util::deploy::init_no_ack_start().await;
156            let flow = __hydro_runtime(&ports);
157            println!("ack start");
158
159            hydro_lang::runtime_support::resource_measurement::run(flow).await;
160        }
161    };
162    source_ast
163}
164
165pub fn create_trybuild()
166-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
167    let Metadata {
168        target_directory: target_dir,
169        workspace_root: workspace,
170        packages,
171    } = cargo::metadata()?;
172
173    let source_dir = cargo::manifest_dir()?;
174    let mut source_manifest = dependencies::get_manifest(&source_dir)?;
175
176    let mut dev_dependency_features = vec![];
177    source_manifest.dev_dependencies.retain(|k, v| {
178        if source_manifest.dependencies.contains_key(k) {
179            // already a non-dev dependency, so drop the dep and put the features under the test flag
180            for feat in &v.features {
181                dev_dependency_features.push(format!("{}/{}", k, feat));
182            }
183
184            false
185        } else {
186            // only enable this in test mode, so make it optional otherwise
187            dev_dependency_features.push(format!("dep:{k}"));
188
189            v.optional = true;
190            true
191        }
192    });
193
194    let mut features = features::find();
195
196    let path_dependencies = source_manifest
197        .dependencies
198        .iter()
199        .filter_map(|(name, dep)| {
200            let path = dep.path.as_ref()?;
201            if packages.iter().any(|p| &p.name == name) {
202                // Skip path dependencies coming from the workspace itself
203                None
204            } else {
205                Some(PathDependency {
206                    name: name.clone(),
207                    normalized_path: path.canonicalize().ok()?,
208                })
209            }
210        })
211        .collect();
212
213    let crate_name = source_manifest.package.name.clone();
214    let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
215    fs::create_dir_all(&project_dir)?;
216
217    let project_name = format!("{}-hydro-trybuild", crate_name);
218    let mut manifest = Runner::make_manifest(
219        &workspace,
220        &project_name,
221        &source_dir,
222        &packages,
223        &[],
224        source_manifest,
225    )?;
226
227    manifest.features.remove("stageleft_devel");
228
229    if let Some(enabled_features) = &mut features {
230        enabled_features
231            .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
232
233        manifest
234            .features
235            .get_mut("default")
236            .iter_mut()
237            .for_each(|v| {
238                v.retain(|f| f != "stageleft_devel");
239            });
240    }
241
242    for runtime_feature in HYDRO_RUNTIME_FEATURES {
243        manifest.features.insert(
244            format!("hydro___feature_{runtime_feature}"),
245            vec![format!("hydro_lang/{runtime_feature}")],
246        );
247    }
248
249    manifest
250        .features
251        .insert("hydro___test".to_string(), dev_dependency_features);
252
253    let project = Project {
254        dir: project_dir,
255        source_dir,
256        target_dir,
257        name: project_name,
258        update: Update::env()?,
259        has_pass: false,
260        has_compile_fail: false,
261        features,
262        workspace,
263        path_dependencies,
264        manifest,
265        keep_going: false,
266    };
267
268    {
269        #[cfg(nightly)]
270        let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
271        #[cfg(nightly)]
272        project_lock.lock()?;
273
274        let manifest_toml = toml::to_string(&project.manifest)?;
275        fs::write(path!(project.dir / "Cargo.toml"), manifest_toml)?;
276
277        let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
278        if workspace_cargo_lock.exists() {
279            let _ = fs::copy(workspace_cargo_lock, path!(project.dir / "Cargo.lock"));
280        } else {
281            let _ = cargo::cargo(&project).arg("generate-lockfile").status();
282        }
283
284        let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
285        if workspace_dot_cargo_config_toml.exists() {
286            let dot_cargo_folder = path!(project.dir / ".cargo");
287            fs::create_dir_all(&dot_cargo_folder)?;
288
289            let _ = fs::copy(
290                workspace_dot_cargo_config_toml,
291                path!(dot_cargo_folder / "config.toml"),
292            );
293        }
294    }
295
296    Ok((
297        project.dir.as_ref().into(),
298        path!(project.target_dir / "hydro_trybuild"),
299        project.features,
300    ))
301}