1use anyhow::{anyhow, Result};
2use std::collections::HashMap;
3
4use regex::Regex;
5
6#[derive(Default, Debug, PartialEq, Eq)]
7pub struct Dependency {
8 pub name: String,
9 pub op: Option<String>,
10 pub version: Option<String>,
11}
12
13impl Dependency {
14 fn parse_list(data: &str) -> Vec<Self> {
15 let re = Regex::new(r"([^ ,<=>!]+)[ ]*(([<=>!]+)[ ]*([^ ,]+)?)?").unwrap();
16 re.captures_iter(data)
17 .flat_map(|c| -> Result<Self> {
18 Ok(Self {
19 name: c
20 .get(1)
21 .ok_or(anyhow!("captured dependency without name: {:?}", c))?
22 .as_str()
23 .to_string(),
24 op: c.get(3).map(|m| m.as_str().to_string()),
25 version: c.get(4).map(|m| m.as_str().to_string()),
26 })
27 })
28 .collect()
29 }
30
31 pub fn from_name(name: &str) -> Self {
32 Self {
33 name: name.to_string(),
34 ..Self::default()
35 }
36 }
37
38 pub fn with_version(name: &str, op: &str, version: &str) -> Self {
39 Self {
40 name: name.to_string(),
41 op: Some(op.to_string()),
42 version: Some(version.to_string()),
43 }
44 }
45}
46
47#[derive(Default, Debug, PartialEq, Eq)]
48pub struct PkgConfigFile {
49 pub name: String,
50 pub version: String,
51 pub description: String,
52 pub url: Option<String>,
53 pub includes: Vec<String>,
54 pub definitions: Vec<String>,
55 pub compile_flags: Vec<String>,
56 pub cflags_private: Option<String>,
57 pub copyright: Option<String>,
58 pub link_locations: Vec<String>,
59 pub link_libraries: Vec<String>,
60 pub link_flags: Vec<String>,
61 pub libs_private: Option<String>,
62 pub license: Option<String>,
63 pub maintainer: Option<String>,
64 pub requires: Vec<Dependency>,
65 pub requires_private: Vec<Dependency>,
66 pub conflicts: Vec<Dependency>,
67 pub provides: Vec<Dependency>,
68}
69
70impl PkgConfigFile {
71 pub fn parse(data: &str) -> Result<Self> {
72 let data = strip_comments(data);
73 let data = expand_variables(&data, 0)?;
74
75 let name =
76 capture_property("Name", &data)?.ok_or(anyhow!("missing required property `Name`"))?;
77 let version = capture_property("Version", &data)?
78 .ok_or(anyhow!("missing required property `Version`"))?;
79 let description = capture_property("Description", &data)?
80 .ok_or(anyhow!("missing required property `Description`"))?;
81 let url = capture_property("URL", &data)?;
82 let cflags = capture_property("Cflags", &data)?;
83 let cflags_private = capture_property("Cflags.private", &data)?;
84 let copyright = capture_property("Copyright", &data)?;
85 let libs = capture_property("Libs", &data)?;
86 let libs_private = capture_property("Libs.private", &data)?;
87 let license = capture_property("License", &data)?;
88 let maintainer = capture_property("Maintainer", &data)?;
89 let requires = capture_property("Requires", &data)?.unwrap_or_default();
90 let requires_private = capture_property("Requires.private", &data)?.unwrap_or_default();
91 let conflicts = capture_property("Conflicts", &data)?.unwrap_or_default();
92 let provides = capture_property("Provides", &data)?.unwrap_or_default();
93
94 let cflags: Vec<_> = cflags
96 .unwrap_or_default()
97 .split_whitespace()
98 .map(String::from)
99 .collect();
100 let includes = filter_flag(&cflags, "-I");
101 let definitions = filter_flag(&cflags, "-D");
102 let compile_flags = filter_excluding_flags(&cflags, &["-I", "-D"]);
103
104 let libs: Vec<_> = libs
106 .unwrap_or_default()
107 .split_whitespace()
108 .map(String::from)
109 .collect();
110 let link_locations = filter_flag(&libs, "-L");
111 let link_libraries = filter_flag(&libs, "-l");
112 let link_flags = filter_excluding_flags(&libs, &["-L", "-l"]);
113
114 let requires = Dependency::parse_list(&requires);
116 let requires_private = Dependency::parse_list(&requires_private);
117 let conflicts = Dependency::parse_list(&conflicts);
118 let provides = Dependency::parse_list(&provides);
119
120 Ok(Self {
121 name,
122 version,
123 description,
124 url,
125 includes,
126 definitions,
127 compile_flags,
128 cflags_private,
129 copyright,
130 link_locations,
131 link_libraries,
132 link_flags,
133 libs_private,
134 license,
135 maintainer,
136 requires,
137 requires_private,
138 conflicts,
139 provides,
140 })
141 }
142}
143
144fn capture_property(name: &str, data: &str) -> Result<Option<String>> {
145 Ok(Regex::new(&format!(r"{}:[ ]+(.+)", name))?
146 .captures(data)
147 .map(|cap| cap[1].trim().to_string()))
148}
149
150fn strip_comments(data: &str) -> String {
151 data.lines()
152 .filter(|line| !line.starts_with('#'))
153 .collect::<Vec<&str>>()
154 .join("\n")
155}
156
157fn parse_variables(data: &str) -> HashMap<String, String> {
158 let re = Regex::new(r"([a-zA-Z0-9\-_]+)[ ]*=[ ]*([:a-zA-Z0-9\-_/=\.+ ]*)?$").unwrap();
159
160 data.lines()
161 .flat_map(|line| re.captures_iter(line))
162 .flat_map(|c| {
163 let name = c.get(1).map(|m| m.as_str().to_string())?;
164 let value = c.get(2).map(|m| m.as_str().to_string()).unwrap_or_default();
165 Some((name, value))
166 })
167 .collect()
168}
169
170fn expand_variables(data: &str, index: i32) -> Result<String> {
171 let variables = parse_variables(data);
172
173 if index > 100 {
174 return Err(anyhow!(
175 "Max recursion hit expanding variables\n\n{}\n\n{:?}",
176 data,
177 variables
178 ));
179 }
180
181 let mut data = data.to_string();
182 for (key, value) in variables {
183 let from = format!("${{{}}}", key);
185 data = data.replace(&from, &value);
186
187 let from = format!("$({})", key);
189 data = data.replace(&from, &value);
190 }
191
192 if data.contains("${") {
193 expand_variables(&data, index + 1)
194 } else {
195 Ok(data)
196 }
197}
198
199fn filter_flag(data: &[String], flag: &str) -> Vec<String> {
200 data.iter()
201 .filter(|&s| s.starts_with(flag))
202 .map(|l| String::from(&l[flag.len()..]))
203 .collect::<Vec<_>>()
204}
205
206fn filter_excluding_flags(data: &[String], flags: &[&str]) -> Vec<String> {
207 data.iter()
208 .filter(|&s| !flags.iter().any(|f| s.starts_with(f)))
209 .map(String::from)
210 .collect::<Vec<_>>()
211}
212
213#[test]
214fn test_parse_pc_files() -> Result<()> {
215 let fcl_pc = r#"
216prefix=/usr
217exec_prefix=${prefix}
218libdir=/usr/lib/x86_64-linux-gnu
219includedir=/usr/include
220
221Name: fcl
222Description: Flexible Collision Library
223Version: 0.7.0
224Requires: ccd eigen3 octomap
225Libs: -L${libdir} -lfcl
226Cflags: -std=c++11 -I${includedir}
227 "#;
228
229 assert_eq!(
230 PkgConfigFile::parse(fcl_pc)?,
231 PkgConfigFile {
232 name: "fcl".to_string(),
233 description: "Flexible Collision Library".to_string(),
234 version: "0.7.0".to_string(),
235 requires: vec![
236 Dependency::from_name("ccd"),
237 Dependency::from_name("eigen3"),
238 Dependency::from_name("octomap"),
239 ],
240 link_locations: vec!["/usr/lib/x86_64-linux-gnu".to_string()],
241 link_libraries: vec!["fcl".to_string()],
242 includes: vec!["/usr/include".to_string()],
243 compile_flags: vec!["-std=c++11".to_string()],
244 ..PkgConfigFile::default()
245 },
246 "input: {}",
247 fcl_pc
248 );
249
250 let srvcore_pc = r#"
251prefix=/usr
252exec_prefix=${prefix}
253libdir=${exec_prefix}/lib/x86_64-linux-gnu
254includedir=${prefix}/include/nss
255
256Name: NSS
257Description: Mozilla Network Security Services
258Version: 3.68.2
259Requires: nspr
260Libs: -L${libdir} -lnss3 -lnssutil3 -lsmime3 -lssl3
261Cflags: -I${includedir}
262 "#;
263
264 assert_eq!(
265 PkgConfigFile::parse(srvcore_pc)?,
266 PkgConfigFile {
267 name: "NSS".to_string(),
268 description: "Mozilla Network Security Services".to_string(),
269 version: "3.68.2".to_string(),
270 requires: vec![Dependency::from_name("nspr"),],
271 link_locations: vec!["/usr/lib/x86_64-linux-gnu".to_string()],
272 link_libraries: vec![
273 "nss3".to_string(),
274 "nssutil3".to_string(),
275 "smime3".to_string(),
276 "ssl3".to_string()
277 ],
278 includes: vec!["/usr/include/nss".to_string()],
279 ..PkgConfigFile::default()
280 },
281 "input: {}",
282 srvcore_pc
283 );
284 Ok(())
285}
286
287#[test]
288fn test_capture_property() -> Result<()> {
289 let data = r#"
290Name: Fontconfig
291Description: Font configuration and customization library
292Version: 2.13.1
293Requires: freetype2 >= 21.0.15
294Requires.private: uuid expat
295Libs: -L${libdir} -lfontconfig
296Libs.private:
297Cflags: -I${includedir}
298 "#;
299
300 assert_eq!(
301 capture_property("Name", data)?.expect("`Name` property not captured"),
302 "Fontconfig"
303 );
304 assert_eq!(
305 capture_property("Version", data)?.expect("`Version` property not captured"),
306 "2.13.1"
307 );
308 assert_eq!(
309 capture_property("Description", data)?.expect("`Description` property not captured"),
310 "Font configuration and customization library"
311 );
312 assert_eq!(
313 capture_property("Cflags", data)?.expect("`Cflags` property not captured"),
314 "-I${includedir}"
315 );
316 assert_eq!(
317 capture_property("Libs", data)?.expect("`Libs` property not captured"),
318 "-L${libdir} -lfontconfig"
319 );
320 assert_eq!(capture_property("Libs.private", data)?, None);
321 assert_eq!(
322 capture_property("Requires", data)?.expect("`Requires` property not captured"),
323 "freetype2 >= 21.0.15"
324 );
325 assert_eq!(
326 capture_property("Requires.private", data)?
327 .expect("`Requires.private` property not captured"),
328 "uuid expat"
329 );
330
331 Ok(())
332}
333
334#[test]
335fn test_parse_dependency_list() -> Result<()> {
336 let dependency_lists = [
337 "ACE_ETCL",
338 "freetype2 >= 21.0.15",
339 "gio-2.0 >= 2.50 gee-0.8 >= 0.20",
340 "gcalc-2 >= 3.34 gtk+-3.0 > 3.19.3",
341 "glib-2.0, gobject-2.0",
342 "libudev >= 199",
343 "nspr, nss",
344 "xproto x11",
345 "",
346 ];
347 let expected = [
348 vec![Dependency::from_name("ACE_ETCL")],
349 vec![Dependency::with_version("freetype2", ">=", "21.0.15")],
350 vec![
351 Dependency::with_version("gio-2.0", ">=", "2.50"),
352 Dependency::with_version("gee-0.8", ">=", "0.20"),
353 ],
354 vec![
355 Dependency::with_version("gcalc-2", ">=", "3.34"),
356 Dependency::with_version("gtk+-3.0", ">", "3.19.3"),
357 ],
358 vec![
359 Dependency::from_name("glib-2.0"),
360 Dependency::from_name("gobject-2.0"),
361 ],
362 vec![Dependency::with_version("libudev", ">=", "199")],
363 vec![Dependency::from_name("nspr"), Dependency::from_name("nss")],
364 vec![
365 Dependency::from_name("xproto"),
366 Dependency::from_name("x11"),
367 ],
368 vec![],
369 ];
370
371 for (dependency_list, expected) in dependency_lists.iter().zip(expected.iter()) {
372 let output = Dependency::parse_list(dependency_list);
373 assert_eq!(output, *expected, "dependency_list: `{}`", dependency_list);
374 }
375
376 Ok(())
377}