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