1use std::fs::{self, File};
2use std::io::{Read, Seek, SeekFrom, Write};
3use std::path::{Path, PathBuf};
4
5#[cfg(any(feature = "deploy", feature = "maelstrom"))]
6use dfir_lang::diagnostic::Diagnostics;
7#[cfg(any(feature = "deploy", feature = "maelstrom"))]
8use dfir_lang::graph::DfirGraph;
9use sha2::{Digest, Sha256};
10#[cfg(any(feature = "deploy", feature = "maelstrom"))]
11use stageleft::internal::quote;
12use trybuild_internals_api::cargo::{self, Metadata};
13use trybuild_internals_api::env::Update;
14use trybuild_internals_api::run::{PathDependency, Project};
15use trybuild_internals_api::{Runner, dependencies, features, path};
16
17pub const HYDRO_RUNTIME_FEATURES: &[&str] = &[
18 "deploy_integration",
19 "runtime_measure",
20 "docker_runtime",
21 "ecs_runtime",
22 "maelstrom_runtime",
23 "sim_runtime",
24];
25
26#[cfg(any(feature = "deploy", feature = "maelstrom"))]
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum LinkingMode {
32 #[cfg_attr(
35 not(feature = "deploy"),
36 expect(
37 dead_code,
38 reason = "only constructed by the deploy backends; Maelstrom-only builds use Dynamic"
39 )
40 )]
41 Static,
42 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
43 Dynamic,
44}
45
46#[cfg(any(feature = "deploy", feature = "maelstrom"))]
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum DeployMode {
50 #[cfg(feature = "deploy")]
51 HydroDeploy,
53 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
54 Containerized,
56 #[cfg(feature = "maelstrom")]
57 Maelstrom,
59}
60
61pub(crate) static IS_TEST: std::sync::atomic::AtomicBool =
62 std::sync::atomic::AtomicBool::new(false);
63
64pub(crate) static CONCURRENT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
65
66pub fn init_test() {
80 IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
81}
82
83#[cfg(any(feature = "deploy", feature = "maelstrom"))]
84fn clean_bin_name_prefix(bin_name_prefix: &str) -> String {
85 bin_name_prefix
86 .replace("::", "__")
87 .replace(" ", "_")
88 .replace(",", "_")
89 .replace("<", "_")
90 .replace(">", "")
91 .replace("(", "")
92 .replace(")", "")
93 .replace("{", "_")
94 .replace("}", "_")
95}
96
97#[derive(Debug, Clone)]
98pub struct TrybuildConfig {
99 pub project_dir: PathBuf,
100 pub target_dir: PathBuf,
101 pub features: Option<Vec<String>>,
102 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
103 #[cfg_attr(
106 not(feature = "deploy"),
107 expect(dead_code, reason = "only read by the deploy backends")
108 )]
109 pub linking_mode: LinkingMode,
113}
114
115#[cfg(any(feature = "deploy", feature = "maelstrom"))]
116pub fn create_graph_trybuild(
117 graph: DfirGraph,
118 extra_stmts: &[syn::Stmt],
119 sidecars: &[syn::Expr],
120 bin_name_prefix: Option<&str>,
121 deploy_mode: DeployMode,
122 linking_mode: LinkingMode,
123) -> (String, TrybuildConfig) {
124 let source_dir = cargo::manifest_dir().unwrap();
125 let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
126 let crate_name = source_manifest.package.name.replace("-", "_");
127
128 let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
129
130 let generated_code =
131 compile_graph_trybuild(graph, extra_stmts, sidecars, &crate_name, deploy_mode);
132
133 let inlined_staged = if is_test {
134 let raw_toml_manifest = toml::from_str::<toml::Value>(
135 &fs::read_to_string(path!(source_dir / "Cargo.toml")).unwrap(),
136 )
137 .unwrap();
138
139 let maybe_custom_lib_path = raw_toml_manifest
140 .get("lib")
141 .and_then(|lib| lib.get("path"))
142 .and_then(|path| path.as_str());
143
144 let mut gen_staged = stageleft_tool::gen_staged_trybuild(
145 &maybe_custom_lib_path
146 .map(|s| path!(source_dir / s))
147 .unwrap_or_else(|| path!(source_dir / "src" / "lib.rs")),
148 &path!(source_dir / "Cargo.toml"),
149 &crate_name,
150 Some("hydro___test".to_owned()),
151 );
152
153 gen_staged.attrs.insert(
154 0,
155 syn::parse_quote! {
156 #![allow(
157 unused,
158 ambiguous_glob_reexports,
159 clippy::suspicious_else_formatting,
160 unexpected_cfgs,
161 reason = "generated code"
162 )]
163 },
164 );
165
166 Some(prettyplease::unparse(&gen_staged))
167 } else {
168 None
169 };
170
171 let source = prettyplease::unparse(&generated_code);
172
173 let hash = format!("{:X}", Sha256::digest(&source))
174 .chars()
175 .take(8)
176 .collect::<String>();
177
178 let bin_name = if let Some(bin_name_prefix) = &bin_name_prefix {
179 format!("{}_{}", clean_bin_name_prefix(bin_name_prefix), &hash)
180 } else {
181 hash
182 };
183
184 let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
185
186 let examples_dir = match linking_mode {
188 LinkingMode::Static => path!(project_dir / "examples"),
189 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
190 LinkingMode::Dynamic => path!(project_dir / "dylib-examples" / "examples"),
191 };
192
193 fs::create_dir_all(&examples_dir).unwrap();
195
196 let out_path = path!(examples_dir / format!("{bin_name}.rs"));
197 {
198 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
199 write_atomic(source.as_ref(), &out_path).unwrap();
200 }
201
202 if let Some(inlined_staged) = inlined_staged {
203 let staged_path = path!(project_dir / "src" / "__staged.rs");
204 {
205 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
206 write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
207 }
208 }
209
210 if is_test {
211 if cur_bin_enabled_features.is_none() {
212 cur_bin_enabled_features = Some(vec![]);
213 }
214
215 cur_bin_enabled_features
216 .as_mut()
217 .unwrap()
218 .push("hydro___test".to_owned());
219 }
220
221 (
222 bin_name,
223 TrybuildConfig {
224 project_dir,
225 target_dir,
226 features: cur_bin_enabled_features,
227 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
228 linking_mode,
229 },
230 )
231}
232
233#[cfg(any(feature = "deploy", feature = "maelstrom"))]
234pub fn compile_graph_trybuild(
235 partitioned_graph: DfirGraph,
236 extra_stmts: &[syn::Stmt],
237 sidecars: &[syn::Expr],
238 crate_name: &str,
239 deploy_mode: DeployMode,
240) -> syn::File {
241 use crate::staging_util::get_this_crate;
242
243 let mut diagnostics = Diagnostics::new();
244 let dfir_expr: syn::Expr = syn::parse2(
245 partitioned_graph
246 .as_code("e! { __root_dfir_rs }, true, quote!(), &mut diagnostics)
247 .expect("DFIR code generation failed with diagnostics."),
248 )
249 .unwrap();
250
251 let orig_crate_name = quote::format_ident!("{}", crate_name);
252 let trybuild_crate_name_ident = quote::format_ident!("{}_hydro_trybuild", crate_name);
253 let root = get_this_crate();
254 let tokio_main_ident = format!("{}::runtime_support::tokio", root);
255 let dfir_ident = quote::format_ident!("{}", crate::compile::DFIR_IDENT);
256
257 let source_ast: syn::File = match deploy_mode {
258 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
259 DeployMode::Containerized => {
260 syn::parse_quote! {
261 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
262 use #trybuild_crate_name_ident::__root as #orig_crate_name;
263 use #orig_crate_name::*;
264 use #orig_crate_name::__staged::__deps::*;
265 use #root::prelude::*;
266 use #root::runtime_support::dfir_rs as __root_dfir_rs;
267 pub use #orig_crate_name::__staged;
268
269 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
270 async fn main() {
271 #root::telemetry::initialize_tracing();
272
273 #( #extra_stmts )*
274
275 let mut #dfir_ident = #dfir_expr;
276
277 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
278 #(
279 let _ = local_set.spawn_local( #sidecars ); )*
281
282 let _ = local_set.run_until(#dfir_ident.run()).await;
283 }
284 }
285 }
286 #[cfg(feature = "deploy")]
287 DeployMode::HydroDeploy => {
288 syn::parse_quote! {
289 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
290 use #trybuild_crate_name_ident::__root as #orig_crate_name;
291 use #orig_crate_name::*;
292 use #orig_crate_name::__staged::__deps::*;
293 use #root::prelude::*;
294 use #root::runtime_support::dfir_rs as __root_dfir_rs;
295 pub use #orig_crate_name::__staged;
296
297 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
298 async fn main() {
299 let __hydro_lang_trybuild_cli_owned: #root::runtime_support::hydro_deploy_integration::DeployPorts<#root::__staged::deploy::deploy_runtime::HydroMeta> = #root::runtime_support::launch::init_no_ack_start().await;
300 let __hydro_lang_trybuild_cli = &__hydro_lang_trybuild_cli_owned;
301
302 #( #extra_stmts )*
303
304 let mut #dfir_ident = #dfir_expr;
305 println!("ack start");
306
307 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
311 #(
312 let _ = local_set.spawn_local( #sidecars ); )*
314
315 let _ = local_set.run_until(#root::runtime_support::launch::run_stdin_commands(
316 async move {
317 #dfir_ident.run().await
318 }
319 )).await;
320 }
321 }
322 }
323 #[cfg(feature = "maelstrom")]
324 DeployMode::Maelstrom => {
325 syn::parse_quote! {
326 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
327 use #trybuild_crate_name_ident::__root as #orig_crate_name;
328 use #orig_crate_name::*;
329 use #orig_crate_name::__staged::__deps::*;
330 use #root::prelude::*;
331 use #root::runtime_support::dfir_rs as __root_dfir_rs;
332 pub use #orig_crate_name::__staged;
333
334 #[allow(unused)]
335 fn __hydro_runtime<'a>(
336 __hydro_lang_maelstrom_meta: &'a #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::MaelstromMeta
337 )
338 -> #root::runtime_support::dfir_rs::scheduled::context::Dfir<impl #root::runtime_support::dfir_rs::scheduled::context::TickClosure + 'a>
339 {
340 #( #extra_stmts )*
341
342 #dfir_expr
343 }
344
345 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
346 async fn main() {
347 #root::telemetry::initialize_tracing();
348
349 let __hydro_lang_maelstrom_meta = #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::maelstrom_init();
351
352 let mut #dfir_ident = __hydro_runtime(&__hydro_lang_maelstrom_meta);
353
354 __hydro_lang_maelstrom_meta.start_receiving(); let local_set = #root::runtime_support::tokio::task::LocalSet::new();
357 #(
358 let _ = local_set.spawn_local( #sidecars ); )*
360
361 let _ = local_set.run_until(#dfir_ident.run()).await;
362 }
363 }
364 }
365 };
366 source_ast
367}
368
369#[cfg(any(feature = "sim", feature = "maelstrom"))]
372pub struct ExampleBuildConfig<'a> {
373 pub trybuild: TrybuildConfig,
375 pub bin_name: String,
379 pub runtime_feature: &'a str,
382 pub example_name: String,
385 pub crate_type: Option<&'a str>,
388 pub set_trybuild_lib_name: bool,
391 pub allow_fuzz: bool,
394}
395
396#[cfg(any(feature = "sim", feature = "maelstrom"))]
398fn rustc_target_libdir() -> Option<String> {
399 static LIBDIR: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
400 LIBDIR
401 .get_or_init(|| {
402 let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
403 std::process::Command::new(rustc)
404 .args(["--print", "target-libdir"])
405 .output()
406 .ok()
407 .filter(|out| out.status.success())
408 .map(|out| String::from_utf8(out.stdout).unwrap().trim().to_owned())
409 })
410 .clone()
411}
412
413#[cfg(any(feature = "sim", feature = "maelstrom"))]
421pub fn compile_trybuild_example(config: ExampleBuildConfig<'_>) -> Result<tempfile::TempPath, ()> {
422 use std::process::{Command, Stdio};
423
424 let ExampleBuildConfig {
425 trybuild,
426 bin_name,
427 runtime_feature,
428 example_name,
429 crate_type,
430 set_trybuild_lib_name,
431 allow_fuzz,
432 } = config;
433
434 let is_fuzz = allow_fuzz && std::env::var("BOLERO_FUZZER").is_ok();
435 let has_custom_rustflags = std::env::var("RUSTFLAGS").is_ok();
438
439 let crate_to_compile = if is_fuzz {
441 trybuild.project_dir.clone()
442 } else {
443 path!(trybuild.project_dir / "dylib-examples")
444 };
445
446 let (final_target_dir, _prebuild_guard, _cargo_lock) = if !has_custom_rustflags {
447 let prebuild_span =
448 tracing::debug_span!(target: "hydro_build", "prebuild", bin_name = %bin_name).entered();
449 let shared_debug = trybuild.target_dir.join("debug");
450 let jobs_dir = trybuild.target_dir.join("jobs");
451 let per_job = hydro_concurrent_cargo::setup_job_dir(&jobs_dir, &bin_name, &shared_debug);
452
453 let mut features: Vec<String> = trybuild.features.clone().unwrap_or_default();
454 features.push(runtime_feature.to_owned());
455
456 let staged_paths = vec![
457 path!(trybuild.project_dir / "src" / "__staged.rs"),
458 path!(trybuild.project_dir / "Cargo.lock"),
459 std::env::current_exe().unwrap(),
460 ];
461
462 let project_dir = trybuild.project_dir.clone();
463 let features_for_closure = features.clone();
464 let is_fuzz_for_closure = is_fuzz;
465
466 let (guard, cargo_lock) = hydro_concurrent_cargo::run_prebuild(
467 &trybuild.target_dir,
468 trybuild.project_dir.file_name().unwrap().to_str().unwrap(),
469 &features,
470 &staged_paths,
471 |prebuild_target| {
472 let features_str = features_for_closure.join(",");
473
474 let prebuild_crate = if is_fuzz_for_closure {
486 project_dir.clone()
487 } else {
488 path!(project_dir / "dylib-examples")
489 };
490 let mut lib_cmd = Command::new("cargo");
491 lib_cmd.current_dir(&prebuild_crate);
492 lib_cmd.args(["build", "--locked", "--lib"]);
493 lib_cmd.args(["--target-dir", prebuild_target.to_str().unwrap()]);
494 lib_cmd.arg("--no-default-features");
495 lib_cmd.args(["--features", &features_str]);
496 lib_cmd.args(["--config", "build.incremental = false"]);
497 lib_cmd.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
498 let status = lib_cmd.stdin(Stdio::null()).status().unwrap();
499 if !status.success() {
500 panic!("dep prebuild failed");
501 }
502 },
503 );
504
505 drop(prebuild_span);
509 (per_job, Some(guard), Some(cargo_lock))
510 } else {
511 (trybuild.target_dir.clone(), None, None)
512 };
513
514 let _job_build_guard = if !has_custom_rustflags {
516 let populate_span =
517 tracing::debug_span!(target: "hydro_build", "populate_job_dir", bin_name = %bin_name)
518 .entered();
519 let shared_debug = trybuild.target_dir.join("debug");
520 let guard = hydro_concurrent_cargo::populate_job_build_dir(
521 &final_target_dir.join("debug"),
522 &shared_debug,
523 );
524 drop(populate_span);
527 Some(guard)
528 } else {
529 None
530 };
531
532 let final_build_span =
533 tracing::debug_span!(target: "hydro_build", "final_build", bin_name = %bin_name).entered();
534 let mut command = Command::new("cargo");
535 command.current_dir(&crate_to_compile);
536 command.args([
537 "rustc",
538 if has_custom_rustflags {
539 "--locked"
540 } else {
541 "--frozen"
542 },
543 ]);
544 command.args(["--example", &example_name]);
545 command.args(["--target-dir", final_target_dir.to_str().unwrap()]);
546 command.arg("--no-default-features");
553 command.args([
554 "--features",
555 &trybuild
556 .features
557 .clone()
558 .into_iter()
559 .flatten()
560 .chain([runtime_feature.to_owned()])
561 .collect::<Vec<_>>()
562 .join(","),
563 ]);
564 command.args(["--config", "build.incremental = false"]);
565 if let Some(crate_type) = crate_type {
566 command.args(["--crate-type", crate_type]);
567 }
568 command.arg("--message-format=json-diagnostic-rendered-ansi");
569 command.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
570 if set_trybuild_lib_name {
571 command.env("TRYBUILD_LIB_NAME", &bin_name);
572 }
573
574 command.arg("--");
575
576 if cfg!(any(target_os = "linux", target_os = "macos")) {
577 let debug_path = if let Ok(target) = std::env::var("CARGO_BUILD_TARGET") {
578 path!(final_target_dir / target / "debug")
579 } else {
580 path!(final_target_dir / "debug")
581 };
582
583 let mut rpaths = vec![debug_path.clone(), path!(debug_path / "deps")];
587 if let Some(libdir) = rustc_target_libdir() {
588 rpaths.push(PathBuf::from(libdir));
589 }
590 for rpath in rpaths {
591 if cfg!(target_os = "macos") {
592 command.args([
596 "-Clink-arg=-rpath".to_owned(),
597 format!("-Clink-arg={}", rpath.to_str().unwrap()),
598 ]);
599 } else {
600 command.args([format!("-Clink-arg=-Wl,-rpath,{}", rpath.to_str().unwrap())]);
601 }
602 }
603
604 if cfg!(all(target_os = "linux", target_env = "gnu")) {
605 command.arg(
606 "-Clink-args=-Wl,-z,nodelete",
608 );
609 }
610 }
611
612 if allow_fuzz && let Ok(fuzzer) = std::env::var("BOLERO_FUZZER") {
613 command.env_remove("BOLERO_FUZZER");
614
615 if fuzzer == "libfuzzer" {
616 #[cfg(target_os = "macos")]
617 {
618 command.args(["-Clink-arg=-undefined", "-Clink-arg=dynamic_lookup"]);
619 }
620
621 #[cfg(target_os = "linux")]
622 {
623 command.args(["-Clink-arg=-Wl,--unresolved-symbols=ignore-all"]);
624 }
625 }
626 }
627
628 tracing::debug!(
629 target: "hydro_build",
630 "final build command (cwd={}): {:?}",
631 crate_to_compile.display(),
632 command
633 );
634
635 let mut spawned = command
636 .stdout(Stdio::piped())
637 .stderr(Stdio::piped())
638 .stdin(Stdio::null())
639 .spawn()
640 .unwrap();
641 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
642 let stderr_handle = spawned.stderr.take().unwrap();
643 let stderr_thread = std::thread::spawn(move || {
644 use std::io::Read;
645 let mut buf = String::new();
646 std::io::BufReader::new(stderr_handle)
647 .read_to_string(&mut buf)
648 .unwrap();
649 buf
650 });
651
652 let mut out = Err(());
653 for message in cargo_metadata::Message::parse_stream(reader) {
654 match message.unwrap() {
655 cargo_metadata::Message::CompilerArtifact(artifact) => {
656 let is_output = artifact.target.is_example();
658
659 if is_output {
660 let path = artifact.filenames.first().unwrap();
661 let path_buf: PathBuf = path.clone().into();
662 out = Ok(path_buf);
663 }
664 }
665 cargo_metadata::Message::CompilerMessage(mut msg) => {
666 if let Some(rendered) = msg.message.rendered.as_mut() {
669 let file_names = msg
670 .message
671 .spans
672 .iter()
673 .map(|s| &s.file_name)
674 .collect::<std::collections::BTreeSet<_>>();
675 for file_name in file_names {
676 *rendered = rendered.replace(
677 file_name,
678 &format!("(full path) {}/{file_name}", trybuild.project_dir.display()),
679 )
680 }
681 }
682 eprintln!("{}", msg.message);
683 }
684 cargo_metadata::Message::TextLine(line) => {
685 eprintln!("{}", line);
686 }
687 cargo_metadata::Message::BuildFinished(_) => {}
688 cargo_metadata::Message::BuildScriptExecuted(_) => {}
689 msg => panic!("Unexpected message type: {:?}", msg),
690 }
691 }
692
693 spawned.wait().unwrap();
694 let stderr_output = stderr_thread.join().unwrap();
695 drop(final_build_span);
696
697 if !has_custom_rustflags {
700 for line in stderr_output.lines() {
701 if line.contains("Compiling") && !line.contains("dylib-examples") {
702 panic!(
703 "unexpected recompilation in final build: {line}\nfull stderr:\n{stderr_output}"
704 );
705 }
706 }
707 }
708
709 if out.is_err() {
710 panic!("final build failed to produce binary.\nstderr:\n{stderr_output}");
711 }
712
713 let out_file = tempfile::NamedTempFile::new().unwrap().into_temp_path();
714 fs::copy(out.as_ref().unwrap(), &out_file).unwrap();
715 Ok(out_file)
716}
717
718pub fn create_trybuild()
719-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
720 let Metadata {
721 target_directory: target_dir,
722 workspace_root: workspace,
723 packages,
724 } = cargo::metadata()?;
725
726 let source_dir = cargo::manifest_dir()?;
727 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
728
729 let mut dev_dependency_features = vec![];
730 source_manifest.dev_dependencies.retain(|k, v| {
731 if source_manifest.dependencies.contains_key(k) {
732 for feat in &v.features {
734 dev_dependency_features.push(format!("{}/{}", k, feat));
735 }
736
737 false
738 } else {
739 dev_dependency_features.push(format!("dep:{k}"));
741
742 v.optional = true;
743 true
744 }
745 });
746
747 let mut features = if std::env::var("RUNNING_AS_EXAMPLE_TEST").is_ok_and(|v| v == "1") {
753 None
754 } else {
755 features::find()
756 };
757
758 let path_dependencies = source_manifest
759 .dependencies
760 .iter()
761 .filter_map(|(name, dep)| {
762 let path = dep.path.as_ref()?;
763 if packages.iter().any(|p| &p.name == name) {
764 None
766 } else {
767 Some(PathDependency {
768 name: name.clone(),
769 normalized_path: path.canonicalize().ok()?,
770 })
771 }
772 })
773 .collect();
774
775 let crate_name = source_manifest.package.name.clone();
776 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
777 fs::create_dir_all(&project_dir)?;
778
779 let project_name = format!("{}-hydro-trybuild", crate_name);
780 let mut manifest = Runner::make_manifest(
781 &workspace,
782 &project_name,
783 &source_dir,
784 &packages,
785 &[],
786 source_manifest,
787 )?;
788
789 if let Some(enabled_features) = &mut features {
790 enabled_features
791 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
792 }
793
794 for runtime_feature in HYDRO_RUNTIME_FEATURES {
795 manifest.features.insert(
796 format!("hydro___feature_{runtime_feature}"),
797 vec![format!("hydro_lang/{runtime_feature}")],
798 );
799 }
800
801 manifest
802 .dependencies
803 .get_mut("hydro_lang")
804 .unwrap()
805 .features
806 .push("runtime_support".to_owned());
807
808 manifest
809 .features
810 .insert("hydro___test".to_owned(), dev_dependency_features);
811
812 if manifest
813 .workspace
814 .as_ref()
815 .is_some_and(|w| w.dependencies.is_empty())
816 {
817 manifest.workspace = None;
818 }
819
820 let project = Project {
821 dir: project_dir,
822 source_dir,
823 target_dir,
824 name: project_name.clone(),
825 update: Update::env()?,
826 has_pass: false,
827 has_compile_fail: false,
828 features,
829 workspace,
830 path_dependencies,
831 manifest,
832 keep_going: false,
833 };
834
835 {
836 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
837
838 let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
839 project_lock.lock()?;
840
841 fs::create_dir_all(path!(project.dir / "src"))?;
842 fs::create_dir_all(path!(project.dir / "examples"))?;
843
844 let crate_name_ident = syn::Ident::new(
845 &crate_name.replace("-", "_"),
846 proc_macro2::Span::call_site(),
847 );
848
849 write_atomic(
850 prettyplease::unparse(&syn::parse_quote! {
851 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
852
853 pub mod __root {
854 pub use #crate_name_ident::*;
855 #[cfg(feature = "hydro___test")]
856 pub use super::__staged;
857 }
858
859 #[cfg(feature = "hydro___test")]
860 pub mod __staged;
861 })
862 .as_bytes(),
863 &path!(project.dir / "src" / "lib.rs"),
864 )
865 .unwrap();
866
867 let base_manifest = toml::to_string(&project.manifest)?;
868
869 let feature_names: Vec<_> = project.manifest.features.keys().cloned().collect();
871
872 let dylib_dir = path!(project.dir / "dylib");
874 fs::create_dir_all(path!(dylib_dir / "src"))?;
875
876 let trybuild_crate_name_ident = syn::Ident::new(
877 &project_name.replace("-", "_"),
878 proc_macro2::Span::call_site(),
879 );
880 write_atomic(
881 [
887 "// v2: dylib must be built as a dependency (prefer-dynamic).\n".as_bytes(),
888 prettyplease::unparse(&syn::parse_quote! {
889 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
890 pub use #trybuild_crate_name_ident::*;
891 })
892 .as_bytes(),
893 ]
894 .concat()
895 .as_slice(),
896 &path!(dylib_dir / "src" / "lib.rs"),
897 )?;
898
899 let serialized_edition = toml::to_string(
900 &vec![("edition", &project.manifest.package.edition)]
901 .into_iter()
902 .collect::<std::collections::HashMap<_, _>>(),
903 )
904 .unwrap();
905
906 let dylib_features_section = feature_names
909 .iter()
910 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
911 .collect::<Vec<_>>()
912 .join("\n");
913
914 let dylib_manifest = format!(
915 r#"[package]
916name = "{project_name}-dylib"
917version = "0.0.0"
918{}
919
920[lib]
921crate-type = ["{}"]
922
923[dependencies]
924{project_name} = {{ path = "..", default-features = false }}
925
926[features]
927{dylib_features_section}
928"#,
929 serialized_edition,
930 if cfg!(target_os = "windows") {
931 "rlib"
932 } else {
933 "dylib"
934 }
935 );
936 write_atomic(dylib_manifest.as_ref(), &path!(dylib_dir / "Cargo.toml"))?;
937
938 let dylib_examples_dir = path!(project.dir / "dylib-examples");
939 fs::create_dir_all(path!(dylib_examples_dir / "src"))?;
940 fs::create_dir_all(path!(dylib_examples_dir / "examples"))?;
941
942 write_atomic(
943 b"#![allow(unused_crate_dependencies)]\n",
944 &path!(dylib_examples_dir / "src" / "lib.rs"),
945 )?;
946
947 let features_section = feature_names
949 .iter()
950 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
951 .collect::<Vec<_>>()
952 .join("\n");
953
954 let dylib_examples_manifest = format!(
966 r#"[package]
967name = "{project_name}-dylib-examples"
968version = "0.0.0"
969{}
970
971[dependencies]
972{project_name} = {{ package = "{project_name}-dylib", path = "../dylib", default-features = false }}
973
974[features]
975{features_section}
976
977[[example]]
978name = "sim-dylib"
979crate-type = ["cdylib"]
980"#,
981 serialized_edition
982 );
983 write_atomic(
984 dylib_examples_manifest.as_ref(),
985 &path!(dylib_examples_dir / "Cargo.toml"),
986 )?;
987
988 let sim_dylib_contents = prettyplease::unparse(&syn::parse_quote! {
990 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
991 include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
992 });
993 write_atomic(
994 sim_dylib_contents.as_bytes(),
995 &path!(project.dir / "examples" / "sim-dylib.rs"),
996 )?;
997 write_atomic(
998 sim_dylib_contents.as_bytes(),
999 &path!(dylib_examples_dir / "examples" / "sim-dylib.rs"),
1000 )?;
1001
1002 let workspace_manifest = format!(
1003 r#"{}
1004[[example]]
1005name = "sim-dylib"
1006crate-type = ["cdylib"]
1007
1008[workspace]
1009members = ["dylib", "dylib-examples"]
1010"#,
1011 base_manifest,
1012 );
1013
1014 write_atomic(
1015 workspace_manifest.as_ref(),
1016 &path!(project.dir / "Cargo.toml"),
1017 )?;
1018
1019 let manifest_hash = {
1022 let mut hasher = Sha256::new();
1023 hasher.update(&workspace_manifest);
1024 hasher.update(&dylib_manifest);
1025 hasher.update(&dylib_examples_manifest);
1026 format!("{:X}", hasher.finalize())
1027 .chars()
1028 .take(8)
1029 .collect::<String>()
1030 };
1031
1032 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
1033 let workspace_cargo_lock_contents_and_hash = if workspace_cargo_lock.exists() {
1034 let cargo_lock_contents = fs::read_to_string(&workspace_cargo_lock)?;
1035
1036 let hash = format!("{:X}", Sha256::digest(&cargo_lock_contents))
1037 .chars()
1038 .take(8)
1039 .collect::<String>();
1040
1041 Some((cargo_lock_contents, hash))
1042 } else {
1043 None
1044 };
1045
1046 let trybuild_hash = format!(
1047 "{}-{}",
1048 manifest_hash,
1049 workspace_cargo_lock_contents_and_hash
1050 .as_ref()
1051 .map(|(_contents, hash)| &**hash)
1052 .unwrap_or_default()
1053 );
1054
1055 if !check_contents(
1056 trybuild_hash.as_bytes(),
1057 &path!(project.dir / ".hydro-trybuild-manifest"),
1058 )
1059 .is_ok_and(|b| b)
1060 {
1061 if let Some((cargo_lock_contents, _)) = workspace_cargo_lock_contents_and_hash {
1063 write_atomic(
1066 cargo_lock_contents.as_ref(),
1067 &path!(project.dir / "Cargo.lock"),
1068 )?;
1069 } else {
1070 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
1071 }
1072
1073 std::process::Command::new("cargo")
1075 .current_dir(&project.dir)
1076 .args(["update", "-w"]) .stdout(std::process::Stdio::null())
1078 .stderr(std::process::Stdio::null())
1079 .status()
1080 .unwrap();
1081
1082 write_atomic(
1083 trybuild_hash.as_bytes(),
1084 &path!(project.dir / ".hydro-trybuild-manifest"),
1085 )?;
1086 }
1087
1088 let examples_folder = path!(project.dir / "examples");
1090 fs::create_dir_all(&examples_folder)?;
1091
1092 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
1093 if workspace_dot_cargo_config_toml.exists() {
1094 let dot_cargo_folder = path!(project.dir / ".cargo");
1095 fs::create_dir_all(&dot_cargo_folder)?;
1096
1097 write_atomic(
1098 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
1099 &path!(dot_cargo_folder / "config.toml"),
1100 )?;
1101 }
1102
1103 let vscode_folder = path!(project.dir / ".vscode");
1104 fs::create_dir_all(&vscode_folder)?;
1105 write_atomic(
1106 include_bytes!("./vscode-trybuild.json"),
1107 &path!(vscode_folder / "settings.json"),
1108 )?;
1109 }
1110
1111 Ok((
1112 project.dir.as_ref().into(),
1113 project.target_dir.as_ref().into(),
1114 project.features,
1115 ))
1116}
1117
1118fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
1119 let mut file = File::options()
1120 .read(true)
1121 .write(false)
1122 .create(false)
1123 .truncate(false)
1124 .open(path)?;
1125 file.lock()?;
1126
1127 let mut existing_contents = Vec::new();
1128 file.read_to_end(&mut existing_contents)?;
1129 Ok(existing_contents == contents)
1130}
1131
1132pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
1133 let mut file = File::options()
1134 .read(true)
1135 .write(true)
1136 .create(true)
1137 .truncate(false)
1138 .open(path)?;
1139
1140 let mut existing_contents = Vec::new();
1141 file.read_to_end(&mut existing_contents)?;
1142 if existing_contents != contents {
1143 file.lock()?;
1144 file.seek(SeekFrom::Start(0))?;
1145 file.set_len(0)?;
1146 file.write_all(contents)?;
1147 }
1148
1149 Ok(())
1150}