cmake/
lib.rs

1//! A build dependency for running `cmake` to build a native library
2//!
3//! This crate provides some necessary boilerplate and shim support for running
4//! the system `cmake` command to build a native library. It will add
5//! appropriate cflags for building code to link into Rust, handle cross
6//! compilation, and use the necessary generator for the platform being
7//! targeted.
8//!
9//! The builder-style configuration allows for various variables and such to be
10//! passed down into the build as well.
11//!
12//! ## Installation
13//!
14//! Add this to your `Cargo.toml`:
15//!
16//! ```toml
17//! [build-dependencies]
18//! cmake = "0.1"
19//! ```
20//!
21//! ## Examples
22//!
23//! ```no_run
24//! use cmake;
25//!
26//! // Builds the project in the directory located in `libfoo`, installing it
27//! // into $OUT_DIR
28//! let dst = cmake::build("libfoo");
29//!
30//! println!("cargo:rustc-link-search=native={}", dst.display());
31//! println!("cargo:rustc-link-lib=static=foo");
32//! ```
33//!
34//! ```no_run
35//! use cmake::Config;
36//!
37//! let dst = Config::new("libfoo")
38//!                  .define("FOO", "BAR")
39//!                  .cflag("-foo")
40//!                  .build();
41//! println!("cargo:rustc-link-search=native={}", dst.display());
42//! println!("cargo:rustc-link-lib=static=foo");
43//! ```
44
45#![deny(missing_docs)]
46
47extern crate cc;
48
49use std::collections::HashMap;
50use std::env;
51use std::ffi::{OsStr, OsString};
52use std::fs::{self, File};
53use std::io::prelude::*;
54use std::io::ErrorKind;
55use std::path::{Path, PathBuf};
56use std::process::Command;
57
58/// Builder style configuration for a pending CMake build.
59pub struct Config {
60    path: PathBuf,
61    generator: Option<OsString>,
62    generator_toolset: Option<OsString>,
63    cflags: OsString,
64    cxxflags: OsString,
65    asmflags: OsString,
66    defines: Vec<(OsString, OsString)>,
67    deps: Vec<String>,
68    target: Option<String>,
69    host: Option<String>,
70    out_dir: Option<PathBuf>,
71    profile: Option<String>,
72    configure_args: Vec<OsString>,
73    build_args: Vec<OsString>,
74    cmake_target: Option<String>,
75    env: Vec<(OsString, OsString)>,
76    static_crt: Option<bool>,
77    uses_cxx11: bool,
78    always_configure: bool,
79    no_build_target: bool,
80    no_default_flags: bool,
81    verbose_cmake: bool,
82    verbose_make: bool,
83    pic: Option<bool>,
84    c_cfg: Option<cc::Build>,
85    cxx_cfg: Option<cc::Build>,
86    env_cache: HashMap<String, Option<OsString>>,
87}
88
89/// Builds the native library rooted at `path` with the default cmake options.
90/// This will return the directory in which the library was installed.
91///
92/// # Examples
93///
94/// ```no_run
95/// use cmake;
96///
97/// // Builds the project in the directory located in `libfoo`, installing it
98/// // into $OUT_DIR
99/// let dst = cmake::build("libfoo");
100///
101/// println!("cargo:rustc-link-search=native={}", dst.display());
102/// println!("cargo:rustc-link-lib=static=foo");
103/// ```
104///
105pub fn build<P: AsRef<Path>>(path: P) -> PathBuf {
106    Config::new(path.as_ref()).build()
107}
108
109impl Config {
110    /// Return explicitly set profile or infer `CMAKE_BUILD_TYPE` from Rust's compilation profile.
111    ///
112    /// * if `opt-level=0` then `CMAKE_BUILD_TYPE=Debug`,
113    /// * if `opt-level={1,2,3}` and:
114    ///   * `debug=false` then `CMAKE_BUILD_TYPE=Release`
115    ///   * otherwise `CMAKE_BUILD_TYPE=RelWithDebInfo`
116    /// * if `opt-level={s,z}` then `CMAKE_BUILD_TYPE=MinSizeRel`
117    pub fn get_profile(&self) -> &str {
118        if let Some(profile) = self.profile.as_ref() {
119            profile
120        } else {
121            // Determine Rust's profile, optimization level, and debug info:
122            #[derive(PartialEq)]
123            enum RustProfile {
124                Debug,
125                Release,
126            }
127            #[derive(PartialEq, Debug)]
128            enum OptLevel {
129                Debug,
130                Release,
131                Size,
132            }
133
134            let rust_profile = match &getenv_unwrap("PROFILE")[..] {
135                "debug" => RustProfile::Debug,
136                "release" | "bench" => RustProfile::Release,
137                unknown => {
138                    eprintln!(
139                        "Warning: unknown Rust profile={}; defaulting to a release build.",
140                        unknown
141                    );
142                    RustProfile::Release
143                }
144            };
145
146            let opt_level = match &getenv_unwrap("OPT_LEVEL")[..] {
147                "0" => OptLevel::Debug,
148                "1" | "2" | "3" => OptLevel::Release,
149                "s" | "z" => OptLevel::Size,
150                unknown => {
151                    let default_opt_level = match rust_profile {
152                        RustProfile::Debug => OptLevel::Debug,
153                        RustProfile::Release => OptLevel::Release,
154                    };
155                    eprintln!(
156                        "Warning: unknown opt-level={}; defaulting to a {:?} build.",
157                        unknown, default_opt_level
158                    );
159                    default_opt_level
160                }
161            };
162
163            let debug_info: bool = match &getenv_unwrap("DEBUG")[..] {
164                "false" => false,
165                "true" => true,
166                unknown => {
167                    eprintln!("Warning: unknown debug={}; defaulting to `true`.", unknown);
168                    true
169                }
170            };
171
172            match (opt_level, debug_info) {
173                (OptLevel::Debug, _) => "Debug",
174                (OptLevel::Release, false) => "Release",
175                (OptLevel::Release, true) => "RelWithDebInfo",
176                (OptLevel::Size, _) => "MinSizeRel",
177            }
178        }
179    }
180
181    /// Creates a new blank set of configuration to build the project specified
182    /// at the path `path`.
183    pub fn new<P: AsRef<Path>>(path: P) -> Config {
184        Config {
185            path: env::current_dir().unwrap().join(path),
186            generator: None,
187            generator_toolset: None,
188            no_default_flags: false,
189            cflags: OsString::new(),
190            cxxflags: OsString::new(),
191            asmflags: OsString::new(),
192            defines: Vec::new(),
193            deps: Vec::new(),
194            profile: None,
195            out_dir: None,
196            target: None,
197            host: None,
198            configure_args: Vec::new(),
199            build_args: Vec::new(),
200            cmake_target: None,
201            env: Vec::new(),
202            static_crt: None,
203            uses_cxx11: false,
204            always_configure: true,
205            no_build_target: false,
206            verbose_cmake: false,
207            verbose_make: false,
208            pic: None,
209            c_cfg: None,
210            cxx_cfg: None,
211            env_cache: HashMap::new(),
212        }
213    }
214
215    /// Sets flag for PIC. Otherwise use cc::Build platform default
216    pub fn pic(&mut self, explicit_flag: bool) -> &mut Config {
217        self.pic = Some(explicit_flag);
218        self
219    }
220
221    /// Sets the build-tool generator (`-G`) for this compilation.
222    ///
223    /// If unset, this crate will use the `CMAKE_GENERATOR` environment variable
224    /// if set. Otherwise, it will guess the best generator to use based on the
225    /// build target.
226    pub fn generator<T: AsRef<OsStr>>(&mut self, generator: T) -> &mut Config {
227        self.generator = Some(generator.as_ref().to_owned());
228        self
229    }
230
231    /// Sets the toolset name (-T) if supported by generator.
232    /// Can be used to compile with Clang/LLVM instead of msvc when Visual Studio generator is selected.
233    ///
234    /// If unset, will use the default toolset of the selected generator.
235    pub fn generator_toolset<T: AsRef<OsStr>>(&mut self, toolset_name: T) -> &mut Config {
236        self.generator_toolset = Some(toolset_name.as_ref().to_owned());
237        self
238    }
239
240    /// Adds a custom flag to pass down to the C compiler, supplementing those
241    /// that this library already passes.
242    pub fn cflag<P: AsRef<OsStr>>(&mut self, flag: P) -> &mut Config {
243        self.cflags.push(" ");
244        self.cflags.push(flag.as_ref());
245        self
246    }
247
248    /// Adds a custom flag to pass down to the C++ compiler, supplementing those
249    /// that this library already passes.
250    pub fn cxxflag<P: AsRef<OsStr>>(&mut self, flag: P) -> &mut Config {
251        self.cxxflags.push(" ");
252        self.cxxflags.push(flag.as_ref());
253        self
254    }
255
256    /// Adds a custom flag to pass down to the ASM compiler, supplementing those
257    /// that this library already passes.
258    pub fn asmflag<P: AsRef<OsStr>>(&mut self, flag: P) -> &mut Config {
259        self.asmflags.push(" ");
260        self.asmflags.push(flag.as_ref());
261        self
262    }
263
264    /// Adds a new `-D` flag to pass to cmake during the generation step.
265    pub fn define<K, V>(&mut self, k: K, v: V) -> &mut Config
266    where
267        K: AsRef<OsStr>,
268        V: AsRef<OsStr>,
269    {
270        self.defines
271            .push((k.as_ref().to_owned(), v.as_ref().to_owned()));
272        self
273    }
274
275    /// Registers a dependency for this compilation on the native library built
276    /// by Cargo previously.
277    ///
278    /// This registration will update the `CMAKE_PREFIX_PATH` environment
279    /// variable for the [`build`][Self::build] system generation step.  The
280    /// path will be updated to include the content of the environment
281    /// variable `DEP_XXX_ROOT`, where `XXX` is replaced with the uppercased
282    /// value of `dep` (if that variable exists).
283    pub fn register_dep(&mut self, dep: &str) -> &mut Config {
284        self.deps.push(dep.to_string());
285        self
286    }
287
288    /// Sets the target triple for this compilation.
289    ///
290    /// This is automatically scraped from `$TARGET` which is set for Cargo
291    /// build scripts so it's not necessary to call this from a build script.
292    pub fn target(&mut self, target: &str) -> &mut Config {
293        self.target = Some(target.to_string());
294        self
295    }
296
297    /// Disables the cmake target option for this compilation.
298    ///
299    /// Note that this isn't related to the target triple passed to the compiler!
300    pub fn no_build_target(&mut self, no_build_target: bool) -> &mut Config {
301        self.no_build_target = no_build_target;
302        self
303    }
304
305    /// Disables the generation of default compiler flags. The default compiler
306    /// flags may cause conflicts in some cross compiling scenarios.
307    pub fn no_default_flags(&mut self, no_default_flags: bool) -> &mut Config {
308        self.no_default_flags = no_default_flags;
309        self
310    }
311
312    /// Sets the host triple for this compilation.
313    ///
314    /// This is automatically scraped from `$HOST` which is set for Cargo
315    /// build scripts so it's not necessary to call this from a build script.
316    pub fn host(&mut self, host: &str) -> &mut Config {
317        self.host = Some(host.to_string());
318        self
319    }
320
321    /// Sets the output directory for this compilation.
322    ///
323    /// This is automatically scraped from `$OUT_DIR` which is set for Cargo
324    /// build scripts so it's not necessary to call this from a build script.
325    pub fn out_dir<P: AsRef<Path>>(&mut self, out: P) -> &mut Config {
326        self.out_dir = Some(out.as_ref().to_path_buf());
327        self
328    }
329
330    /// Sets the `CMAKE_BUILD_TYPE=build_type` variable.
331    ///
332    /// By default, this value is automatically inferred from Rust's compilation
333    /// profile as follows:
334    ///
335    /// * if `opt-level=0` then `CMAKE_BUILD_TYPE=Debug`,
336    /// * if `opt-level={1,2,3}` and:
337    ///   * `debug=false` then `CMAKE_BUILD_TYPE=Release`
338    ///   * otherwise `CMAKE_BUILD_TYPE=RelWithDebInfo`
339    /// * if `opt-level={s,z}` then `CMAKE_BUILD_TYPE=MinSizeRel`
340    pub fn profile(&mut self, profile: &str) -> &mut Config {
341        self.profile = Some(profile.to_string());
342        self
343    }
344
345    /// Configures whether the /MT flag or the /MD flag will be passed to msvc build tools.
346    ///
347    /// This option defaults to `false`, and affect only msvc targets.
348    pub fn static_crt(&mut self, static_crt: bool) -> &mut Config {
349        self.static_crt = Some(static_crt);
350        self
351    }
352
353    /// Add an argument to the `cmake` configure step
354    pub fn configure_arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Config {
355        self.configure_args.push(arg.as_ref().to_owned());
356        self
357    }
358
359    /// Add an argument to the final `cmake` build step
360    pub fn build_arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Config {
361        self.build_args.push(arg.as_ref().to_owned());
362        self
363    }
364
365    /// Configure an environment variable for the `cmake` processes spawned by
366    /// this crate in the `build` step.
367    pub fn env<K, V>(&mut self, key: K, value: V) -> &mut Config
368    where
369        K: AsRef<OsStr>,
370        V: AsRef<OsStr>,
371    {
372        self.env
373            .push((key.as_ref().to_owned(), value.as_ref().to_owned()));
374        self
375    }
376
377    /// Sets the build target for the final `cmake` build step, this will
378    /// default to "install" if not specified.
379    pub fn build_target(&mut self, target: &str) -> &mut Config {
380        self.cmake_target = Some(target.to_string());
381        self
382    }
383
384    /// Alters the default target triple on OSX to ensure that c++11 is
385    /// available. Does not change the target triple if it is explicitly
386    /// specified.
387    ///
388    /// This does not otherwise affect any CXX flags, i.e. it does not set
389    /// -std=c++11 or -stdlib=libc++.
390    #[deprecated = "no longer does anything, C++ is determined based on `cc::Build`, and the macOS issue has been fixed upstream"]
391    pub fn uses_cxx11(&mut self) -> &mut Config {
392        self.uses_cxx11 = true;
393        self
394    }
395
396    /// Forces CMake to always run before building the custom target.
397    ///
398    /// In some cases, when you have a big project, you can disable
399    /// subsequents runs of cmake to make `cargo build` faster.
400    pub fn always_configure(&mut self, always_configure: bool) -> &mut Config {
401        self.always_configure = always_configure;
402        self
403    }
404
405    /// Sets very verbose output.
406    pub fn very_verbose(&mut self, value: bool) -> &mut Config {
407        self.verbose_cmake = value;
408        self.verbose_make = value;
409        self
410    }
411
412    // Simple heuristic to determine if we're cross-compiling using the Android
413    // NDK toolchain file.
414    fn uses_android_ndk(&self) -> bool {
415        // `ANDROID_ABI` is the only required flag:
416        // https://developer.android.com/ndk/guides/cmake#android_abi
417        self.defined("ANDROID_ABI")
418            && self.defines.iter().any(|(flag, value)| {
419                flag == "CMAKE_TOOLCHAIN_FILE"
420                    && Path::new(value).file_name() == Some("android.toolchain.cmake".as_ref())
421            })
422    }
423
424    /// Initializes the C build configuration.
425    pub fn init_c_cfg(&mut self, c_cfg: cc::Build) -> &mut Config {
426        self.c_cfg = Some(c_cfg);
427        self
428    }
429
430    /// Initializes the C++ build configuration.
431    pub fn init_cxx_cfg(&mut self, cxx_cfg: cc::Build) -> &mut Config {
432        self.cxx_cfg = Some(cxx_cfg);
433        self
434    }
435
436    /// Run this configuration, compiling the library with all the configured
437    /// options.
438    ///
439    /// This will run both the build system generator command as well as the
440    /// command to build the library.
441    pub fn build(&mut self) -> PathBuf {
442        let target = match self.target.clone() {
443            Some(t) => t,
444            None => getenv_unwrap("TARGET"),
445        };
446        let host = self.host.clone().unwrap_or_else(|| getenv_unwrap("HOST"));
447
448        // Some decisions later on are made if CMAKE_TOOLCHAIN_FILE is defined,
449        // so we need to read it from the environment variables from the beginning.
450        if !self.defined("CMAKE_TOOLCHAIN_FILE") {
451            if let Some(s) = self.getenv_target_os("CMAKE_TOOLCHAIN_FILE") {
452                self.define("CMAKE_TOOLCHAIN_FILE", s);
453            } else if target.contains("redox") {
454                if !self.defined("CMAKE_SYSTEM_NAME") {
455                    self.define("CMAKE_SYSTEM_NAME", "Generic");
456                }
457            } else if target != host && !self.defined("CMAKE_SYSTEM_NAME") {
458                // Set CMAKE_SYSTEM_NAME and CMAKE_SYSTEM_PROCESSOR when cross compiling
459                let os = getenv_unwrap("CARGO_CFG_TARGET_OS");
460                let arch = getenv_unwrap("CARGO_CFG_TARGET_ARCH");
461                // CMAKE_SYSTEM_NAME list
462                // https://gitlab.kitware.com/cmake/cmake/-/issues/21489#note_1077167
463                //
464                // CMAKE_SYSTEM_PROCESSOR
465                // some of the values come from https://en.wikipedia.org/wiki/Uname
466                let (system_name, system_processor) = match (os.as_str(), arch.as_str()) {
467                    ("android", "arm") => ("Android", "armv7-a"),
468                    ("android", "x86") => ("Android", "i686"),
469                    ("android", arch) => ("Android", arch),
470                    ("dragonfly", arch) => ("DragonFly", arch),
471                    ("macos", "aarch64") => ("Darwin", "arm64"),
472                    ("macos", arch) => ("Darwin", arch),
473                    ("freebsd", "x86_64") => ("FreeBSD", "amd64"),
474                    ("freebsd", arch) => ("FreeBSD", arch),
475                    ("fuchsia", arch) => ("Fuchsia", arch),
476                    ("haiku", arch) => ("Haiku", arch),
477                    ("ios", "aarch64") => ("iOS", "arm64"),
478                    ("ios", arch) => ("iOS", arch),
479                    ("linux", arch) => {
480                        let name = "Linux";
481                        match arch {
482                            "powerpc" => (name, "ppc"),
483                            "powerpc64" => (name, "ppc64"),
484                            "powerpc64le" => (name, "ppc64le"),
485                            _ => (name, arch),
486                        }
487                    }
488                    ("netbsd", arch) => ("NetBSD", arch),
489                    ("openbsd", "x86_64") => ("OpenBSD", "amd64"),
490                    ("openbsd", arch) => ("OpenBSD", arch),
491                    ("solaris", arch) => ("SunOS", arch),
492                    ("tvos", "aarch64") => ("tvOS", "arm64"),
493                    ("tvos", arch) => ("tvOS", arch),
494                    ("visionos", "aarch64") => ("visionOS", "arm64"),
495                    ("visionos", arch) => ("visionOS", arch),
496                    ("watchos", "aarch64") => ("watchOS", "arm64"),
497                    ("watchos", arch) => ("watchOS", arch),
498                    ("windows", "x86_64") => ("Windows", "AMD64"),
499                    ("windows", "x86") => ("Windows", "X86"),
500                    ("windows", "aarch64") => ("Windows", "ARM64"),
501                    ("none", arch) => ("Generic", arch),
502                    // Others
503                    (os, arch) => (os, arch),
504                };
505                self.define("CMAKE_SYSTEM_NAME", system_name);
506                self.define("CMAKE_SYSTEM_PROCESSOR", system_processor);
507            }
508        }
509
510        let generator = self
511            .generator
512            .clone()
513            .or_else(|| self.getenv_target_os("CMAKE_GENERATOR"));
514
515        let msvc = target.contains("msvc");
516        let ndk = self.uses_android_ndk();
517        let mut c_cfg = self.c_cfg.clone().unwrap_or_default();
518        c_cfg
519            .cargo_metadata(false)
520            .cpp(false)
521            .opt_level(0)
522            .debug(false)
523            .warnings(false)
524            .host(&host)
525            .no_default_flags(ndk || self.no_default_flags);
526        if !ndk {
527            c_cfg.target(&target);
528        }
529        let mut cxx_cfg = self.cxx_cfg.clone().unwrap_or_default();
530        cxx_cfg
531            .cargo_metadata(false)
532            .cpp(true)
533            .opt_level(0)
534            .debug(false)
535            .warnings(false)
536            .host(&host)
537            .no_default_flags(ndk || self.no_default_flags);
538        if !ndk {
539            cxx_cfg.target(&target);
540        }
541        if let Some(static_crt) = self.static_crt {
542            c_cfg.static_crt(static_crt);
543            cxx_cfg.static_crt(static_crt);
544        }
545        if let Some(explicit_flag) = self.pic {
546            c_cfg.pic(explicit_flag);
547            cxx_cfg.pic(explicit_flag);
548        }
549        let c_compiler = c_cfg.get_compiler();
550        let cxx_compiler = cxx_cfg.get_compiler();
551        let asm_compiler = c_cfg.get_compiler();
552
553        let dst = self
554            .out_dir
555            .clone()
556            .unwrap_or_else(|| PathBuf::from(getenv_unwrap("OUT_DIR")));
557        let build = dst.join("build");
558        self.maybe_clear(&build);
559        let _ = fs::create_dir_all(&build);
560
561        // Add all our dependencies to our cmake paths
562        let mut cmake_prefix_path = Vec::new();
563        for dep in &self.deps {
564            let dep = dep.to_uppercase().replace('-', "_");
565            if let Some(root) = env::var_os(format!("DEP_{}_ROOT", dep)) {
566                cmake_prefix_path.push(PathBuf::from(root));
567            }
568        }
569        let system_prefix = self
570            .getenv_target_os("CMAKE_PREFIX_PATH")
571            .unwrap_or_default();
572        cmake_prefix_path.extend(env::split_paths(&system_prefix));
573        let cmake_prefix_path = env::join_paths(&cmake_prefix_path).unwrap();
574
575        // Build up the first cmake command to build the build system.
576        let mut cmd = self.cmake_configure_command(&target);
577
578        let version = Version::from_command(cmd.get_program()).unwrap_or_default();
579
580        if self.verbose_cmake {
581            cmd.arg("-Wdev");
582            cmd.arg("--debug-output");
583        }
584
585        cmd.arg(&self.path).current_dir(&build);
586        let mut is_ninja = false;
587        if let Some(ref generator) = generator {
588            is_ninja = generator.to_string_lossy().contains("Ninja");
589        }
590        if target.contains("windows-gnu") {
591            if host.contains("windows") {
592                // On MinGW we need to coerce cmake to not generate a visual
593                // studio build system but instead use makefiles that MinGW can
594                // use to build.
595                if generator.is_none() {
596                    // If make.exe isn't found, that means we may be using a MinGW
597                    // toolchain instead of a MSYS2 toolchain. If neither is found,
598                    // the build cannot continue.
599                    let has_msys2 = Command::new("make")
600                        .arg("--version")
601                        .output()
602                        .err()
603                        .map(|e| e.kind() != ErrorKind::NotFound)
604                        .unwrap_or(true);
605                    let has_mingw32 = Command::new("mingw32-make")
606                        .arg("--version")
607                        .output()
608                        .err()
609                        .map(|e| e.kind() != ErrorKind::NotFound)
610                        .unwrap_or(true);
611
612                    let generator = match (has_msys2, has_mingw32) {
613                        (true, _) => "MSYS Makefiles",
614                        (false, true) => "MinGW Makefiles",
615                        (false, false) => fail("no valid generator found for GNU toolchain; MSYS or MinGW must be installed")
616                    };
617
618                    cmd.arg("-G").arg(generator);
619                }
620            } else {
621                // If we're cross compiling onto windows, then set some
622                // variables which will hopefully get things to succeed. Some
623                // systems may need the `windres` or `dlltool` variables set, so
624                // set them if possible.
625                if !self.defined("CMAKE_RC_COMPILER") {
626                    let exe = find_exe(c_compiler.path());
627                    if let Some(name) = exe.file_name().unwrap().to_str() {
628                        let name = name.replace("gcc", "windres");
629                        let windres = exe.with_file_name(name);
630                        if windres.is_file() {
631                            let mut arg = OsString::from("-DCMAKE_RC_COMPILER=");
632                            arg.push(&windres);
633                            cmd.arg(arg);
634                        }
635                    }
636                }
637            }
638        } else if msvc {
639            // If we're on MSVC we need to be sure to use the right generator or
640            // otherwise we won't get 32/64 bit correct automatically.
641            // This also guarantees that NMake generator isn't chosen implicitly.
642            let using_nmake_generator = if let Some(g) = &generator {
643                g == "NMake Makefiles" || g == "NMake Makefiles JOM"
644            } else {
645                cmd.arg("-G").arg(self.visual_studio_generator(&target));
646                false
647            };
648            if !is_ninja && !using_nmake_generator {
649                if target.contains("x86_64") {
650                    if self.generator_toolset.is_none() {
651                        cmd.arg("-Thost=x64");
652                    }
653                    cmd.arg("-Ax64");
654                } else if target.contains("thumbv7a") {
655                    if self.generator_toolset.is_none() {
656                        cmd.arg("-Thost=x64");
657                    }
658                    cmd.arg("-Aarm");
659                } else if target.contains("aarch64") {
660                    if self.generator_toolset.is_none() {
661                        cmd.arg("-Thost=x64");
662                    }
663                    cmd.arg("-AARM64");
664                } else if target.contains("i686") {
665                    if self.generator_toolset.is_none() {
666                        cmd.arg("-Thost=x86");
667                    }
668                    cmd.arg("-AWin32");
669                } else {
670                    panic!("unsupported msvc target: {}", target);
671                }
672            }
673        } else if target.contains("darwin") && !self.defined("CMAKE_OSX_ARCHITECTURES") {
674            if target.contains("x86_64") {
675                cmd.arg("-DCMAKE_OSX_ARCHITECTURES=x86_64");
676            } else if target.contains("aarch64") {
677                cmd.arg("-DCMAKE_OSX_ARCHITECTURES=arm64");
678            } else {
679                panic!("unsupported darwin target: {}", target);
680            }
681        }
682        if let Some(ref generator) = generator {
683            cmd.arg("-G").arg(generator);
684        }
685        if let Some(ref generator_toolset) = self.generator_toolset {
686            cmd.arg("-T").arg(generator_toolset);
687        }
688        let profile = self.get_profile().to_string();
689        for (k, v) in &self.defines {
690            let mut os = OsString::from("-D");
691            os.push(k);
692            os.push("=");
693            os.push(v);
694            cmd.arg(os);
695        }
696
697        if !self.defined("CMAKE_INSTALL_PREFIX") {
698            let mut dstflag = OsString::from("-DCMAKE_INSTALL_PREFIX=");
699            dstflag.push(&dst);
700            cmd.arg(dstflag);
701        }
702
703        let build_type = self
704            .defines
705            .iter()
706            .find(|&(a, _)| a == "CMAKE_BUILD_TYPE")
707            .map(|x| x.1.to_str().unwrap())
708            .unwrap_or(&profile);
709        let build_type_upcase = build_type
710            .chars()
711            .flat_map(|c| c.to_uppercase())
712            .collect::<String>();
713
714        {
715            // let cmake deal with optimization/debuginfo
716            let skip_arg = |arg: &OsStr| match arg.to_str() {
717                Some(s) => s.starts_with("-O") || s.starts_with("/O") || s == "-g",
718                None => false,
719            };
720            let mut set_compiler = |kind: &str, compiler: &cc::Tool, extra: &OsString| {
721                let flag_var = format!("CMAKE_{}_FLAGS", kind);
722                let tool_var = format!("CMAKE_{}_COMPILER", kind);
723                if !self.defined(&flag_var) {
724                    let mut flagsflag = OsString::from("-D");
725                    flagsflag.push(&flag_var);
726                    flagsflag.push("=");
727                    flagsflag.push(extra);
728                    for arg in compiler.args() {
729                        if skip_arg(arg) {
730                            continue;
731                        }
732                        flagsflag.push(" ");
733                        flagsflag.push(arg);
734                    }
735                    cmd.arg(flagsflag);
736                }
737
738                // The visual studio generator apparently doesn't respect
739                // `CMAKE_C_FLAGS` but does respect `CMAKE_C_FLAGS_RELEASE` and
740                // such. We need to communicate /MD vs /MT, so set those vars
741                // here.
742                //
743                // Note that for other generators, though, this *overrides*
744                // things like the optimization flags, which is bad.
745                if generator.is_none() && msvc {
746                    let flag_var_alt = format!("CMAKE_{}_FLAGS_{}", kind, build_type_upcase);
747                    if !self.defined(&flag_var_alt) {
748                        let mut flagsflag = OsString::from("-D");
749                        flagsflag.push(&flag_var_alt);
750                        flagsflag.push("=");
751                        flagsflag.push(extra);
752                        for arg in compiler.args() {
753                            if skip_arg(arg) {
754                                continue;
755                            }
756                            flagsflag.push(" ");
757                            flagsflag.push(arg);
758                        }
759                        cmd.arg(flagsflag);
760                    }
761                }
762
763                // Apparently cmake likes to have an absolute path to the
764                // compiler as otherwise it sometimes thinks that this variable
765                // changed as it thinks the found compiler, /usr/bin/cc,
766                // differs from the specified compiler, cc. Not entirely sure
767                // what's up, but at least this means cmake doesn't get
768                // confused?
769                //
770                // Also specify this on Windows only if we use MSVC with Ninja,
771                // as it's not needed for MSVC with Visual Studio generators and
772                // for MinGW it doesn't really vary.
773                if !self.defined("CMAKE_TOOLCHAIN_FILE")
774                    && !self.defined(&tool_var)
775                    && (env::consts::FAMILY != "windows" || (msvc && is_ninja))
776                {
777                    let mut ccompiler = OsString::from("-D");
778                    ccompiler.push(&tool_var);
779                    ccompiler.push("=");
780                    ccompiler.push(find_exe(compiler.path()));
781                    #[cfg(windows)]
782                    {
783                        // CMake doesn't like unescaped `\`s in compiler paths
784                        // so we either have to escape them or replace with `/`s.
785                        use std::os::windows::ffi::{OsStrExt, OsStringExt};
786                        let wchars = ccompiler
787                            .encode_wide()
788                            .map(|wchar| {
789                                if wchar == b'\\' as u16 {
790                                    '/' as u16
791                                } else {
792                                    wchar
793                                }
794                            })
795                            .collect::<Vec<_>>();
796                        ccompiler = OsString::from_wide(&wchars);
797                    }
798                    cmd.arg(ccompiler);
799                }
800            };
801
802            set_compiler("C", &c_compiler, &self.cflags);
803            set_compiler("CXX", &cxx_compiler, &self.cxxflags);
804            set_compiler("ASM", &asm_compiler, &self.asmflags);
805        }
806
807        if !self.defined("CMAKE_BUILD_TYPE") {
808            cmd.arg(format!("-DCMAKE_BUILD_TYPE={}", profile));
809        }
810
811        if self.verbose_make {
812            cmd.arg("-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON");
813        }
814
815        for (k, v) in c_compiler.env().iter().chain(&self.env) {
816            cmd.env(k, v);
817        }
818
819        if self.always_configure || !build.join("CMakeCache.txt").exists() {
820            cmd.args(&self.configure_args);
821            run(cmd.env("CMAKE_PREFIX_PATH", cmake_prefix_path), "cmake");
822        } else {
823            println!("CMake project was already configured. Skipping configuration step.");
824        }
825
826        // And build!
827        let mut cmd = self.cmake_build_command(&target);
828        cmd.current_dir(&build);
829
830        for (k, v) in c_compiler.env().iter().chain(&self.env) {
831            cmd.env(k, v);
832        }
833
834        // If the generated project is Makefile based we should carefully transfer corresponding CARGO_MAKEFLAGS
835        let mut use_jobserver = false;
836        if fs::metadata(build.join("Makefile")).is_ok() {
837            match env::var_os("CARGO_MAKEFLAGS") {
838                // Only do this on non-windows, non-bsd, and non-macos (unless a named pipe
839                // jobserver is available)
840                // * On Windows, we could be invoking make instead of
841                //   mingw32-make which doesn't work with our jobserver
842                // * bsdmake also does not work with our job server
843                // * On macOS, CMake blocks propagation of the jobserver's file descriptors to make
844                //   However, if the jobserver is based on a named pipe, this will be available to
845                //   the build.
846                Some(ref makeflags)
847                    if !(cfg!(windows)
848                        || cfg!(target_os = "openbsd")
849                        || cfg!(target_os = "netbsd")
850                        || cfg!(target_os = "freebsd")
851                        || cfg!(target_os = "dragonfly")
852                        || (cfg!(target_os = "macos")
853                            && !uses_named_pipe_jobserver(makeflags))) =>
854                {
855                    use_jobserver = true;
856                    cmd.env("MAKEFLAGS", makeflags);
857                }
858                _ => {}
859            }
860        }
861
862        cmd.arg("--build").arg(&build);
863
864        if !self.no_build_target {
865            let target = self
866                .cmake_target
867                .clone()
868                .unwrap_or_else(|| "install".to_string());
869            cmd.arg("--target").arg(target);
870        }
871
872        cmd.arg("--config").arg(&profile);
873
874        // --parallel requires CMake 3.12:
875        // https://cmake.org/cmake/help/latest/release/3.12.html#command-line
876        if version >= Version::new(3, 12) && !use_jobserver {
877            if let Ok(s) = env::var("NUM_JOBS") {
878                // See https://cmake.org/cmake/help/v3.12/manual/cmake.1.html#build-tool-mode
879                cmd.arg("--parallel").arg(s);
880            }
881        }
882
883        if !&self.build_args.is_empty() {
884            cmd.arg("--").args(&self.build_args);
885        }
886
887        run(&mut cmd, "cmake");
888
889        println!("cargo:root={}", dst.display());
890        dst
891    }
892
893    fn cmake_executable(&mut self) -> OsString {
894        self.getenv_target_os("CMAKE")
895            .unwrap_or_else(|| OsString::from("cmake"))
896    }
897
898    // If we are building for Emscripten, wrap the calls to CMake
899    // as "emcmake cmake ..." and "emmake cmake --build ...".
900    // https://emscripten.org/docs/compiling/Building-Projects.html
901
902    fn cmake_configure_command(&mut self, target: &str) -> Command {
903        if target.contains("emscripten") {
904            let emcmake = self
905                .getenv_target_os("EMCMAKE")
906                .unwrap_or_else(|| OsString::from("emcmake"));
907            let mut cmd = Command::new(emcmake);
908            cmd.arg(self.cmake_executable());
909            cmd
910        } else {
911            Command::new(self.cmake_executable())
912        }
913    }
914
915    fn cmake_build_command(&mut self, target: &str) -> Command {
916        if target.contains("emscripten") {
917            let emmake = self
918                .getenv_target_os("EMMAKE")
919                .unwrap_or_else(|| OsString::from("emmake"));
920            let mut cmd = Command::new(emmake);
921            cmd.arg(self.cmake_executable());
922            cmd
923        } else {
924            Command::new(self.cmake_executable())
925        }
926    }
927
928    fn getenv_os(&mut self, v: &str) -> Option<OsString> {
929        if let Some(val) = self.env_cache.get(v) {
930            return val.clone();
931        }
932        let r = env::var_os(v);
933        println!("{} = {:?}", v, r);
934        self.env_cache.insert(v.to_string(), r.clone());
935        r
936    }
937
938    /// Gets a target-specific environment variable.
939    fn getenv_target_os(&mut self, var_base: &str) -> Option<OsString> {
940        let host = self.host.clone().unwrap_or_else(|| getenv_unwrap("HOST"));
941        let target = self
942            .target
943            .clone()
944            .unwrap_or_else(|| getenv_unwrap("TARGET"));
945
946        let kind = if host == target { "HOST" } else { "TARGET" };
947        let target_u = target.replace('-', "_");
948        self.getenv_os(&format!("{}_{}", var_base, target))
949            .or_else(|| self.getenv_os(&format!("{}_{}", var_base, target_u)))
950            .or_else(|| self.getenv_os(&format!("{}_{}", kind, var_base)))
951            .or_else(|| self.getenv_os(var_base))
952    }
953
954    fn visual_studio_generator(&self, target: &str) -> String {
955        use cc::windows_registry::{find_vs_version, VsVers};
956
957        let base = match find_vs_version() {
958            Ok(VsVers::Vs17) => "Visual Studio 17 2022",
959            Ok(VsVers::Vs16) => "Visual Studio 16 2019",
960            Ok(VsVers::Vs15) => "Visual Studio 15 2017",
961            Ok(VsVers::Vs14) => "Visual Studio 14 2015",
962            // This was deprecated recently (2024-07). Ignore the warning for now.
963            #[allow(deprecated)]
964            Ok(VsVers::Vs12) => "Visual Studio 12 2013",
965            Ok(_) => panic!(
966                "Visual studio version detected but this crate \
967                 doesn't know how to generate cmake files for it, \
968                 can the `cmake` crate be updated?"
969            ),
970            Err(msg) => panic!("{}", msg),
971        };
972        if ["i686", "x86_64", "thumbv7a", "aarch64"]
973            .iter()
974            .any(|t| target.contains(t))
975        {
976            base.to_string()
977        } else {
978            panic!("unsupported msvc target: {}", target);
979        }
980    }
981
982    fn defined(&self, var: &str) -> bool {
983        self.defines.iter().any(|(a, _)| a == var)
984    }
985
986    // If a cmake project has previously been built (e.g. CMakeCache.txt already
987    // exists), then cmake will choke if the source directory for the original
988    // project being built has changed. Detect this situation through the
989    // `CMAKE_HOME_DIRECTORY` variable that cmake emits and if it doesn't match
990    // we blow away the build directory and start from scratch (the recommended
991    // solution apparently [1]).
992    //
993    // [1]: https://cmake.org/pipermail/cmake/2012-August/051545.html
994    fn maybe_clear(&self, dir: &Path) {
995        // CMake will apparently store canonicalized paths which normally
996        // isn't relevant to us but we canonicalize it here to ensure
997        // we're both checking the same thing.
998        let path = fs::canonicalize(&self.path).unwrap_or_else(|_| self.path.clone());
999        let mut f = match File::open(dir.join("CMakeCache.txt")) {
1000            Ok(f) => f,
1001            Err(..) => return,
1002        };
1003        let mut u8contents = Vec::new();
1004        match f.read_to_end(&mut u8contents) {
1005            Ok(f) => f,
1006            Err(..) => return,
1007        };
1008        let contents = String::from_utf8_lossy(&u8contents);
1009        drop(f);
1010        for line in contents.lines() {
1011            if line.starts_with("CMAKE_HOME_DIRECTORY") {
1012                let needs_cleanup = match line.split('=').next_back() {
1013                    Some(cmake_home) => fs::canonicalize(cmake_home)
1014                        .ok()
1015                        .map(|cmake_home| cmake_home != path)
1016                        .unwrap_or(true),
1017                    None => true,
1018                };
1019                if needs_cleanup {
1020                    println!(
1021                        "detected home dir change, cleaning out entire build \
1022                         directory"
1023                    );
1024                    fs::remove_dir_all(dir).unwrap();
1025                }
1026                break;
1027            }
1028        }
1029    }
1030}
1031
1032#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1033struct Version {
1034    major: u32,
1035    minor: u32,
1036}
1037
1038impl Version {
1039    fn new(major: u32, minor: u32) -> Self {
1040        Self { major, minor }
1041    }
1042
1043    fn parse(s: &str) -> Option<Self> {
1044        // As of 3.22, the format of the version output is "cmake version <major>.<minor>.<patch>".
1045        // ```
1046        // $ cmake --version
1047        // cmake version 3.22.2
1048        //
1049        // CMake suite maintained and supported by Kitware (kitware.com/cmake).
1050        // ```
1051        let version = s.lines().next()?.strip_prefix("cmake version ")?;
1052        let mut digits = version.splitn(3, '.'); // split version string to major minor patch
1053        let major = digits.next()?.parse::<u32>().ok()?;
1054        let minor = digits.next()?.parse::<u32>().ok()?;
1055        // Ignore the patch version because it does not change the API.
1056        Some(Version::new(major, minor))
1057    }
1058
1059    fn from_command(executable: &OsStr) -> Option<Self> {
1060        let output = Command::new(executable).arg("--version").output().ok()?;
1061        if !output.status.success() {
1062            return None;
1063        }
1064        let stdout = core::str::from_utf8(&output.stdout).ok()?;
1065        Self::parse(stdout)
1066    }
1067}
1068
1069impl Default for Version {
1070    fn default() -> Self {
1071        // If the version parsing fails, we assume that it is the latest known
1072        // version. This is because the failure of version parsing may be due to
1073        // the version output being changed.
1074        Self::new(3, 22)
1075    }
1076}
1077
1078fn run(cmd: &mut Command, program: &str) {
1079    println!("running: {:?}", cmd);
1080    let status = match cmd.status() {
1081        Ok(status) => status,
1082        Err(ref e) if e.kind() == ErrorKind::NotFound => {
1083            fail(&format!(
1084                "failed to execute command: {}\nis `{}` not installed?",
1085                e, program
1086            ));
1087        }
1088        Err(e) => fail(&format!("failed to execute command: {}", e)),
1089    };
1090    if !status.success() {
1091        if status.code() == Some(127) {
1092            fail(&format!(
1093                "command did not execute successfully, got: {}, is `{}` not installed?",
1094                status, program
1095            ));
1096        }
1097        fail(&format!(
1098            "command did not execute successfully, got: {}",
1099            status
1100        ));
1101    }
1102}
1103
1104fn find_exe(path: &Path) -> PathBuf {
1105    env::split_paths(&env::var_os("PATH").unwrap_or_default())
1106        .map(|p| p.join(path))
1107        .find(|p| fs::metadata(p).is_ok())
1108        .unwrap_or_else(|| path.to_owned())
1109}
1110
1111fn getenv_unwrap(v: &str) -> String {
1112    match env::var(v) {
1113        Ok(s) => s,
1114        Err(..) => fail(&format!("environment variable `{}` not defined", v)),
1115    }
1116}
1117
1118fn fail(s: &str) -> ! {
1119    panic!("\n{}\n\nbuild script failed, must exit now", s)
1120}
1121
1122/// Returns whether the given MAKEFLAGS indicate that there is an available
1123/// jobserver that uses a named pipe (fifo)
1124fn uses_named_pipe_jobserver(makeflags: &OsStr) -> bool {
1125    makeflags
1126        .to_string_lossy()
1127        // auth option as defined in
1128        // https://www.gnu.org/software/make/manual/html_node/POSIX-Jobserver.html#POSIX-Jobserver
1129        .contains("--jobserver-auth=fifo:")
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134    use super::uses_named_pipe_jobserver;
1135    use super::Version;
1136
1137    #[test]
1138    fn test_cmake_version() {
1139        let text = "cmake version 3.22.2
1140
1141CMake suite maintained and supported by Kitware (kitware.com/cmake).
1142";
1143        let v = Version::parse(text).unwrap();
1144        assert_eq!(v, Version::new(3, 22));
1145        assert!(Version::new(3, 22) > Version::new(3, 21));
1146        assert!(Version::new(3, 22) < Version::new(3, 23));
1147
1148        let _v = Version::from_command("cmake".as_ref()).unwrap();
1149    }
1150
1151    #[test]
1152    fn test_uses_fifo_jobserver() {
1153        assert!(uses_named_pipe_jobserver(
1154            "-j --jobserver-auth=fifo:/foo".as_ref()
1155        ));
1156        assert!(!uses_named_pipe_jobserver(
1157            "-j --jobserver-auth=8:9".as_ref()
1158        ));
1159    }
1160}