use crate::PlatformTag;
use anyhow::{format_err, Result};
use fs_err as fs;
use pyproject_toml::PyProjectToml as ProjectToml;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Tool {
maturin: Option<ToolMaturin>,
}
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Format {
Sdist,
Wheel,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum Formats {
Single(Format),
Multiple(Vec<Format>),
}
impl Formats {
pub fn targets(&self, format: Format) -> bool {
match self {
Self::Single(val) if val == &format => true,
Self::Multiple(formats) if formats.contains(&format) => true,
_ => false,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum GlobPattern {
Path(String),
WithFormat {
path: String,
format: Formats,
},
}
impl GlobPattern {
pub fn targets(&self, format: Format) -> Option<&str> {
match self {
Self::Path(ref glob) => Some(glob),
Self::WithFormat {
path,
format: formats,
} if formats.targets(format) => Some(path),
_ => None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct ToolMaturin {
sdist_include: Option<Vec<String>>,
include: Option<Vec<GlobPattern>>,
exclude: Option<Vec<GlobPattern>>,
bindings: Option<String>,
#[serde(alias = "manylinux")]
compatibility: Option<PlatformTag>,
#[serde(default)]
skip_auditwheel: bool,
#[serde(default)]
strip: bool,
python_source: Option<PathBuf>,
python_packages: Option<Vec<String>>,
data: Option<PathBuf>,
pub profile: Option<String>,
pub features: Option<Vec<String>>,
pub all_features: Option<bool>,
pub no_default_features: Option<bool>,
pub manifest_path: Option<PathBuf>,
pub frozen: Option<bool>,
pub locked: Option<bool>,
pub config: Option<Vec<String>>,
pub unstable_flags: Option<Vec<String>>,
pub rustc_args: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct PyProjectToml {
#[serde(flatten)]
inner: ProjectToml,
pub tool: Option<Tool>,
}
impl std::ops::Deref for PyProjectToml {
type Target = ProjectToml;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl PyProjectToml {
pub fn new(pyproject_file: impl AsRef<Path>) -> Result<PyProjectToml> {
let path = pyproject_file.as_ref();
let contents = fs::read_to_string(path)?;
let pyproject: PyProjectToml = toml_edit::easy::from_str(&contents)
.map_err(|err| format_err!("pyproject.toml is not PEP 517 compliant: {}", err))?;
Ok(pyproject)
}
pub fn project_name(&self) -> Option<&str> {
self.project.as_ref().map(|project| project.name.as_str())
}
#[inline]
pub fn maturin(&self) -> Option<&ToolMaturin> {
self.tool.as_ref()?.maturin.as_ref()
}
#[deprecated(
since = "0.14.0",
note = "please use `PyProjectToml::include` (<https://github.com/PyO3/maturin/pulls/1255>)"
)]
pub fn sdist_include(&self) -> Option<&Vec<String>> {
self.maturin()?.sdist_include.as_ref()
}
pub fn include(&self) -> Option<&[GlobPattern]> {
self.maturin()?.include.as_ref().map(AsRef::as_ref)
}
pub fn exclude(&self) -> Option<&[GlobPattern]> {
self.maturin()?.exclude.as_ref().map(AsRef::as_ref)
}
pub fn bindings(&self) -> Option<&str> {
self.maturin()?.bindings.as_deref()
}
pub fn compatibility(&self) -> Option<PlatformTag> {
self.maturin()?.compatibility
}
pub fn skip_auditwheel(&self) -> bool {
self.maturin()
.map(|maturin| maturin.skip_auditwheel)
.unwrap_or_default()
}
pub fn strip(&self) -> bool {
self.maturin()
.map(|maturin| maturin.strip)
.unwrap_or_default()
}
pub fn python_source(&self) -> Option<&Path> {
self.maturin()
.and_then(|maturin| maturin.python_source.as_deref())
}
pub fn python_packages(&self) -> Option<&[String]> {
self.maturin()
.and_then(|maturin| maturin.python_packages.as_deref())
}
pub fn data(&self) -> Option<&Path> {
self.maturin().and_then(|maturin| maturin.data.as_deref())
}
pub fn manifest_path(&self) -> Option<&Path> {
self.maturin()?.manifest_path.as_deref()
}
pub fn warn_missing_maturin_version(&self) -> bool {
let maturin = env!("CARGO_PKG_NAME");
if let Some(requires_maturin) = self
.build_system
.requires
.iter()
.find(|x| x.starts_with(maturin))
{
assert_eq!(env!("CARGO_PKG_VERSION_MAJOR"), "0");
let current_minor: usize = env!("CARGO_PKG_VERSION_MINOR").parse().unwrap();
if requires_maturin == maturin {
eprintln!(
"⚠️ Warning: Please use {maturin} in pyproject.toml with a version constraint, \
e.g. `requires = [\"{maturin}>=0.{current},<0.{next}\"]`. \
This will become an error.",
maturin = maturin,
current = current_minor,
next = current_minor + 1,
);
return false;
}
}
true
}
pub fn warn_missing_build_backend(&self) -> bool {
let maturin = env!("CARGO_PKG_NAME");
if self.build_system.build_backend.as_deref() != Some(maturin) {
eprintln!(
"⚠️ Warning: `build-backend` in pyproject.toml is not set to `{maturin}`, \
packaging tools such as pip will not use maturin to build this project."
);
return false;
}
true
}
}
#[cfg(test)]
mod tests {
use crate::{
pyproject_toml::{Format, Formats, GlobPattern, ToolMaturin},
PyProjectToml,
};
use fs_err as fs;
use pretty_assertions::assert_eq;
use std::path::Path;
use tempfile::TempDir;
#[test]
fn test_parse_tool_maturin() {
let tmp_dir = TempDir::new().unwrap();
let pyproject_file = tmp_dir.path().join("pyproject.toml");
fs::write(
&pyproject_file,
r#"[build-system]
requires = ["maturin"]
build-backend = "maturin"
[tool.maturin]
manylinux = "2010"
python-packages = ["foo", "bar"]
manifest-path = "Cargo.toml"
profile = "dev"
features = ["foo", "bar"]
no-default-features = true
locked = true
rustc-args = ["-Z", "unstable-options"]
"#,
)
.unwrap();
let pyproject = PyProjectToml::new(pyproject_file).unwrap();
assert_eq!(pyproject.manifest_path(), Some(Path::new("Cargo.toml")));
let maturin = pyproject.maturin().unwrap();
assert_eq!(maturin.profile.as_deref(), Some("dev"));
assert_eq!(
maturin.features,
Some(vec!["foo".to_string(), "bar".to_string()])
);
assert!(maturin.all_features.is_none());
assert_eq!(maturin.no_default_features, Some(true));
assert_eq!(maturin.locked, Some(true));
assert!(maturin.frozen.is_none());
assert_eq!(
maturin.rustc_args,
Some(vec!["-Z".to_string(), "unstable-options".to_string()])
);
assert_eq!(
maturin.python_packages,
Some(vec!["foo".to_string(), "bar".to_string()])
);
}
#[test]
fn test_warn_missing_maturin_version() {
let with_constraint = PyProjectToml::new("test-crates/pyo3-pure/pyproject.toml").unwrap();
assert!(with_constraint.warn_missing_maturin_version());
let without_constraint_dir = TempDir::new().unwrap();
let pyproject_file = without_constraint_dir.path().join("pyproject.toml");
fs::write(
&pyproject_file,
r#"[build-system]
requires = ["maturin"]
build-backend = "maturin"
[tool.maturin]
manylinux = "2010"
"#,
)
.unwrap();
let without_constraint = PyProjectToml::new(pyproject_file).unwrap();
assert!(!without_constraint.warn_missing_maturin_version());
}
#[test]
fn deserialize_include_exclude() {
let single = r#"include = ["single"]"#;
assert_eq!(
toml_edit::easy::from_str::<ToolMaturin>(single)
.unwrap()
.include,
Some(vec![GlobPattern::Path("single".to_string())])
);
let multiple = r#"include = ["one", "two"]"#;
assert_eq!(
toml_edit::easy::from_str::<ToolMaturin>(multiple)
.unwrap()
.include,
Some(vec![
GlobPattern::Path("one".to_string()),
GlobPattern::Path("two".to_string())
])
);
let single_format = r#"include = [{path = "path", format="sdist"}]"#;
assert_eq!(
toml_edit::easy::from_str::<ToolMaturin>(single_format)
.unwrap()
.include,
Some(vec![GlobPattern::WithFormat {
path: "path".to_string(),
format: Formats::Single(Format::Sdist)
},])
);
let multiple_formats = r#"include = [{path = "path", format=["sdist", "wheel"]}]"#;
assert_eq!(
toml_edit::easy::from_str::<ToolMaturin>(multiple_formats)
.unwrap()
.include,
Some(vec![GlobPattern::WithFormat {
path: "path".to_string(),
format: Formats::Multiple(vec![Format::Sdist, Format::Wheel])
},])
);
let mixed = r#"include = ["one", {path = "two", format="sdist"}, {path = "three", format=["sdist", "wheel"]}]"#;
assert_eq!(
toml_edit::easy::from_str::<ToolMaturin>(mixed)
.unwrap()
.include,
Some(vec![
GlobPattern::Path("one".to_string()),
GlobPattern::WithFormat {
path: "two".to_string(),
format: Formats::Single(Format::Sdist),
},
GlobPattern::WithFormat {
path: "three".to_string(),
format: Formats::Multiple(vec![Format::Sdist, Format::Wheel])
}
])
);
}
}