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; 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 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 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 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 (project_dir, target_dir, cur_bin_enabled_features),
125 )
126}
127
128pub fn compile_graph_trybuild(
129 partitioned_graph: DfirGraph,
130 extra_stmts: Vec<syn::Stmt>,
131) -> syn::File {
132 let mut diagnostics = Vec::new();
133 let tokens = partitioned_graph.as_code(
134 "e! { hydro_lang::runtime_support::dfir_rs },
135 true,
136 quote!(),
137 &mut diagnostics,
138 );
139
140 let source_ast: syn::File = syn::parse_quote! {
141 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
142 use hydro_lang::*;
143
144 #[allow(unused)]
145 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> {
146 #(#extra_stmts)*
147 #tokens
148 }
149
150 #[hydro_lang::runtime_support::tokio::main(crate = "hydro_lang::runtime_support::tokio")]
151 async fn main() {
152 let ports = hydro_lang::runtime_support::dfir_rs::util::deploy::init_no_ack_start().await;
153 let flow = __hydro_runtime(&ports);
154 println!("ack start");
155
156 hydro_lang::runtime_support::resource_measurement::run(flow).await;
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 for feat in &v.features {
178 dev_dependency_features.push(format!("{}/{}", k, feat));
179 }
180
181 false
182 } else {
183 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 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 let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
276 #[cfg(nightly)]
278 project_lock.lock()?;
279 #[cfg(not(nightly))]
280 fs2::FileExt::lock_exclusive(&project_lock)?;
281
282 let manifest_toml = toml::to_string(&project.manifest)?;
283 write_atomic(manifest_toml.as_ref(), &path!(project.dir / "Cargo.toml"))?;
284
285 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
286 if workspace_cargo_lock.exists() {
287 write_atomic(
288 fs::read_to_string(&workspace_cargo_lock)?.as_ref(),
289 &path!(project.dir / "Cargo.lock"),
290 )?;
291 } else {
292 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
293 }
294
295 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
296 if workspace_dot_cargo_config_toml.exists() {
297 let dot_cargo_folder = path!(project.dir / ".cargo");
298 fs::create_dir_all(&dot_cargo_folder)?;
299
300 write_atomic(
301 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
302 &path!(dot_cargo_folder / "config.toml"),
303 )?;
304 }
305 }
306
307 Ok((
308 project.dir.as_ref().into(),
309 path!(project.target_dir / "hydro_trybuild"),
310 project.features,
311 ))
312}
313
314fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
315 let mut file = File::options()
316 .read(true)
317 .write(true)
318 .create(true)
319 .truncate(false)
320 .open(path)?;
321
322 #[cfg(nightly)]
324 file.lock()?;
325 #[cfg(not(nightly))]
326 fs2::FileExt::lock_exclusive(&file)?;
327
328 let mut existing_contents = Vec::new();
329 file.read_to_end(&mut existing_contents)?;
330 if existing_contents != contents {
331 file.seek(SeekFrom::Start(0))?;
332 file.set_len(0)?;
333 file.write_all(contents)?;
334 }
335
336 Ok(())
337}