cargo_c/
pkg_config_gen.rs

1#![allow(dead_code)]
2
3use crate::build::CApiConfig;
4use crate::install::InstallPaths;
5use std::path::{Component, Path, PathBuf};
6
7fn canonicalize<P: AsRef<Path>>(path: P) -> String {
8    let mut stack = Vec::with_capacity(16);
9
10    struct Item<'a> {
11        separator: bool,
12        component: Component<'a>,
13    }
14
15    let mut separator = false;
16
17    for component in path.as_ref().components() {
18        match component {
19            Component::RootDir => {
20                separator = true;
21            }
22            Component::Prefix(_) => stack.push(Item {
23                separator: false,
24                component,
25            }),
26            Component::ParentDir => {
27                let _ = stack.pop();
28            }
29            Component::CurDir => stack.push(Item {
30                separator: false,
31                component,
32            }),
33            Component::Normal(_) => {
34                stack.push(Item {
35                    separator,
36                    component,
37                });
38                separator = true;
39            }
40        }
41    }
42
43    if stack.is_empty() {
44        String::from("/")
45    } else {
46        let mut buf = String::with_capacity(64);
47
48        for item in stack {
49            if item.separator {
50                buf.push('/');
51            }
52
53            buf.push_str(&item.component.as_os_str().to_string_lossy());
54        }
55
56        buf
57    }
58}
59
60#[derive(Debug, Clone)]
61pub struct PkgConfig {
62    prefix: PathBuf,
63    exec_prefix: PathBuf,
64    includedir: PathBuf,
65    libdir: PathBuf,
66
67    name: String,
68    description: String,
69    version: String,
70
71    requires: Vec<String>,
72    requires_private: Vec<String>,
73
74    libs: Vec<String>,
75    libs_private: Vec<String>,
76
77    cflags: Vec<String>,
78
79    conflicts: Vec<String>,
80}
81
82impl PkgConfig {
83    ///
84    /// Build a pkgconfig structure with the following defaults:
85    ///
86    /// prefix=/usr/local
87    /// exec_prefix=${prefix}
88    /// includedir=${prefix}/include
89    /// libdir=${exec_prefix}/lib
90    ///
91    /// Name: $name
92    /// Description: $description
93    /// Version: $version
94    /// Cflags: -I${includedir}/$name
95    /// Libs: -L${libdir} -l$name
96    ///
97    pub fn new(_name: &str, capi_config: &CApiConfig) -> Self {
98        let requires = match &capi_config.pkg_config.requires {
99            Some(reqs) => reqs.split(',').map(|s| s.trim().to_string()).collect(),
100            _ => Vec::new(),
101        };
102        let requires_private = match &capi_config.pkg_config.requires_private {
103            Some(reqs) => reqs.split(',').map(|s| s.trim().to_string()).collect(),
104            _ => Vec::new(),
105        };
106
107        let mut libdir = PathBuf::new();
108        libdir.push("${libdir}");
109        if let Some(subdir) = &capi_config.library.install_subdir {
110            libdir.push(subdir);
111        }
112
113        let libs = vec![
114            format!("-L{}", canonicalize(libdir.display().to_string())),
115            format!("-l{}", capi_config.library.name),
116        ];
117
118        let cflags = if capi_config.header.enabled {
119            let includedir = Path::new("${includedir}").join(&capi_config.header.subdirectory);
120            let includedir = includedir
121                .ancestors()
122                .nth(capi_config.pkg_config.strip_include_path_components)
123                .unwrap_or_else(|| Path::new(""));
124
125            format!("-I{}", canonicalize(includedir))
126        } else {
127            String::from("")
128        };
129
130        PkgConfig {
131            name: capi_config.pkg_config.name.clone(),
132            description: capi_config.pkg_config.description.clone(),
133            version: capi_config.pkg_config.version.clone(),
134
135            prefix: "/usr/local".into(),
136            exec_prefix: "${prefix}".into(),
137            includedir: "${prefix}/include".into(),
138            libdir: "${exec_prefix}/lib".into(),
139
140            libs,
141            libs_private: Vec::new(),
142
143            requires,
144            requires_private,
145
146            cflags: vec![cflags],
147
148            conflicts: Vec::new(),
149        }
150    }
151
152    pub(crate) fn from_workspace(
153        name: &str,
154        install_paths: &InstallPaths,
155        args: &clap::ArgMatches,
156        capi_config: &CApiConfig,
157    ) -> Self {
158        let mut pc = PkgConfig::new(name, capi_config);
159
160        pc.prefix.clone_from(&install_paths.prefix);
161        // TODO: support exec_prefix
162        if args.contains_id("includedir") {
163            if let Ok(suffix) = install_paths.includedir.strip_prefix(&pc.prefix) {
164                pc.includedir = PathBuf::from("${prefix}").join(suffix);
165            } else {
166                pc.includedir.clone_from(&install_paths.includedir);
167            }
168        }
169        if args.contains_id("libdir") {
170            if let Ok(suffix) = install_paths.libdir.strip_prefix(&pc.prefix) {
171                pc.libdir = PathBuf::from("${prefix}").join(suffix);
172            } else {
173                pc.libdir.clone_from(&install_paths.libdir);
174            }
175        }
176        pc
177    }
178
179    pub(crate) fn uninstalled(&self, output: &Path) -> Self {
180        let mut uninstalled = self.clone();
181        uninstalled.prefix = output.to_path_buf();
182        uninstalled.includedir = "${prefix}/include".into();
183        uninstalled.libdir = "${prefix}".into();
184        // First libs item is the search path
185        uninstalled.libs[0] = "-L${prefix}".into();
186
187        uninstalled
188    }
189
190    pub fn set_description<S: AsRef<str>>(&mut self, descr: S) -> &mut Self {
191        descr.as_ref().clone_into(&mut self.description);
192        self
193    }
194
195    pub fn set_libs<S: AsRef<str>>(&mut self, lib: S) -> &mut Self {
196        let lib = lib.as_ref().to_owned();
197        self.libs.clear();
198        self.libs.push(lib);
199        self
200    }
201
202    pub fn add_lib<S: AsRef<str>>(&mut self, lib: S) -> &mut Self {
203        let lib = lib.as_ref().to_owned();
204        self.libs.push(lib);
205        self
206    }
207
208    pub fn set_libs_private<S: AsRef<str>>(&mut self, lib: S) -> &mut Self {
209        let lib = lib.as_ref().to_owned();
210        self.libs_private.clear();
211        self.libs_private.push(lib);
212        self
213    }
214
215    pub fn add_lib_private<S: AsRef<str>>(&mut self, lib: S) -> &mut Self {
216        let lib = lib.as_ref().to_owned();
217        self.libs_private.push(lib);
218        self
219    }
220
221    pub fn set_cflags<S: AsRef<str>>(&mut self, flag: S) -> &mut Self {
222        let flag = flag.as_ref().to_owned();
223        self.cflags.clear();
224        self.cflags.push(flag);
225        self
226    }
227
228    pub fn add_cflag<S: AsRef<str>>(&mut self, flag: S) -> &mut Self {
229        let flag = flag.as_ref();
230        self.cflags.push(flag.to_owned());
231        self
232    }
233
234    pub fn render(&self) -> String {
235        // writing to a String only fails on OOM, which we disregard
236        self.render_help(String::with_capacity(1024)).unwrap()
237    }
238
239    fn render_help<W: core::fmt::Write>(&self, mut w: W) -> Result<W, core::fmt::Error> {
240        writeln!(w, "prefix={}", canonicalize(&self.prefix))?;
241        writeln!(w, "exec_prefix={}", canonicalize(&self.exec_prefix))?;
242        writeln!(w, "libdir={}", canonicalize(&self.libdir))?;
243        writeln!(w, "includedir={}", canonicalize(&self.includedir))?;
244
245        writeln!(w)?;
246
247        writeln!(w, "Name: {}", self.name)?;
248        writeln!(w, "Description: {}", self.description.replace('\n', " "))?; // avoid endlines
249        writeln!(w, "Version: {}", self.version)?;
250        writeln!(w, "Libs: {}", self.libs.join(" "))?;
251        writeln!(w, "Cflags: {}", self.cflags.join(" "))?;
252
253        if !self.libs_private.is_empty() {
254            writeln!(w, "Libs.private: {}", self.libs_private.join(" "))?;
255        }
256
257        if !self.requires.is_empty() {
258            writeln!(w, "Requires: {}", self.requires.join(", "))?;
259        }
260
261        if !self.requires_private.is_empty() {
262            let joined = self.requires_private.join(", ");
263            writeln!(w, "Requires.private: {joined}")?;
264        }
265
266        Ok(w)
267    }
268}
269
270#[cfg(test)]
271mod test {
272    use super::*;
273    use semver::Version;
274
275    #[test]
276    fn simple() {
277        let mut pkg = PkgConfig::new(
278            "foo",
279            &CApiConfig {
280                header: crate::build::HeaderCApiConfig {
281                    name: "foo".into(),
282                    subdirectory: "".into(),
283                    generation: true,
284                    enabled: true,
285                    emit_version_constants: true,
286                },
287                pkg_config: crate::build::PkgConfigCApiConfig {
288                    name: "foo".into(),
289                    filename: "foo".into(),
290                    description: "".into(),
291                    version: "0.1".into(),
292                    requires: Some("somelib, someotherlib".into()),
293                    requires_private: Some("someprivatelib >= 1.0".into()),
294                    strip_include_path_components: 0,
295                },
296                library: crate::build::LibraryCApiConfig {
297                    name: "foo".into(),
298                    version: Version::parse("0.1.0").unwrap(),
299                    install_subdir: None,
300                    versioning: true,
301                    version_suffix_components: None,
302                    import_library: true,
303                    rustflags: Vec::default(),
304                },
305                install: Default::default(),
306            },
307        );
308        pkg.add_lib("-lbar").add_cflag("-DFOO");
309
310        let expected = concat!(
311            "prefix=/usr/local\n",
312            "exec_prefix=${prefix}\n",
313            "libdir=${exec_prefix}/lib\n",
314            "includedir=${prefix}/include\n",
315            "\n",
316            "Name: foo\n",
317            "Description: \n",
318            "Version: 0.1\n",
319            "Libs: -L${libdir} -lfoo -lbar\n",
320            "Cflags: -I${includedir} -DFOO\n",
321            "Requires: somelib, someotherlib\n",
322            "Requires.private: someprivatelib >= 1.0\n",
323        );
324
325        assert_eq!(expected, pkg.render());
326    }
327
328    mod test_canonicalize {
329        use super::canonicalize;
330
331        #[test]
332        fn test_absolute_path() {
333            let path = "/home/user/docs";
334            let result = canonicalize(path);
335            assert_eq!(result, "/home/user/docs");
336        }
337
338        #[test]
339        fn test_relative_path() {
340            let path = "home/user/docs";
341            let result = canonicalize(path);
342            assert_eq!(result, "home/user/docs");
343        }
344
345        #[test]
346        fn test_current_directory() {
347            let path = "/home/user/./docs";
348            let result = canonicalize(path);
349            assert_eq!(result, "/home/user/docs");
350        }
351
352        #[test]
353        fn test_parent_directory() {
354            let path = "/home/user/../docs";
355            let result = canonicalize(path);
356            assert_eq!(result, "/home/docs");
357        }
358
359        #[test]
360        fn test_mixed_dots_and_parent_dirs() {
361            let path = "/home/./user/../docs/./files";
362            let result = canonicalize(path);
363            assert_eq!(result, "/home/docs/files");
364        }
365
366        #[test]
367        fn test_multiple_consecutive_slashes() {
368            let path = "/home//user///docs";
369            let result = canonicalize(path);
370            assert_eq!(result, "/home/user/docs");
371        }
372
373        #[test]
374        fn test_empty_path() {
375            let path = "";
376            let result = canonicalize(path);
377            assert_eq!(result, "/");
378        }
379
380        #[test]
381        fn test_single_dot() {
382            let path = ".";
383            let result = canonicalize(path);
384            assert_eq!(result, ".");
385        }
386
387        #[test]
388        fn test_single_dot_in_absolute_path() {
389            let path = "/.";
390            let result = canonicalize(path);
391            assert_eq!(result, "/");
392        }
393
394        #[test]
395        fn test_trailing_slash() {
396            let path = "/home/user/docs/";
397            let result = canonicalize(path);
398            assert_eq!(result, "/home/user/docs");
399        }
400
401        #[test]
402        fn test_dots_complex_case() {
403            let path = "/a/b/./c/../d//e/./../f";
404            let result = canonicalize(path);
405            assert_eq!(result, "/a/b/d/f");
406        }
407
408        #[cfg(windows)]
409        mod windows {
410            use std::path::Path;
411
412            use super::*;
413
414            #[test]
415            fn test_canonicalize_basic_windows_path() {
416                let input = Path::new(r"C:\Users\test\..\Documents");
417                let expected = r"C:/Users/Documents";
418                let result = canonicalize(input);
419                assert_eq!(result, expected);
420            }
421
422            #[test]
423            fn test_canonicalize_with_current_dir() {
424                let input = Path::new(r"C:\Users\.\Documents");
425                let expected = r"C:/Users/Documents";
426                let result = canonicalize(input);
427                assert_eq!(result, expected);
428            }
429
430            #[test]
431            fn test_canonicalize_with_double_parent_dir() {
432                let input = Path::new(r"C:\Users\test\..\..\Documents");
433                let expected = r"C:/Documents";
434                let result = canonicalize(input);
435                assert_eq!(result, expected);
436            }
437
438            #[test]
439            fn test_canonicalize_with_trailing_slash() {
440                let input = Path::new(r"C:\Users\test\..\Documents\");
441                let expected = r"C:/Users/Documents";
442                let result = canonicalize(input);
443                assert_eq!(result, expected);
444            }
445
446            #[test]
447            fn test_canonicalize_relative_path() {
448                let input = Path::new(r"Users\test\..\Documents");
449                let expected = r"Users/Documents";
450                let result = canonicalize(input);
451                assert_eq!(result, expected);
452            }
453
454            #[test]
455            fn test_canonicalize_current_dir_only() {
456                let input = Path::new(r".\");
457                let expected = r".";
458                let result = canonicalize(input);
459                assert_eq!(result, expected);
460            }
461        }
462    }
463}