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 syn::parse_quote! {
305 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
306 use #trybuild_crate_name_ident::__root as #orig_crate_name;
307 use #orig_crate_name::*;
308 use #orig_crate_name::__staged::__deps::*;
309 use #root::prelude::*;
310 use #root::runtime_support::dfir_rs as __root_dfir_rs;
311 pub use #orig_crate_name::__staged;
312
313 #[allow(unused)]
314 fn __hydro_runtime<'a>(
315 __hydro_lang_maelstrom_meta: &'a #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::MaelstromMeta
316 )
317 -> #root::runtime_support::dfir_rs::scheduled::context::Dfir<impl #root::runtime_support::dfir_rs::scheduled::context::TickClosure + 'a>
318 {
319 #( #extra_stmts )*
320
321 #dfir_expr
322 }
323
324 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
325 async fn main() {
326 #root::telemetry::initialize_tracing();
327
328 let __hydro_lang_maelstrom_meta = #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::maelstrom_init();
330
331 let mut #dfir_ident = __hydro_runtime(&__hydro_lang_maelstrom_meta);
332
333 __hydro_lang_maelstrom_meta.start_receiving(); let local_set = #root::runtime_support::tokio::task::LocalSet::new();
336 #(
337 let _ = local_set.spawn_local( #sidecars ); )*
339
340 let _ = local_set.run_until(#dfir_ident.run()).await;
341 }
342 }
343 }
344 };
345 source_ast
346}
347
348#[cfg(any(feature = "sim", feature = "maelstrom"))]
351pub struct ExampleBuildConfig<'a> {
352 pub trybuild: TrybuildConfig,
354 pub bin_name: String,
358 pub runtime_feature: &'a str,
361 pub example_name: String,
364 pub crate_type: Option<&'a str>,
367 pub set_trybuild_lib_name: bool,
370 pub allow_fuzz: bool,
373}
374
375#[cfg(any(feature = "sim", feature = "maelstrom"))]
377fn rustc_target_libdir() -> Option<String> {
378 static LIBDIR: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
379 LIBDIR
380 .get_or_init(|| {
381 let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
382 std::process::Command::new(rustc)
383 .args(["--print", "target-libdir"])
384 .output()
385 .ok()
386 .filter(|out| out.status.success())
387 .map(|out| String::from_utf8(out.stdout).unwrap().trim().to_owned())
388 })
389 .clone()
390}
391
392pub enum BuiltArtifact {
407 Temp(tempfile::TempPath),
409 Persisted(PathBuf),
411}
412
413impl std::ops::Deref for BuiltArtifact {
414 type Target = Path;
415
416 fn deref(&self) -> &Path {
417 match self {
418 BuiltArtifact::Temp(path) => path,
419 BuiltArtifact::Persisted(path) => path,
420 }
421 }
422}
423
424impl BuiltArtifact {
425 #[cfg(feature = "maelstrom")]
431 pub fn keep(self) -> Result<PathBuf, std::io::Error> {
432 match self {
433 BuiltArtifact::Temp(path) => path.keep().map_err(|e| e.error),
434 BuiltArtifact::Persisted(path) => Ok(path),
435 }
436 }
437}
438
439const ENCODED_RUSTFLAGS: &str = env!("HYDRO_ENCODED_RUSTFLAGS");
449
450#[cfg(any(feature = "sim", feature = "maelstrom"))]
469fn forward_rustflags(command: &mut std::process::Command) {
470 let flags = toml::Value::try_from(
471 ENCODED_RUSTFLAGS
472 .split('\x1f')
473 .filter(|flag| !flag.is_empty())
474 .collect::<Vec<_>>(),
475 )
476 .unwrap();
477 command.args(["--config", &format!("build.rustflags={flags}")]);
478}
479
480#[cfg(any(feature = "sim", feature = "maelstrom"))]
488pub fn compile_trybuild_example(config: ExampleBuildConfig<'_>) -> Result<BuiltArtifact, ()> {
489 use std::process::{Command, Stdio};
490
491 let ExampleBuildConfig {
492 trybuild,
493 bin_name,
494 runtime_feature,
495 example_name,
496 crate_type,
497 set_trybuild_lib_name,
498 allow_fuzz,
499 } = config;
500
501 let is_fuzz = allow_fuzz && std::env::var("BOLERO_FUZZER").is_ok();
502
503 let use_prebuild = !is_fuzz && std::env::var_os("CARGO_BUILD_TARGET").is_none();
509
510 let coverage_enabled =
514 rustflags::from_encoded(std::ffi::OsStr::new(ENCODED_RUSTFLAGS)).any(|flag| {
515 matches!(
516 flag,
517 rustflags::Flag::Codegen { opt, value }
518 if opt == "instrument-coverage"
519 && !matches!(value.as_deref(), Some("off" | "no" | "n" | "false" | "0"))
520 )
521 });
522
523 let crate_to_compile = if is_fuzz {
525 trybuild.project_dir.clone()
526 } else {
527 path!(trybuild.project_dir / "dylib-examples")
528 };
529
530 let (final_target_dir, _prebuild_guard, _cargo_lock) = if use_prebuild {
531 let prebuild_span =
532 tracing::debug_span!(target: "hydro_build", "prebuild", bin_name = %bin_name).entered();
533 let shared_debug = trybuild.target_dir.join("debug");
534 let jobs_dir = trybuild.target_dir.join("jobs");
535 let per_job = hydro_concurrent_cargo::setup_job_dir(&jobs_dir, &bin_name, &shared_debug);
536
537 let mut features: Vec<String> = trybuild.features.clone().unwrap_or_default();
538 features.push(runtime_feature.to_owned());
539
540 let staged_paths = vec![
541 path!(trybuild.project_dir / "src" / "__staged.rs"),
542 path!(trybuild.project_dir / "Cargo.lock"),
543 std::env::current_exe().unwrap(),
544 ];
545
546 let features_for_closure = features.clone();
547
548 let (guard, cargo_lock) = hydro_concurrent_cargo::run_prebuild(
553 &trybuild.target_dir,
554 trybuild.project_dir.file_name().unwrap().to_str().unwrap(),
555 &features,
556 ENCODED_RUSTFLAGS,
557 &staged_paths,
558 |prebuild_target| {
559 hydro_concurrent_cargo::set_dylib_lib_name(
560 &trybuild.project_dir,
561 ENCODED_RUSTFLAGS,
562 );
563 let features_str = features_for_closure.join(",");
564
565 let mut lib_cmd = Command::new("cargo");
574 lib_cmd.current_dir(&crate_to_compile);
575 lib_cmd.args(["build", "--locked", "--lib"]);
576 lib_cmd.args(["--target-dir", prebuild_target.to_str().unwrap()]);
577 lib_cmd.arg("--no-default-features");
578 lib_cmd.args(["--features", &features_str]);
579 lib_cmd.args(["--config", "build.incremental = false"]);
580 lib_cmd.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
581 forward_rustflags(&mut lib_cmd);
582 let status = lib_cmd.stdin(Stdio::null()).status().unwrap();
583 assert!(status.success(), "dep prebuild failed")
584 },
585 );
586
587 drop(prebuild_span);
591 (per_job, Some(guard), Some(cargo_lock))
592 } else {
593 (trybuild.target_dir.clone(), None, None)
594 };
595
596 let _job_build_guard = if use_prebuild {
598 let populate_span =
599 tracing::debug_span!(target: "hydro_build", "populate_job_dir", bin_name = %bin_name)
600 .entered();
601 let shared_debug = trybuild.target_dir.join("debug");
602 let guard = hydro_concurrent_cargo::populate_job_build_dir(
603 &final_target_dir.join("debug"),
604 &shared_debug,
605 );
606 drop(populate_span);
609 Some(guard)
610 } else {
611 None
612 };
613
614 let final_build_span =
615 tracing::debug_span!(target: "hydro_build", "final_build", bin_name = %bin_name).entered();
616 let mut command = Command::new("cargo");
617 command.current_dir(&crate_to_compile);
618 command.args(["rustc", if use_prebuild { "--frozen" } else { "--locked" }]);
619 command.args(["--example", &example_name]);
620 command.args(["--target-dir", final_target_dir.to_str().unwrap()]);
621 command.arg("--no-default-features");
628 command.args([
629 "--features",
630 &trybuild
631 .features
632 .clone()
633 .into_iter()
634 .flatten()
635 .chain([runtime_feature.to_owned()])
636 .collect::<Vec<_>>()
637 .join(","),
638 ]);
639 command.args(["--config", "build.incremental = false"]);
640 forward_rustflags(&mut command);
641 if let Some(crate_type) = crate_type {
642 command.args(["--crate-type", crate_type]);
643 }
644 command.arg("--message-format=json-diagnostic-rendered-ansi");
645 command.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
646 if set_trybuild_lib_name {
647 command.env("TRYBUILD_LIB_NAME", &bin_name);
648 }
649
650 command.arg("--");
651
652 if cfg!(any(target_os = "linux", target_os = "macos")) {
653 let (debug_path, shared_debug_path) =
654 if let Ok(target) = std::env::var("CARGO_BUILD_TARGET") {
655 (
656 path!(final_target_dir / target / "debug"),
657 path!(trybuild.target_dir / target / "debug"),
658 )
659 } else {
660 (
661 path!(final_target_dir / "debug"),
662 path!(trybuild.target_dir / "debug"),
663 )
664 };
665
666 let mut rpaths = hydro_concurrent_cargo::dylib_search_dirs(
671 &trybuild.dylib_package_name,
672 &debug_path,
673 &shared_debug_path,
674 );
675 if let Some(libdir) = rustc_target_libdir() {
676 rpaths.push(PathBuf::from(libdir));
677 }
678 for rpath in rpaths {
679 if cfg!(target_os = "macos") {
680 command.args([
684 "-Clink-arg=-rpath".to_owned(),
685 format!("-Clink-arg={}", rpath.to_str().unwrap()),
686 ]);
687 } else {
688 command.args([format!("-Clink-arg=-Wl,-rpath,{}", rpath.to_str().unwrap())]);
689 }
690 }
691
692 if cfg!(all(target_os = "linux", target_env = "gnu")) {
693 command.arg(
694 "-Clink-args=-Wl,-z,nodelete",
696 );
697 }
698 }
699
700 if allow_fuzz && let Ok(fuzzer) = std::env::var("BOLERO_FUZZER") {
701 command.env_remove("BOLERO_FUZZER");
702
703 if fuzzer == "libfuzzer" {
704 #[cfg(target_os = "macos")]
705 {
706 command.args(["-Clink-arg=-undefined", "-Clink-arg=dynamic_lookup"]);
707 }
708
709 #[cfg(target_os = "linux")]
710 {
711 command.args(["-Clink-arg=-Wl,--unresolved-symbols=ignore-all"]);
712 }
713 }
714 }
715
716 tracing::debug!(
717 target: "hydro_build",
718 "final build command (cwd={}): {:?}",
719 crate_to_compile.display(),
720 command
721 );
722
723 let mut spawned = command
724 .stdout(Stdio::piped())
725 .stderr(Stdio::piped())
726 .stdin(Stdio::null())
727 .spawn()
728 .unwrap();
729 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
730 let stderr_handle = spawned.stderr.take().unwrap();
731 let stderr_thread = std::thread::spawn(move || {
732 use std::io::Read;
733 let mut buf = String::new();
734 std::io::BufReader::new(stderr_handle)
735 .read_to_string(&mut buf)
736 .unwrap();
737 buf
738 });
739
740 let mut out = Err(());
741 for message in cargo_metadata::Message::parse_stream(reader) {
742 match message.unwrap() {
743 cargo_metadata::Message::CompilerArtifact(artifact) => {
744 let is_output = artifact.target.is_example();
746
747 if is_output {
748 let path = artifact.filenames.first().unwrap();
749 let path_buf: PathBuf = path.clone().into();
750 out = Ok(path_buf);
751 }
752 }
753 cargo_metadata::Message::CompilerMessage(mut msg) => {
754 if let Some(rendered) = msg.message.rendered.as_mut() {
757 let file_names = msg
758 .message
759 .spans
760 .iter()
761 .map(|s| &s.file_name)
762 .collect::<std::collections::BTreeSet<_>>();
763 for file_name in file_names {
764 *rendered = rendered.replace(
765 file_name,
766 &format!("(full path) {}/{file_name}", trybuild.project_dir.display()),
767 )
768 }
769 }
770 eprintln!("{}", msg.message);
771 }
772 cargo_metadata::Message::TextLine(line) => {
773 eprintln!("{}", line);
774 }
775 cargo_metadata::Message::BuildFinished(_) => {}
776 cargo_metadata::Message::BuildScriptExecuted(_) => {}
777 msg => panic!("Unexpected message type: {:?}", msg),
778 }
779 }
780
781 spawned.wait().unwrap();
782 let stderr_output = stderr_thread.join().unwrap();
783 drop(final_build_span);
784
785 if use_prebuild {
788 for line in stderr_output.lines() {
789 if line.contains("Compiling") {
790 assert!(
791 line.contains("dylib-examples"),
792 "unexpected recompilation in final build: {line}\nfull stderr:\n{stderr_output}"
793 );
794 }
795 }
796 }
797
798 assert!(
799 out.is_ok(),
800 "final build failed to produce binary.\nstderr:\n{stderr_output}"
801 );
802
803 if coverage_enabled {
804 let coverage_dir = path!(trybuild.target_dir / "debug" / "deps" / "hydro-coverage");
817 fs::create_dir_all(&coverage_dir).unwrap();
818 let artifact_name = out.as_ref().unwrap().file_name().unwrap().to_str().unwrap();
819 let fingerprint = hydro_concurrent_cargo::toolchain_fingerprint(ENCODED_RUSTFLAGS);
820 let persisted = path!(coverage_dir / format!("{bin_name}-{fingerprint}-{artifact_name}"));
821 let staging = tempfile::NamedTempFile::new_in(&coverage_dir).unwrap();
824 fs::copy(out.as_ref().unwrap(), staging.path()).unwrap();
825 staging.persist(&persisted).unwrap();
826 return Ok(BuiltArtifact::Persisted(persisted));
827 }
828
829 let out_file = tempfile::NamedTempFile::new().unwrap().into_temp_path();
830 fs::copy(out.as_ref().unwrap(), &out_file).unwrap();
831 Ok(BuiltArtifact::Temp(out_file))
832}
833
834pub(crate) fn write_staged_source_cached(source_dir: &Path, crate_name: &str, project_dir: &Path) {
847 let _span = tracing::debug_span!(target: "hydro_build", "gen_staged").entered();
848
849 let staged_path = path!(project_dir / "src" / "__staged.rs");
850 let stamp_path = path!(project_dir / ".hydro-staged-stamp");
851
852 let exe_stamp = std::env::current_exe().ok().and_then(|exe| {
853 let mtime = fs::metadata(&exe)
854 .ok()?
855 .modified()
856 .ok()?
857 .duration_since(std::time::SystemTime::UNIX_EPOCH)
858 .ok()?
859 .as_nanos();
860 Some(format!("{}\t{}", exe.display(), mtime))
861 });
862
863 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
864
865 fs::create_dir_all(path!(project_dir / "src")).unwrap();
866
867 let mut stamp_file = File::options()
871 .read(true)
872 .write(true)
873 .create(true)
874 .truncate(false)
875 .open(&stamp_path)
876 .unwrap();
877 stamp_file.lock().unwrap();
878
879 let mut existing_stamp = String::new();
880 if stamp_file.read_to_string(&mut existing_stamp).is_err() {
881 existing_stamp.clear();
882 }
883
884 if let Some(stamp) = &exe_stamp
885 && existing_stamp == *stamp
886 && staged_path.exists()
887 {
888 return;
889 }
890
891 let raw_toml_manifest = toml::from_str::<toml::Value>(
892 &fs::read_to_string(path!(source_dir / "Cargo.toml")).unwrap(),
893 )
894 .unwrap();
895
896 let maybe_custom_lib_path = raw_toml_manifest
897 .get("lib")
898 .and_then(|lib| lib.get("path"))
899 .and_then(|path| path.as_str());
900
901 let mut gen_staged = stageleft_tool::gen_staged_trybuild(
902 &maybe_custom_lib_path
903 .map(|s| path!(source_dir / s))
904 .unwrap_or_else(|| path!(source_dir / "src" / "lib.rs")),
905 &path!(source_dir / "Cargo.toml"),
906 crate_name,
907 Some("hydro___test".to_owned()),
908 );
909
910 gen_staged.attrs.insert(
911 0,
912 syn::parse_quote! {
913 #![allow(
914 unused,
915 ambiguous_glob_reexports,
916 clippy::suspicious_else_formatting,
917 unexpected_cfgs,
918 reason = "generated code"
919 )]
920 },
921 );
922
923 let inlined_staged = prettyplease::unparse(&gen_staged);
924
925 write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
926
927 if let Some(stamp) = exe_stamp {
928 stamp_file.set_len(0).unwrap();
929 stamp_file.seek(SeekFrom::Start(0)).unwrap();
930 stamp_file.write_all(stamp.as_bytes()).unwrap();
931 }
932}
933
934pub fn create_trybuild()
935-> Result<(PathBuf, PathBuf, String, Option<Vec<String>>), trybuild_internals_api::error::Error> {
936 let _span = tracing::debug_span!(target: "hydro_build", "create_trybuild").entered();
937 let Metadata {
938 target_directory: target_dir,
939 workspace_root: workspace,
940 packages,
941 } = {
942 let _span = tracing::debug_span!(target: "hydro_build", "cargo_metadata").entered();
943 cargo::metadata()?
944 };
945
946 let source_dir = cargo::manifest_dir()?;
947 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
948
949 let mut dev_dependency_features = vec![];
950 source_manifest.dev_dependencies.retain(|k, v| {
951 if source_manifest.dependencies.contains_key(k) {
952 for feat in &v.features {
954 dev_dependency_features.push(format!("{}/{}", k, feat));
955 }
956
957 false
958 } else {
959 dev_dependency_features.push(format!("dep:{k}"));
961
962 v.optional = true;
963 true
964 }
965 });
966
967 let mut features = if std::env::var("RUNNING_AS_EXAMPLE_TEST").is_ok_and(|v| v == "1") {
973 None
974 } else {
975 features::find()
976 };
977
978 let path_dependencies = source_manifest
979 .dependencies
980 .iter()
981 .filter_map(|(name, dep)| {
982 let path = dep.path.as_ref()?;
983 if packages.iter().any(|p| &p.name == name) {
984 None
986 } else {
987 Some(PathDependency {
988 name: name.clone(),
989 normalized_path: path.canonicalize().ok()?,
990 normalization: Normalization::PathDependencies,
991 })
992 }
993 })
994 .collect();
995
996 let crate_name = source_manifest.package.name.clone();
997 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
998 fs::create_dir_all(&project_dir)?;
999
1000 let project_name = format!("{}-hydro-trybuild", crate_name);
1001 let dylib_package_name = format!("{project_name}-dylib");
1002 let mut manifest = Runner::make_manifest(
1003 &workspace,
1004 &project_name,
1005 &source_dir,
1006 &packages,
1007 &[],
1008 source_manifest,
1009 )?;
1010
1011 if let Some(enabled_features) = &mut features {
1012 enabled_features
1013 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
1014 }
1015
1016 for runtime_feature in HYDRO_RUNTIME_FEATURES {
1017 manifest.features.insert(
1018 format!("hydro___feature_{runtime_feature}"),
1019 vec![format!("hydro_lang/{runtime_feature}")],
1020 );
1021 }
1022
1023 manifest
1024 .dependencies
1025 .get_mut("hydro_lang")
1026 .unwrap()
1027 .features
1028 .push("runtime_support".to_owned());
1029
1030 manifest
1031 .features
1032 .insert("hydro___test".to_owned(), dev_dependency_features);
1033
1034 if manifest
1035 .workspace
1036 .as_ref()
1037 .is_some_and(|w| w.dependencies.is_empty())
1038 {
1039 manifest.workspace = None;
1040 }
1041
1042 let project = Project {
1043 dir: project_dir,
1044 source_dir,
1045 target_dir,
1046 name: project_name.clone(),
1047 update: Update::env()?,
1048 has_pass: false,
1049 has_compile_fail: false,
1050 features,
1051 workspace,
1052 path_dependencies,
1053 manifest,
1054 keep_going: false,
1055 };
1056
1057 {
1058 let _span = tracing::debug_span!(target: "hydro_build", "write_project_files").entered();
1059 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
1060
1061 let _project_lock = hydro_concurrent_cargo::lock_project(project.dir.as_ref());
1062
1063 fs::create_dir_all(path!(project.dir / "src"))?;
1064 fs::create_dir_all(path!(project.dir / "examples"))?;
1065
1066 let crate_name_ident = syn::Ident::new(
1067 &crate_name.replace('-', "_"),
1068 proc_macro2::Span::call_site(),
1069 );
1070
1071 write_atomic(
1072 prettyplease::unparse(&syn::parse_quote! {
1073 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1074
1075 pub mod __root {
1076 pub use #crate_name_ident::*;
1077 #[cfg(feature = "hydro___test")]
1078 pub use super::__staged;
1079 }
1080
1081 #[cfg(feature = "hydro___test")]
1082 pub mod __staged;
1083 })
1084 .as_bytes(),
1085 &path!(project.dir / "src" / "lib.rs"),
1086 )
1087 .unwrap();
1088
1089 let base_manifest = toml::to_string(&project.manifest)?;
1090
1091 let feature_names: Vec<_> = project.manifest.features.keys().cloned().collect();
1093
1094 let dylib_dir = path!(project.dir / "dylib");
1096 fs::create_dir_all(path!(dylib_dir / "src"))?;
1097
1098 let trybuild_crate_name_ident = syn::Ident::new(
1099 &project_name.replace('-', "_"),
1100 proc_macro2::Span::call_site(),
1101 );
1102 write_atomic(
1103 [
1109 "// v2: dylib must be built as a dependency (prefer-dynamic).\n".as_bytes(),
1110 prettyplease::unparse(&syn::parse_quote! {
1111 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1112 pub use #trybuild_crate_name_ident::*;
1113 })
1114 .as_bytes(),
1115 ]
1116 .concat()
1117 .as_slice(),
1118 &path!(dylib_dir / "src" / "lib.rs"),
1119 )?;
1120
1121 let serialized_edition = toml::to_string(
1122 &vec![("edition", &project.manifest.package.edition)]
1123 .into_iter()
1124 .collect::<std::collections::HashMap<_, _>>(),
1125 )
1126 .unwrap();
1127
1128 let dylib_features_section = feature_names
1131 .iter()
1132 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
1133 .collect::<Vec<_>>()
1134 .join("\n");
1135
1136 let dylib_lib_name = hydro_concurrent_cargo::current_dylib_lib_name(project.dir.as_ref())
1144 .unwrap_or_else(|| {
1145 hydro_concurrent_cargo::dylib_lib_name(&dylib_package_name, ENCODED_RUSTFLAGS)
1146 });
1147
1148 let dylib_manifest = format!(
1149 r#"[package]
1150name = "{dylib_package_name}"
1151version = "0.0.0"
1152{}
1153
1154[lib]
1155name = "{dylib_lib_name}"
1156crate-type = ["{}"]
1157
1158[dependencies]
1159{project_name} = {{ path = "..", default-features = false }}
1160
1161[features]
1162{dylib_features_section}
1163"#,
1164 serialized_edition,
1165 if cfg!(target_os = "windows") {
1166 "rlib"
1167 } else {
1168 "dylib"
1169 }
1170 );
1171 write_atomic(dylib_manifest.as_ref(), &path!(dylib_dir / "Cargo.toml"))?;
1172
1173 let dylib_examples_dir = path!(project.dir / "dylib-examples");
1174 fs::create_dir_all(path!(dylib_examples_dir / "src"))?;
1175 fs::create_dir_all(path!(dylib_examples_dir / "examples"))?;
1176
1177 write_atomic(
1178 b"#![allow(unused_crate_dependencies)]\n",
1179 &path!(dylib_examples_dir / "src" / "lib.rs"),
1180 )?;
1181
1182 let features_section = feature_names
1184 .iter()
1185 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
1186 .collect::<Vec<_>>()
1187 .join("\n");
1188
1189 let dylib_examples_manifest = format!(
1201 r#"[package]
1202name = "{project_name}-dylib-examples"
1203version = "0.0.0"
1204{}
1205
1206[dependencies]
1207{project_name} = {{ package = "{project_name}-dylib", path = "../dylib", default-features = false }}
1208
1209[features]
1210{features_section}
1211
1212[[example]]
1213name = "sim-dylib"
1214crate-type = ["cdylib"]
1215"#,
1216 serialized_edition
1217 );
1218 write_atomic(
1219 dylib_examples_manifest.as_ref(),
1220 &path!(dylib_examples_dir / "Cargo.toml"),
1221 )?;
1222
1223 let sim_dylib_contents = prettyplease::unparse(&syn::parse_quote! {
1225 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1226 include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
1227 });
1228 write_atomic(
1229 sim_dylib_contents.as_bytes(),
1230 &path!(project.dir / "examples" / "sim-dylib.rs"),
1231 )?;
1232 write_atomic(
1233 sim_dylib_contents.as_bytes(),
1234 &path!(dylib_examples_dir / "examples" / "sim-dylib.rs"),
1235 )?;
1236
1237 let workspace_manifest = format!(
1238 r#"{}
1239[[example]]
1240name = "sim-dylib"
1241crate-type = ["cdylib"]
1242
1243[workspace]
1244members = ["dylib", "dylib-examples"]
1245"#,
1246 base_manifest,
1247 );
1248
1249 write_atomic(
1250 workspace_manifest.as_ref(),
1251 &path!(project.dir / "Cargo.toml"),
1252 )?;
1253
1254 let manifest_hash = {
1260 let mut hasher = Sha256::new();
1261 hasher.update(&workspace_manifest);
1262 hasher.update(dylib_manifest.replace(&dylib_lib_name, ""));
1263 hasher.update(&dylib_examples_manifest);
1264 format!("{:X}", hasher.finalize())
1265 .chars()
1266 .take(8)
1267 .collect::<String>()
1268 };
1269
1270 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
1271 let workspace_cargo_lock_contents_and_hash = if workspace_cargo_lock.exists() {
1272 let cargo_lock_contents = fs::read_to_string(&workspace_cargo_lock)?;
1273
1274 let hash = format!("{:X}", Sha256::digest(&cargo_lock_contents))
1275 .chars()
1276 .take(8)
1277 .collect::<String>();
1278
1279 Some((cargo_lock_contents, hash))
1280 } else {
1281 None
1282 };
1283
1284 let trybuild_hash = format!(
1285 "{}-{}",
1286 manifest_hash,
1287 workspace_cargo_lock_contents_and_hash
1288 .as_ref()
1289 .map(|(_contents, hash)| &**hash)
1290 .unwrap_or_default()
1291 );
1292
1293 if !check_contents(
1294 trybuild_hash.as_bytes(),
1295 &path!(project.dir / ".hydro-trybuild-manifest"),
1296 )
1297 .is_ok_and(|b| b)
1298 {
1299 let _span = tracing::debug_span!(target: "hydro_build", "update_lockfile").entered();
1300 if let Some((cargo_lock_contents, _)) = workspace_cargo_lock_contents_and_hash {
1302 write_atomic(
1305 cargo_lock_contents.as_ref(),
1306 &path!(project.dir / "Cargo.lock"),
1307 )?;
1308 } else {
1309 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
1310 }
1311
1312 std::process::Command::new("cargo")
1314 .current_dir(&project.dir)
1315 .args(["update", "-w"]) .stdout(std::process::Stdio::null())
1317 .stderr(std::process::Stdio::null())
1318 .status()
1319 .unwrap();
1320
1321 write_atomic(
1322 trybuild_hash.as_bytes(),
1323 &path!(project.dir / ".hydro-trybuild-manifest"),
1324 )?;
1325 }
1326
1327 let examples_folder = path!(project.dir / "examples");
1329 fs::create_dir_all(&examples_folder)?;
1330
1331 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
1332 if workspace_dot_cargo_config_toml.exists() {
1333 let dot_cargo_folder = path!(project.dir / ".cargo");
1334 fs::create_dir_all(&dot_cargo_folder)?;
1335
1336 write_atomic(
1337 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
1338 &path!(dot_cargo_folder / "config.toml"),
1339 )?;
1340 }
1341
1342 let vscode_folder = path!(project.dir / ".vscode");
1343 fs::create_dir_all(&vscode_folder)?;
1344 write_atomic(
1345 include_bytes!("./vscode-trybuild.json"),
1346 &path!(vscode_folder / "settings.json"),
1347 )?;
1348 }
1349
1350 Ok((
1351 project.dir.as_ref().into(),
1352 project.target_dir.as_ref().into(),
1353 dylib_package_name,
1354 project.features,
1355 ))
1356}
1357
1358fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
1359 let mut file = File::options()
1360 .read(true)
1361 .write(false)
1362 .create(false)
1363 .truncate(false)
1364 .open(path)?;
1365 file.lock()?;
1366
1367 let mut existing_contents = Vec::new();
1368 file.read_to_end(&mut existing_contents)?;
1369 Ok(existing_contents == contents)
1370}
1371
1372pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
1373 let mut file = File::options()
1374 .read(true)
1375 .write(true)
1376 .create(true)
1377 .truncate(false)
1378 .open(path)?;
1379
1380 let mut existing_contents = Vec::new();
1381 file.read_to_end(&mut existing_contents)?;
1382 if existing_contents != contents {
1383 file.lock()?;
1384 file.seek(SeekFrom::Start(0))?;
1385 file.set_len(0)?;
1386 file.write_all(contents)?;
1387 }
1388
1389 Ok(())
1390}