1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//! Implements building a CMake-based C++ library.

use errors::Result;
use file_utils::{create_dir_all, path_to_str};
use utils::run_command;
use utils::MapIfOk;
use string_utils::JoinWithSeparator;
use std::process::Command;
use std::path::{Path, PathBuf};
use log;
use target;

/// A CMake variable with a name and a value.
#[derive(Debug, Clone)]
pub struct CMakeVar {
  pub name: String,
  pub value: String,
}
impl CMakeVar {
  /// Creates a new variable.
  pub fn new<S1: Into<String>, S2: Into<String>>(name: S1, value: S2) -> CMakeVar {
    CMakeVar {
      name: name.into(),
      value: value.into(),
    }
  }
  /// Creates a new variable containing a list of values.
  pub fn new_list<I, S, L>(name: S, values: L) -> Result<CMakeVar>
    where S: Into<String>,
          I: AsRef<str>,
          L: IntoIterator<Item = I>
  {
    let value = values
      .into_iter()
      .map_if_ok(|s| -> Result<_> {
        if s.as_ref().contains(';') {
          Err(format!("can't pass value to cmake because ';' symbol is reserved: {}",
                      s.as_ref())
                  .into())
        } else {
          Ok(s)
        }
      })?
      .into_iter()
      .join(";");
    Ok(CMakeVar::new(name, value))
  }

  /// Creates a new variable containing a list of paths.
  pub fn new_path_list<I, S, L>(name: S, paths: L) -> Result<CMakeVar>
    where S: Into<String>,
          I: AsRef<Path>,
          L: IntoIterator<Item = I>
  {
    CMakeVar::new_list(name,
                       paths
                         .into_iter()
                         .map_if_ok(|x| path_to_str(x.as_ref()).map(|x| x.to_string()))?)
  }
}

/// CMake build type (Debug or Release)
#[derive(Debug, Clone)]
pub enum BuildType {
  Debug,
  Release,
}

/// Implements building a CMake-based C++ library.
/// Construct a value and call `run()` to execute building.
#[derive(Debug, Clone)]
pub struct CppLibBuilder {
  /// Path to the source directory containing CMake config file
  pub cmake_source_dir: PathBuf,
  /// Path to the build directory (may not exist before building)
  pub build_dir: PathBuf,
  /// Path to the install directory (may not exist before building)
  pub install_dir: PathBuf,
  /// Number of threads used to build the library. If `None` is supplied,
  /// number of threads will be detected automatically.
  pub num_jobs: Option<usize>,
  /// CMake build type (Debug or Release)
  pub build_type: BuildType,
  /// Additional variables passed to CMake
  pub cmake_vars: Vec<CMakeVar>,
}

impl CppLibBuilder {
  /// Builds the library.
  pub fn run(self) -> Result<()> {
    if !self.build_dir.exists() {
      create_dir_all(&self.build_dir)?;
    }
    let mut cmake_command = Command::new("cmake");
    cmake_command
      .arg(self.cmake_source_dir)
      .current_dir(&self.build_dir);
    let actual_build_type = if target::current_env() == target::Env::Msvc {
      // Rust always links to release version of MSVC runtime, so
      // link will fail if C library is built in debug mode
      BuildType::Release
    } else {
      self.build_type
    };
    if target::current_os() == target::OS::Windows {
      match target::current_env() {
        target::Env::Msvc => {
          cmake_command.arg("-G").arg("NMake Makefiles");
        }
        target::Env::Gnu => {
          cmake_command.arg("-G").arg("MinGW Makefiles");
        }
        _ => {}
      }
    }
    let mut actual_cmake_vars = self.cmake_vars.clone();
    actual_cmake_vars.push(CMakeVar::new("CMAKE_BUILD_TYPE",
                                         match actual_build_type {
                                           BuildType::Release => "Release",
                                           BuildType::Debug => "Debug",
                                         }));
    actual_cmake_vars.push(CMakeVar::new("CMAKE_INSTALL_PREFIX", path_to_str(&self.install_dir)?));

    for var in actual_cmake_vars {
      cmake_command.arg(format!("-D{}={}", var.name, var.value));
    }
    run_command(&mut cmake_command)?;

    let mut make_command_name = if target::current_os() == target::OS::Windows {
      match target::current_env() {
        target::Env::Msvc => "nmake",
        target::Env::Gnu => "mingw32-make",
        _ => "make",
      }
    } else {
      "make"
    };

    let mut make_args = Vec::new();
    let num_jobs = if let Some(x) = self.num_jobs {
      x
    } else {
      ::num_cpus::get()
    };
    if target::current_env() == target::Env::Msvc && num_jobs > 1 {
      log::status("Checking for jom");
      if run_command(&mut Command::new("jom").arg("/version")).is_ok() {
        log::status("jom will be used instead of nmake.");
        make_command_name = "jom";
        make_args.push("/J".to_string());
        make_args.push(num_jobs.to_string());
      } else {
        log::status("jom not found in PATH. Using nmake.")
      }
    }
    if target::current_env() != target::Env::Msvc {
      make_args.push(format!("-j{}", num_jobs));
    }
    make_args.push("install".to_string());
    let mut make_command = Command::new(make_command_name);
    make_command.args(&make_args).current_dir(self.build_dir);
    run_command(&mut make_command)?;
    Ok(())
  }
}