cps_deps/
pkg_config.rs

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        // process cflags
95        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        // process libs
105        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        // process requires
115        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        // ${variable} syntax
184        let from = format!("${{{}}}", key);
185        data = data.replace(&from, &value);
186
187        // $(variable) syntax
188        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}