cps_deps/
generate_from_pkg_config.rs

1use crate::lib_search::LibraryLocation;
2use crate::{cps, lib_search, pkg_config};
3use anyhow::{Context, Result};
4use std::collections::HashMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7use walkdir::WalkDir;
8
9fn find_pc_files() -> Vec<PathBuf> {
10    [
11        "/usr/lib",
12        "/usr/share",
13        "/usr/local/lib",
14        "/usr/local/share",
15    ]
16    .iter()
17    .map(PathBuf::from)
18    .flat_map(|dir| WalkDir::new(dir).into_iter().filter_map(Result::ok))
19    .filter(|dir_entry| dir_entry.file_type().is_file())
20    .filter(|dir_entry| dir_entry.path().extension().is_some_and(|ex| ex == "pc"))
21    .map(|dir_entry| PathBuf::from(dir_entry.path()))
22    .collect()
23}
24
25impl TryFrom<pkg_config::PkgConfigFile> for cps::Package {
26    type Error = anyhow::Error;
27
28    fn try_from(pkg_config: pkg_config::PkgConfigFile) -> Result<cps::Package> {
29        let library_locations = lib_search::find_locations(&pkg_config)?;
30
31        let location_library_name = pkg_config.link_libraries.first();
32        let default_component_name = location_library_name.unwrap_or(&pkg_config.name);
33
34        let package_requires_map: HashMap<_, _> = pkg_config
35            .requires
36            .iter()
37            .filter(|req| req.version.is_some())
38            .map(|req| {
39                (
40                    req.name.clone(),
41                    cps::Requirement {
42                        version: req.version.clone(),
43                        ..cps::Requirement::default()
44                    },
45                )
46            })
47            .collect();
48        let package_requires_map =
49            (!package_requires_map.is_empty()).then_some(package_requires_map);
50
51        let local_requires: Vec<String> = library_locations
52            .keys()
53            .filter(|&name| {
54                location_library_name.is_some() && name != location_library_name.unwrap()
55            })
56            .map(|name| format!(":{}", name))
57            .collect();
58        let local_requires = (!local_requires.is_empty()).then_some(local_requires);
59        let remote_requres = (!pkg_config.requires.is_empty()).then(|| {
60            pkg_config
61                .requires
62                .iter()
63                .map(|d| d.name.clone())
64                .collect::<Vec<_>>()
65        });
66        let default_component_requires = match (local_requires, remote_requres) {
67            (Some(local), Some(remote)) => Some(local.into_iter().chain(remote).collect()),
68            (Some(local), None) => Some(local),
69            (None, Some(remote)) => Some(remote),
70            (None, None) => None,
71        };
72
73        let mut package_configurations: Option<Vec<String>> = None;
74        let mut components = HashMap::<String, cps::MaybeComponent>::new();
75        for (name, location) in library_locations {
76            match location {
77                LibraryLocation::Dylib(location) => {
78                    components.insert(
79                        name.clone(),
80                        cps::MaybeComponent::from_dylib_location(&location),
81                    );
82                }
83                LibraryLocation::Archive(location) => {
84                    components.insert(
85                        name.clone(),
86                        cps::MaybeComponent::from_archive_location(&location),
87                    );
88                }
89                LibraryLocation::Both { archive, dylib } => {
90                    package_configurations = Some(vec!["shared".to_string(), "static".to_string()]);
91                    components.insert(
92                        name.clone(),
93                        cps::MaybeComponent::Component(cps::Component::Interface(
94                            cps::ComponentFields {
95                                configurations: Some(
96                                    [
97                                        (
98                                            "shared".to_string(),
99                                            cps::Configuration {
100                                                requires: Some(vec![format!(":{}-shared", name)]),
101                                                ..cps::Configuration::default()
102                                            },
103                                        ),
104                                        (
105                                            "static".to_string(),
106                                            cps::Configuration {
107                                                requires: Some(vec![format!(":{}-static", name)]),
108                                                ..cps::Configuration::default()
109                                            },
110                                        ),
111                                    ]
112                                    .into_iter()
113                                    .collect(),
114                                ),
115                                ..cps::ComponentFields::default()
116                            },
117                        )),
118                    );
119                    components.insert(
120                        format!("{}-shared", name),
121                        cps::MaybeComponent::from_dylib_location(&archive),
122                    );
123                    components.insert(
124                        format!("{}-static", name),
125                        cps::MaybeComponent::from_archive_location(&dylib),
126                    );
127                }
128            };
129        }
130
131        let default_component = components.entry(default_component_name.clone()).or_insert(
132            cps::MaybeComponent::Component(cps::Component::Interface(
133                cps::ComponentFields::default(),
134            )),
135        );
136        let default_component = match default_component {
137            cps::MaybeComponent::Component(cps::Component::Interface(fields)) => fields,
138            cps::MaybeComponent::Component(cps::Component::Dylib(fields)) => fields,
139            cps::MaybeComponent::Component(cps::Component::Archive(fields)) => fields,
140            component => {
141                anyhow::bail!("Unknwon default component type found: {:?}", component)
142            }
143        };
144
145        // Requires could be per-configuration or on the component
146        if default_component_requires.is_some() {
147            if let Some(configurations) = &mut default_component.configurations {
148                for configuration in configurations.values_mut() {
149                    configuration.requires = Some(
150                        [
151                            &configuration.requires.clone().unwrap_or_default()[..],
152                            &default_component_requires.clone().unwrap_or_default()[..],
153                        ]
154                        .concat(),
155                    );
156                }
157            } else {
158                default_component.requires = default_component_requires;
159            }
160        }
161
162        default_component.compile_flags = (!pkg_config.compile_flags.is_empty())
163            .then(|| cps::LanguageStringList::any_language_map(pkg_config.compile_flags));
164        default_component.definitions = (!pkg_config.definitions.is_empty())
165            .then(|| cps::LanguageStringList::any_language_map(pkg_config.definitions));
166        default_component.includes = (!pkg_config.includes.is_empty())
167            .then(|| cps::LanguageStringList::any_language_map(pkg_config.includes));
168        default_component.link_flags =
169            (!pkg_config.link_flags.is_empty()).then_some(pkg_config.link_flags);
170
171        let cps = cps::Package {
172            name: pkg_config.name.clone(),
173            version: Some(pkg_config.version),
174            description: Some(pkg_config.description),
175            default_components: Some(vec![default_component_name.clone()]),
176            requires: package_requires_map,
177            components,
178            configurations: package_configurations,
179            ..cps::Package::default()
180        };
181        Ok(cps)
182    }
183}
184
185pub fn generate_all_from_pkg_config(outdir: &Path) -> Result<()> {
186    let pc_files = find_pc_files();
187
188    fs::create_dir_all(outdir)?;
189
190    for path in pc_files {
191        dbg!(&path);
192        let pc_filename = path
193            .file_name()
194            .context("error getting filename of pc file")?
195            .to_str()
196            .context("error converting OsStr to str")?
197            .to_string();
198        let data = std::fs::read_to_string(path)?;
199        let pkg_config = match pkg_config::PkgConfigFile::parse(&data) {
200            Ok(pkg_config) => pkg_config,
201            Err(error) => {
202                eprintln!("Error:\n{}", error);
203                continue;
204            }
205        };
206        let cps_package: cps::Package = match pkg_config.try_into() {
207            Ok(cps) => cps,
208            Err(error) => {
209                eprintln!("Error:\n{}", error);
210                continue;
211            }
212        };
213        let json = serde_json::to_string_pretty(&cps_package)?;
214        let cps_filename = pc_filename.replace(".pc", ".cps");
215        std::fs::write(outdir.join(cps_filename), json)?;
216    }
217
218    Ok(())
219}
220
221pub fn generate_from_pkg_config(pc_filepath: &Path, cps_filepath: &Path) -> Result<()> {
222    let data = std::fs::read_to_string(pc_filepath)?;
223    let pkg_config = pkg_config::PkgConfigFile::parse(&data)?;
224    let cps_package: cps::Package = pkg_config.try_into()?;
225    let json = serde_json::to_string_pretty(&cps_package)?;
226    std::fs::write(cps_filepath, json)?;
227    Ok(())
228}