//! A build dependency for Cargo libraries to find libraries in a //! [Vcpkg](https://github.com/Microsoft/vcpkg) tree. //! //! Note: You must set one of `RUSTFLAGS=-Ctarget-feature=+crt-static` or //! `VCPKGRS_DYNAMIC=1` in your environment or the vcpkg-rs helper //! will not find any libraries. If `VCPKGRS_DYNAMIC` is set, `cargo install` will //! generate dynamically linked binaries, in which case you will have to arrange for //! dlls from your Vcpkg installation to be available in your path. //! //! The simplest possible usage for a library whose Vcpkg port name matches the //! name of the lib and DLL that are being looked for looks like this :- //! //! ```rust,no_run //! vcpkg::probe_package("libssh2").unwrap(); //! ``` //! //! In practice the .lib and .dll often differ in name from the package itself, //! in which case the library names must be specified, like this :- //! //! ```rust,no_run //! vcpkg::Config::new() //! .lib_names("zlib","zlib1") //! .probe("zlib").unwrap(); //! ``` //! //! If the search was successful all appropriate Cargo metadata will be printed //! on stdout. //! //! The decision to choose static variants of libraries is driven by adding //! `RUSTFLAGS=-Ctarget-feature=+crt-static` to the environment. This requires //! a nightly compiler but is scheduled to be stable in rustc 1.19. //! //! A number of environment variables are available to globally configure which //! libraries are selected. //! //! * `VCPKG_ROOT` - Set the directory to look in for a vcpkg installation. If //! it is not set, vcpkg will use the user-wide installation if one has been //! set up with `vcpkg integrate install` //! //! * `VCPKGRS_NO_FOO` - if set, vcpkg-rs will not attempt to find the //! library named `foo`. //! //! * `VCPKGRS_DISABLE` - if set, vcpkg-rs will not attempt to find any libraries. //! //! * `VCPKGRS_DYNAMIC` - if set, vcpkg-rs will link to DLL builds of ports. //! //! There is a companion crate `vcpkg_cli` that allows testing of environment //! and flag combinations. //! //! ```Batchfile //! C:\src> vcpkg_cli probe -l static mysqlclient //! Found library mysqlclient //! Include paths: //! C:\src\diesel_build\vcpkg-dll\installed\x64-windows-static\include //! Library paths: //! C:\src\diesel_build\vcpkg-dll\installed\x64-windows-static\lib //! Cargo metadata: //! cargo:rustc-link-search=native=C:\src\diesel_build\vcpkg-dll\installed\x64-windows-static\lib //! cargo:rustc-link-lib=static=mysqlclient //! ``` use std::ascii::AsciiExt; use std::env; use std::error; use std::fs::{self, File}; use std::fmt; use std::io::{BufRead, BufReader}; use std::path::{PathBuf, Path}; // #[derive(Clone)] pub struct Config { /// should the cargo metadata actually be emitted cargo_metadata: bool, /// should cargo:include= metadata be emitted (defaults to false) emit_includes: bool, /// libs that must be be found for probing to be considered successful required_libs: Vec, /// should DLLs be copies to OUT_DIR? copy_dlls: bool, } #[derive(Debug)] pub struct Library { /// Paths for the linker to search for static or import libraries pub link_paths: Vec, /// Paths to search at runtme to find DLLs pub dll_paths: Vec, /// Paths to search for pub include_paths: Vec, /// cargo: metadata lines pub cargo_metadata: Vec, /// libraries found are static pub is_static: bool, // DLLs found pub found_dlls: Vec, // static libs or import libs found pub found_libs: Vec, } enum MSVCTarget { X86, X64, } impl fmt::Display for MSVCTarget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { MSVCTarget::X86 => write!(f, "x86-windows"), MSVCTarget::X64 => write!(f, "x64-windows"), } } } #[derive(Debug)] // need Display? pub enum Error { /// Aborted because of a `VCPKGRS_NO_*` environment variable. /// /// Contains the name of the responsible environment variable. DisabledByEnv(String), /// Aborted because a required environment variable was not set. RequiredEnvMissing(String), /// Only MSVC ABI is supported NotMSVC, /// Can't find a vcpkg tree VcpkgNotFound(String), /// Library not found in vcpkg tree LibNotFound(String), #[doc(hidden)] __Nonexhaustive, } impl error::Error for Error { fn description(&self) -> &str { match *self { Error::DisabledByEnv(_) => "vcpkg-rs requested to be aborted", Error::RequiredEnvMissing(_) => "a required env setting is missing", Error::NotMSVC => "vcpkg-rs only can only find libraries for MSVC ABI 64 bit builds", Error::VcpkgNotFound(_) => "could not find Vcpkg tree", Error::LibNotFound(_) => "could not find library in Vcpkg tree", // Error::LibNotFound(_) => "could not find library in vcpkg tree", Error::__Nonexhaustive => panic!(), } } fn cause(&self) -> Option<&error::Error> { match *self { // Error::Command { ref cause, .. } => Some(cause), _ => None, } } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match *self { Error::DisabledByEnv(ref name) => write!(f, "Aborted because {} is set", name), Error::RequiredEnvMissing(ref name) => write!(f, "Aborted because {} is not set", name), Error::NotMSVC => { write!(f, "the vcpkg-rs Vcpkg build helper can only find libraries built for the MSVC ABI.") } Error::VcpkgNotFound(ref detail) => write!(f, "Could not find Vcpkg tree: {}", detail), Error::LibNotFound(ref detail) => { write!(f, "Could not find library in Vcpkg tree {}", detail) } Error::__Nonexhaustive => panic!(), } } } pub fn probe_package(name: &str) -> Result { Config::new().probe(name) } fn find_vcpkg_root() -> Result { // prefer the setting from the environment is there is one if let Some(path) = env::var_os("VCPKG_ROOT") { return Ok(PathBuf::from(path)); } // see if there is a per-user vcpkg tree that has been integrated into msbuild // using `vcpkg integrate install` let local_app_data = try!(env::var("LOCALAPPDATA").map_err(|_| { Error::VcpkgNotFound("Failed to read LOCALAPPDATA environment variable".to_string()) })); // not present or can't utf8 let vcpkg_user_targets_path = Path::new(local_app_data.as_str()) .join("vcpkg") .join("vcpkg.user.targets"); let file = try!(File::open(vcpkg_user_targets_path.clone()).map_err(|_| { Error::VcpkgNotFound("No vcpkg.user.targets found. Set the VCPKG_ROOT environment \ variable or run 'vcpkg integrate install'".to_string()) })); let file = BufReader::new(&file); for line in file.lines() { let line = try!(line.map_err(|_| { Error::VcpkgNotFound(format!("Parsing of {} failed.", vcpkg_user_targets_path .to_string_lossy() .to_owned())) })); let mut split = line.split("Project=\""); split.next(); // eat anything before Project=" if let Some(found) = split.next() { // " is illegal in a Windows pathname if let Some(found) = found.split_terminator("\"").next() { let mut vcpkg_root = PathBuf::from(found); if !(vcpkg_root.pop() && vcpkg_root.pop() && vcpkg_root.pop() && vcpkg_root.pop()) { return Err(Error::VcpkgNotFound(format!("Could not find vcpkg root above {}", found))); } return Ok(vcpkg_root); } } } Err(Error::VcpkgNotFound(format!("Project location not found parsing {}.", vcpkg_user_targets_path.to_string_lossy().to_owned()))) } fn validate_vcpkg_root(path: &PathBuf) -> Result<(), Error> { let mut vcpkg_root_path = path.clone(); vcpkg_root_path.push(".vcpkg-root"); if vcpkg_root_path.exists() { Ok(()) } else { Err(Error::VcpkgNotFound(format!("Could not find Vcpkg root at {}", vcpkg_root_path.to_string_lossy()))) } } /// names of the libraries struct LibNames { lib_stem: String, dll_stem: String, } fn find_vcpkg_target(msvc_target: &MSVCTarget) -> Result { let vcpkg_root = try!(find_vcpkg_root()); try!(validate_vcpkg_root(&vcpkg_root)); let static_lib = env::var("CARGO_CFG_TARGET_FEATURE") .unwrap_or(String::new()) .contains("crt-static"); let mut base = vcpkg_root; base.push("installed"); let static_appendage = if static_lib { "-static" } else { "" }; let vcpkg_triple = format!("{}{}", msvc_target.to_string(), static_appendage); base.push(vcpkg_triple); let lib_path = base.join("lib"); let bin_path = base.join("bin"); let include_path = base.join("include"); Ok(VcpkgTarget { // vcpkg_triple: vcpkg_triple, lib_path: lib_path, bin_path: bin_path, include_path: include_path, is_static: static_lib, }) } /// paths and triple for the chosen target struct VcpkgTarget { // vcpkg_triple: String, lib_path: PathBuf, bin_path: PathBuf, include_path: PathBuf, is_static: bool, } impl Config { pub fn new() -> Config { Config { cargo_metadata: true, emit_includes: false, required_libs: Vec::new(), copy_dlls: true, } } /// Override the name of the library to look for if it differs from the package name. /// /// This may be called more than once if multiple libs are required. /// All libs must be found for the probe to succeed. `.probe()` must /// be run with a different configuration to look for libraries under one of several names. /// `.libname("ssleay32")` will look for ssleay32.lib and also ssleay32.dll if /// dynamic linking is selected. pub fn lib_name(&mut self, lib_stem: &str) -> &mut Config { self.required_libs .push(LibNames { lib_stem: lib_stem.to_owned(), dll_stem: lib_stem.to_owned(), }); self } /// Override the name of the library to look for if it differs from the package name. /// /// This may be called more than once if multiple libs are required. /// All libs must be found for the probe to succeed. `.probe()` must /// be run with a different configuration to look for libraries under one of several names. /// `.lib_names("libcurl_imp","curl")` will look for libcurl_imp.lib and also curl.dll if /// dynamic linking is selected. pub fn lib_names(&mut self, lib_stem: &str, dll_stem: &str) -> &mut Config { self.required_libs .push(LibNames { lib_stem: lib_stem.to_owned(), dll_stem: dll_stem.to_owned(), }); self } /// Define whether metadata should be emitted for cargo allowing it to /// automatically link the binary. Defaults to `true`. pub fn cargo_metadata(&mut self, cargo_metadata: bool) -> &mut Config { self.cargo_metadata = cargo_metadata; self } /// Define cargo:include= metadata should be emitted. Defaults to `false`. pub fn emit_includes(&mut self, emit_includes: bool) -> &mut Config { self.emit_includes = emit_includes; self } /// Should DLLs be copied to OUT_DIR? /// Defaults to `true`. pub fn copy_dlls(&mut self, copy_dlls: bool) -> &mut Config { self.copy_dlls = copy_dlls; self } /// Find the library `port_name` in a Vcpkg tree. /// /// This will use all configuration previously set to select the /// architecture and linkage. pub fn probe(&mut self, port_name: &str) -> Result { // determine the target type, bailing out if it is not some // kind of msvc let msvc_target = try!(msvc_target()); // bail out if requested to not try at all if env::var_os("VCPKGRS_DISABLE").is_some() { return Err(Error::DisabledByEnv("VCPKGRS_DISABLE".to_owned())); } // bail out if requested to not try at all (old) if env::var_os("NO_VCPKG").is_some() { return Err(Error::DisabledByEnv("NO_VCPKG".to_owned())); } // bail out if requested to skip this package let abort_var_name = format!("VCPKGRS_NO_{}", envify(port_name)); if env::var_os(&abort_var_name).is_some() { return Err(Error::DisabledByEnv(abort_var_name)); } // bail out if requested to skip this package (old) let abort_var_name = format!("{}_NO_VCPKG", envify(port_name)); if env::var_os(&abort_var_name).is_some() { return Err(Error::DisabledByEnv(abort_var_name)); } // if no overrides have been selected, then the Vcpkg port name // is the the .lib name and the .dll name if self.required_libs.is_empty() { self.required_libs .push(LibNames { lib_stem: port_name.to_owned(), dll_stem: port_name.to_owned(), }); } let vcpkg_target = try!(find_vcpkg_target(&msvc_target)); // require explicit opt-in before using dynamically linked // variants, otherwise cargo install of various things will // stop working if Vcpkg is installed. if !vcpkg_target.is_static && !env::var_os("VCPKGRS_DYNAMIC").is_some() { return Err(Error::RequiredEnvMissing("VCPKGRS_DYNAMIC".to_owned())); } let mut lib = Library::new(vcpkg_target.is_static); if self.emit_includes { lib.cargo_metadata .push(format!("cargo:include={}", vcpkg_target.include_path.display())); } lib.include_paths.push(vcpkg_target.include_path); lib.cargo_metadata .push(format!("cargo:rustc-link-search=native={}", vcpkg_target .lib_path .to_str() .expect("failed to convert string type"))); lib.link_paths.push(vcpkg_target.lib_path.clone()); if !vcpkg_target.is_static { lib.cargo_metadata .push(format!("cargo:rustc-link-search=native={}", vcpkg_target .bin_path .to_str() .expect("failed to convert string type"))); // this path is dropped by recent version of cargo hence the copies to OUT_DIR below lib.dll_paths.push(vcpkg_target.bin_path.clone()); } drop(port_name); for required_lib in &self.required_libs { if vcpkg_target.is_static { lib.cargo_metadata .push(format!("cargo:rustc-link-lib=static={}", required_lib.lib_stem)); } else { lib.cargo_metadata .push(format!("cargo:rustc-link-lib={}", required_lib.lib_stem)); } // verify that the library exists let mut lib_location = vcpkg_target.lib_path.clone(); lib_location.push(required_lib.lib_stem.clone()); lib_location.set_extension("lib"); if !lib_location.exists() { return Err(Error::LibNotFound(lib_location.display().to_string())); } lib.found_libs.push(lib_location); // verify that the DLL exists if !vcpkg_target.is_static { let mut lib_location = vcpkg_target.bin_path.clone(); lib_location.push(required_lib.dll_stem.clone()); lib_location.set_extension("dll"); if !lib_location.exists() { return Err(Error::LibNotFound(lib_location.display().to_string())); } lib.found_dlls.push(lib_location); } } if self.copy_dlls { if let Some(target_dir) = env::var_os("OUT_DIR") { if !lib.found_dlls.is_empty() { for file in &lib.found_dlls { let mut dest_path = Path::new(target_dir.as_os_str()).to_path_buf(); dest_path.push(Path::new(file.file_name().unwrap())); try!(fs::copy(file, &dest_path).map_err(|_| { Error::LibNotFound(format!("Can't copy file {} to {}", file.to_string_lossy(), dest_path.to_string_lossy())) })); println!("vcpkg build helper copied {} to {}", file.to_string_lossy(), dest_path.to_string_lossy()); } lib.cargo_metadata .push(format!("cargo:rustc-link-search=native={}", env::var("OUT_DIR").unwrap())); // work around https://github.com/rust-lang/cargo/issues/3957 lib.cargo_metadata .push(format!("cargo:rustc-link-search={}", env::var("OUT_DIR").unwrap())); } } else { return Err(Error::LibNotFound("Unable to get OUT_DIR".to_owned())); } } if self.cargo_metadata { for line in &lib.cargo_metadata { println!("{}", line); } } Ok(lib) } } impl Library { pub fn new(is_static: bool) -> Library { Library { link_paths: Vec::new(), dll_paths: Vec::new(), include_paths: Vec::new(), cargo_metadata: Vec::new(), is_static: is_static, found_dlls: Vec::new(), found_libs: Vec::new(), } } } fn envify(name: &str) -> String { name.chars() .map(|c| c.to_ascii_uppercase()) .map(|c| if c == '-' { '_' } else { c }) .collect() } fn msvc_target() -> Result { let target = env::var("TARGET").unwrap_or(String::new()); if !target.contains("-pc-windows-msvc") { Err(Error::NotMSVC) } else if target.starts_with("x86_64-") { Ok(MSVCTarget::X64) } else { // everything else is x86 Ok(MSVCTarget::X86) } }