1use std::fmt::Display;
2
3use anyhow::Result;
4use kstring::KString;
5use libabbs::apml::value::array::StringArray;
6use libpfu::Session;
7use log::{debug, error};
8use serde::Deserialize;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Dependency {
12 pub name: KString,
13 pub build_dep: bool,
14 pub origin: DependencyOrigin,
15 pub raw_req: String,
16}
17
18impl Dependency {
19 pub fn extract_name_from_req(req: &str) -> Option<KString> {
21 if let Some((_, cond)) = req.split_once(';') {
23 let cond = cond.to_ascii_lowercase();
24 if cond.contains("platform_system")
25 && (cond.contains("windows") || cond.contains("darwin"))
26 {
27 return None;
28 }
29 }
30
31 let req = req
33 .split_once([' ', '>', '<', '~', '=', ';', '['])
34 .map_or(req, |(req, _)| req);
35 Some(KString::from_ref(req))
36 }
37
38 pub fn guess_aosc_package_name(&self) -> String {
40 self.name.replace('_', "-")
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum DependencyOrigin {
46 RequirementsTxt,
47 Pep517Dependencies,
48 Pep517BuildRequires,
49 Pep517BuildBackend,
50}
51
52impl Display for DependencyOrigin {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 DependencyOrigin::RequirementsTxt => {
56 f.write_str("requirements.txt")
57 }
58 DependencyOrigin::Pep517Dependencies => {
59 f.write_str("project.dependencies from pyproject.toml")
60 }
61 DependencyOrigin::Pep517BuildRequires => {
62 f.write_str("build-system.requires from pyproject.toml")
63 }
64 DependencyOrigin::Pep517BuildBackend => {
65 f.write_str("build-system.build-backend from pyproject.toml")
66 }
67 }
68 }
69}
70
71pub async fn collect_deps(sess: &Session) -> Result<Vec<Dependency>> {
72 debug!("collecting Python dependencies of {:?}", sess.package);
73
74 if let Ok(pyproj_str) = sess.source_fs().await?.read("pyproject.toml").await
75 {
76 debug!("pyproject.toml found in {:?}", sess.package);
77 collect_from_pyproject(&String::from_utf8(pyproj_str.to_vec())?)
78 } else if let Ok(req_txt_str) =
79 sess.source_fs().await?.read("requirements.txt").await
80 {
81 debug!("requirements.txt found in {:?}", sess.package);
82 collect_from_requirementstxt(&String::from_utf8(req_txt_str.to_vec())?)
83 } else {
84 Ok(vec![])
85 }
86}
87
88fn collect_from_pyproject(pyproject_str: &str) -> Result<Vec<Dependency>> {
89 let pyproject = toml::from_str::<PyprojectToml>(pyproject_str)?;
90 debug!("Parsed pyproject.toml: {pyproject:?}");
91
92 let mut py_deps = vec![];
93 for raw_req in pyproject.project.dependencies {
94 if let Some(name) = Dependency::extract_name_from_req(&raw_req) {
95 py_deps.push(Dependency {
96 name,
97 build_dep: false,
98 origin: DependencyOrigin::Pep517Dependencies,
99 raw_req,
100 });
101 }
102 }
103 for raw_req in pyproject.build_system.requires {
104 if let Some(name) = Dependency::extract_name_from_req(&raw_req) {
105 py_deps.push(Dependency {
106 name,
107 build_dep: true,
108 origin: DependencyOrigin::Pep517BuildRequires,
109 raw_req,
110 });
111 }
112 }
113 if let Some(backend) = pyproject.build_system.build_backend {
114 py_deps.push(Dependency {
115 name: KString::from_ref(
116 backend.split_once('.').map_or(backend.as_str(), |(s, _)| s),
117 ),
118 build_dep: true,
119 origin: DependencyOrigin::Pep517BuildBackend,
120 raw_req: backend,
121 });
122 }
123
124 Ok(py_deps)
125}
126
127fn collect_from_requirementstxt(req_txt_str: &str) -> Result<Vec<Dependency>> {
128 Ok(req_txt_str
129 .lines()
130 .map(|s| s.split_once('#').map_or(s, |(s, _)| s))
131 .map(|s| s.trim())
132 .filter(|s| !s.is_empty())
133 .filter_map(|raw_req| {
134 Dependency::extract_name_from_req(raw_req).map(|name| Dependency {
135 name,
136 build_dep: false,
137 origin: DependencyOrigin::RequirementsTxt,
138 raw_req: raw_req.to_string(),
139 })
140 })
141 .collect())
142}
143
144#[derive(Debug, Deserialize)]
145struct PyprojectToml {
146 #[serde(default)]
147 project: PyprojectProject,
148 #[serde(default, rename = "build-system")]
149 build_system: PyprojectBuildSystem,
150}
151
152#[derive(Debug, Deserialize, Default)]
153struct PyprojectProject {
154 #[serde(default)]
155 dependencies: Vec<String>,
156}
157
158#[derive(Debug, Deserialize, Default)]
159struct PyprojectBuildSystem {
160 #[serde(default, rename = "build-backend")]
161 build_backend: Option<String>,
162 #[serde(default, rename = "requires")]
163 requires: Vec<String>,
164}
165
166pub async fn find_system_package(
168 dep: &Dependency,
169 pkgdep: &StringArray,
170 builddep: &StringArray,
171) -> Result<Option<String>> {
172 let find_dep = |pkg: &str| {
173 if pkgdep.iter().any(|dep| dep == pkg) {
174 debug!(
175 "Matched Python dependency package in PKGDEP: {} -> {}",
176 dep.name, pkg
177 );
178 return true;
179 }
180 if dep.build_dep && builddep.iter().any(|dep| dep == pkg) {
181 debug!(
182 "Matched Python dependency package in BUILDDEP: {} -> {}",
183 dep.name, pkg
184 );
185 return true;
186 }
187 false
188 };
189
190 let aosc_package_name = dep.guess_aosc_package_name();
192 if find_dep(&aosc_package_name) {
193 debug!(
194 "Matched Python dependency through name-normalization: {}",
195 dep.name
196 );
197 return Ok(Some(aosc_package_name));
198 }
199
200 let mut found = None;
202 match oma_contents::searcher::search(
203 "/var/lib/apt/lists",
204 oma_contents::searcher::Mode::Provides,
205 &if dep.name.contains('-') {
206 format!("/site-packages/{}/", dep.name.replace('-', "_"))
207 } else {
208 format!("/site-packages/{}/", dep.name)
209 },
210 |(pkg, path)| {
211 if path.starts_with("/usr/lib/python") {
212 found = Some(pkg)
213 }
214 },
215 ) {
216 Ok(()) => {
217 match &found {
218 Some(pkg) => debug!(
219 "Found system package for Python package: {} -> {}",
220 dep.name, pkg
221 ),
222 None => debug!(
223 "No system package was found for Python package: {}",
224 dep.name
225 ),
226 }
227 Ok(found)
228 }
229 Err(err) => {
230 error!(
231 "Failed to find system package for Python package {}: {:?}",
232 dep.name, err
233 );
234 Ok(None)
235 }
236 }
237}
238
239#[cfg(test)]
240mod test {
241 use super::*;
242
243 #[test]
244 fn test_extract_name_from_req() {
245 assert!(
246 Dependency::extract_name_from_req("a; platform_system=windows")
247 .is_none()
248 );
249 assert_eq!(Dependency::extract_name_from_req("a").unwrap(), "a");
250 assert_eq!(Dependency::extract_name_from_req("a; b").unwrap(), "a");
251 assert_eq!(Dependency::extract_name_from_req("a ; b").unwrap(), "a");
252 assert_eq!(Dependency::extract_name_from_req("a== 1.0").unwrap(), "a");
253 assert_eq!(Dependency::extract_name_from_req("a~= 1.0").unwrap(), "a");
254 assert_eq!(Dependency::extract_name_from_req("a>= 1.0").unwrap(), "a");
255 assert_eq!(Dependency::extract_name_from_req("a< 1.0").unwrap(), "a");
256 }
257
258 #[test]
259 fn test_collect_from_pyproject() {
260 assert_eq!(
261 collect_from_pyproject(
262 r##"
263[build-system]
264requires = ["flit-core"]
265build-backend = "flit_core.buildapi"
266
267[project]
268dependencies = [
269 "packaging>=23.2",
270 "wheels; platform_system=windows",
271]
272"##
273 )
274 .unwrap(),
275 vec![
276 Dependency {
277 name: "packaging".into(),
278 build_dep: false,
279 origin: DependencyOrigin::Pep517Dependencies,
280 raw_req: "packaging>=23.2".into(),
281 },
282 Dependency {
283 name: "flit-core".into(),
284 build_dep: true,
285 origin: DependencyOrigin::Pep517BuildRequires,
286 raw_req: "flit-core".into(),
287 },
288 Dependency {
289 name: "flit_core".into(),
290 build_dep: true,
291 origin: DependencyOrigin::Pep517BuildBackend,
292 raw_req: "flit_core.buildapi".into(),
293 }
294 ]
295 );
296 }
297
298 #[test]
299 fn test_collect_from_requirementstxt() {
300 assert_eq!(
301 collect_from_requirementstxt(
302 r##"beautifulsoup4==4.5.1
303decorator==4.0.10
304requests
305pip~=100.0
306a[b]
307"##
308 )
309 .unwrap(),
310 vec![
311 Dependency {
312 name: "beautifulsoup4".into(),
313 build_dep: false,
314 origin: DependencyOrigin::RequirementsTxt,
315 raw_req: "beautifulsoup4==4.5.1".into(),
316 },
317 Dependency {
318 name: "decorator".into(),
319 build_dep: false,
320 origin: DependencyOrigin::RequirementsTxt,
321 raw_req: "decorator==4.0.10".into(),
322 },
323 Dependency {
324 name: "requests".into(),
325 build_dep: false,
326 origin: DependencyOrigin::RequirementsTxt,
327 raw_req: "requests".into(),
328 },
329 Dependency {
330 name: "pip".into(),
331 build_dep: false,
332 origin: DependencyOrigin::RequirementsTxt,
333 raw_req: "pip~=100.0".into(),
334 },
335 Dependency {
336 name: "a".into(),
337 build_dep: false,
338 origin: DependencyOrigin::RequirementsTxt,
339 raw_req: "a[b]".into(),
340 },
341 ]
342 );
343 }
344}