Skip to main content

cmake_package/
lib.rs

1// SPDX-FileCopyrightText: 2024 Daniel Vrátil <dvratil@kde.org>
2//
3// SPDX-License-Identifier: MIT
4
5//! A simple CMake package finder.
6//!
7//! This crate is intended to be used in [cargo build scripts][cargo_build_script] to obtain
8//! information about and existing system [CMake package][cmake_package] and [CMake targets][cmake_target]
9//! defined in the package, such as include directories and link libraries for individual
10//! CMake targets defined in the package.
11//!
12//! The crate runs the `cmake` command in the background to query the system for the package
13//! and to extract the necessary information, which means that in order for this crate to work,
14//! the `cmake` executable must be located the system [`PATH`][wiki_path]. CMake version
15//! [3.19][CMAKE_MIN_VERSION] or newer is required for this crate to work.
16//!
17//! The entry point for the crate is the [`find_package()`] function that returns a builder,
18//! which you can use to specify further constraints on the package ([version][FindPackageBuilder::version]
19//! or [components][FindPackageBuilder::components]). Once you call the [`find()`][FindPackageBuilder::find]
20//! method on the builder, the crate will try to find the package on the system or return an
21//! error. If the package is found, an instance of the [`CMakePackage`] struct is returned that
22//! contains information about the package. Using its [`target()`][CMakePackage::target] method,
23//! you can query information about individual CMake targets defined in the package.
24//!
25//! If you want to make your dependency on CMake optional, you can use the [`find_cmake()`]
26//! function to check that a suitable version of CMake is found on the system and then decide
27//! how to proceed yourself. It is not necessary to call the function before using [`find_package()`].
28//!
29//! # Example
30//! ```no_run
31//! use cmake_package::find_package;
32//!
33//! let package = find_package("OpenSSL").version("1.0").find();
34//! let target = match package {
35//!     Err(_) => panic!("OpenSSL>=1.0 not found"),
36//!     Ok(package) => {
37//!         package.target("OpenSSL::SSL").unwrap()
38//!     }
39//! };
40//!
41//! println!("Include directories: {:?}", target.include_directories);
42//! target.link();
43//! ```
44//!
45//! # How Does it Work?
46//!
47//! When you call [`FindPackageBuilder::find()`], the crate will create a temporary directory
48//! with a `CMakeLists.txt` file that contains actual [`find_package()`][cmake_find_package]
49//! command to search for the package. The crate will then run actual `cmake` command in the
50//! temporary directory to let CMake find the package. The `CMakeLists.txt` then writes the
51//! information about the package into a JSON file that is then read by this crate to produce
52//! the [`CMakePackage`].
53//!
54//! When a target is queried using the [`CMakePackage::target()`] method, the crate runs the
55//! CMake command again the same directory, but this time the `CMakeLists.txt` attempts to locate
56//! the specified CMake target and list all its (relevant) properties and properties of all its
57//! transitive dependencies. The result is again written into a JSON file that is then processed
58//! by the crate to produce the [`CMakeTarget`] instance.
59//!
60//! # Known Limitations
61//!
62//! The crate currently supports primarily linking against shared libraries. Linking against
63//! static libraries is not tested and may not work as expected. The crate currently does not
64//! support linking against MacOS frameworks.
65//!
66//! [CMake generator expressions][cmake_generator_expr] are not supported in property values
67//! right now, because they are evaluated at later stage of the build, not during the "configure"
68//! phase of CMake, which is what this crate does. Some generator expressions could be supported
69//! by the crate in the future (e.g. by evaluating them ourselves).
70//!
71//! There's currently no way to customize the `CMakeLists.txt` file that is used to query the
72//! package or the target in order to extract non-standard properties or variables set by
73//! the CMake package. This may be addressed in the future.
74//!
75//! [wiki_path]: https://en.wikipedia.org/wiki/PATH_(variable)
76//! [cmake_package]: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html
77//! [cmake_target]: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#target-build-specification
78//! [cargo_build_script]: https://doc.rust-lang.org/cargo/reference/build-scripts.html
79//! [cmake_find_package]: https://cmake.org/cmake/help/latest/command/find_package.html
80//! [cmake_generator_expr]: https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html
81
82use std::io::Write;
83use std::path::PathBuf;
84
85#[cfg(any(target_os = "linux", target_os = "macos"))]
86use once_cell::sync::Lazy;
87#[cfg(any(target_os = "linux", target_os = "macos"))]
88use regex::Regex;
89use tempfile::TempDir;
90
91mod cmake;
92mod version;
93
94pub use cmake::{find_cmake, CMakeProgram, Error, CMAKE_MIN_VERSION};
95pub use version::{Version, VersionError};
96
97/// A CMake package found on the system.
98///
99/// Represents a CMake package found on the system. To find a package, use the [`find_package()`] function.
100/// The package can be queried for information about its individual CMake targets by [`CMakePackage::target()`].
101///
102/// # Example
103/// ```no_run
104/// use cmake_package::{CMakePackage, find_package};
105///
106/// let package: CMakePackage = find_package("OpenSSL").version("1.0").find().unwrap();
107/// ```
108#[derive(Debug)]
109pub struct CMakePackage {
110    cmake: CMakeProgram,
111    working_directory: TempDir,
112    verbose: bool,
113
114    /// Name of the CMake package
115    pub name: String,
116    /// Version of the package found on the system
117    pub version: Option<Version>,
118    /// Components of the package, as requested by the user in [`find_package()`]
119    pub components: Option<Vec<String>>,
120    /// Alternative names for the package, as requested by the user in [`find_package()`]
121    pub names: Option<Vec<String>>,
122}
123
124impl CMakePackage {
125    fn new(
126        cmake: CMakeProgram,
127        working_directory: TempDir,
128        name: String,
129        version: Option<Version>,
130        components: Option<Vec<String>>,
131        names: Option<Vec<String>>,
132        verbose: bool,
133    ) -> Self {
134        Self {
135            cmake,
136            working_directory,
137            name,
138            version,
139            components,
140            names,
141            verbose,
142        }
143    }
144
145    /// Queries the CMake package for information about a specific [CMake target][cmake_target].
146    /// Returns `None` if the target is not found in the package.
147    ///
148    /// [cmake_target]: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#imported-targets
149    pub fn target(&self, target: impl Into<String>) -> Option<CMakeTarget> {
150        cmake::find_target(self, target)
151    }
152
153    /// Queries the CMake target for the value of a specific property.
154    ///
155    /// Returns `None` if the property is not defined on the target.
156    ///
157    /// This is equivalent to calling [`get_target_property()`][cmake_get_target_property] in CMake.
158    ///
159    /// [cmake_get_target_property]: https://cmake.org/cmake/help/latest/command/get_target_property.html
160    pub fn target_property(
161        &self,
162        target: &CMakeTarget,
163        property: impl Into<String>,
164    ) -> Option<String> {
165        cmake::target_property(self, target, property)
166    }
167}
168
169/// Describes a CMake target found in a CMake package.
170///
171/// The target can be obtained by calling the [`target()`][CMakePackage::target()] method on a [`CMakePackage`] instance.
172///
173/// Use [`link()`][Self::link()] method to instruct cargo to link the final binary against the target.
174/// There's currently no way to automatically apply compiler arguments or include directories, since
175/// that depends on how the C/C++ code in your project is compiled (e.g. using the [cc][cc_crate] crate).
176/// Optional support for this may be added in the future.
177///
178/// # Example
179/// ```no_run
180/// use cmake_package::find_package;
181///
182/// let package = find_package("OpenSSL").version("1.0").find().unwrap();
183/// let target = package.target("OpenSSL::SSL").unwrap();
184/// println!("Include directories: {:?}", target.include_directories);
185/// println!("Link libraries: {:?}", target.link_libraries);
186/// target.link();
187/// ```
188///
189/// [cc_crate]: https://crates.io/crates/cc
190#[derive(Debug, Default, Clone)]
191pub struct CMakeTarget {
192    /// Name of the CMake target
193    pub name: String,
194    /// List of public compile definitions requirements for a library.
195    ///
196    /// Contains preprocessor definitions provided by the target and all its transitive dependencies
197    /// via their [`INTERFACE_COMPILE_DEFINITIONS`][cmake_interface_compile_definitions] target properties.
198    ///
199    /// [cmake_interface_compile_definitions]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_COMPILE_DEFINITIONS.html
200    pub compile_definitions: Vec<String>,
201    /// List of options to pass to the compiler.
202    ///
203    /// Contains compiler options provided by the target and all its transitive dependencies via
204    /// their [`INTERFACE_COMPILE_OPTIONS`][cmake_interface_compile_options] target properties.
205    ///
206    /// [cmake_interface_compile_options]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_COMPILE_OPTIONS.html
207    pub compile_options: Vec<String>,
208    /// List of include directories required to build the target.
209    ///
210    /// Contains include directories provided by the target and all its transitive dependencies via
211    /// their [`INTERFACE_INCLUDE_DIRECTORIES`][cmake_interface_include_directories] target properties.
212    ///
213    /// [cmake_interface_include_directories]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_INCLUDE_DIRECTORIES.html
214    pub include_directories: Vec<String>,
215    /// List of directories to use for the link step of shared library, module and executable targets.
216    ///
217    /// Contains link directories provided by the target and all its transitive dependencies via
218    /// their [`INTERFACE_LINK_DIRECTORIES`][cmake_interface_link_directories] target properties.
219    ///
220    /// [cmake_interface_link_directories]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_LINK_DIRECTORIES.html
221    pub link_directories: Vec<String>,
222    /// List of target's direct link dependencies, followed by indirect dependencies from the transitive closure of the direct
223    /// dependencies' [`INTERFACE_LINK_LIBRARIES`][cmake_interface_link_libraries] properties
224    ///
225    /// [cmake_interface_link_libraries]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_LINK_LIBRARIES.html
226    pub link_libraries: Vec<String>,
227    /// List of options to use for the link step of shared library, module and executable targets as well as the device link step.
228    ///
229    /// Contains link options provided by the target and all its transitive dependencies via
230    /// their [`INTERFACE_LINK_OPTIONS`][cmake_interface_link_options] target properties.
231    ///
232    /// [cmake_interface_link_options]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_LINK_OPTIONS.html
233    pub link_options: Vec<String>,
234    /// The location of the target on disk.
235    ///
236    /// [cmake_interface_location]: https://cmake.org/cmake/help/latest/prop_tgt/LOCATION.html
237    pub location: Option<String>,
238}
239
240/// Turns /usr/lib/libfoo.so.5 into foo, so that -lfoo rather than -l/usr/lib/libfoo.so.5
241/// is passed to the linker. Leaves "foo" untouched.
242#[cfg(any(target_os = "linux", target_os = "macos"))]
243fn link_name(lib: &str) -> &str {
244    static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"lib([^/]+)\.(?:so|dylib|a).*").unwrap());
245    match RE.captures(lib) {
246        Some(captures) => captures.get(1).map(|f| f.as_str()).unwrap_or(lib),
247        None => lib,
248    }
249}
250
251#[cfg(target_os = "windows")]
252fn link_name(lib: &str) -> &str {
253    lib
254}
255
256#[cfg(any(target_os = "linux", target_os = "macos"))]
257fn link_dir(lib: &str) -> Option<&str> {
258    static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(.*)/lib[^/]+\.(?:so|dylib|a).*").unwrap());
259    RE.captures(lib)?.get(1).map(|f| f.as_str())
260}
261
262#[cfg(target_os = "windows")]
263fn link_dir(_lib: &str) -> Option<&str> {
264    None
265}
266
267impl CMakeTarget {
268    /// Instructs cargo to link the final binary against the target.
269    ///
270    /// This method prints the necessary [`cargo:rustc-link-search=native={}`][cargo_rustc_link_search],
271    /// [`cargo:rustc-link-arg={}`][cargo_rustc_link_arg], and [`cargo:rustc-link-lib=dylib={}`][cargo_rustc_link_lib]
272    /// directives to the standard output for each of the target's [`link_directories`][Self::link_directories],
273    /// [`link_options`][Self::link_options], and [`link_libraries`][Self::link_libraries] respectively.
274    ///
275    /// [cargo_rustc_link_search]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-search
276    /// [cargo_rustc_link_arg]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-arg
277    /// [cargo_rustc_link_lib]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib]
278    pub fn link(&self) {
279        self.link_write(&mut std::io::stdout());
280    }
281
282    fn link_write<W: Write>(&self, io: &mut W) {
283        self.link_directories.iter().for_each(|dir| {
284            writeln!(io, "cargo:rustc-link-search=native={}", dir).unwrap();
285        });
286        self.link_options.iter().for_each(|opt| {
287            writeln!(io, "cargo:rustc-link-arg={}", opt).unwrap();
288        });
289        self.link_libraries.iter().for_each(|lib| {
290            if lib.starts_with("-") {
291                writeln!(io, "cargo:rustc-link-arg={}", lib).unwrap();
292            } else {
293                let kind = if lib.ends_with(".a") { "static" } else { "dylib" };
294                writeln!(io, "cargo:rustc-link-lib={}={}", kind, link_name(lib)).unwrap();
295            }
296
297            if let Some(lib) = link_dir(lib) {
298                writeln!(io, "cargo:rustc-link-search=native={}", lib).unwrap();
299            }
300        });
301    }
302}
303
304/// A builder for creating a [`CMakePackage`] instance. An instance of the builder is created by calling
305/// the [`find_package()`] function. Once the package is configured, [`FindPackageBuilder::find()`] will actually
306/// try to find the CMake package and return a [`CMakePackage`] instance (or error if the package is not found
307/// or an error occurs during the search).
308#[derive(Debug, Clone)]
309pub struct FindPackageBuilder {
310    name: String,
311    version: Option<Version>,
312    components: Option<Vec<String>>,
313    names: Option<Vec<String>>,
314    verbose: bool,
315    prefix_paths: Option<Vec<PathBuf>>,
316    defines: Vec<(String, String)>,
317}
318
319impl FindPackageBuilder {
320    fn new(name: String) -> Self {
321        Self {
322            name,
323            version: None,
324            components: None,
325            names: None,
326            verbose: false,
327            prefix_paths: None,
328            defines: Vec::new(),
329        }
330    }
331
332    /// Optionally specifies the minimum required version for the package to find.
333    /// If the package is not found or the version is too low, the `find()` method will return
334    /// [`Error::Version`] with the version of the package found on the system.
335    pub fn version(self, version: impl TryInto<Version>) -> Self {
336        Self {
337            version: Some(
338                version
339                    .try_into()
340                    .unwrap_or_else(|_| panic!("Invalid version specified!")),
341            ),
342            ..self
343        }
344    }
345
346    /// Optionally specifies the required components to locate in the package.
347    /// If the package is found, but any of the components is missing, the package is considered
348    /// as not found and the `find()` method will return [`Error::PackageNotFound`].
349    /// See the documentation on CMake's [`find_package()`][cmake_find_package] function and how it
350    /// treats the `COMPONENTS` argument.
351    ///
352    /// [cmake_find_package]: https://cmake.org/cmake/help/latest/command/find_package.html
353    pub fn components(self, components: impl Into<Vec<String>>) -> Self {
354        Self {
355            components: Some(components.into()),
356            ..self
357        }
358    }
359
360    /// Optionally specifies alternative package names to search for.
361    /// See the documentation on CMake's [`find_package()`][cmake_find_package] function
362    /// and how it treats the `NAMES` argument.
363    ///
364    /// [cmake_find_package]: https://cmake.org/cmake/help/latest/command/find_package.html
365    pub fn names<S, I>(self, names: I) -> Self
366    where
367        S: Into<String>,
368        I: IntoIterator<Item = S>,
369    {
370        let names: Vec<_> = names.into_iter().map(Into::into).collect();
371        if names.is_empty() {
372            return self;
373        }
374
375        Self {
376            names: Some(names),
377            ..self
378        }
379    }
380
381    /// Enable verbose output.
382    /// This will redirect output from actual execution of the `cmake` command to the standard output
383    /// and standard error of the build script.
384    pub fn verbose(self) -> Self {
385        Self {
386            verbose: true,
387            ..self
388        }
389    }
390
391    /// Add a custom CMake cache variable definition (`-DKEY=VALUE`) that will be passed
392    /// to the `cmake` invocation. This is useful when the CMake package's Find-module
393    /// or config-file depends on extra variables (e.g. `Boost_USE_STATIC_LIBS`).
394    ///
395    /// Can be called multiple times to add multiple definitions.
396    pub fn define(
397        mut self,
398        key: impl Into<String>,
399        value: impl Into<String>,
400    ) -> Self {
401        self.defines.push((key.into(), value.into()));
402        self
403    }
404
405    // Specify prefix paths.
406    // This sets directories to be searched for the package.
407    // [cmake_prefix_path]: https://cmake.org/cmake/help/latest/variable/CMAKE_PREFIX_PATH.html
408    pub fn prefix_paths(self, prefix_paths: Vec<PathBuf>) -> Self {
409        Self {
410            prefix_paths: Some(prefix_paths),
411            ..self
412        }
413    }
414
415    /// Tries to find the CMake package on the system.
416    /// Returns a [`CMakePackage`] instance if the package is found, otherwise an error.
417    pub fn find(self) -> Result<CMakePackage, cmake::Error> {
418        cmake::find_package(
419            self.name,
420            self.version,
421            self.components,
422            self.names,
423            self.verbose,
424            self.prefix_paths,
425            self.defines,
426        )
427    }
428}
429
430/// Find a CMake package on the system.
431///
432/// This function is the main entrypoint for the crate. It returns a builder object that you
433/// can use to specify further constraints on the package to find, such as the [version][FindPackageBuilder::version]
434/// or [components][FindPackageBuilder::components]. Once you call the [`find()`][FindPackageBuilder::find]
435/// method on the builder, the crate will try to find the package on the system or return an
436/// error if the package does not exist or does not satisfy some of the constraints. If the package
437/// is found, an instance of the [`CMakePackage`] struct is returned that can be used to further
438/// query the package for information about its individual CMake targets.
439///
440/// See the documentation for [`FindPackageBuilder`], [`CMakePackage`], and [`CMakeTarget`] for more
441/// information and the example in the crate documentation for a simple usage example.
442pub fn find_package(name: impl Into<String>) -> FindPackageBuilder {
443    FindPackageBuilder::new(name.into())
444}
445
446#[cfg(test)]
447mod testing {
448    use super::*;
449
450    // Note: requires cmake to be installed on the system
451    #[test]
452    fn test_find_package() {
453        let package = find_package("totallynonexistentpackage").find();
454        match package {
455            Ok(_) => panic!("Package should not be found"),
456            Err(cmake::Error::PackageNotFound) => (),
457            Err(err) => panic!("Unexpected error: {:?}", err),
458        }
459    }
460
461    // Note: requires cmake to be installed on the system
462    #[test]
463    fn test_find_package_with_version() {
464        let package = find_package("foo").version("1.0").find();
465        match package {
466            Ok(_) => panic!("Package should not be found"),
467            Err(cmake::Error::PackageNotFound) => (),
468            Err(err) => panic!("Unexpected error: {:?}", err),
469        }
470    }
471
472    #[test]
473    #[cfg(any(target_os = "linux", target_os = "macos"))]
474    fn test_link_to() {
475        let target = CMakeTarget {
476            name: "foo".into(),
477            compile_definitions: vec![],
478            compile_options: vec![],
479            include_directories: vec![],
480            link_directories: vec!["/usr/lib64".into()],
481            link_libraries: vec!["/usr/lib/libbar.so".into(), "/usr/lib64/libfoo.so.5".into()],
482            link_options: vec![],
483            location: None,
484        };
485
486        let mut buf = Vec::new();
487        target.link_write(&mut buf);
488        let output = String::from_utf8(buf).unwrap();
489        assert_eq!(
490            output.lines().collect::<Vec<&str>>(),
491            vec![
492                "cargo:rustc-link-search=native=/usr/lib64",
493                "cargo:rustc-link-lib=dylib=bar",
494                "cargo:rustc-link-search=native=/usr/lib",
495                "cargo:rustc-link-lib=dylib=foo",
496                "cargo:rustc-link-search=native=/usr/lib64"
497            ]
498        );
499    }
500}