use std::io::Write;
use std::path::PathBuf;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use once_cell::sync::Lazy;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use regex::Regex;
use tempfile::TempDir;
mod cmake;
mod version;
pub use cmake::{find_cmake, CMakeProgram, Error, CMAKE_MIN_VERSION};
pub use version::{Version, VersionError};
#[derive(Debug)]
pub struct CMakePackage {
cmake: CMakeProgram,
working_directory: TempDir,
verbose: bool,
pub name: String,
pub version: Option<Version>,
pub components: Option<Vec<String>>,
pub names: Option<Vec<String>>,
}
impl CMakePackage {
fn new(
cmake: CMakeProgram,
working_directory: TempDir,
name: String,
version: Option<Version>,
components: Option<Vec<String>>,
names: Option<Vec<String>>,
verbose: bool,
) -> Self {
Self {
cmake,
working_directory,
name,
version,
components,
names,
verbose,
}
}
pub fn target(&self, target: impl Into<String>) -> Option<CMakeTarget> {
cmake::find_target(self, target)
}
pub fn target_property(
&self,
target: &CMakeTarget,
property: impl Into<String>,
) -> Option<String> {
cmake::target_property(self, target, property)
}
}
#[derive(Debug, Default, Clone)]
pub struct CMakeTarget {
pub name: String,
pub compile_definitions: Vec<String>,
pub compile_options: Vec<String>,
pub include_directories: Vec<String>,
pub link_directories: Vec<String>,
pub link_libraries: Vec<String>,
pub link_options: Vec<String>,
pub location: Option<String>,
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn link_name(lib: &str) -> &str {
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"lib([^/]+)\.(?:so|dylib|a).*").unwrap());
match RE.captures(lib) {
Some(captures) => captures.get(1).map(|f| f.as_str()).unwrap_or(lib),
None => lib,
}
}
#[cfg(target_os = "windows")]
fn link_name(lib: &str) -> &str {
lib
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn link_dir(lib: &str) -> Option<&str> {
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(.*)/lib[^/]+\.(?:so|dylib|a).*").unwrap());
RE.captures(lib)?.get(1).map(|f| f.as_str())
}
#[cfg(target_os = "windows")]
fn link_dir(_lib: &str) -> Option<&str> {
None
}
impl CMakeTarget {
pub fn link(&self) {
self.link_write(&mut std::io::stdout());
}
fn link_write<W: Write>(&self, io: &mut W) {
self.link_directories.iter().for_each(|dir| {
writeln!(io, "cargo:rustc-link-search=native={}", dir).unwrap();
});
self.link_options.iter().for_each(|opt| {
writeln!(io, "cargo:rustc-link-arg={}", opt).unwrap();
});
self.link_libraries.iter().for_each(|lib| {
if lib.starts_with("-") {
writeln!(io, "cargo:rustc-link-arg={}", lib).unwrap();
} else {
let kind = if lib.ends_with(".a") { "static" } else { "dylib" };
writeln!(io, "cargo:rustc-link-lib={}={}", kind, link_name(lib)).unwrap();
}
if let Some(lib) = link_dir(lib) {
writeln!(io, "cargo:rustc-link-search=native={}", lib).unwrap();
}
});
}
}
#[derive(Debug, Clone)]
pub struct FindPackageBuilder {
name: String,
version: Option<Version>,
components: Option<Vec<String>>,
names: Option<Vec<String>>,
verbose: bool,
prefix_paths: Option<Vec<PathBuf>>,
defines: Vec<(String, String)>,
}
impl FindPackageBuilder {
fn new(name: String) -> Self {
Self {
name,
version: None,
components: None,
names: None,
verbose: false,
prefix_paths: None,
defines: Vec::new(),
}
}
pub fn version(self, version: impl TryInto<Version>) -> Self {
Self {
version: Some(
version
.try_into()
.unwrap_or_else(|_| panic!("Invalid version specified!")),
),
..self
}
}
pub fn components(self, components: impl Into<Vec<String>>) -> Self {
Self {
components: Some(components.into()),
..self
}
}
pub fn names<S, I>(self, names: I) -> Self
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
let names: Vec<_> = names.into_iter().map(Into::into).collect();
if names.is_empty() {
return self;
}
Self {
names: Some(names),
..self
}
}
pub fn verbose(self) -> Self {
Self {
verbose: true,
..self
}
}
pub fn define(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.defines.push((key.into(), value.into()));
self
}
pub fn prefix_paths(self, prefix_paths: Vec<PathBuf>) -> Self {
Self {
prefix_paths: Some(prefix_paths),
..self
}
}
pub fn find(self) -> Result<CMakePackage, cmake::Error> {
cmake::find_package(
self.name,
self.version,
self.components,
self.names,
self.verbose,
self.prefix_paths,
self.defines,
)
}
}
pub fn find_package(name: impl Into<String>) -> FindPackageBuilder {
FindPackageBuilder::new(name.into())
}
#[cfg(test)]
mod testing {
use super::*;
#[test]
fn test_find_package() {
let package = find_package("totallynonexistentpackage").find();
match package {
Ok(_) => panic!("Package should not be found"),
Err(cmake::Error::PackageNotFound) => (),
Err(err) => panic!("Unexpected error: {:?}", err),
}
}
#[test]
fn test_find_package_with_version() {
let package = find_package("foo").version("1.0").find();
match package {
Ok(_) => panic!("Package should not be found"),
Err(cmake::Error::PackageNotFound) => (),
Err(err) => panic!("Unexpected error: {:?}", err),
}
}
#[test]
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn test_link_to() {
let target = CMakeTarget {
name: "foo".into(),
compile_definitions: vec![],
compile_options: vec![],
include_directories: vec![],
link_directories: vec!["/usr/lib64".into()],
link_libraries: vec!["/usr/lib/libbar.so".into(), "/usr/lib64/libfoo.so.5".into()],
link_options: vec![],
location: None,
};
let mut buf = Vec::new();
target.link_write(&mut buf);
let output = String::from_utf8(buf).unwrap();
assert_eq!(
output.lines().collect::<Vec<&str>>(),
vec![
"cargo:rustc-link-search=native=/usr/lib64",
"cargo:rustc-link-lib=dylib=bar",
"cargo:rustc-link-search=native=/usr/lib",
"cargo:rustc-link-lib=dylib=foo",
"cargo:rustc-link-search=native=/usr/lib64"
]
);
}
}