[go: up one dir, main page]

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
558        let build_dir = try_canonicalize(&dst.join("build"));
559
560        self.maybe_clear(&build_dir);
561        let _ = fs::create_dir_all(&build_dir);
562
563        // Add all our dependencies to our cmake paths
564        let mut cmake_prefix_path = Vec::new();
565        for dep in &self.deps {
566            let dep = dep.to_uppercase().replace('-', "_");
567            if let Some(root) = env::var_os(format!("DEP_{}_ROOT", dep)) {
568                cmake_prefix_path.push(PathBuf::from(root));
569            }
570        }
571        let system_prefix = self
572            .getenv_target_os("CMAKE_PREFIX_PATH")
573            .unwrap_or_default();
574        cmake_prefix_path.extend(env::split_paths(&system_prefix));
575        let cmake_prefix_path = env::join_paths(&cmake_prefix_path).unwrap();
576
577        // Build up the first cmake command to build the build system.
578        let mut cmd = self.cmake_configure_command(&target);
579
580        let version = Version::from_command(cmd.get_program()).unwrap_or_default();
581
582        if self.verbose_cmake {
583            cmd.arg("-Wdev");
584            cmd.arg("--debug-output");
585        }
586
587        cmd.arg(&self.path).current_dir(&build_dir);
588
589        if version >= Version::new(3, 13) {
590            cmd.arg("-B").arg(&build_dir);
591        }
592
593        let mut is_ninja = false;
594        if let Some(ref generator) = generator {
595            is_ninja = generator.to_string_lossy().contains("Ninja");
596        }
597        if target.contains("windows-gnu") {
598            if host.contains("windows") {
599                // On MinGW we need to coerce cmake to not generate a visual
600                // studio build system but instead use makefiles that MinGW can
601                // use to build.
602                if generator.is_none() {
603                    // If make.exe isn't found, that means we may be using a MinGW
604                    // toolchain instead of a MSYS2 toolchain. If neither is found,
605                    // the build cannot continue.
606                    let has_msys2 = Command::new("make")
607                        .arg("--version")
608                        .output()
609                        .err()
610                        .map(|e| e.kind() != ErrorKind::NotFound)
611                        .unwrap_or(true);
612                    let has_mingw32 = Command::new("mingw32-make")
613                        .arg("--version")
614                        .output()
615                        .err()
616                        .map(|e| e.kind() != ErrorKind::NotFound)
617                        .unwrap_or(true);
618
619                    let generator = match (has_msys2, has_mingw32) {
620                        (true, _) => "MSYS Makefiles",
621                        (false, true) => "MinGW Makefiles",
622                        (false, false) => fail("no valid generator found for GNU toolchain; MSYS or MinGW must be installed")
623                    };
624
625                    cmd.arg("-G").arg(generator);
626                }
627            } else {
628                // If we're cross compiling onto windows, then set some
629                // variables which will hopefully get things to succeed. Some
630                // systems may need the `windres` or `dlltool` variables set, so
631                // set them if possible.
632                if !self.defined("CMAKE_RC_COMPILER") {
633                    let exe = find_exe(c_compiler.path());
634                    if let Some(name) = exe.file_name().unwrap().to_str() {
635                        let name = name.replace("gcc", "windres");
636                        let windres = exe.with_file_name(name);
637                        if windres.is_file() {
638                            let mut arg = OsString::from("-DCMAKE_RC_COMPILER=");
639                            arg.push(&windres);
640                            cmd.arg(arg);
641                        }
642                    }
643                }
644            }
645        } else if msvc {
646            // If we're on MSVC we need to be sure to use the right generator or
647            // otherwise we won't get 32/64 bit correct automatically.
648            // This also guarantees that NMake generator isn't chosen implicitly.
649            let using_nmake_generator = if let Some(g) = &generator {
650                g == "NMake Makefiles" || g == "NMake Makefiles JOM"
651            } else {
652                cmd.arg("-G").arg(self.visual_studio_generator(&target));
653                false
654            };
655            if !is_ninja && !using_nmake_generator {
656                if target.contains("x86_64") {
657                    if self.generator_toolset.is_none() {
658                        cmd.arg("-Thost=x64");
659                    }
660                    cmd.arg("-Ax64");
661                } else if target.contains("thumbv7a") {
662                    if self.generator_toolset.is_none() {
663                        cmd.arg("-Thost=x64");
664                    }
665                    cmd.arg("-Aarm");
666                } else if target.contains("aarch64") {
667                    if self.generator_toolset.is_none() {
668                        cmd.arg("-Thost=x64");
669                    }
670                    cmd.arg("-AARM64");
671                } else if target.contains("i686") {
672                    if self.generator_toolset.is_none() {
673                        cmd.arg("-Thost=x86");
674                    }
675                    cmd.arg("-AWin32");
676                } else {
677                    panic!("unsupported msvc target: {}", target);
678                }
679            }
680        } else if target.contains("darwin") && !self.defined("CMAKE_OSX_ARCHITECTURES") {
681            if target.contains("x86_64") {
682                cmd.arg("-DCMAKE_OSX_ARCHITECTURES=x86_64");
683            } else if target.contains("aarch64") {
684                cmd.arg("-DCMAKE_OSX_ARCHITECTURES=arm64");
685            } else {
686                panic!("unsupported darwin target: {}", target);
687            }
688        }
689        if let Some(ref generator) = generator {
690            cmd.arg("-G").arg(generator);
691        }
692        if let Some(ref generator_toolset) = self.generator_toolset {
693            cmd.arg("-T").arg(generator_toolset);
694        }
695        let profile = self.get_profile().to_string();
696        for (k, v) in &self.defines {
697            let mut os = OsString::from("-D");
698            os.push(k);
699            os.push("=");
700            os.push(v);
701            cmd.arg(os);
702        }
703
704        if !self.defined("CMAKE_INSTALL_PREFIX") {
705            let mut dstflag = OsString::from("-DCMAKE_INSTALL_PREFIX=");
706            dstflag.push(&dst);
707            cmd.arg(dstflag);
708        }
709
710        let build_type = self
711            .defines
712            .iter()
713            .find(|&(a, _)| a == "CMAKE_BUILD_TYPE")
714            .map(|x| x.1.to_str().unwrap())
715            .unwrap_or(&profile);
716        let build_type_upcase = build_type
717            .chars()
718            .flat_map(|c| c.to_uppercase())
719            .collect::<String>();
720
721        {
722            // let cmake deal with optimization/debuginfo
723            let skip_arg = |arg: &OsStr| match arg.to_str() {
724                Some(s) => s.starts_with("-O") || s.starts_with("/O") || s == "-g",
725                None => false,
726            };
727            let mut set_compiler = |kind: &str, compiler: &cc::Tool, extra: &OsString| {
728                let flag_var = format!("CMAKE_{}_FLAGS", kind);
729                let tool_var = format!("CMAKE_{}_COMPILER", kind);
730                if !self.defined(&flag_var) {
731                    let mut flagsflag = OsString::from("-D");
732                    flagsflag.push(&flag_var);
733                    flagsflag.push("=");
734                    flagsflag.push(extra);
735                    for arg in compiler.args() {
736                        if skip_arg(arg) {
737                            continue;
738                        }
739                        flagsflag.push(" ");
740                        flagsflag.push(arg);
741                    }
742                    cmd.arg(flagsflag);
743                }
744
745                // The visual studio generator apparently doesn't respect
746                // `CMAKE_C_FLAGS` but does respect `CMAKE_C_FLAGS_RELEASE` and
747                // such. We need to communicate /MD vs /MT, so set those vars
748                // here.
749                //
750                // Note that for other generators, though, this *overrides*
751                // things like the optimization flags, which is bad.
752                if generator.is_none() && msvc {
753                    let flag_var_alt = format!("CMAKE_{}_FLAGS_{}", kind, build_type_upcase);
754                    if !self.defined(&flag_var_alt) {
755                        let mut flagsflag = OsString::from("-D");
756                        flagsflag.push(&flag_var_alt);
757                        flagsflag.push("=");
758                        flagsflag.push(extra);
759                        for arg in compiler.args() {
760                            if skip_arg(arg) {
761                                continue;
762                            }
763                            flagsflag.push(" ");
764                            flagsflag.push(arg);
765                        }
766                        cmd.arg(flagsflag);
767                    }
768                }
769
770                // Apparently cmake likes to have an absolute path to the
771                // compiler as otherwise it sometimes thinks that this variable
772                // changed as it thinks the found compiler, /usr/bin/cc,
773                // differs from the specified compiler, cc. Not entirely sure
774                // what's up, but at least this means cmake doesn't get
775                // confused?
776                //
777                // Also specify this on Windows only if we use MSVC with Ninja,
778                // as it's not needed for MSVC with Visual Studio generators and
779                // for MinGW it doesn't really vary.
780                if !self.defined("CMAKE_TOOLCHAIN_FILE")
781                    && !self.defined(&tool_var)
782                    && (env::consts::FAMILY != "windows" || (msvc && is_ninja))
783                {
784                    let mut ccompiler = OsString::from("-D");
785                    ccompiler.push(&tool_var);
786                    ccompiler.push("=");
787                    ccompiler.push(find_exe(compiler.path()));
788                    #[cfg(windows)]
789                    {
790                        // CMake doesn't like unescaped `\`s in compiler paths
791                        // so we either have to escape them or replace with `/`s.
792                        use std::os::windows::ffi::{OsStrExt, OsStringExt};
793                        let wchars = ccompiler
794                            .encode_wide()
795                            .map(|wchar| {
796                                if wchar == b'\\' as u16 {
797                                    '/' as u16
798                                } else {
799                                    wchar
800                                }
801                            })
802                            .collect::<Vec<_>>();
803                        ccompiler = OsString::from_wide(&wchars);
804                    }
805                    cmd.arg(ccompiler);
806                }
807            };
808
809            set_compiler("C", &c_compiler, &self.cflags);
810            set_compiler("CXX", &cxx_compiler, &self.cxxflags);
811            set_compiler("ASM", &asm_compiler, &self.asmflags);
812        }
813
814        if !self.defined("CMAKE_BUILD_TYPE") {
815            cmd.arg(format!("-DCMAKE_BUILD_TYPE={}", profile));
816        }
817
818        if self.verbose_make {
819            cmd.arg("-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON");
820        }
821
822        for (k, v) in c_compiler.env().iter().chain(&self.env) {
823            cmd.env(k, v);
824        }
825
826        if self.always_configure || !build_dir.join("CMakeCache.txt").exists() {
827            cmd.args(&self.configure_args);
828            run(cmd.env("CMAKE_PREFIX_PATH", cmake_prefix_path), "cmake");
829        } else {
830            println!("CMake project was already configured. Skipping configuration step.");
831        }
832
833        // And build!
834        let mut cmd = self.cmake_build_command(&target);
835        cmd.current_dir(&build_dir);
836
837        for (k, v) in c_compiler.env().iter().chain(&self.env) {
838            cmd.env(k, v);
839        }
840
841        // If the generated project is Makefile based we should carefully transfer corresponding CARGO_MAKEFLAGS
842        let mut use_jobserver = false;
843        if fs::metadata(build_dir.join("Makefile")).is_ok() {
844            match env::var_os("CARGO_MAKEFLAGS") {
845                // Only do this on non-windows, non-bsd, and non-macos (unless a named pipe
846                // jobserver is available)
847                // * On Windows, we could be invoking make instead of
848                //   mingw32-make which doesn't work with our jobserver
849                // * bsdmake also does not work with our job server
850                // * On macOS, CMake blocks propagation of the jobserver's file descriptors to make
851                //   However, if the jobserver is based on a named pipe, this will be available to
852                //   the build.
853                Some(ref makeflags)
854                    if !(cfg!(windows)
855                        || cfg!(target_os = "openbsd")
856                        || cfg!(target_os = "netbsd")
857                        || cfg!(target_os = "freebsd")
858                        || cfg!(target_os = "dragonfly")
859                        || (cfg!(target_os = "macos")
860                            && !uses_named_pipe_jobserver(makeflags))) =>
861                {
862                    use_jobserver = true;
863                    cmd.env("MAKEFLAGS", makeflags);
864                }
865                _ => {}
866            }
867        }
868
869        cmd.arg("--build").arg(&build_dir);
870
871        if !self.no_build_target {
872            let target = self
873                .cmake_target
874                .clone()
875                .unwrap_or_else(|| "install".to_string());
876            cmd.arg("--target").arg(target);
877        }
878
879        cmd.arg("--config").arg(&profile);
880
881        // --parallel requires CMake 3.12:
882        // https://cmake.org/cmake/help/latest/release/3.12.html#command-line
883        if version >= Version::new(3, 12) && !use_jobserver {
884            if let Ok(s) = env::var("NUM_JOBS") {
885                // See https://cmake.org/cmake/help/v3.12/manual/cmake.1.html#build-tool-mode
886                cmd.arg("--parallel").arg(s);
887            }
888        }
889
890        if !&self.build_args.is_empty() {
891            cmd.arg("--").args(&self.build_args);
892        }
893
894        run(&mut cmd, "cmake");
895
896        println!("cargo:root={}", dst.display());
897        dst
898    }
899
900    fn cmake_executable(&mut self, target: &str) -> OsString {
901        self.getenv_target_os("CMAKE")
902            .or_else(|| find_cmake_executable(target))
903            .unwrap_or_else(|| OsString::from("cmake"))
904    }
905
906    // If we are building for Emscripten, wrap the calls to CMake
907    // as "emcmake cmake ..." and "emmake cmake --build ...".
908    // https://emscripten.org/docs/compiling/Building-Projects.html
909
910    fn cmake_configure_command(&mut self, target: &str) -> Command {
911        if target.contains("emscripten") {
912            let emcmake = self
913                .getenv_target_os("EMCMAKE")
914                .unwrap_or_else(|| OsString::from("emcmake"));
915            let mut cmd = Command::new(emcmake);
916            cmd.arg(self.cmake_executable(target));
917            cmd
918        } else {
919            Command::new(self.cmake_executable(target))
920        }
921    }
922
923    fn cmake_build_command(&mut self, target: &str) -> Command {
924        if target.contains("emscripten") {
925            let emmake = self
926                .getenv_target_os("EMMAKE")
927                .unwrap_or_else(|| OsString::from("emmake"));
928            let mut cmd = Command::new(emmake);
929            cmd.arg(self.cmake_executable(target));
930            cmd
931        } else {
932            Command::new(self.cmake_executable(target))
933        }
934    }
935
936    fn getenv_os(&mut self, v: &str) -> Option<OsString> {
937        if let Some(val) = self.env_cache.get(v) {
938            return val.clone();
939        }
940        let r = env::var_os(v);
941        println!("{} = {:?}", v, r);
942        self.env_cache.insert(v.to_string(), r.clone());
943        r
944    }
945
946    /// Gets a target-specific environment variable.
947    fn getenv_target_os(&mut self, var_base: &str) -> Option<OsString> {
948        let host = self.host.clone().unwrap_or_else(|| getenv_unwrap("HOST"));
949        let target = self
950            .target
951            .clone()
952            .unwrap_or_else(|| getenv_unwrap("TARGET"));
953
954        let kind = if host == target { "HOST" } else { "TARGET" };
955        let target_u = target.replace('-', "_");
956        self.getenv_os(&format!("{}_{}", var_base, target))
957            .or_else(|| self.getenv_os(&format!("{}_{}", var_base, target_u)))
958            .or_else(|| self.getenv_os(&format!("{}_{}", kind, var_base)))
959            .or_else(|| self.getenv_os(var_base))
960    }
961
962    fn visual_studio_generator(&self, target: &str) -> String {
963        use cc::windows_registry::{find_vs_version, VsVers};
964
965        let base = match find_vs_version() {
966            Ok(VsVers::Vs18) => "Visual Studio 18 2026",
967            Ok(VsVers::Vs17) => "Visual Studio 17 2022",
968            Ok(VsVers::Vs16) => "Visual Studio 16 2019",
969            Ok(VsVers::Vs15) => "Visual Studio 15 2017",
970            Ok(VsVers::Vs14) => "Visual Studio 14 2015",
971            // This was deprecated recently (2024-07). Ignore the warning for now.
972            #[allow(deprecated)]
973            Ok(VsVers::Vs12) => "Visual Studio 12 2013",
974            Ok(_) => panic!(
975                "Visual studio version detected but this crate \
976                 doesn't know how to generate cmake files for it, \
977                 can the `cmake` crate be updated?"
978            ),
979            Err(msg) => panic!("{}", msg),
980        };
981        if ["i686", "x86_64", "thumbv7a", "aarch64"]
982            .iter()
983            .any(|t| target.contains(t))
984        {
985            base.to_string()
986        } else {
987            panic!("unsupported msvc target: {}", target);
988        }
989    }
990
991    fn defined(&self, var: &str) -> bool {
992        self.defines.iter().any(|(a, _)| a == var)
993    }
994
995    // If a cmake project has previously been built (e.g. CMakeCache.txt already
996    // exists), then cmake will choke if the source directory for the original
997    // project being built has changed. Detect this situation through the
998    // `CMAKE_HOME_DIRECTORY` variable that cmake emits and if it doesn't match
999    // we blow away the build directory and start from scratch (the recommended
1000    // solution apparently [1]).
1001    //
1002    // [1]: https://cmake.org/pipermail/cmake/2012-August/051545.html
1003    fn maybe_clear(&self, dir: &Path) {
1004        // CMake will apparently store canonicalized paths which normally
1005        // isn't relevant to us but we canonicalize it here to ensure
1006        // we're both checking the same thing.
1007        let path = try_canonicalize(&self.path);
1008
1009        let mut f = match File::open(dir.join("CMakeCache.txt")) {
1010            Ok(f) => f,
1011            Err(..) => return,
1012        };
1013        let mut u8contents = Vec::new();
1014        match f.read_to_end(&mut u8contents) {
1015            Ok(f) => f,
1016            Err(..) => return,
1017        };
1018        let contents = String::from_utf8_lossy(&u8contents);
1019        drop(f);
1020        for line in contents.lines() {
1021            if line.starts_with("CMAKE_HOME_DIRECTORY") {
1022                let needs_cleanup = match line.split('=').next_back() {
1023                    Some(cmake_home) => fs::canonicalize(cmake_home)
1024                        .ok()
1025                        .map(|cmake_home| cmake_home != path)
1026                        .unwrap_or(true),
1027                    None => true,
1028                };
1029                if needs_cleanup {
1030                    println!(
1031                        "detected home dir change, cleaning out entire build \
1032                         directory"
1033                    );
1034                    fs::remove_dir_all(dir).unwrap();
1035                }
1036                break;
1037            }
1038        }
1039    }
1040}
1041
1042#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1043struct Version {
1044    major: u32,
1045    minor: u32,
1046}
1047
1048impl Version {
1049    fn new(major: u32, minor: u32) -> Self {
1050        Self { major, minor }
1051    }
1052
1053    fn parse(s: &str) -> Option<Self> {
1054        // As of 3.22, the format of the version output is "cmake version <major>.<minor>.<patch>".
1055        // ```
1056        // $ cmake --version
1057        // cmake version 3.22.2
1058        //
1059        // CMake suite maintained and supported by Kitware (kitware.com/cmake).
1060        // ```
1061        let version = s.lines().next()?.strip_prefix("cmake version ")?;
1062        let mut digits = version.splitn(3, '.'); // split version string to major minor patch
1063        let major = digits.next()?.parse::<u32>().ok()?;
1064        let minor = digits.next()?.parse::<u32>().ok()?;
1065        // Ignore the patch version because it does not change the API.
1066        Some(Version::new(major, minor))
1067    }
1068
1069    fn from_command(executable: &OsStr) -> Option<Self> {
1070        let output = Command::new(executable).arg("--version").output().ok()?;
1071        if !output.status.success() {
1072            return None;
1073        }
1074        let stdout = core::str::from_utf8(&output.stdout).ok()?;
1075        Self::parse(stdout)
1076    }
1077}
1078
1079impl Default for Version {
1080    fn default() -> Self {
1081        // If the version parsing fails, we assume that it is the latest known
1082        // version. This is because the failure of version parsing may be due to
1083        // the version output being changed.
1084        Self::new(3, 22)
1085    }
1086}
1087
1088fn run(cmd: &mut Command, program: &str) {
1089    eprintln!("running: {:?}", cmd);
1090    let status = match cmd.status() {
1091        Ok(status) => status,
1092        Err(ref e) if e.kind() == ErrorKind::NotFound => {
1093            fail(&format!(
1094                "failed to execute command: {}\nis `{}` not installed?",
1095                e, program
1096            ));
1097        }
1098        Err(e) => fail(&format!("failed to execute command: {}", e)),
1099    };
1100    if !status.success() {
1101        if status.code() == Some(127) {
1102            fail(&format!(
1103                "command did not execute successfully, got: {}, is `{}` not installed?",
1104                status, program
1105            ));
1106        }
1107        fail(&format!(
1108            "command did not execute successfully, got: {}",
1109            status
1110        ));
1111    }
1112}
1113
1114fn find_exe(path: &Path) -> PathBuf {
1115    env::split_paths(&env::var_os("PATH").unwrap_or_default())
1116        .map(|p| p.join(path))
1117        .find(|p| fs::metadata(p).is_ok())
1118        .unwrap_or_else(|| path.to_owned())
1119}
1120
1121fn getenv_unwrap(v: &str) -> String {
1122    match env::var(v) {
1123        Ok(s) => s,
1124        Err(..) => fail(&format!("environment variable `{}` not defined", v)),
1125    }
1126}
1127
1128fn fail(s: &str) -> ! {
1129    panic!("\n{}\n\nbuild script failed, must exit now", s)
1130}
1131
1132/// Returns whether the given MAKEFLAGS indicate that there is an available
1133/// jobserver that uses a named pipe (fifo)
1134fn uses_named_pipe_jobserver(makeflags: &OsStr) -> bool {
1135    makeflags
1136        .to_string_lossy()
1137        // auth option as defined in
1138        // https://www.gnu.org/software/make/manual/html_node/POSIX-Jobserver.html#POSIX-Jobserver
1139        .contains("--jobserver-auth=fifo:")
1140}
1141
1142/// Attempt to canonicalize; fall back to the original path if unsuccessful, in case `cmake` knows
1143/// something we don't.
1144fn try_canonicalize(path: &Path) -> PathBuf {
1145    let path = path.canonicalize().unwrap_or_else(|_| path.to_owned());
1146    // On Windows, attempt to remove the verbatim prefix from the canonicalized path.
1147    // FIXME(ChrisDenton): once MSRV is >=1.79 use `std::path::absolute` instead of canonicalize.
1148    // That will avoid the need for this hack.
1149    #[cfg(windows)]
1150    {
1151        use std::os::windows::ffi::{OsStrExt, OsStringExt};
1152        let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
1153        if wide.starts_with(&[b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16]) {
1154            if wide.get(5..7) == Some(&[b':' as u16, b'\\' as u16]) {
1155                // Convert \\?\C:\ to C:\
1156                wide.copy_within(4.., 0);
1157                wide.truncate(wide.len() - 4);
1158            } else if wide.get(4..8) == Some(&[b'U' as u16, b'N' as u16, b'C' as u16, b'\\' as u16])
1159            {
1160                // Convert \\?\UNC\ to \\
1161                wide.copy_within(8.., 2);
1162                wide.truncate(wide.len() - (8 - 2));
1163            }
1164            return OsString::from_wide(&wide).into();
1165        }
1166    }
1167    path
1168}
1169
1170#[cfg(windows)]
1171fn find_cmake_executable(target: &str) -> Option<OsString> {
1172    use cc::windows_registry::find_tool;
1173
1174    // Try to find cmake.exe bundled with MSVC, but only if there isn't another one in path
1175    let cmake_in_path = env::split_paths(&env::var_os("PATH").unwrap_or(OsString::new()))
1176        .any(|p| p.join("cmake.exe").exists());
1177    if cmake_in_path {
1178        None
1179    } else {
1180        find_tool(target, "devenv").and_then(|t| {
1181            t.path()
1182                .join("..\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe")
1183                .canonicalize()
1184                .ok()
1185                .map(OsString::from)
1186        })
1187    }
1188}
1189
1190#[cfg(not(windows))]
1191fn find_cmake_executable(_target: &str) -> Option<OsString> {
1192    None
1193}
1194
1195#[cfg(test)]
1196mod tests {
1197    use super::uses_named_pipe_jobserver;
1198    use super::Version;
1199
1200    #[test]
1201    fn test_cmake_version() {
1202        let text = "cmake version 3.22.2
1203
1204CMake suite maintained and supported by Kitware (kitware.com/cmake).
1205";
1206        let v = Version::parse(text).unwrap();
1207        assert_eq!(v, Version::new(3, 22));
1208        assert!(Version::new(3, 22) > Version::new(3, 21));
1209        assert!(Version::new(3, 22) < Version::new(3, 23));
1210
1211        let _v = Version::from_command("cmake".as_ref()).unwrap();
1212    }
1213
1214    #[test]
1215    fn test_uses_fifo_jobserver() {
1216        assert!(uses_named_pipe_jobserver(
1217            "-j --jobserver-auth=fifo:/foo".as_ref()
1218        ));
1219        assert!(!uses_named_pipe_jobserver(
1220            "-j --jobserver-auth=8:9".as_ref()
1221        ));
1222    }
1223}