denyhosts/clamav/libclamav_rust/build.rs

322 lines
11 KiB
Rust

use std::env;
use std::path::{Path, PathBuf};
use bindgen::builder;
// Note to maintainers: this is currently a hybrid of examination of the
// CMake environment, and leaning on Rust `cfg` elements. Ideally, it
// should be possible to work in this space (e.g., execute tests from an
// IDE) without having to rely on CMake elements to properly link the
// unit tests). Hence the bizarre mix of CMake inspection and Cargo-based
// elements.
//
// It's handy to know that all the `cfg` goodies are defined here:
//
// https://doc.rust-lang.org/reference/conditional-compilation.html
// A list of environment variables to query to determine additional libraries
// that need to be linked to resolve dependencies.
const LIB_ENV_LINK: &[&str] = &[
"LIBSSL",
"LIBCRYPTO",
"LIBZ",
"LIBBZ2",
"LIBPCRE2",
"LIBXML2",
"LIBCURL",
"LIBJSONC",
"LIBCLAMMSPACK",
"LIBCLAMUNRARIFACE",
"LIBCLAMUNRAR",
"LIBICONV",
];
// The same, but additional values to check on Windows platforms
const LIB_ENV_LINK_WINDOWS: &[&str] = &["LIBPTHREADW32", "LIBWIN32COMPAT"];
// Additional [verbatim] libraries to link on Windows platforms
const LIB_LINK_WINDOWS: &[&str] = &["wsock32", "ws2_32", "Shell32", "User32"];
// Windows library names that must have the leading `lib` trimmed (if encountered)
const WINDOWS_TRIM_LOCAL_LIB: &[&str] = &["libclamav", "libclammspack"];
// Generate bindings for these functions:
const BINDGEN_FUNCTIONS: &[&str] = &[
"cli_ctx",
"cli_warnmsg",
"cli_dbgmsg_no_inline",
"cli_infomsg_simple",
"cli_errmsg",
"cli_append_virus",
"lsig_increment_subsig_match",
"cli_versig2",
"cli_getdsig",
"cli_get_debug_flag",
];
// Generate bindings for these types (structs, enums):
const BINDGEN_TYPES: &[&str] = &["cli_matcher", "cli_ac_data", "cli_ac_result"];
// Find the required functions and types in these headers:
const BINDGEN_HEADERS: &[&str] = &[
"../libclamav/matcher.h",
"../libclamav/matcher-ac.h",
"../libclamav/others.h",
"../libclamav/dsig.h",
];
// Find the required headers in these directories:
const BINDGEN_INCLUDE_PATHS: &[&str] = &[
"-I../libclamav",
"-I../libclamunrar_iface",
"-I../libclammspack",
];
// Write the bindings to this file:
const BINDGEN_OUTPUT_FILE: &str = "src/sys.rs";
const C_HEADER_OUTPUT: &str = "clamav_rust.h";
// Environment variable name prefixes worth including for diags
const ENV_PATTERNS: &[&str] = &["CARGO_", "RUST", "LIB"];
fn main() -> Result<(), &'static str> {
// Dump the command line and interesting environment variables for diagnostic
// purposes. These will end up in a 'stderr' file under the target directory,
// in a ".../clamav_rust-<hex>" subdirectory
eprintln!("build.rs command line: {:?}", std::env::args());
eprintln!("Environment:");
std::env::vars()
.filter(|(k, _)| ENV_PATTERNS.iter().any(|prefix| k.starts_with(prefix)))
.for_each(|(k, v)| eprintln!(" {}={:?}", k, v));
detect_clamav_build()?;
// We only want to generate bindings for `cargo build`, not `cargo test`.
// FindRust.cmake defines $CARGO_CMD so we can differentiate.
let cargo_cmd = env::var("CARGO_CMD").unwrap_or_else(|_| "".into());
if cargo_cmd == "build" {
// Always generate the C-headers when CMake kicks off a build.
execute_cbindgen()?;
// Only generate the `.rs` bindings when maintainer-mode is enabled.
//
// Bindgen requires libclang, which may not readily available, so we
// will commit the bindings to version control and use maintainer-mode
// to update them, as needed.
// On the plus-side, this means that our `.rs` file is present before our
// first build, so at least rust-analyzer will be happy.
let maintainer_mode = env::var("MAINTAINER_MODE").unwrap_or_else(|_| "".into());
if maintainer_mode == "ON" {
execute_bindgen()?;
}
} else {
eprintln!("NOTE: Not generating bindings because CARGO_CMD != build");
}
Ok(())
}
/// Use bindgen to generate Rust bindings to call into C libraries.
fn execute_bindgen() -> Result<(), &'static str> {
let build_dir = PathBuf::from(env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| ".".into()));
let build_include_path = format!("-I{}", build_dir.join(".").to_str().unwrap());
// Configure and generate bindings.
let mut builder = builder()
// Silence code-style warnings for generated bindings.
.raw_line("#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]")
// Make the bindings pretty.
.rustfmt_bindings(true)
// Disable the layout tests.
// We're commiting to source control. Pointer width, integer size, etc
// are probably not the same when generated as when compiled.
.layout_tests(false)
// Enable bindgen to find generated headers in the build directory, too.
.clang_arg(build_include_path);
for &include_path in BINDGEN_INCLUDE_PATHS {
builder = builder.clang_arg(include_path);
}
for &header in BINDGEN_HEADERS {
builder = builder.header(header);
}
for &c_function in BINDGEN_FUNCTIONS {
builder = builder.allowlist_function(c_function);
}
for &c_type in BINDGEN_TYPES {
builder = builder.allowlist_type(c_type);
}
// Generate!
builder
.generate()
.expect("Generating Rust bindings for C code")
.write_to_file(BINDGEN_OUTPUT_FILE)
.expect("Writing Rust bindings to output file");
eprintln!("bindgen outputting \"{}\"", BINDGEN_OUTPUT_FILE);
Ok(())
}
/// Use cbindgen to generate C-header's for Rust static libraries.
fn execute_cbindgen() -> Result<(), &'static str> {
let crate_dir = env::var("CARGO_MANIFEST_DIR").or(Err("CARGO_MANIFEST_DIR not specified"))?;
let build_dir = PathBuf::from(env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| ".".into()));
let outfile_path = build_dir.join(C_HEADER_OUTPUT);
// Useful for build diagnostics
eprintln!("cbindgen outputting {:?}", &outfile_path);
cbindgen::generate(crate_dir)
.expect("Unable to generate bindings")
.write_to_file(&outfile_path);
Ok(())
}
fn detect_clamav_build() -> Result<(), &'static str> {
println!("cargo:rerun-if-env-changed=LIBCLAMAV");
if search_and_link_lib("LIBCLAMAV")? {
eprintln!("NOTE: LIBCLAMAV defined. Examining LIB* environment variables");
// Need to link with libclamav dependencies
// LLVM is optional, and don't have a path to each library like we do with the other libs.
let llvm_libs = env::var("LLVM_LIBS").unwrap_or("".into());
if llvm_libs != "" {
match env::var("LLVM_DIRS") {
Err(env::VarError::NotPresent) => eprintln!("LLVM_DIRS not set"),
Err(env::VarError::NotUnicode(_)) => return Err("environment value not unicode"),
Ok(s) => {
if s.is_empty() {
eprintln!("LLVM_DIRS not set");
} else {
s.split(',').for_each(|dirpath| {
println!("cargo:rustc-link-search={}", dirpath);
});
}
}
};
llvm_libs
.split(',')
.for_each(|filepath_str| match parse_lib_path(&filepath_str) {
Ok(parsed_path) => {
println!("cargo:rustc-link-search={}", parsed_path.dir);
eprintln!(" - requesting that rustc link {:?}", &parsed_path.libname);
println!("cargo:rustc-link-lib={}", parsed_path.libname);
}
Err(_) => {
eprintln!(" - requesting that rustc link {:?}", filepath_str);
println!("cargo:rustc-link-lib={}", filepath_str);
}
});
}
for var in LIB_ENV_LINK {
let _ = search_and_link_lib(var);
}
if cfg!(windows) {
for var in LIB_ENV_LINK_WINDOWS {
let _ = search_and_link_lib(var);
}
for lib in LIB_LINK_WINDOWS {
println!("cargo:rustc-link-lib={}", lib);
}
} else {
// Link the test executable with libstdc++ on unix systems,
// This is needed for fully-static build where clamav & 3rd party
// dependencies excluding the std libs are static.
if cfg!(target_os = "linux") {
eprintln!("NOTE: linking libstdc++ (linux target)");
println!("cargo:rustc-link-lib=stdc++");
} else {
eprintln!("NOTE: NOT linking libstdc++ (non-linux target)");
}
}
} else {
println!("NOTE: LIBCLAMAV not defined");
}
Ok(())
}
//
// Return whether the specified environment variable has been set, and output
// linking directives as a side-effect
//
fn search_and_link_lib(environment_variable: &str) -> Result<bool, &'static str> {
eprintln!(" - checking for {:?} in environment", environment_variable);
let filepath_str = match env::var(environment_variable) {
Err(env::VarError::NotPresent) => return Ok(false),
Err(env::VarError::NotUnicode(_)) => return Err("environment value not unicode"),
Ok(s) => {
if s.is_empty() {
return Ok(false);
} else {
s
}
}
};
let parsed_path = parse_lib_path(&filepath_str)?;
eprintln!(
" - adding {:?} to rustc library search path",
&parsed_path.dir
);
println!("cargo:rustc-link-search={}", parsed_path.dir);
eprintln!(" - requesting that rustc link {:?}", &parsed_path.libname);
println!("cargo:rustc-link-lib={}", parsed_path.libname);
Ok(true)
}
struct ParsedLibraryPath {
dir: String,
libname: String,
}
// Parse a library path, returning the portion expected after the `-l`, and the
// directory containing the library
fn parse_lib_path<'a>(path: &'a str) -> Result<ParsedLibraryPath, &'static str> {
let path = PathBuf::from(path);
let file_name = path
.file_name()
.ok_or("file name not found")?
.to_str()
.ok_or("file name not unicode")?;
// This can't fail because it came from a &str
let dir = path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_str()
.unwrap()
.to_owned();
// Grab the portion up to the first '.'
let full_libname = file_name
.split('.')
.next()
.ok_or("no '.' found in file name")?;
// Windows typically requires the full filename when linking system libraries,
// but not when it's one of the locally-generated libraries.
let should_trim_leading_lib =
!cfg!(windows) || WINDOWS_TRIM_LOCAL_LIB.iter().any(|s| *s == full_libname);
let libname = if should_trim_leading_lib {
full_libname
.strip_prefix("lib")
.ok_or(r#"file name doesn't begin with "lib""#)?
} else {
full_libname
}
.to_owned();
Ok(ParsedLibraryPath { dir, libname })
}