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