From d7aa0bcc22dadb9281678629c8b3c184103ec850 Mon Sep 17 00:00:00 2001 From: Naman Garg Date: Thu, 28 Aug 2025 16:11:28 +0530 Subject: [PATCH 1/3] Add test for multiple `OUT_DIR` --- tests/testsuite/build_scripts_multiple.rs | 132 ++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/tests/testsuite/build_scripts_multiple.rs b/tests/testsuite/build_scripts_multiple.rs index 8e7457fb954..5fed6da6511 100644 --- a/tests/testsuite/build_scripts_multiple.rs +++ b/tests/testsuite/build_scripts_multiple.rs @@ -764,3 +764,135 @@ 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!("OUT_DIR"), "/foo.rs")); + fn main() { + println!("{}", message()); + } + "#, + ) + .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 message() -> &'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 message() -> &'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 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#" +[WARNING] output filename collision. +The build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)` has the same output filename as the build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)`. +Colliding filename is: [ROOT]/foo/target/debug/build/foo-[HASH]/build_script_build1-[HASH] +The targets should have unique names. +Consider changing their names to be unique or compiling them separately. +This may become a hard error in the future; see . +[WARNING] output filename collision. +The build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)` has the same output filename as the build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)`. +Colliding filename is: [ROOT]/foo/target/debug/build/foo-[HASH]/build-script-build1 +The targets should have unique names. +Consider changing their names to be unique or compiling them separately. +This may become a hard error in the future; see . +[WARNING] output filename collision. +The build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)` has the same output filename as the build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)`. +Colliding filename is: [ROOT]/foo/target/debug/build/foo-[HASH]/build_script_build1-[HASH].dwp +The targets should have unique names. +Consider changing their names to be unique or compiling them separately. +This may become a hard error in the future; see . +[WARNING] output filename collision. +The build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)` has the same output filename as the build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)`. +Colliding filename is: [ROOT]/foo/target/debug/build/foo-[HASH]/build-script-build1.dwp +The targets should have unique names. +Consider changing their names to be unique or compiling them separately. +This may become a hard error in the future; see . +[COMPILING] foo v0.1.0 ([ROOT]/foo) +... +"#]]) + .run(); +} From 93515acf1e41e155f974ab503b2422805ce52884 Mon Sep 17 00:00:00 2001 From: Naman Garg Date: Sun, 7 Sep 2025 18:48:41 +0530 Subject: [PATCH 2/3] Add environment variables for accessing the `OUT_DIR` --- src/cargo/core/compiler/mod.rs | 33 ++++++++++++++++++----- src/doc/src/reference/unstable.md | 4 +++ tests/testsuite/build_scripts_multiple.rs | 19 +++++++------ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 90a8bd0a893..e273e335ee5 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -1704,15 +1704,34 @@ fn build_deps_args( let mut unstable_opts = false; - 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), - ); + // 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); + } + + // Adding output directory for each build script + let is_multiple_build_scripts_enabled = unit + .pkg + .manifest() + .unstable_features() + .require(Feature::multiple_build_scripts()) + .is_ok(); + + if is_multiple_build_scripts_enabled { + for dep in deps { + if dep.unit.mode.is_run_custom_build() { + 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); + } } } - for arg in extern_args(build_runner, unit, &mut unstable_opts)? { cmd.arg(arg); } diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index ee90cb74e90..da3556788bb 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -324,6 +324,10 @@ version = "0.0.1" build = ["foo.rs", "bar.rs"] ``` +**Accessing Output Directories**: Output directory of each build script can be accessed by using `_OUT_DIR` + where 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, can be accessed via `env!` macro) + ## public-dependency * Tracking Issue: [#44663](https://github.com/rust-lang/rust/issues/44663) diff --git a/tests/testsuite/build_scripts_multiple.rs b/tests/testsuite/build_scripts_multiple.rs index 5fed6da6511..456b9f88209 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(); @@ -785,9 +785,11 @@ fn multiple_out_dirs() { .file( "src/main.rs", r#" - include!(concat!(env!("OUT_DIR"), "/foo.rs")); + include!(concat!(env!("build1_OUT_DIR"), "/foo.rs")); + include!(concat!(env!("build2_OUT_DIR"), "/foo.rs")); fn main() { - println!("{}", message()); + println!("{}", message1()); + println!("{}", message2()); } "#, ) @@ -803,7 +805,7 @@ fn multiple_out_dirs() { let dest_path = Path::new(&out_dir).join("foo.rs"); fs::write( &dest_path, - "pub fn message() -> &'static str { + "pub fn message1() -> &'static str { \"Hello, from Build Script 1!\" } " @@ -822,7 +824,7 @@ fn multiple_out_dirs() { let dest_path = Path::new(&out_dir).join("foo.rs"); fs::write( &dest_path, - "pub fn message() -> &'static str { + "pub fn message2() -> &'static str { \"Hello, from Build Script 2!\" } " @@ -835,6 +837,7 @@ fn multiple_out_dirs() { .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! "#]]) From 4d74451c33d6150692185bd126d01d52f2f44318 Mon Sep 17 00:00:00 2001 From: Naman Garg Date: Tue, 16 Sep 2025 00:44:27 +0530 Subject: [PATCH 3/3] Validate the uniqueness of build scripts --- src/cargo/util/toml/targets.rs | 29 +++++++++++++++++++- tests/testsuite/build_scripts_multiple.rs | 32 +++++------------------ 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/cargo/util/toml/targets.rs b/src/cargo/util/toml/targets.rs index 65c4ea70771..74c729d568c 100644 --- a/src/cargo/util/toml/targets.rs +++ b/src/cargo/util/toml/targets.rs @@ -10,7 +10,8 @@ //! 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::fmt::Write; use std::fs::{self, DirEntry}; use std::path::{Path, PathBuf}; @@ -104,6 +105,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 +903,31 @@ 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; + write!(&mut err_msg, "\n for stem `{stem}`: {}", paths.join(", "))?; + } + } + if conflict_file_stem { + anyhow::bail!(err_msg); + } + Ok(()) +} + fn configure( toml: &TomlTarget, target: &mut Target, diff --git a/tests/testsuite/build_scripts_multiple.rs b/tests/testsuite/build_scripts_multiple.rs index 456b9f88209..d3f06b111d9 100644 --- a/tests/testsuite/build_scripts_multiple.rs +++ b/tests/testsuite/build_scripts_multiple.rs @@ -870,32 +870,12 @@ fn duplicate_build_script_stems() { .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_status(101) .with_stderr_data(str![[r#" -[WARNING] output filename collision. -The build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)` has the same output filename as the build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)`. -Colliding filename is: [ROOT]/foo/target/debug/build/foo-[HASH]/build_script_build1-[HASH] -The targets should have unique names. -Consider changing their names to be unique or compiling them separately. -This may become a hard error in the future; see . -[WARNING] output filename collision. -The build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)` has the same output filename as the build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)`. -Colliding filename is: [ROOT]/foo/target/debug/build/foo-[HASH]/build-script-build1 -The targets should have unique names. -Consider changing their names to be unique or compiling them separately. -This may become a hard error in the future; see . -[WARNING] output filename collision. -The build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)` has the same output filename as the build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)`. -Colliding filename is: [ROOT]/foo/target/debug/build/foo-[HASH]/build_script_build1-[HASH].dwp -The targets should have unique names. -Consider changing their names to be unique or compiling them separately. -This may become a hard error in the future; see . -[WARNING] output filename collision. -The build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)` has the same output filename as the build-script target `build-script-build1` in package `foo v0.1.0 ([ROOT]/foo)`. -Colliding filename is: [ROOT]/foo/target/debug/build/foo-[HASH]/build-script-build1.dwp -The targets should have unique names. -Consider changing their names to be unique or compiling them separately. -This may become a hard error in the future; see . -[COMPILING] foo v0.1.0 ([ROOT]/foo) -... +[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 + for stem `build1`: build1.rs, foo/build1.rs + "#]]) .run(); }