Skip to content

feat(toml): Parse support for multiple build scripts #15630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
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
Loading