322 lines
11 KiB
Rust
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 })
|
|
}
|