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::{AsCodeOptions, 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::normalize::Normalization;
15use trybuild_internals_api::run::{PathDependency, Project};
16use trybuild_internals_api::{Runner, dependencies, features, path};
17
18pub const HYDRO_RUNTIME_FEATURES: &[&str] = &[
19 "deploy_integration",
20 "runtime_measure",
21 "docker_runtime",
22 "ecs_runtime",
23 "maelstrom_runtime",
24 "sim_runtime",
25];
26
27#[cfg(any(feature = "deploy", feature = "maelstrom"))]
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum LinkingMode {
33 #[cfg_attr(
36 not(feature = "deploy"),
37 expect(
38 dead_code,
39 reason = "only constructed by the deploy backends; Maelstrom-only builds use Dynamic"
40 )
41 )]
42 Static,
43 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
44 Dynamic,
45}
46
47#[cfg(any(feature = "deploy", feature = "maelstrom"))]
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum DeployMode {
51 #[cfg(feature = "deploy")]
52 HydroDeploy,
54 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
55 Containerized,
57 #[cfg(feature = "maelstrom")]
58 Maelstrom,
60}
61
62pub(crate) static IS_TEST: std::sync::atomic::AtomicBool =
63 std::sync::atomic::AtomicBool::new(false);
64
65pub(crate) static CONCURRENT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
66
67pub fn init_test() {
81 IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
82}
83
84#[cfg(any(feature = "deploy", feature = "maelstrom"))]
85fn clean_bin_name_prefix(bin_name_prefix: &str) -> String {
86 bin_name_prefix
87 .replace("::", "__")
88 .replace([' ', ',', '<', '{', '}'], "_")
89 .replace(['>', '(', ')'], "")
90}
91
92#[derive(Debug, Clone)]
93pub struct TrybuildConfig {
94 pub project_dir: PathBuf,
95 pub target_dir: PathBuf,
96 pub dylib_package_name: String,
99 pub features: Option<Vec<String>>,
100 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
101 #[cfg_attr(
104 not(feature = "deploy"),
105 expect(dead_code, reason = "only read by the deploy backends")
106 )]
107 pub linking_mode: LinkingMode,
111}
112
113#[cfg(any(feature = "deploy", feature = "maelstrom"))]
114pub fn create_graph_trybuild(
115 graph: DfirGraph,
116 extra_stmts: &[syn::Stmt],
117 sidecars: &[syn::Expr],
118 as_code_options: &AsCodeOptions,
119 bin_name_prefix: Option<&str>,
120 deploy_mode: DeployMode,
121 linking_mode: LinkingMode,
122) -> (String, TrybuildConfig) {
123 let source_dir = cargo::manifest_dir().unwrap();
124 let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
125 let crate_name = source_manifest.package.name.replace('-', "_");
126
127 let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
128
129 let generated_code = {
130 let _span = tracing::debug_span!(target: "hydro_build", "graph_codegen").entered();
131 compile_graph_trybuild(
132 graph,
133 extra_stmts,
134 sidecars,
135 as_code_options,
136 &crate_name,
137 deploy_mode,
138 )
139 };
140
141 let source = {
142 let _span = tracing::debug_span!(target: "hydro_build", "unparse_source").entered();
143 prettyplease::unparse(&generated_code)
144 };
145
146 let hash = format!("{:X}", Sha256::digest(&source))
147 .chars()
148 .take(8)
149 .collect::<String>();
150
151 let bin_name = if let Some(bin_name_prefix) = bin_name_prefix {
152 format!("{}_{}", clean_bin_name_prefix(bin_name_prefix), hash)
153 } else {
154 hash
155 };
156
157 let (project_dir, target_dir, dylib_package_name, mut cur_bin_enabled_features) =
158 create_trybuild().unwrap();
159
160 let examples_dir = match linking_mode {
162 LinkingMode::Static => path!(project_dir / "examples"),
163 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
164 LinkingMode::Dynamic => path!(project_dir / "dylib-examples" / "examples"),
165 };
166
167 fs::create_dir_all(&examples_dir).unwrap();
169
170 let out_path = path!(examples_dir / format!("{bin_name}.rs"));
171 {
172 let _span =
173 tracing::debug_span!(target: "hydro_build", "write_generated_sources").entered();
174 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
175 write_atomic(source.as_ref(), &out_path).unwrap();
176 }
177
178 if is_test {
179 write_staged_source_cached(source_dir.as_ref(), &crate_name, &project_dir);
180 }
181
182 if is_test {
183 if cur_bin_enabled_features.is_none() {
184 cur_bin_enabled_features = Some(vec![]);
185 }
186
187 cur_bin_enabled_features
188 .as_mut()
189 .unwrap()
190 .push("hydro___test".to_owned());
191 }
192
193 (
194 bin_name,
195 TrybuildConfig {
196 project_dir,
197 target_dir,
198 dylib_package_name,
199 features: cur_bin_enabled_features,
200 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
201 linking_mode,
202 },
203 )
204}
205
206#[cfg(any(feature = "deploy", feature = "maelstrom"))]
207pub fn compile_graph_trybuild(
208 partitioned_graph: DfirGraph,
209 extra_stmts: &[syn::Stmt],
210 sidecars: &[syn::Expr],
211 as_code_options: &AsCodeOptions,
212 crate_name: &str,
213 deploy_mode: DeployMode,
214) -> syn::File {
215 use crate::staging_util::get_this_crate;
216
217 let mut diagnostics = Diagnostics::new();
218 let dfir_expr: syn::Expr = syn::parse2(
219 partitioned_graph
220 .as_code_with_options(
221 "e! { __root_dfir_rs },
222 as_code_options,
223 quote!(),
224 &mut diagnostics,
225 )
226 .expect("DFIR code generation failed with diagnostics."),
227 )
228 .unwrap();
229
230 let orig_crate_name = quote::format_ident!("{}", crate_name);
231 let trybuild_crate_name_ident = quote::format_ident!("{}_hydro_trybuild", crate_name);
232 let root = get_this_crate();
233 let tokio_main_ident = format!("{}::runtime_support::tokio", root);
234 let dfir_ident = quote::format_ident!("{}", crate::compile::DFIR_IDENT);
235
236 let source_ast: syn::File = match deploy_mode {
237 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
238 DeployMode::Containerized => {
239 syn::parse_quote! {
240 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
241 use #trybuild_crate_name_ident::__root as #orig_crate_name;
242 use #orig_crate_name::*;
243 use #orig_crate_name::__staged::__deps::*;
244 use #root::prelude::*;
245 use #root::runtime_support::dfir_rs as __root_dfir_rs;
246 pub use #orig_crate_name::__staged;
247
248 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
249 async fn main() {
250 #root::telemetry::initialize_tracing();
251
252 #( #extra_stmts )*
253
254 let mut #dfir_ident = #dfir_expr;
255
256 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
257 #(
258 let _ = local_set.spawn_local( #sidecars ); )*
260
261 let _ = local_set.run_until(#dfir_ident.run()).await;
262 }
263 }
264 }
265 #[cfg(feature = "deploy")]
266 DeployMode::HydroDeploy => {
267 syn::parse_quote! {
268 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
269 use #trybuild_crate_name_ident::__root as #orig_crate_name;
270 use #orig_crate_name::*;
271 use #orig_crate_name::__staged::__deps::*;
272 use #root::prelude::*;
273 use #root::runtime_support::dfir_rs as __root_dfir_rs;
274 pub use #orig_crate_name::__staged;
275
276 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
277 async fn main() {
278 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;
279 let __hydro_lang_trybuild_cli = &__hydro_lang_trybuild_cli_owned;
280
281 #( #extra_stmts )*
282
283 let mut #dfir_ident = #dfir_expr;
284 println!("ack start");
285
286 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
290 #(
291 let _ = local_set.spawn_local( #sidecars ); )*
293
294 let _ = local_set.run_until(#root::runtime_support::launch::run_stdin_commands(
295 async move {
296 #dfir_ident.run().await
297 }
298 )).await;
299 }
300 }
301 }
302 #[cfg(feature = "maelstrom")]
303 DeployMode::Maelstrom => {
304 let dfir_metrics_ty = crate::compile::dfir_metrics_state_type(as_code_options, &root);
307 syn::parse_quote! {
308 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
309 use #trybuild_crate_name_ident::__root as #orig_crate_name;
310 use #orig_crate_name::*;
311 use #orig_crate_name::__staged::__deps::*;
312 use #root::prelude::*;
313 use #root::runtime_support::dfir_rs as __root_dfir_rs;
314 pub use #orig_crate_name::__staged;
315
316 #[allow(unused)]
317 fn __hydro_runtime<'a>(
318 __hydro_lang_maelstrom_meta: &'a #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::MaelstromMeta
319 )
320 -> #root::runtime_support::dfir_rs::scheduled::context::Dfir<impl #root::runtime_support::dfir_rs::scheduled::context::TickClosure<Metrics = #dfir_metrics_ty> + 'a>
321 {
322 #( #extra_stmts )*
323
324 #dfir_expr
325 }
326
327 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
328 async fn main() {
329 #root::telemetry::initialize_tracing();
330
331 let __hydro_lang_maelstrom_meta = #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::maelstrom_init();
333
334 let mut #dfir_ident = __hydro_runtime(&__hydro_lang_maelstrom_meta);
335
336 __hydro_lang_maelstrom_meta.start_receiving(); let local_set = #root::runtime_support::tokio::task::LocalSet::new();
339 #(
340 let _ = local_set.spawn_local( #sidecars ); )*
342
343 let _ = local_set.run_until(#dfir_ident.run()).await;
344 }
345 }
346 }
347 };
348 source_ast
349}
350
351#[cfg(any(feature = "sim", feature = "maelstrom"))]
354pub struct ExampleBuildConfig<'a> {
355 pub trybuild: TrybuildConfig,
357 pub bin_name: String,
361 pub runtime_feature: &'a str,
364 pub example_name: String,
367 pub crate_type: Option<&'a str>,
370 pub set_trybuild_lib_name: bool,
373 pub allow_fuzz: bool,
376}
377
378#[cfg(any(feature = "sim", feature = "maelstrom"))]
380fn rustc_target_libdir() -> Option<String> {
381 static LIBDIR: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
382 LIBDIR
383 .get_or_init(|| {
384 let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
385 std::process::Command::new(rustc)
386 .args(["--print", "target-libdir"])
387 .output()
388 .ok()
389 .filter(|out| out.status.success())
390 .map(|out| String::from_utf8(out.stdout).unwrap().trim().to_owned())
391 })
392 .clone()
393}
394
395const ENCODED_RUSTFLAGS: &str = env!("HYDRO_ENCODED_RUSTFLAGS");
405
406#[cfg(any(feature = "sim", feature = "maelstrom"))]
425fn forward_rustflags(command: &mut std::process::Command) {
426 let flags = toml::Value::try_from(
427 ENCODED_RUSTFLAGS
428 .split('\x1f')
429 .filter(|flag| !flag.is_empty())
430 .collect::<Vec<_>>(),
431 )
432 .unwrap();
433 command.args(["--config", &format!("build.rustflags={flags}")]);
434}
435
436#[cfg(any(feature = "sim", feature = "maelstrom"))]
449pub fn compile_trybuild_example(config: ExampleBuildConfig<'_>) -> Result<PathBuf, ()> {
450 use std::process::{Command, Stdio};
451
452 let ExampleBuildConfig {
453 trybuild,
454 bin_name,
455 runtime_feature,
456 example_name,
457 crate_type,
458 set_trybuild_lib_name,
459 allow_fuzz,
460 } = config;
461
462 let is_fuzz = allow_fuzz && std::env::var("BOLERO_FUZZER").is_ok();
463
464 let use_prebuild = !is_fuzz && std::env::var_os("CARGO_BUILD_TARGET").is_none();
470
471 let crate_to_compile = if is_fuzz {
473 trybuild.project_dir.clone()
474 } else {
475 path!(trybuild.project_dir / "dylib-examples")
476 };
477
478 let (final_target_dir, _prebuild_guard, _cargo_lock) = if use_prebuild {
479 let prebuild_span =
480 tracing::debug_span!(target: "hydro_build", "prebuild", bin_name = %bin_name).entered();
481 let shared_debug = trybuild.target_dir.join("debug");
482 let jobs_dir = trybuild.target_dir.join("jobs");
483 let per_job = hydro_concurrent_cargo::setup_job_dir(&jobs_dir, &bin_name, &shared_debug);
484
485 let mut features: Vec<String> = trybuild.features.clone().unwrap_or_default();
486 features.push(runtime_feature.to_owned());
487
488 let staged_paths = vec![
489 path!(trybuild.project_dir / "src" / "__staged.rs"),
490 path!(trybuild.project_dir / "Cargo.lock"),
491 std::env::current_exe().unwrap(),
492 ];
493
494 let features_for_closure = features.clone();
495
496 let (guard, cargo_lock) = hydro_concurrent_cargo::run_prebuild(
501 &trybuild.target_dir,
502 trybuild.project_dir.file_name().unwrap().to_str().unwrap(),
503 &features,
504 ENCODED_RUSTFLAGS,
505 &staged_paths,
506 |prebuild_target| {
507 hydro_concurrent_cargo::set_dylib_lib_name(
508 &trybuild.project_dir,
509 ENCODED_RUSTFLAGS,
510 );
511 let features_str = features_for_closure.join(",");
512
513 let mut lib_cmd = Command::new("cargo");
522 lib_cmd.current_dir(&crate_to_compile);
523 lib_cmd.args(["build", "--locked", "--lib"]);
524 lib_cmd.args(["--target-dir", prebuild_target.to_str().unwrap()]);
525 lib_cmd.arg("--no-default-features");
526 lib_cmd.args(["--features", &features_str]);
527 lib_cmd.args(["--config", "build.incremental = false"]);
528 lib_cmd.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
529 forward_rustflags(&mut lib_cmd);
530 let status = lib_cmd.stdin(Stdio::null()).status().unwrap();
531 assert!(status.success(), "dep prebuild failed")
532 },
533 );
534
535 drop(prebuild_span);
539 (per_job, Some(guard), Some(cargo_lock))
540 } else {
541 (trybuild.target_dir.clone(), None, None)
542 };
543
544 let _job_build_guard = if use_prebuild {
546 let populate_span =
547 tracing::debug_span!(target: "hydro_build", "populate_job_dir", bin_name = %bin_name)
548 .entered();
549 let shared_debug = trybuild.target_dir.join("debug");
550 let guard = hydro_concurrent_cargo::populate_job_build_dir(
551 &final_target_dir.join("debug"),
552 &shared_debug,
553 );
554 drop(populate_span);
557 Some(guard)
558 } else {
559 None
560 };
561
562 let final_build_span =
563 tracing::debug_span!(target: "hydro_build", "final_build", bin_name = %bin_name).entered();
564 let mut command = Command::new("cargo");
565 command.current_dir(&crate_to_compile);
566 command.args(["rustc", if use_prebuild { "--frozen" } else { "--locked" }]);
567 command.args(["--example", &example_name]);
568 command.args(["--target-dir", final_target_dir.to_str().unwrap()]);
569 command.arg("--no-default-features");
576 command.args([
577 "--features",
578 &trybuild
579 .features
580 .clone()
581 .into_iter()
582 .flatten()
583 .chain([runtime_feature.to_owned()])
584 .collect::<Vec<_>>()
585 .join(","),
586 ]);
587 command.args(["--config", "build.incremental = false"]);
588 forward_rustflags(&mut command);
589 if let Some(crate_type) = crate_type {
590 command.args(["--crate-type", crate_type]);
591 }
592 command.arg("--message-format=json-diagnostic-rendered-ansi");
593 command.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
594 if set_trybuild_lib_name {
595 command.env("TRYBUILD_LIB_NAME", &bin_name);
596 }
597
598 command.arg("--");
599
600 command.arg(format!("-Cmetadata={bin_name}"));
609
610 if cfg!(any(target_os = "linux", target_os = "macos")) {
611 let (debug_path, shared_debug_path) =
612 if let Ok(target) = std::env::var("CARGO_BUILD_TARGET") {
613 (
614 path!(final_target_dir / target / "debug"),
615 path!(trybuild.target_dir / target / "debug"),
616 )
617 } else {
618 (
619 path!(final_target_dir / "debug"),
620 path!(trybuild.target_dir / "debug"),
621 )
622 };
623
624 let mut rpaths = hydro_concurrent_cargo::dylib_search_dirs(
629 &trybuild.dylib_package_name,
630 &debug_path,
631 &shared_debug_path,
632 );
633 if let Some(libdir) = rustc_target_libdir() {
634 rpaths.push(PathBuf::from(libdir));
635 }
636 for rpath in rpaths {
637 if cfg!(target_os = "macos") {
638 command.args([
642 "-Clink-arg=-rpath".to_owned(),
643 format!("-Clink-arg={}", rpath.to_str().unwrap()),
644 ]);
645 } else {
646 command.args([format!("-Clink-arg=-Wl,-rpath,{}", rpath.to_str().unwrap())]);
647 }
648 }
649
650 if cfg!(all(target_os = "linux", target_env = "gnu")) {
651 command.arg(
652 "-Clink-args=-Wl,-z,nodelete",
654 );
655 }
656 }
657
658 if allow_fuzz && let Ok(fuzzer) = std::env::var("BOLERO_FUZZER") {
659 command.env_remove("BOLERO_FUZZER");
660
661 if fuzzer == "libfuzzer" {
662 #[cfg(target_os = "macos")]
663 {
664 command.args(["-Clink-arg=-undefined", "-Clink-arg=dynamic_lookup"]);
665 }
666
667 #[cfg(target_os = "linux")]
668 {
669 command.args(["-Clink-arg=-Wl,--unresolved-symbols=ignore-all"]);
670 }
671 }
672 }
673
674 tracing::debug!(
675 target: "hydro_build",
676 "final build command (cwd={}): {:?}",
677 crate_to_compile.display(),
678 command
679 );
680
681 let mut spawned = command
682 .stdout(Stdio::piped())
683 .stderr(Stdio::piped())
684 .stdin(Stdio::null())
685 .spawn()
686 .unwrap();
687 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
688 let stderr_handle = spawned.stderr.take().unwrap();
689 let stderr_thread = std::thread::spawn(move || {
690 use std::io::Read;
691 let mut buf = String::new();
692 std::io::BufReader::new(stderr_handle)
693 .read_to_string(&mut buf)
694 .unwrap();
695 buf
696 });
697
698 let mut out = Err(());
699 for message in cargo_metadata::Message::parse_stream(reader) {
700 match message.unwrap() {
701 cargo_metadata::Message::CompilerArtifact(artifact) => {
702 let is_output = artifact.target.is_example();
704
705 if is_output {
706 let path = artifact.filenames.first().unwrap();
707 let path_buf: PathBuf = path.clone().into();
708 out = Ok(path_buf);
709 }
710 }
711 cargo_metadata::Message::CompilerMessage(mut msg) => {
712 if let Some(rendered) = msg.message.rendered.as_mut() {
715 let file_names = msg
716 .message
717 .spans
718 .iter()
719 .map(|s| &s.file_name)
720 .collect::<std::collections::BTreeSet<_>>();
721 for file_name in file_names {
722 *rendered = rendered.replace(
723 file_name,
724 &format!("(full path) {}/{file_name}", trybuild.project_dir.display()),
725 )
726 }
727 }
728 eprintln!("{}", msg.message);
729 }
730 cargo_metadata::Message::TextLine(line) => {
731 eprintln!("{}", line);
732 }
733 cargo_metadata::Message::BuildFinished(_) => {}
734 cargo_metadata::Message::BuildScriptExecuted(_) => {}
735 msg => panic!("Unexpected message type: {:?}", msg),
736 }
737 }
738
739 spawned.wait().unwrap();
740 let stderr_output = stderr_thread.join().unwrap();
741 drop(final_build_span);
742
743 if use_prebuild {
746 for line in stderr_output.lines() {
747 if line.contains("Compiling") {
748 assert!(
749 line.contains("dylib-examples"),
750 "unexpected recompilation in final build: {line}\nfull stderr:\n{stderr_output}"
751 );
752 }
753 }
754 }
755
756 assert!(
757 out.is_ok(),
758 "final build failed to produce binary.\nstderr:\n{stderr_output}"
759 );
760
761 let out = out.unwrap();
772 let coverage_dir = path!(trybuild.target_dir / "debug" / "deps" / "hydro-coverage");
773 fs::create_dir_all(&coverage_dir).unwrap();
774 let artifact_name = out.file_name().unwrap().to_str().unwrap();
775 let fingerprint = hydro_concurrent_cargo::toolchain_fingerprint(ENCODED_RUSTFLAGS);
776 let persisted = path!(coverage_dir / format!("{bin_name}-{fingerprint}-{artifact_name}"));
777
778 if same_file::is_same_file(&out, &persisted).unwrap_or(false) {
784 return Ok(persisted);
785 }
786
787 let staging = path!(
790 coverage_dir
791 / format!(
792 ".{bin_name}.{}.{}.tmp",
793 std::process::id(),
794 std::time::SystemTime::now()
795 .duration_since(std::time::UNIX_EPOCH)
796 .unwrap()
797 .as_nanos()
798 )
799 );
800 if fs::hard_link(&out, &staging).is_err() {
801 fs::copy(&out, &staging).unwrap();
804 }
805 fs::rename(&staging, &persisted).unwrap_or_else(|err| {
806 panic!(
811 "failed to rename {} to {}: {err}",
812 staging.display(),
813 persisted.display()
814 )
815 });
816 match fs::remove_file(&staging) {
819 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
820 result => result.unwrap(),
821 }
822 Ok(persisted)
823}
824
825pub(crate) fn write_staged_source_cached(source_dir: &Path, crate_name: &str, project_dir: &Path) {
838 let _span = tracing::debug_span!(target: "hydro_build", "gen_staged").entered();
839
840 let staged_path = path!(project_dir / "src" / "__staged.rs");
841 let stamp_path = path!(project_dir / ".hydro-staged-stamp");
842
843 let exe_stamp = std::env::current_exe().ok().and_then(|exe| {
844 let mtime = fs::metadata(&exe)
845 .ok()?
846 .modified()
847 .ok()?
848 .duration_since(std::time::SystemTime::UNIX_EPOCH)
849 .ok()?
850 .as_nanos();
851 Some(format!("{}\t{}", exe.display(), mtime))
852 });
853
854 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
855
856 fs::create_dir_all(path!(project_dir / "src")).unwrap();
857
858 let mut stamp_file = File::options()
862 .read(true)
863 .write(true)
864 .create(true)
865 .truncate(false)
866 .open(&stamp_path)
867 .unwrap();
868 stamp_file.lock().unwrap();
869
870 let mut existing_stamp = String::new();
871 if stamp_file.read_to_string(&mut existing_stamp).is_err() {
872 existing_stamp.clear();
873 }
874
875 if let Some(stamp) = &exe_stamp
876 && existing_stamp == *stamp
877 && staged_path.exists()
878 {
879 return;
880 }
881
882 let raw_toml_manifest = toml::from_str::<toml::Value>(
883 &fs::read_to_string(path!(source_dir / "Cargo.toml")).unwrap(),
884 )
885 .unwrap();
886
887 let maybe_custom_lib_path = raw_toml_manifest
888 .get("lib")
889 .and_then(|lib| lib.get("path"))
890 .and_then(|path| path.as_str());
891
892 let mut gen_staged = stageleft_tool::gen_staged_trybuild(
893 &maybe_custom_lib_path
894 .map(|s| path!(source_dir / s))
895 .unwrap_or_else(|| path!(source_dir / "src" / "lib.rs")),
896 &path!(source_dir / "Cargo.toml"),
897 crate_name,
898 Some("hydro___test".to_owned()),
899 );
900
901 gen_staged.attrs.insert(
902 0,
903 syn::parse_quote! {
904 #![allow(
905 unused,
906 ambiguous_glob_reexports,
907 clippy::suspicious_else_formatting,
908 unexpected_cfgs,
909 reason = "generated code"
910 )]
911 },
912 );
913
914 let inlined_staged = prettyplease::unparse(&gen_staged);
915
916 write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
917
918 if let Some(stamp) = exe_stamp {
919 stamp_file.set_len(0).unwrap();
920 stamp_file.seek(SeekFrom::Start(0)).unwrap();
921 stamp_file.write_all(stamp.as_bytes()).unwrap();
922 }
923}
924
925pub fn create_trybuild()
926-> Result<(PathBuf, PathBuf, String, Option<Vec<String>>), trybuild_internals_api::error::Error> {
927 let _span = tracing::debug_span!(target: "hydro_build", "create_trybuild").entered();
928 let Metadata {
929 target_directory: target_dir,
930 workspace_root: workspace,
931 packages,
932 } = {
933 let _span = tracing::debug_span!(target: "hydro_build", "cargo_metadata").entered();
934 cargo::metadata()?
935 };
936
937 let source_dir = cargo::manifest_dir()?;
938 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
939
940 let mut dev_dependency_features = vec![];
941 source_manifest.dev_dependencies.retain(|k, v| {
942 if source_manifest.dependencies.contains_key(k) {
943 for feat in &v.features {
945 dev_dependency_features.push(format!("{}/{}", k, feat));
946 }
947
948 false
949 } else {
950 dev_dependency_features.push(format!("dep:{k}"));
952
953 v.optional = true;
954 true
955 }
956 });
957
958 let mut features = if std::env::var("RUNNING_AS_EXAMPLE_TEST").is_ok_and(|v| v == "1") {
964 None
965 } else {
966 features::find()
967 };
968
969 let path_dependencies = source_manifest
970 .dependencies
971 .iter()
972 .filter_map(|(name, dep)| {
973 let path = dep.path.as_ref()?;
974 if packages.iter().any(|p| &p.name == name) {
975 None
977 } else {
978 Some(PathDependency {
979 name: name.clone(),
980 normalized_path: path.canonicalize().ok()?,
981 normalization: Normalization::PathDependencies,
982 })
983 }
984 })
985 .collect();
986
987 let crate_name = source_manifest.package.name.clone();
988 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
989 fs::create_dir_all(&project_dir)?;
990
991 let project_name = format!("{}-hydro-trybuild", crate_name);
992 let dylib_package_name = format!("{project_name}-dylib");
993 let mut manifest = Runner::make_manifest(
994 &workspace,
995 &project_name,
996 &source_dir,
997 &packages,
998 &[],
999 source_manifest,
1000 )?;
1001
1002 if let Some(enabled_features) = &mut features {
1003 enabled_features
1004 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
1005 }
1006
1007 for runtime_feature in HYDRO_RUNTIME_FEATURES {
1008 manifest.features.insert(
1009 format!("hydro___feature_{runtime_feature}"),
1010 vec![format!("hydro_lang/{runtime_feature}")],
1011 );
1012 }
1013
1014 manifest
1015 .dependencies
1016 .get_mut("hydro_lang")
1017 .unwrap()
1018 .features
1019 .push("runtime_support".to_owned());
1020
1021 manifest
1022 .features
1023 .insert("hydro___test".to_owned(), dev_dependency_features);
1024
1025 if manifest
1026 .workspace
1027 .as_ref()
1028 .is_some_and(|w| w.dependencies.is_empty())
1029 {
1030 manifest.workspace = None;
1031 }
1032
1033 let project = Project {
1034 dir: project_dir,
1035 source_dir,
1036 target_dir,
1037 name: project_name.clone(),
1038 update: Update::env()?,
1039 has_pass: false,
1040 has_compile_fail: false,
1041 features,
1042 workspace,
1043 path_dependencies,
1044 manifest,
1045 keep_going: false,
1046 };
1047
1048 {
1049 let _span = tracing::debug_span!(target: "hydro_build", "write_project_files").entered();
1050 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
1051
1052 let _project_lock = hydro_concurrent_cargo::lock_project(project.dir.as_ref());
1053
1054 fs::create_dir_all(path!(project.dir / "src"))?;
1055 fs::create_dir_all(path!(project.dir / "examples"))?;
1056
1057 let crate_name_ident = syn::Ident::new(
1058 &crate_name.replace('-', "_"),
1059 proc_macro2::Span::call_site(),
1060 );
1061
1062 write_atomic(
1063 prettyplease::unparse(&syn::parse_quote! {
1064 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1065
1066 pub mod __root {
1067 pub use #crate_name_ident::*;
1068 #[cfg(feature = "hydro___test")]
1069 pub use super::__staged;
1070 }
1071
1072 #[cfg(feature = "hydro___test")]
1073 pub mod __staged;
1074 })
1075 .as_bytes(),
1076 &path!(project.dir / "src" / "lib.rs"),
1077 )
1078 .unwrap();
1079
1080 let base_manifest = toml::to_string(&project.manifest)?;
1081
1082 let feature_names: Vec<_> = project.manifest.features.keys().cloned().collect();
1084
1085 let dylib_dir = path!(project.dir / "dylib");
1087 fs::create_dir_all(path!(dylib_dir / "src"))?;
1088
1089 let trybuild_crate_name_ident = syn::Ident::new(
1090 &project_name.replace('-', "_"),
1091 proc_macro2::Span::call_site(),
1092 );
1093 write_atomic(
1094 [
1100 "// v2: dylib must be built as a dependency (prefer-dynamic).\n".as_bytes(),
1101 prettyplease::unparse(&syn::parse_quote! {
1102 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1103 pub use #trybuild_crate_name_ident::*;
1104 })
1105 .as_bytes(),
1106 ]
1107 .concat()
1108 .as_slice(),
1109 &path!(dylib_dir / "src" / "lib.rs"),
1110 )?;
1111
1112 let serialized_edition = toml::to_string(
1113 &vec![("edition", &project.manifest.package.edition)]
1114 .into_iter()
1115 .collect::<std::collections::HashMap<_, _>>(),
1116 )
1117 .unwrap();
1118
1119 let dylib_features_section = feature_names
1122 .iter()
1123 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
1124 .collect::<Vec<_>>()
1125 .join("\n");
1126
1127 let dylib_lib_name = hydro_concurrent_cargo::current_dylib_lib_name(project.dir.as_ref())
1135 .unwrap_or_else(|| {
1136 hydro_concurrent_cargo::dylib_lib_name(&dylib_package_name, ENCODED_RUSTFLAGS)
1137 });
1138
1139 let dylib_manifest = format!(
1140 r#"[package]
1141name = "{dylib_package_name}"
1142version = "0.0.0"
1143{}
1144
1145[lib]
1146name = "{dylib_lib_name}"
1147crate-type = ["{}"]
1148
1149[dependencies]
1150{project_name} = {{ path = "..", default-features = false }}
1151
1152[features]
1153{dylib_features_section}
1154"#,
1155 serialized_edition,
1156 if cfg!(target_os = "windows") {
1157 "rlib"
1158 } else {
1159 "dylib"
1160 }
1161 );
1162 write_atomic(dylib_manifest.as_ref(), &path!(dylib_dir / "Cargo.toml"))?;
1163
1164 let dylib_examples_dir = path!(project.dir / "dylib-examples");
1165 fs::create_dir_all(path!(dylib_examples_dir / "src"))?;
1166 fs::create_dir_all(path!(dylib_examples_dir / "examples"))?;
1167
1168 write_atomic(
1169 b"#![allow(unused_crate_dependencies)]\n",
1170 &path!(dylib_examples_dir / "src" / "lib.rs"),
1171 )?;
1172
1173 let features_section = feature_names
1175 .iter()
1176 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
1177 .collect::<Vec<_>>()
1178 .join("\n");
1179
1180 let dylib_examples_manifest = format!(
1192 r#"[package]
1193name = "{project_name}-dylib-examples"
1194version = "0.0.0"
1195{}
1196
1197[dependencies]
1198{project_name} = {{ package = "{project_name}-dylib", path = "../dylib", default-features = false }}
1199
1200[features]
1201{features_section}
1202
1203[[example]]
1204name = "sim-dylib"
1205crate-type = ["cdylib"]
1206"#,
1207 serialized_edition
1208 );
1209 write_atomic(
1210 dylib_examples_manifest.as_ref(),
1211 &path!(dylib_examples_dir / "Cargo.toml"),
1212 )?;
1213
1214 let sim_dylib_contents = prettyplease::unparse(&syn::parse_quote! {
1216 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1217 include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
1218 });
1219 write_atomic(
1220 sim_dylib_contents.as_bytes(),
1221 &path!(project.dir / "examples" / "sim-dylib.rs"),
1222 )?;
1223 write_atomic(
1224 sim_dylib_contents.as_bytes(),
1225 &path!(dylib_examples_dir / "examples" / "sim-dylib.rs"),
1226 )?;
1227
1228 let workspace_manifest = format!(
1229 r#"{}
1230[[example]]
1231name = "sim-dylib"
1232crate-type = ["cdylib"]
1233
1234[workspace]
1235members = ["dylib", "dylib-examples"]
1236"#,
1237 base_manifest,
1238 );
1239
1240 write_atomic(
1241 workspace_manifest.as_ref(),
1242 &path!(project.dir / "Cargo.toml"),
1243 )?;
1244
1245 let manifest_hash = {
1251 let mut hasher = Sha256::new();
1252 hasher.update(&workspace_manifest);
1253 hasher.update(dylib_manifest.replace(&dylib_lib_name, ""));
1254 hasher.update(&dylib_examples_manifest);
1255 format!("{:X}", hasher.finalize())
1256 .chars()
1257 .take(8)
1258 .collect::<String>()
1259 };
1260
1261 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
1262 let workspace_cargo_lock_contents_and_hash = if workspace_cargo_lock.exists() {
1263 let cargo_lock_contents = fs::read_to_string(&workspace_cargo_lock)?;
1264
1265 let hash = format!("{:X}", Sha256::digest(&cargo_lock_contents))
1266 .chars()
1267 .take(8)
1268 .collect::<String>();
1269
1270 Some((cargo_lock_contents, hash))
1271 } else {
1272 None
1273 };
1274
1275 let trybuild_hash = format!(
1276 "{}-{}",
1277 manifest_hash,
1278 workspace_cargo_lock_contents_and_hash
1279 .as_ref()
1280 .map(|(_contents, hash)| &**hash)
1281 .unwrap_or_default()
1282 );
1283
1284 if !check_contents(
1285 trybuild_hash.as_bytes(),
1286 &path!(project.dir / ".hydro-trybuild-manifest"),
1287 )
1288 .is_ok_and(|b| b)
1289 {
1290 let _span = tracing::debug_span!(target: "hydro_build", "update_lockfile").entered();
1291 if let Some((cargo_lock_contents, _)) = workspace_cargo_lock_contents_and_hash {
1293 write_atomic(
1296 cargo_lock_contents.as_ref(),
1297 &path!(project.dir / "Cargo.lock"),
1298 )?;
1299 } else {
1300 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
1301 }
1302
1303 std::process::Command::new("cargo")
1305 .current_dir(&project.dir)
1306 .args(["update", "-w"]) .stdout(std::process::Stdio::null())
1308 .stderr(std::process::Stdio::null())
1309 .status()
1310 .unwrap();
1311
1312 write_atomic(
1313 trybuild_hash.as_bytes(),
1314 &path!(project.dir / ".hydro-trybuild-manifest"),
1315 )?;
1316 }
1317
1318 let examples_folder = path!(project.dir / "examples");
1320 fs::create_dir_all(&examples_folder)?;
1321
1322 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
1323 if workspace_dot_cargo_config_toml.exists() {
1324 let dot_cargo_folder = path!(project.dir / ".cargo");
1325 fs::create_dir_all(&dot_cargo_folder)?;
1326
1327 write_atomic(
1328 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
1329 &path!(dot_cargo_folder / "config.toml"),
1330 )?;
1331 }
1332
1333 let vscode_folder = path!(project.dir / ".vscode");
1334 fs::create_dir_all(&vscode_folder)?;
1335 write_atomic(
1336 include_bytes!("./vscode-trybuild.json"),
1337 &path!(vscode_folder / "settings.json"),
1338 )?;
1339 }
1340
1341 Ok((
1342 project.dir.as_ref().into(),
1343 project.target_dir.as_ref().into(),
1344 dylib_package_name,
1345 project.features,
1346 ))
1347}
1348
1349fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
1350 let mut file = File::options()
1351 .read(true)
1352 .write(false)
1353 .create(false)
1354 .truncate(false)
1355 .open(path)?;
1356 file.lock()?;
1357
1358 let mut existing_contents = Vec::new();
1359 file.read_to_end(&mut existing_contents)?;
1360 Ok(existing_contents == contents)
1361}
1362
1363pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
1364 let mut file = File::options()
1365 .read(true)
1366 .write(true)
1367 .create(true)
1368 .truncate(false)
1369 .open(path)?;
1370
1371 let mut existing_contents = Vec::new();
1372 file.read_to_end(&mut existing_contents)?;
1373 if existing_contents != contents {
1374 file.lock()?;
1375 file.seek(SeekFrom::Start(0))?;
1376 file.set_len(0)?;
1377 file.write_all(contents)?;
1378 }
1379
1380 Ok(())
1381}