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