Skip to content

Commit f94409a

Browse files
authored
feat(toml): Parse support for multiple build scripts (#15630)
Hi Everyone! This is PR for the manifest parsing of the first milestone of [GSoC Project : Build Script Delegation](https://summerofcode.withgoogle.com/programs/2025/projects/nUt4PdAA) ### What does this PR try to resolve? Currently, just a single build script is allowed for each package. This PR will allow users to create and use multiple build scripts, and is backward compatible with single script as well as boolean values. **Motivation :** This will help users to maintain separate smaller and cleaner build scripts instead of one large build script. This is also necessary for build script delegation. **Open questions:** - What should the build script target names be? - Currently they use the file stem of the build script which could run into conflicts **Known Issues:** - This is just parsing support, and currently, only the first build script of the array is actually executed. ### 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 **This PR is ready to be reviewed and merged**
2 parents 84709f0 + 5c7f68f commit f94409a

File tree

11 files changed

+645
-76
lines changed

11 files changed

+645
-76
lines changed

crates/cargo-util-schemas/manifest.schema.json

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@
269269
"build": {
270270
"anyOf": [
271271
{
272-
"$ref": "#/$defs/StringOrBool"
272+
"$ref": "#/$defs/TomlPackageBuild"
273273
},
274274
{
275275
"type": "null"
@@ -540,13 +540,22 @@
540540
}
541541
]
542542
},
543-
"StringOrBool": {
543+
"TomlPackageBuild": {
544544
"anyOf": [
545545
{
546+
"description": "If build scripts are disabled or enabled.\n If true, `build.rs` in the root folder will be the build script.",
547+
"type": "boolean"
548+
},
549+
{
550+
"description": "Path of Build Script if there's just one script.",
546551
"type": "string"
547552
},
548553
{
549-
"type": "boolean"
554+
"description": "Vector of paths if multiple build script are to be used.",
555+
"type": "array",
556+
"items": {
557+
"type": "string"
558+
}
550559
}
551560
]
552561
},
@@ -596,6 +605,16 @@
596605
}
597606
]
598607
},
608+
"StringOrBool": {
609+
"anyOf": [
610+
{
611+
"type": "string"
612+
},
613+
{
614+
"type": "boolean"
615+
}
616+
]
617+
},
599618
"TomlValue": true,
600619
"TomlTarget": {
601620
"type": "object",

crates/cargo-util-schemas/src/manifest/mod.rs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ pub struct TomlPackage {
182182
pub name: Option<PackageName>,
183183
pub version: Option<InheritableSemverVersion>,
184184
pub authors: Option<InheritableVecString>,
185-
pub build: Option<StringOrBool>,
185+
pub build: Option<TomlPackageBuild>,
186186
pub metabuild: Option<StringOrVec>,
187187
pub default_target: Option<String>,
188188
pub forced_target: Option<String>,
@@ -254,12 +254,13 @@ impl TomlPackage {
254254
self.authors.as_ref().map(|v| v.normalized()).transpose()
255255
}
256256

257-
pub fn normalized_build(&self) -> Result<Option<&String>, UnresolvedError> {
258-
let readme = self.build.as_ref().ok_or(UnresolvedError)?;
259-
match readme {
260-
StringOrBool::Bool(false) => Ok(None),
261-
StringOrBool::Bool(true) => Err(UnresolvedError),
262-
StringOrBool::String(value) => Ok(Some(value)),
257+
pub fn normalized_build(&self) -> Result<Option<&[String]>, UnresolvedError> {
258+
let build = self.build.as_ref().ok_or(UnresolvedError)?;
259+
match build {
260+
TomlPackageBuild::Auto(false) => Ok(None),
261+
TomlPackageBuild::Auto(true) => Err(UnresolvedError),
262+
TomlPackageBuild::SingleScript(value) => Ok(Some(std::slice::from_ref(value))),
263+
TomlPackageBuild::MultipleScript(scripts) => Ok(Some(scripts)),
263264
}
264265
}
265266

@@ -1702,6 +1703,34 @@ impl<'de> Deserialize<'de> for StringOrBool {
17021703
}
17031704
}
17041705

1706+
#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
1707+
#[serde(untagged)]
1708+
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
1709+
pub enum TomlPackageBuild {
1710+
/// If build scripts are disabled or enabled.
1711+
/// If true, `build.rs` in the root folder will be the build script.
1712+
Auto(bool),
1713+
1714+
/// Path of Build Script if there's just one script.
1715+
SingleScript(String),
1716+
1717+
/// Vector of paths if multiple build script are to be used.
1718+
MultipleScript(Vec<String>),
1719+
}
1720+
1721+
impl<'de> Deserialize<'de> for TomlPackageBuild {
1722+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1723+
where
1724+
D: de::Deserializer<'de>,
1725+
{
1726+
UntaggedEnumVisitor::new()
1727+
.bool(|b| Ok(TomlPackageBuild::Auto(b)))
1728+
.string(|s| Ok(TomlPackageBuild::SingleScript(s.to_owned())))
1729+
.seq(|value| value.deserialize().map(TomlPackageBuild::MultipleScript))
1730+
.deserialize(deserializer)
1731+
}
1732+
}
1733+
17051734
#[derive(PartialEq, Clone, Debug, Serialize)]
17061735
#[serde(untagged)]
17071736
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]

src/cargo/core/features.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,9 @@ features! {
577577

578578
/// Allows use of editions that are not yet stable.
579579
(unstable, unstable_editions, "", "reference/unstable.html#unstable-editions"),
580+
581+
/// Allows use of multiple build scripts.
582+
(unstable, multiple_build_scripts, "", "reference/unstable.html#multiple-build-scripts"),
580583
}
581584

582585
/// Status and metadata for a single unstable feature.

src/cargo/ops/vendor.rs

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::util::{try_canonicalize, CargoResult, GlobalContext};
1212
use anyhow::{bail, Context as _};
1313
use cargo_util::{paths, Sha256};
1414
use cargo_util_schemas::core::SourceKind;
15+
use cargo_util_schemas::manifest::TomlPackageBuild;
1516
use serde::Serialize;
1617
use walkdir::WalkDir;
1718

@@ -513,24 +514,31 @@ fn prepare_toml_for_vendor(
513514
.package
514515
.as_mut()
515516
.expect("venedored manifests must have packages");
516-
if let Some(cargo_util_schemas::manifest::StringOrBool::String(path)) = &package.build {
517-
let path = paths::normalize_path(Path::new(path));
518-
let included = packaged_files.contains(&path);
519-
let build = if included {
520-
let path = path
521-
.into_os_string()
522-
.into_string()
523-
.map_err(|_err| anyhow::format_err!("non-UTF8 `package.build`"))?;
524-
let path = crate::util::toml::normalize_path_string_sep(path);
525-
cargo_util_schemas::manifest::StringOrBool::String(path)
526-
} else {
527-
gctx.shell().warn(format!(
528-
"ignoring `package.build` as `{}` is not included in the published package",
529-
path.display()
530-
))?;
531-
cargo_util_schemas::manifest::StringOrBool::Bool(false)
532-
};
533-
package.build = Some(build);
517+
// Validates if build script file is included in package. If not, warn and ignore.
518+
if let Some(custom_build_scripts) = package.normalized_build().expect("previously normalized") {
519+
let mut included_scripts = Vec::new();
520+
for script in custom_build_scripts {
521+
let path = paths::normalize_path(Path::new(script));
522+
let included = packaged_files.contains(&path);
523+
if included {
524+
let path = path
525+
.into_os_string()
526+
.into_string()
527+
.map_err(|_err| anyhow::format_err!("non-UTF8 `package.build`"))?;
528+
let path = crate::util::toml::normalize_path_string_sep(path);
529+
included_scripts.push(path);
530+
} else {
531+
gctx.shell().warn(format!(
532+
"ignoring `package.build` entry `{}` as it is not included in the published package",
533+
path.display()
534+
))?;
535+
}
536+
}
537+
package.build = Some(match included_scripts.len() {
538+
0 => TomlPackageBuild::Auto(false),
539+
1 => TomlPackageBuild::SingleScript(included_scripts[0].clone()),
540+
_ => TomlPackageBuild::MultipleScript(included_scripts),
541+
});
534542
}
535543

536544
let lib = if let Some(target) = &me.lib {

src/cargo/util/toml/mod.rs

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use cargo_platform::Platform;
1313
use cargo_util::paths;
1414
use cargo_util_schemas::manifest::{
1515
self, PackageName, PathBaseName, TomlDependency, TomlDetailedDependency, TomlManifest,
16-
TomlWorkspace,
16+
TomlPackageBuild, TomlWorkspace,
1717
};
1818
use cargo_util_schemas::manifest::{RustVersion, StringOrBool};
1919
use itertools::Itertools;
@@ -344,6 +344,7 @@ fn normalize_toml(
344344
is_embedded,
345345
gctx,
346346
&inherit,
347+
features,
347348
)?;
348349
let package_name = &normalized_package
349350
.normalized_name()
@@ -607,6 +608,7 @@ fn normalize_package_toml<'a>(
607608
is_embedded: bool,
608609
gctx: &GlobalContext,
609610
inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>,
611+
features: &Features,
610612
) -> CargoResult<Box<manifest::TomlPackage>> {
611613
let package_root = manifest_file.parent().unwrap();
612614

@@ -670,9 +672,12 @@ fn normalize_package_toml<'a>(
670672
.transpose()?
671673
.map(manifest::InheritableField::Value);
672674
let build = if is_embedded {
673-
Some(StringOrBool::Bool(false))
675+
Some(TomlPackageBuild::Auto(false))
674676
} else {
675-
targets::normalize_build(original_package.build.as_ref(), package_root)
677+
if let Some(TomlPackageBuild::MultipleScript(_)) = original_package.build {
678+
features.require(Feature::multiple_build_scripts())?;
679+
}
680+
targets::normalize_build(original_package.build.as_ref(), package_root)?
676681
};
677682
let metabuild = original_package.metabuild.clone();
678683
let default_target = original_package.default_target.clone();
@@ -2885,24 +2890,32 @@ fn prepare_toml_for_publish(
28852890

28862891
let mut package = me.package().unwrap().clone();
28872892
package.workspace = None;
2888-
if let Some(StringOrBool::String(path)) = &package.build {
2889-
let path = Path::new(path).to_path_buf();
2890-
let included = packaged_files.map(|i| i.contains(&path)).unwrap_or(true);
2891-
let build = if included {
2892-
let path = path
2893-
.into_os_string()
2894-
.into_string()
2895-
.map_err(|_err| anyhow::format_err!("non-UTF8 `package.build`"))?;
2896-
let path = normalize_path_string_sep(path);
2897-
StringOrBool::String(path)
2898-
} else {
2899-
ws.gctx().shell().warn(format!(
2900-
"ignoring `package.build` as `{}` is not included in the published package",
2901-
path.display()
2902-
))?;
2903-
StringOrBool::Bool(false)
2904-
};
2905-
package.build = Some(build);
2893+
// Validates if build script file is included in package. If not, warn and ignore.
2894+
if let Some(custom_build_scripts) = package.normalized_build().expect("previously normalized") {
2895+
let mut included_scripts = Vec::new();
2896+
for script in custom_build_scripts {
2897+
let path = Path::new(script).to_path_buf();
2898+
let included = packaged_files.map(|i| i.contains(&path)).unwrap_or(true);
2899+
if included {
2900+
let path = path
2901+
.into_os_string()
2902+
.into_string()
2903+
.map_err(|_err| anyhow::format_err!("non-UTF8 `package.build`"))?;
2904+
let path = normalize_path_string_sep(path);
2905+
included_scripts.push(path);
2906+
} else {
2907+
ws.gctx().shell().warn(format!(
2908+
"ignoring `package.build` entry `{}` as it is not included in the published package",
2909+
path.display()
2910+
))?;
2911+
}
2912+
}
2913+
2914+
package.build = Some(match included_scripts.len() {
2915+
0 => TomlPackageBuild::Auto(false),
2916+
1 => TomlPackageBuild::SingleScript(included_scripts[0].clone()),
2917+
_ => TomlPackageBuild::MultipleScript(included_scripts),
2918+
});
29062919
}
29072920
let current_resolver = package
29082921
.resolver

src/cargo/util/toml/targets.rs

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use std::path::{Path, PathBuf};
1717
use anyhow::Context as _;
1818
use cargo_util::paths;
1919
use cargo_util_schemas::manifest::{
20-
PathValue, StringOrBool, StringOrVec, TomlBenchTarget, TomlBinTarget, TomlExampleTarget,
21-
TomlLibTarget, TomlManifest, TomlTarget, TomlTestTarget,
20+
PathValue, StringOrVec, TomlBenchTarget, TomlBinTarget, TomlExampleTarget, TomlLibTarget,
21+
TomlManifest, TomlPackageBuild, TomlTarget, TomlTestTarget,
2222
};
2323

2424
use crate::core::compiler::rustdoc::RustdocScrapeExamples;
@@ -105,19 +105,21 @@ pub(super) fn to_targets(
105105
if metabuild.is_some() {
106106
anyhow::bail!("cannot specify both `metabuild` and `build`");
107107
}
108-
let custom_build = Path::new(custom_build);
109-
let name = format!(
110-
"build-script-{}",
111-
custom_build
112-
.file_stem()
113-
.and_then(|s| s.to_str())
114-
.unwrap_or("")
115-
);
116-
targets.push(Target::custom_build_target(
117-
&name,
118-
package_root.join(custom_build),
119-
edition,
120-
));
108+
for script in custom_build {
109+
let script_path = Path::new(script);
110+
let name = format!(
111+
"build-script-{}",
112+
script_path
113+
.file_stem()
114+
.and_then(|s| s.to_str())
115+
.unwrap_or("")
116+
);
117+
targets.push(Target::custom_build_target(
118+
&name,
119+
package_root.join(script_path),
120+
edition,
121+
));
122+
}
121123
}
122124
if let Some(metabuild) = metabuild {
123125
// Verify names match available build deps.
@@ -1076,29 +1078,35 @@ Cargo doesn't know which to use because multiple target files found at `{}` and
10761078

10771079
/// Returns the path to the build script if one exists for this crate.
10781080
#[tracing::instrument(skip_all)]
1079-
pub fn normalize_build(build: Option<&StringOrBool>, package_root: &Path) -> Option<StringOrBool> {
1081+
pub fn normalize_build(
1082+
build: Option<&TomlPackageBuild>,
1083+
package_root: &Path,
1084+
) -> CargoResult<Option<TomlPackageBuild>> {
10801085
const BUILD_RS: &str = "build.rs";
10811086
match build {
10821087
None => {
10831088
// If there is a `build.rs` file next to the `Cargo.toml`, assume it is
10841089
// a build script.
10851090
let build_rs = package_root.join(BUILD_RS);
10861091
if build_rs.is_file() {
1087-
Some(StringOrBool::String(BUILD_RS.to_owned()))
1092+
Ok(Some(TomlPackageBuild::SingleScript(BUILD_RS.to_owned())))
10881093
} else {
1089-
Some(StringOrBool::Bool(false))
1094+
Ok(Some(TomlPackageBuild::Auto(false)))
10901095
}
10911096
}
10921097
// Explicitly no build script.
1093-
Some(StringOrBool::Bool(false)) => build.cloned(),
1094-
Some(StringOrBool::String(build_file)) => {
1098+
Some(TomlPackageBuild::Auto(false)) => Ok(build.cloned()),
1099+
Some(TomlPackageBuild::SingleScript(build_file)) => {
10951100
let build_file = paths::normalize_path(Path::new(build_file));
10961101
let build = build_file.into_os_string().into_string().expect(
10971102
"`build_file` started as a String and `normalize_path` shouldn't have changed that",
10981103
);
1099-
Some(StringOrBool::String(build))
1104+
Ok(Some(TomlPackageBuild::SingleScript(build)))
1105+
}
1106+
Some(TomlPackageBuild::Auto(true)) => {
1107+
Ok(Some(TomlPackageBuild::SingleScript(BUILD_RS.to_owned())))
11001108
}
1101-
Some(StringOrBool::Bool(true)) => Some(StringOrBool::String(BUILD_RS.to_owned())),
1109+
Some(TomlPackageBuild::MultipleScript(_scripts)) => Ok(build.cloned()),
11021110
}
11031111
}
11041112

src/doc/src/reference/unstable.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Each new feature described below should explain how to use it.
6666
* [-Z allow-features](#allow-features) --- Provides a way to restrict which unstable features are used.
6767
* Build scripts and linking
6868
* [Metabuild](#metabuild) --- Provides declarative build scripts.
69+
* [Multiple Build Scripts](#multiple-build-scripts) --- Allows use of multiple build scripts.
6970
* Resolver and features
7071
* [no-index-update](#no-index-update) --- Prevents cargo from updating the index cache.
7172
* [avoid-dev-deps](#avoid-dev-deps) --- Prevents the resolver from including dev-dependencies during resolution.
@@ -332,6 +333,24 @@ extra-info = "qwerty"
332333
Metabuild packages should have a public function called `metabuild` that
333334
performs the same actions as a regular `build.rs` script would perform.
334335

336+
## Multiple Build Scripts
337+
* Tracking Issue: [#14903](https://github.com/rust-lang/cargo/issues/14903)
338+
* Original Pull Request: [#15630](https://github.com/rust-lang/cargo/pull/15630)
339+
340+
Multiple Build Scripts feature allows you to have multiple build scripts in your package.
341+
342+
Include `cargo-features` at the top of `Cargo.toml` and add `multiple-build-scripts` to enable feature.
343+
Add the paths of the build scripts as an array in `package.build`. For example:
344+
345+
```toml
346+
cargo-features = ["multiple-build-scripts"]
347+
348+
[package]
349+
name = "mypackage"
350+
version = "0.0.1"
351+
build = ["foo.rs", "bar.rs"]
352+
```
353+
335354
## public-dependency
336355
* Tracking Issue: [#44663](https://github.com/rust-lang/rust/issues/44663)
337356

0 commit comments

Comments
 (0)