Skip to content

Commit ed8b696

Browse files
authored
Accessing each build script's OUT_DIR (#15891)
This PR is continuation of my work on [GSoC Project : Build Script Delegation](https://summerofcode.withgoogle.com/programs/2025/projects/nUt4PdAA) ### What does this PR try to resolve? Through this PR, I want the user to be able to access each build script's `OUT_DIR`. ### How to test and review this PR? There is a feature gate `multiple-build-scripts` that can be passed via `cargo-features` in `Cargo.toml`. So, you have to add ```toml cargo-features = ["multiple-build-scripts"] ``` Preferably on the top of the `Cargo.toml` and use nightly toolchain to use the feature Then, you can access the `OUT_DIR` of given build script by accessing it like a compile time environment variable, for example: ```rust const BUILD1_OUT_DIR: &'static str = env!("BUILD1_OUT_DIR"); const BUILD2_OUT_DIR: &'static str = env!("BUILD2_OUT_DIR"); fn main() { println!("{}", BUILD1_OUT_DIR); println!("{}", BUILD2_OUT_DIR); } ```
2 parents 2394ea6 + 4d74451 commit ed8b696

File tree

4 files changed

+177
-12
lines changed

4 files changed

+177
-12
lines changed

src/cargo/core/compiler/mod.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,15 +1710,34 @@ fn build_deps_args(
17101710

17111711
let mut unstable_opts = false;
17121712

1713-
for dep in deps {
1714-
if dep.unit.mode.is_run_custom_build() {
1715-
cmd.env(
1716-
"OUT_DIR",
1717-
&build_runner.files().build_script_out_dir(&dep.unit),
1718-
);
1713+
// Add `OUT_DIR` environment variables for build scripts
1714+
let first_custom_build_dep = deps.iter().find(|dep| dep.unit.mode.is_run_custom_build());
1715+
if let Some(dep) = first_custom_build_dep {
1716+
let out_dir = &build_runner.files().build_script_out_dir(&dep.unit);
1717+
cmd.env("OUT_DIR", &out_dir);
1718+
}
1719+
1720+
// Adding output directory for each build script
1721+
let is_multiple_build_scripts_enabled = unit
1722+
.pkg
1723+
.manifest()
1724+
.unstable_features()
1725+
.require(Feature::multiple_build_scripts())
1726+
.is_ok();
1727+
1728+
if is_multiple_build_scripts_enabled {
1729+
for dep in deps {
1730+
if dep.unit.mode.is_run_custom_build() {
1731+
let out_dir = &build_runner.files().build_script_out_dir(&dep.unit);
1732+
let target_name = dep.unit.target.name();
1733+
let out_dir_prefix = target_name
1734+
.strip_prefix("build-script-")
1735+
.unwrap_or(target_name);
1736+
let out_dir_name = format!("{out_dir_prefix}_OUT_DIR");
1737+
cmd.env(&out_dir_name, &out_dir);
1738+
}
17191739
}
17201740
}
1721-
17221741
for arg in extern_args(build_runner, unit, &mut unstable_opts)? {
17231742
cmd.arg(arg);
17241743
}

src/cargo/util/toml/targets.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
//! It is a bit tricky because we need match explicit information from `Cargo.toml`
1111
//! with implicit info in directory layout.
1212
13-
use std::collections::HashSet;
13+
use std::collections::{HashMap, HashSet};
14+
use std::fmt::Write;
1415
use std::fs::{self, DirEntry};
1516
use std::path::{Path, PathBuf};
1617

@@ -104,6 +105,7 @@ pub(super) fn to_targets(
104105
if metabuild.is_some() {
105106
anyhow::bail!("cannot specify both `metabuild` and `build`");
106107
}
108+
validate_unique_build_scripts(custom_build)?;
107109
for script in custom_build {
108110
let script_path = Path::new(script);
109111
let name = format!(
@@ -901,6 +903,31 @@ fn validate_unique_names(targets: &[TomlTarget], target_kind: &str) -> CargoResu
901903
Ok(())
902904
}
903905

906+
/// Will check a list of build scripts, and make sure script file stems are unique within a vector.
907+
fn validate_unique_build_scripts(scripts: &[String]) -> CargoResult<()> {
908+
let mut seen = HashMap::new();
909+
for script in scripts {
910+
let stem = Path::new(script).file_stem().unwrap().to_str().unwrap();
911+
seen.entry(stem)
912+
.or_insert_with(Vec::new)
913+
.push(script.as_str());
914+
}
915+
let mut conflict_file_stem = false;
916+
let mut err_msg = String::from(
917+
"found build scripts with duplicate file stems, but all build scripts must have a unique file stem",
918+
);
919+
for (stem, paths) in seen {
920+
if paths.len() > 1 {
921+
conflict_file_stem = true;
922+
write!(&mut err_msg, "\n for stem `{stem}`: {}", paths.join(", "))?;
923+
}
924+
}
925+
if conflict_file_stem {
926+
anyhow::bail!(err_msg);
927+
}
928+
Ok(())
929+
}
930+
904931
fn configure(
905932
toml: &TomlTarget,
906933
target: &mut Target,

src/doc/src/reference/unstable.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,10 @@ version = "0.0.1"
324324
build = ["foo.rs", "bar.rs"]
325325
```
326326

327+
**Accessing Output Directories**: Output directory of each build script can be accessed by using `<script-name>_OUT_DIR`
328+
where the `<script-name>` is the file-stem of the build script, exactly as-is.
329+
For example, `bar_OUT_DIR` for script at `foo/bar.rs`. (Only set during compilation, can be accessed via `env!` macro)
330+
327331
## public-dependency
328332
* Tracking Issue: [#44663](https://github.com/rust-lang/rust/issues/44663)
329333

tests/testsuite/build_scripts_multiple.rs

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ fn build_script_with_conflicting_out_dirs() {
549549
build = ["build1.rs", "build2.rs"]
550550
"#,
551551
)
552-
// OUT_DIR is set to the lexicographically largest build script's OUT_DIR by default
552+
// By default, OUT_DIR is set to that of the first build script in the array
553553
.file(
554554
"src/main.rs",
555555
r#"
@@ -603,7 +603,7 @@ fn build_script_with_conflicting_out_dirs() {
603603
.masquerade_as_nightly_cargo(&["multiple-build-scripts"])
604604
.with_status(0)
605605
.with_stdout_data(str![[r#"
606-
Hello, from Build Script 2!
606+
Hello, from Build Script 1!
607607
608608
"#]])
609609
.run();
@@ -628,7 +628,7 @@ fn build_script_with_conflicts_reverse_sorted() {
628628
build = ["build2.rs", "build1.rs"]
629629
"#,
630630
)
631-
// OUT_DIR is set to the lexicographically largest build script's OUT_DIR by default
631+
// By default, OUT_DIR is set to that of the first build script in the array
632632
.file(
633633
"src/main.rs",
634634
r#"
@@ -682,7 +682,7 @@ fn build_script_with_conflicts_reverse_sorted() {
682682
.masquerade_as_nightly_cargo(&["multiple-build-scripts"])
683683
.with_status(0)
684684
.with_stdout_data(str![[r#"
685-
Hello, from Build Script 1!
685+
Hello, from Build Script 2!
686686
687687
"#]])
688688
.run();
@@ -764,3 +764,118 @@ fn bar() {
764764
"#]])
765765
.run();
766766
}
767+
768+
#[cargo_test]
769+
fn multiple_out_dirs() {
770+
// Test to verify access to the `OUT_DIR` of the respective build scripts.
771+
772+
let p = project()
773+
.file(
774+
"Cargo.toml",
775+
r#"
776+
cargo-features = ["multiple-build-scripts"]
777+
778+
[package]
779+
name = "foo"
780+
version = "0.1.0"
781+
edition = "2024"
782+
build = ["build1.rs", "build2.rs"]
783+
"#,
784+
)
785+
.file(
786+
"src/main.rs",
787+
r#"
788+
include!(concat!(env!("build1_OUT_DIR"), "/foo.rs"));
789+
include!(concat!(env!("build2_OUT_DIR"), "/foo.rs"));
790+
fn main() {
791+
println!("{}", message1());
792+
println!("{}", message2());
793+
}
794+
"#,
795+
)
796+
.file(
797+
"build1.rs",
798+
r#"
799+
use std::env;
800+
use std::fs;
801+
use std::path::Path;
802+
803+
fn main() {
804+
let out_dir = env::var_os("OUT_DIR").unwrap();
805+
let dest_path = Path::new(&out_dir).join("foo.rs");
806+
fs::write(
807+
&dest_path,
808+
"pub fn message1() -> &'static str {
809+
\"Hello, from Build Script 1!\"
810+
}
811+
"
812+
).unwrap();
813+
}"#,
814+
)
815+
.file(
816+
"build2.rs",
817+
r#"
818+
use std::env;
819+
use std::fs;
820+
use std::path::Path;
821+
822+
fn main() {
823+
let out_dir = env::var_os("OUT_DIR").unwrap();
824+
let dest_path = Path::new(&out_dir).join("foo.rs");
825+
fs::write(
826+
&dest_path,
827+
"pub fn message2() -> &'static str {
828+
\"Hello, from Build Script 2!\"
829+
}
830+
"
831+
).unwrap();
832+
}"#,
833+
)
834+
.build();
835+
836+
p.cargo("run -v")
837+
.masquerade_as_nightly_cargo(&["multiple-build-scripts"])
838+
.with_status(0)
839+
.with_stdout_data(str![[r#"
840+
Hello, from Build Script 1!
841+
Hello, from Build Script 2!
842+
843+
"#]])
844+
.run();
845+
}
846+
847+
#[cargo_test]
848+
fn duplicate_build_script_stems() {
849+
// Test to verify that duplicate build script file stems throws error.
850+
851+
let p = project()
852+
.file(
853+
"Cargo.toml",
854+
r#"
855+
cargo-features = ["multiple-build-scripts"]
856+
857+
[package]
858+
name = "foo"
859+
version = "0.1.0"
860+
edition = "2024"
861+
build = ["build1.rs", "foo/build1.rs"]
862+
"#,
863+
)
864+
.file("src/main.rs", "fn main() {}")
865+
.file("build1.rs", "fn main() {}")
866+
.file("foo/build1.rs", "fn main() {}")
867+
.build();
868+
869+
p.cargo("check -v")
870+
.masquerade_as_nightly_cargo(&["multiple-build-scripts"])
871+
.with_status(101)
872+
.with_stderr_data(str![[r#"
873+
[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml`
874+
875+
Caused by:
876+
found build scripts with duplicate file stems, but all build scripts must have a unique file stem
877+
for stem `build1`: build1.rs, foo/build1.rs
878+
879+
"#]])
880+
.run();
881+
}

0 commit comments

Comments
 (0)