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