cxx_qt_build/
dependencies.rs

1// SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Leon Matthes <leon.matthes@kdab.com>
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5
6//! This modules contains utilities for specifying dependencies with cxx-qt-build.
7
8use serde::{Deserialize, Serialize};
9
10use std::collections::{HashMap, HashSet};
11use std::path::{Path, PathBuf};
12
13/// When generating a library with cxx-qt-build, the library may need to export certain flags or headers.
14/// These are all specified by this Interface struct, which should be passed to the [crate::CxxQtBuilder::library] function.
15pub struct Interface {
16    pub(crate) compile_definitions: HashMap<String, Option<String>>,
17    pub(crate) initializers: Vec<PathBuf>,
18    // The name of the links keys, whose CXX-Qt dependencies to reexport
19    pub(crate) reexport_links: HashSet<String>,
20    pub(crate) exported_include_prefixes: Vec<String>,
21    pub(crate) exported_include_directories: Vec<(PathBuf, String)>,
22    // TODO: In future, we want to also set up the include paths so that you can include anything
23    // from the crates source directory.
24    // Once this is done, this flag should indicate whether or not to export our own crates source
25    // directory to downstream dependencies?
26    // export_crate_directory: bool,
27}
28
29impl Default for Interface {
30    fn default() -> Self {
31        Self {
32            compile_definitions: HashMap::new(),
33            initializers: Vec::new(),
34            reexport_links: HashSet::new(),
35            exported_include_prefixes: vec![super::crate_name()],
36            exported_include_directories: Vec::new(),
37        }
38    }
39}
40
41impl Interface {
42    /// Add a compile-time-definition for the C++ code built by this crate and all downstream
43    /// dependencies
44    ///
45    /// This function will panic if the variable has already been defined with a different value.
46    ///
47    /// Also please note that any definitions added here will only be exported throughout the cargo
48    /// build. Due to technical limitations, they can not be imported into CMake with the
49    /// cxxqt_import_crate function.
50    pub fn define(mut self, variable: &str, value: Option<&str>) -> Self {
51        use std::collections::hash_map::Entry::*;
52
53        let entry = self.compile_definitions.entry(variable.to_owned());
54        match entry {
55            Vacant(entry) => entry.insert(value.map(String::from)),
56            Occupied(entry) => {
57                if entry.get().as_deref() == value {
58                    println!("Warning: Silently ignoring duplicate compiler definition for {variable} with {value:?}.");
59                }
60                panic!(
61                    "Cxx-Qt-build - Error: Interface::define - Duplicate compile-time definition for variable {variable} with value {value:?}!"
62                );
63            }
64        };
65        self
66    }
67
68    /// Add a C++ file path that will be exported as an initializer to downstream dependencies.
69    ///
70    /// Initializer files will be built into object files, instead of linked into the static
71    /// library.
72    /// This way, the static variables and their constructors in this code will not be optimized
73    /// out by the linker.
74    pub fn initializer(mut self, path: impl AsRef<Path>) -> Self {
75        let path = PathBuf::from(path.as_ref());
76        let path = path
77            .canonicalize()
78            .expect("Failed to canonicalize path to initializer! Does the path exist?");
79        self.initializers.push(path);
80        self
81    }
82
83    /// Export all headers with the given prefix to downstream dependencies
84    ///
85    /// Note: This will overwrite any previously specified header_prefixes, including the default
86    /// header_prefix of this crate.
87    ///
88    /// This function will panic if any of the given prefixes are already exported through the
89    /// [Self::export_include_directory] function.
90    pub fn export_include_prefixes<'a>(
91        mut self,
92        prefixes: impl IntoIterator<Item = &'a str>,
93    ) -> Self {
94        let prefixes = prefixes.into_iter().map(|item| item.to_string()).collect();
95
96        let mut exported_prefixes = self
97            .exported_include_directories
98            .iter()
99            .map(|(_path, prefix)| prefix);
100        for prefix in &prefixes {
101            if let Some(duplicate) =
102                exported_prefixes.find(|exported_prefix| exported_prefix.starts_with(prefix))
103            {
104                panic!("Duplicate export_prefix! Cannot export `{prefix}`, as `{duplicate}` is already exported as an export_include_directory!");
105            }
106        }
107
108        self.exported_include_prefixes = prefixes;
109        self
110    }
111
112    /// Add a directory that will be added as an include directory under the given prefix.
113    ///
114    /// The prefix will automatically be exported (see also: [Self::export_include_prefixes])
115    ///
116    /// This function will panic if the given prefix is already exported.
117    pub fn export_include_directory(mut self, directory: impl AsRef<Path>, prefix: &str) -> Self {
118        let mut exported_prefixes = self.exported_include_prefixes.iter().chain(
119            self.exported_include_directories
120                .iter()
121                .map(|(_path, prefix)| prefix),
122        );
123        if let Some(duplicate) =
124            exported_prefixes.find(|exported_prefix| exported_prefix.starts_with(prefix))
125        {
126            panic!("Duplicate export_prefix! Cannot export `{prefix}`, as `{duplicate}` is already exported!");
127        }
128
129        self.exported_include_directories
130            .push((directory.as_ref().into(), prefix.to_owned()));
131        self
132    }
133
134    /// Reexport the dependency with the given link name.
135    /// This will make the dependency available to downstream dependencies.
136    ///
137    /// Specifically it will reexport all include_prefixes of the given dependency
138    /// as well as any definitions made by that dependency.
139    ///
140    /// Note that the link name may differ from the crate name.
141    /// Check your dependencies manifest file for the correct link name.
142    pub fn reexport_dependency(mut self, link_name: &str) -> Self {
143        self.reexport_links.insert(link_name.to_owned());
144        self
145    }
146}
147
148#[derive(Clone, Serialize, Deserialize)]
149/// This struct is used by cxx-qt-build internally to propagate data through to downstream
150/// dependencies
151pub(crate) struct Manifest {
152    pub(crate) name: String,
153    pub(crate) link_name: String,
154    pub(crate) qt_modules: Vec<String>,
155    pub(crate) defines: Vec<(String, Option<String>)>,
156    pub(crate) initializers: Vec<PathBuf>,
157    pub(crate) exported_include_prefixes: Vec<String>,
158}
159
160#[derive(Clone)]
161/// A dependency that has been set up with [crate::CxxQtBuilder::library] and is available to
162/// the crate that is currently being built.
163pub(crate) struct Dependency {
164    /// The path of the dependencies export directory
165    pub(crate) path: PathBuf,
166    /// The deserialized manifest of the dependency
167    pub(crate) manifest: Manifest,
168}
169
170impl Dependency {
171    /// This function will search the environment for all dependencies that have been set up with
172    /// CxxQtBuilder::library.
173    /// They export their manifest paths as metadata, which will be exposed to us as an environment
174    /// variable.
175    /// We extract those paths here, parse the manifest and make sure to set it up correctly as a
176    /// dependency.
177    ///
178    /// See also the internals "build system" section of our book.
179    pub(crate) fn find_all() -> Vec<Dependency> {
180        std::env::vars_os()
181            .map(|(var, value)| (var.to_string_lossy().to_string(), value))
182            .filter(|(var, _)| var.starts_with("DEP_") && var.ends_with("_CXX_QT_MANIFEST_PATH"))
183            .map(|(_, manifest_path)| {
184                let manifest_path = PathBuf::from(manifest_path);
185                let manifest: Manifest = serde_json::from_str(
186                    &std::fs::read_to_string(&manifest_path)
187                        .expect("Could not read dependency manifest file!"),
188                )
189                .expect("Could not deserialize dependency manifest file!");
190
191                println!(
192                    "cxx-qt-build: Discovered dependency `{}` at: {}",
193                    manifest.name,
194                    manifest_path.to_string_lossy()
195                );
196                Dependency {
197                    path: manifest_path.parent().unwrap().to_owned(),
198                    manifest,
199                }
200            })
201            .collect()
202    }
203}
204
205pub(crate) fn initializer_paths(
206    interface: Option<&Interface>,
207    dependencies: &[Dependency],
208) -> HashSet<PathBuf> {
209    dependencies
210        .iter()
211        .flat_map(|dep| dep.manifest.initializers.iter().cloned())
212        .chain(
213            interface
214                .iter()
215                .flat_map(|interface| interface.initializers.iter().cloned()),
216        )
217        .collect()
218}
219
220pub(crate) fn all_include_prefixes(
221    interface: &Interface,
222    dependencies: &[Dependency],
223) -> Vec<String> {
224    interface
225        .exported_include_prefixes
226        .iter()
227        .cloned()
228        .chain(
229            interface
230                .exported_include_directories
231                .iter()
232                .map(|(_path, prefix)| prefix.clone()),
233        )
234        .chain(
235            dependencies
236                .iter()
237                .flat_map(|dep| &dep.manifest.exported_include_prefixes)
238                .cloned(),
239        )
240        .collect()
241}
242
243pub(crate) fn reexported_dependencies(
244    interface: &Interface,
245    dependencies: &[Dependency],
246) -> Vec<Dependency> {
247    let mut exported_dependencies = Vec::new();
248    for link_name in &interface.reexport_links {
249        if let Some(dependency) = dependencies
250            .iter()
251            .find(|dep| &dep.manifest.link_name == link_name)
252        {
253            exported_dependencies.push(dependency.clone());
254        } else {
255            panic!("Could not find dependency with link name `{link_name}` to reexport!");
256        }
257    }
258    exported_dependencies
259}
260
261pub(crate) fn all_compile_definitions(
262    interface: Option<&Interface>,
263    dependencies: &[Dependency],
264) -> Vec<(String, Option<String>)> {
265    // For each definition, store the name of the crate that defines it so we can generate a
266    // nicer error message
267    let mut definitions: HashMap<String, (Option<String>, String)> = interface
268        .iter()
269        .flat_map(|interface| &interface.compile_definitions)
270        .map(|(key, value)| (key.clone(), (value.clone(), crate::crate_name())))
271        .collect();
272
273    for dependency in dependencies {
274        for (variable, value) in &dependency.manifest.defines {
275            use std::collections::hash_map::Entry::*;
276            let entry = definitions.entry(variable.to_owned());
277
278            match entry {
279                Vacant(entry) => {
280                    entry.insert((value.to_owned(), dependency.manifest.name.clone()));
281                }
282                Occupied(entry) => {
283                    let existing_value = &entry.get().0;
284                    // Only allow duplicate definitions with the same value
285                    if existing_value != value {
286                        panic!("Conflicting compiler definitions requested!\nCrate {existing} exports {variable}={existing_value:?}, and crate {conflicting} exports {variable}={value:?}",
287                            existing=entry.get().1,
288                            conflicting = dependency.manifest.name);
289                    }
290                }
291            }
292        }
293    }
294
295    definitions
296        .into_iter()
297        .map(|(key, (value, _crate_name))| (key, value))
298        .collect()
299}