//! Tests for multiple build scripts feature. use crate::prelude::*; use cargo_test_support::compare::assert_e2e; use cargo_test_support::git; use cargo_test_support::publish::validate_crate_contents; use cargo_test_support::str; use cargo_test_support::{Project, project}; use std::fs::File; #[cargo_test] fn build_without_feature_enabled_aborts_with_error() { let p = project() .file( "Cargo.toml", r#" [package] name = "foo" version = "0.1.0" edition = "2024" build = ["build1.rs", "build2.rs"] "#, ) .file("src/main.rs", "fn main() {}") .file("build1.rs", "fn main() {}") .file("build2.rs", "fn main() {}") .build(); p.cargo("check") .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: feature `multiple-build-scripts` is required The package requires the Cargo feature called `multiple-build-scripts`, but that feature is not stabilized in this version of Cargo ([..]). Consider adding `cargo-features = ["multiple-build-scripts"]` to the top of Cargo.toml (above the [package] table) to tell Cargo you are opting in to use this unstable feature. See https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#multiple-build-scripts for more information about the status of this feature. "#]]) .run(); } fn basic_empty_project() -> Project { 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", "fn main() {}") .file("build1.rs", "fn main() {}") .file("build2.rs", "fn main() {}") .build() } #[cargo_test] fn empty_multiple_build_script_project() { let p = basic_empty_project(); p.cargo("check") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_status(0) .with_stderr_data(str![[r#" [COMPILING] foo v0.1.0 ([ROOT]/foo) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) .run(); } #[cargo_test] fn multiple_build_scripts_metadata() { let p = basic_empty_project(); p.cargo("metadata --format-version=1") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_status(0) .with_stderr_data("") .with_stdout_data( str![[r#" { "metadata": null, "packages": [ { "authors": [], "categories": [], "default_run": null, "dependencies": [], "description": null, "documentation": null, "edition": "2024", "features": {}, "homepage": null, "id": "path+[ROOTURL]/foo#0.1.0", "keywords": [], "license": null, "license_file": null, "links": null, "manifest_path": "[ROOT]/foo/Cargo.toml", "metadata": null, "name": "foo", "publish": null, "readme": null, "repository": null, "rust_version": null, "source": null, "targets": [ { "crate_types": [ "bin" ], "doc": true, "doctest": false, "edition": "2024", "kind": [ "bin" ], "name": "foo", "src_path": "[ROOT]/foo/src/main.rs", "test": true }, { "crate_types": [ "bin" ], "doc": false, "doctest": false, "edition": "2024", "kind": [ "custom-build" ], "name": "build-script-build1", "src_path": "[ROOT]/foo/build1.rs", "test": false }, { "crate_types": [ "bin" ], "doc": false, "doctest": false, "edition": "2024", "kind": [ "custom-build" ], "name": "build-script-build2", "src_path": "[ROOT]/foo/build2.rs", "test": false } ], "version": "0.1.0" } ], "resolve": { "nodes": [ { "dependencies": [], "deps": [], "features": [], "id": "path+[ROOTURL]/foo#0.1.0" } ], "root": "path+[ROOTURL]/foo#0.1.0" }, "target_directory": "[ROOT]/foo/target", "build_directory": "[ROOT]/foo/target", "version": 1, "workspace_default_members": [ "path+[ROOTURL]/foo#0.1.0" ], "workspace_members": [ "path+[ROOTURL]/foo#0.1.0" ], "workspace_root": "[ROOT]/foo" } "#]] .is_json(), ) .run(); } #[cargo_test] fn verify_package_multiple_build_scripts() { let p = project() .file( "Cargo.toml", r#" cargo-features = ["multiple-build-scripts"] [package] name = "foo" version = "0.1.0" edition = "2024" license = "MIT" description = "foo" documentation = "docs.rs/foo" authors = [] build = ["build1.rs", "build2.rs"] include = [ "src/main.rs", "build1.rs" ] "#, ) .file("src/main.rs", "fn main() {}") .file("build1.rs", "fn main() {}") .file("build2.rs", "fn main() {}") .build(); p.cargo("package") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_status(0) .with_stderr_data(str![[r#" [PACKAGING] foo v0.1.0 ([ROOT]/foo) [WARNING] ignoring `package.build` entry `build2.rs` as it is not included in the published package [PACKAGED] 5 files, [FILE_SIZE]B ([FILE_SIZE]B compressed) [VERIFYING] foo v0.1.0 ([ROOT]/foo) [COMPILING] foo v0.1.0 ([ROOT]/foo/target/package/foo-0.1.0) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) .run(); let f = File::open(&p.root().join("target/package/foo-0.1.0.crate")).unwrap(); validate_crate_contents( f, "foo-0.1.0.crate", &[ "Cargo.toml", "Cargo.toml.orig", "src/main.rs", "build1.rs", "Cargo.lock", ], [( "Cargo.toml", str![[r##" # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. cargo-features = ["multiple-build-scripts"] [package] edition = "2024" name = "foo" version = "0.1.0" authors = [] build = "build1.rs" include = [ "src/main.rs", "build1.rs", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "foo" documentation = "docs.rs/foo" readme = false license = "MIT" [[bin]] name = "foo" path = "src/main.rs" "##]], )], ); } fn add_git_vendor_config(p: &Project, git_project: &Project) { p.change_file( ".cargo/config.toml", &format!( r#" [source."git+{url}"] git = "{url}" replace-with = 'vendor' [source.vendor] directory = 'vendor' "#, url = git_project.url() ), ); } #[cargo_test] fn verify_vendor_multiple_build_scripts() { let git_project = git::new("dep", |project| { project .file( "Cargo.toml", r#" cargo-features = ["multiple-build-scripts"] [package] name = "dep" version = "0.1.0" edition = "2024" license = "MIT" description = "dependency of foo" documentation = "docs.rs/dep" authors = [] build = ["build1.rs", "build2.rs"] include = [ "src/main.rs", "build1.rs" ] "#, ) .file("src/main.rs", "fn main() {}") .file("build1.rs", "fn main() {}") .file("build2.rs", "fn main() {}") }); let p = project() .file( "Cargo.toml", &format!( r#" cargo-features = ["multiple-build-scripts"] [package] name = "foo" version = "0.1.0" edition = "2024" [dependencies.dep] git = '{}' "#, git_project.url() ), ) .file("src/main.rs", "fn main() {}") .build(); p.cargo("vendor --respect-source-config") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_status(0) .with_stderr_data(str![[r#" [UPDATING] git repository `[ROOTURL]/dep` [LOCKING] 1 package to latest [..] compatible version Vendoring dep v0.1.0 ([ROOTURL]/dep#[..]) ([ROOT]/home/.cargo/git/checkouts/dep-[HASH]/[..]) to vendor/dep [WARNING] ignoring `package.build` entry `build2.rs` as it is not included in the published package To use vendored sources, add this to your .cargo/config.toml for this project: "#]]) .run(); add_git_vendor_config(&p, &git_project); assert_e2e().eq( p.read_file("vendor/dep/Cargo.toml"), str![[r##" # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. cargo-features = ["multiple-build-scripts"] [package] edition = "2024" name = "dep" version = "0.1.0" authors = [] build = "build1.rs" include = [ "src/main.rs", "build1.rs", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "dependency of foo" documentation = "docs.rs/dep" readme = false license = "MIT" [[bin]] name = "dep" path = "src/main.rs" "##]], ); p.cargo("check") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .run(); } #[cargo_test] fn custom_build_script_first_index_script_failed() { // In this, the script that is at first index in the build script array fails 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", "fn main() {}") .file("build1.rs", "fn main() { std::process::exit(101); }") .file("build2.rs", "fn main() {}") .build(); p.cargo("check -v") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_status(101) .with_stderr_data(str![[r#" [COMPILING] foo v0.1.0 ([ROOT]/foo) ... [ERROR] failed to run custom build command for `foo v0.1.0 ([ROOT]/foo)` Caused by: process didn't exit successfully: `[ROOT]/foo/target/debug/build/foo-[HASH]/build-script-build1` ([EXIT_STATUS]: 101) ... "#]]) .run(); } #[cargo_test] fn custom_build_script_second_index_script_failed() { // In this, the script that is at second index in the build script array fails // This test was necessary because earlier, the program failed only if first script failed. 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", "fn main() {}") .file("build1.rs", "fn main() {}") .file("build2.rs", "fn main() { std::process::exit(101); }") .build(); p.cargo("check -v") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_status(101) .with_stderr_data(str![[r#" [COMPILING] foo v0.1.0 ([ROOT]/foo) ... [ERROR] failed to run custom build command for `foo v0.1.0 ([ROOT]/foo)` Caused by: process didn't exit successfully: `[ROOT]/foo/target/debug/build/foo-[HASH]/build-script-build2` ([EXIT_STATUS]: 101) ... "#]]) .run(); } #[cargo_test] fn build_script_with_conflicting_environment_variables() { // In this, multiple scripts set different values to same environment variables 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#" const FOO: &'static str = env!("FOO"); fn main() { println!("{}", FOO); } "#, ) .file( "build1.rs", r#"fn main() { println!("cargo::rustc-env=FOO=bar1"); }"#, ) .file( "build2.rs", r#"fn main() { println!("cargo::rustc-env=FOO=bar2"); }"#, ) .build(); p.cargo("run -v") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_status(0) .with_stdout_data(str![[r#" bar2 "#]]) .run(); } #[cargo_test] fn build_script_with_conflicting_out_dirs() { // In this, multiple scripts create file with same name in their respective OUT_DIR. 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"] "#, ) // OUT_DIR is set to the lexicographically largest build script's OUT_DIR by default .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 build_script_with_conflicts_reverse_sorted() { // In this, multiple scripts create file with same name in their respective OUT_DIR. // It is different from above because `package.build` is not sorted in this. let p = project() .file( "Cargo.toml", r#" cargo-features = ["multiple-build-scripts"] [package] name = "foo" version = "0.1.0" edition = "2024" build = ["build2.rs", "build1.rs"] "#, ) // OUT_DIR is set to the lexicographically largest build script's OUT_DIR by default .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 1! "#]]) .run(); } #[cargo_test] fn rerun_untracks_other_files() { 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", "fn main() {}") .file( "build1.rs", r#" fn main() { foo(); } fn foo() { let _path = "assets/foo.txt"; } "#, ) .file( "build2.rs", r#" fn main() { bar(); } fn bar() { let path = "assets/bar.txt"; println!("cargo::rerun-if-changed={path}"); }"#, ) .file("assets/foo.txt", "foo") .file("assets/bar.txt", "bar") .build(); p.cargo("check") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .run(); // Editing foo.txt will also recompile now since they are separate build scripts p.change_file("assets/foo.txt", "foo updated"); p.cargo("check -v") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_stderr_data(str![[r#" [DIRTY] foo v0.1.0 ([ROOT]/foo): the [..] [COMPILING] foo v0.1.0 ([ROOT]/foo) [RUNNING] `[ROOT]/foo/target/debug/build/foo-[HASH]/build-script-build1` [RUNNING] `rustc --crate-name foo --edition=2024 src/main.rs [..] --crate-type bin [..] [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) .run(); // Editing bar.txt will also recompile now since they are separate build scripts p.change_file("assets/bar.txt", "bar updated"); p.cargo("check -v") .masquerade_as_nightly_cargo(&["multiple-build-scripts"]) .with_stderr_data(str![[r#" [DIRTY] foo v0.1.0 ([ROOT]/foo): the [..] [COMPILING] foo v0.1.0 ([ROOT]/foo) [RUNNING] `[ROOT]/foo/target/debug/build/foo-[HASH]/build-script-build[..]` [RUNNING] `[ROOT]/foo/target/debug/build/foo-[HASH]/build-script-build[..]` [RUNNING] `rustc --crate-name foo --edition=2024 src/main.rs [..] --crate-type bin [..] [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) .run(); }