diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 90a8bd0a893..805783dc3b9 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -1704,12 +1704,22 @@ fn build_deps_args( let mut unstable_opts = false; + // Add `OUT_DIR` environment variables for build scripts + let first_custom_build_dep = deps.iter().find(|dep| dep.unit.mode.is_run_custom_build()); + if let Some(dep) = first_custom_build_dep { + let out_dir = &build_runner.files().build_script_out_dir(&dep.unit); + cmd.env("OUT_DIR", &out_dir); + } + for dep in deps { if dep.unit.mode.is_run_custom_build() { - cmd.env( - "OUT_DIR", - &build_runner.files().build_script_out_dir(&dep.unit), - ); + let out_dir = &build_runner.files().build_script_out_dir(&dep.unit); + let target_name = dep.unit.target.name(); + let out_dir_prefix = target_name + .strip_prefix("build-script-") + .unwrap_or(target_name); + let out_dir_name = format!("{out_dir_prefix}_OUT_DIR"); + cmd.env(&out_dir_name, &out_dir); } } diff --git a/src/cargo/util/toml/targets.rs b/src/cargo/util/toml/targets.rs index 65c4ea70771..e0a726e5e97 100644 --- a/src/cargo/util/toml/targets.rs +++ b/src/cargo/util/toml/targets.rs @@ -10,7 +10,7 @@ //! It is a bit tricky because we need match explicit information from `Cargo.toml` //! with implicit info in directory layout. -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fs::{self, DirEntry}; use std::path::{Path, PathBuf}; @@ -104,6 +104,7 @@ pub(super) fn to_targets( if metabuild.is_some() { anyhow::bail!("cannot specify both `metabuild` and `build`"); } + validate_unique_build_scripts(custom_build)?; for script in custom_build { let script_path = Path::new(script); let name = format!( @@ -901,6 +902,35 @@ fn validate_unique_names(targets: &[TomlTarget], target_kind: &str) -> CargoResu Ok(()) } +/// Will check a list of build scripts, and make sure script file stems are unique within a vector. +fn validate_unique_build_scripts(scripts: &[String]) -> CargoResult<()> { + let mut seen = HashMap::new(); + for script in scripts { + let stem = Path::new(script).file_stem().unwrap().to_str().unwrap(); + seen.entry(stem) + .or_insert_with(Vec::new) + .push(script.as_str()); + } + let mut conflict_file_stem = false; + let mut err_msg = String::from( + "found build scripts with duplicate file stems, but all build scripts must have a unique file stem", + ); + for (stem, paths) in seen { + if paths.len() > 1 { + conflict_file_stem = true; + err_msg += &format!( + "\nBuild Scripts : {} have the same file stem, that is {}", + paths.join(", "), + stem + ); + } + } + if conflict_file_stem { + anyhow::bail!(err_msg); + } + Ok(()) +} + fn configure( toml: &TomlTarget, target: &mut Target, diff --git a/src/doc/src/reference/environment-variables.md b/src/doc/src/reference/environment-variables.md index 2f69434ddc0..489a99fc78c 100644 --- a/src/doc/src/reference/environment-variables.md +++ b/src/doc/src/reference/environment-variables.md @@ -256,6 +256,9 @@ corresponding environment variable is set to the empty string, `""`. * `OUT_DIR` --- If the package has a build script, this is set to the folder where the build script should place its output. See below for more information. (Only set during compilation.) +* `_OUT_DIR` --- If the package has build script(s), this is set to the folder where the + build script should place its output. The `` is the file-stem of the build script, exactly as-is. + For example, `bar_OUT_DIR` for script at `foo/bar.rs`. (Only set during compilation.) * `CARGO_BIN_EXE_` --- The absolute path to a binary target's executable. This is only set when building an [integration test] or benchmark. This may be used with the [`env` macro] to find the executable to run for testing diff --git a/tests/testsuite/build_scripts_multiple.rs b/tests/testsuite/build_scripts_multiple.rs index 8e7457fb954..bbd400ff417 100644 --- a/tests/testsuite/build_scripts_multiple.rs +++ b/tests/testsuite/build_scripts_multiple.rs @@ -549,7 +549,7 @@ fn build_script_with_conflicting_out_dirs() { build = ["build1.rs", "build2.rs"] "#, ) - // OUT_DIR is set to the lexicographically largest build script's OUT_DIR by default + // By default, OUT_DIR is set to that of the first build script in the array .file( "src/main.rs", r#" @@ -603,7 +603,7 @@ fn build_script_with_conflicting_out_dirs() { .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_status(0) .with_stdout_data(str![[r#" -Hello, from Build Script 2! +Hello, from Build Script 1! "#]]) .run(); @@ -628,7 +628,7 @@ fn build_script_with_conflicts_reverse_sorted() { build = ["build2.rs", "build1.rs"] "#, ) - // OUT_DIR is set to the lexicographically largest build script's OUT_DIR by default + // By default, OUT_DIR is set to that of the first build script in the array .file( "src/main.rs", r#" @@ -682,7 +682,7 @@ fn build_script_with_conflicts_reverse_sorted() { .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_status(0) .with_stdout_data(str![[r#" -Hello, from Build Script 1! +Hello, from Build Script 2! "#]]) .run(); @@ -764,3 +764,118 @@ fn bar() { "#]]) .run(); } + +#[cargo_test] +fn multiple_out_dirs() { + // Test to verify access to the `OUT_DIR` of the respective build scripts. + + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["multiple-build-scripts"] + + [package] + name = "foo" + version = "0.1.0" + edition = "2024" + build = ["build1.rs", "build2.rs"] + "#, + ) + .file( + "src/main.rs", + r#" + include!(concat!(env!("build1_OUT_DIR"), "/foo.rs")); + include!(concat!(env!("build2_OUT_DIR"), "/foo.rs")); + fn main() { + println!("{}", message1()); + println!("{}", message2()); + } + "#, + ) + .file( + "build1.rs", + r#" + use std::env; + use std::fs; + use std::path::Path; + + fn main() { + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("foo.rs"); + fs::write( + &dest_path, + "pub fn message1() -> &'static str { + \"Hello, from Build Script 1!\" + } + " + ).unwrap(); + }"#, + ) + .file( + "build2.rs", + r#" + use std::env; + use std::fs; + use std::path::Path; + + fn main() { + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("foo.rs"); + fs::write( + &dest_path, + "pub fn message2() -> &'static str { + \"Hello, from Build Script 2!\" + } + " + ).unwrap(); + }"#, + ) + .build(); + + p.cargo("run -v") + .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) + .with_status(0) + .with_stdout_data(str![[r#" +Hello, from Build Script 1! +Hello, from Build Script 2! + +"#]]) + .run(); +} + +#[cargo_test] +fn duplicate_build_script_stems() { + // Test to verify that duplicate build script file stems throws error. + + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["multiple-build-scripts"] + + [package] + name = "foo" + version = "0.1.0" + edition = "2024" + build = ["build1.rs", "foo/build1.rs"] + "#, + ) + .file("src/main.rs", "fn main() {}") + .file("build1.rs", "fn main() {}") + .file("foo/build1.rs", "fn main() {}") + .build(); + + p.cargo("check -v") + .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml` + +Caused by: + found build scripts with duplicate file stems, but all build scripts must have a unique file stem + Build Scripts : build1.rs, foo/build1.rs have the same file stem, that is build1 + +"#]]) + .run(); +}