1 use anyhow::{Context, Result};
2 use fs_err as fs;
3 use serde::{Deserialize, Serialize};
4 use std::collections::HashMap;
5 use std::path::Path;
6 
7 /// The `[lib]` section of a Cargo.toml
8 #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
9 #[serde(rename_all = "kebab-case")]
10 pub(crate) struct CargoTomlLib {
11     pub(crate) crate_type: Option<Vec<String>>,
12     pub(crate) name: Option<String>,
13 }
14 
15 /// The `[package]` section of a Cargo.toml
16 #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
17 #[serde(rename_all = "kebab-case")]
18 pub(crate) struct CargoTomlPackage {
19     // Those three fields are mandatory
20     // https://doc.rust-lang.org/cargo/reference/manifest.html#the-package-section
21     pub(crate) name: String,
22     pub(crate) version: String,
23     // All other fields are optional
24     pub(crate) authors: Option<Vec<String>>,
25     pub(crate) description: Option<String>,
26     pub(crate) documentation: Option<String>,
27     pub(crate) homepage: Option<String>,
28     pub(crate) repository: Option<String>,
29     pub(crate) readme: Option<String>,
30     pub(crate) keywords: Option<Vec<String>>,
31     pub(crate) categories: Option<Vec<String>>,
32     pub(crate) license: Option<String>,
33     metadata: Option<CargoTomlMetadata>,
34 }
35 
36 /// Extract of the Cargo.toml that can be reused for the python metadata
37 ///
38 /// See https://doc.rust-lang.org/cargo/reference/manifest.html for a specification
39 #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
40 pub struct CargoToml {
41     pub(crate) lib: Option<CargoTomlLib>,
42     pub(crate) package: CargoTomlPackage,
43 }
44 
45 impl CargoToml {
46     /// Reads and parses the Cargo.toml at the given location
from_path(manifest_file: impl AsRef<Path>) -> Result<Self>47     pub fn from_path(manifest_file: impl AsRef<Path>) -> Result<Self> {
48         let contents = fs::read_to_string(&manifest_file).context(format!(
49             "Can't read Cargo.toml at {}",
50             manifest_file.as_ref().display(),
51         ))?;
52         let cargo_toml = toml::from_str(&contents).context(format!(
53             "Failed to parse Cargo.toml at {}",
54             manifest_file.as_ref().display()
55         ))?;
56         Ok(cargo_toml)
57     }
58 
59     /// Returns the python entrypoints
scripts(&self) -> HashMap<String, String>60     pub fn scripts(&self) -> HashMap<String, String> {
61         match self.package.metadata {
62             Some(CargoTomlMetadata {
63                 maturin:
64                     Some(RemainingCoreMetadata {
65                         scripts: Some(ref scripts),
66                         ..
67                     }),
68             }) => scripts.clone(),
69             _ => HashMap::new(),
70         }
71     }
72 
73     /// Returns the trove classifiers
classifiers(&self) -> Vec<String>74     pub fn classifiers(&self) -> Vec<String> {
75         match self.package.metadata {
76             Some(CargoTomlMetadata {
77                 maturin:
78                     Some(RemainingCoreMetadata {
79                         classifiers: Some(ref classifier),
80                         ..
81                     }),
82             }) => classifier.clone(),
83             _ => Vec::new(),
84         }
85     }
86 
87     /// Returns the value of `[project.metadata.maturin]` or an empty stub
remaining_core_metadata(&self) -> RemainingCoreMetadata88     pub fn remaining_core_metadata(&self) -> RemainingCoreMetadata {
89         match &self.package.metadata {
90             Some(CargoTomlMetadata {
91                 maturin: Some(extra_metadata),
92             }) => extra_metadata.clone(),
93             _ => Default::default(),
94         }
95     }
96 }
97 
98 #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
99 #[serde(rename_all = "kebab-case")]
100 struct CargoTomlMetadata {
101     maturin: Option<RemainingCoreMetadata>,
102 }
103 
104 /// The `[project.metadata.maturin]` with the python specific metadata
105 ///
106 /// Those fields are the part of the
107 /// [python core metadata](https://packaging.python.org/specifications/core-metadata/)
108 /// that doesn't have an equivalent in cargo's `[package]` table
109 #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Default)]
110 #[serde(rename_all = "kebab-case")]
111 #[serde(deny_unknown_fields)]
112 pub struct RemainingCoreMetadata {
113     pub name: Option<String>,
114     pub scripts: Option<HashMap<String, String>>,
115     // For backward compatibility, we also allow classifier.
116     #[serde(alias = "classifier")]
117     pub classifiers: Option<Vec<String>>,
118     pub maintainer: Option<String>,
119     pub maintainer_email: Option<String>,
120     pub requires_dist: Option<Vec<String>>,
121     pub requires_python: Option<String>,
122     pub requires_external: Option<Vec<String>>,
123     pub project_url: Option<HashMap<String, String>>,
124     pub provides_extra: Option<Vec<String>>,
125     pub description_content_type: Option<String>,
126     pub python_source: Option<String>,
127 }
128 
129 #[cfg(test)]
130 mod test {
131     use super::*;
132     use indoc::indoc;
133 
134     #[test]
test_metadata_from_cargo_toml()135     fn test_metadata_from_cargo_toml() {
136         let cargo_toml = indoc!(
137             r#"
138             [package]
139             authors = ["konstin <konstin@mailbox.org>"]
140             name = "info-project"
141             version = "0.1.0"
142             description = "A test project"
143             homepage = "https://example.org"
144             keywords = ["ffi", "test"]
145 
146             [lib]
147             crate-type = ["cdylib"]
148             name = "pyo3_pure"
149 
150             [package.metadata.maturin.scripts]
151             ph = "maturin:print_hello"
152 
153             [package.metadata.maturin]
154             classifiers = ["Programming Language :: Python"]
155             requires-dist = ["flask~=1.1.0", "toml==0.10.0"]
156         "#
157         );
158 
159         let cargo_toml: CargoToml = toml::from_str(&cargo_toml).unwrap();
160 
161         let mut scripts = HashMap::new();
162         scripts.insert("ph".to_string(), "maturin:print_hello".to_string());
163 
164         let classifiers = vec!["Programming Language :: Python".to_string()];
165 
166         let requires_dist = Some(vec!["flask~=1.1.0".to_string(), "toml==0.10.0".to_string()]);
167 
168         assert_eq!(cargo_toml.scripts(), scripts);
169         assert_eq!(cargo_toml.classifiers(), classifiers);
170         assert_eq!(
171             cargo_toml.remaining_core_metadata().requires_dist,
172             requires_dist
173         );
174     }
175 
176     #[test]
test_old_classifier_works()177     fn test_old_classifier_works() {
178         let cargo_toml = indoc!(
179             r#"
180             [package]
181             authors = ["konstin <konstin@mailbox.org>"]
182             name = "info-project"
183             version = "0.1.0"
184             description = "A test project"
185             homepage = "https://example.org"
186             keywords = ["ffi", "test"]
187 
188             [lib]
189             crate-type = ["cdylib"]
190             name = "pyo3_pure"
191 
192             [package.metadata.maturin]
193             # Not classifiers
194             classifier = ["Programming Language :: Python"]
195         "#
196         );
197 
198         let cargo_toml: CargoToml = toml::from_str(&cargo_toml).unwrap();
199 
200         let classifiers = vec!["Programming Language :: Python".to_string()];
201 
202         assert_eq!(cargo_toml.classifiers(), classifiers);
203     }
204 
205     #[test]
test_metadata_from_cargo_toml_without_authors()206     fn test_metadata_from_cargo_toml_without_authors() {
207         let cargo_toml = indoc!(
208             r#"
209             [package]
210             name = "info-project"
211             version = "0.1.0"
212             description = "A test project"
213             homepage = "https://example.org"
214             keywords = ["ffi", "test"]
215 
216             [lib]
217             crate-type = ["cdylib"]
218             name = "pyo3_pure"
219 
220             [package.metadata.maturin.scripts]
221             ph = "maturin:print_hello"
222 
223             [package.metadata.maturin]
224             classifiers = ["Programming Language :: Python"]
225             requires-dist = ["flask~=1.1.0", "toml==0.10.0"]
226         "#
227         );
228 
229         let cargo_toml: Result<CargoToml, _> = toml::from_str(&cargo_toml);
230         assert!(cargo_toml.is_ok());
231     }
232 }
233