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::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 as_code_options: &AsCodeOptions,
121 bin_name_prefix: Option<&str>,
122 deploy_mode: DeployMode,
123 linking_mode: LinkingMode,
124) -> (String, TrybuildConfig) {
125 let source_dir = cargo::manifest_dir().unwrap();
126 let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
127 let crate_name = source_manifest.package.name.replace("-", "_");
128
129 let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
130
131 let generated_code = {
132 let _span = tracing::debug_span!(target: "hydro_build", "graph_codegen").entered();
133 compile_graph_trybuild(
134 graph,
135 extra_stmts,
136 sidecars,
137 as_code_options,
138 &crate_name,
139 deploy_mode,
140 )
141 };
142
143 let source = {
144 let _span = tracing::debug_span!(target: "hydro_build", "unparse_source").entered();
145 prettyplease::unparse(&generated_code)
146 };
147
148 let hash = format!("{:X}", Sha256::digest(&source))
149 .chars()
150 .take(8)
151 .collect::<String>();
152
153 let bin_name = if let Some(bin_name_prefix) = bin_name_prefix {
154 format!("{}_{}", clean_bin_name_prefix(bin_name_prefix), hash)
155 } else {
156 hash
157 };
158
159 let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
160
161 let examples_dir = match linking_mode {
163 LinkingMode::Static => path!(project_dir / "examples"),
164 #[cfg(any(feature = "deploy", feature = "maelstrom"))]
165 LinkingMode::Dynamic => path!(project_dir / "dylib-examples" / "examples"),
166 };
167
168 fs::create_dir_all(&examples_dir).unwrap();
170
171 let out_path = path!(examples_dir / format!("{bin_name}.rs"));
172 {
173 let _span =
174 tracing::debug_span!(target: "hydro_build", "write_generated_sources").entered();
175 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
176 write_atomic(source.as_ref(), &out_path).unwrap();
177 }
178
179 if is_test {
180 write_staged_source_cached(source_dir.as_ref(), &crate_name, &project_dir);
181 }
182
183 if is_test {
184 if cur_bin_enabled_features.is_none() {
185 cur_bin_enabled_features = Some(vec![]);
186 }
187
188 cur_bin_enabled_features
189 .as_mut()
190 .unwrap()
191 .push("hydro___test".to_owned());
192 }
193
194 (
195 bin_name,
196 TrybuildConfig {
197 project_dir,
198 target_dir,
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 if !status.success() {
584 panic!("dep prebuild failed");
585 }
586 },
587 );
588
589 drop(prebuild_span);
593 (per_job, Some(guard), Some(cargo_lock))
594 } else {
595 (trybuild.target_dir.clone(), None, None)
596 };
597
598 let _job_build_guard = if use_prebuild {
600 let populate_span =
601 tracing::debug_span!(target: "hydro_build", "populate_job_dir", bin_name = %bin_name)
602 .entered();
603 let shared_debug = trybuild.target_dir.join("debug");
604 let guard = hydro_concurrent_cargo::populate_job_build_dir(
605 &final_target_dir.join("debug"),
606 &shared_debug,
607 );
608 drop(populate_span);
611 Some(guard)
612 } else {
613 None
614 };
615
616 let final_build_span =
617 tracing::debug_span!(target: "hydro_build", "final_build", bin_name = %bin_name).entered();
618 let mut command = Command::new("cargo");
619 command.current_dir(&crate_to_compile);
620 command.args(["rustc", if use_prebuild { "--frozen" } else { "--locked" }]);
621 command.args(["--example", &example_name]);
622 command.args(["--target-dir", final_target_dir.to_str().unwrap()]);
623 command.arg("--no-default-features");
630 command.args([
631 "--features",
632 &trybuild
633 .features
634 .clone()
635 .into_iter()
636 .flatten()
637 .chain([runtime_feature.to_owned()])
638 .collect::<Vec<_>>()
639 .join(","),
640 ]);
641 command.args(["--config", "build.incremental = false"]);
642 forward_rustflags(&mut command);
643 if let Some(crate_type) = crate_type {
644 command.args(["--crate-type", crate_type]);
645 }
646 command.arg("--message-format=json-diagnostic-rendered-ansi");
647 command.env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
648 if set_trybuild_lib_name {
649 command.env("TRYBUILD_LIB_NAME", &bin_name);
650 }
651
652 command.arg("--");
653
654 if cfg!(any(target_os = "linux", target_os = "macos")) {
655 let debug_path = if let Ok(target) = std::env::var("CARGO_BUILD_TARGET") {
656 path!(final_target_dir / target / "debug")
657 } else {
658 path!(final_target_dir / "debug")
659 };
660
661 let mut rpaths = vec![debug_path.clone(), path!(debug_path / "deps")];
665 if let Some(libdir) = rustc_target_libdir() {
666 rpaths.push(PathBuf::from(libdir));
667 }
668 for rpath in rpaths {
669 if cfg!(target_os = "macos") {
670 command.args([
674 "-Clink-arg=-rpath".to_owned(),
675 format!("-Clink-arg={}", rpath.to_str().unwrap()),
676 ]);
677 } else {
678 command.args([format!("-Clink-arg=-Wl,-rpath,{}", rpath.to_str().unwrap())]);
679 }
680 }
681
682 if cfg!(all(target_os = "linux", target_env = "gnu")) {
683 command.arg(
684 "-Clink-args=-Wl,-z,nodelete",
686 );
687 }
688 }
689
690 if allow_fuzz && let Ok(fuzzer) = std::env::var("BOLERO_FUZZER") {
691 command.env_remove("BOLERO_FUZZER");
692
693 if fuzzer == "libfuzzer" {
694 #[cfg(target_os = "macos")]
695 {
696 command.args(["-Clink-arg=-undefined", "-Clink-arg=dynamic_lookup"]);
697 }
698
699 #[cfg(target_os = "linux")]
700 {
701 command.args(["-Clink-arg=-Wl,--unresolved-symbols=ignore-all"]);
702 }
703 }
704 }
705
706 tracing::debug!(
707 target: "hydro_build",
708 "final build command (cwd={}): {:?}",
709 crate_to_compile.display(),
710 command
711 );
712
713 let mut spawned = command
714 .stdout(Stdio::piped())
715 .stderr(Stdio::piped())
716 .stdin(Stdio::null())
717 .spawn()
718 .unwrap();
719 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
720 let stderr_handle = spawned.stderr.take().unwrap();
721 let stderr_thread = std::thread::spawn(move || {
722 use std::io::Read;
723 let mut buf = String::new();
724 std::io::BufReader::new(stderr_handle)
725 .read_to_string(&mut buf)
726 .unwrap();
727 buf
728 });
729
730 let mut out = Err(());
731 for message in cargo_metadata::Message::parse_stream(reader) {
732 match message.unwrap() {
733 cargo_metadata::Message::CompilerArtifact(artifact) => {
734 let is_output = artifact.target.is_example();
736
737 if is_output {
738 let path = artifact.filenames.first().unwrap();
739 let path_buf: PathBuf = path.clone().into();
740 out = Ok(path_buf);
741 }
742 }
743 cargo_metadata::Message::CompilerMessage(mut msg) => {
744 if let Some(rendered) = msg.message.rendered.as_mut() {
747 let file_names = msg
748 .message
749 .spans
750 .iter()
751 .map(|s| &s.file_name)
752 .collect::<std::collections::BTreeSet<_>>();
753 for file_name in file_names {
754 *rendered = rendered.replace(
755 file_name,
756 &format!("(full path) {}/{file_name}", trybuild.project_dir.display()),
757 )
758 }
759 }
760 eprintln!("{}", msg.message);
761 }
762 cargo_metadata::Message::TextLine(line) => {
763 eprintln!("{}", line);
764 }
765 cargo_metadata::Message::BuildFinished(_) => {}
766 cargo_metadata::Message::BuildScriptExecuted(_) => {}
767 msg => panic!("Unexpected message type: {:?}", msg),
768 }
769 }
770
771 spawned.wait().unwrap();
772 let stderr_output = stderr_thread.join().unwrap();
773 drop(final_build_span);
774
775 if use_prebuild {
778 for line in stderr_output.lines() {
779 if line.contains("Compiling") && !line.contains("dylib-examples") {
780 panic!(
781 "unexpected recompilation in final build: {line}\nfull stderr:\n{stderr_output}"
782 );
783 }
784 }
785 }
786
787 if out.is_err() {
788 panic!("final build failed to produce binary.\nstderr:\n{stderr_output}");
789 }
790
791 if coverage_enabled {
792 let coverage_dir = path!(trybuild.target_dir / "debug" / "deps" / "hydro-coverage");
805 fs::create_dir_all(&coverage_dir).unwrap();
806 let artifact_name = out.as_ref().unwrap().file_name().unwrap().to_str().unwrap();
807 let fingerprint = hydro_concurrent_cargo::toolchain_fingerprint(ENCODED_RUSTFLAGS);
808 let persisted = path!(coverage_dir / format!("{bin_name}-{fingerprint}-{artifact_name}"));
809 let staging = tempfile::NamedTempFile::new_in(&coverage_dir).unwrap();
812 fs::copy(out.as_ref().unwrap(), staging.path()).unwrap();
813 staging.persist(&persisted).unwrap();
814 return Ok(BuiltArtifact::Persisted(persisted));
815 }
816
817 let out_file = tempfile::NamedTempFile::new().unwrap().into_temp_path();
818 fs::copy(out.as_ref().unwrap(), &out_file).unwrap();
819 Ok(BuiltArtifact::Temp(out_file))
820}
821
822pub(crate) fn write_staged_source_cached(source_dir: &Path, crate_name: &str, project_dir: &Path) {
835 let _span = tracing::debug_span!(target: "hydro_build", "gen_staged").entered();
836
837 let staged_path = path!(project_dir / "src" / "__staged.rs");
838 let stamp_path = path!(project_dir / ".hydro-staged-stamp");
839
840 let exe_stamp = std::env::current_exe().ok().and_then(|exe| {
841 let mtime = fs::metadata(&exe)
842 .ok()?
843 .modified()
844 .ok()?
845 .duration_since(std::time::SystemTime::UNIX_EPOCH)
846 .ok()?
847 .as_nanos();
848 Some(format!("{}\t{}", exe.display(), mtime))
849 });
850
851 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
852
853 fs::create_dir_all(path!(project_dir / "src")).unwrap();
854
855 let mut stamp_file = File::options()
859 .read(true)
860 .write(true)
861 .create(true)
862 .truncate(false)
863 .open(&stamp_path)
864 .unwrap();
865 stamp_file.lock().unwrap();
866
867 let mut existing_stamp = String::new();
868 if stamp_file.read_to_string(&mut existing_stamp).is_err() {
869 existing_stamp.clear();
870 }
871
872 if let Some(stamp) = &exe_stamp
873 && existing_stamp == *stamp
874 && staged_path.exists()
875 {
876 return;
877 }
878
879 let raw_toml_manifest = toml::from_str::<toml::Value>(
880 &fs::read_to_string(path!(source_dir / "Cargo.toml")).unwrap(),
881 )
882 .unwrap();
883
884 let maybe_custom_lib_path = raw_toml_manifest
885 .get("lib")
886 .and_then(|lib| lib.get("path"))
887 .and_then(|path| path.as_str());
888
889 let mut gen_staged = stageleft_tool::gen_staged_trybuild(
890 &maybe_custom_lib_path
891 .map(|s| path!(source_dir / s))
892 .unwrap_or_else(|| path!(source_dir / "src" / "lib.rs")),
893 &path!(source_dir / "Cargo.toml"),
894 crate_name,
895 Some("hydro___test".to_owned()),
896 );
897
898 gen_staged.attrs.insert(
899 0,
900 syn::parse_quote! {
901 #![allow(
902 unused,
903 ambiguous_glob_reexports,
904 clippy::suspicious_else_formatting,
905 unexpected_cfgs,
906 reason = "generated code"
907 )]
908 },
909 );
910
911 let inlined_staged = prettyplease::unparse(&gen_staged);
912
913 write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
914
915 if let Some(stamp) = exe_stamp {
916 stamp_file.set_len(0).unwrap();
917 stamp_file.seek(SeekFrom::Start(0)).unwrap();
918 stamp_file.write_all(stamp.as_bytes()).unwrap();
919 }
920}
921
922pub fn create_trybuild()
923-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
924 let _span = tracing::debug_span!(target: "hydro_build", "create_trybuild").entered();
925 let Metadata {
926 target_directory: target_dir,
927 workspace_root: workspace,
928 packages,
929 } = {
930 let _span = tracing::debug_span!(target: "hydro_build", "cargo_metadata").entered();
931 cargo::metadata()?
932 };
933
934 let source_dir = cargo::manifest_dir()?;
935 let mut source_manifest = dependencies::get_manifest(&source_dir)?;
936
937 let mut dev_dependency_features = vec![];
938 source_manifest.dev_dependencies.retain(|k, v| {
939 if source_manifest.dependencies.contains_key(k) {
940 for feat in &v.features {
942 dev_dependency_features.push(format!("{}/{}", k, feat));
943 }
944
945 false
946 } else {
947 dev_dependency_features.push(format!("dep:{k}"));
949
950 v.optional = true;
951 true
952 }
953 });
954
955 let mut features = if std::env::var("RUNNING_AS_EXAMPLE_TEST").is_ok_and(|v| v == "1") {
961 None
962 } else {
963 features::find()
964 };
965
966 let path_dependencies = source_manifest
967 .dependencies
968 .iter()
969 .filter_map(|(name, dep)| {
970 let path = dep.path.as_ref()?;
971 if packages.iter().any(|p| &p.name == name) {
972 None
974 } else {
975 Some(PathDependency {
976 name: name.clone(),
977 normalized_path: path.canonicalize().ok()?,
978 })
979 }
980 })
981 .collect();
982
983 let crate_name = source_manifest.package.name.clone();
984 let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
985 fs::create_dir_all(&project_dir)?;
986
987 let project_name = format!("{}-hydro-trybuild", crate_name);
988 let mut manifest = Runner::make_manifest(
989 &workspace,
990 &project_name,
991 &source_dir,
992 &packages,
993 &[],
994 source_manifest,
995 )?;
996
997 if let Some(enabled_features) = &mut features {
998 enabled_features
999 .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
1000 }
1001
1002 for runtime_feature in HYDRO_RUNTIME_FEATURES {
1003 manifest.features.insert(
1004 format!("hydro___feature_{runtime_feature}"),
1005 vec![format!("hydro_lang/{runtime_feature}")],
1006 );
1007 }
1008
1009 manifest
1010 .dependencies
1011 .get_mut("hydro_lang")
1012 .unwrap()
1013 .features
1014 .push("runtime_support".to_owned());
1015
1016 manifest
1017 .features
1018 .insert("hydro___test".to_owned(), dev_dependency_features);
1019
1020 if manifest
1021 .workspace
1022 .as_ref()
1023 .is_some_and(|w| w.dependencies.is_empty())
1024 {
1025 manifest.workspace = None;
1026 }
1027
1028 let project = Project {
1029 dir: project_dir,
1030 source_dir,
1031 target_dir,
1032 name: project_name.clone(),
1033 update: Update::env()?,
1034 has_pass: false,
1035 has_compile_fail: false,
1036 features,
1037 workspace,
1038 path_dependencies,
1039 manifest,
1040 keep_going: false,
1041 };
1042
1043 {
1044 let _span = tracing::debug_span!(target: "hydro_build", "write_project_files").entered();
1045 let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
1046
1047 let _project_lock = hydro_concurrent_cargo::lock_project(project.dir.as_ref());
1048
1049 fs::create_dir_all(path!(project.dir / "src"))?;
1050 fs::create_dir_all(path!(project.dir / "examples"))?;
1051
1052 let crate_name_ident = syn::Ident::new(
1053 &crate_name.replace("-", "_"),
1054 proc_macro2::Span::call_site(),
1055 );
1056
1057 write_atomic(
1058 prettyplease::unparse(&syn::parse_quote! {
1059 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1060
1061 pub mod __root {
1062 pub use #crate_name_ident::*;
1063 #[cfg(feature = "hydro___test")]
1064 pub use super::__staged;
1065 }
1066
1067 #[cfg(feature = "hydro___test")]
1068 pub mod __staged;
1069 })
1070 .as_bytes(),
1071 &path!(project.dir / "src" / "lib.rs"),
1072 )
1073 .unwrap();
1074
1075 let base_manifest = toml::to_string(&project.manifest)?;
1076
1077 let feature_names: Vec<_> = project.manifest.features.keys().cloned().collect();
1079
1080 let dylib_dir = path!(project.dir / "dylib");
1082 fs::create_dir_all(path!(dylib_dir / "src"))?;
1083
1084 let trybuild_crate_name_ident = syn::Ident::new(
1085 &project_name.replace("-", "_"),
1086 proc_macro2::Span::call_site(),
1087 );
1088 write_atomic(
1089 [
1095 "// v2: dylib must be built as a dependency (prefer-dynamic).\n".as_bytes(),
1096 prettyplease::unparse(&syn::parse_quote! {
1097 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1098 pub use #trybuild_crate_name_ident::*;
1099 })
1100 .as_bytes(),
1101 ]
1102 .concat()
1103 .as_slice(),
1104 &path!(dylib_dir / "src" / "lib.rs"),
1105 )?;
1106
1107 let serialized_edition = toml::to_string(
1108 &vec![("edition", &project.manifest.package.edition)]
1109 .into_iter()
1110 .collect::<std::collections::HashMap<_, _>>(),
1111 )
1112 .unwrap();
1113
1114 let dylib_features_section = feature_names
1117 .iter()
1118 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
1119 .collect::<Vec<_>>()
1120 .join("\n");
1121
1122 let dylib_package_name = format!("{project_name}-dylib");
1130 let dylib_lib_name = hydro_concurrent_cargo::current_dylib_lib_name(project.dir.as_ref())
1131 .unwrap_or_else(|| {
1132 hydro_concurrent_cargo::dylib_lib_name(&dylib_package_name, ENCODED_RUSTFLAGS)
1133 });
1134
1135 let dylib_manifest = format!(
1136 r#"[package]
1137name = "{dylib_package_name}"
1138version = "0.0.0"
1139{}
1140
1141[lib]
1142name = "{dylib_lib_name}"
1143crate-type = ["{}"]
1144
1145[dependencies]
1146{project_name} = {{ path = "..", default-features = false }}
1147
1148[features]
1149{dylib_features_section}
1150"#,
1151 serialized_edition,
1152 if cfg!(target_os = "windows") {
1153 "rlib"
1154 } else {
1155 "dylib"
1156 }
1157 );
1158 write_atomic(dylib_manifest.as_ref(), &path!(dylib_dir / "Cargo.toml"))?;
1159
1160 let dylib_examples_dir = path!(project.dir / "dylib-examples");
1161 fs::create_dir_all(path!(dylib_examples_dir / "src"))?;
1162 fs::create_dir_all(path!(dylib_examples_dir / "examples"))?;
1163
1164 write_atomic(
1165 b"#![allow(unused_crate_dependencies)]\n",
1166 &path!(dylib_examples_dir / "src" / "lib.rs"),
1167 )?;
1168
1169 let features_section = feature_names
1171 .iter()
1172 .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
1173 .collect::<Vec<_>>()
1174 .join("\n");
1175
1176 let dylib_examples_manifest = format!(
1188 r#"[package]
1189name = "{project_name}-dylib-examples"
1190version = "0.0.0"
1191{}
1192
1193[dependencies]
1194{project_name} = {{ package = "{project_name}-dylib", path = "../dylib", default-features = false }}
1195
1196[features]
1197{features_section}
1198
1199[[example]]
1200name = "sim-dylib"
1201crate-type = ["cdylib"]
1202"#,
1203 serialized_edition
1204 );
1205 write_atomic(
1206 dylib_examples_manifest.as_ref(),
1207 &path!(dylib_examples_dir / "Cargo.toml"),
1208 )?;
1209
1210 let sim_dylib_contents = prettyplease::unparse(&syn::parse_quote! {
1212 #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case, unexpected_cfgs, unfulfilled_lint_expectations)]
1213 include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
1214 });
1215 write_atomic(
1216 sim_dylib_contents.as_bytes(),
1217 &path!(project.dir / "examples" / "sim-dylib.rs"),
1218 )?;
1219 write_atomic(
1220 sim_dylib_contents.as_bytes(),
1221 &path!(dylib_examples_dir / "examples" / "sim-dylib.rs"),
1222 )?;
1223
1224 let workspace_manifest = format!(
1225 r#"{}
1226[[example]]
1227name = "sim-dylib"
1228crate-type = ["cdylib"]
1229
1230[workspace]
1231members = ["dylib", "dylib-examples"]
1232"#,
1233 base_manifest,
1234 );
1235
1236 write_atomic(
1237 workspace_manifest.as_ref(),
1238 &path!(project.dir / "Cargo.toml"),
1239 )?;
1240
1241 let manifest_hash = {
1247 let mut hasher = Sha256::new();
1248 hasher.update(&workspace_manifest);
1249 hasher.update(dylib_manifest.replace(&dylib_lib_name, ""));
1250 hasher.update(&dylib_examples_manifest);
1251 format!("{:X}", hasher.finalize())
1252 .chars()
1253 .take(8)
1254 .collect::<String>()
1255 };
1256
1257 let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
1258 let workspace_cargo_lock_contents_and_hash = if workspace_cargo_lock.exists() {
1259 let cargo_lock_contents = fs::read_to_string(&workspace_cargo_lock)?;
1260
1261 let hash = format!("{:X}", Sha256::digest(&cargo_lock_contents))
1262 .chars()
1263 .take(8)
1264 .collect::<String>();
1265
1266 Some((cargo_lock_contents, hash))
1267 } else {
1268 None
1269 };
1270
1271 let trybuild_hash = format!(
1272 "{}-{}",
1273 manifest_hash,
1274 workspace_cargo_lock_contents_and_hash
1275 .as_ref()
1276 .map(|(_contents, hash)| &**hash)
1277 .unwrap_or_default()
1278 );
1279
1280 if !check_contents(
1281 trybuild_hash.as_bytes(),
1282 &path!(project.dir / ".hydro-trybuild-manifest"),
1283 )
1284 .is_ok_and(|b| b)
1285 {
1286 let _span = tracing::debug_span!(target: "hydro_build", "update_lockfile").entered();
1287 if let Some((cargo_lock_contents, _)) = workspace_cargo_lock_contents_and_hash {
1289 write_atomic(
1292 cargo_lock_contents.as_ref(),
1293 &path!(project.dir / "Cargo.lock"),
1294 )?;
1295 } else {
1296 let _ = cargo::cargo(&project).arg("generate-lockfile").status();
1297 }
1298
1299 std::process::Command::new("cargo")
1301 .current_dir(&project.dir)
1302 .args(["update", "-w"]) .stdout(std::process::Stdio::null())
1304 .stderr(std::process::Stdio::null())
1305 .status()
1306 .unwrap();
1307
1308 write_atomic(
1309 trybuild_hash.as_bytes(),
1310 &path!(project.dir / ".hydro-trybuild-manifest"),
1311 )?;
1312 }
1313
1314 let examples_folder = path!(project.dir / "examples");
1316 fs::create_dir_all(&examples_folder)?;
1317
1318 let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
1319 if workspace_dot_cargo_config_toml.exists() {
1320 let dot_cargo_folder = path!(project.dir / ".cargo");
1321 fs::create_dir_all(&dot_cargo_folder)?;
1322
1323 write_atomic(
1324 fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
1325 &path!(dot_cargo_folder / "config.toml"),
1326 )?;
1327 }
1328
1329 let vscode_folder = path!(project.dir / ".vscode");
1330 fs::create_dir_all(&vscode_folder)?;
1331 write_atomic(
1332 include_bytes!("./vscode-trybuild.json"),
1333 &path!(vscode_folder / "settings.json"),
1334 )?;
1335 }
1336
1337 Ok((
1338 project.dir.as_ref().into(),
1339 project.target_dir.as_ref().into(),
1340 project.features,
1341 ))
1342}
1343
1344fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
1345 let mut file = File::options()
1346 .read(true)
1347 .write(false)
1348 .create(false)
1349 .truncate(false)
1350 .open(path)?;
1351 file.lock()?;
1352
1353 let mut existing_contents = Vec::new();
1354 file.read_to_end(&mut existing_contents)?;
1355 Ok(existing_contents == contents)
1356}
1357
1358pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
1359 let mut file = File::options()
1360 .read(true)
1361 .write(true)
1362 .create(true)
1363 .truncate(false)
1364 .open(path)?;
1365
1366 let mut existing_contents = Vec::new();
1367 file.read_to_end(&mut existing_contents)?;
1368 if existing_contents != contents {
1369 file.lock()?;
1370 file.seek(SeekFrom::Start(0))?;
1371 file.set_len(0)?;
1372 file.write_all(contents)?;
1373 }
1374
1375 Ok(())
1376}