Skip to main content

cmake_package/
cmake.rs

1// SPDX-FileCopyrightText: 2024 Daniel Vrátil <dvratil@kde.org>
2//
3// SPDX-License-Identifier: MIT
4
5use crate::version::{Version, VersionError};
6use crate::{CMakePackage, CMakeTarget};
7use std::{error, fmt};
8
9use itertools::Itertools;
10use serde::Deserialize;
11use std::path::{Path, PathBuf};
12use std::process::{Command, Stdio};
13use tempfile::TempDir;
14use which::which;
15
16/// The minimum version of CMake required by this crate.
17pub const CMAKE_MIN_VERSION: &str = "3.19";
18
19/// A structure representing the CMake program found on the system.
20#[derive(Debug, Clone)]
21pub struct CMakeProgram {
22    /// Path to the `cmake` executable to be used.
23    pub path: PathBuf,
24    /// Version of the `cmake` detected. Must be at least [`CMAKE_MIN_VERSION`].
25    pub version: Version,
26}
27
28fn script_path(script: &str) -> PathBuf {
29    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
30        .join("cmake")
31        .join(script)
32}
33
34/// Errors that can occur while working with CMake.
35#[derive(Debug)]
36pub enum Error {
37    /// The `cmake` executable was not found in system `PATH` environment variable.
38    CMakeNotFound,
39    /// The available CMake version is too old (see [`CMAKE_MIN_VERSION`]).
40    UnsupportedCMakeVersion,
41    /// An internal error in the library.
42    Internal,
43    /// An I/O error while executing `cmake`
44    IO(std::io::Error),
45    /// An version-related error (e.g. the found package version is too old)
46    Version(VersionError),
47    /// The requested package was not found by CMake.
48    PackageNotFound,
49}
50
51impl fmt::Display for Error {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "CMake Error: {:?}", self)
54    }
55}
56
57impl error::Error for Error {}
58
59#[derive(Clone, Debug, Deserialize)]
60struct PackageResult {
61    name: Option<String>,
62    version: Option<String>,
63    components: Option<Vec<String>>,
64}
65
66/// Find the CMake program on the system and check version compatibility.
67///
68/// Tries to find the `cmake` executable in all paths listed in the `PATH` environment variable.
69/// If found, it also checks that the version of CMake is at least [`CMAKE_MIN_VERSION`].
70///
71/// Returns [`CMakeProgram`] on success and [`Error::CMakeNotFound`] when the `cmake` executable
72/// is not found or [`Error::UnsupportedCMakeVersion`] when the version is too low.
73pub fn find_cmake() -> Result<CMakeProgram, Error> {
74    let path = which("cmake").or(Err(Error::CMakeNotFound))?;
75
76    let working_directory = get_temporary_working_directory()?;
77    let output_file = working_directory.path().join("version_info.json");
78
79    if Command::new(&path)
80        .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
81        .arg(format!("-DOUTPUT_FILE={}", output_file.display()))
82        .arg("-P")
83        .arg(script_path("cmake_version.cmake"))
84        .status()
85        .map_err(|_| Error::Internal)?
86        .success()
87    {
88        let reader = std::fs::File::open(output_file).map_err(Error::IO)?;
89        let version: Version = serde_json::from_reader(reader).or(Err(Error::Internal))?;
90        Ok(CMakeProgram { path, version })
91    } else {
92        Err(Error::UnsupportedCMakeVersion)
93    }
94}
95
96fn get_temporary_working_directory() -> Result<TempDir, Error> {
97    #[cfg(test)]
98    let out_dir = std::env::temp_dir();
99    #[cfg(not(test))]
100    let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap_or_else(|_| {
101        panic!("OUT_DIR is not set, are you running the crate from build.rs?")
102    }));
103
104    // Make a unique directory inside
105    tempfile::Builder::new()
106        .prefix("cmake-package-rs")
107        .tempdir_in(out_dir)
108        .or(Err(Error::Internal))
109}
110
111fn setup_cmake_project(working_directory: &Path) -> Result<(), Error> {
112    std::fs::copy(
113        script_path("find_package.cmake"),
114        working_directory.join("CMakeLists.txt"),
115    )
116    .map_err(Error::IO)?;
117    Ok(())
118}
119
120fn stdio(verbose: bool) -> Stdio {
121    if verbose {
122        Stdio::inherit()
123    } else {
124        Stdio::null()
125    }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
129enum CMakeBuildType {
130    Debug,
131    Release,
132    RelWithDebInfo,
133    MinSizeRel,
134}
135
136fn build_type() -> CMakeBuildType {
137    // The PROFILE variable is set to "release" for release builds and to "debug" for any other build type.
138    // This is fairly easy to map to CMake's build types...
139    match std::env::var("PROFILE")
140        .as_ref()
141        .unwrap_or(&"debug".to_string())
142        .as_str()
143    {
144        "release" => {
145            // If the release profile is enabled, and also "s" or "z" optimimzation is set, meaning "optimize for binary size",
146            // then we want to use MinSizeRel.
147            // There's no way in CMake to combine MinSizeRel and RelWithDebInfo. Since those two options kinds contradict themselves,
148            // we make the assumption here that if the user wants to optimize for binary size, they want that more than they want
149            // debug info, so MinSizeRel is checked first.
150            let opt_level = std::env::var("OPT_LEVEL").unwrap_or("0".to_string());
151            if "sz".contains(&opt_level) {
152                return CMakeBuildType::MinSizeRel;
153            }
154
155            // If DEBUG is set to anything other than "0", "false" or "none" (meaning to include /some/ kind of debug info),
156            // then we want to use RelWithDebInfo.
157            let debug = std::env::var("DEBUG").unwrap_or("0".to_string());
158            if !["0", "false", "none"].contains(&debug.as_str()) {
159                return CMakeBuildType::RelWithDebInfo;
160            }
161
162            // For everything else, there's Mastercard...I mean Release.
163            CMakeBuildType::Release
164        }
165        // Any other profile (which really should only be "debug"), we map to Debug.
166        _ => CMakeBuildType::Debug,
167    }
168}
169
170/// Performs the actual `find_package()` operation with CMake
171pub(crate) fn find_package(
172    name: String,
173    version: Option<Version>,
174    components: Option<Vec<String>>,
175    names: Option<Vec<String>>,
176    verbose: bool,
177    prefix_paths: Option<Vec<PathBuf>>,
178) -> Result<CMakePackage, Error> {
179    // Find cmake or panic
180    let cmake = find_cmake()?;
181
182    let working_directory = get_temporary_working_directory()?;
183
184    setup_cmake_project(working_directory.path())?;
185
186    let output_file = working_directory.path().join("package.json");
187    // Run the CMake - see the find_package.cmake script for docs
188    let mut command = Command::new(&cmake.path);
189    command
190        .stdout(stdio(verbose))
191        .stderr(stdio(verbose))
192        .current_dir(&working_directory)
193        .arg(".")
194        .arg(format!("-DCMAKE_BUILD_TYPE={:?}", build_type()))
195        .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
196        .arg(format!("-DPACKAGE={}", name))
197        .arg(format!("-DOUTPUT_FILE={}", output_file.display()))
198        .arg(format!(
199            "-DCMAKE_PREFIX_PATH={}",
200            prefix_paths
201                .unwrap_or_default()
202                .into_iter()
203                .map(|path| path.display().to_string())
204                .join(";")
205        ))
206        .arg(format!(
207            "-DCMAKE_FIND_DEBUG_MODE={}",
208            if verbose { "TRUE" } else { "FALSE" }
209        ));
210    if let Some(version) = version {
211        command.arg(format!("-DVERSION={}", version));
212    }
213    if let Some(components) = components {
214        command.arg(format!("-DCOMPONENTS={}", components.join(";")));
215    }
216    if let Some(ref names) = names {
217        command.arg(format!("-DNAMES={}", names.join(";")));
218    }
219    command.output().map_err(Error::IO)?;
220
221    // Read from the generated JSON file
222    let reader = std::fs::File::open(output_file).map_err(Error::IO)?;
223    let package: PackageResult = serde_json::from_reader(reader).or(Err(Error::Internal))?;
224
225    let package_name = match package.name {
226        Some(name) => name,
227        None => return Err(Error::PackageNotFound),
228    };
229
230    let package_version = match package.version {
231        Some(version) => Some(version.try_into().map_err(Error::Version)?),
232        None => None, // Missing version is not an error
233    };
234
235    if let Some(version) = version {
236        if let Some(package_version) = package_version {
237            if package_version < version {
238                return Err(Error::Version(VersionError::VersionTooOld(package_version)));
239            }
240        }
241
242        // It's not an error if the package did not provide a version.
243    }
244
245    Ok(CMakePackage::new(
246        cmake,
247        working_directory,
248        package_name,
249        package_version,
250        package.components,
251        names,
252        verbose,
253    ))
254}
255
256#[derive(Clone, Debug, Deserialize)]
257#[serde(untagged)]
258enum PropertyValue {
259    String(String),
260    Target(Target),
261}
262
263impl From<PropertyValue> for Vec<String> {
264    fn from(value: PropertyValue) -> Self {
265        match value {
266            PropertyValue::String(value) => vec![value],
267            PropertyValue::Target(target) => match target.location {
268                Some(location) => vec![location],
269                None => vec![],
270            }
271            .into_iter()
272            .chain(
273                target
274                    .interface_link_libraries
275                    .unwrap_or_default()
276                    .into_iter()
277                    .flat_map(Into::<Vec<String>>::into),
278            )
279            .collect(),
280        }
281    }
282}
283
284#[derive(Debug, Default, Deserialize, Clone)]
285#[serde(default, rename_all = "UPPERCASE")]
286struct Target {
287    name: String,
288    location: Option<String>,
289    #[serde(rename = "LOCATION_Release")]
290    location_release: Option<String>,
291    #[serde(rename = "LOCATION_Debug")]
292    location_debug: Option<String>,
293    #[serde(rename = "LOCATION_RelWithDebInfo")]
294    location_relwithdebinfo: Option<String>,
295    #[serde(rename = "LOCATION_MinSizeRel")]
296    location_minsizerel: Option<String>,
297    imported_implib: Option<String>,
298    #[serde(rename = "IMPORTED_IMPLIB_Release")]
299    imported_implib_release: Option<String>,
300    #[serde(rename = "IMPORTED_IMPLIB_Debug")]
301    imported_implib_debug: Option<String>,
302    #[serde(rename = "IMPORTED_IMPLIB_RelWithDebInfo")]
303    imported_implib_relwithdebinfo: Option<String>,
304    #[serde(rename = "IMPORTED_IMPLIB_MinSizeRel")]
305    imported_implib_minsizerel: Option<String>,
306    interface_compile_definitions: Option<Vec<String>>,
307    interface_compile_options: Option<Vec<String>>,
308    interface_include_directories: Option<Vec<String>>,
309    interface_link_directories: Option<Vec<String>>,
310    interface_link_libraries: Option<Vec<PropertyValue>>,
311    interface_link_options: Option<Vec<String>>,
312}
313
314/// Collects values from `property` of the current target and from `property` of
315/// all targets linked in `interface_link_libraries` recursively.
316///
317/// This basically implements the CMake logic as described in the documentation
318/// of e.g. [`INTERFACE_COMPILE_DEFINITIONS`][cmake_interface_compile_definitions] for
319/// the target property:
320///
321/// > When target dependencies are specified using [`target_link_libraries()`][target_link_libraries],
322/// > CMake will read this property from all target dependencies to determine the build properties of
323/// > the consumer.
324///
325/// This function preserves the order of the values as they are found in the targets, the value of the
326/// immediate `target` value is first, followed by all transitive properties of each linked target.
327///
328/// [cmake_interface_compile_definitions]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_COMPILE_DEFINITIONS.html
329/// [target_link_libraries]: https://cmake.org/cmake/help/latest/command/target_link_libraries.html
330fn collect_from_targets<'a>(
331    target: &'a Target,
332    property: impl Fn(&Target) -> &Option<Vec<String>> + 'a + Copy,
333) -> Vec<String> {
334    property(target)
335        .as_ref()
336        .map_or(Vec::new(), Clone::clone)
337        .into_iter()
338        .chain(
339            target
340                .interface_link_libraries
341                .as_ref()
342                .map_or(Vec::new(), Clone::clone)
343                .iter()
344                .filter_map(|value| match value {
345                    PropertyValue::String(_) => None,
346                    PropertyValue::Target(target) => Some(target),
347                })
348                .flat_map(|target| collect_from_targets(target, property)),
349        )
350        .collect()
351}
352
353/// Equivalent to `collect_from_target`, but it sorts and deduplicates the properties - use with
354/// care, as the order of the properties might be important (e.g. for compile options).
355fn collect_from_targets_unique<'a>(
356    target: &'a Target,
357    property: impl Fn(&Target) -> &Option<Vec<String>> + 'a + Copy,
358) -> Vec<String> {
359    collect_from_targets(target, property)
360        .into_iter()
361        .sorted()
362        .dedup()
363        .collect()
364}
365
366fn implib_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
367    match build_type {
368        CMakeBuildType::Debug => target
369            .imported_implib_debug
370            .clone()
371            .or(target.imported_implib.clone()),
372        CMakeBuildType::Release => target
373            .imported_implib_release
374            .clone()
375            .or(target.imported_implib.clone()),
376        CMakeBuildType::RelWithDebInfo => target
377            .imported_implib_relwithdebinfo
378            .clone()
379            .or(target.imported_implib.clone()),
380        CMakeBuildType::MinSizeRel => target
381            .imported_implib_minsizerel
382            .clone()
383            .or(target.imported_implib.clone()),
384    }
385    .or_else(|| location_for_build_type(build_type, target))
386}
387
388fn location_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
389    match build_type {
390        CMakeBuildType::Debug => target.location_debug.clone().or(target.location.clone()),
391        CMakeBuildType::Release => target.location_release.clone().or(target.location.clone()),
392        CMakeBuildType::RelWithDebInfo => target
393            .location_relwithdebinfo
394            .clone()
395            .or(target.location.clone()),
396        CMakeBuildType::MinSizeRel => target
397            .location_minsizerel
398            .clone()
399            .or(target.location.clone()),
400    }
401}
402
403fn library_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
404    if cfg!(target_os = "windows") {
405        implib_for_build_type(build_type, target)
406    } else {
407        location_for_build_type(build_type, target)
408    }
409}
410
411impl Target {
412    fn into_cmake_target(self, build_type: CMakeBuildType) -> CMakeTarget {
413        CMakeTarget {
414            compile_definitions: collect_from_targets_unique(&self, |target| {
415                &target.interface_compile_definitions
416            }),
417            compile_options: collect_from_targets(&self, |target| {
418                &target.interface_compile_options
419            }),
420            include_directories: collect_from_targets_unique(&self, |target| {
421                &target.interface_include_directories
422            }),
423            link_directories: collect_from_targets_unique(&self, |target| {
424                &target.interface_link_directories
425            }),
426            link_options: collect_from_targets(&self, |target| &target.interface_link_options),
427            link_libraries: library_for_build_type(build_type, &self)
428                .as_ref()
429                .map_or(vec![], |location| vec![location.clone()])
430                .into_iter()
431                .chain(
432                    self.interface_link_libraries
433                        .as_ref()
434                        .map_or(Vec::new(), Clone::clone)
435                        .into_iter()
436                        .flat_map(Into::<Vec<String>>::into),
437                )
438                .sorted() // FIXME: should we really do this for libraries? Linking order might be important...
439                .dedup()
440                .collect(),
441            name: self.name,
442            location: self.location,
443        }
444    }
445}
446
447/// Finds the specified target in the CMake package and extracts its properties.
448/// Returns `None` if the target was not found.
449pub(crate) fn find_target(
450    package: &CMakePackage,
451    target: impl Into<String>,
452) -> Option<CMakeTarget> {
453    let target: String = target.into();
454
455    // Run the CMake script
456    let output_file = package.working_directory.path().join(format!(
457        "target_{}.json",
458        target.to_lowercase().replace(":", "_")
459    ));
460    let build_type = build_type();
461    let mut command = Command::new(&package.cmake.path);
462    command
463        .stdout(stdio(package.verbose))
464        .stderr(stdio(package.verbose))
465        .current_dir(package.working_directory.path())
466        .arg(".")
467        .arg(format!("-DCMAKE_BUILD_TYPE={:?}", build_type))
468        .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
469        .arg(format!("-DPACKAGE={}", package.name))
470        .arg(format!("-DTARGET={}", target))
471        .arg(format!("-DOUTPUT_FILE={}", output_file.display()));
472    if let Some(version) = package.version {
473        command.arg(format!("-DVERSION={}", version));
474    }
475    if let Some(components) = &package.components {
476        command.arg(format!("-DCOMPONENTS={}", components.join(";")));
477    }
478    if let Some(names) = &package.names {
479        command.arg(format!("-DNAMES={}", names.join(";")));
480    }
481    command.output().ok()?;
482
483    // Read from the generated JSON file
484    let reader = std::fs::File::open(&output_file).ok()?;
485    let target: Target = serde_json::from_reader(reader)
486        .map_err(|e| {
487            eprintln!("Failed to parse target JSON: {:?}", e);
488        })
489        .ok()?;
490    if target.name.is_empty() {
491        return None;
492    }
493    Some(target.into_cmake_target(build_type))
494}
495
496#[derive(Debug, Deserialize)]
497struct TargetProperty {
498    value: Option<PropertyValue>,
499}
500
501pub(crate) fn target_property(
502    package: &CMakePackage,
503    target: &CMakeTarget,
504    property: impl Into<String>,
505) -> Option<String> {
506    let property: String = property.into();
507
508    // Run the CMake script
509    let output_file = package.working_directory.path().join(format!(
510        "target_property_{}_{}.json",
511        target.name.to_lowercase().replace(":", "_"),
512        property.to_lowercase(),
513    ));
514    let build_type = build_type();
515    let mut command = Command::new(&package.cmake.path);
516    command
517        .stdout(stdio(package.verbose))
518        .stderr(stdio(package.verbose))
519        .current_dir(package.working_directory.path())
520        .arg(".")
521        .arg(format!("-DCMAKE_BUILD_TYPE={:?}", build_type))
522        .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
523        .arg(format!("-DPACKAGE={}", package.name))
524        .arg(format!("-DTARGET={}", target.name))
525        .arg(format!("-DPROPERTY={}", property))
526        .arg(format!("-DOUTPUT_FILE={}", output_file.display()));
527    if let Some(version) = package.version {
528        command.arg(format!("-DVERSION={}", version));
529    }
530    if let Some(components) = &package.components {
531        command.arg(format!("-DCOMPONENTS={}", components.join(";")));
532    }
533    if let Some(names) = &package.names {
534        command.arg(format!("-DNAMES={}", names.join(";")));
535    }
536    command.output().ok()?;
537
538    // Read from the generated JSON file
539    let reader = std::fs::File::open(&output_file).ok()?;
540    let property_result: TargetProperty = serde_json::from_reader(reader)
541        .map_err(|e| {
542            eprintln!("Failed to parse target property JSON: {:?}", e);
543        })
544        .ok()?;
545
546    match property_result.value {
547        Some(PropertyValue::String(value)) => Some(value),
548        Some(PropertyValue::Target(_)) => {
549            eprintln!("Returning PropertyValue::Target from target_property not supported");
550            None
551        }
552        None => None,
553    }
554}
555
556#[cfg(test)]
557mod testing {
558    use scopeguard::{guard, ScopeGuard};
559    use serial_test::serial;
560
561    use super::*;
562
563    #[test]
564    fn from_target() {
565        let target = Target {
566            name: "my_target".to_string(),
567            location: Some("/path/to/target.so".to_string()),
568            interface_compile_definitions: Some(vec!["DEFINE1".to_string(), "DEFINE2".to_string()]),
569            interface_compile_options: Some(vec!["-O2".to_string(), "-Wall".to_string()]),
570            interface_include_directories: Some(vec!["/path/to/include".to_string()]),
571            interface_link_directories: Some(vec!["/path/to/lib".to_string()]),
572            interface_link_options: Some(vec!["-L/path/to/lib".to_string()]),
573            interface_link_libraries: Some(vec![
574                PropertyValue::String("library1".to_string()),
575                PropertyValue::String("library2".to_string()),
576                PropertyValue::Target(Target {
577                    name: "dependency".to_string(),
578                    location: Some("/path/to/dependency.so".to_string()),
579                    interface_compile_definitions: Some(vec!["DEFINE3".to_string()]),
580                    interface_compile_options: Some(vec!["-O3".to_string()]),
581                    interface_include_directories: Some(vec![
582                        "/path/to/dependency/include".to_string()
583                    ]),
584                    interface_link_directories: Some(vec!["/path/to/dependency/lib".to_string()]),
585                    interface_link_options: Some(vec!["-L/path/to/dependency/lib".to_string()]),
586                    interface_link_libraries: Some(vec![PropertyValue::String(
587                        "dependency_library".to_string(),
588                    )]),
589                    ..Default::default()
590                }),
591            ]),
592            ..Default::default()
593        };
594
595        let cmake_target: CMakeTarget = target.into_cmake_target(CMakeBuildType::Release);
596
597        assert_eq!(cmake_target.name, "my_target");
598        assert_eq!(
599            cmake_target.compile_definitions,
600            vec!["DEFINE1", "DEFINE2", "DEFINE3"]
601        );
602        assert_eq!(cmake_target.compile_options, vec!["-O2", "-Wall", "-O3"]);
603        assert_eq!(
604            cmake_target.include_directories,
605            vec!["/path/to/dependency/include", "/path/to/include"]
606        );
607        assert_eq!(
608            cmake_target.link_directories,
609            vec!["/path/to/dependency/lib", "/path/to/lib"]
610        );
611        assert_eq!(
612            cmake_target.link_options,
613            vec!["-L/path/to/lib", "-L/path/to/dependency/lib"]
614        );
615        assert_eq!(
616            cmake_target.link_libraries,
617            vec![
618                "/path/to/dependency.so",
619                "/path/to/target.so",
620                "dependency_library",
621                "library1",
622                "library2",
623            ]
624        );
625    }
626
627    #[test]
628    fn from_debug_target() {
629        let target = Target {
630            name: "test_target".to_string(),
631            location: Some("/path/to/target.so".to_string()),
632            location_debug: Some("/path/to/libtarget_debug.so".to_string()),
633            ..Default::default()
634        };
635
636        let cmake_target = target.into_cmake_target(CMakeBuildType::Debug);
637        assert_eq!(
638            cmake_target.link_libraries,
639            vec!["/path/to/libtarget_debug.so"]
640        );
641    }
642
643    #[test]
644    fn from_json() {
645        let json = r#"
646{
647  "INTERFACE_INCLUDE_DIRECTORIES" : [ "/usr/include" ],
648  "INTERFACE_LINK_LIBRARIES" :
649  [
650    {
651      "INTERFACE_INCLUDE_DIRECTORIES" : [ "/usr/include" ],
652      "LOCATION" : "/usr/lib/libcrypto.so",
653      "NAME" : "OpenSSL::Crypto"
654    }
655  ],
656  "LOCATION" : "/usr/lib/libssl.so",
657  "NAME" : "OpenSSL::SSL"
658}
659"#;
660        let target: Target = serde_json::from_str(json).expect("Failed to parse JSON");
661        assert_eq!(target.name, "OpenSSL::SSL");
662        assert_eq!(target.location, Some("/usr/lib/libssl.so".to_string()));
663        assert_eq!(
664            target.interface_include_directories,
665            Some(vec!["/usr/include".to_string()])
666        );
667        assert!(target.interface_link_libraries.is_some());
668        assert_eq!(target.interface_link_libraries.as_ref().unwrap().len(), 1);
669        let sub_target = target
670            .interface_link_libraries
671            .as_ref()
672            .unwrap()
673            .first()
674            .unwrap();
675        match sub_target {
676            PropertyValue::Target(sub_target) => {
677                assert_eq!(sub_target.name, "OpenSSL::Crypto");
678                assert_eq!(
679                    sub_target.location,
680                    Some("/usr/lib/libcrypto.so".to_string())
681                );
682            }
683            _ => panic!("Expected PropertyValue::Target"),
684        }
685    }
686
687    fn clear_env(name: &'static str) -> ScopeGuard<(), impl FnOnce(())> {
688        let value = std::env::var(name);
689        std::env::remove_var(name);
690        guard((), move |_| {
691            if let Ok(value) = value {
692                std::env::set_var(name, value);
693            } else {
694                std::env::remove_var(name);
695            }
696        })
697    }
698
699    #[test]
700    #[serial]
701    fn test_build_type() {
702        let _profile = clear_env("PROFILE");
703        let _debug = clear_env("DEBUG");
704        let _opt_level = clear_env("OPT_LEVEL");
705
706        assert_eq!(build_type(), CMakeBuildType::Debug);
707
708        std::env::set_var("PROFILE", "release");
709        assert_eq!(build_type(), CMakeBuildType::Release);
710
711        std::env::set_var("DEBUG", "1");
712        assert_eq!(build_type(), CMakeBuildType::RelWithDebInfo);
713
714        std::env::set_var("DEBUG", "0");
715        std::env::set_var("OPT_LEVEL", "s");
716        assert_eq!(build_type(), CMakeBuildType::MinSizeRel);
717    }
718}