cmake-package 0.1.15

A helper library for Cargo build-scripts to find and link against existing CMake packages.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
// SPDX-FileCopyrightText: 2024 Daniel Vrátil <dvratil@kde.org>
//
// SPDX-License-Identifier: MIT

//! A simple CMake package finder.
//!
//! This crate is intended to be used in [cargo build scripts][cargo_build_script] to obtain
//! information about and existing system [CMake package][cmake_package] and [CMake targets][cmake_target]
//! defined in the package, such as include directories and link libraries for individual
//! CMake targets defined in the package.
//!
//! The crate runs the `cmake` command in the background to query the system for the package
//! and to extract the necessary information, which means that in order for this crate to work,
//! the `cmake` executable must be located the system [`PATH`][wiki_path]. CMake version
//! [3.19][CMAKE_MIN_VERSION] or newer is required for this crate to work.
//!
//! The entry point for the crate is the [`find_package()`] function that returns a builder,
//! which you can use to specify further constraints on the package ([version][FindPackageBuilder::version]
//! or [components][FindPackageBuilder::components]). Once you call the [`find()`][FindPackageBuilder::find]
//! method on the builder, the crate will try to find the package on the system or return an
//! error. If the package is found, an instance of the [`CMakePackage`] struct is returned that
//! contains information about the package. Using its [`target()`][CMakePackage::target] method,
//! you can query information about individual CMake targets defined in the package.
//!
//! If you want to make your dependency on CMake optional, you can use the [`find_cmake()`]
//! function to check that a suitable version of CMake is found on the system and then decide
//! how to proceed yourself. It is not necessary to call the function before using [`find_package()`].
//!
//! # Example
//! ```no_run
//! use cmake_package::find_package;
//!
//! let package = find_package("OpenSSL").version("1.0").find();
//! let target = match package {
//!     Err(_) => panic!("OpenSSL>=1.0 not found"),
//!     Ok(package) => {
//!         package.target("OpenSSL::SSL").unwrap()
//!     }
//! };
//!
//! println!("Include directories: {:?}", target.include_directories);
//! target.link();
//! ```
//!
//! # How Does it Work?
//!
//! When you call [`FindPackageBuilder::find()`], the crate will create a temporary directory
//! with a `CMakeLists.txt` file that contains actual [`find_package()`][cmake_find_package]
//! command to search for the package. The crate will then run actual `cmake` command in the
//! temporary directory to let CMake find the package. The `CMakeLists.txt` then writes the
//! information about the package into a JSON file that is then read by this crate to produce
//! the [`CMakePackage`].
//!
//! When a target is queried using the [`CMakePackage::target()`] method, the crate runs the
//! CMake command again the same directory, but this time the `CMakeLists.txt` attempts to locate
//! the specified CMake target and list all its (relevant) properties and properties of all its
//! transitive dependencies. The result is again written into a JSON file that is then processed
//! by the crate to produce the [`CMakeTarget`] instance.
//!
//! # Known Limitations
//!
//! The crate currently supports primarily linking against shared libraries. Linking against
//! static libraries is not tested and may not work as expected. The crate currently does not
//! support linking against MacOS frameworks.
//!
//! [CMake generator expressions][cmake_generator_expr] are not supported in property values
//! right now, because they are evaluated at later stage of the build, not during the "configure"
//! phase of CMake, which is what this crate does. Some generator expressions could be supported
//! by the crate in the future (e.g. by evaluating them ourselves).
//!
//! There's currently no way to customize the `CMakeLists.txt` file that is used to query the
//! package or the target in order to extract non-standard properties or variables set by
//! the CMake package. This may be addressed in the future.
//!
//! [wiki_path]: https://en.wikipedia.org/wiki/PATH_(variable)
//! [cmake_package]: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html
//! [cmake_target]: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#target-build-specification
//! [cargo_build_script]: https://doc.rust-lang.org/cargo/reference/build-scripts.html
//! [cmake_find_package]: https://cmake.org/cmake/help/latest/command/find_package.html
//! [cmake_generator_expr]: https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html

use std::io::Write;
use std::path::PathBuf;

#[cfg(any(target_os = "linux", target_os = "macos"))]
use once_cell::sync::Lazy;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use regex::Regex;
use tempfile::TempDir;

mod cmake;
mod version;

pub use cmake::{find_cmake, CMakeProgram, Error, CMAKE_MIN_VERSION};
pub use version::{Version, VersionError};

/// A CMake package found on the system.
///
/// Represents a CMake package found on the system. To find a package, use the [`find_package()`] function.
/// The package can be queried for information about its individual CMake targets by [`CMakePackage::target()`].
///
/// # Example
/// ```no_run
/// use cmake_package::{CMakePackage, find_package};
///
/// let package: CMakePackage = find_package("OpenSSL").version("1.0").find().unwrap();
/// ```
#[derive(Debug)]
pub struct CMakePackage {
    cmake: CMakeProgram,
    working_directory: TempDir,
    verbose: bool,

    /// Name of the CMake package
    pub name: String,
    /// Version of the package found on the system
    pub version: Option<Version>,
    /// Components of the package, as requested by the user in [`find_package()`]
    pub components: Option<Vec<String>>,
    /// Alternative names for the package, as requested by the user in [`find_package()`]
    pub names: Option<Vec<String>>,
}

impl CMakePackage {
    fn new(
        cmake: CMakeProgram,
        working_directory: TempDir,
        name: String,
        version: Option<Version>,
        components: Option<Vec<String>>,
        names: Option<Vec<String>>,
        verbose: bool,
    ) -> Self {
        Self {
            cmake,
            working_directory,
            name,
            version,
            components,
            names,
            verbose,
        }
    }

    /// Queries the CMake package for information about a specific [CMake target][cmake_target].
    /// Returns `None` if the target is not found in the package.
    ///
    /// [cmake_target]: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#imported-targets
    pub fn target(&self, target: impl Into<String>) -> Option<CMakeTarget> {
        cmake::find_target(self, target)
    }

    /// Queries the CMake target for the value of a specific property.
    ///
    /// Returns `None` if the property is not defined on the target.
    ///
    /// This is equivalent to calling [`get_target_property()`][cmake_get_target_property] in CMake.
    ///
    /// [cmake_get_target_property]: https://cmake.org/cmake/help/latest/command/get_target_property.html
    pub fn target_property(
        &self,
        target: &CMakeTarget,
        property: impl Into<String>,
    ) -> Option<String> {
        cmake::target_property(self, target, property)
    }
}

/// Describes a CMake target found in a CMake package.
///
/// The target can be obtained by calling the [`target()`][CMakePackage::target()] method on a [`CMakePackage`] instance.
///
/// Use [`link()`][Self::link()] method to instruct cargo to link the final binary against the target.
/// There's currently no way to automatically apply compiler arguments or include directories, since
/// that depends on how the C/C++ code in your project is compiled (e.g. using the [cc][cc_crate] crate).
/// Optional support for this may be added in the future.
///
/// # Example
/// ```no_run
/// use cmake_package::find_package;
///
/// let package = find_package("OpenSSL").version("1.0").find().unwrap();
/// let target = package.target("OpenSSL::SSL").unwrap();
/// println!("Include directories: {:?}", target.include_directories);
/// println!("Link libraries: {:?}", target.link_libraries);
/// target.link();
/// ```
///
/// [cc_crate]: https://crates.io/crates/cc
#[derive(Debug, Default, Clone)]
pub struct CMakeTarget {
    /// Name of the CMake target
    pub name: String,
    /// List of public compile definitions requirements for a library.
    ///
    /// Contains preprocessor definitions provided by the target and all its transitive dependencies
    /// via their [`INTERFACE_COMPILE_DEFINITIONS`][cmake_interface_compile_definitions] target properties.
    ///
    /// [cmake_interface_compile_definitions]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_COMPILE_DEFINITIONS.html
    pub compile_definitions: Vec<String>,
    /// List of options to pass to the compiler.
    ///
    /// Contains compiler options provided by the target and all its transitive dependencies via
    /// their [`INTERFACE_COMPILE_OPTIONS`][cmake_interface_compile_options] target properties.
    ///
    /// [cmake_interface_compile_options]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_COMPILE_OPTIONS.html
    pub compile_options: Vec<String>,
    /// List of include directories required to build the target.
    ///
    /// Contains include directories provided by the target and all its transitive dependencies via
    /// their [`INTERFACE_INCLUDE_DIRECTORIES`][cmake_interface_include_directories] target properties.
    ///
    /// [cmake_interface_include_directories]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_INCLUDE_DIRECTORIES.html
    pub include_directories: Vec<String>,
    /// List of directories to use for the link step of shared library, module and executable targets.
    ///
    /// Contains link directories provided by the target and all its transitive dependencies via
    /// their [`INTERFACE_LINK_DIRECTORIES`][cmake_interface_link_directories] target properties.
    ///
    /// [cmake_interface_link_directories]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_LINK_DIRECTORIES.html
    pub link_directories: Vec<String>,
    /// List of target's direct link dependencies, followed by indirect dependencies from the transitive closure of the direct
    /// dependencies' [`INTERFACE_LINK_LIBRARIES`][cmake_interface_link_libraries] properties
    ///
    /// [cmake_interface_link_libraries]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_LINK_LIBRARIES.html
    pub link_libraries: Vec<String>,
    /// List of options to use for the link step of shared library, module and executable targets as well as the device link step.
    ///
    /// Contains link options provided by the target and all its transitive dependencies via
    /// their [`INTERFACE_LINK_OPTIONS`][cmake_interface_link_options] target properties.
    ///
    /// [cmake_interface_link_options]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_LINK_OPTIONS.html
    pub link_options: Vec<String>,
    /// The location of the target on disk.
    ///
    /// [cmake_interface_location]: https://cmake.org/cmake/help/latest/prop_tgt/LOCATION.html
    pub location: Option<String>,
}

/// Turns /usr/lib/libfoo.so.5 into foo, so that -lfoo rather than -l/usr/lib/libfoo.so.5
/// is passed to the linker. Leaves "foo" untouched.
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn link_name(lib: &str) -> &str {
    static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"lib([^/]+)\.(?:so|dylib|a).*").unwrap());
    match RE.captures(lib) {
        Some(captures) => captures.get(1).map(|f| f.as_str()).unwrap_or(lib),
        None => lib,
    }
}

#[cfg(target_os = "windows")]
fn link_name(lib: &str) -> &str {
    lib
}

#[cfg(any(target_os = "linux", target_os = "macos"))]
fn link_dir(lib: &str) -> Option<&str> {
    static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(.*)/lib[^/]+\.(?:so|dylib|a).*").unwrap());
    RE.captures(lib)?.get(1).map(|f| f.as_str())
}

#[cfg(target_os = "windows")]
fn link_dir(_lib: &str) -> Option<&str> {
    None
}

impl CMakeTarget {
    /// Instructs cargo to link the final binary against the target.
    ///
    /// This method prints the necessary [`cargo:rustc-link-search=native={}`][cargo_rustc_link_search],
    /// [`cargo:rustc-link-arg={}`][cargo_rustc_link_arg], and [`cargo:rustc-link-lib=dylib={}`][cargo_rustc_link_lib]
    /// directives to the standard output for each of the target's [`link_directories`][Self::link_directories],
    /// [`link_options`][Self::link_options], and [`link_libraries`][Self::link_libraries] respectively.
    ///
    /// [cargo_rustc_link_search]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-search
    /// [cargo_rustc_link_arg]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-arg
    /// [cargo_rustc_link_lib]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib]
    pub fn link(&self) {
        self.link_write(&mut std::io::stdout());
    }

    fn link_write<W: Write>(&self, io: &mut W) {
        self.link_directories.iter().for_each(|dir| {
            writeln!(io, "cargo:rustc-link-search=native={}", dir).unwrap();
        });
        self.link_options.iter().for_each(|opt| {
            writeln!(io, "cargo:rustc-link-arg={}", opt).unwrap();
        });
        self.link_libraries.iter().for_each(|lib| {
            if lib.starts_with("-") {
                writeln!(io, "cargo:rustc-link-arg={}", lib).unwrap();
            } else {
                let kind = if lib.ends_with(".a") { "static" } else { "dylib" };
                writeln!(io, "cargo:rustc-link-lib={}={}", kind, link_name(lib)).unwrap();
            }

            if let Some(lib) = link_dir(lib) {
                writeln!(io, "cargo:rustc-link-search=native={}", lib).unwrap();
            }
        });
    }
}

/// A builder for creating a [`CMakePackage`] instance. An instance of the builder is created by calling
/// the [`find_package()`] function. Once the package is configured, [`FindPackageBuilder::find()`] will actually
/// try to find the CMake package and return a [`CMakePackage`] instance (or error if the package is not found
/// or an error occurs during the search).
#[derive(Debug, Clone)]
pub struct FindPackageBuilder {
    name: String,
    version: Option<Version>,
    components: Option<Vec<String>>,
    names: Option<Vec<String>>,
    verbose: bool,
    prefix_paths: Option<Vec<PathBuf>>,
    defines: Vec<(String, String)>,
}

impl FindPackageBuilder {
    fn new(name: String) -> Self {
        Self {
            name,
            version: None,
            components: None,
            names: None,
            verbose: false,
            prefix_paths: None,
            defines: Vec::new(),
        }
    }

    /// Optionally specifies the minimum required version for the package to find.
    /// If the package is not found or the version is too low, the `find()` method will return
    /// [`Error::Version`] with the version of the package found on the system.
    pub fn version(self, version: impl TryInto<Version>) -> Self {
        Self {
            version: Some(
                version
                    .try_into()
                    .unwrap_or_else(|_| panic!("Invalid version specified!")),
            ),
            ..self
        }
    }

    /// Optionally specifies the required components to locate in the package.
    /// If the package is found, but any of the components is missing, the package is considered
    /// as not found and the `find()` method will return [`Error::PackageNotFound`].
    /// See the documentation on CMake's [`find_package()`][cmake_find_package] function and how it
    /// treats the `COMPONENTS` argument.
    ///
    /// [cmake_find_package]: https://cmake.org/cmake/help/latest/command/find_package.html
    pub fn components(self, components: impl Into<Vec<String>>) -> Self {
        Self {
            components: Some(components.into()),
            ..self
        }
    }

    /// Optionally specifies alternative package names to search for.
    /// See the documentation on CMake's [`find_package()`][cmake_find_package] function
    /// and how it treats the `NAMES` argument.
    ///
    /// [cmake_find_package]: https://cmake.org/cmake/help/latest/command/find_package.html
    pub fn names<S, I>(self, names: I) -> Self
    where
        S: Into<String>,
        I: IntoIterator<Item = S>,
    {
        let names: Vec<_> = names.into_iter().map(Into::into).collect();
        if names.is_empty() {
            return self;
        }

        Self {
            names: Some(names),
            ..self
        }
    }

    /// Enable verbose output.
    /// This will redirect output from actual execution of the `cmake` command to the standard output
    /// and standard error of the build script.
    pub fn verbose(self) -> Self {
        Self {
            verbose: true,
            ..self
        }
    }

    /// Add a custom CMake cache variable definition (`-DKEY=VALUE`) that will be passed
    /// to the `cmake` invocation. This is useful when the CMake package's Find-module
    /// or config-file depends on extra variables (e.g. `Boost_USE_STATIC_LIBS`).
    ///
    /// Can be called multiple times to add multiple definitions.
    pub fn define(
        mut self,
        key: impl Into<String>,
        value: impl Into<String>,
    ) -> Self {
        self.defines.push((key.into(), value.into()));
        self
    }

    // Specify prefix paths.
    // This sets directories to be searched for the package.
    // [cmake_prefix_path]: https://cmake.org/cmake/help/latest/variable/CMAKE_PREFIX_PATH.html
    pub fn prefix_paths(self, prefix_paths: Vec<PathBuf>) -> Self {
        Self {
            prefix_paths: Some(prefix_paths),
            ..self
        }
    }

    /// Tries to find the CMake package on the system.
    /// Returns a [`CMakePackage`] instance if the package is found, otherwise an error.
    pub fn find(self) -> Result<CMakePackage, cmake::Error> {
        cmake::find_package(
            self.name,
            self.version,
            self.components,
            self.names,
            self.verbose,
            self.prefix_paths,
            self.defines,
        )
    }
}

/// Find a CMake package on the system.
///
/// This function is the main entrypoint for the crate. It returns a builder object that you
/// can use to specify further constraints on the package to find, such as the [version][FindPackageBuilder::version]
/// or [components][FindPackageBuilder::components]. Once you call the [`find()`][FindPackageBuilder::find]
/// method on the builder, the crate will try to find the package on the system or return an
/// error if the package does not exist or does not satisfy some of the constraints. If the package
/// is found, an instance of the [`CMakePackage`] struct is returned that can be used to further
/// query the package for information about its individual CMake targets.
///
/// See the documentation for [`FindPackageBuilder`], [`CMakePackage`], and [`CMakeTarget`] for more
/// information and the example in the crate documentation for a simple usage example.
pub fn find_package(name: impl Into<String>) -> FindPackageBuilder {
    FindPackageBuilder::new(name.into())
}

#[cfg(test)]
mod testing {
    use super::*;

    // Note: requires cmake to be installed on the system
    #[test]
    fn test_find_package() {
        let package = find_package("totallynonexistentpackage").find();
        match package {
            Ok(_) => panic!("Package should not be found"),
            Err(cmake::Error::PackageNotFound) => (),
            Err(err) => panic!("Unexpected error: {:?}", err),
        }
    }

    // Note: requires cmake to be installed on the system
    #[test]
    fn test_find_package_with_version() {
        let package = find_package("foo").version("1.0").find();
        match package {
            Ok(_) => panic!("Package should not be found"),
            Err(cmake::Error::PackageNotFound) => (),
            Err(err) => panic!("Unexpected error: {:?}", err),
        }
    }

    #[test]
    #[cfg(any(target_os = "linux", target_os = "macos"))]
    fn test_link_to() {
        let target = CMakeTarget {
            name: "foo".into(),
            compile_definitions: vec![],
            compile_options: vec![],
            include_directories: vec![],
            link_directories: vec!["/usr/lib64".into()],
            link_libraries: vec!["/usr/lib/libbar.so".into(), "/usr/lib64/libfoo.so.5".into()],
            link_options: vec![],
            location: None,
        };

        let mut buf = Vec::new();
        target.link_write(&mut buf);
        let output = String::from_utf8(buf).unwrap();
        assert_eq!(
            output.lines().collect::<Vec<&str>>(),
            vec![
                "cargo:rustc-link-search=native=/usr/lib64",
                "cargo:rustc-link-lib=dylib=bar",
                "cargo:rustc-link-search=native=/usr/lib",
                "cargo:rustc-link-lib=dylib=foo",
                "cargo:rustc-link-search=native=/usr/lib64"
            ]
        );
    }
}