use crate::fips_probe::verify_fips_install;
use crate::{
crate_env_var_name, emit_rustc_cfg, emit_warning, is_fips_build, is_static_library,
link_fips_runtime_check, out_dir, target_env, target_os, Builder, OutputLibType,
};
use std::cell::RefCell;
use std::path::{Path, PathBuf};
pub(crate) struct InstallLayout {
include_dir: PathBuf,
lib_dirs: Vec<PathBuf>,
bindings_prefix: Option<PathBuf>,
marker_dir: PathBuf,
}
impl InstallLayout {
pub(crate) fn from_prefix(prefix: PathBuf) -> Self {
let include_dir = prefix.join("include");
let mut lib_dirs = Vec::new();
let target_is_64bit =
std::env::var("CARGO_CFG_TARGET_POINTER_WIDTH").is_ok_and(|w| w == "64");
let lib64 = prefix.join("lib64");
if target_is_64bit && lib64.exists() {
lib_dirs.push(lib64);
}
lib_dirs.push(prefix.join("lib"));
Self {
include_dir,
lib_dirs,
bindings_prefix: Some(prefix.clone()),
marker_dir: prefix,
}
}
pub(crate) fn from_paths(
include_dir: PathBuf,
lib_dirs: Vec<PathBuf>,
bindings_prefix: Option<PathBuf>,
) -> Self {
let marker_dir = bindings_prefix
.clone()
.unwrap_or_else(|| include_dir.clone());
Self {
include_dir,
lib_dirs,
bindings_prefix,
marker_dir,
}
}
}
pub(crate) struct SystemLib {
pub manifest_dir: PathBuf,
layout: InstallLayout,
pub bindings_file: Option<PathBuf>,
pub skip_version_check: bool,
pub crypto_lib: RefCell<Option<ResolvedLib>>,
pub ssl_lib: RefCell<Option<ResolvedLib>>,
validated_bindings: RefCell<Option<PathBuf>>,
}
impl SystemLib {
pub(crate) fn new(
manifest_dir: PathBuf,
install_dir: PathBuf,
bindings_file: Option<PathBuf>,
skip_version_check: bool,
) -> Self {
Self::from_layout(
manifest_dir,
InstallLayout::from_prefix(install_dir),
bindings_file,
skip_version_check,
)
}
pub(crate) fn from_layout(
manifest_dir: PathBuf,
layout: InstallLayout,
bindings_file: Option<PathBuf>,
skip_version_check: bool,
) -> Self {
Self {
manifest_dir,
layout,
bindings_file,
skip_version_check,
crypto_lib: RefCell::new(None),
ssl_lib: RefCell::new(None),
validated_bindings: RefCell::new(None),
}
}
pub(crate) fn marker_dir(&self) -> &Path {
&self.layout.marker_dir
}
}
pub(crate) struct ResolvedLib {
pub lib_type: OutputLibType,
pub name: String,
pub path: PathBuf,
}
impl Builder for SystemLib {
fn check_dependencies(&self) -> Result<(), String> {
let requested_lib_type = is_static_library().map(|stc| {
if stc {
OutputLibType::Static
} else {
OutputLibType::Dynamic
}
});
let mut last_err: Option<String> = None;
for lib_dir in &self.layout.lib_dirs {
let crypto_lib = match resolve_library(
lib_dir.as_path(),
requested_lib_type,
"crypto",
CRYPTO_LIB_CANDIDATES,
) {
Ok(lib) => lib,
Err(e) => {
last_err = Some(e);
continue;
}
};
if cfg!(feature = "ssl") {
let ssl_lib = match resolve_library(
lib_dir.as_path(),
requested_lib_type,
"ssl",
SSL_LIB_CANDIDATES,
) {
Ok(lib) => lib,
Err(e) => {
last_err = Some(e);
continue;
}
};
self.ssl_lib.replace(Some(ssl_lib));
}
self.crypto_lib.replace(Some(crypto_lib));
return Ok(());
}
Err(last_err.unwrap_or_else(|| {
format!(
"AWS-LC libcrypto{ssl} not found under {dirs}",
ssl = if cfg!(feature = "ssl") { "/libssl" } else { "" },
dirs = self
.layout
.lib_dirs
.iter()
.map(|d| d.display().to_string())
.collect::<Vec<_>>()
.join(", "),
)
}))
}
fn build(&self) -> Result<(), String> {
self.link()
}
fn name(&self) -> &'static str {
"System Library"
}
}
impl SystemLib {
fn validate(&self) -> Result<PathBuf, String> {
if let Some(bindings) = self.validated_bindings.borrow().clone() {
return Ok(bindings);
}
let crypto_lib = self.crypto_lib.borrow();
let crypto_lib = crypto_lib
.as_ref()
.expect("check_dependencies must run first");
let lib_dir = crypto_lib
.path
.parent()
.ok_or("libcrypto parent directory not found")?;
let include_dir = &self.layout.include_dir;
if !include_dir.exists() {
return Err(format!(
"Include directory not found: {}\n\
Verify the AWS-LC headers exist (via {}, OPENSSL_DIR, or \
OPENSSL_INCLUDE_DIR).",
include_dir.display(),
crate_env_var_name("SYSTEM_DIR"),
));
}
if self.skip_version_check {
emit_warning("Skipping AWS-LC version compatibility check.");
read_and_validate_base_h(include_dir)?;
} else if is_fips_build() {
let fips_version = validate_and_resolve_fips_version(include_dir)?;
if fips_version < MINIMUM_FIPS_VERSION {
return Err(format!(
"AWS-LC FIPS module version too old: installed FIPS version {fips_version} < \
minimum supported {MINIMUM_FIPS_VERSION}.\n\
Please use a newer AWS-LC FIPS build or unset {} to build from source.\n\
To bypass this check (not recommended), set {}=1",
crate_env_var_name("SYSTEM_DIR"),
crate_env_var_name("SYSTEM_SKIP_VERSION_CHECK"),
));
}
} else {
let version = validate_and_extract_version(include_dir)?;
if !version_at_least(&version, MINIMUM_AWS_LC_VERSION)? {
return Err(format!(
"AWS-LC version too old: installed {version} < minimum supported \
{MINIMUM_AWS_LC_VERSION}.\n\
Please upgrade AWS-LC or unset {} to build from source.\n\
To bypass this check (not recommended), set {}=1",
crate_env_var_name("SYSTEM_DIR"),
crate_env_var_name("SYSTEM_SKIP_VERSION_CHECK"),
));
}
}
let bindings =
resolve_bindings(self.layout.bindings_prefix.as_deref(), &self.bindings_file)?;
if is_fips_build() {
verify_fips_install(
self.manifest_dir.as_path(),
include_dir,
crypto_lib,
lib_dir,
)?;
}
self.validated_bindings.replace(Some(bindings.clone()));
Ok(bindings)
}
pub(crate) fn probe(&self) -> Result<(), String> {
self.check_dependencies()?;
self.validate().map(|_| ())
}
pub(crate) fn link(&self) -> Result<(), String> {
let bindings = self.validate()?;
let crypto_lib = self.crypto_lib.borrow();
let crypto_lib = crypto_lib
.as_ref()
.expect("check_dependencies must run first");
let lib_dir = crypto_lib
.path
.parent()
.ok_or("libcrypto parent directory not found")?;
let include_dir = &self.layout.include_dir;
std::fs::copy(&bindings, out_dir().join("bindings.rs"))
.map_err(|e| format!("Failed to copy bindings from {}: {}", bindings.display(), e))?;
emit_warning(format!(
"Using pre-generated bindings from: {}",
bindings.display()
));
emit_rustc_cfg("use_bindgen_pregenerated");
emit_warning(format!(
"Using system-installed AWS-LC from: {}",
self.layout.marker_dir.display()
));
if is_fips_build() {
link_fips_runtime_check(self.manifest_dir.as_path(), include_dir)?;
}
self.emit_link_directives(crypto_lib, include_dir, lib_dir);
Ok(())
}
fn emit_link_directives(&self, crypto_lib: &ResolvedLib, include_dir: &Path, lib_dir: &Path) {
let optional_ssl_lib = self.ssl_lib.borrow();
let kind = crypto_lib.lib_type.rust_lib_type();
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:libcrypto={}", crypto_lib.name);
println!("cargo:rustc-link-lib={kind}={}", crypto_lib.name);
if let Some(ssl_lib) = optional_ssl_lib.as_ref() {
println!("cargo:rustc-link-lib={kind}={}", ssl_lib.name);
println!("cargo:libssl={}", ssl_lib.name);
println!("cargo:rerun-if-changed={}", ssl_lib.path.display());
}
println!("cargo:include={}", include_dir.display());
println!("cargo:rerun-if-changed={}", include_dir.display());
println!("cargo:rerun-if-changed={}", crypto_lib.path.display());
}
}
const MINIMUM_AWS_LC_VERSION: &str = "1.68.0";
const MINIMUM_FIPS_VERSION: u32 = 3;
fn find_define<'a>(content: &'a str, name: &str) -> Option<&'a str> {
content.lines().find_map(|line| {
let mut tokens = line.split_whitespace();
if tokens.next() == Some("#define") && tokens.next() == Some(name) {
Some(tokens.next().unwrap_or(""))
} else {
None
}
})
}
fn read_and_validate_base_h(include_dir: &Path) -> Result<(PathBuf, String), String> {
let base_h = include_dir.join("openssl").join("base.h");
let content = std::fs::read_to_string(&base_h)
.map_err(|e| format!("Failed to read {}: {}", base_h.display(), e))?;
let has_awslc_marker = find_define(&content, "OPENSSL_IS_AWSLC").is_some();
if !has_awslc_marker {
return Err(format!(
"Headers at {} are not valid AWS-LC headers.\n\
The OPENSSL_IS_AWSLC marker was not found in base.h.\n\
Ensure the path contains AWS-LC headers, not OpenSSL or BoringSSL.",
include_dir.display()
));
}
Ok((base_h, content))
}
fn validate_and_extract_version(include_dir: &Path) -> Result<String, String> {
let (base_h, content) = read_and_validate_base_h(include_dir)?;
extract_version(&base_h, &content)
}
fn validate_and_resolve_fips_version(include_dir: &Path) -> Result<u32, String> {
let (base_h, content) = read_and_validate_base_h(include_dir)?;
resolve_fips_version(&base_h, &content)
}
fn extract_version(base_h: &Path, content: &str) -> Result<String, String> {
let Some(value) = find_define(content, "AWSLC_VERSION_NUMBER_STRING") else {
return Err(format!(
"Could not find AWSLC_VERSION_NUMBER_STRING in {}.",
base_h.display()
));
};
let trimmed = value.trim_matches('"');
if trimmed != value && !trimmed.is_empty() {
Ok(trimmed.to_string())
} else {
Err(format!(
"Malformed AWSLC_VERSION_NUMBER_STRING in {}",
base_h.display()
))
}
}
fn version_at_least(installed: &str, required: &str) -> Result<bool, String> {
let parse = |s: &str| -> Result<(u32, u32, u32), String> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return Err(format!("Invalid version format: {s}"));
}
Ok((
parts[0]
.parse()
.map_err(|_| format!("Invalid major: {}", parts[0]))?,
parts[1]
.parse()
.map_err(|_| format!("Invalid minor: {}", parts[1]))?,
parts[2]
.parse()
.map_err(|_| format!("Invalid patch: {}", parts[2]))?,
))
};
Ok(parse(installed)? >= parse(required)?)
}
fn resolve_fips_version(base_h: &Path, content: &str) -> Result<u32, String> {
if let Some(version) = extract_fips_version_number(content)? {
return Ok(version);
}
let version = extract_version(base_h, content)?;
version_major(&version)
}
fn extract_fips_version_number(content: &str) -> Result<Option<u32>, String> {
let Some(value) = find_define(content, "AWSLC_FIPS_VERSION_NUMBER") else {
return Ok(None);
};
value.parse::<u32>().map(Some).map_err(|_| {
let shown = if value.is_empty() { "<empty>" } else { value };
format!("Malformed AWSLC_FIPS_VERSION_NUMBER: expected an unsigned integer, found {shown}")
})
}
fn version_major(version: &str) -> Result<u32, String> {
version
.split('.')
.next()
.and_then(|major| major.parse::<u32>().ok())
.ok_or_else(|| format!("Invalid version format: {version}"))
}
const CRYPTO_LIB_CANDIDATES: &[&str] = &["crypto", "crypto-awslc"];
const SSL_LIB_CANDIDATES: &[&str] = &["ssl", "ssl-awslc"];
fn resolve_library(
lib_dir: &Path,
requested_lib_type: Option<OutputLibType>,
lib_kind: &str,
candidate_names: &[&str],
) -> Result<ResolvedLib, String> {
let preferred = requested_lib_type.unwrap_or_default();
if let Some(found) = find_candidate(lib_dir, preferred, candidate_names) {
return Ok(found);
}
let opposite = preferred.opposite();
let opposite_match = find_candidate(lib_dir, opposite, candidate_names);
if requested_lib_type.is_some() {
if opposite_match.is_some() {
return Err(format!(
"{var} is set to request {requested} linking, but only a {got} {lib_kind} library was found in {}.\n\
Expected one of: {}\n\
Provide the requested form at this location, or unset {var} to allow \
the available form.",
lib_dir.display(),
expected_lib_filenames(candidate_names, Some(preferred)),
requested = preferred.description(),
got = opposite.description(),
var = crate_env_var_name("STATIC"),
));
}
return Err(format!(
"{var} is set to request {requested} linking, but no {lib_kind} library was found in {}.\n\
Expected one of: {}",
lib_dir.display(),
expected_lib_filenames(candidate_names, Some(preferred)),
requested = preferred.description(),
var = crate_env_var_name("STATIC"),
));
}
if let Some(found) = opposite_match {
let desc = opposite.description();
emit_warning(format!(
"Only {desc} {lib_kind} library found in system install; using {desc} linking."
));
return Ok(found);
}
Err(format!(
"No {lib_kind} library found in {}\n\
Expected one of: {}",
lib_dir.display(),
expected_lib_filenames(candidate_names, None),
))
}
fn find_candidate(
lib_dir: &Path,
lib_type: OutputLibType,
candidates: &[&str],
) -> Option<ResolvedLib> {
candidates.iter().find_map(|name| {
probe_lib(lib_dir, name, lib_type).map(|path| ResolvedLib {
lib_type,
name: (*name).to_string(),
path,
})
})
}
fn is_msvc() -> bool {
matches!(
(target_os().as_str(), target_env().as_str()),
("windows", "msvc")
)
}
fn probe_lib(lib_dir: &Path, name: &str, lib_type: OutputLibType) -> Option<PathBuf> {
let want_dynamic = matches!(lib_type, OutputLibType::Dynamic);
let path = lib_dir.join(lib_filename(name, lib_type));
if !path.exists() {
return None;
}
if is_msvc() && msvc_has_sibling_dll(lib_dir, name) != want_dynamic {
return None;
}
Some(path)
}
fn lib_filename(base: &str, lib_type: OutputLibType) -> String {
if is_msvc() {
return format!("{base}.lib");
}
match lib_type {
OutputLibType::Static => format!("lib{base}.a"),
OutputLibType::Dynamic => match target_os().as_str() {
"windows" => format!("lib{base}.dll.a"),
"macos" | "ios" | "tvos" => format!("lib{base}.dylib"),
_ => format!("lib{base}.so"),
},
}
}
fn expected_lib_filenames(candidates: &[&str], filter: Option<OutputLibType>) -> String {
let lib_types: &[OutputLibType] = match filter {
Some(OutputLibType::Static) => &[OutputLibType::Static],
Some(OutputLibType::Dynamic) => &[OutputLibType::Dynamic],
None => &[OutputLibType::Static, OutputLibType::Dynamic],
};
let mut names: Vec<String> = Vec::new();
for < in lib_types {
for base in candidates {
let name = lib_filename(base, lt);
if !names.contains(&name) {
names.push(name);
}
}
}
names.join(", ")
}
fn msvc_has_sibling_dll(lib_dir: &Path, name: &str) -> bool {
lib_dir
.parent()
.is_some_and(|root| root.join("bin").join(format!("{name}.dll")).exists())
}
fn resolve_bindings(
bindings_prefix: Option<&Path>,
bindings_override: &Option<PathBuf>,
) -> Result<PathBuf, String> {
if let Some(path) = bindings_override {
if path.is_file() {
return Ok(path.clone());
}
return Err(format!(
"{} does not point to a file: {}",
crate_env_var_name("SYSTEM_BINDINGS"),
path.display()
));
}
let Some(prefix) = bindings_prefix else {
return Err(format!(
"No pre-generated bindings found for system-installed AWS-LC.\n\
The detected install did not expose an install prefix from which to\n\
locate share/rust/aws_lc_bindings.rs. Set {} to point at a bindings file.",
crate_env_var_name("SYSTEM_BINDINGS"),
));
};
let conventional = prefix.join("share").join("rust").join("aws_lc_bindings.rs");
if conventional.is_file() {
return Ok(conventional);
}
Err(format!(
"No pre-generated bindings found for system-installed AWS-LC.\n\n\
To resolve this, either:\n\
1. Install AWS-LC with Rust bindings (share/rust/aws_lc_bindings.rs) under {}, or\n\
2. Set {} to point to a bindings file",
prefix.display(),
crate_env_var_name("SYSTEM_BINDINGS"),
))
}
#[cfg(test)]
#[path = "system_library_tests.rs"]
mod tests;