1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
use crate::PlatformTag;
use anyhow::{format_err, Context, Result};
use pyproject_toml::PyProjectToml as ProjectToml;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;

/// The `[tool]` section of a pyproject.toml
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Tool {
    maturin: Option<ToolMaturin>,
}

/// The `[tool.maturin]` section of a pyproject.toml
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct ToolMaturin {
    sdist_include: Option<Vec<String>>,
    bindings: Option<String>,
    cargo_extra_args: Option<String>,
    #[serde(alias = "manylinux")]
    compatibility: Option<PlatformTag>,
    rustc_extra_args: Option<String>,
    #[serde(default)]
    skip_auditwheel: bool,
    #[serde(default)]
    strip: bool,
}

/// A pyproject.toml as specified in PEP 517
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct PyProjectToml {
    #[serde(flatten)]
    inner: ProjectToml,
    /// PEP 518: The `[tool]` table is where any tool related to your Python project, not just build
    /// tools, can have users specify configuration data as long as they use a sub-table within
    /// `[tool]`, e.g. the flit tool would store its configuration in `[tool.flit]`.
    ///
    /// We use it for `[tool.maturin]`
    pub tool: Option<Tool>,
}

impl std::ops::Deref for PyProjectToml {
    type Target = ProjectToml;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl PyProjectToml {
    /// Returns the contents of a pyproject.toml with a `[build-system]` entry or an error
    ///
    /// Does no specific error handling because it's only used to check whether or not to build
    /// source distributions
    pub fn new(project_root: impl AsRef<Path>) -> Result<PyProjectToml> {
        let path = project_root.as_ref().join("pyproject.toml");
        let contents = fs::read_to_string(&path).context(format!(
            "Couldn't find pyproject.toml at {}",
            path.display()
        ))?;
        let pyproject: PyProjectToml = toml::from_str(&contents)
            .map_err(|err| format_err!("pyproject.toml is not PEP 517 compliant: {}", err))?;
        Ok(pyproject)
    }

    /// Returns the value of `[tool.maturin.sdist-include]` in pyproject.toml
    pub fn sdist_include(&self) -> Option<&Vec<String>> {
        self.tool.as_ref()?.maturin.as_ref()?.sdist_include.as_ref()
    }

    /// Returns the value of `[tool.maturin.bindings]` in pyproject.toml
    pub fn bindings(&self) -> Option<&str> {
        self.tool.as_ref()?.maturin.as_ref()?.bindings.as_deref()
    }

    /// Returns the value of `[tool.maturin.cargo-extra-args]` in pyproject.toml
    pub fn cargo_extra_args(&self) -> Option<&str> {
        self.tool
            .as_ref()?
            .maturin
            .as_ref()?
            .cargo_extra_args
            .as_deref()
    }

    /// Returns the value of `[tool.maturin.compatibility]` in pyproject.toml
    pub fn compatibility(&self) -> Option<PlatformTag> {
        self.tool.as_ref()?.maturin.as_ref()?.compatibility
    }

    /// Returns the value of `[tool.maturin.rustc-extra-args]` in pyproject.toml
    pub fn rustc_extra_args(&self) -> Option<&str> {
        self.tool
            .as_ref()?
            .maturin
            .as_ref()?
            .rustc_extra_args
            .as_deref()
    }

    /// Returns the value of `[tool.maturin.skip-auditwheel]` in pyproject.toml
    pub fn skip_auditwheel(&self) -> bool {
        self.tool
            .as_ref()
            .and_then(|tool| tool.maturin.as_ref())
            .map(|maturin| maturin.skip_auditwheel)
            .unwrap_or_default()
    }

    /// Returns the value of `[tool.maturin.strip]` in pyproject.toml
    pub fn strip(&self) -> bool {
        self.tool
            .as_ref()
            .and_then(|tool| tool.maturin.as_ref())
            .map(|maturin| maturin.strip)
            .unwrap_or_default()
    }

    /// Having a pyproject.toml without a version constraint is a bad idea
    /// because at some point we'll have to do breaking changes and then source
    /// distributions would break
    ///
    /// Returns true if the pyproject.toml has the constraint
    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))
        {
            // Note: Update this once 1.0 is out
            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
    }

    /// Having a pyproject.toml without `build-backend` set to `maturin`
    /// may result in build errors when build from source distribution
    ///
    /// Returns true if the pyproject.toml has `build-backend` set to `maturin`
    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.",
                maturin = maturin
            );
            return false;
        }
        true
    }
}

#[cfg(test)]
mod tests {
    use crate::PyProjectToml;
    use fs_err as fs;
    use tempfile::TempDir;

    #[test]
    fn test_warn_missing_maturin_version() {
        let with_constraint = PyProjectToml::new("test-crates/pyo3-pure").unwrap();
        assert!(with_constraint.warn_missing_maturin_version());
        let without_constraint_dir = TempDir::new().unwrap();

        fs::write(
            without_constraint_dir.path().join("pyproject.toml"),
            r#"[build-system]
            requires = ["maturin"]
            build-backend = "maturin"

            [tool.maturin]
            manylinux = "2010""#,
        )
        .unwrap();
        let without_constraint = PyProjectToml::new(without_constraint_dir).unwrap();
        assert!(!without_constraint.warn_missing_maturin_version());
    }
}