Skip to main content

hydro_lang/compile/trybuild/
generate.rs

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/// Whether to use dynamic linking for the generated binary.
28/// - `Static`: Place in base crate examples (for remote/containerized deploys)
29/// - `Dynamic`: Place in dylib crate examples (for sim and localhost deploys)
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum LinkingMode {
32    Static,
33    #[cfg(feature = "deploy")]
34    Dynamic,
35}
36
37#[cfg(any(feature = "deploy", feature = "maelstrom"))]
38/// The deployment mode for code generation.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum DeployMode {
41    #[cfg(feature = "deploy")]
42    /// Standard HydroDeploy
43    HydroDeploy,
44    #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
45    /// Containerized deployment (Docker/ECS)
46    Containerized,
47    #[cfg(feature = "maelstrom")]
48    /// Maelstrom deployment with stdin/stdout JSON protocol
49    Maelstrom,
50}
51
52pub(crate) static IS_TEST: std::sync::atomic::AtomicBool =
53    std::sync::atomic::AtomicBool::new(false);
54
55pub(crate) static CONCURRENT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
56
57/// Enables "test mode" for Hydro, which makes it possible to compile Hydro programs written
58/// inside a `#[cfg(test)]` module. This should be enabled in a global [`ctor`] hook.
59///
60/// # Example
61/// ```ignore
62/// #[cfg(test)]
63/// mod test_init {
64///    #[ctor::ctor]
65///    fn init() {
66///        hydro_lang::compile::init_test();
67///    }
68/// }
69/// ```
70pub fn init_test() {
71    IS_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
72}
73
74#[cfg(any(feature = "deploy", feature = "maelstrom"))]
75fn clean_bin_name_prefix(bin_name_prefix: &str) -> String {
76    bin_name_prefix
77        .replace("::", "__")
78        .replace(" ", "_")
79        .replace(",", "_")
80        .replace("<", "_")
81        .replace(">", "")
82        .replace("(", "")
83        .replace(")", "")
84        .replace("{", "_")
85        .replace("}", "_")
86}
87
88#[derive(Debug, Clone)]
89pub struct TrybuildConfig {
90    pub project_dir: PathBuf,
91    pub target_dir: PathBuf,
92    pub features: Option<Vec<String>>,
93    #[cfg(feature = "deploy")]
94    /// Which crate within the workspace to use for examples.
95    /// - `Static`: base crate (for remote/containerized deploys)
96    /// - `Dynamic`: dylib-examples crate (for sim and localhost deploys)
97    pub linking_mode: LinkingMode,
98}
99
100#[cfg(any(feature = "deploy", feature = "maelstrom"))]
101pub fn create_graph_trybuild(
102    graph: DfirGraph,
103    extra_stmts: &[syn::Stmt],
104    sidecars: &[syn::Expr],
105    bin_name_prefix: Option<&str>,
106    deploy_mode: DeployMode,
107    linking_mode: LinkingMode,
108) -> (String, TrybuildConfig) {
109    let source_dir = cargo::manifest_dir().unwrap();
110    let source_manifest = dependencies::get_manifest(&source_dir).unwrap();
111    let crate_name = source_manifest.package.name.replace("-", "_");
112
113    let is_test = IS_TEST.load(std::sync::atomic::Ordering::Relaxed);
114
115    let generated_code =
116        compile_graph_trybuild(graph, extra_stmts, sidecars, &crate_name, deploy_mode);
117
118    let inlined_staged = if is_test {
119        let raw_toml_manifest = toml::from_str::<toml::Value>(
120            &fs::read_to_string(path!(source_dir / "Cargo.toml")).unwrap(),
121        )
122        .unwrap();
123
124        let maybe_custom_lib_path = raw_toml_manifest
125            .get("lib")
126            .and_then(|lib| lib.get("path"))
127            .and_then(|path| path.as_str());
128
129        let mut gen_staged = stageleft_tool::gen_staged_trybuild(
130            &maybe_custom_lib_path
131                .map(|s| path!(source_dir / s))
132                .unwrap_or_else(|| path!(source_dir / "src" / "lib.rs")),
133            &path!(source_dir / "Cargo.toml"),
134            &crate_name,
135            Some("hydro___test".to_owned()),
136        );
137
138        gen_staged.attrs.insert(
139            0,
140            syn::parse_quote! {
141                #![allow(
142                    unused,
143                    ambiguous_glob_reexports,
144                    clippy::suspicious_else_formatting,
145                    unexpected_cfgs,
146                    reason = "generated code"
147                )]
148            },
149        );
150
151        Some(prettyplease::unparse(&gen_staged))
152    } else {
153        None
154    };
155
156    let source = prettyplease::unparse(&generated_code);
157
158    let hash = format!("{:X}", Sha256::digest(&source))
159        .chars()
160        .take(8)
161        .collect::<String>();
162
163    let bin_name = if let Some(bin_name_prefix) = &bin_name_prefix {
164        format!("{}_{}", clean_bin_name_prefix(bin_name_prefix), &hash)
165    } else {
166        hash
167    };
168
169    let (project_dir, target_dir, mut cur_bin_enabled_features) = create_trybuild().unwrap();
170
171    // Determine which crate's examples folder to use based on linking mode
172    let examples_dir = match linking_mode {
173        LinkingMode::Static => path!(project_dir / "examples"),
174        #[cfg(feature = "deploy")]
175        LinkingMode::Dynamic => path!(project_dir / "dylib-examples" / "examples"),
176    };
177
178    // TODO(shadaj): garbage collect this directory occasionally
179    fs::create_dir_all(&examples_dir).unwrap();
180
181    let out_path = path!(examples_dir / format!("{bin_name}.rs"));
182    {
183        let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
184        write_atomic(source.as_ref(), &out_path).unwrap();
185    }
186
187    if let Some(inlined_staged) = inlined_staged {
188        let staged_path = path!(project_dir / "src" / "__staged.rs");
189        {
190            let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
191            write_atomic(inlined_staged.as_bytes(), &staged_path).unwrap();
192        }
193    }
194
195    if is_test {
196        if cur_bin_enabled_features.is_none() {
197            cur_bin_enabled_features = Some(vec![]);
198        }
199
200        cur_bin_enabled_features
201            .as_mut()
202            .unwrap()
203            .push("hydro___test".to_owned());
204    }
205
206    (
207        bin_name,
208        TrybuildConfig {
209            project_dir,
210            target_dir,
211            features: cur_bin_enabled_features,
212            #[cfg(feature = "deploy")]
213            linking_mode,
214        },
215    )
216}
217
218#[cfg(any(feature = "deploy", feature = "maelstrom"))]
219pub fn compile_graph_trybuild(
220    partitioned_graph: DfirGraph,
221    extra_stmts: &[syn::Stmt],
222    sidecars: &[syn::Expr],
223    crate_name: &str,
224    deploy_mode: DeployMode,
225) -> syn::File {
226    use crate::staging_util::get_this_crate;
227
228    let mut diagnostics = Diagnostics::new();
229    let dfir_expr: syn::Expr = syn::parse2(
230        partitioned_graph
231            .as_code(&quote! { __root_dfir_rs }, true, quote!(), &mut diagnostics)
232            .expect("DFIR code generation failed with diagnostics."),
233    )
234    .unwrap();
235
236    let orig_crate_name = quote::format_ident!("{}", crate_name);
237    let trybuild_crate_name_ident = quote::format_ident!("{}_hydro_trybuild", crate_name);
238    let root = get_this_crate();
239    let tokio_main_ident = format!("{}::runtime_support::tokio", root);
240    let dfir_ident = quote::format_ident!("{}", crate::compile::DFIR_IDENT);
241
242    let source_ast: syn::File = match deploy_mode {
243        #[cfg(any(feature = "docker_deploy", feature = "ecs_deploy"))]
244        DeployMode::Containerized => {
245            syn::parse_quote! {
246                #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
247                use #trybuild_crate_name_ident::__root as #orig_crate_name;
248                use #orig_crate_name::*;
249                use #orig_crate_name::__staged::__deps::*;
250                use #root::prelude::*;
251                use #root::runtime_support::dfir_rs as __root_dfir_rs;
252                pub use #orig_crate_name::__staged;
253
254                #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
255                async fn main() {
256                    #root::telemetry::initialize_tracing();
257
258                    #( #extra_stmts )*
259
260                    let mut #dfir_ident = #dfir_expr;
261
262                    let local_set = #root::runtime_support::tokio::task::LocalSet::new();
263                    #(
264                        let _ = local_set.spawn_local( #sidecars ); // Uses #dfir_ident
265                    )*
266
267                    let _ = local_set.run_until(#dfir_ident.run()).await;
268                }
269            }
270        }
271        #[cfg(feature = "deploy")]
272        DeployMode::HydroDeploy => {
273            syn::parse_quote! {
274                #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
275                use #trybuild_crate_name_ident::__root as #orig_crate_name;
276                use #orig_crate_name::*;
277                use #orig_crate_name::__staged::__deps::*;
278                use #root::prelude::*;
279                use #root::runtime_support::dfir_rs as __root_dfir_rs;
280                pub use #orig_crate_name::__staged;
281
282                #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
283                async fn main() {
284                    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;
285                    let __hydro_lang_trybuild_cli = &__hydro_lang_trybuild_cli_owned;
286
287                    #( #extra_stmts )*
288
289                    let mut #dfir_ident = #dfir_expr;
290                    println!("ack start");
291
292                    // TODO(mingwei): initialize `tracing` at this point in execution.
293                    // After "ack start" is when we can print whatever we want.
294
295                    let local_set = #root::runtime_support::tokio::task::LocalSet::new();
296                    #(
297                        let _ = local_set.spawn_local( #sidecars ); // Uses #dfir_ident
298                    )*
299
300                    let _ = local_set.run_until(#root::runtime_support::launch::run_stdin_commands(
301                        async move {
302                            #dfir_ident.run().await
303                        }
304                    )).await;
305                }
306            }
307        }
308        #[cfg(feature = "maelstrom")]
309        DeployMode::Maelstrom => {
310            syn::parse_quote! {
311                #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
312                use #trybuild_crate_name_ident::__root as #orig_crate_name;
313                use #orig_crate_name::*;
314                use #orig_crate_name::__staged::__deps::*;
315                use #root::prelude::*;
316                use #root::runtime_support::dfir_rs as __root_dfir_rs;
317                pub use #orig_crate_name::__staged;
318
319                #[allow(unused)]
320                fn __hydro_runtime<'a>(
321                    __hydro_lang_maelstrom_meta: &'a #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::MaelstromMeta
322                )
323                    -> #root::runtime_support::dfir_rs::scheduled::context::Dfir<impl #root::runtime_support::dfir_rs::scheduled::context::TickClosure + 'a>
324                {
325                    #( #extra_stmts )*
326
327                    #dfir_expr
328                }
329
330                #[#root::runtime_support::tokio::main(crate = #tokio_main_ident, flavor = "current_thread")]
331                async fn main() {
332                    #root::telemetry::initialize_tracing();
333
334                    // Initialize Maelstrom protocol - read init message and send init_ok
335                    let __hydro_lang_maelstrom_meta = #root::__staged::deploy::maelstrom::deploy_runtime_maelstrom::maelstrom_init();
336
337                    let mut #dfir_ident = __hydro_runtime(&__hydro_lang_maelstrom_meta);
338
339                    __hydro_lang_maelstrom_meta.start_receiving(); // start receiving messages after initializing subscribers
340
341                    let local_set = #root::runtime_support::tokio::task::LocalSet::new();
342                    #(
343                        let _ = local_set.spawn_local( #sidecars ); // Uses #dfir_ident
344                    )*
345
346                    let _ = local_set.run_until(#dfir_ident.run()).await;
347                }
348            }
349        }
350    };
351    source_ast
352}
353
354pub fn create_trybuild()
355-> Result<(PathBuf, PathBuf, Option<Vec<String>>), trybuild_internals_api::error::Error> {
356    let Metadata {
357        target_directory: target_dir,
358        workspace_root: workspace,
359        packages,
360    } = cargo::metadata()?;
361
362    let source_dir = cargo::manifest_dir()?;
363    let mut source_manifest = dependencies::get_manifest(&source_dir)?;
364
365    let mut dev_dependency_features = vec![];
366    source_manifest.dev_dependencies.retain(|k, v| {
367        if source_manifest.dependencies.contains_key(k) {
368            // already a non-dev dependency, so drop the dep and put the features under the test flag
369            for feat in &v.features {
370                dev_dependency_features.push(format!("{}/{}", k, feat));
371            }
372
373            false
374        } else {
375            // only enable this in test mode, so make it optional otherwise
376            dev_dependency_features.push(format!("dep:{k}"));
377
378            v.optional = true;
379            true
380        }
381    });
382
383    let mut features = features::find();
384
385    let path_dependencies = source_manifest
386        .dependencies
387        .iter()
388        .filter_map(|(name, dep)| {
389            let path = dep.path.as_ref()?;
390            if packages.iter().any(|p| &p.name == name) {
391                // Skip path dependencies coming from the workspace itself
392                None
393            } else {
394                Some(PathDependency {
395                    name: name.clone(),
396                    normalized_path: path.canonicalize().ok()?,
397                })
398            }
399        })
400        .collect();
401
402    let crate_name = source_manifest.package.name.clone();
403    let project_dir = path!(target_dir / "hydro_trybuild" / crate_name /);
404    fs::create_dir_all(&project_dir)?;
405
406    let project_name = format!("{}-hydro-trybuild", crate_name);
407    let mut manifest = Runner::make_manifest(
408        &workspace,
409        &project_name,
410        &source_dir,
411        &packages,
412        &[],
413        source_manifest,
414    )?;
415
416    if let Some(enabled_features) = &mut features {
417        enabled_features
418            .retain(|feature| manifest.features.contains_key(feature) || feature == "default");
419    }
420
421    for runtime_feature in HYDRO_RUNTIME_FEATURES {
422        manifest.features.insert(
423            format!("hydro___feature_{runtime_feature}"),
424            vec![format!("hydro_lang/{runtime_feature}")],
425        );
426    }
427
428    manifest
429        .dependencies
430        .get_mut("hydro_lang")
431        .unwrap()
432        .features
433        .push("runtime_support".to_owned());
434
435    manifest
436        .features
437        .insert("hydro___test".to_owned(), dev_dependency_features);
438
439    if manifest
440        .workspace
441        .as_ref()
442        .is_some_and(|w| w.dependencies.is_empty())
443    {
444        manifest.workspace = None;
445    }
446
447    let project = Project {
448        dir: project_dir,
449        source_dir,
450        target_dir,
451        name: project_name.clone(),
452        update: Update::env()?,
453        has_pass: false,
454        has_compile_fail: false,
455        features,
456        workspace,
457        path_dependencies,
458        manifest,
459        keep_going: false,
460    };
461
462    {
463        let _concurrent_test_lock = CONCURRENT_TEST_LOCK.lock().unwrap();
464
465        let project_lock = File::create(path!(project.dir / ".hydro-trybuild-lock"))?;
466        project_lock.lock()?;
467
468        fs::create_dir_all(path!(project.dir / "src"))?;
469        fs::create_dir_all(path!(project.dir / "examples"))?;
470
471        let crate_name_ident = syn::Ident::new(
472            &crate_name.replace("-", "_"),
473            proc_macro2::Span::call_site(),
474        );
475
476        write_atomic(
477            prettyplease::unparse(&syn::parse_quote! {
478                #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
479
480                pub mod __root {
481                    pub use #crate_name_ident::*;
482                    #[cfg(feature = "hydro___test")]
483                    pub use super::__staged;
484                }
485
486                #[cfg(feature = "hydro___test")]
487                pub mod __staged;
488            })
489            .as_bytes(),
490            &path!(project.dir / "src" / "lib.rs"),
491        )
492        .unwrap();
493
494        let base_manifest = toml::to_string(&project.manifest)?;
495
496        // Collect feature names for forwarding to dylib and dylib-examples crates
497        let feature_names: Vec<_> = project.manifest.features.keys().cloned().collect();
498
499        // Create dylib crate directory
500        let dylib_dir = path!(project.dir / "dylib");
501        fs::create_dir_all(path!(dylib_dir / "src"))?;
502
503        let trybuild_crate_name_ident = syn::Ident::new(
504            &project_name.replace("-", "_"),
505            proc_macro2::Span::call_site(),
506        );
507        write_atomic(
508            prettyplease::unparse(&syn::parse_quote! {
509                #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
510                pub use #trybuild_crate_name_ident::*;
511            })
512            .as_bytes(),
513            &path!(dylib_dir / "src" / "lib.rs"),
514        )?;
515
516        let serialized_edition = toml::to_string(
517            &vec![("edition", &project.manifest.package.edition)]
518                .into_iter()
519                .collect::<std::collections::HashMap<_, _>>(),
520        )
521        .unwrap();
522
523        // Dylib crate Cargo.toml - only dylib crate-type, no features needed
524        // Features are enabled on the base crate directly from dylib-examples
525        // On Windows, we currently disable dylib compilation due to https://github.com/bevyengine/bevy/pull/2016
526        let dylib_manifest = format!(
527            r#"[package]
528name = "{project_name}-dylib"
529version = "0.0.0"
530{}
531
532[lib]
533crate-type = ["{}"]
534
535[dependencies]
536{project_name} = {{ path = "..", default-features = false }}
537"#,
538            serialized_edition,
539            if cfg!(target_os = "windows") {
540                "rlib"
541            } else {
542                "dylib"
543            }
544        );
545        write_atomic(dylib_manifest.as_ref(), &path!(dylib_dir / "Cargo.toml"))?;
546
547        let dylib_examples_dir = path!(project.dir / "dylib-examples");
548        fs::create_dir_all(path!(dylib_examples_dir / "src"))?;
549        fs::create_dir_all(path!(dylib_examples_dir / "examples"))?;
550
551        write_atomic(
552            b"#![allow(unused_crate_dependencies)]\n",
553            &path!(dylib_examples_dir / "src" / "lib.rs"),
554        )?;
555
556        // Build feature forwarding for dylib-examples - forward directly to base crate
557        let features_section = feature_names
558            .iter()
559            .map(|f| format!("{f} = [\"{project_name}/{f}\"]"))
560            .collect::<Vec<_>>()
561            .join("\n");
562
563        // Dylib-examples crate Cargo.toml - has dylib as dev-dependency, features go to base crate
564        let dylib_examples_manifest = format!(
565            r#"[package]
566name = "{project_name}-dylib-examples"
567version = "0.0.0"
568{}
569
570[dev-dependencies]
571{project_name} = {{ path = "..", default-features = false }}
572{project_name}-dylib = {{ path = "../dylib", default-features = false }}
573
574[features]
575{features_section}
576
577[[example]]
578name = "sim-dylib"
579crate-type = ["cdylib"]
580"#,
581            serialized_edition
582        );
583        write_atomic(
584            dylib_examples_manifest.as_ref(),
585            &path!(dylib_examples_dir / "Cargo.toml"),
586        )?;
587
588        // sim-dylib.rs for the base crate and dylib-examples crate
589        let sim_dylib_contents = prettyplease::unparse(&syn::parse_quote! {
590            #![allow(unused_imports, unused_crate_dependencies, missing_docs, non_snake_case)]
591            include!(std::concat!(env!("TRYBUILD_LIB_NAME"), ".rs"));
592        });
593        write_atomic(
594            sim_dylib_contents.as_bytes(),
595            &path!(project.dir / "examples" / "sim-dylib.rs"),
596        )?;
597        write_atomic(
598            sim_dylib_contents.as_bytes(),
599            &path!(dylib_examples_dir / "examples" / "sim-dylib.rs"),
600        )?;
601
602        let workspace_manifest = format!(
603            r#"{}
604[[example]]
605name = "sim-dylib"
606crate-type = ["cdylib"]
607
608[workspace]
609members = ["dylib", "dylib-examples"]
610"#,
611            base_manifest,
612        );
613
614        write_atomic(
615            workspace_manifest.as_ref(),
616            &path!(project.dir / "Cargo.toml"),
617        )?;
618
619        // Compute hash for cache invalidation (dylib and dylib-examples are functions of workspace_manifest)
620        let manifest_hash = format!("{:X}", Sha256::digest(&workspace_manifest))
621            .chars()
622            .take(8)
623            .collect::<String>();
624
625        let workspace_cargo_lock = path!(project.workspace / "Cargo.lock");
626        let workspace_cargo_lock_contents_and_hash = if workspace_cargo_lock.exists() {
627            let cargo_lock_contents = fs::read_to_string(&workspace_cargo_lock)?;
628
629            let hash = format!("{:X}", Sha256::digest(&cargo_lock_contents))
630                .chars()
631                .take(8)
632                .collect::<String>();
633
634            Some((cargo_lock_contents, hash))
635        } else {
636            None
637        };
638
639        let trybuild_hash = format!(
640            "{}-{}",
641            manifest_hash,
642            workspace_cargo_lock_contents_and_hash
643                .as_ref()
644                .map(|(_contents, hash)| &**hash)
645                .unwrap_or_default()
646        );
647
648        if !check_contents(
649            trybuild_hash.as_bytes(),
650            &path!(project.dir / ".hydro-trybuild-manifest"),
651        )
652        .is_ok_and(|b| b)
653        {
654            // this is expensive, so we only do it if the manifest changed
655            if let Some((cargo_lock_contents, _)) = workspace_cargo_lock_contents_and_hash {
656                // only overwrite when the hash changed, because writing Cargo.lock must be
657                // immediately followed by a local `cargo update -w`
658                write_atomic(
659                    cargo_lock_contents.as_ref(),
660                    &path!(project.dir / "Cargo.lock"),
661                )?;
662            } else {
663                let _ = cargo::cargo(&project).arg("generate-lockfile").status();
664            }
665
666            // not `--offline` because some new runtime features may be enabled
667            std::process::Command::new("cargo")
668                .current_dir(&project.dir)
669                .args(["update", "-w"]) // -w to not actually update any versions
670                .stdout(std::process::Stdio::null())
671                .stderr(std::process::Stdio::null())
672                .status()
673                .unwrap();
674
675            write_atomic(
676                trybuild_hash.as_bytes(),
677                &path!(project.dir / ".hydro-trybuild-manifest"),
678            )?;
679        }
680
681        // Create examples folder for base crate (static linking)
682        let examples_folder = path!(project.dir / "examples");
683        fs::create_dir_all(&examples_folder)?;
684
685        let workspace_dot_cargo_config_toml = path!(project.workspace / ".cargo" / "config.toml");
686        if workspace_dot_cargo_config_toml.exists() {
687            let dot_cargo_folder = path!(project.dir / ".cargo");
688            fs::create_dir_all(&dot_cargo_folder)?;
689
690            write_atomic(
691                fs::read_to_string(&workspace_dot_cargo_config_toml)?.as_ref(),
692                &path!(dot_cargo_folder / "config.toml"),
693            )?;
694        }
695
696        let vscode_folder = path!(project.dir / ".vscode");
697        fs::create_dir_all(&vscode_folder)?;
698        write_atomic(
699            include_bytes!("./vscode-trybuild.json"),
700            &path!(vscode_folder / "settings.json"),
701        )?;
702    }
703
704    Ok((
705        project.dir.as_ref().into(),
706        project.target_dir.as_ref().into(),
707        project.features,
708    ))
709}
710
711fn check_contents(contents: &[u8], path: &Path) -> Result<bool, std::io::Error> {
712    let mut file = File::options()
713        .read(true)
714        .write(false)
715        .create(false)
716        .truncate(false)
717        .open(path)?;
718    file.lock()?;
719
720    let mut existing_contents = Vec::new();
721    file.read_to_end(&mut existing_contents)?;
722    Ok(existing_contents == contents)
723}
724
725pub(crate) fn write_atomic(contents: &[u8], path: &Path) -> Result<(), std::io::Error> {
726    let mut file = File::options()
727        .read(true)
728        .write(true)
729        .create(true)
730        .truncate(false)
731        .open(path)?;
732
733    let mut existing_contents = Vec::new();
734    file.read_to_end(&mut existing_contents)?;
735    if existing_contents != contents {
736        file.lock()?;
737        file.seek(SeekFrom::Start(0))?;
738        file.set_len(0)?;
739        file.write_all(contents)?;
740    }
741
742    Ok(())
743}