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    defines: Vec<(String, String)>,
179) -> Result<CMakePackage, Error> {
180    // Find cmake or panic
181    let cmake = find_cmake()?;
182
183    let working_directory = get_temporary_working_directory()?;
184
185    setup_cmake_project(working_directory.path())?;
186
187    let output_file = working_directory.path().join("package.json");
188    // Run the CMake - see the find_package.cmake script for docs
189    let mut command = Command::new(&cmake.path);
190    command
191        .stdout(stdio(verbose))
192        .stderr(stdio(verbose))
193        .current_dir(&working_directory)
194        .arg(".")
195        .arg(format!("-DCMAKE_BUILD_TYPE={:?}", build_type()))
196        .arg(format!("-DCMAKE_MIN_VERSION={CMAKE_MIN_VERSION}"))
197        .arg(format!("-DPACKAGE={}", name))
198        .arg(format!("-DOUTPUT_FILE={}", output_file.display()))
199        .arg(format!(
200            "-DCMAKE_PREFIX_PATH={}",
201            prefix_paths
202                .unwrap_or_default()
203                .into_iter()
204                .map(|path| path.display().to_string())
205                .join(";")
206        ))
207        .arg(format!(
208            "-DCMAKE_FIND_DEBUG_MODE={}",
209            if verbose { "TRUE" } else { "FALSE" }
210        ));
211    if let Some(version) = version {
212        command.arg(format!("-DVERSION={}", version));
213    }
214    if let Some(components) = components {
215        command.arg(format!("-DCOMPONENTS={}", components.join(";")));
216    }
217    if let Some(ref names) = names {
218        command.arg(format!("-DNAMES={}", names.join(";")));
219    }
220    for (key, value) in &defines {
221        command.arg(format!("-D{}={}", key, value));
222    }
223    command.output().map_err(Error::IO)?;
224
225    // Read from the generated JSON file
226    let reader = std::fs::File::open(output_file).map_err(Error::IO)?;
227    let package: PackageResult = serde_json::from_reader(reader).or(Err(Error::Internal))?;
228
229    let package_name = match package.name {
230        Some(name) => name,
231        None => return Err(Error::PackageNotFound),
232    };
233
234    let package_version = match package.version {
235        Some(version) => Some(version.try_into().map_err(Error::Version)?),
236        None => None, // Missing version is not an error
237    };
238
239    if let Some(version) = version {
240        if let Some(package_version) = package_version {
241            if package_version < version {
242                return Err(Error::Version(VersionError::VersionTooOld(package_version)));
243            }
244        }
245
246        // It's not an error if the package did not provide a version.
247    }
248
249    Ok(CMakePackage::new(
250        cmake,
251        working_directory,
252        package_name,
253        package_version,
254        package.components,
255        names,
256        verbose,
257    ))
258}
259
260#[derive(Clone, Debug, Deserialize)]
261#[serde(untagged)]
262enum PropertyValue {
263    String(String),
264    Target(Target),
265}
266
267impl From<PropertyValue> for Vec<String> {
268    fn from(value: PropertyValue) -> Self {
269        match value {
270            PropertyValue::String(value) => vec![value],
271            PropertyValue::Target(target) => match target.location {
272                Some(location) => vec![location],
273                None => vec![],
274            }
275            .into_iter()
276            .chain(
277                target
278                    .interface_link_libraries
279                    .unwrap_or_default()
280                    .into_iter()
281                    .flat_map(Into::<Vec<String>>::into),
282            )
283            .collect(),
284        }
285    }
286}
287
288#[derive(Debug, Default, Deserialize, Clone)]
289#[serde(default, rename_all = "UPPERCASE")]
290struct Target {
291    name: String,
292    location: Option<String>,
293    #[serde(rename = "LOCATION_Release")]
294    location_release: Option<String>,
295    #[serde(rename = "LOCATION_Debug")]
296    location_debug: Option<String>,
297    #[serde(rename = "LOCATION_RelWithDebInfo")]
298    location_relwithdebinfo: Option<String>,
299    #[serde(rename = "LOCATION_MinSizeRel")]
300    location_minsizerel: Option<String>,
301    imported_implib: Option<String>,
302    #[serde(rename = "IMPORTED_IMPLIB_Release")]
303    imported_implib_release: Option<String>,
304    #[serde(rename = "IMPORTED_IMPLIB_Debug")]
305    imported_implib_debug: Option<String>,
306    #[serde(rename = "IMPORTED_IMPLIB_RelWithDebInfo")]
307    imported_implib_relwithdebinfo: Option<String>,
308    #[serde(rename = "IMPORTED_IMPLIB_MinSizeRel")]
309    imported_implib_minsizerel: Option<String>,
310    interface_compile_definitions: Option<Vec<String>>,
311    interface_compile_options: Option<Vec<String>>,
312    interface_include_directories: Option<Vec<String>>,
313    interface_link_directories: Option<Vec<String>>,
314    interface_link_libraries: Option<Vec<PropertyValue>>,
315    interface_link_options: Option<Vec<String>>,
316}
317
318/// Collects values from `property` of the current target and from `property` of
319/// all targets linked in `interface_link_libraries` recursively.
320///
321/// This basically implements the CMake logic as described in the documentation
322/// of e.g. [`INTERFACE_COMPILE_DEFINITIONS`][cmake_interface_compile_definitions] for
323/// the target property:
324///
325/// > When target dependencies are specified using [`target_link_libraries()`][target_link_libraries],
326/// > CMake will read this property from all target dependencies to determine the build properties of
327/// > the consumer.
328///
329/// This function preserves the order of the values as they are found in the targets, the value of the
330/// immediate `target` value is first, followed by all transitive properties of each linked target.
331///
332/// [cmake_interface_compile_definitions]: https://cmake.org/cmake/help/latest/prop_tgt/INTERFACE_COMPILE_DEFINITIONS.html
333/// [target_link_libraries]: https://cmake.org/cmake/help/latest/command/target_link_libraries.html
334fn collect_from_targets<'a>(
335    target: &'a Target,
336    property: impl Fn(&Target) -> &Option<Vec<String>> + 'a + Copy,
337) -> Vec<String> {
338    property(target)
339        .as_ref()
340        .map_or(Vec::new(), Clone::clone)
341        .into_iter()
342        .chain(
343            target
344                .interface_link_libraries
345                .as_ref()
346                .map_or(Vec::new(), Clone::clone)
347                .iter()
348                .filter_map(|value| match value {
349                    PropertyValue::String(_) => None,
350                    PropertyValue::Target(target) => Some(target),
351                })
352                .flat_map(|target| collect_from_targets(target, property)),
353        )
354        .collect()
355}
356
357/// Equivalent to `collect_from_target`, but it sorts and deduplicates the properties - use with
358/// care, as the order of the properties might be important (e.g. for compile options).
359fn collect_from_targets_unique<'a>(
360    target: &'a Target,
361    property: impl Fn(&Target) -> &Option<Vec<String>> + 'a + Copy,
362) -> Vec<String> {
363    collect_from_targets(target, property)
364        .into_iter()
365        .sorted()
366        .dedup()
367        .collect()
368}
369
370fn implib_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
371    match build_type {
372        CMakeBuildType::Debug => target
373            .imported_implib_debug
374            .clone()
375            .or(target.imported_implib.clone()),
376        CMakeBuildType::Release => target
377            .imported_implib_release
378            .clone()
379            .or(target.imported_implib.clone()),
380        CMakeBuildType::RelWithDebInfo => target
381            .imported_implib_relwithdebinfo
382            .clone()
383            .or(target.imported_implib.clone()),
384        CMakeBuildType::MinSizeRel => target
385            .imported_implib_minsizerel
386            .clone()
387            .or(target.imported_implib.clone()),
388    }
389    .or_else(|| location_for_build_type(build_type, target))
390}
391
392fn location_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
393    match build_type {
394        CMakeBuildType::Debug => target.location_debug.clone().or(target.location.clone()),
395        CMakeBuildType::Release => target.location_release.clone().or(target.location.clone()),
396        CMakeBuildType::RelWithDebInfo => target
397            .location_relwithdebinfo
398            .clone()
399            .or(target.location.clone()),
400        CMakeBuildType::MinSizeRel => target
401            .location_minsizerel
402            .clone()
403            .or(target.location.clone()),
404    }
405}
406
407fn library_for_build_type(build_type: CMakeBuildType, target: &Target) -> Option<String> {
408    if cfg!(target_os = "windows") {
409        implib_for_build_type(build_type, target)
410    } else {
411        location_for_build_type(build_type, target)
412    }
413}
414
415impl Target {
416    fn into_cmake_target(self, build_type: CMakeBuildType) -> CMakeTarget {
417        CMakeTarget {
418            compile_definitions: collect_from_targets_unique(&self, |target| {
419                &target.interface_compile_definitions
420            }),
421            compile_options: collect_from_targets(&self, |target| {
422                &target.interface_compile_options
423            }),
424            include_directories: collect_from_targets_unique(&self, |target| {
425                &target.interface_include_directories
426            }),
427            link_directories: collect_from_targets_unique(&self, |target| {
428                &target.interface_link_directories
429            }),
430            link_options: collect_from_targets(&self, |target| &target.interface_link_options),
431            link_libraries: library_for_build_type(build_type, &self)
432                .as_ref()
433                .map_or(vec![], |location| vec![location.clone()])
434                .into_iter()
435                .chain(
436                    self.interface_link_libraries
437                        .as_ref()
438                        .map_or(Vec::new(), Clone::clone)
439                        .into_iter()
440                        .flat_map(Into::<Vec<String>>::into),
441                )
442                .sorted() // FIXME: should we really do this for libraries? Linking order might be important...
443                .dedup()
444                .collect(),
445            name: self.name,
446            location: self.location,
447        }
448    }
449}
450
451/// Finds the specified target in the CMake package and extracts its properties.
452/// Returns `None` if the target was not found.
453pub(crate) fn find_target(
454    package: &CMakePackage,
455    target: impl Into<String>,
456) -> Option<CMakeTarget> {
457    let target: String = target.into();
458
459    // Run the CMake script
460    let output_file = package.working_directory.path().join(format!(
461        "target_{}.json",
462        target.to_lowercase().replace(":", "_")
463    ));
464    let build_type = build_type();
465    let mut command = Command::new(&package.cmake.path);
466    command
467        .stdout(stdio(package.verbose))
468        .stderr(stdio(package.verbose))
469        .current_dir(package.working_directory.path())
470        .arg(".")
471        .arg(format!("-DTARGET={}", target))
472        .arg(format!("-DOUTPUT_FILE={}", output_file.display()));
473    command.output().ok()?;
474
475    // Read from the generated JSON file
476    let reader = std::fs::File::open(&output_file).ok()?;
477    let target: Target = serde_json::from_reader(reader)
478        .map_err(|e| {
479            eprintln!("Failed to parse target JSON: {:?}", e);
480        })
481        .ok()?;
482    if target.name.is_empty() {
483        return None;
484    }
485    Some(target.into_cmake_target(build_type))
486}
487
488#[derive(Debug, Deserialize)]
489struct TargetProperty {
490    value: Option<PropertyValue>,
491}
492
493pub(crate) fn target_property(
494    package: &CMakePackage,
495    target: &CMakeTarget,
496    property: impl Into<String>,
497) -> Option<String> {
498    let property: String = property.into();
499
500    // Run the CMake script
501    let output_file = package.working_directory.path().join(format!(
502        "target_property_{}_{}.json",
503        target.name.to_lowercase().replace(":", "_"),
504        property.to_lowercase(),
505    ));
506    let mut command = Command::new(&package.cmake.path);
507    command
508        .stdout(stdio(package.verbose))
509        .stderr(stdio(package.verbose))
510        .current_dir(package.working_directory.path())
511        .arg(".")
512        .arg(format!("-DTARGET={}", target.name))
513        .arg(format!("-DPROPERTY={}", property))
514        .arg(format!("-DOUTPUT_FILE={}", output_file.display()));
515    command.output().ok()?;
516
517    // Read from the generated JSON file
518    let reader = std::fs::File::open(&output_file).ok()?;
519    let property_result: TargetProperty = serde_json::from_reader(reader)
520        .map_err(|e| {
521            eprintln!("Failed to parse target property JSON: {:?}", e);
522        })
523        .ok()?;
524
525    match property_result.value {
526        Some(PropertyValue::String(value)) => Some(value),
527        Some(PropertyValue::Target(_)) => {
528            eprintln!("Returning PropertyValue::Target from target_property not supported");
529            None
530        }
531        None => None,
532    }
533}
534
535#[cfg(test)]
536mod testing {
537    use scopeguard::{guard, ScopeGuard};
538    use serial_test::serial;
539
540    use super::*;
541
542    #[test]
543    fn from_target() {
544        let target = Target {
545            name: "my_target".to_string(),
546            location: Some("/path/to/target.so".to_string()),
547            interface_compile_definitions: Some(vec!["DEFINE1".to_string(), "DEFINE2".to_string()]),
548            interface_compile_options: Some(vec!["-O2".to_string(), "-Wall".to_string()]),
549            interface_include_directories: Some(vec!["/path/to/include".to_string()]),
550            interface_link_directories: Some(vec!["/path/to/lib".to_string()]),
551            interface_link_options: Some(vec!["-L/path/to/lib".to_string()]),
552            interface_link_libraries: Some(vec![
553                PropertyValue::String("library1".to_string()),
554                PropertyValue::String("library2".to_string()),
555                PropertyValue::Target(Target {
556                    name: "dependency".to_string(),
557                    location: Some("/path/to/dependency.so".to_string()),
558                    interface_compile_definitions: Some(vec!["DEFINE3".to_string()]),
559                    interface_compile_options: Some(vec!["-O3".to_string()]),
560                    interface_include_directories: Some(vec![
561                        "/path/to/dependency/include".to_string()
562                    ]),
563                    interface_link_directories: Some(vec!["/path/to/dependency/lib".to_string()]),
564                    interface_link_options: Some(vec!["-L/path/to/dependency/lib".to_string()]),
565                    interface_link_libraries: Some(vec![PropertyValue::String(
566                        "dependency_library".to_string(),
567                    )]),
568                    ..Default::default()
569                }),
570            ]),
571            ..Default::default()
572        };
573
574        let cmake_target: CMakeTarget = target.into_cmake_target(CMakeBuildType::Release);
575
576        assert_eq!(cmake_target.name, "my_target");
577        assert_eq!(
578            cmake_target.compile_definitions,
579            vec!["DEFINE1", "DEFINE2", "DEFINE3"]
580        );
581        assert_eq!(cmake_target.compile_options, vec!["-O2", "-Wall", "-O3"]);
582        assert_eq!(
583            cmake_target.include_directories,
584            vec!["/path/to/dependency/include", "/path/to/include"]
585        );
586        assert_eq!(
587            cmake_target.link_directories,
588            vec!["/path/to/dependency/lib", "/path/to/lib"]
589        );
590        assert_eq!(
591            cmake_target.link_options,
592            vec!["-L/path/to/lib", "-L/path/to/dependency/lib"]
593        );
594        assert_eq!(
595            cmake_target.link_libraries,
596            vec![
597                "/path/to/dependency.so",
598                "/path/to/target.so",
599                "dependency_library",
600                "library1",
601                "library2",
602            ]
603        );
604    }
605
606    #[test]
607    fn from_debug_target() {
608        let target = Target {
609            name: "test_target".to_string(),
610            location: Some("/path/to/target.so".to_string()),
611            location_debug: Some("/path/to/libtarget_debug.so".to_string()),
612            ..Default::default()
613        };
614
615        let cmake_target = target.into_cmake_target(CMakeBuildType::Debug);
616        assert_eq!(
617            cmake_target.link_libraries,
618            vec!["/path/to/libtarget_debug.so"]
619        );
620    }
621
622    #[test]
623    fn from_json() {
624        let json = r#"
625{
626  "INTERFACE_INCLUDE_DIRECTORIES" : [ "/usr/include" ],
627  "INTERFACE_LINK_LIBRARIES" :
628  [
629    {
630      "INTERFACE_INCLUDE_DIRECTORIES" : [ "/usr/include" ],
631      "LOCATION" : "/usr/lib/libcrypto.so",
632      "NAME" : "OpenSSL::Crypto"
633    }
634  ],
635  "LOCATION" : "/usr/lib/libssl.so",
636  "NAME" : "OpenSSL::SSL"
637}
638"#;
639        let target: Target = serde_json::from_str(json).expect("Failed to parse JSON");
640        assert_eq!(target.name, "OpenSSL::SSL");
641        assert_eq!(target.location, Some("/usr/lib/libssl.so".to_string()));
642        assert_eq!(
643            target.interface_include_directories,
644            Some(vec!["/usr/include".to_string()])
645        );
646        assert!(target.interface_link_libraries.is_some());
647        assert_eq!(target.interface_link_libraries.as_ref().unwrap().len(), 1);
648        let sub_target = target
649            .interface_link_libraries
650            .as_ref()
651            .unwrap()
652            .first()
653            .unwrap();
654        match sub_target {
655            PropertyValue::Target(sub_target) => {
656                assert_eq!(sub_target.name, "OpenSSL::Crypto");
657                assert_eq!(
658                    sub_target.location,
659                    Some("/usr/lib/libcrypto.so".to_string())
660                );
661            }
662            _ => panic!("Expected PropertyValue::Target"),
663        }
664    }
665
666    fn clear_env(name: &'static str) -> ScopeGuard<(), impl FnOnce(())> {
667        let value = std::env::var(name);
668        std::env::remove_var(name);
669        guard((), move |_| {
670            if let Ok(value) = value {
671                std::env::set_var(name, value);
672            } else {
673                std::env::remove_var(name);
674            }
675        })
676    }
677
678    #[test]
679    #[serial]
680    fn test_build_type() {
681        let _profile = clear_env("PROFILE");
682        let _debug = clear_env("DEBUG");
683        let _opt_level = clear_env("OPT_LEVEL");
684
685        assert_eq!(build_type(), CMakeBuildType::Debug);
686
687        std::env::set_var("PROFILE", "release");
688        assert_eq!(build_type(), CMakeBuildType::Release);
689
690        std::env::set_var("DEBUG", "1");
691        assert_eq!(build_type(), CMakeBuildType::RelWithDebInfo);
692
693        std::env::set_var("DEBUG", "0");
694        std::env::set_var("OPT_LEVEL", "s");
695        assert_eq!(build_type(), CMakeBuildType::MinSizeRel);
696    }
697}