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;
83
84#[cfg(any(target_os = "linux", target_os = "macos"))]
85use once_cell::sync::Lazy;
86#[cfg(any(target_os = "linux", target_os = "macos"))]
87use regex::Regex;
88use tempfile::TempDir;
89
90mod cmake;
91mod version;
92
93pub use cmake::{find_cmake, CMakeProgram, Error, CMAKE_MIN_VERSION};
94pub use version::{Version, VersionError};
95
96/// A CMake package found on the system.
97///
98/// Represents a CMake package found on the system. To find a package, use the [`find_package()`] function.
99/// The package can be queried for information about its individual CMake targets by [`CMakePackage::target()`].
100///
101/// # Example
102/// ```no_run
103/// use cmake_package::{CMakePackage, find_package};
104///
105/// let package: CMakePackage = find_package("OpenSSL").version("1.0").find().unwrap();
106/// ```
107#[derive(Debug)]
108pub struct CMakePackage {
109 cmake: CMakeProgram,
110 working_directory: TempDir,
111 verbose: bool,
112
113 /// Name of the CMake package
114 pub name: String,
115 /// Version of the package found on the system
116 pub version: Option<Version>,
117 /// Components of the package, as requested by the user in [`find_package()`]
118 pub components: Option<Vec<String>>,
119}
120
121impl CMakePackage {
122 fn new(
123 cmake: CMakeProgram,
124 working_directory: TempDir,
125 name: String,
126 version: Option<Version>,
127 components: Option<Vec<String>>,
128 verbose: bool,
129 ) -> Self {
130 Self {
131 cmake,
132 working_directory,
133 name,
134 version,
135 components,
136 verbose,
137 }
138 }
139
140 /// Queries the CMake package for information about a specific [CMake target][cmake_target].
141 /// Returns `None` if the target is not found in the package.
142 ///
143 /// [cmake_target]: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#imported-targets
144 pub fn target(&self, target: impl Into<String>) -> Option<CMakeTarget> {
145 cmake::find_target(self, target)
146 }
147}
148
149/// Describes a CMake target found in a CMake package.
150///
151/// The target can be obtained by calling the [`target()`][CMakePackage::target()] method on a [`CMakePackage`] instance.
152///
153/// Use [`link()`][Self::link()] method to instruct cargo to link the final binary against the target.
154/// There's currently no way to automatically apply compiler arguments or include directories, since
155/// that depends on how the C/C++ code in your project is compiled (e.g. using the [cc][cc_crate] crate).
156/// Optional support for this may be added in the future.
157///
158/// # Example
159/// ```no_run
160/// use cmake_package::find_package;
161///
162/// let package = find_package("OpenSSL").version("1.0").find().unwrap();
163/// let target = package.target("OpenSSL::SSL").unwrap();
164/// println!("Include directories: {:?}", target.include_directories);
165/// println!("Link libraries: {:?}", target.link_libraries);
166/// target.link();
167/// ```
168///
169/// [cc_crate]: https://crates.io/crates/cc
170#[derive(Debug, Default, Clone)]
171pub struct CMakeTarget {
172 /// Name of the CMake target
173 pub name: String,
174 /// List of public compile definitions requirements for a library.
175 ///
176 /// Contains preprocessor definitions provided by the target and all its transitive dependencies
177 /// via their [`INTERFACE_COMPILE_DEFINITIONS`][cmake_interface_compile_definitions] target properties.
178 ///
179 /// [cmake_interface_compile_definitions]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_COMPILE_DEFINITIONS.html
180 pub compile_definitions: Vec<String>,
181 /// List of options to pass to the compiler.
182 ///
183 /// Contains compiler options provided by the target and all its transitive dependencies via
184 /// their [`INTERFACE_COMPILE_OPTIONS`][cmake_interface_compile_options] target properties.
185 ///
186 /// [cmake_interface_compile_options]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_COMPILE_OPTIONS.html
187 pub compile_options: Vec<String>,
188 /// List of include directories required to build the target.
189 ///
190 /// Contains include directories provided by the target and all its transitive dependencies via
191 /// their [`INTERFACE_INCLUDE_DIRECTORIES`][cmake_interface_include_directories] target properties.
192 ///
193 /// [cmake_interface_include_directories]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_INCLUDE_DIRECTORIES.html
194 pub include_directories: Vec<String>,
195 /// List of directories to use for the link step of shared library, module and executable targets.
196 ///
197 /// Contains link directories provided by the target and all its transitive dependencies via
198 /// their [`INTERFACE_LINK_DIRECTORIES`][cmake_interface_link_directories] target properties.
199 ///
200 /// [cmake_interface_link_directories]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_LINK_DIRECTORIES.html
201 pub link_directories: Vec<String>,
202 /// List of target's direct link dependencies, followed by indirect dependencies from the transitive closure of the direct
203 /// dependencies' [`INTERFACE_LINK_LIBRARIES`][cmake_interface_link_libraries] properties
204 ///
205 /// [cmake_interface_link_libraries]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_LINK_LIBRARIES.html
206 pub link_libraries: Vec<String>,
207 /// List of options to use for the link step of shared library, module and executable targets as well as the device link step.
208 ///
209 /// Contains link options provided by the target and all its transitive dependencies via
210 /// their [`INTERFACE_LINK_OPTIONS`][cmake_interface_link_options] target properties.
211 ///
212 /// [cmake_interface_link_options]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_LINK_OPTIONS.html
213 pub link_options: Vec<String>,
214 /// The location of the target on disk.
215 ///
216 /// [cmake_interface_location]: https://cmake.org/cmake/help/latest/prop_tgt/LOCATION.html
217 pub location: Option<String>,
218}
219
220/// Turns /usr/lib/libfoo.so.5 into foo, so that -lfoo rather than -l/usr/lib/libfoo.so.5
221/// is passed to the linker. Leaves "foo" untouched.
222#[cfg(any(target_os = "linux", target_os = "macos"))]
223fn link_name(lib: &str) -> &str {
224 static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"lib([^/]+)\.so.*").unwrap());
225 match RE.captures(lib) {
226 Some(captures) => captures.get(1).map(|f| f.as_str()).unwrap_or(lib),
227 None => lib,
228 }
229}
230
231#[cfg(target_os = "windows")]
232fn link_name(lib: &str) -> &str {
233 lib
234}
235
236#[cfg(any(target_os = "linux", target_os = "macos"))]
237fn link_dir(lib: &str) -> Option<&str> {
238 static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(.*)/lib[^/]+\.so.*").unwrap());
239 RE.captures(lib)?.get(1).map(|f| f.as_str())
240}
241
242#[cfg(target_os = "windows")]
243fn link_dir(_lib: &str) -> Option<&str> {
244 None
245}
246
247impl CMakeTarget {
248 /// Instructs cargo to link the final binary against the target.
249 ///
250 /// This method prints the necessary [`cargo:rustc-link-search=native={}`][cargo_rustc_link_search],
251 /// [`cargo:rustc-link-arg={}`][cargo_rustc_link_arg], and [`cargo:rustc-link-lib=dylib={}`][cargo_rustc_link_lib]
252 /// directives to the standard output for each of the target's [`link_directories`][Self::link_directories],
253 /// [`link_options`][Self::link_options], and [`link_libraries`][Self::link_libraries] respectively.
254 ///
255 /// [cargo_rustc_link_search]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-search
256 /// [cargo_rustc_link_arg]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-arg
257 /// [cargo_rustc_link_lib]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib]
258 pub fn link(&self) {
259 self.link_write(&mut std::io::stdout());
260 }
261
262 fn link_write<W: Write>(&self, io: &mut W) {
263 self.link_directories.iter().for_each(|dir| {
264 writeln!(io, "cargo:rustc-link-search=native={}", dir).unwrap();
265 });
266 self.link_options.iter().for_each(|opt| {
267 writeln!(io, "cargo:rustc-link-arg={}", opt).unwrap();
268 });
269 self.link_libraries.iter().for_each(|lib| {
270 if lib.starts_with("-") {
271 writeln!(io, "cargo:rustc-link-arg={}", lib).unwrap();
272 } else {
273 writeln!(io, "cargo:rustc-link-lib=dylib={}", link_name(lib)).unwrap();
274 }
275
276 if let Some(lib) = link_dir(lib) {
277 writeln!(io, "cargo:rustc-link-search=native={}", lib).unwrap();
278 }
279 });
280 }
281}
282
283/// A builder for creating a [`CMakePackage`] instance. An instance of the builder is created by calling
284/// the [`find_package()`] function. Once the package is configured, [`FindPackageBuilder::find()`] will actually
285/// try to find the CMake package and return a [`CMakePackage`] instance (or error if the package is not found
286/// or an error occurs during the search).
287#[derive(Debug, Clone)]
288pub struct FindPackageBuilder {
289 name: String,
290 version: Option<Version>,
291 components: Option<Vec<String>>,
292 verbose: bool,
293}
294
295impl FindPackageBuilder {
296 fn new(name: String) -> Self {
297 Self {
298 name,
299 version: None,
300 components: None,
301 verbose: false,
302 }
303 }
304
305 /// Optionally specifies the minimum required version for the package to find.
306 /// If the package is not found or the version is too low, the `find()` method will return
307 /// [`Error::Version`] with the version of the package found on the system.
308 pub fn version(self, version: impl TryInto<Version>) -> Self {
309 Self {
310 version: Some(
311 version
312 .try_into()
313 .unwrap_or_else(|_| panic!("Invalid version specified!")),
314 ),
315 ..self
316 }
317 }
318
319 /// Optionally specifies the required components to locate in the package.
320 /// If the package is found, but any of the components is missing, the package is considered
321 /// as not found and the `find()` method will return [`Error::PackageNotFound`].
322 /// See the documentation on CMake's [`find_package()`][cmake_find_package] function and how it
323 /// treats the `COMPONENTS` argument.
324 ///
325 /// [cmake_find_package]: https://cmake.org/cmake/help/latest/command/find_package.html
326 pub fn components(self, components: impl Into<Vec<String>>) -> Self {
327 Self {
328 components: Some(components.into()),
329 ..self
330 }
331 }
332
333 /// Enable verbose output.
334 /// This will redirect output from actual execution of the `cmake` command to the standard output
335 /// and standard error of the build script.
336 pub fn verbose(self) -> Self {
337 Self {
338 verbose: true,
339 ..self
340 }
341 }
342
343 /// Tries to find the CMake package on the system.
344 /// Returns a [`CMakePackage`] instance if the package is found, otherwise an error.
345 pub fn find(self) -> Result<CMakePackage, cmake::Error> {
346 cmake::find_package(self.name, self.version, self.components, self.verbose)
347 }
348}
349
350/// Find a CMake package on the system.
351///
352/// This function is the main entrypoint for the crate. It returns a builder object that you
353/// can use to specify further constraints on the package to find, such as the [version][FindPackageBuilder::version]
354/// or [components][FindPackageBuilder::components]. Once you call the [`find()`][FindPackageBuilder::find]
355/// method on the builder, the crate will try to find the package on the system or return an
356/// error if the package does not exist or does not satisfy some of the constraints. If the package
357/// is found, an instance of the [`CMakePackage`] struct is returned that can be used to further
358/// query the package for information about its individual CMake targets.
359///
360/// See the documentation for [`FindPackageBuilder`], [`CMakePackage`], and [`CMakeTarget`] for more
361/// information and the example in the crate documentation for a simple usage example.
362pub fn find_package(name: impl Into<String>) -> FindPackageBuilder {
363 FindPackageBuilder::new(name.into())
364}
365
366#[cfg(test)]
367mod testing {
368 use super::*;
369
370 // Note: requires cmake to be installed on the system
371 #[test]
372 fn test_find_package() {
373 let package = find_package("totallynonexistentpackage").find();
374 match package {
375 Ok(_) => panic!("Package should not be found"),
376 Err(cmake::Error::PackageNotFound) => (),
377 Err(err) => panic!("Unexpected error: {:?}", err),
378 }
379 }
380
381 // Note: requires cmake to be installed on the system
382 #[test]
383 fn test_find_package_with_version() {
384 let package = find_package("foo").version("1.0").find();
385 match package {
386 Ok(_) => panic!("Package should not be found"),
387 Err(cmake::Error::PackageNotFound) => (),
388 Err(err) => panic!("Unexpected error: {:?}", err),
389 }
390 }
391
392 #[test]
393 #[cfg(any(target_os = "linux", target_os = "macos"))]
394 fn test_link_to() {
395 let target = CMakeTarget {
396 name: "foo".into(),
397 compile_definitions: vec![],
398 compile_options: vec![],
399 include_directories: vec![],
400 link_directories: vec!["/usr/lib64".into()],
401 link_libraries: vec!["/usr/lib/libbar.so".into(), "/usr/lib64/libfoo.so.5".into()],
402 link_options: vec![],
403 location: None,
404 };
405
406 let mut buf = Vec::new();
407 target.link_write(&mut buf);
408 let output = String::from_utf8(buf).unwrap();
409 assert_eq!(
410 output.lines().collect::<Vec<&str>>(),
411 vec![
412 "cargo:rustc-link-search=native=/usr/lib64",
413 "cargo:rustc-link-lib=dylib=bar",
414 "cargo:rustc-link-search=native=/usr/lib",
415 "cargo:rustc-link-lib=dylib=foo",
416 "cargo:rustc-link-search=native=/usr/lib64"
417 ]
418 );
419 }
420}