diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e5b2ffc..51a7c4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,6 +12,16 @@ jobs: - uses: actions/checkout@master - name: Install Rust run: rustup update ${{ matrix.rust }} && rustup default ${{ matrix.rust }} + - name: Check versions + run: | + set -e + cargo --version + rustc --version + cmake --version + gcc --version + clang --version + echo "end of versions checking" + shell: bash - run: cargo test rustfmt: diff --git a/Cargo.toml b/Cargo.toml index b345171..9cc77e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,6 @@ categories = ["development-tools::build-utils"] [dependencies] cc = "1.0.72" + +[dev-dependencies] +tempfile = "3.1.0" diff --git a/src/lib.rs b/src/lib.rs index 18c2fdc..a0f9027 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,10 +50,11 @@ use std::collections::HashMap; use std::env; use std::ffi::{OsStr, OsString}; use std::fs::{self, File}; -use std::io::prelude::*; -use std::io::ErrorKind; +use std::io::{self, prelude::*, ErrorKind}; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, ExitStatus, Stdio}; +use std::sync::{Arc, Mutex}; +use std::thread::{self}; /// Builder style configuration for a pending CMake build. pub struct Config { @@ -105,6 +106,8 @@ pub fn build>(path: P) -> PathBuf { Config::new(path.as_ref()).build() } +static CMAKE_CACHE_FILE: &str = "CMakeCache.txt"; + impl Config { /// Return explicitly set profile or infer `CMAKE_BUILD_TYPE` from Rust's compilation profile. /// @@ -515,14 +518,15 @@ impl Config { let executable = self .getenv_target_os("CMAKE") .unwrap_or(OsString::from("cmake")); - let mut cmd = Command::new(&executable); + let mut conf_cmd = Command::new(&executable); + conf_cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); if self.verbose_cmake { - cmd.arg("-Wdev"); - cmd.arg("--debug-output"); + conf_cmd.arg("-Wdev"); + conf_cmd.arg("--debug-output"); } - cmd.arg(&self.path).current_dir(&build); + conf_cmd.arg(&self.path).current_dir(&build); let mut is_ninja = false; if let Some(ref generator) = generator { is_ninja = generator.to_string_lossy().contains("Ninja"); @@ -555,7 +559,7 @@ impl Config { (false, false) => fail("no valid generator found for GNU toolchain; MSYS or MinGW must be installed") }; - cmd.arg("-G").arg(generator); + conf_cmd.arg("-G").arg(generator); } } else { // If we're cross compiling onto windows, then set some @@ -563,7 +567,7 @@ impl Config { // systems may need the `windres` or `dlltool` variables set, so // set them if possible. if !self.defined("CMAKE_SYSTEM_NAME") { - cmd.arg("-DCMAKE_SYSTEM_NAME=Windows"); + conf_cmd.arg("-DCMAKE_SYSTEM_NAME=Windows"); } if !self.defined("CMAKE_RC_COMPILER") { let exe = find_exe(c_compiler.path()); @@ -573,7 +577,7 @@ impl Config { if windres.is_file() { let mut arg = OsString::from("-DCMAKE_RC_COMPILER="); arg.push(&windres); - cmd.arg(arg); + conf_cmd.arg(arg); } } } @@ -584,7 +588,9 @@ impl Config { // This also guarantees that NMake generator isn't chosen implicitly. let using_nmake_generator; if generator.is_none() { - cmd.arg("-G").arg(self.visual_studio_generator(&target)); + conf_cmd + .arg("-G") + .arg(self.visual_studio_generator(&target)); using_nmake_generator = false; } else { using_nmake_generator = generator.as_ref().unwrap() == "NMake Makefiles"; @@ -592,19 +598,19 @@ impl Config { if !is_ninja && !using_nmake_generator { if target.contains("x86_64") { if self.generator_toolset.is_none() { - cmd.arg("-Thost=x64"); + conf_cmd.arg("-Thost=x64"); } - cmd.arg("-Ax64"); + conf_cmd.arg("-Ax64"); } else if target.contains("thumbv7a") { if self.generator_toolset.is_none() { - cmd.arg("-Thost=x64"); + conf_cmd.arg("-Thost=x64"); } - cmd.arg("-Aarm"); + conf_cmd.arg("-Aarm"); } else if target.contains("aarch64") { if self.generator_toolset.is_none() { - cmd.arg("-Thost=x64"); + conf_cmd.arg("-Thost=x64"); } - cmd.arg("-AARM64"); + conf_cmd.arg("-AARM64"); } else if target.contains("i686") { use cc::windows_registry::{find_vs_version, VsVers}; match find_vs_version() { @@ -613,9 +619,9 @@ impl Config { // but Visual Studio 2019 changed the default toolset to match the host, // so we need to manually override it for x86 targets if self.generator_toolset.is_none() { - cmd.arg("-Thost=x86"); + conf_cmd.arg("-Thost=x86"); } - cmd.arg("-AWin32"); + conf_cmd.arg("-AWin32"); } _ => {} }; @@ -625,24 +631,24 @@ impl Config { } } else if target.contains("redox") { if !self.defined("CMAKE_SYSTEM_NAME") { - cmd.arg("-DCMAKE_SYSTEM_NAME=Generic"); + conf_cmd.arg("-DCMAKE_SYSTEM_NAME=Generic"); } } else if target.contains("solaris") { if !self.defined("CMAKE_SYSTEM_NAME") { - cmd.arg("-DCMAKE_SYSTEM_NAME=SunOS"); + conf_cmd.arg("-DCMAKE_SYSTEM_NAME=SunOS"); } } else if target.contains("apple-ios") || target.contains("apple-tvos") { // These two flags prevent CMake from adding an OSX sysroot, which messes up compilation. if !self.defined("CMAKE_OSX_SYSROOT") && !self.defined("CMAKE_OSX_DEPLOYMENT_TARGET") { - cmd.arg("-DCMAKE_OSX_SYSROOT=/"); - cmd.arg("-DCMAKE_OSX_DEPLOYMENT_TARGET="); + conf_cmd.arg("-DCMAKE_OSX_SYSROOT=/"); + conf_cmd.arg("-DCMAKE_OSX_DEPLOYMENT_TARGET="); } } if let Some(ref generator) = generator { - cmd.arg("-G").arg(generator); + conf_cmd.arg("-G").arg(generator); } if let Some(ref generator_toolset) = self.generator_toolset { - cmd.arg("-T").arg(generator_toolset); + conf_cmd.arg("-T").arg(generator_toolset); } let profile = self.get_profile().to_string(); for &(ref k, ref v) in &self.defines { @@ -650,13 +656,13 @@ impl Config { os.push(k); os.push("="); os.push(v); - cmd.arg(os); + conf_cmd.arg(os); } if !self.defined("CMAKE_INSTALL_PREFIX") { let mut dstflag = OsString::from("-DCMAKE_INSTALL_PREFIX="); dstflag.push(&dst); - cmd.arg(dstflag); + conf_cmd.arg(dstflag); } let build_type = self @@ -691,7 +697,7 @@ impl Config { flagsflag.push(" "); flagsflag.push(arg); } - cmd.arg(flagsflag); + conf_cmd.arg(flagsflag); } // The visual studio generator apparently doesn't respect @@ -715,7 +721,7 @@ impl Config { flagsflag.push(" "); flagsflag.push(arg); } - cmd.arg(flagsflag); + conf_cmd.arg(flagsflag); } } @@ -754,7 +760,7 @@ impl Config { .collect::>(); ccompiler = OsString::from_wide(&wchars); } - cmd.arg(ccompiler); + conf_cmd.arg(ccompiler); } }; @@ -764,31 +770,38 @@ impl Config { } if !self.defined("CMAKE_BUILD_TYPE") { - cmd.arg(&format!("-DCMAKE_BUILD_TYPE={}", profile)); + conf_cmd.arg(&format!("-DCMAKE_BUILD_TYPE={}", profile)); } if self.verbose_make { - cmd.arg("-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON"); + conf_cmd.arg("-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON"); } for &(ref k, ref v) in c_compiler.env().iter().chain(&self.env) { - cmd.env(k, v); + conf_cmd.env(k, v); } - if self.always_configure || !build.join("CMakeCache.txt").exists() { - cmd.args(&self.configure_args); - run(cmd.env("CMAKE_PREFIX_PATH", cmake_prefix_path), "cmake"); + conf_cmd.env("CMAKE_PREFIX_PATH", cmake_prefix_path); + conf_cmd.args(&self.configure_args); + if self.always_configure || !build.join(CMAKE_CACHE_FILE).exists() { + run_cmake_action( + &build, + CMakeAction::Configure { + conf_cmd: &mut conf_cmd, + }, + ); } else { println!("CMake project was already configured. Skipping configuration step."); } // And build! let target = self.cmake_target.clone().unwrap_or("install".to_string()); - let mut cmd = Command::new(&executable); - cmd.current_dir(&build); + let mut build_cmd = Command::new(&executable); + build_cmd.current_dir(&build); + build_cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); for &(ref k, ref v) in c_compiler.env().iter().chain(&self.env) { - cmd.env(k, v); + build_cmd.env(k, v); } // If the generated project is Makefile based we should carefully transfer corresponding CARGO_MAKEFLAGS @@ -806,30 +819,36 @@ impl Config { || cfg!(target_os = "bitrig") || cfg!(target_os = "dragonflybsd")) => { - cmd.env("MAKEFLAGS", makeflags); + build_cmd.env("MAKEFLAGS", makeflags); } _ => {} } } - cmd.arg("--build").arg("."); + build_cmd.arg("--build").arg("."); if !self.no_build_target { - cmd.arg("--target").arg(target); + build_cmd.arg("--target").arg(target); } - cmd.arg("--config").arg(&profile); + build_cmd.arg("--config").arg(&profile); if let Ok(s) = env::var("NUM_JOBS") { // See https://cmake.org/cmake/help/v3.12/manual/cmake.1.html#build-tool-mode - cmd.arg("--parallel").arg(s); + build_cmd.arg("--parallel").arg(s); } if !&self.build_args.is_empty() { - cmd.arg("--").args(&self.build_args); + build_cmd.arg("--").args(&self.build_args); } - run(&mut cmd, "cmake"); + run_cmake_action( + &build, + CMakeAction::Build { + build_cmd: &mut build_cmd, + conf_cmd: &mut conf_cmd, + }, + ); println!("cargo:root={}", dst.display()); return dst; @@ -904,7 +923,7 @@ impl Config { // isn't relevant to us but we canonicalize it here to ensure // we're both checking the same thing. let path = fs::canonicalize(&self.path).unwrap_or(self.path.clone()); - let mut f = match File::open(dir.join("CMakeCache.txt")) { + let mut f = match File::open(dir.join(CMAKE_CACHE_FILE)) { Ok(f) => f, Err(..) => return, }; @@ -937,9 +956,118 @@ impl Config { } } +enum CMakeAction<'a> { + Configure { + conf_cmd: &'a mut Command, + }, + Build { + conf_cmd: &'a mut Command, + build_cmd: &'a mut Command, + }, +} + +fn run_cmake_action(build_dir: &Path, mut action: CMakeAction) { + let program = "cmake"; + let cmd = match &mut action { + CMakeAction::Configure { conf_cmd } => conf_cmd, + CMakeAction::Build { build_cmd, .. } => build_cmd, + }; + let need_rerun = match run_and_check_if_need_reconf(*cmd, program) { + Ok(x) => x, + Err(err) => { + handle_cmake_exec_result(Err(err), program); + return; + } + }; + if need_rerun { + println!("Looks like toolchain was changed"); + //just in case some wrong value was cached + let _ = fs::remove_file(&build_dir.join(CMAKE_CACHE_FILE)); + match action { + CMakeAction::Configure { conf_cmd } => run(conf_cmd, program), + CMakeAction::Build { + conf_cmd, + build_cmd, + } => { + run(conf_cmd, program); + run(build_cmd, program); + } + } + } +} + +// Acording to +// https://gitlab.kitware.com/cmake/cmake/-/issues/18959 +// CMake does not support usage of the same build directory for different +// compilers. The problem is that we can not make sure that we use the same compiler +// before running of CMake without CMake's logic duplication (for example consider +// usage of CMAKE_TOOLCHAIN_FILE). Fortunately for us, CMake can detect is +// compiler changed by itself. This is done for interactive CMake's configuration, +// like ccmake/cmake-gui. But after compiler change CMake resets all cached variables. +fn run_and_check_if_need_reconf(cmd: &mut Command, program: &str) -> Result { + println!("running: {:?}", cmd); + let mut child = cmd.spawn()?; + let mut child_stderr = child.stderr.take().expect("Internal error no stderr"); + let full_stderr = Arc::new(Mutex::new(Vec::::with_capacity(1024))); + let full_stderr2 = full_stderr.clone(); + let stderr_thread = thread::spawn(move || { + let mut full_stderr = full_stderr2 + .lock() + .expect("Internal error: Lock of stderr buffer failed"); + log_and_copy_stream(&mut child_stderr, &mut io::stderr(), &mut full_stderr) + }); + + let mut child_stdout = child.stdout.take().expect("Internal error no stdout"); + let mut full_stdout = Vec::with_capacity(1024); + log_and_copy_stream(&mut child_stdout, &mut io::stdout(), &mut full_stdout)?; + stderr_thread + .join() + .expect("Internal stderr thread join failed")?; + + static RESET_MSG: &[u8] = b"Configure will be re-run and you may have to reset some variables"; + let full_stderr = full_stderr + .lock() + .expect("Internal error stderr lock failed"); + if contains(&full_stderr, RESET_MSG) || contains(&full_stdout, RESET_MSG) { + return Ok(true); + } else { + handle_cmake_exec_result(child.wait(), program); + return Ok(false); + } +} + fn run(cmd: &mut Command, program: &str) { println!("running: {:?}", cmd); - let status = match cmd.status() { + handle_cmake_exec_result(cmd.status(), program); +} + +fn contains(haystack: &[u8], needle: &[u8]) -> bool { + haystack + .windows(needle.len()) + .any(|window| window == needle) +} + +fn log_and_copy_stream( + reader: &mut R, + writer: &mut W, + log: &mut Vec, +) -> io::Result<()> { + let mut buf = [0; 80]; + loop { + let len = match reader.read(&mut buf) { + Ok(0) => break, + Ok(len) => len, + Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + }; + log.extend_from_slice(&buf[0..len]); + writer.write_all(&buf[0..len])?; + } + Ok(()) +} + +fn handle_cmake_exec_result(r: Result, program: &str) { + let status = match r { Ok(status) => status, Err(ref e) if e.kind() == ErrorKind::NotFound => { fail(&format!( diff --git a/tests/handling_reset_cache_after_set_var.rs b/tests/handling_reset_cache_after_set_var.rs new file mode 100644 index 0000000..7f87549 --- /dev/null +++ b/tests/handling_reset_cache_after_set_var.rs @@ -0,0 +1,68 @@ +extern crate tempfile; + +use std::{env, error::Error, fs, path::Path}; + +#[test] +fn handling_reset_cache_after_set_var() { + let tmp_dir = tempfile::tempdir().unwrap(); + + fill_dir(tmp_dir.path()).unwrap(); + + if cfg!(all( + target_os = "linux", + target_arch = "x86_64", + target_vendor = "unknown" + )) { + env::set_var("TARGET", "x86_64-unknown-linux-gnu"); + env::set_var("HOST", "x86_64-unknown-linux-gnu"); + env::set_var("OUT_DIR", tmp_dir.path()); + env::set_var("PROFILE", "debug"); + env::set_var("OPT_LEVEL", "0"); + env::set_var("DEBUG", "true"); + let dst = cmake::Config::new(tmp_dir.path()) + .define("OPT1", "False") + .build_target("all") + .build() + .join("build"); + + assert!(fs::read_to_string(dst.join("CMakeCache.txt")) + .unwrap() + .contains("OPT1:BOOL=False")); + env::set_var("CC", "clang"); + env::set_var("CXX", "clang++"); + let dst = cmake::Config::new(tmp_dir.path()) + .define("OPT1", "False") + .build_target("all") + .build() + .join("build"); + + assert!(fs::read_to_string(dst.join("CMakeCache.txt")) + .unwrap() + .contains("OPT1:BOOL=False")); + } +} + +fn fill_dir(tmp_dir: &Path) -> Result<(), Box> { + fs::write( + tmp_dir.join("CMakeLists.txt"), + r#" +project(xyz) +cmake_minimum_required(VERSION 3.9) + +option(OPT1 "some option" ON) +add_executable(xyz main.cpp) +"#, + )?; + fs::write( + tmp_dir.join("main.cpp"), + r#" +#include +#define DO_STRINGIFY(x) #x +#define STRINGIFY(x) DO_STRINGIFY(x) +int main() { + printf("option: %s\n", STRINGIFY(OPT1)); +} +"#, + )?; + Ok(()) +}