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 let _span = tracing::debug_span!(target: "hydro_build", "graph_codegen").entered();
132 compile_graph_trybuild(graph, extra_stmts, sidecars, &crate_name, deploy_mode)
133 };
134
135 let source = {
136 let _span = tracing::debug_span!(target: "hydro_build", "unparse_source").entered();
137 prettyplease::unparse(&generated_code)
138 };
139
140 let hash = format!("{:X}", Sha256::digest(&source))
141 .chars()
142 .take(8)
143 .collect::<String>();
144
145 let bin_name = if let Some(bin_name_prefix) = bin_name_prefix {
146 format!("{}_{}", clean_bin_name_prefix(bin_name_prefix), hash)
147 } else {
148 hash
149 };
150
151 let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
152
153 let examples_dir = match linking_mode {
155 LinkingMode::Static => path!(project_dir / "examples"),
156 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
157 LinkingMode::Dynamic => path!(project_dir / "dylib-examples" / "examples"),
158 };
159
160 fs::create_dir_all(&examples_dir).unwrap();
162
163 let out_path = path!(examples_dir / format!("{bin_name}.rs"));
164 {
165 let _span =
166 tracing::debug_span!(target: "hydro_build", "write_generated_sources").entered();
167 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
168 write_atomic(source.as_ref(), &out_path).unwrap();
169 }
170
171 if is_test {
172 write_staged_source_cached(source_dir.as_ref(), &crate_name, &project_dir);
173 }
174
175 if is_test {
176 if cur_bin_enabled_features.is_none() {
177 cur_bin_enabled_features = Some(vec![]);
178 }
179
180 cur_bin_enabled_features
181 .as_mut()
182 .unwrap()
183 .push("hydro___test".to_owned());
184 }
185
186 (
187 bin_name,
188 TrybuildConfig {
189 project_dir,
190 target_dir,
191 features: cur_bin_enabled_features,
192 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
193 linking_mode,
194 },
195 )
196}
197
198#[cfg(any(feature = "deploy", feature = "maelstrom"))]
199pub fn compile_graph_trybuild(
200 partitioned_graph: DfirGraph,
201 extra_stmts: &[syn::Stmt],
202 sidecars: &[syn::Expr],
203 crate_name: &str,
204 deploy_mode: DeployMode,
205) -> syn::File {
206 use crate::staging_util::get_this_crate;
207
208 let mut diagnostics = Diagnostics::new();
209 let dfir_expr: syn::Expr = syn::parse2(
210 partitioned_graph
211 .as_code("e! { __root_dfir_rs }, true, quote!(), &mut diagnostics)
212 .expect("DFIR code generation failed with diagnostics."),
213 )
214 .unwrap();
215
216 let orig_crate_name = quote::format_ident!("{}", crate_name);
217 let trybuild_crate_name_ident = quote::format_ident!("{}_hydro_trybuild", crate_name);
218 let root = get_this_crate();
219 let tokio_main_ident = format!("{}::runtime_support::tokio", root);
220 let dfir_ident = quote::format_ident!("{}", crate::compile::DFIR_IDENT);
221
222 let source_ast: syn::File = match deploy_mode {
223 #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
224 DeployMode::Containerized => {
225 syn::parse_quote! {
226 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
227 use #trybuild_crate_name_ident::__root as #orig_crate_name;
228 use #orig_crate_name::*;
229 use #orig_crate_name::__staged::__deps::*;
230 use #root::prelude::*;
231 use #root::runtime_support::dfir_rs as __root_dfir_rs;
232 pub use #orig_crate_name::__staged;
233
234 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
235 async fn main() {
236 #root::telemetry::initialize_tracing();
237
238 #( #extra_stmts )*
239
240 let mut #dfir_ident = #dfir_expr;
241
242 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
243 #(
244 let _ = local_set.spawn_local( #sidecars ); )*
246
247 let _ = local_set.run_until(#dfir_ident.run()).await;
248 }
249 }
250 }
251 #[cfg(feature = "deploy")]
252 DeployMode::HydroDeploy => {
253 syn::parse_quote! {
254 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
255 use #trybuild_crate_name_ident::__root as #orig_crate_name;
256 use #orig_crate_name::*;
257 use #orig_crate_name::__staged::__deps::*;
258 use #root::prelude::*;
259 use #root::runtime_support::dfir_rs as __root_dfir_rs;
260 pub use #orig_crate_name::__staged;
261
262 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
263 async fn main() {
264 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;
265 let __hydro_lang_trybuild_cli = &__hydro_lang_trybuild_cli_owned;
266
267 #( #extra_stmts )*
268
269 let mut #dfir_ident = #dfir_expr;
270 println!("ack start");
271
272 let local_set = #root::runtime_support::tokio::task::LocalSet::new();
276 #(
277 let _ = local_set.spawn_local( #sidecars ); )*
279
280 let _ = local_set.run_until(#root::runtime_support::launch::run_stdin_commands(
281 async move {
282 #dfir_ident.run().await
283 }
284 )).await;
285 }
286 }
287 }
288 #[cfg(feature = "maelstrom")]
289 DeployMode::Maelstrom => {
290 syn::parse_quote! {
291 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
292 use #trybuild_crate_name_ident::__root as #orig_crate_name;
293 use #orig_crate_name::*;
294 use #orig_crate_name::__staged::__deps::*;
295 use #root::prelude::*;
296 use #root::runtime_support::dfir_rs as __root_dfir_rs;
297 pub use #orig_crate_name::__staged;
298
299 #[allow(unused)]
300 fn __hydro_runtime<'a>(
301 __hydro_lang_maelstrom_meta: &'a #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::MaelstromMeta
302 )
303 -> #root::runtime_support::dfir_rs::scheduled::context::Dfir<impl #root::runtime_support::dfir_rs::scheduled::context::TickClosure + 'a>
304 {
305 #( #extra_stmts )*
306
307 #dfir_expr
308 }
309
310 #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
311 async fn main() {
312 #root::telemetry::initialize_tracing();
313
314 let __hydro_lang_maelstrom_meta = #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::maelstrom_init();
316
317 let mut #dfir_ident = __hydro_runtime(&__hydro_lang_maelstrom_meta);
318
319 __hydro_lang_maelstrom_meta.start_receiving(); let local_set = #root::runtime_support::tokio::task::LocalSet::new();
322 #(
323 let _ = local_set.spawn_local( #sidecars ); )*
325
326 let _ = local_set.run_until(#dfir_ident.run()).await;
327 }
328 }
329 }
330 };
331 source_ast
332}
333
334#[cfg(any(feature = "sim", feature = "maelstrom"))]
337pub struct ExampleBuildConfig<'a> {
338 pub trybuild: TrybuildConfig,
340 pub bin_name: String,
344 pub runtime_feature: &'a str,
347 pub example_name: String,
350 pub crate_type: Option<&'a str>,
353 pub set_trybuild_lib_name: bool,
356 pub allow_fuzz: bool,
359}
360
361#[cfg(any(feature = "sim", feature = "maelstrom"))]
363fn rustc_target_libdir() -> Option<String> {
364 static LIBDIR: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
365 LIBDIR
366 .get_or_init(|| {
367 let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
368 std::process::Command::new(rustc)
369 .args(["--print", "target-libdir"])
370 .output()
371 .ok()
372 .filter(|out| out.status.success())
373 .map(|out| String::from_utf8(out.stdout).unwrap().trim().to_owned())
374 })
375 .clone()
376}
377
378pub enum BuiltArtifact {
392 Temp(tempfile::TempPath),
394 Persisted(PathBuf),
396}
397
398impl std::ops::Deref for BuiltArtifact {
399 type Target = Path;
400
401 fn deref(&self) -> &Path {
402 match self {
403 BuiltArtifact::Temp(path) => path,
404 BuiltArtifact::Persisted(path) => path,
405 }
406 }
407}
408
409impl BuiltArtifact {
410 pub fn keep(self) -> Result<PathBuf, std::io::Error> {
413 match self {
414 BuiltArtifact::Temp(path) => path.keep().map_err(|e| e.error),
415 BuiltArtifact::Persisted(path) => Ok(path),
416 }
417 }
418}
419
420#[cfg(any(feature = "sim", feature = "maelstrom"))]
428pub fn compile_trybuild_example(config: ExampleBuildConfig<'_>) -> Result<BuiltArtifact, ()> {
429 use std::process::{Command, Stdio};
430
431 let ExampleBuildConfig {
432 trybuild,
433 bin_name,
434 runtime_feature,
435 example_name,
436 crate_type,
437 set_trybuild_lib_name,
438 allow_fuzz,
439 } = config;
440
441 let is_fuzz = allow_fuzz && std::env::var("BOLERO_FUZZER").is_ok();
442 let mut custom_rustflags = std::env::var("RUSTFLAGS").ok();
459 if std::env::var_os("LLVM_PROFILE_FILE").is_some()
460 && !custom_rustflags
461 .as_deref()
462 .is_some_and(|flags| flags.contains("instrument-coverage"))
463 {
464 let flags = custom_rustflags.get_or_insert_default();
465 if !flags.is_empty() {
466 flags.push(' ');
467 }
468 flags.push_str("-Cinstrument-coverage");
469 }
470 let has_custom_rustflags = custom_rustflags.is_some();
471 let coverage_enabled = custom_rustflags
472 .as_deref()
473 .is_some_and(|flags| flags.contains("instrument-coverage"));
474
475 let crate_to_compile = if is_fuzz {
477 trybuild.project_dir.clone()
478 } else {
479 path!(trybuild.project_dir / "dylib-examples")
480 };
481
482 let (final_target_dir, _prebuild_guard, _cargo_lock) = if !has_custom_rustflags {
483 let prebuild_span =
484 tracing::debug_span!(target: "hydro_build", "prebuild", bin_name = %bin_name).entered();
485 let shared_debug = trybuild.target_dir.join("debug");
486 let jobs_dir = trybuild.target_dir.join("jobs");
487 let per_job = hydro_concurrent_cargo::setup_job_dir(&jobs_dir, &bin_name, &shared_debug);
488
489 let mut features: Vec<String> = trybuild.features.clone().unwrap_or_default();
490 features.push(runtime_feature.to_owned());
491
492 let staged_paths = vec![
493 path!(trybuild.project_dir / "src" / "__staged.rs"),
494 path!(trybuild.project_dir / "Cargo.lock"),
495 std::env::current_exe().unwrap(),
496 ];
497
498 let project_dir = trybuild.project_dir.clone();
499 let features_for_closure = features.clone();
500 let is_fuzz_for_closure = is_fuzz;
501
502 let (guard, cargo_lock) = hydro_concurrent_cargo::run_prebuild(
503 &trybuild.target_dir,
504 trybuild.project_dir.file_name().unwrap().to_str().unwrap(),
505 &features,
506 &staged_paths,
507 |prebuild_target| {
508 let features_str = features_for_closure.join(",");
509
510 let prebuild_crate = if is_fuzz_for_closure {
522 project_dir.clone()
523 } else {
524 path!(project_dir / "dylib-examples")
525 };
526 let mut lib_cmd = Command::new("cargo");
527 lib_cmd.current_dir(&prebuild_crate);
528 lib_cmd.args(["build", "--locked", "--lib"]);
529 lib_cmd.args(["--target-dir", prebuild_target.to_str().unwrap()]);
530 lib_cmd.arg("--no-default-features");
531 lib_cmd.args(["--features", &features_str]);
532 lib_cmd.args(["--config", "build.incremental = false"]);
533 lib_cmd.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
534 let status = lib_cmd.stdin(Stdio::null()).status().unwrap();
535 if !status.success() {
536 panic!("dep prebuild failed");
537 }
538 },
539 );
540
541 drop(prebuild_span);
545 (per_job, Some(guard), Some(cargo_lock))
546 } else {
547 let target_dir = if coverage_enabled {
552 path!(trybuild.target_dir / "coverage")
553 } else {
554 trybuild.target_dir.clone()
555 };
556 (target_dir, None, None)
557 };
558
559 let _job_build_guard = if !has_custom_rustflags {
561 let populate_span =
562 tracing::debug_span!(target: "hydro_build", "populate_job_dir", bin_name = %bin_name)
563 .entered();
564 let shared_debug = trybuild.target_dir.join("debug");
565 let guard = hydro_concurrent_cargo::populate_job_build_dir(
566 &final_target_dir.join("debug"),
567 &shared_debug,
568 );
569 drop(populate_span);
572 Some(guard)
573 } else {
574 None
575 };
576
577 let final_build_span =
578 tracing::debug_span!(target: "hydro_build", "final_build", bin_name = %bin_name).entered();
579 let mut command = Command::new("cargo");
580 command.current_dir(&crate_to_compile);
581 command.args([
582 "rustc",
583 if has_custom_rustflags {
584 "--locked"
585 } else {
586 "--frozen"
587 },
588 ]);
589 command.args(["--example", &example_name]);
590 command.args(["--target-dir", final_target_dir.to_str().unwrap()]);
591 command.arg("--no-default-features");
598 command.args([
599 "--features",
600 &trybuild
601 .features
602 .clone()
603 .into_iter()
604 .flatten()
605 .chain([runtime_feature.to_owned()])
606 .collect::<Vec<_>>()
607 .join(","),
608 ]);
609 command.args(["--config", "build.incremental = false"]);
610 if let Some(flags) = &custom_rustflags {
611 command.env("RUSTFLAGS", flags);
617 }
618 if let Some(crate_type) = crate_type {
619 command.args(["--crate-type", crate_type]);
620 }
621 command.arg("--message-format=json-diagnostic-rendered-ansi");
622 command.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
623 if set_trybuild_lib_name {
624 command.env("TRYBUILD_LIB_NAME", &bin_name);
625 }
626
627 command.arg("--");
628
629 if cfg!(any(target_os = "linux", target_os = "macos")) {
630 let debug_path = if let Ok(target) = std::env::var("CARGO_BUILD_TARGET") {
631 path!(final_target_dir / target / "debug")
632 } else {
633 path!(final_target_dir / "debug")
634 };
635
636 let mut rpaths = vec![debug_path.clone(), path!(debug_path / "deps")];
640 if let Some(libdir) = rustc_target_libdir() {
641 rpaths.push(PathBuf::from(libdir));
642 }
643 for rpath in rpaths {
644 if cfg!(target_os = "macos") {
645 command.args([
649 "-Clink-arg=-rpath".to_owned(),
650 format!("-Clink-arg={}", rpath.to_str().unwrap()),
651 ]);
652 } else {
653 command.args([format!("-Clink-arg=-Wl,-rpath,{}", rpath.to_str().unwrap())]);
654 }
655 }
656
657 if cfg!(all(target_os = "linux", target_env = "gnu")) {
658 command.arg(
659 "-Clink-args=-Wl,-z,nodelete",
661 );
662 }
663
664 if coverage_enabled && cfg!(target_os = "linux") {
665 command.arg("-Clink-arg=-Wl,--disable-new-dtags");
672 }
673 }
674
675 if allow_fuzz && let Ok(fuzzer) = std::env::var("BOLERO_FUZZER") {
676 command.env_remove("BOLERO_FUZZER");
677
678 if fuzzer == "libfuzzer" {
679 #[cfg(target_os = "macos")]
680 {
681 command.args(["-Clink-arg=-undefined", "-Clink-arg=dynamic_lookup"]);
682 }
683
684 #[cfg(target_os = "linux")]
685 {
686 command.args(["-Clink-arg=-Wl,--unresolved-symbols=ignore-all"]);
687 }
688 }
689 }
690
691 tracing::debug!(
692 target: "hydro_build",
693 "final build command (cwd={}): {:?}",
694 crate_to_compile.display(),
695 command
696 );
697
698 let mut spawned = command
699 .stdout(Stdio::piped())
700 .stderr(Stdio::piped())
701 .stdin(Stdio::null())
702 .spawn()
703 .unwrap();
704 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
705 let stderr_handle = spawned.stderr.take().unwrap();
706 let stderr_thread = std::thread::spawn(move || {
707 use std::io::Read;
708 let mut buf = String::new();
709 std::io::BufReader::new(stderr_handle)
710 .read_to_string(&mut buf)
711 .unwrap();
712 buf
713 });
714
715 let mut out = Err(());
716 for message in cargo_metadata::Message::parse_stream(reader) {
717 match message.unwrap() {
718 cargo_metadata::Message::CompilerArtifact(artifact) => {
719 let is_output = artifact.target.is_example();
721
722 if is_output {
723 let path = artifact.filenames.first().unwrap();
724 let path_buf: PathBuf = path.clone().into();
725 out = Ok(path_buf);
726 }
727 }
728 cargo_metadata::Message::CompilerMessage(mut msg) => {
729 if let Some(rendered) = msg.message.rendered.as_mut() {
732 let file_names = msg
733 .message
734 .spans
735 .iter()
736 .map(|s| &s.file_name)
737 .collect::<std::collections::BTreeSet<_>>();
738 for file_name in file_names {
739 *rendered = rendered.replace(
740 file_name,
741 &format!("(full path) {}/{file_name}", trybuild.project_dir.display()),
742 )
743 }
744 }
745 eprintln!("{}", msg.message);
746 }
747 cargo_metadata::Message::TextLine(line) => {
748 eprintln!("{}", line);
749 }
750 cargo_metadata::Message::BuildFinished(_) => {}
751 cargo_metadata::Message::BuildScriptExecuted(_) => {}
752 msg => panic!("Unexpected message type: {:?}", msg),
753 }
754 }
755
756 spawned.wait().unwrap();
757 let stderr_output = stderr_thread.join().unwrap();
758 drop(final_build_span);
759
760 if !has_custom_rustflags {
763 for line in stderr_output.lines() {
764 if line.contains("Compiling") && !line.contains("dylib-examples") {
765 panic!(
766 "unexpected recompilation in final build: {line}\nfull stderr:\n{stderr_output}"
767 );
768 }
769 }
770 }
771
772 if out.is_err() {
773 panic!("final build failed to produce binary.\nstderr:\n{stderr_output}");
774 }
775
776 if coverage_enabled {
777 let coverage_dir = path!(trybuild.target_dir / "debug" / "deps" / "hydro-coverage");
788 fs::create_dir_all(&coverage_dir).unwrap();
789 let artifact_name = out.as_ref().unwrap().file_name().unwrap().to_str().unwrap();
790 let persisted = path!(coverage_dir / format!("{bin_name}-{artifact_name}"));
791 let staging = tempfile::NamedTempFile::new_in(&coverage_dir).unwrap();
794 fs::copy(out.as_ref().unwrap(), staging.path()).unwrap();
795 staging.persist(&persisted).unwrap();
796 return Ok(BuiltArtifact::Persisted(persisted));
797 }
798
799 let out_file = tempfile::NamedTempFile::new().unwrap().into_temp_path();
800 fs::copy(out.as_ref().unwrap(), &out_file).unwrap();
801 Ok(BuiltArtifact::Temp(out_file))
802}
803
804pub(crate) fn write_staged_source_cached(source_dir: &Path, crate_name: &str, project_dir: &Path) {
817 let _span = tracing::debug_span!(target: "hydro_build", "gen_staged").entered();
818
819 let staged_path = path!(project_dir / "src" / "__staged.rs");
820 let stamp_path = path!(project_dir / ".hydro-staged-stamp");
821
822 let exe_stamp = std::env::current_exe().ok().and_then(|exe| {
823 let mtime = fs::metadata(&exe)
824 .ok()?
825 .modified()
826 .ok()?
827 .duration_since(std::time::SystemTime::UNIX_EPOCH)
828 .ok()?
829 .as_nanos();
830 Some(format!("{}\t{}", exe.display(), mtime))
831 });
832
833 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
834
835 fs::create_dir_all(path!(project_dir / "src")).unwrap();
836
837 let mut stamp_file = File::options()
841 .read(true)
842 .write(true)
843 .create(true)
844 .truncate(false)
845 .open(&stamp_path)
846 .unwrap();
847 stamp_file.lock().unwrap();
848
849 let mut existing_stamp = String::new();
850 if stamp_file.read_to_string(&mut existing_stamp).is_err() {
851 existing_stamp.clear();
852 }
853
854 if let Some(stamp) = &exe_stamp
855 && existing_stamp == *stamp
856 && staged_path.exists()
857 {
858 return;
859 }
860
861 let raw_toml_manifest = toml::from_str::<toml::Value>(
862 &fs::read_to_string(path!(source_dir / "Cargo.toml")).unwrap(),
863 )
864 .unwrap();
865
866 let maybe_custom_lib_path = raw_toml_manifest
867 .get("lib")
868 .and_then(|lib| lib.get("path"))
869 .and_then(|path| path.as_str());
870
871 let mut gen_staged = stageleft_tool::gen_staged_trybuild(
872 &maybe_custom_lib_path
873 .map(|s| path!(source_dir / s))
874 .unwrap_or_else(|| path!(source_dir / "src" / "lib.rs")),
875 &path!(source_dir / "Cargo.toml"),
876 crate_name,
877 Some("hydro___test".to_owned()),
878 );
879
880 gen_staged.attrs.insert(
881 0,
882 syn::parse_quote! {
883 #![allow(
884 unused,
885 ambiguous_glob_reexports,
886 clippy::suspicious_else_formatting,
887 unexpected_cfgs,
888 reason = "generated code"
889 )]
890 },
891 );
892
893 let inlined_staged = prettyplease::unparse(&gen_staged);
894
895 write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
896
897 if let Some(stamp) = exe_stamp {
898 stamp_file.set_len(0).unwrap();
899 stamp_file.seek(SeekFrom::Start(0)).unwrap();
900 stamp_file.write_all(stamp.as_bytes()).unwrap();
901 }
902}
903
904pub fn create_trybuild()
905-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
906 let _span = tracing::debug_span!(target: "hydro_build", "create_trybuild").entered();
907 let Metadata {
908 target_directory: target_dir,
909 workspace_root: workspace,
910 packages,
911 } = {
912 let _span = tracing::debug_span!(target: "hydro_build", "cargo_metadata").entered();
913 cargo::metadata()?
914 };
915
916 let source_dir = cargo::manifest_dir()?;
917 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
918
919 let mut dev_dependency_features = vec![];
920 source_manifest.dev_dependencies.retain(|k, v| {
921 if source_manifest.dependencies.contains_key(k) {
922 for feat in &v.features {
924 dev_dependency_features.push(format!("{}/{}", k, feat));
925 }
926
927 false
928 } else {
929 dev_dependency_features.push(format!("dep:{k}"));
931
932 v.optional = true;
933 true
934 }
935 });
936
937 let mut features = if std::env::var("RUNNING_AS_EXAMPLE_TEST").is_ok_and(|v| v == "1") {
943 None
944 } else {
945 features::find()
946 };
947
948 let path_dependencies = source_manifest
949 .dependencies
950 .iter()
951 .filter_map(|(name, dep)| {
952 let path = dep.path.as_ref()?;
953 if packages.iter().any(|p| &p.name == name) {
954 None
956 } else {
957 Some(PathDependency {
958 name: name.clone(),
959 normalized_path: path.canonicalize().ok()?,
960 })
961 }
962 })
963 .collect();
964
965 let crate_name = source_manifest.package.name.clone();
966 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
967 fs::create_dir_all(&project_dir)?;
968
969 let project_name = format!("{}-hydro-trybuild", crate_name);
970 let mut manifest = Runner::make_manifest(
971 &workspace,
972 &project_name,
973 &source_dir,
974 &packages,
975 &[],
976 source_manifest,
977 )?;
978
979 if let Some(enabled_features) = &mut features {
980 enabled_features
981 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
982 }
983
984 for runtime_feature in HYDRO_RUNTIME_FEATURES {
985 manifest.features.insert(
986 format!("hydro___feature_{runtime_feature}"),
987 vec![format!("hydro_lang/{runtime_feature}")],
988 );
989 }
990
991 manifest
992 .dependencies
993 .get_mut("hydro_lang")
994 .unwrap()
995 .features
996 .push("runtime_support".to_owned());
997
998 manifest
999 .features
1000 .insert("hydro___test".to_owned(), dev_dependency_features);
1001
1002 if manifest
1003 .workspace
1004 .as_ref()
1005 .is_some_and(|w| w.dependencies.is_empty())
1006 {
1007 manifest.workspace = None;
1008 }
1009
1010 let project = Project {
1011 dir: project_dir,
1012 source_dir,
1013 target_dir,
1014 name: project_name.clone(),
1015 update: Update::env()?,
1016 has_pass: false,
1017 has_compile_fail: false,
1018 features,
1019 workspace,
1020 path_dependencies,
1021 manifest,
1022 keep_going: false,
1023 };
1024
1025 {
1026 let _span = tracing::debug_span!(target: "hydro_build", "write_project_files").entered();
1027 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
1028
1029 let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
1030 project_lock.lock()?;
1031
1032 fs::create_dir_all(path!(project.dir / "src"))?;
1033 fs::create_dir_all(path!(project.dir / "examples"))?;
1034
1035 let crate_name_ident = syn::Ident::new(
1036 &crate_name.replace("-", "_"),
1037 proc_macro2::Span::call_site(),
1038 );
1039
1040 write_atomic(
1041 prettyplease::unparse(&syn::parse_quote! {
1042 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1043
1044 pub mod __root {
1045 pub use #crate_name_ident::*;
1046 #[cfg(feature = "hydro___test")]
1047 pub use super::__staged;
1048 }
1049
1050 #[cfg(feature = "hydro___test")]
1051 pub mod __staged;
1052 })
1053 .as_bytes(),
1054 &path!(project.dir / "src" / "lib.rs"),
1055 )
1056 .unwrap();
1057
1058 let base_manifest = toml::to_string(&project.manifest)?;
1059
1060 let feature_names: Vec<_> = project.manifest.features.keys().cloned().collect();
1062
1063 let dylib_dir = path!(project.dir / "dylib");
1065 fs::create_dir_all(path!(dylib_dir / "src"))?;
1066
1067 let trybuild_crate_name_ident = syn::Ident::new(
1068 &project_name.replace("-", "_"),
1069 proc_macro2::Span::call_site(),
1070 );
1071 write_atomic(
1072 [
1078 "// v2: dylib must be built as a dependency (prefer-dynamic).\n".as_bytes(),
1079 prettyplease::unparse(&syn::parse_quote! {
1080 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1081 pub use #trybuild_crate_name_ident::*;
1082 })
1083 .as_bytes(),
1084 ]
1085 .concat()
1086 .as_slice(),
1087 &path!(dylib_dir / "src" / "lib.rs"),
1088 )?;
1089
1090 let serialized_edition = toml::to_string(
1091 &vec![("edition", &project.manifest.package.edition)]
1092 .into_iter()
1093 .collect::<std::collections::HashMap<_, _>>(),
1094 )
1095 .unwrap();
1096
1097 let dylib_features_section = feature_names
1100 .iter()
1101 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
1102 .collect::<Vec<_>>()
1103 .join("\n");
1104
1105 let dylib_manifest = format!(
1106 r#"[package]
1107name = "{project_name}-dylib"
1108version = "0.0.0"
1109{}
1110
1111[lib]
1112crate-type = ["{}"]
1113
1114[dependencies]
1115{project_name} = {{ path = "..", default-features = false }}
1116
1117[features]
1118{dylib_features_section}
1119"#,
1120 serialized_edition,
1121 if cfg!(target_os = "windows") {
1122 "rlib"
1123 } else {
1124 "dylib"
1125 }
1126 );
1127 write_atomic(dylib_manifest.as_ref(), &path!(dylib_dir / "Cargo.toml"))?;
1128
1129 let dylib_examples_dir = path!(project.dir / "dylib-examples");
1130 fs::create_dir_all(path!(dylib_examples_dir / "src"))?;
1131 fs::create_dir_all(path!(dylib_examples_dir / "examples"))?;
1132
1133 write_atomic(
1134 b"#![allow(unused_crate_dependencies)]\n",
1135 &path!(dylib_examples_dir / "src" / "lib.rs"),
1136 )?;
1137
1138 let features_section = feature_names
1140 .iter()
1141 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
1142 .collect::<Vec<_>>()
1143 .join("\n");
1144
1145 let dylib_examples_manifest = format!(
1157 r#"[package]
1158name = "{project_name}-dylib-examples"
1159version = "0.0.0"
1160{}
1161
1162[dependencies]
1163{project_name} = {{ package = "{project_name}-dylib", path = "../dylib", default-features = false }}
1164
1165[features]
1166{features_section}
1167
1168[[example]]
1169name = "sim-dylib"
1170crate-type = ["cdylib"]
1171"#,
1172 serialized_edition
1173 );
1174 write_atomic(
1175 dylib_examples_manifest.as_ref(),
1176 &path!(dylib_examples_dir / "Cargo.toml"),
1177 )?;
1178
1179 let sim_dylib_contents = prettyplease::unparse(&syn::parse_quote! {
1181 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1182 include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
1183 });
1184 write_atomic(
1185 sim_dylib_contents.as_bytes(),
1186 &path!(project.dir / "examples" / "sim-dylib.rs"),
1187 )?;
1188 write_atomic(
1189 sim_dylib_contents.as_bytes(),
1190 &path!(dylib_examples_dir / "examples" / "sim-dylib.rs"),
1191 )?;
1192
1193 let workspace_manifest = format!(
1194 r#"{}
1195[[example]]
1196name = "sim-dylib"
1197crate-type = ["cdylib"]
1198
1199[workspace]
1200members = ["dylib", "dylib-examples"]
1201"#,
1202 base_manifest,
1203 );
1204
1205 write_atomic(
1206 workspace_manifest.as_ref(),
1207 &path!(project.dir / "Cargo.toml"),
1208 )?;
1209
1210 let manifest_hash = {
1213 let mut hasher = Sha256::new();
1214 hasher.update(&workspace_manifest);
1215 hasher.update(&dylib_manifest);
1216 hasher.update(&dylib_examples_manifest);
1217 format!("{:X}", hasher.finalize())
1218 .chars()
1219 .take(8)
1220 .collect::<String>()
1221 };
1222
1223 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
1224 let workspace_cargo_lock_contents_and_hash = if workspace_cargo_lock.exists() {
1225 let cargo_lock_contents = fs::read_to_string(&workspace_cargo_lock)?;
1226
1227 let hash = format!("{:X}", Sha256::digest(&cargo_lock_contents))
1228 .chars()
1229 .take(8)
1230 .collect::<String>();
1231
1232 Some((cargo_lock_contents, hash))
1233 } else {
1234 None
1235 };
1236
1237 let trybuild_hash = format!(
1238 "{}-{}",
1239 manifest_hash,
1240 workspace_cargo_lock_contents_and_hash
1241 .as_ref()
1242 .map(|(_contents, hash)| &**hash)
1243 .unwrap_or_default()
1244 );
1245
1246 if !check_contents(
1247 trybuild_hash.as_bytes(),
1248 &path!(project.dir / ".hydro-trybuild-manifest"),
1249 )
1250 .is_ok_and(|b| b)
1251 {
1252 let _span = tracing::debug_span!(target: "hydro_build", "update_lockfile").entered();
1253 if let Some((cargo_lock_contents, _)) = workspace_cargo_lock_contents_and_hash {
1255 write_atomic(
1258 cargo_lock_contents.as_ref(),
1259 &path!(project.dir / "Cargo.lock"),
1260 )?;
1261 } else {
1262 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
1263 }
1264
1265 std::process::Command::new("cargo")
1267 .current_dir(&project.dir)
1268 .args(["update", "-w"]) .stdout(std::process::Stdio::null())
1270 .stderr(std::process::Stdio::null())
1271 .status()
1272 .unwrap();
1273
1274 write_atomic(
1275 trybuild_hash.as_bytes(),
1276 &path!(project.dir / ".hydro-trybuild-manifest"),
1277 )?;
1278 }
1279
1280 let examples_folder = path!(project.dir / "examples");
1282 fs::create_dir_all(&examples_folder)?;
1283
1284 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
1285 if workspace_dot_cargo_config_toml.exists() {
1286 let dot_cargo_folder = path!(project.dir / ".cargo");
1287 fs::create_dir_all(&dot_cargo_folder)?;
1288
1289 write_atomic(
1290 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
1291 &path!(dot_cargo_folder / "config.toml"),
1292 )?;
1293 }
1294
1295 let vscode_folder = path!(project.dir / ".vscode");
1296 fs::create_dir_all(&vscode_folder)?;
1297 write_atomic(
1298 include_bytes!("./vscode-trybuild.json"),
1299 &path!(vscode_folder / "settings.json"),
1300 )?;
1301 }
1302
1303 Ok((
1304 project.dir.as_ref().into(),
1305 project.target_dir.as_ref().into(),
1306 project.features,
1307 ))
1308}
1309
1310fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
1311 let mut file = File::options()
1312 .read(true)
1313 .write(false)
1314 .create(false)
1315 .truncate(false)
1316 .open(path)?;
1317 file.lock()?;
1318
1319 let mut existing_contents = Vec::new();
1320 file.read_to_end(&mut existing_contents)?;
1321 Ok(existing_contents == contents)
1322}
1323
1324pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
1325 let mut file = File::options()
1326 .read(true)
1327 .write(true)
1328 .create(true)
1329 .truncate(false)
1330 .open(path)?;
1331
1332 let mut existing_contents = Vec::new();
1333 file.read_to_end(&mut existing_contents)?;
1334 if existing_contents != contents {
1335 file.lock()?;
1336 file.seek(SeekFrom::Start(0))?;
1337 file.set_len(0)?;
1338 file.write_all(contents)?;
1339 }
1340
1341 Ok(())
1342}