use crate::PlatformTag;
use crate::auditwheel::AuditWheelMode;
use anyhow::{Context, Result};
use fs_err as fs;
use pep440_rs::{Version, VersionSpecifiers};
use pep508_rs::VersionOrUrl;
use pyproject_toml::{BuildSystem, Project};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Tool {
pub maturin: Option<ToolMaturin>,
}
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Format {
Sdist,
Wheel,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(untagged)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
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)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum GlobPattern {
Path(String),
WithFormat {
path: String,
format: Formats,
},
WithOutDir {
path: String,
from: IncludeFrom,
to: String,
#[serde(default)]
crate_name: Option<String>,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum IncludeFrom {
OutDir,
}
pub struct OutDirInclude<'a> {
pub path: &'a str,
pub to: &'a str,
pub crate_name: Option<&'a str>,
}
impl GlobPattern {
pub fn targets(&self, format: Format) -> Option<&str> {
match self {
Self::Path(glob) => Some(glob),
Self::WithFormat {
path,
format: formats,
} if formats.targets(format) => Some(path),
Self::WithOutDir { .. } => None,
_ => None,
}
}
pub fn as_out_dir_include(&self) -> Option<OutDirInclude<'_>> {
match self {
Self::WithOutDir {
path,
to,
crate_name,
..
} => Some(OutDirInclude {
path,
to,
crate_name: crate_name.as_deref(),
}),
_ => None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CargoTarget {
pub name: String,
pub kind: Option<CargoCrateType>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum CargoCrateType {
#[serde(rename = "bin")]
Bin,
#[serde(rename = "cdylib")]
CDyLib,
#[serde(rename = "dylib")]
DyLib,
#[serde(rename = "lib")]
Lib,
#[serde(rename = "rlib")]
RLib,
#[serde(rename = "staticlib")]
StaticLib,
}
impl From<CargoCrateType> for cargo_metadata::CrateType {
fn from(value: CargoCrateType) -> Self {
match value {
CargoCrateType::Bin => cargo_metadata::CrateType::Bin,
CargoCrateType::CDyLib => cargo_metadata::CrateType::CDyLib,
CargoCrateType::DyLib => cargo_metadata::CrateType::DyLib,
CargoCrateType::Lib => cargo_metadata::CrateType::Lib,
CargoCrateType::RLib => cargo_metadata::CrateType::RLib,
CargoCrateType::StaticLib => cargo_metadata::CrateType::StaticLib,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TargetConfig {
#[serde(alias = "macosx-deployment-target")]
pub macos_deployment_target: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum SdistGenerator {
#[default]
Cargo,
Git,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SbomConfig {
pub rust: Option<bool>,
pub auditwheel: Option<bool>,
pub include: Option<Vec<PathBuf>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum FeatureSpec {
Plain(String),
Conditional {
feature: String,
#[serde(
rename = "python-version",
default,
skip_serializing_if = "Option::is_none"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
python_version: Option<VersionSpecifiers>,
#[serde(
rename = "python-implementation",
default,
skip_serializing_if = "Option::is_none"
)]
python_implementation: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConditionalFeature {
pub feature: String,
pub python_version: Option<VersionSpecifiers>,
pub python_implementation: Option<String>,
}
impl FeatureSpec {
pub fn split(specs: Vec<FeatureSpec>) -> (Vec<String>, Vec<ConditionalFeature>) {
let mut plain = Vec::new();
let mut conditional = Vec::new();
for spec in specs {
match spec {
FeatureSpec::Plain(f) => plain.push(f),
FeatureSpec::Conditional {
feature,
python_version: None,
python_implementation: None,
} => plain.push(feature),
FeatureSpec::Conditional {
feature,
python_version,
python_implementation,
} => conditional.push(ConditionalFeature {
feature,
python_version,
python_implementation,
}),
}
}
(plain, conditional)
}
pub fn resolve_conditional(
conditional_features: &[ConditionalFeature],
env: &FeatureConditionEnv,
) -> Vec<String> {
let python_version = Version::new([env.major as u64, env.minor as u64]);
conditional_features
.iter()
.filter(|f| {
f.python_version
.as_ref()
.is_none_or(|s| s.contains(&python_version))
})
.filter(|f| {
f.python_implementation
.as_ref()
.is_none_or(|i| i.eq_ignore_ascii_case(env.implementation_name))
})
.map(|f| f.feature.clone())
.collect()
}
}
pub struct FeatureConditionEnv<'a> {
pub major: usize,
pub minor: usize,
pub implementation_name: &'a str,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ToolMaturin {
pub module_name: Option<String>,
pub include: Option<Vec<GlobPattern>>,
pub exclude: Option<Vec<GlobPattern>>,
pub bindings: Option<String>,
#[serde(alias = "manylinux")]
pub compatibility: Option<PlatformTag>,
pub auditwheel: Option<AuditWheelMode>,
#[serde(default)]
pub skip_auditwheel: bool,
#[serde(default)]
pub strip: bool,
#[serde(default)]
pub sdist_generator: SdistGenerator,
pub python_source: Option<PathBuf>,
pub python_packages: Option<Vec<String>>,
pub data: Option<PathBuf>,
pub targets: Option<Vec<CargoTarget>>,
#[serde(default, rename = "target")]
pub target_config: HashMap<String, TargetConfig>,
pub profile: Option<String>,
pub editable_profile: Option<String>,
pub features: Option<Vec<FeatureSpec>>,
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>>,
#[serde(default)]
pub use_base_python: bool,
pub sbom: Option<SbomConfig>,
#[serde(default)]
pub include_import_lib: bool,
pub pgo_command: Option<String>,
pub generate_ci: Option<GenerateCIConfig>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GenerateCIConfig {
pub github: Option<GitHubCIConfig>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GitHubCIConfig {
pub pytest: Option<bool>,
pub zig: Option<bool>,
pub skip_attestation: Option<bool>,
pub args: Option<String>,
pub linux: Option<PlatformCIConfig>,
pub musllinux: Option<PlatformCIConfig>,
pub windows: Option<PlatformCIConfig>,
pub macos: Option<PlatformCIConfig>,
pub emscripten: Option<PlatformCIConfig>,
pub android: Option<PlatformCIConfig>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CIConfigOverrides {
pub runner: Option<String>,
pub manylinux: Option<String>,
pub container: Option<String>,
pub docker_options: Option<String>,
pub rust_toolchain: Option<String>,
pub rustup_components: Option<String>,
pub before_script_linux: Option<String>,
pub args: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PlatformCIConfig {
pub targets: Option<Vec<String>>,
pub target: Option<Vec<TargetCIConfig>>,
#[serde(flatten)]
pub overrides: CIConfigOverrides,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TargetCIConfig {
pub arch: String,
#[serde(flatten)]
pub overrides: CIConfigOverrides,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct PyProjectToml {
pub build_system: BuildSystem,
pub project: Option<Project>,
pub tool: Option<Tool>,
pub dependency_groups: Option<pyproject_toml::DependencyGroups>,
}
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 = toml::from_str(&contents).with_context(|| {
format!(
"pyproject.toml at {} is invalid",
pyproject_file.as_ref().display()
)
})?;
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()
}
pub fn module_name(&self) -> Option<&str> {
self.maturin()?.module_name.as_deref()
}
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 pgo_command(&self) -> Option<&str> {
self.maturin().and_then(|m| m.pgo_command.as_deref())
}
pub fn compatibility(&self) -> Option<PlatformTag> {
self.maturin()?.compatibility
}
pub fn auditwheel(&self) -> Option<AuditWheelMode> {
self.maturin()
.map(|maturin| maturin.auditwheel)
.unwrap_or_default()
}
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 sdist_generator(&self) -> SdistGenerator {
self.maturin()
.map(|maturin| maturin.sdist_generator)
.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 targets(&self) -> Option<&[CargoTarget]> {
self.maturin()
.and_then(|maturin| maturin.targets.as_deref())
}
pub fn target_config(&self, target: &str) -> Option<&TargetConfig> {
self.maturin()
.and_then(|maturin| maturin.target_config.get(target))
}
pub fn manifest_path(&self) -> Option<&Path> {
self.maturin()?.manifest_path.as_deref()
}
pub fn include_import_lib(&self) -> bool {
self.maturin()
.map(|maturin| maturin.include_import_lib)
.unwrap_or_default()
}
pub fn generate_ci(&self) -> Option<&GenerateCIConfig> {
self.maturin()?.generate_ci.as_ref()
}
pub fn warn_bad_maturin_version(&self) -> bool {
let maturin = env!("CARGO_PKG_NAME");
let current_major = env!("CARGO_PKG_VERSION_MAJOR").parse::<usize>().unwrap();
let self_version = Version::from_str(env!("CARGO_PKG_VERSION")).unwrap();
let requires_maturin = self
.build_system
.requires
.iter()
.find(|x| x.name.as_ref() == maturin);
if let Some(requires_maturin) = requires_maturin {
match requires_maturin.version_or_url.as_ref() {
Some(VersionOrUrl::VersionSpecifier(version_specifier)) => {
if !version_specifier.contains(&self_version) {
eprintln!(
"⚠️ Warning: You specified {requires_maturin} in pyproject.toml under \
`build-system.requires`, but the current {maturin} version is {self_version}",
);
return false;
}
}
Some(VersionOrUrl::Url(_)) => {
}
None => {
eprintln!(
"⚠️ Warning: Please use {maturin} in pyproject.toml with a version constraint, \
e.g. `requires = [\"{maturin}>={current}.0,<{next}.0\"]`. \
This will become an error.",
maturin = maturin,
current = current_major,
next = current_major + 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) {
return true;
}
if std::env::var("MATURIN_NO_MISSING_BUILD_BACKEND_WARNING").is_ok() {
return false;
}
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."
);
false
}
pub fn warn_invalid_version_info(&self) -> bool {
let Some(project) = &self.project else {
return true;
};
let has_static_version = project.version.is_some();
let has_dynamic_version = project
.dynamic
.as_ref()
.is_some_and(|d| d.iter().any(|s| s == "version"));
if has_static_version && has_dynamic_version {
eprintln!(
"⚠️ Warning: `project.dynamic` must not specify `version` when `project.version` is present in pyproject.toml"
);
return false;
}
if !has_static_version && !has_dynamic_version {
eprintln!(
"⚠️ Warning: `project.version` field is required in pyproject.toml unless it is present in the `project.dynamic` list"
);
return false;
}
true
}
}
#[cfg(test)]
mod tests {
use crate::test_utils::test_crate_path;
use crate::{
PyProjectToml,
pyproject_toml::{
FeatureConditionEnv, FeatureSpec, Format, Formats, GlobPattern, ToolMaturin,
},
};
use expect_test::expect;
use fs_err as fs;
use indoc::indoc;
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"]
[[tool.maturin.targets]]
name = "pyo3_pure"
kind = "lib"
bindings = "pyo3"
[tool.maturin.target."x86_64-apple-darwin"]
macos-deployment-target = "10.12"
"#,
)
.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![
FeatureSpec::Plain("foo".to_string()),
FeatureSpec::Plain("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()])
);
let targets = maturin.targets.as_ref().unwrap();
assert_eq!("pyo3_pure", targets[0].name);
let target_config = pyproject.target_config("x86_64-apple-darwin").unwrap();
assert_eq!(
target_config.macos_deployment_target.as_deref(),
Some("10.12")
);
}
#[test]
fn test_warn_missing_maturin_version() {
let with_constraint =
PyProjectToml::new(test_crate_path("pyo3-pure").join("pyproject.toml")).unwrap();
assert!(with_constraint.warn_bad_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_bad_maturin_version());
}
#[test]
fn test_warn_incorrect_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==0.0.1"]
build-backend = "maturin"
[tool.maturin]
manylinux = "2010"
"#,
)
.unwrap();
let without_constraint = PyProjectToml::new(pyproject_file).unwrap();
assert!(!without_constraint.warn_bad_maturin_version());
}
#[test]
fn test_warn_invalid_version_info_conflict() {
let conflict = toml::from_str::<PyProjectToml>(
r#"[build-system]
requires = ["maturin==1.0.0"]
[project]
name = "..."
version = "1.2.3"
dynamic = ['version']
"#,
)
.unwrap();
assert!(!conflict.warn_invalid_version_info());
}
#[test]
fn test_warn_invalid_version_info_missing() {
let missing = toml::from_str::<PyProjectToml>(
r#"[build-system]
requires = ["maturin==1.0.0"]
[project]
name = "..."
"#,
)
.unwrap();
assert!(!missing.warn_invalid_version_info());
}
#[test]
fn test_warn_invalid_version_info_ok() {
let static_ver = toml::from_str::<PyProjectToml>(
r#"[build-system]
requires = ["maturin==1.0.0"]
[project]
name = "..."
version = "1.2.3"
"#,
)
.unwrap();
assert!(static_ver.warn_invalid_version_info());
let dynamic_ver = toml::from_str::<PyProjectToml>(
r#"[build-system]
requires = ["maturin==1.0.0"]
[project]
name = "..."
dynamic = ['version']
"#,
)
.unwrap();
assert!(dynamic_ver.warn_invalid_version_info());
}
#[test]
fn deserialize_include_exclude() {
let single = r#"include = ["single"]"#;
assert_eq!(
toml::from_str::<ToolMaturin>(single).unwrap().include,
Some(vec![GlobPattern::Path("single".to_string())])
);
let multiple = r#"include = ["one", "two"]"#;
assert_eq!(
toml::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::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::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::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])
}
])
);
}
#[test]
fn test_gh_1615() {
let source = indoc!(
r#"[build-system]
requires = [ "maturin>=0.14", "numpy", "wheel", "patchelf",]
build-backend = "maturin"
[project]
name = "..."
license-files = [ "license.txt",]
requires-python = ">=3.8"
requires-dist = [ "maturin>=0.14", "...",]
dependencies = [ "packaging", "...",]
zip-safe = false
version = "..."
readme = "..."
description = "..."
classifiers = [ "...",]
"#
);
let temp_dir = TempDir::new().unwrap();
let pyproject_toml = temp_dir.path().join("pyproject.toml");
fs::write(&pyproject_toml, source).unwrap();
let outer_error = PyProjectToml::new(&pyproject_toml).unwrap_err();
let inner_error = outer_error.source().unwrap();
let expected = expect![[r#"
TOML parse error at line 10, column 16
|
10 | dependencies = [ "packaging", "...",]
| ^^^^^^^^^^^^^^^^^^^^^^
URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ /path/to/file`).
...
^^^
"#]];
expected.assert_eq(&inner_error.to_string());
}
#[test]
fn test_resolve_conditional_features() {
let specs = vec![
FeatureSpec::Conditional {
feature: "pyo3/abi3-py311".to_string(),
python_version: Some(">=3.11".parse().unwrap()),
python_implementation: None,
},
FeatureSpec::Conditional {
feature: "pyo3/abi3-py38".to_string(),
python_version: Some("<3.11".parse().unwrap()),
python_implementation: None,
},
FeatureSpec::Conditional {
feature: "fast-buffer".to_string(),
python_version: Some(">=3.11".parse().unwrap()),
python_implementation: None,
},
];
let (_plain, conditional) = FeatureSpec::split(specs);
let cpython = |major, minor| FeatureConditionEnv {
major,
minor,
implementation_name: "cpython",
};
let resolved = FeatureSpec::resolve_conditional(&conditional, &cpython(3, 12));
assert_eq!(resolved, vec!["pyo3/abi3-py311", "fast-buffer"]);
let resolved = FeatureSpec::resolve_conditional(&conditional, &cpython(3, 11));
assert_eq!(resolved, vec!["pyo3/abi3-py311", "fast-buffer"]);
let resolved = FeatureSpec::resolve_conditional(&conditional, &cpython(3, 9));
assert_eq!(resolved, vec!["pyo3/abi3-py38"]);
let resolved = FeatureSpec::resolve_conditional(&conditional, &cpython(3, 8));
assert_eq!(resolved, vec!["pyo3/abi3-py38"]);
}
#[test]
fn test_resolve_conditional_features_with_implementation() {
let specs = vec![
FeatureSpec::Conditional {
feature: "pyo3/abi3-py311".to_string(),
python_version: Some(">=3.11".parse().unwrap()),
python_implementation: Some("cpython".to_string()),
},
FeatureSpec::Conditional {
feature: "pypy-compat".to_string(),
python_version: None,
python_implementation: Some("pypy".to_string()),
},
FeatureSpec::Conditional {
feature: "always-conditional".to_string(),
python_version: Some(">=3.10".parse().unwrap()),
python_implementation: None,
},
];
let (_plain, conditional) = FeatureSpec::split(specs);
let env = |major, minor, impl_name| FeatureConditionEnv {
major,
minor,
implementation_name: impl_name,
};
let resolved = FeatureSpec::resolve_conditional(&conditional, &env(3, 12, "cpython"));
assert_eq!(resolved, vec!["pyo3/abi3-py311", "always-conditional"]);
let resolved = FeatureSpec::resolve_conditional(&conditional, &env(3, 10, "pypy"));
assert_eq!(resolved, vec!["pypy-compat", "always-conditional"]);
let resolved = FeatureSpec::resolve_conditional(&conditional, &env(3, 10, "cpython"));
assert_eq!(resolved, vec!["always-conditional"]);
let resolved = FeatureSpec::resolve_conditional(&conditional, &env(3, 9, "pypy"));
assert_eq!(resolved, vec!["pypy-compat"]);
}
#[test]
fn test_feature_spec_deserialize_mixed() {
let toml_str = r#"
features = [
"plain-feature",
{ feature = "pyo3/abi3-py311", python-version = ">=3.11" },
]
"#;
let maturin: ToolMaturin = toml::from_str(toml_str).unwrap();
assert_eq!(
maturin.features,
Some(vec![
FeatureSpec::Plain("plain-feature".to_string()),
FeatureSpec::Conditional {
feature: "pyo3/abi3-py311".to_string(),
python_version: Some(">=3.11".parse().unwrap()),
python_implementation: None,
},
])
);
}
#[test]
fn test_feature_spec_deserialize_with_implementation() {
let toml_str = r#"
features = [
{ feature = "pyo3/abi3-py311", python-version = ">=3.11", python-implementation = "cpython" },
{ feature = "pypy-compat", python-implementation = "pypy" },
]
"#;
let maturin: ToolMaturin = toml::from_str(toml_str).unwrap();
assert_eq!(
maturin.features,
Some(vec![
FeatureSpec::Conditional {
feature: "pyo3/abi3-py311".to_string(),
python_version: Some(">=3.11".parse().unwrap()),
python_implementation: Some("cpython".to_string()),
},
FeatureSpec::Conditional {
feature: "pypy-compat".to_string(),
python_version: None,
python_implementation: Some("pypy".to_string()),
},
])
);
}
#[test]
fn test_feature_spec_deserialize_invalid_specifier() {
let toml_str = r#"
features = [
{ feature = "foo", python-version = "not-a-version" },
]
"#;
let result: Result<ToolMaturin, _> = toml::from_str(toml_str);
assert!(result.is_err());
}
#[test]
fn test_generate_ci_config_deserialization() {
let toml_str = r#"
[generate-ci.github]
pytest = true
zig = true
skip-attestation = false
[generate-ci.github.linux]
runner = "ubuntu-22.04"
manylinux = "2_28"
targets = ["x86_64", "aarch64"]
[generate-ci.github.macos]
targets = ["aarch64"]
"#;
let maturin: ToolMaturin = toml::from_str(toml_str).unwrap();
let ci = maturin.generate_ci.unwrap();
let gh = ci.github.unwrap();
assert_eq!(gh.pytest, Some(true));
assert_eq!(gh.zig, Some(true));
assert_eq!(gh.skip_attestation, Some(false));
let linux = gh.linux.unwrap();
assert_eq!(linux.overrides.runner, Some("ubuntu-22.04".to_string()));
assert_eq!(linux.overrides.manylinux, Some("2_28".to_string()));
assert_eq!(
linux.targets,
Some(vec!["x86_64".to_string(), "aarch64".to_string()])
);
let macos = gh.macos.unwrap();
assert_eq!(macos.targets, Some(vec!["aarch64".to_string()]));
assert!(gh.windows.is_none());
}
#[test]
fn test_generate_ci_config_detailed_targets() {
let toml_str = r#"
[[generate-ci.github.linux.target]]
arch = "x86_64"
manylinux = "2_28"
[[generate-ci.github.linux.target]]
arch = "aarch64"
runner = "self-hosted-arm64"
manylinux = "2_17"
before-script-linux = "yum install -y openssl-devel"
"#;
let maturin: ToolMaturin = toml::from_str(toml_str).unwrap();
let ci = maturin.generate_ci.unwrap();
let gh = ci.github.unwrap();
let linux = gh.linux.unwrap();
assert!(linux.targets.is_none());
let detailed = linux.target.unwrap();
assert_eq!(detailed.len(), 2);
assert_eq!(detailed[0].arch, "x86_64");
assert_eq!(detailed[0].overrides.manylinux, Some("2_28".to_string()));
assert_eq!(detailed[1].arch, "aarch64");
assert_eq!(
detailed[1].overrides.runner,
Some("self-hosted-arm64".to_string())
);
assert_eq!(
detailed[1].overrides.before_script_linux,
Some("yum install -y openssl-devel".to_string())
);
}
#[test]
fn test_pgo_command() {
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]
pgo-command = "python -m pytest tests/benchmarks"
"#,
)
.unwrap();
let pyproject = PyProjectToml::new(pyproject_file).unwrap();
assert_eq!(
pyproject.pgo_command(),
Some("python -m pytest tests/benchmarks")
);
}
#[test]
fn test_pgo_command_absent() {
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"
"#,
)
.unwrap();
let pyproject = PyProjectToml::new(pyproject_file).unwrap();
assert_eq!(pyproject.pgo_command(), None);
}
}