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