Skip to content
25 changes: 22 additions & 3 deletions crates/cargo-util-schemas/manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@
"build": {
"anyOf": [
{
"$ref": "#/$defs/StringOrBool"
"$ref": "#/$defs/TomlPackageBuild"
},
{
"type": "null"
Expand Down Expand Up @@ -540,13 +540,22 @@
}
]
},
"StringOrBool": {
"TomlPackageBuild": {
"anyOf": [
{
"description": "If build scripts are disabled or enabled.\n If true, `build.rs` in the root folder will be the build script.",
"type": "boolean"
},
{
"description": "Path of Build Script if there's just one script.",
"type": "string"
},
{
"type": "boolean"
"description": "Vector of paths if multiple build script are to be used.",
"type": "array",
"items": {
"type": "string"
}
}
]
},
Expand Down Expand Up @@ -596,6 +605,16 @@
}
]
},
"StringOrBool": {
"anyOf": [
{
"type": "string"
},
{
"type": "boolean"
}
]
},
"TomlValue": true,
"TomlTarget": {
"type": "object",
Expand Down
43 changes: 36 additions & 7 deletions crates/cargo-util-schemas/src/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ pub struct TomlPackage {
pub name: Option<PackageName>,
pub version: Option<InheritableSemverVersion>,
pub authors: Option<InheritableVecString>,
pub build: Option<StringOrBool>,
pub build: Option<TomlPackageBuild>,
pub metabuild: Option<StringOrVec>,
pub default_target: Option<String>,
pub forced_target: Option<String>,
Expand Down Expand Up @@ -254,12 +254,13 @@ impl TomlPackage {
self.authors.as_ref().map(|v| v.normalized()).transpose()
}

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

Expand Down Expand Up @@ -1702,6 +1703,34 @@ impl<'de> Deserialize<'de> for StringOrBool {
}
}

#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
#[serde(untagged)]
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
pub enum TomlPackageBuild {
/// If build scripts are disabled or enabled.
/// If true, `build.rs` in the root folder will be the build script.
Auto(bool),

/// Path of Build Script if there's just one script.
SingleScript(String),

/// Vector of paths if multiple build script are to be used.
MultipleScript(Vec<String>),
}

impl<'de> Deserialize<'de> for TomlPackageBuild {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
UntaggedEnumVisitor::new()
.bool(|b| Ok(TomlPackageBuild::Auto(b)))
.string(|s| Ok(TomlPackageBuild::SingleScript(s.to_owned())))
.seq(|value| value.deserialize().map(TomlPackageBuild::MultipleScript))
.deserialize(deserializer)
}
}

#[derive(PartialEq, Clone, Debug, Serialize)]
#[serde(untagged)]
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
Expand Down
3 changes: 3 additions & 0 deletions src/cargo/core/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,9 @@ features! {

/// Allows use of editions that are not yet stable.
(unstable, unstable_editions, "", "reference/unstable.html#unstable-editions"),

/// Allows use of multiple build scripts.
(unstable, multiple_build_scripts, "", "reference/unstable.html#multiple-build-scripts"),
}

/// Status and metadata for a single unstable feature.
Expand Down
44 changes: 26 additions & 18 deletions src/cargo/ops/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::util::{try_canonicalize, CargoResult, GlobalContext};
use anyhow::{bail, Context as _};
use cargo_util::{paths, Sha256};
use cargo_util_schemas::core::SourceKind;
use cargo_util_schemas::manifest::TomlPackageBuild;
use serde::Serialize;
use walkdir::WalkDir;

Expand Down Expand Up @@ -513,24 +514,31 @@ fn prepare_toml_for_vendor(
.package
.as_mut()
.expect("venedored manifests must have packages");
if let Some(cargo_util_schemas::manifest::StringOrBool::String(path)) = &package.build {
let path = paths::normalize_path(Path::new(path));
let included = packaged_files.contains(&path);
let build = if included {
let path = path
.into_os_string()
.into_string()
.map_err(|_err| anyhow::format_err!("non-UTF8 `package.build`"))?;
let path = crate::util::toml::normalize_path_string_sep(path);
cargo_util_schemas::manifest::StringOrBool::String(path)
} else {
gctx.shell().warn(format!(
"ignoring `package.build` as `{}` is not included in the published package",
path.display()
))?;
cargo_util_schemas::manifest::StringOrBool::Bool(false)
};
package.build = Some(build);
// Validates if build script file is included in package. If not, warn and ignore.
if let Some(custom_build_scripts) = package.normalized_build().expect("previously normalized") {
let mut included_scripts = Vec::new();
for script in custom_build_scripts {
let path = paths::normalize_path(Path::new(script));
let included = packaged_files.contains(&path);
if included {
let path = path
.into_os_string()
.into_string()
.map_err(|_err| anyhow::format_err!("non-UTF8 `package.build`"))?;
let path = crate::util::toml::normalize_path_string_sep(path);
included_scripts.push(path);
} else {
gctx.shell().warn(format!(
"ignoring `package.build` entry `{}` as it is not included in the published package",
path.display()
))?;
}
}
package.build = Some(match included_scripts.len() {
0 => TomlPackageBuild::Auto(false),
1 => TomlPackageBuild::SingleScript(included_scripts[0].clone()),
_ => TomlPackageBuild::MultipleScript(included_scripts),
});
}

let lib = if let Some(target) = &me.lib {
Expand Down
55 changes: 34 additions & 21 deletions src/cargo/util/toml/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use cargo_platform::Platform;
use cargo_util::paths;
use cargo_util_schemas::manifest::{
self, PackageName, PathBaseName, TomlDependency, TomlDetailedDependency, TomlManifest,
TomlWorkspace,
TomlPackageBuild, TomlWorkspace,
};
use cargo_util_schemas::manifest::{RustVersion, StringOrBool};
use itertools::Itertools;
Expand Down Expand Up @@ -344,6 +344,7 @@ fn normalize_toml(
is_embedded,
gctx,
&inherit,
features,
)?;
let package_name = &normalized_package
.normalized_name()
Expand Down Expand Up @@ -607,6 +608,7 @@ fn normalize_package_toml<'a>(
is_embedded: bool,
gctx: &GlobalContext,
inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>,
features: &Features,
) -> CargoResult<Box<manifest::TomlPackage>> {
let package_root = manifest_file.parent().unwrap();

Expand Down Expand Up @@ -670,9 +672,12 @@ fn normalize_package_toml<'a>(
.transpose()?
.map(manifest::InheritableField::Value);
let build = if is_embedded {
Some(StringOrBool::Bool(false))
Some(TomlPackageBuild::Auto(false))
} else {
targets::normalize_build(original_package.build.as_ref(), package_root)
if let Some(TomlPackageBuild::MultipleScript(_)) = original_package.build {
features.require(Feature::multiple_build_scripts())?;
}
targets::normalize_build(original_package.build.as_ref(), package_root)?
};
let metabuild = original_package.metabuild.clone();
let default_target = original_package.default_target.clone();
Expand Down Expand Up @@ -2885,24 +2890,32 @@ fn prepare_toml_for_publish(

let mut package = me.package().unwrap().clone();
package.workspace = None;
if let Some(StringOrBool::String(path)) = &package.build {
let path = Path::new(path).to_path_buf();
let included = packaged_files.map(|i| i.contains(&path)).unwrap_or(true);
let build = if included {
let path = path
.into_os_string()
.into_string()
.map_err(|_err| anyhow::format_err!("non-UTF8 `package.build`"))?;
let path = normalize_path_string_sep(path);
StringOrBool::String(path)
} else {
ws.gctx().shell().warn(format!(
"ignoring `package.build` as `{}` is not included in the published package",
path.display()
))?;
StringOrBool::Bool(false)
};
package.build = Some(build);
// Validates if build script file is included in package. If not, warn and ignore.
if let Some(custom_build_scripts) = package.normalized_build().expect("previously normalized") {
let mut included_scripts = Vec::new();
for script in custom_build_scripts {
let path = Path::new(script).to_path_buf();
let included = packaged_files.map(|i| i.contains(&path)).unwrap_or(true);
if included {
let path = path
.into_os_string()
.into_string()
.map_err(|_err| anyhow::format_err!("non-UTF8 `package.build`"))?;
let path = normalize_path_string_sep(path);
included_scripts.push(path);
} else {
ws.gctx().shell().warn(format!(
"ignoring `package.build` entry `{}` as it is not included in the published package",
path.display()
))?;
}
}

package.build = Some(match included_scripts.len() {
0 => TomlPackageBuild::Auto(false),
1 => TomlPackageBuild::SingleScript(included_scripts[0].clone()),
_ => TomlPackageBuild::MultipleScript(included_scripts),
});
}
let current_resolver = package
.resolver
Expand Down
52 changes: 30 additions & 22 deletions src/cargo/util/toml/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use std::path::{Path, PathBuf};
use anyhow::Context as _;
use cargo_util::paths;
use cargo_util_schemas::manifest::{
PathValue, StringOrBool, StringOrVec, TomlBenchTarget, TomlBinTarget, TomlExampleTarget,
TomlLibTarget, TomlManifest, TomlTarget, TomlTestTarget,
PathValue, StringOrVec, TomlBenchTarget, TomlBinTarget, TomlExampleTarget, TomlLibTarget,
TomlManifest, TomlPackageBuild, TomlTarget, TomlTestTarget,
};

use crate::core::compiler::rustdoc::RustdocScrapeExamples;
Expand Down Expand Up @@ -105,19 +105,21 @@ pub(super) fn to_targets(
if metabuild.is_some() {
anyhow::bail!("cannot specify both `metabuild` and `build`");
}
let custom_build = Path::new(custom_build);
let name = format!(
"build-script-{}",
custom_build
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
);
targets.push(Target::custom_build_target(
&name,
package_root.join(custom_build),
edition,
));
for script in custom_build {
let script_path = Path::new(script);
let name = format!(
"build-script-{}",
script_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
);
targets.push(Target::custom_build_target(
&name,
package_root.join(script_path),
edition,
));
}
}
if let Some(metabuild) = metabuild {
// Verify names match available build deps.
Expand Down Expand Up @@ -1076,29 +1078,35 @@ Cargo doesn't know which to use because multiple target files found at `{}` and

/// Returns the path to the build script if one exists for this crate.
#[tracing::instrument(skip_all)]
pub fn normalize_build(build: Option<&StringOrBool>, package_root: &Path) -> Option<StringOrBool> {
pub fn normalize_build(
build: Option<&TomlPackageBuild>,
package_root: &Path,
) -> CargoResult<Option<TomlPackageBuild>> {
const BUILD_RS: &str = "build.rs";
match build {
None => {
// If there is a `build.rs` file next to the `Cargo.toml`, assume it is
// a build script.
let build_rs = package_root.join(BUILD_RS);
if build_rs.is_file() {
Some(StringOrBool::String(BUILD_RS.to_owned()))
Ok(Some(TomlPackageBuild::SingleScript(BUILD_RS.to_owned())))
} else {
Some(StringOrBool::Bool(false))
Ok(Some(TomlPackageBuild::Auto(false)))
}
}
// Explicitly no build script.
Some(StringOrBool::Bool(false)) => build.cloned(),
Some(StringOrBool::String(build_file)) => {
Some(TomlPackageBuild::Auto(false)) => Ok(build.cloned()),
Some(TomlPackageBuild::SingleScript(build_file)) => {
let build_file = paths::normalize_path(Path::new(build_file));
let build = build_file.into_os_string().into_string().expect(
"`build_file` started as a String and `normalize_path` shouldn't have changed that",
);
Some(StringOrBool::String(build))
Ok(Some(TomlPackageBuild::SingleScript(build)))
}
Some(TomlPackageBuild::Auto(true)) => {
Ok(Some(TomlPackageBuild::SingleScript(BUILD_RS.to_owned())))
}
Some(StringOrBool::Bool(true)) => Some(StringOrBool::String(BUILD_RS.to_owned())),
Some(TomlPackageBuild::MultipleScript(_scripts)) => Ok(build.cloned()),
}
}

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

## Multiple Build Scripts
* Tracking Issue: [#14903](https://github.com/rust-lang/cargo/issues/14903)
* Original Pull Request: [#15630](https://github.com/rust-lang/cargo/pull/15630)

Multiple Build Scripts feature allows you to have multiple build scripts in your package.

Include `cargo-features` at the top of `Cargo.toml` and add `multiple-build-scripts` to enable feature.
Add the paths of the build scripts as an array in `package.build`. For example:

```toml
cargo-features = ["multiple-build-scripts"]

[package]
name = "mypackage"
version = "0.0.1"
build = ["foo.rs", "bar.rs"]
```

## public-dependency
* Tracking Issue: [#44663](https://github.com/rust-lang/rust/issues/44663)

Expand Down
Loading