extern crate bindgen;
use cmake::Config;
use std::env;
use std::error::Error;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(PartialEq)]
enum HostType {
Linux,
MacOS,
Windows,
Unknown,
}
#[derive(Debug)]
enum BuildError {
IoError(String),
OpenSSL(String),
Unknown(String),
}
impl Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildError::IoError(msg) => write!(f, "Failed to execute shell command: {}", msg),
BuildError::OpenSSL(msg) => write!(f, "OpenSSL Error: {}", msg),
BuildError::Unknown(msg) => write!(f, "Error: {}", msg),
}
}
}
impl Error for BuildError {}
fn build_mgclient_macos() -> Result<PathBuf, BuildError> {
println!("MacOS detected. We will check if you have either the MacPorts or Homebrew package managers.");
println!("Checking for MacPorts...");
let output = Command::new("/usr/bin/command")
.args(["-v", "port"])
.output()
.map_err(|err| BuildError::IoError(format!("'/usr/bin/command -v port': {}", err)))?
.stdout;
let port_path = String::from_utf8(output).unwrap();
if !port_path.is_empty() {
let port_path = &port_path[..port_path.len() - 1];
println!(
"'port' binary detected at {:?}. We assume MacPorts is installed and is your primary package manager.",
&port_path
);
let port_binary_path = Path::new(&port_path);
println!("Checking if the 'openssl' port is installed.");
let output = String::from_utf8(
Command::new(port_path)
.args(["installed", "openssl"])
.output()
.expect("Failed to execute shell command 'port installed openssl'")
.stdout,
)
.unwrap();
if output == "None of the specified ports are installed.\n" {
panic!("The openssl port does not seem to be installed! Please install it using 'port install openssl'.");
}
let openssl_lib_dir = port_binary_path
.ancestors()
.nth(2)
.unwrap()
.join("libexec")
.join("openssl3")
.join("lib");
println!(
"cargo:rustc-link-search=native={}",
openssl_lib_dir.display()
);
let path = Config::new("mgclient").build();
Ok(path)
} else {
println!("Macports not found.");
println!("Checking for Homebrew...");
let output = Command::new("/usr/bin/command")
.args(["-v", "brew"])
.output()
.map_err(|err| BuildError::IoError(format!("'/usr/bin/command -v brew': {}", err)))?
.stdout;
let brew_path = String::from_utf8(output).unwrap();
if brew_path.is_empty() {
println!("Homebrew not found.");
BuildError::Unknown(
"We did not detect either MacPorts or Homebrew on your machine. We cannot proceed."
.to_string(),
);
} else {
println!("'brew' executable detected at {:?}", &brew_path);
println!("Proceeding with installation assuming Homebrew is your package manager");
}
let path_openssl = if cfg!(target_arch = "aarch64") {
"/opt/homebrew/Cellar/openssl@3"
} else {
"/usr/local/Cellar/openssl@3"
};
println!("Found OpenSSL at path: {}", path_openssl);
let mut openssl_dirs = std::fs::read_dir(PathBuf::new().join(path_openssl))
.map_err(|err| {
BuildError::OpenSSL(format!("Failed to read OpenSSL directory: '{}'", err))
})?
.filter_map(|r| match r {
Ok(entry) => Some(entry.path()),
Err(err) => {
Err(BuildError::OpenSSL(format!(
"Failed to read directory entry: '{}'",
err
)))
.ok()
}
})
.collect::<Vec<PathBuf>>();
openssl_dirs.sort_by(|a, b| {
let a_time = a.metadata().unwrap().modified().unwrap();
let b_time = b.metadata().unwrap().modified().unwrap();
b_time.cmp(&a_time)
});
let openssl_root_path = openssl_dirs[0].clone();
println!(
"cargo:rustc-link-search=native={}",
openssl_root_path.join("lib").display()
);
let openssl_root = openssl_dirs[0].clone();
let path = Config::new("mgclient")
.define("OPENSSL_ROOT_DIR", format!("{}", openssl_root.display()))
.define(
"OPENSSL_INCLUDE_DIR",
format!("{}", openssl_root.join("include").display()),
)
.define(
"OPENSSL_CRYPTO_LIBRARY",
format!(
"{}",
openssl_root.join("lib").join("libcrypto.dylib").display()
),
)
.define(
"OPENSSL_SSL_LIBRARY",
format!(
"{}",
openssl_root.join("lib").join("libssl.dylib").display()
),
)
.build();
Ok(path)
}
}
fn build_mgclient_linux() -> Result<PathBuf, BuildError> {
let path = Config::new("mgclient").build();
Ok(path)
}
fn build_mgclient_windows() -> Result<PathBuf, BuildError> {
let openssl_lib_dir = PathBuf::from(
std::env::var("OPENSSL_LIB_DIR")
.unwrap_or_else(|_| "C:\\Program Files\\OpenSSL-Win64\\lib".to_string()),
);
let openssl_include_dir = PathBuf::from(
std::env::var("OPENSSL_INCLUDE_DIR")
.unwrap_or_else(|_| "C:\\Program Files\\OpenSSL-Win64\\include".to_string()),
);
let openssl_root_dir = openssl_lib_dir.parent().unwrap_or(&openssl_lib_dir);
println!(
"cargo:rustc-link-search=native={}",
openssl_lib_dir.display()
);
let is_vcpkg = openssl_lib_dir.to_string_lossy().contains("vcpkg");
let (crypto_lib, ssl_lib) = if is_vcpkg {
(
format!("{}\\libcrypto.lib", openssl_lib_dir.display()),
format!("{}\\libssl.lib", openssl_lib_dir.display()),
)
} else {
(
format!("{}\\libcrypto.lib", openssl_lib_dir.display()),
format!("{}\\libssl.lib", openssl_lib_dir.display()),
)
};
let mut config = Config::new("mgclient");
config
.define(
"OPENSSL_ROOT_DIR",
format!("{}", openssl_root_dir.display()),
)
.define(
"OPENSSL_INCLUDE_DIR",
format!("{}", openssl_include_dir.display()),
)
.define("OPENSSL_CRYPTO_LIBRARY", crypto_lib)
.define("OPENSSL_SSL_LIBRARY", ssl_lib);
if is_vcpkg {
config.define("OPENSSL_USE_STATIC_LIBS", "TRUE");
config.define("CMAKE_EXE_LINKER_FLAGS", "/DEFAULTLIB:crypt32.lib /DEFAULTLIB:ws2_32.lib /DEFAULTLIB:user32.lib /DEFAULTLIB:advapi32.lib");
config.define("CMAKE_SHARED_LINKER_FLAGS", "/DEFAULTLIB:crypt32.lib /DEFAULTLIB:ws2_32.lib /DEFAULTLIB:user32.lib /DEFAULTLIB:advapi32.lib");
}
let path = config.build();
Ok(path)
}
fn main() -> Result<(), BuildError> {
let host_type = if cfg!(target_os = "linux") {
HostType::Linux
} else if cfg!(target_os = "windows") {
HostType::Windows
} else if cfg!(target_os = "macos") {
HostType::MacOS
} else {
HostType::Unknown
};
let mgclient = PathBuf::new().join("mgclient");
let mgclient_out = match host_type {
HostType::Windows => build_mgclient_windows(),
HostType::MacOS => build_mgclient_macos(),
HostType::Linux => build_mgclient_linux(),
HostType::Unknown => Err(BuildError::Unknown("Unknown operating system".to_string())),
}?;
let mgclient_h = mgclient_out.join("include").join("mgclient.h");
let mgclient_export_h = mgclient_out.join("include").join("mgclient-export.h");
let mgclient_mgvalue_h = mgclient.join("src").join("mgvalue.h");
println!("cargo:rerun-if-changed={}", mgclient_h.display());
println!("cargo:rerun-if-changed={}", mgclient_export_h.display());
let bindings = bindgen::Builder::default()
.header(format!("{}", mgclient_h.display()))
.header(format!("{}", mgclient_export_h.display()))
.header(format!("{}", mgclient_mgvalue_h.display()))
.clang_arg(format!("-I{}", mgclient_out.join("include").display()))
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
let lib_dir = if Path::new(&mgclient_out.join("lib64")).exists() {
"lib64"
} else {
"lib"
};
println!(
"cargo:rustc-link-search=native={}",
mgclient_out.join(lib_dir).display()
);
println!("cargo:rustc-link-lib=static=mgclient");
match host_type {
HostType::Linux => {
println!("cargo:rustc-link-lib=dylib=crypto");
println!("cargo:rustc-link-lib=dylib=ssl");
}
HostType::Windows => {
let openssl_static = std::env::var("OPENSSL_STATIC").unwrap_or_default() == "1";
if openssl_static {
println!("cargo:rustc-link-lib=static=libcrypto");
println!("cargo:rustc-link-lib=static=libssl");
println!("cargo:rustc-link-lib=crypt32");
println!("cargo:rustc-link-lib=ws2_32");
println!("cargo:rustc-link-lib=user32");
println!("cargo:rustc-link-lib=advapi32");
} else {
println!("cargo:rustc-link-lib=dylib=libcrypto");
println!("cargo:rustc-link-lib=dylib=libssl");
}
}
HostType::MacOS => {
println!("cargo:rustc-link-lib=dylib=crypto");
println!("cargo:rustc-link-lib=dylib=ssl");
}
HostType::Unknown => panic!("Unknown operating system"),
}
Ok(())
}