aws-lc-fips-sys/builder/cmake_builder.rs (424 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 OR ISC
use crate::OutputLib::{Crypto, RustWrapper, Ssl};
use crate::{
cargo_env, effective_target, emit_rustc_cfg, emit_warning, execute_command,
is_cpu_jitter_entropy, is_no_asm, option_env, target_arch, target_env, target_family,
target_os, target_underscored, target_vendor, OutputLibType, TestCommandResult,
};
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
pub(crate) struct CmakeBuilder {
manifest_dir: PathBuf,
out_dir: PathBuf,
build_prefix: Option<String>,
output_lib_type: OutputLibType,
}
fn test_perl_command() -> bool {
execute_command("perl".as_ref(), &["--version".as_ref()]).status
}
fn test_go_command() -> bool {
let result = execute_command("go".as_ref(), &["version".as_ref()]);
if !result.status && result.executed {
eprintln!("Go stderr:\n--------\n{}\n--------", result.stderr);
}
result.status
}
fn test_ninja_command() -> bool {
execute_command("ninja".as_ref(), &["--version".as_ref()]).status
|| execute_command("ninja-build".as_ref(), &["--version".as_ref()]).status
}
fn test_nasm_command() -> bool {
execute_command("nasm".as_ref(), &["-version".as_ref()]).status
}
fn find_cmake_command() -> Option<OsString> {
if let Some(cmake) = option_env("CMAKE") {
emit_warning(&format!(
"CMAKE environment variable set: {}",
cmake.clone()
));
if execute_command(cmake.as_ref(), &["--version".as_ref()]).status {
Some(cmake.into())
} else {
None
}
} else if execute_command("cmake3".as_ref(), &["--version".as_ref()]).status {
Some("cmake3".into())
} else if execute_command("cmake".as_ref(), &["--version".as_ref()]).status {
Some("cmake".into())
} else {
None
}
}
fn has_target_cpu_optimization() -> bool {
matches!(
target_arch().as_str(),
"x86_64" | "x86" | "aarch64" | "arm" | "powerpc64"
)
}
impl CmakeBuilder {
pub(crate) fn new(
manifest_dir: PathBuf,
out_dir: PathBuf,
build_prefix: Option<String>,
output_lib_type: OutputLibType,
) -> Self {
Self {
manifest_dir,
out_dir,
build_prefix,
output_lib_type,
}
}
fn artifact_output_dir(&self) -> PathBuf {
self.out_dir.join("build").join("artifacts")
}
fn get_cmake_config(&self) -> cmake::Config {
cmake::Config::new(&self.manifest_dir)
}
const GOCACHE_DIR_NAME: &'static str = "go-cache";
#[allow(clippy::too_many_lines)]
fn prepare_cmake_build(&self) -> cmake::Config {
env::set_var(
"GOCACHE",
self.out_dir.join(Self::GOCACHE_DIR_NAME).as_os_str(),
);
let mut cmake_cfg = self.get_cmake_config();
if OutputLibType::default() == OutputLibType::Dynamic {
cmake_cfg.define("BUILD_SHARED_LIBS", "1");
} else {
cmake_cfg.define("BUILD_SHARED_LIBS", "0");
}
if is_cpu_jitter_entropy() {
cmake_cfg.define("ENABLE_FIPS_ENTROPY_CPU_JITTER", "ON");
emit_rustc_cfg("cpu_jitter_entropy");
}
if let Some(cc) = option_env!("AWS_LC_FIPS_SYS_CC") {
env::set_var("CC", cc);
emit_warning(&format!("Setting CC: {cc}"));
}
if let Some(cxx) = option_env!("AWS_LC_FIPS_SYS_CXX") {
env::set_var("CXX", cxx);
emit_warning(&format!("Setting CXX: {cxx}"));
}
let cc_build = cc::Build::new();
let opt_level = cargo_env("OPT_LEVEL");
if opt_level.ne("0") {
if opt_level.eq("1") || opt_level.eq("2") {
cmake_cfg.define("CMAKE_BUILD_TYPE", "relwithdebinfo");
} else {
if opt_level.eq("s") || opt_level.eq("z") {
cmake_cfg.define("CMAKE_BUILD_TYPE", "minsizerel");
} else {
cmake_cfg.define("CMAKE_BUILD_TYPE", "release");
}
// TODO: Due to the nature of the FIPS build (e.g., its dynamic generation of
// assembly files and its custom compilation commands within CMake), not all
// source paths are stripped from the resulting binary.
emit_warning(
"NOTICE: Build environment source paths might be visible in release binary.",
);
let parent_dir = self.manifest_dir.parent();
if parent_dir.is_some() && (target_family() == "unix" || target_env() == "gnu") {
let parent_dir = parent_dir.unwrap();
let flag = format!("\"-ffile-prefix-map={}=\"", parent_dir.display());
if let Ok(true) = cc_build.is_flag_supported(&flag) {
emit_warning(&format!("Using flag: {}", &flag));
cmake_cfg.asmflag(&flag);
cmake_cfg.cflag(&flag);
} else {
let flag = format!("\"-fdebug-prefix-map={}=\"", parent_dir.display());
if let Ok(true) = cc_build.is_flag_supported(&flag) {
emit_warning(&format!("Using flag: {}", &flag));
cmake_cfg.asmflag(&flag);
cmake_cfg.cflag(&flag);
}
}
}
}
} else if target_os() == "windows" {
// The Windows/FIPS build rejects "debug" profile
// https://github.com/aws/aws-lc/blob/main/CMakeLists.txt#L656
cmake_cfg.define("CMAKE_BUILD_TYPE", "relwithdebinfo");
} else {
cmake_cfg.define("CMAKE_BUILD_TYPE", "debug");
}
Self::verify_compiler_support(&cc_build.get_compiler());
if let Some(prefix) = &self.build_prefix {
cmake_cfg.define("BORINGSSL_PREFIX", format!("{prefix}_"));
let include_path = self.manifest_dir.join("generated-include");
cmake_cfg.define(
"BORINGSSL_PREFIX_HEADERS",
include_path.display().to_string(),
);
}
// Build flags that minimize our crate size.
cmake_cfg.define("BUILD_TESTING", "OFF");
cmake_cfg.define("BUILD_TOOL", "OFF");
if cfg!(feature = "ssl") {
cmake_cfg.define("BUILD_LIBSSL", "ON");
} else {
cmake_cfg.define("BUILD_LIBSSL", "OFF");
}
cmake_cfg.define("FIPS", "1");
if is_no_asm() {
let opt_level = cargo_env("OPT_LEVEL");
if opt_level == "0" {
cmake_cfg.define("OPENSSL_NO_ASM", "1");
} else {
panic!("AWS_LC_SYS_NO_ASM only allowed for debug builds!")
}
} else if !has_target_cpu_optimization() {
emit_warning(&format!(
"Assembly optimizations not available for target arch: {}.",
target_arch()
));
// TODO: This should not be needed once resolved upstream
// See: https://github.com/aws/aws-lc-rs/issues/655
cmake_cfg.define("OPENSSL_NO_ASM", "1");
}
if cfg!(feature = "asan") {
env::set_var("CC", "clang");
env::set_var("CXX", "clang++");
env::set_var("ASM", "clang");
cmake_cfg.define("ASAN", "1");
}
// Allow environment to specify CMake toolchain.
if let Some(toolchain) = option_env("CMAKE_TOOLCHAIN_FILE").or(option_env(format!(
"CMAKE_TOOLCHAIN_FILE_{}",
target_underscored()
))) {
emit_warning(&format!(
"CMAKE_TOOLCHAIN_FILE environment variable set: {toolchain}"
));
return cmake_cfg;
}
if target_vendor() == "apple" {
let disable_warnings: [&str; 2] =
["-Wno-overriding-t-option", "-Wno-overriding-option"];
for disabler in disable_warnings {
if let Ok(true) = cc_build.is_flag_supported(disabler) {
cmake_cfg.cflag(disabler);
}
}
if target_arch() == "aarch64" {
cmake_cfg.define("CMAKE_OSX_ARCHITECTURES", "arm64");
cmake_cfg.define("CMAKE_SYSTEM_PROCESSOR", "arm64");
}
if target_arch() == "x86_64" {
cmake_cfg.define("CMAKE_OSX_ARCHITECTURES", "x86_64");
cmake_cfg.define("CMAKE_SYSTEM_PROCESSOR", "x86_64");
}
if target_os().trim() == "ios" {
cmake_cfg.define("CMAKE_SYSTEM_NAME", "iOS");
if effective_target().ends_with("-ios-sim") || target_arch() == "x86_64" {
cmake_cfg.define("CMAKE_OSX_SYSROOT", "iphonesimulator");
} else {
cmake_cfg.define("CMAKE_OSX_SYSROOT", "iphoneos");
}
cmake_cfg.define("CMAKE_THREAD_LIBS_INIT", "-lpthread");
}
if target_os().trim() == "macos" {
cmake_cfg.define("CMAKE_SYSTEM_NAME", "Darwin");
cmake_cfg.define("CMAKE_OSX_SYSROOT", "macosx");
}
}
if target_os() == "windows" {
cmake_cfg.generator("Ninja");
let env_map = self
.collect_vcvarsall_bat()
.map_err(|x| panic!("{}", x))
.unwrap();
for (key, value) in env_map {
cmake_cfg.env(key, value);
}
}
if target_env() == "ohos" {
Self::configure_open_harmony(&mut cmake_cfg);
}
cmake_cfg
}
fn verify_compiler_support(compiler: &cc::Tool) -> Option<bool> {
let compiler_path = compiler.path();
if compiler.is_like_gnu() || compiler.is_like_clang() {
if let TestCommandResult {
stderr: _,
stdout,
executed: true,
status: true,
} = execute_command(compiler_path.as_os_str(), &["--version".as_ref()])
{
if let Some(first_line) = stdout.lines().nth(0) {
if let Some((major, minor, patch)) = parse_version(first_line) {
// We don't force a build failure, but we generate a clear message.
if compiler.is_like_gnu() {
emit_warning(&format!("GCC v{major}.{minor}.{patch} detected."));
if major > 13 {
// TODO: Update when FIPS GCC 14 build is fixed
emit_warning("WARNING: FIPS build is known to fail on GCC >= 14. See: https://github.com/aws/aws-lc-rs/issues/569");
emit_warning("Consider specifying a different compiler in your environment by setting `CC` or: `export AWS_LC_FIPS_SYS_CC=clang`");
return Some(false);
}
}
if compiler.is_like_clang() {
// AWS-LC-FIPS 2.0 was unable to compile with Clang 19
emit_warning(&format!("Clang v{major}.{minor}.{patch} detected."));
}
return Some(true);
}
}
}
} else if compiler.is_like_msvc() {
if let TestCommandResult {
stderr,
stdout: _,
executed: true,
status: true,
} = execute_command(compiler_path.as_os_str(), &["/help".as_ref()])
{
if let Some(first_line) = stderr.lines().nth(0) {
if let Some((major, minor, patch)) = parse_version(first_line) {
emit_warning(&format!("MSVC v{major}.{minor}.{patch} detected."));
return Some(true);
}
}
}
}
None
}
fn configure_open_harmony(cmake_cfg: &mut cmake::Config) {
const OHOS_NDK_HOME: &str = "OHOS_NDK_HOME";
if let Ok(ndk) = env::var(OHOS_NDK_HOME) {
cmake_cfg.define(
"CMAKE_TOOLCHAIN_FILE",
format!("{ndk}/native/build/cmake/ohos.toolchain.cmake"),
);
let mut cflags = vec!["-Wno-unused-command-line-argument"];
let mut asmflags = vec![];
match effective_target().as_str() {
"aarch64-unknown-linux-ohos" => {}
"armv7-unknown-linux-ohos" => {
const ARM7_FLAGS: [&str; 6] = [
"-march=armv7-a",
"-mfloat-abi=softfp",
"-mtune=generic-armv7-a",
"-mthumb",
"-mfpu=neon",
"-DHAVE_NEON",
];
cflags.extend(ARM7_FLAGS);
asmflags.extend(ARM7_FLAGS);
}
"x86_64-unknown-linux-ohos" => {
const X86_64_FLAGS: [&str; 3] = ["-msse4.1", "-DHAVE_NEON_X86", "-DHAVE_NEON"];
cflags.extend(X86_64_FLAGS);
asmflags.extend(X86_64_FLAGS);
}
ohos_target => {
emit_warning(format!("Target: {ohos_target} is not support yet!").as_str());
}
}
cmake_cfg
.cflag(cflags.join(" ").as_str())
.cxxflag(cflags.join(" ").as_str())
.asmflag(asmflags.join(" ").as_str());
} else {
emit_warning(format!("{OHOS_NDK_HOME} not set!").as_str());
}
}
fn build_rust_wrapper(&self) -> PathBuf {
self.prepare_cmake_build()
.configure_arg("--no-warn-unused-cli")
.build()
}
fn collect_vcvarsall_bat(&self) -> Result<HashMap<String, String>, String> {
let mut map: HashMap<String, String> = HashMap::new();
let script_path = self.manifest_dir.join("builder").join("printenv.bat");
let result = execute_command(script_path.as_os_str(), &[]);
if !result.status {
eprintln!("{}", result.stdout);
return Err("Failed to run vcvarsall.bat.".to_owned());
}
eprintln!("{}", result.stdout);
let lines = result.stdout.lines();
for line in lines {
if let Some((var, val)) = line.split_once('=') {
map.insert(var.to_string(), val.to_string());
}
}
Ok(map)
}
}
impl crate::Builder for CmakeBuilder {
fn check_dependencies(&self) -> Result<(), String> {
let mut missing_dependency = false;
if target_os() == "windows" && !test_ninja_command() {
eprintln!("Missing dependency: Ninja is required for FIPS on Windows.");
missing_dependency = true;
}
if !test_go_command() {
eprintln!("Missing dependency: Go is required for FIPS.");
missing_dependency = true;
}
if !test_perl_command() {
eprintln!("Missing dependency: perl is required for FIPS.");
missing_dependency = true;
}
if target_os() == "windows"
&& target_arch() == "x86_64"
&& !test_nasm_command()
&& !is_no_asm()
{
eprintln!(
"Consider setting `AWS_LC_FIPS_SYS_NO_ASM` in the environment for development builds.\
See User Guide about the limitations: https://aws.github.io/aws-lc-rs/index.html"
);
eprintln!("Missing dependency: nasm is required for FIPS.");
missing_dependency = true;
}
if let Some(cmake_cmd) = find_cmake_command() {
env::set_var("CMAKE", cmake_cmd);
} else {
eprintln!("Missing dependency: cmake");
missing_dependency = true;
}
if missing_dependency {
return Err("Required build dependency is missing. Halting build.".to_owned());
}
Ok(())
}
fn build(&self) -> Result<(), String> {
self.build_rust_wrapper();
println!(
"cargo:rustc-link-search=native={}",
self.artifact_output_dir().display()
);
println!(
"cargo:rustc-link-lib={}={}",
self.output_lib_type.rust_lib_type(),
Crypto.libname(&self.build_prefix)
);
if cfg!(feature = "ssl") {
println!(
"cargo:rustc-link-lib={}={}",
self.output_lib_type.rust_lib_type(),
Ssl.libname(&self.build_prefix)
);
}
println!(
"cargo:rustc-link-lib={}={}",
self.output_lib_type.rust_lib_type(),
RustWrapper.libname(&self.build_prefix)
);
Ok(())
}
}
fn parse_version(line: &str) -> Option<(u32, u32, u32)> {
let version_pattern = regex::Regex::new(r"\s(\d{1,2})\.(\d{1,2})\.(\d+)").ok()?;
let captures = version_pattern.captures(line)?;
let major_str = captures.get(1)?.as_str();
let minor_str = captures.get(2)?.as_str();
let patch_str = captures.get(3)?.as_str();
let major = major_str.parse::<u32>().ok()?;
let minor = minor_str.parse::<u32>().ok()?;
let patch = patch_str.parse::<u32>().ok()?;
Some((major, minor, patch))
}
// Tests inside build script don't actually get run.
// These tests and the function above need to be copied elsewhere to test.
//
// #[cfg(test)]
// mod tests {
// #[test]
// fn test_parse_version() {
// let test_cases = [
// ("Apple clang version 14.0.0 (clang-1500.1.0.2.5)\n", (14, 0, 0)),
// ("gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0", (13,2,0)),
// ("FreeBSD clang version 18.1.5 (https://github.com/llvm/llvm-project.git llvmorg-18.1.5-0-g617a15a9eac9)", (18,1,5)),
// ("gcc (GCC) 11.4.1 20230605 (Red Hat 11.4.1-2)", (11, 4, 1)),
// ("Microsoft (R) C/C++ Optimizing Compiler Version 19.40.33812 for x64", (19, 40, 33812))
// ];
// for case in test_cases {
// let (major, minor, patch) = super::parse_version(case.0).unwrap();
// assert_eq!(major, case.1 .0);
// assert_eq!(minor, case.1 .1);
// assert_eq!(patch, case.1 .2);
// }
// }
// }