use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
let fips = env::var("CARGO_FEATURE_FIPS").is_ok();
let aws_lc = env::var("CARGO_FEATURE_AWS_LC").is_ok();
let transit = env::var("CARGO_FEATURE_TRANSIT").is_ok();
if !fips && !aws_lc {
panic!(
"no crypto backend feature selected -- enable exactly one of the `aws-lc` (default) \
or `fips` features."
);
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let vendor_dir = manifest_dir.join("vendor");
let i2pd_dir = vendor_dir.join("i2pd-src");
let boost_dir = vendor_dir.join("boost-src");
let zlib_dir = vendor_dir.join("zlib-src");
let target = env::var("TARGET").expect("TARGET");
let host = env::var("HOST").expect("HOST");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let compilers = if target == host {
None } else {
Some(resolve_cross_compilers(&target))
};
let zlib_lib = build_zlib(&zlib_dir, &out_dir, compilers.as_ref());
let boost_install = build_boost(&boost_dir, &out_dir, compilers.as_ref());
let crypto = CryptoBackend::discover(fips);
let openssl_include = PathBuf::from(&crypto.include);
let openssl_crypto_stub = openssl_stub_lib(&out_dir, "crypto", compilers.as_ref());
let openssl_ssl_stub = openssl_stub_lib(&out_dir, "ssl", compilers.as_ref());
let i2pd_install = build_i2pd(
&i2pd_dir,
&out_dir,
&boost_install,
&openssl_include,
&openssl_crypto_stub,
&openssl_ssl_stub,
&zlib_dir,
&zlib_lib,
transit,
compilers.as_ref(),
);
println!("cargo:rustc-link-search=native={}", i2pd_install.join("lib").display());
println!("cargo:rustc-link-search=native={}", boost_install.join("lib").display());
println!("cargo:rustc-link-search=native={}", zlib_lib.parent().unwrap().display());
let aws_lc_crypto = &crypto.libcrypto;
let aws_lc_ssl = crypto.libssl.as_deref();
let mut shim = cc::Build::new();
shim.cpp(true).std("c++17").warnings(false);
if let Some(c) = &compilers {
shim.compiler(&c.cxx);
}
for flag in HARDENING_CFLAGS {
shim.flag_if_supported(flag);
}
if !transit {
shim.define("I2PD_SYS_NO_TRANSIT", None);
}
shim.include(i2pd_dir.join("libi2pd"))
.include(boost_install.join("include"))
.include(&openssl_include)
.include(zlib_dir)
.file("shim/shim.cpp")
.compile("tachyon_i2pd_shim");
println!("cargo:rustc-link-search=native={}", out_dir.display());
for lib in ["tachyon_i2pd_shim", "i2pd"] {
println!("cargo:rustc-link-lib=static:+verbatim=lib{lib}.a");
}
for lib in ["filesystem", "program_options", "atomic", "container"] {
println!("cargo:rustc-link-lib=static:+verbatim=libboost_{lib}.a");
}
println!("cargo:rustc-link-lib=static:+verbatim=libz.a");
if let Some(aws_lc_ssl) = aws_lc_ssl {
println!("cargo:rustc-link-lib=static:+verbatim=lib{aws_lc_ssl}.a");
}
println!("cargo:rustc-link-lib=static:+verbatim=lib{aws_lc_crypto}.a");
bindgen::Builder::default()
.header("shim/shim.h")
.allowlist_function("i2pd_.*")
.allowlist_type("I2pd.*")
.clang_arg(format!("--target={target}"))
.generate()
.expect("bindgen failed to generate shim bindings")
.write_to_file(out_dir.join("bindings.rs"))
.expect("failed to write bindgen bindings.rs");
for path in [
"build.rs",
"shim/shim.h",
"shim/shim.cpp",
"vendor/i2pd-src/SNAPSHOT_COMMIT.txt",
"vendor/boost-src/SNAPSHOT_TAG.txt",
"vendor/zlib-src/SNAPSHOT_VERSION.txt",
] {
println!("cargo:rerun-if-changed={path}");
}
for var in ["MUSL_CROSS_TOOLCHAIN", "CC", "CXX", "AR", "CMAKE", "CMAKE_GENERATOR", "RUSTC_WRAPPER"] {
println!("cargo:rerun-if-env-changed={var}");
}
}
struct CryptoBackend {
include: String,
libcrypto: String,
libssl: Option<String>,
}
impl CryptoBackend {
fn discover(fips: bool) -> Self {
const FIPS_PREFIX: &str = "DEP_AWS_LC_FIPS_";
const PREFIX: &str = "DEP_AWS_LC_";
let (krate, matches): (_, fn(&str) -> bool) = if fips {
("aws-lc-fips-sys", |k| k.starts_with(FIPS_PREFIX))
} else {
("aws-lc-sys", |k| k.starts_with(PREFIX) && !k.starts_with(FIPS_PREFIX))
};
let get = |suffix: &str| {
env::vars().find(|(k, _)| matches(k) && k.ends_with(suffix)).map(|(k, v)| {
println!("cargo:rerun-if-env-changed={k}");
v
})
};
let include = get("_INCLUDE").unwrap_or_else(|| {
panic!(
"{krate} published no DEP_*_INCLUDE metadata -- is it actually in the dependency \
graph for the selected feature set?"
)
});
let libcrypto =
get("_LIBCRYPTO").unwrap_or_else(|| panic!("{krate} published no DEP_*_LIBCRYPTO metadata"));
Self { include, libcrypto, libssl: get("_LIBSSL") }
}
}
struct CrossCompilers {
cc: PathBuf,
cxx: PathBuf,
ar: Option<PathBuf>,
}
fn resolve_cross_compilers(target: &str) -> CrossCompilers {
let env_key = |prefix: &str| format!("{prefix}_{}", target.replace('-', "_"));
if let (Ok(cc), Ok(cxx)) = (env::var(env_key("CC")), env::var(env_key("CXX"))) {
return CrossCompilers {
cc: cc.into(),
cxx: cxx.into(),
ar: env::var(env_key("AR")).ok().map(PathBuf::from),
};
}
if let Ok(toolchain_dir) = env::var("MUSL_CROSS_TOOLCHAIN") {
let bin = PathBuf::from(&toolchain_dir).join("bin");
let cc = bin.join(format!("{target}-gcc"));
let cxx = bin.join(format!("{target}-g++"));
if cxx.exists() {
let ar = bin.join(format!("{target}-ar"));
return CrossCompilers { cc, cxx, ar: ar.exists().then_some(ar) };
}
}
let path_cc = format!("{target}-gcc");
let path_cxx = format!("{target}-g++");
if which(&path_cxx) {
return CrossCompilers {
cc: path_cc.into(),
cxx: path_cxx.into(),
ar: which(&format!("{target}-ar")).then(|| format!("{target}-ar").into()),
};
}
panic!(
"cross-compiling to `{target}` needs a real musl C++ toolchain (binutils+gcc+musl+\
libstdc++) -- no C++ compiler shipped by default on a glibc host can target musl. \
Point `MUSL_CROSS_TOOLCHAIN` at a `musl-cross-make` install (containing \
bin/{target}-g++), or set `CC_{t}`/`CXX_{t}` directly, or put `{target}-g++` on \
$PATH. See i2pd-sys/README.md for how to build one with musl-cross-make. (Building \
natively *inside* a musl system, e.g. an Alpine container, needs none of this --\
only true cross-compilation from a non-musl host does.)",
t = target.replace('-', "_"),
);
}
fn which(cmd: &str) -> bool {
Command::new(cmd)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
const HARDENING_CFLAGS: [&str; 3] = ["-U_FORTIFY_SOURCE", "-D_FORTIFY_SOURCE=2", "-fstack-protector-strong"];
fn build_zlib(zlib_dir: &Path, out_dir: &Path, compilers: Option<&CrossCompilers>) -> PathBuf {
let sources = [
"adler32.c",
"compress.c",
"crc32.c",
"deflate.c",
"gzclose.c",
"gzlib.c",
"gzread.c",
"gzwrite.c",
"infback.c",
"inflate.c",
"inftrees.c",
"inffast.c",
"trees.c",
"uncompr.c",
"zutil.c",
];
let mut build = cc::Build::new();
build.warnings(false).include(zlib_dir).define("HAVE_UNISTD_H", None).define("HAVE_STDARG_H", None);
if let Some(c) = compilers {
build.compiler(&c.cc);
}
for flag in HARDENING_CFLAGS {
build.flag_if_supported(flag);
}
for src in sources {
build.file(zlib_dir.join(src));
}
build.compile("z");
out_dir.join("libz.a")
}
fn build_boost(boost_dir: &Path, out_dir: &Path, compilers: Option<&CrossCompilers>) -> PathBuf {
let mut cfg = cmake::Config::new(boost_dir);
cfg.out_dir(out_dir.join("boost"));
cfg.define(
"BOOST_INCLUDE_LIBRARIES",
"filesystem;program_options;atomic;system;asio;property_tree",
)
.define("CMAKE_BUILD_TYPE", "Release")
.define("BUILD_TESTING", "OFF")
.build_target("install");
define_unless_env(&mut cfg, "CMAKE_C_FLAGS_RELEASE", "-O2 -DNDEBUG");
define_unless_env(&mut cfg, "CMAKE_CXX_FLAGS_RELEASE", "-O2 -DNDEBUG");
define_unless_env(&mut cfg, "CMAKE_UNITY_BUILD", "ON");
define_unless_env(&mut cfg, "CMAKE_UNITY_BUILD_BATCH_SIZE", "8");
apply_cross(&mut cfg, compilers);
native_build_speedups(&mut cfg, &out_dir.join("boost"));
cfg.build()
}
fn openssl_stub_lib(out_dir: &Path, name: &str, compilers: Option<&CrossCompilers>) -> PathBuf {
let stub_dir = out_dir.join("openssl-stub");
std::fs::create_dir_all(&stub_dir).expect("create openssl-stub dir");
let stub_c = stub_dir.join(format!("{name}_stub.c"));
std::fs::write(&stub_c, "// empty -- see openssl_stub_lib in build.rs\n").expect("write stub source");
let mut build = cc::Build::new();
build.warnings(false);
if let Some(c) = compilers {
build.compiler(&c.cc);
}
build.file(&stub_c).compile(&format!("tachyon_openssl_stub_{name}"));
out_dir.join(format!("libtachyon_openssl_stub_{name}.a"))
}
#[allow(clippy::too_many_arguments)]
fn build_i2pd(
i2pd_dir: &Path,
out_dir: &Path,
boost_install: &Path,
openssl_include: &Path,
openssl_crypto_stub: &Path,
openssl_ssl_stub: &Path,
zlib_dir: &Path,
zlib_lib: &Path,
transit: bool,
compilers: Option<&CrossCompilers>,
) -> PathBuf {
let mut cfg = cmake::Config::new(i2pd_dir.join("build"));
cfg.out_dir(out_dir.join("i2pd"));
cfg.define("WITH_LIBRARY", "ON")
.define("WITH_BINARY", "OFF")
.define("WITH_UPNP", "OFF")
.define("WITH_HARDENING", "ON")
.define("BUILD_TESTING", "OFF")
.define("CMAKE_BUILD_TYPE", "Release")
.define("Boost_NO_SYSTEM_PATHS", "ON")
.define("Boost_USE_STATIC_LIBS", "ON")
.define("Boost_USE_STATIC_RUNTIME", "ON")
.define("BOOST_ROOT", boost_install)
.define("OPENSSL_INCLUDE_DIR", openssl_include)
.define("OPENSSL_CRYPTO_LIBRARY", openssl_crypto_stub)
.define("OPENSSL_SSL_LIBRARY", openssl_ssl_stub)
.define("OPENSSL_VERSION", "1.1.1")
.define("ZLIB_INCLUDE_DIR", zlib_dir)
.define("ZLIB_LIBRARY", zlib_lib)
.build_target("install");
for flag in HARDENING_CFLAGS {
cfg.cflag(flag).cxxflag(flag);
}
if !transit {
cfg.cflag("-DI2PD_SYS_NO_TRANSIT").cxxflag("-DI2PD_SYS_NO_TRANSIT");
}
define_unless_env(&mut cfg, "CMAKE_C_FLAGS_RELEASE", "-O2 -DNDEBUG");
define_unless_env(&mut cfg, "CMAKE_CXX_FLAGS_RELEASE", "-O2 -DNDEBUG");
define_unless_env(&mut cfg, "CMAKE_UNITY_BUILD", "ON");
define_unless_env(&mut cfg, "CMAKE_UNITY_BUILD_BATCH_SIZE", "8");
define_unless_env(&mut cfg, "I2PD_SYS_PCH", "ON");
apply_cross(&mut cfg, compilers);
native_build_speedups(&mut cfg, &out_dir.join("i2pd"));
cfg.build()
}
fn define_unless_env(cfg: &mut cmake::Config, key: &str, value: &str) {
println!("cargo:rerun-if-env-changed={key}");
match env::var(key) {
Ok(from_env) => cfg.define(key, from_env),
Err(_) => cfg.define(key, value),
};
}
fn native_build_speedups(cfg: &mut cmake::Config, cmake_out_dir: &Path) {
let generator = match cmake_generator_override() {
Some(explicit) => explicit,
None if which("ninja") => {
cfg.generator("Ninja");
if let Ok(jobs) = env::var("NUM_JOBS") {
cfg.build_arg(format!("-j{jobs}"));
}
"Ninja".to_string()
}
None => "Unix Makefiles".to_string(),
};
discard_tree_on_generator_change(cmake_out_dir, &generator);
if env::var_os("CMAKE_CXX_COMPILER_LAUNCHER").is_some() {
return;
}
if let Some(cache) = ["sccache", "ccache"].into_iter().find(|c| which(c)) {
cfg.define("CMAKE_C_COMPILER_LAUNCHER", cache).define("CMAKE_CXX_COMPILER_LAUNCHER", cache);
if env::var_os("CCACHE_SLOPPINESS").is_none() {
cfg.env("CCACHE_SLOPPINESS", "pch_defines,time_macros");
}
println!("cargo:rerun-if-env-changed=CCACHE_SLOPPINESS");
}
}
fn cmake_generator_override() -> Option<String> {
let target = env::var("TARGET").unwrap_or_default();
let host = env::var("HOST").unwrap_or_default();
let kind = if host == target { "HOST" } else { "TARGET" };
let keys = [
format!("CMAKE_GENERATOR_{target}"),
format!("CMAKE_GENERATOR_{}", target.replace('-', "_")),
format!("{kind}_CMAKE_GENERATOR"),
"CMAKE_GENERATOR".to_string(),
];
for key in &keys {
println!("cargo:rerun-if-env-changed={key}");
}
keys.into_iter().find_map(|key| env::var(key).ok())
}
fn discard_tree_on_generator_change(cmake_out_dir: &Path, generator: &str) {
let cache = cmake_out_dir.join("build/CMakeCache.txt");
let Ok(contents) = std::fs::read_to_string(&cache) else { return };
let configured =
contents.lines().find_map(|l| l.strip_prefix("CMAKE_GENERATOR:INTERNAL=")).unwrap_or_default();
if configured != generator {
let _ = std::fs::remove_dir_all(cmake_out_dir.join("build"));
}
}
fn apply_cross(cfg: &mut cmake::Config, compilers: Option<&CrossCompilers>) {
let Some(c) = compilers else { return };
cfg.define("CMAKE_C_COMPILER", &c.cc)
.define("CMAKE_CXX_COMPILER", &c.cxx)
.define("CMAKE_EXE_LINKER_FLAGS", "-static");
if let Some(ar) = &c.ar {
cfg.define("CMAKE_AR", ar);
}
}