diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c3d808dc1ad8..848ac1929fe3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -274,6 +274,46 @@ jobs: # See: https://github.com/rust-lang/cargo/issues/8531 run: cargo test -p aws-lc-rs --tests + prebuilt-install-test: + if: github.repository_owner == 'aws' + name: aws-lc-rs prebuilt install (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + # Windows is omitted: the prebuilt code paths are Windows-aware + # (find_static_lib/find_dynamic_lib handle .lib/.dll/.dll.a), but the + # integration script is bash-only and a Windows AWS-LC install would + # also require NASM setup. Add as a follow-up. + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, macos-15-intel, macos-latest ] + steps: + - uses: actions/checkout@v6 + with: + submodules: 'recursive' + - uses: dtolnay/rust-toolchain@stable + - name: Build and install AWS-LC (static) + run: | + cmake -S aws-lc-sys/aws-lc -B aws-lc-build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF \ + -DBUILD_TOOL=OFF \ + -DDISABLE_GO=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX="${PWD}/aws-lc-install" + cmake --build aws-lc-build --target install -j + - name: Build and install AWS-LC (shared) + run: | + cmake -S aws-lc-sys/aws-lc -B aws-lc-shared-build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF \ + -DBUILD_TOOL=OFF \ + -DDISABLE_GO=ON \ + -DBUILD_SHARED_LIBS=ON \ + -DCMAKE_INSTALL_PREFIX="${PWD}/aws-lc-install" + cmake --build aws-lc-shared-build --target install -j + - name: Run prebuilt integration tests + run: ./scripts/tests/test_prebuilt.sh "${PWD}/aws-lc-install" + build-prebuild-nasm-test: if: github.repository_owner == 'aws' name: prebuilt-nasm usage diff --git a/Cargo.toml b/Cargo.toml index 95042c422e9d..f1ab7f0ea95c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ openssl = "0.10.73" paste = "1.0.15" ring = "0.17.14" toml_edit = "0.25.0" +tempfile = "3" [profile.bench] lto = true diff --git a/aws-lc-sys/README.md b/aws-lc-sys/README.md index 034170462dfa..94ef2f6ab70c 100644 --- a/aws-lc-sys/README.md +++ b/aws-lc-sys/README.md @@ -63,6 +63,57 @@ For each PR submitted, [CI verifies](https://github.com/aws/aws-lc-rs/blob/main/.github/workflows/tests.yml) that the NASM objects newly built from source match the NASM objects currently in the repository. +## Linking against a prebuilt AWS-LC + +If you have an existing AWS-LC installation (built and installed via CMake), +you can link against it instead of building AWS-LC from the bundled source. +Set `AWS_LC_SYS_PREBUILT_INSTALL_DIR` to the install prefix: + +```shell +AWS_LC_SYS_PREBUILT_INSTALL_DIR=/path/to/aws-lc-install cargo build +``` + +The install directory must contain: + +* `include/openssl/base.h` — used to detect the `OPENSSL_IS_AWSLC` marker and + the AWS-LC version (`AWSLC_VERSION_NUMBER_STRING`). +* `lib/` (or `lib64/` for 64-bit targets, when present) containing `libcrypto`. + When the `ssl` feature is enabled, `libssl` is also required. + +Static vs. dynamic linking honors `AWS_LC_SYS_STATIC` (the same variable used +when building from source). When both static and dynamic libraries are present +the preferred form is selected; if only one is present it is used regardless +of the preference, with a warning. + +If a prefixed AWS-LC build is detected (via `include/openssl/boringssl_prefix_symbols.h`), +the prefix is extracted and applied automatically to library names and bindings. + +### Bindings for prebuilt installations + +When the prebuilt path is taken, bindings are resolved in this order: + +1. **`AWS_LC_SYS_PREBUILT_BINDINGS`** — explicit path to a pre-generated + `bindings.rs`. A misconfigured path is a hard error. +2. **`/share/rust/aws_lc_bindings.rs`** — populated by AWS-LC's + CMake install (AWS-LC v1.68.0+). See [aws-lc#2999](https://github.com/aws/aws-lc/pull/2999). +3. **Internal `bindgen`** — when the `bindgen` feature is enabled. +4. **External `bindgen-cli`** — when the `bindgen` binary is on `PATH`. + +If none of these are available the build fails with guidance on how to proceed. + +### Version compatibility + +The version embedded in the prebuilt headers must be greater than or equal to +the AWS-LC version bundled with this crate. To bypass this check (not +recommended), set `AWS_LC_SYS_PREBUILT_SKIP_VERSION_CHECK=1`. + +### Limitations + +Prebuilt linking is not supported for FIPS builds (`aws-lc-fips-sys`). +Validating that a prebuilt installation actually meets FIPS requirements is +non-trivial, so the build refuses rather than silently linking a non-FIPS +build into a FIPS crate. + ## Build Prerequisites Since this crate builds AWS-LC as a native library, most build tools needed to build AWS-LC are applicable diff --git a/builder-test/Cargo.toml b/builder-test/Cargo.toml index 3da531ec70e9..0f0abf94f38b 100644 --- a/builder-test/Cargo.toml +++ b/builder-test/Cargo.toml @@ -5,15 +5,13 @@ edition = "2021" publish = false description = "Test harness for aws-lc-sys builder modules - does not duplicate code" -# Silence warnings that are expected when compiling builder code as a library. -# The builder code is designed to run as a build script, so many functions -# appear "unused" when compiled as a library for testing purposes. +# This crate exists solely to run unit tests defined in builder/main.rs and its +# modules. The builder code normally compiles only as a build script, so many +# functions appear "unused" when compiled as a library here. [lints.rust] dead_code = "allow" unused = "allow" -# Point to the actual builder/main.rs as the library source -# The #[cfg(test)] module in main.rs will be compiled and run [lib] name = "builder" path = "../builder/main.rs" @@ -26,6 +24,9 @@ dunce = { workspace = true } fs_extra = { workspace = true } bindgen = { workspace = true, optional = true } +[dev-dependencies] +tempfile = { workspace = true } + [features] default = [] # Mirror the features from aws-lc-sys that affect builder compilation diff --git a/builder/main.rs b/builder/main.rs index 41615db2c050..8750ae65ca7d 100644 --- a/builder/main.rs +++ b/builder/main.rs @@ -75,6 +75,7 @@ const OSSL_CONF_DEFINES: &[&str] = &[ mod cc_builder; mod cmake_builder; mod nasm_builder; +mod prebuilt; #[cfg(any(feature = "bindgen", feature = "fips"))] mod sys_bindgen; @@ -391,6 +392,7 @@ fn generate_bindings(manifest_dir: &Path, prefix: &Option, bindings_path build_prefix: prefix.clone(), include_ssl: cfg!(feature = "ssl"), disable_prelude: true, + external_include_dir: None, }; let bindings = sys_bindgen::generate_bindings(manifest_dir, &options); @@ -621,6 +623,9 @@ static mut SYS_SANITIZER: Option = None; static mut SYS_C_STD: CStdRequested = CStdRequested::None; fn initialize() { + // Initialize prebuilt configuration first (reads PREBUILT_INSTALL_DIR env var) + prebuilt::initialize(); + unsafe { SYS_NO_PREFIX = env_crate_var_to_bool("NO_PREFIX").unwrap_or(false); SYS_PREGENERATING_BINDINGS = @@ -945,18 +950,20 @@ fn is_crt_static() -> bool { #[cfg(any(feature = "bindgen", feature = "fips"))] fn handle_bindgen(manifest_dir: &Path, prefix: &Option) -> bool { - if internal_bindgen_supported() && !is_external_bindgen_requested().unwrap_or(false) { - emit_warning(format!( - "Generating bindings - internal bindgen. Platform: {}", - effective_target() - )); - let gen_bindings_path = out_dir().join("bindings.rs"); - generate_bindings(manifest_dir, prefix, &gen_bindings_path); - emit_rustc_cfg("use_bindgen_pregenerated"); - true - } else { - false + if is_external_bindgen_requested().unwrap_or(false) { + return false; } + if !internal_bindgen_supported() { + return false; + } + emit_warning(format!( + "Generating bindings - internal bindgen. Platform: {}", + effective_target() + )); + let gen_bindings_path = out_dir().join("bindings.rs"); + generate_bindings(manifest_dir, prefix, &gen_bindings_path); + emit_rustc_cfg("use_bindgen_pregenerated"); + true } #[cfg(not(any(feature = "bindgen", feature = "fips")))] @@ -972,12 +979,30 @@ fn canonicalized_manifest_dir() -> PathBuf { manifest_dir } +#[cfg(any(feature = "bindgen", feature = "fips"))] +fn handle_pregenerating_bindings(manifest_dir: &Path, prefix: &Option) { + assert!( + !is_external_bindgen_requested().unwrap_or(false), + "Pregenerated bindings not supported using external bindgen.", + ); + let src_bindings_path = Path::new(manifest_dir) + .join("src") + .join(format!("{}.rs", target_platform_prefix("crypto"))); + generate_src_bindings(manifest_dir, prefix, &src_bindings_path); +} + #[cfg(not(test))] fn main() { initialize(); prepare_cargo_cfg(); let manifest_dir = canonicalized_manifest_dir(); + + if let Some(config) = prebuilt::get_config() { + handle_prebuilt_build(config, &manifest_dir); + return; + } + let prefix = (!is_no_prefix()).then(prefix_string); let builder = get_builder(&prefix, &manifest_dir, &out_dir()); @@ -992,18 +1017,7 @@ fn main() { if is_pregenerating_bindings() { #[cfg(any(feature = "bindgen", feature = "fips"))] { - let src_bindings_path = Path::new(&manifest_dir) - .join("src") - .join(format!("{}.rs", target_platform_prefix("crypto"))); - if is_external_bindgen_requested().unwrap_or(false) { - assert!( - !is_pregenerating_bindings(), - "Pregenerated bindings not supported using external bindgen.", - ); - invoke_external_bindgen(&manifest_dir, &prefix, &src_bindings_path).unwrap(); - } else { - generate_src_bindings(&manifest_dir, &prefix, &src_bindings_path); - } + handle_pregenerating_bindings(&manifest_dir, &prefix); bindings_available = true; } } else if is_bindgen_required() { @@ -1048,7 +1062,15 @@ fn main() { "Bindgen currently cannot generate prefixed bindings w/o the \\x01 prefix.", ); let gen_bindings_path = out_dir().join("bindings.rs"); - let result = invoke_external_bindgen(&manifest_dir, &prefix, &gen_bindings_path); + let result = invoke_external_bindgen( + &manifest_dir, + &BindingOptions { + disable_prelude: true, + ..Default::default() + }, + &prefix, + &gen_bindings_path, + ); match result { Ok(()) => { emit_rustc_cfg("use_bindgen_pregenerated"); @@ -1083,6 +1105,65 @@ fn main() { println!("cargo:rerun-if-changed=aws-lc/"); } +fn handle_prebuilt_build(config: &prebuilt::Config, manifest_dir: &Path) { + use prebuilt::PrebuiltBuilder; + + let include_dir = config.install_dir.join("include"); + + let prefix = prebuilt::validate_installation(&include_dir, config.skip_version_check) + .unwrap_or_else(|e| panic!("Prebuilt validation failed: {e}")); + + let builder = PrebuiltBuilder::new(config.install_dir.clone(), prefix.clone()) + .unwrap_or_else(|e| panic!("Prebuilt configuration failed: {e}")); + + builder + .check_dependencies() + .unwrap_or_else(|e| panic!("Prebuilt library check failed: {e}")); + + prebuilt::handle_prebuilt_bindings( + config, + builder.include_dir(), + manifest_dir, + &out_dir(), + &prefix, + ) + .unwrap_or_else(|e| panic!("Prebuilt bindings failed: {e}")); + + emit_rustc_cfg("use_bindgen_pregenerated"); + + builder + .build() + .unwrap_or_else(|e| panic!("Prebuilt linking failed: {e}")); + + println!("cargo:include={}", builder.include_dir().display()); + println!("cargo:libcrypto={}", builder.crypto_lib_name()); + if cfg!(feature = "ssl") { + println!("cargo:libssl={}", builder.ssl_lib_name()); + } + println!("cargo:conf={}", OSSL_CONF_DEFINES.join(",")); + + println!("cargo:rerun-if-changed=builder/"); + println!("cargo:rerun-if-changed={}", builder.include_dir().display()); + for lib_path in builder.library_file_paths() { + println!("cargo:rerun-if-changed={}", lib_path.display()); + } + // These are already registered by optional_env() during initialization, but + // listed explicitly so the set of inputs is visible in one place. + // `STATIC` selects between static and dynamic linking; the rest configure + // the prebuilt path itself. + for env_var in [ + "PREBUILT_INSTALL_DIR", + "PREBUILT_BINDINGS", + "PREBUILT_SKIP_VERSION_CHECK", + "STATIC", + ] { + println!( + "cargo:rerun-if-env-changed={}", + prebuilt::crate_env_var_name(env_var) + ); + } +} + fn setup_include_paths(out_dir: &Path, manifest_dir: &Path) -> PathBuf { let mut include_paths = vec![ get_rust_include_path(manifest_dir), @@ -1127,6 +1208,9 @@ pub(crate) struct BindingOptions { pub build_prefix: Option, pub include_ssl: bool, pub disable_prelude: bool, + /// When set, bindgen uses these headers instead of the bundled AWS-LC source. + /// Currently used by the prebuilt linking path to point at an installed `include/`. + pub external_include_dir: Option, } impl Debug for BindingOptions { @@ -1135,6 +1219,7 @@ impl Debug for BindingOptions { .field("build_prefix", &self.build_prefix) .field("include_ssl", &self.include_ssl) .field("disable_prelude", &self.disable_prelude) + .field("external_include_dir", &self.external_include_dir) .finish() } } @@ -1219,6 +1304,7 @@ const PRELUDE: &str = r" fn invoke_external_bindgen( manifest_dir: &Path, + options: &BindingOptions, prefix: &Option, gen_bindings_path: &Path, ) -> Result<(), String> { @@ -1231,14 +1317,7 @@ fn invoke_external_bindgen( let _guard_target = EnvGuard::new("TARGET", effective_target()); - let options = BindingOptions { - // We collect the symbols w/o the prefix added - build_prefix: None, - include_ssl: false, - disable_prelude: true, - }; - - let clang_args = prepare_clang_args(manifest_dir, &options); + let clang_args = prepare_clang_args(manifest_dir, options); let header = get_rust_include_path(manifest_dir) .join("rust_wrapper.h") .display() @@ -1295,6 +1374,11 @@ fn invoke_external_bindgen( for x in &clang_args { bindgen_params.push(x.as_str()); } + + // Add SSL define as a clang arg (after the -- separator) + if options.include_ssl { + bindgen_params.push("-DAWS_LC_RUST_INCLUDE_SSL"); + } let cmd_params: Vec = bindgen_params.iter().map(OsString::from).collect(); let cmd_params: Vec<&OsStr> = cmd_params.iter().map(OsString::as_os_str).collect(); @@ -1321,28 +1405,34 @@ fn add_header_include_path(args: &mut Vec, path: String) { fn prepare_clang_args(manifest_dir: &Path, options: &BindingOptions) -> Vec { let mut clang_args: Vec = Vec::new(); + // Always include rust_wrapper.h location from bundled source add_header_include_path( &mut clang_args, get_rust_include_path(manifest_dir).display().to_string(), ); - if options.build_prefix.is_some() { - // NOTE: It's possible that the prefix embedded in the header files doesn't match the prefix - // specified. This only happens when the version number as changed in Cargo.toml, but the - // new headers have not yet been generated. + // When an external include dir is provided (e.g., a prebuilt AWS-LC install), + // use it instead of the bundled headers. + if let Some(ref external_include) = options.external_include_dir { + add_header_include_path(&mut clang_args, external_include.display().to_string()); + } else { + if options.build_prefix.is_some() { + // NOTE: It's possible that the prefix embedded in the header files doesn't match the prefix + // specified. This only happens when the version number as changed in Cargo.toml, but the + // new headers have not yet been generated. + add_header_include_path( + &mut clang_args, + get_generated_include_path(manifest_dir) + .display() + .to_string(), + ); + } add_header_include_path( &mut clang_args, - get_generated_include_path(manifest_dir) - .display() - .to_string(), + get_aws_lc_include_path(manifest_dir).display().to_string(), ); } - add_header_include_path( - &mut clang_args, - get_aws_lc_include_path(manifest_dir).display().to_string(), - ); - if let Some(include_paths) = get_env_includes_path() { for path in include_paths { add_header_include_path(&mut clang_args, path.display().to_string()); diff --git a/builder/prebuilt.rs b/builder/prebuilt.rs new file mode 100644 index 000000000000..9177a39cd3af --- /dev/null +++ b/builder/prebuilt.rs @@ -0,0 +1,1018 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +//! Support for linking against a prebuilt AWS-LC installation. + +use crate::{ + emit_warning, get_aws_lc_include_path, is_fips_build, optional_env_crate_target, target_env, + target_os, OutputLibType, +}; +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; + +/// Configuration for prebuilt AWS-LC linking mode. +pub(crate) struct Config { + /// Path to the AWS-LC installation directory + pub install_dir: PathBuf, + /// Optional path to pre-generated Rust bindings file (from `PREBUILT_BINDINGS` env var) + pub bindings_override: Option, + /// Whether to skip version compatibility check + pub skip_version_check: bool, +} + +static SYS_CONFIG: OnceLock> = OnceLock::new(); + +/// Formats the full environment variable name for the current crate + given +/// suffix (e.g. `"PREBUILT_INSTALL_DIR"` → `"AWS_LC_SYS_PREBUILT_INSTALL_DIR"`). +/// Does NOT read the variable — use `optional_env_crate_target` for that. +pub(crate) fn crate_env_var_name(name: &str) -> String { + let crate_name = crate::crate_name().to_uppercase().replace('-', "_"); + format!("{crate_name}_{name}") +} + +pub(crate) fn initialize() { + SYS_CONFIG.get_or_init(load_config); +} + +fn load_config() -> Option { + let install_dir = match optional_env_crate_target("PREBUILT_INSTALL_DIR") { + Some(dir) if !dir.is_empty() => PathBuf::from(dir), + _ => return None, + }; + + // Prebuilt linking is not supported for FIPS builds: validating that a + // prebuilt installation actually meets FIPS requirements is non-trivial, + // so refuse rather than silently link a non-FIPS build into a FIPS crate. + assert!( + !is_fips_build(), + "{} is set, but prebuilt linking is not supported for FIPS builds.", + crate_env_var_name("PREBUILT_INSTALL_DIR"), + ); + + let bindings_override = optional_env_crate_target("PREBUILT_BINDINGS") + .filter(|v| !v.is_empty()) + .map(PathBuf::from); + + let skip_version_check = optional_env_crate_target("PREBUILT_SKIP_VERSION_CHECK") + .is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true")); + + Some(Config { + install_dir, + bindings_override, + skip_version_check, + }) +} + +pub(crate) fn get_config() -> Option<&'static Config> { + SYS_CONFIG.get().and_then(Option::as_ref) +} + +fn detect_prefix(include_dir: &Path) -> Option { + let prefix_header = include_dir + .join("openssl") + .join("boringssl_prefix_symbols.h"); + + let content = std::fs::read_to_string(&prefix_header).ok()?; + + // Look for: #define BORINGSSL_PREFIX + for line in content.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 3 && parts[0] == "#define" && parts[1] == "BORINGSSL_PREFIX" { + return Some(parts[2].to_string()); + } + } + + None +} + +/// Applies the optional symbol prefix to a base library name (e.g. `crypto` → +/// `my_prefix_crypto` when a prefix is set, otherwise just `crypto`). +fn prefixed_lib_name(prefix: &Option, base: &str) -> String { + match prefix { + Some(p) => format!("{p}_{base}"), + None => base.to_string(), + } +} + +fn validate_and_extract_version(include_dir: &Path) -> Result { + 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))?; + + // Verify this is AWS-LC (not OpenSSL or BoringSSL) + if !content.contains("OPENSSL_IS_AWSLC") { + 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() + )); + } + + // Extract version: look for #define AWSLC_VERSION_NUMBER_STRING "X.Y.Z" + for line in content.lines() { + let line = line.trim(); + if line.contains("AWSLC_VERSION_NUMBER_STRING") && line.contains('"') { + if let Some(start) = line.find('"') { + if let Some(end) = line[start + 1..].find('"') { + return Ok(line[start + 1..start + 1 + end].to_string()); + } + } + } + } + + Err(format!( + "Could not find AWSLC_VERSION_NUMBER_STRING in {}.\n\ + The file appears to be AWS-LC headers but version could not be determined.", + base_h.display() + )) +} + +fn parse_version(version_str: &str) -> Result<(u32, u32, u32), String> { + let parts: Vec<&str> = version_str.split('.').collect(); + if parts.len() != 3 { + return Err(format!("Invalid version format: {version_str}")); + } + Ok(( + parts[0] + .parse() + .map_err(|_| format!("Invalid major version: {}", parts[0]))?, + parts[1] + .parse() + .map_err(|_| format!("Invalid minor version: {}", parts[1]))?, + parts[2] + .parse() + .map_err(|_| format!("Invalid patch version: {}", parts[2]))?, + )) +} + +fn version_compatible(installed: &str, required: &str) -> Result { + let (i_maj, i_min, i_pat) = parse_version(installed)?; + let (r_maj, r_min, r_pat) = parse_version(required)?; + + Ok((i_maj, i_min, i_pat) >= (r_maj, r_min, r_pat)) +} + +fn get_bundled_awslc_version() -> String { + let manifest_dir = crate::current_dir(); + let bundled_include = get_aws_lc_include_path(&manifest_dir); + validate_and_extract_version(&bundled_include).unwrap_or_else(|e| { + panic!( + "Failed to determine bundled AWS-LC version from {}: {e}", + bundled_include.display() + ) + }) +} + +pub(crate) fn validate_installation( + include_dir: &Path, + skip_version_check: bool, +) -> Result, String> { + // Get the appropriate env var name for error messages + let install_dir_env_var = crate_env_var_name("PREBUILT_INSTALL_DIR"); + + // 1. Verify include directory exists + if !include_dir.exists() { + return Err(format!( + "Include directory not found: {}\n\ + Verify {} points to a valid installation.", + include_dir.display(), + install_dir_env_var + )); + } + + // 2. Read base.h and validate AWS-LC headers + extract version in one pass + let version = validate_and_extract_version(include_dir)?; + + // 3. Check version compatibility + let required_version = get_bundled_awslc_version(); + let compatible = version_compatible(&version, &required_version)?; + if !compatible { + if skip_version_check { + emit_warning(format!( + "WARNING: Skipping version check. Installed {version} < required {required_version}. \ + This may cause runtime issues." + )); + } else { + let env_prefix = install_dir_env_var.trim_end_matches("_INSTALL_DIR"); + return Err(format!( + "AWS-LC version mismatch: installed {version} < required {required_version}.\n\ + Please upgrade AWS-LC or unset {install_dir_env_var} to build from source.\n\ + To bypass this check (not recommended), set {env_prefix}_SKIP_VERSION_CHECK=1" + )); + } + } + + // 4. Detect symbol prefix + let prefix = detect_prefix(include_dir); + + emit_warning(format!( + "Prebuilt AWS-LC: version={}, prefix={:?}", + version, + prefix.as_deref().unwrap_or("none") + )); + + Ok(prefix) +} + +/// Builder for linking against a prebuilt AWS-LC installation. +pub(crate) struct PrebuiltBuilder { + install_dir: PathBuf, + include_dir: PathBuf, + lib_dir: PathBuf, + output_lib_type: OutputLibType, + prefix: Option, +} + +impl PrebuiltBuilder { + pub(crate) fn new(install_dir: PathBuf, prefix: Option) -> Result { + let include_dir = install_dir.join("include"); + + // Check lib64 first (RHEL, Fedora, SUSE use lib64 for 64-bit libraries). + // Use CARGO_CFG_TARGET_POINTER_WIDTH (target) rather than cfg!() (host) + // to correctly handle cross-compilation scenarios. + let target_is_64bit = std::env::var("CARGO_CFG_TARGET_POINTER_WIDTH") + .map(|w| w == "64") + .unwrap_or(cfg!(target_pointer_width = "64")); + let lib_dir = if install_dir.join("lib64").exists() && target_is_64bit { + install_dir.join("lib64") + } else { + install_dir.join("lib") + }; + + if !lib_dir.exists() { + return Err(format!( + "Library directory not found: {} (also checked lib64/)\n\ + Expected AWS-LC libraries at this location.\n\ + Verify {} points to a valid AWS-LC installation.", + install_dir.join("lib").display(), + crate_env_var_name("PREBUILT_INSTALL_DIR") + )); + } + + let output_lib_type = Self::detect_lib_type(&lib_dir, &prefix)?; + + Ok(Self { + install_dir, + include_dir, + lib_dir, + output_lib_type, + prefix, + }) + } + + pub(crate) fn include_dir(&self) -> &Path { + &self.include_dir + } + + #[cfg(test)] + pub(crate) fn lib_dir(&self) -> &Path { + &self.lib_dir + } + + #[cfg(test)] + pub(crate) fn prefix(&self) -> Option<&str> { + self.prefix.as_deref() + } + + #[cfg(test)] + pub(crate) fn output_lib_type(&self) -> OutputLibType { + self.output_lib_type + } + + pub(crate) fn library_file_paths(&self) -> Vec { + let mut paths = Vec::new(); + if let Some(p) = self.find_lib(&self.crypto_lib_name()) { + paths.push(p); + } + if cfg!(feature = "ssl") { + if let Some(p) = self.find_lib(&self.ssl_lib_name()) { + paths.push(p); + } + } + paths + } + + fn find_lib(&self, name: &str) -> Option { + match self.output_lib_type { + OutputLibType::Static => Self::find_static_lib(&self.lib_dir, name), + OutputLibType::Dynamic => Self::find_dynamic_lib(&self.lib_dir, name), + } + } + + pub(crate) fn crypto_lib_name(&self) -> String { + prefixed_lib_name(&self.prefix, "crypto") + } + + pub(crate) fn ssl_lib_name(&self) -> String { + prefixed_lib_name(&self.prefix, "ssl") + } + + fn detect_lib_type(lib_dir: &Path, prefix: &Option) -> Result { + let crypto_name = prefixed_lib_name(prefix, "crypto"); + + let static_exists = Self::find_static_lib(lib_dir, &crypto_name).is_some(); + let dynamic_exists = Self::find_dynamic_lib(lib_dir, &crypto_name).is_some(); + + // Get user's preference (respects AWS_LC_SYS_STATIC env var) + let preferred = OutputLibType::default(); + + match (static_exists, dynamic_exists, preferred) { + // User explicitly wants static and it exists + (true, _, OutputLibType::Static) => Ok(OutputLibType::Static), + // User explicitly wants dynamic and it exists + (_, true, OutputLibType::Dynamic) => Ok(OutputLibType::Dynamic), + // User wants static but only dynamic exists + (false, true, OutputLibType::Static) => { + emit_warning( + "Static library requested but not found, falling back to dynamic linking", + ); + Ok(OutputLibType::Dynamic) + } + // User wants dynamic but only static exists + (true, false, OutputLibType::Dynamic) => { + emit_warning( + "Dynamic library requested but not found, falling back to static linking", + ); + Ok(OutputLibType::Static) + } + // Neither library found + (false, false, _) => { + let expected = match prefix { + Some(p) => format!("lib{p}_crypto.a, lib{p}_crypto.so, {p}_crypto.lib, etc."), + None => "libcrypto.a, libcrypto.so, crypto.lib, etc.".to_string(), + }; + Err(format!( + "No crypto library found in {}\nExpected: {}", + lib_dir.display(), + expected + )) + } + } + } + + fn find_static_lib(lib_dir: &Path, name: &str) -> Option { + let candidates = match (target_os().as_str(), target_env().as_str()) { + ("windows", "msvc") => vec![format!("{}.lib", name)], + _ => vec![format!("lib{}.a", name)], + }; + candidates + .into_iter() + .map(|c| lib_dir.join(c)) + .find(|p| p.exists()) + } + + fn find_dynamic_lib(lib_dir: &Path, name: &str) -> Option { + let candidates = match target_os().as_str() { + "windows" => vec![format!("{}.dll", name), format!("lib{}.dll.a", name)], + "macos" | "ios" | "tvos" => vec![format!("lib{}.dylib", name)], + _ => vec![format!("lib{}.so", name)], + }; + candidates + .into_iter() + .map(|c| lib_dir.join(c)) + .find(|p| p.exists()) + } + + fn verify_library_exists(&self, base_name: &str) -> Result<(), String> { + let name = prefixed_lib_name(&self.prefix, base_name); + + if self.find_lib(&name).is_none() { + return Err(format!( + "Required library '{}' not found in {}", + name, + self.lib_dir.display() + )); + } + Ok(()) + } +} + +impl crate::Builder for PrebuiltBuilder { + fn check_dependencies(&self) -> Result<(), String> { + // crypto is already verified by detect_lib_type() during PrebuiltBuilder::new. + if cfg!(feature = "ssl") { + self.verify_library_exists("ssl")?; + } + Ok(()) + } + + fn build(&self) -> Result<(), String> { + emit_warning(format!( + "Using prebuilt AWS-LC from: {}", + self.install_dir.display() + )); + + println!("cargo:rustc-link-search=native={}", self.lib_dir.display()); + + println!( + "cargo:rustc-link-lib={}={}", + self.output_lib_type.rust_lib_type(), + self.crypto_lib_name() + ); + + if cfg!(feature = "ssl") { + println!( + "cargo:rustc-link-lib={}={}", + self.output_lib_type.rust_lib_type(), + self.ssl_lib_name() + ); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + "Prebuilt" + } +} + +fn find_prebuilt_bindings(config: &Config) -> Result, String> { + // 1. Explicit override takes priority. A misconfigured path is a hard error + // rather than a silent fall-through to bindgen, since the user clearly + // asked for these specific bindings. + if let Some(ref path) = config.bindings_override { + if path.is_file() { + return Ok(Some(path.clone())); + } + return Err(format!( + "{} does not point to a file: {}", + crate_env_var_name("PREBUILT_BINDINGS"), + path.display() + )); + } + + // 2. Check conventional location populated by AWS-LC's CMake install + // (see https://github.com/aws/aws-lc/pull/2999, AWS-LC v1.68.0+). + let conventional = config + .install_dir + .join("share") + .join("rust") + .join("aws_lc_bindings.rs"); + if conventional.exists() { + return Ok(Some(conventional)); + } + + Ok(None) +} + +pub(crate) fn handle_prebuilt_bindings( + config: &Config, + include_dir: &Path, + manifest_dir: &Path, + out_dir: &Path, + prefix: &Option, +) -> Result<(), String> { + let dest = out_dir.join("bindings.rs"); + + if let Some(bindings_path) = find_prebuilt_bindings(config)? { + std::fs::copy(&bindings_path, &dest).map_err(|e| { + format!( + "Failed to copy bindings from {}: {}", + bindings_path.display(), + e + ) + })?; + emit_warning(format!( + "Using prebuilt bindings from: {}", + bindings_path.display() + )); + return Ok(()); + } + + generate_bindings_with_bindgen(include_dir, manifest_dir, out_dir, prefix) +} + +fn generate_bindings_with_bindgen( + include_dir: &Path, + manifest_dir: &Path, + out_dir: &Path, + prefix: &Option, +) -> Result<(), String> { + // Try internal bindgen first (when bindgen crate is available) + #[cfg(any(feature = "bindgen", feature = "fips"))] + { + if crate::internal_bindgen_supported() + && !crate::is_external_bindgen_requested().unwrap_or(false) + { + emit_warning(format!( + "Generating bindings for prebuilt AWS-LC (prefix: {:?})", + prefix.as_deref().unwrap_or("none") + )); + + let options = crate::BindingOptions { + build_prefix: prefix.clone(), + include_ssl: cfg!(feature = "ssl"), + disable_prelude: true, + external_include_dir: Some(include_dir.to_path_buf()), + }; + + let bindings = crate::sys_bindgen::generate_bindings(manifest_dir, &options); + let bindings_path = out_dir.join("bindings.rs"); + bindings + .write_to_file(&bindings_path) + .map_err(|e| format!("Failed to write bindings: {e}"))?; + + return Ok(()); + } + } + + // Try external bindgen-cli as fallback + if try_external_bindgen(include_dir, manifest_dir, out_dir, prefix)? { + return Ok(()); + } + + // Neither internal nor external bindgen available + let bindings_env_var = crate_env_var_name("PREBUILT_BINDINGS"); + Err(format!( + "No pre-generated bindings found and bindgen is not available.\n\n\ + To resolve this, either:\n\ + 1. Install AWS-LC with Rust bindings (share/rust/aws_lc_bindings.rs), or\n\ + 2. Set {bindings_env_var} to point to a bindings file, or\n\ + 3. Enable the 'bindgen' feature: cargo build --features bindgen, or\n\ + 4. Install bindgen-cli: cargo install bindgen-cli" + )) +} + +fn try_external_bindgen( + include_dir: &Path, + manifest_dir: &Path, + out_dir: &Path, + prefix: &Option, +) -> Result { + // Check if external bindgen is available by testing the command + if !test_bindgen_cli_command() { + return Ok(false); + } + + emit_warning(format!( + "Generating bindings for prebuilt AWS-LC via bindgen-cli (prefix: {:?})", + prefix.as_deref().unwrap_or("none") + )); + + // External bindgen collects symbols without the prefix applied—the prefix + // is resolved at link time via `--prefix-link-name`. This matches the + // non-prebuilt external bindgen behavior in invoke_external_bindgen(). + let options = crate::BindingOptions { + build_prefix: None, + include_ssl: cfg!(feature = "ssl"), + disable_prelude: true, + external_include_dir: Some(include_dir.to_path_buf()), + }; + + let bindings_path = out_dir.join("bindings.rs"); + + // Use existing invoke_external_bindgen infrastructure + crate::invoke_external_bindgen(manifest_dir, &options, prefix, &bindings_path) + .map_err(|e| format!("External bindgen failed: {e}"))?; + + Ok(true) +} + +fn test_bindgen_cli_command() -> bool { + use std::ffi::OsStr; + crate::execute_command(OsStr::new("bindgen"), &[OsStr::new("--version")]).status +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use std::sync::{Mutex, MutexGuard}; + + /// Mutex to serialize tests that modify environment variables. + /// Environment variables are process-global state, so tests that modify them + /// must not run in parallel. + static ENV_MUTEX: Mutex<()> = Mutex::new(()); + + /// Sets up Cargo environment variables needed for tests that call target_os(), target_env(), etc. + /// Returns a guard that restores the original environment when dropped. + /// The guard also holds a mutex lock to prevent parallel test execution. + fn setup_test_env() -> impl Drop { + struct EnvGuard<'a> { + vars: Vec<(String, Option)>, + _lock: MutexGuard<'a, ()>, + } + impl Drop for EnvGuard<'_> { + fn drop(&mut self) { + for (key, original) in &self.vars { + match original { + Some(val) => unsafe { std::env::set_var(key, val) }, + None => unsafe { std::env::remove_var(key) }, + } + } + // _lock is dropped here, releasing the mutex + } + } + + // Acquire lock first to ensure exclusive access to env vars. + // This serializes tests that modify environment variables across threads. + // + // WARNING: Do not call setup_test_env() multiple times in the same scope/thread. + // std::sync::Mutex is not reentrant, so a second call from the same thread will + // deadlock waiting for the lock held by the first call. + let lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let vars_to_set = [ + ("CARGO_CFG_TARGET_OS", std::env::consts::OS), + ("CARGO_CFG_TARGET_ENV", ""), + ("CARGO_CFG_TARGET_FEATURE", ""), + ("CARGO_CFG_TARGET_ARCH", std::env::consts::ARCH), + ("CARGO_CFG_TARGET_POINTER_WIDTH", "64"), + ("TARGET", std::env::consts::ARCH), // Simplified target for testing + ("CARGO_PKG_NAME", "aws-lc-sys"), // Required by crate_name() + ]; + + let mut guard = EnvGuard { + vars: Vec::new(), + _lock: lock, + }; + for (key, val) in vars_to_set { + guard.vars.push((key.to_string(), std::env::var(key).ok())); + unsafe { std::env::set_var(key, val) }; + } + guard + } + + // ------------------------------------------------------------------------- + // parse_version tests + // ------------------------------------------------------------------------- + + #[test] + fn test_parse_version_valid() { + assert_eq!(parse_version("1.2.3").unwrap(), (1, 2, 3)); + assert_eq!(parse_version("0.0.0").unwrap(), (0, 0, 0)); + assert_eq!(parse_version("10.20.30").unwrap(), (10, 20, 30)); + } + + #[test] + fn test_parse_version_invalid() { + assert!(parse_version("1.2").is_err()); + assert!(parse_version("1.2.3.4").is_err()); + assert!(parse_version("a.b.c").is_err()); + assert!(parse_version("").is_err()); + assert!(parse_version("1.2.").is_err()); + } + + // ------------------------------------------------------------------------- + // version_compatible tests + // ------------------------------------------------------------------------- + + #[test] + fn test_version_compatible_equal() { + assert!(version_compatible("1.2.3", "1.2.3").unwrap()); + } + + #[test] + fn test_version_compatible_newer_major() { + assert!(version_compatible("2.0.0", "1.9.9").unwrap()); + } + + #[test] + fn test_version_compatible_newer_minor() { + assert!(version_compatible("1.3.0", "1.2.9").unwrap()); + } + + #[test] + fn test_version_compatible_newer_patch() { + assert!(version_compatible("1.2.4", "1.2.3").unwrap()); + } + + #[test] + fn test_version_compatible_older_major() { + assert!(!version_compatible("1.0.0", "2.0.0").unwrap()); + } + + #[test] + fn test_version_compatible_older_minor() { + assert!(!version_compatible("1.1.0", "1.2.0").unwrap()); + } + + #[test] + fn test_version_compatible_older_patch() { + assert!(!version_compatible("1.2.2", "1.2.3").unwrap()); + } + + // ------------------------------------------------------------------------- + // detect_prefix tests + // ------------------------------------------------------------------------- + + #[test] + fn test_detect_prefix_with_prefix() { + let temp_dir = tempfile::tempdir().unwrap(); + let openssl_dir = temp_dir.path().join("openssl"); + std::fs::create_dir_all(&openssl_dir).unwrap(); + + let header_path = openssl_dir.join("boringssl_prefix_symbols.h"); + let mut file = std::fs::File::create(&header_path).unwrap(); + writeln!(file, "#ifndef BORINGSSL_PREFIX_SYMBOLS_H").unwrap(); + writeln!(file, "#define BORINGSSL_PREFIX_SYMBOLS_H").unwrap(); + writeln!(file, "#define BORINGSSL_PREFIX my_custom_prefix").unwrap(); + writeln!(file, "#endif").unwrap(); + + let prefix = detect_prefix(temp_dir.path()); + assert_eq!(prefix, Some("my_custom_prefix".to_string())); + } + + #[test] + fn test_detect_prefix_without_header() { + let temp_dir = tempfile::tempdir().unwrap(); + let prefix = detect_prefix(temp_dir.path()); + assert_eq!(prefix, None); + } + + #[test] + fn test_detect_prefix_header_without_define() { + let temp_dir = tempfile::tempdir().unwrap(); + let openssl_dir = temp_dir.path().join("openssl"); + std::fs::create_dir_all(&openssl_dir).unwrap(); + + let header_path = openssl_dir.join("boringssl_prefix_symbols.h"); + let mut file = std::fs::File::create(&header_path).unwrap(); + writeln!(file, "#ifndef BORINGSSL_PREFIX_SYMBOLS_H").unwrap(); + writeln!(file, "#define BORINGSSL_PREFIX_SYMBOLS_H").unwrap(); + writeln!(file, "// No BORINGSSL_PREFIX defined").unwrap(); + writeln!(file, "#endif").unwrap(); + + let prefix = detect_prefix(temp_dir.path()); + assert_eq!(prefix, None); + } + + // ------------------------------------------------------------------------- + // validate_and_extract_version tests + // ------------------------------------------------------------------------- + + #[test] + fn test_validate_and_extract_version_valid() { + let temp_dir = tempfile::tempdir().unwrap(); + let openssl_dir = temp_dir.path().join("openssl"); + std::fs::create_dir_all(&openssl_dir).unwrap(); + + let base_h = openssl_dir.join("base.h"); + let mut file = std::fs::File::create(&base_h).unwrap(); + writeln!(file, "#ifndef OPENSSL_BASE_H").unwrap(); + writeln!(file, "#define OPENSSL_BASE_H").unwrap(); + writeln!(file, "#define OPENSSL_IS_AWSLC 1").unwrap(); + writeln!(file, "#define AWSLC_VERSION_NUMBER_STRING \"1.35.0\"").unwrap(); + writeln!(file, "#endif").unwrap(); + + let version = validate_and_extract_version(temp_dir.path()).unwrap(); + assert_eq!(version, "1.35.0"); + } + + #[test] + fn test_validate_and_extract_version_not_awslc() { + let temp_dir = tempfile::tempdir().unwrap(); + let openssl_dir = temp_dir.path().join("openssl"); + std::fs::create_dir_all(&openssl_dir).unwrap(); + + let base_h = openssl_dir.join("base.h"); + let mut file = std::fs::File::create(&base_h).unwrap(); + writeln!(file, "#ifndef OPENSSL_BASE_H").unwrap(); + writeln!(file, "#define OPENSSL_BASE_H").unwrap(); + writeln!(file, "// Not AWS-LC").unwrap(); + writeln!(file, "#endif").unwrap(); + + let result = validate_and_extract_version(temp_dir.path()); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("not valid AWS-LC headers")); + } + + #[test] + fn test_validate_and_extract_version_missing_file() { + let temp_dir = tempfile::tempdir().unwrap(); + let result = validate_and_extract_version(temp_dir.path()); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Failed to read")); + } + + #[test] + fn test_validate_and_extract_version_missing_version_string() { + let temp_dir = tempfile::tempdir().unwrap(); + let openssl_dir = temp_dir.path().join("openssl"); + std::fs::create_dir_all(&openssl_dir).unwrap(); + + let base_h = openssl_dir.join("base.h"); + let mut file = std::fs::File::create(&base_h).unwrap(); + writeln!(file, "#ifndef OPENSSL_BASE_H").unwrap(); + writeln!(file, "#define OPENSSL_BASE_H").unwrap(); + writeln!(file, "#define OPENSSL_IS_AWSLC 1").unwrap(); + // Missing AWSLC_VERSION_NUMBER_STRING + writeln!(file, "#endif").unwrap(); + + let result = validate_and_extract_version(temp_dir.path()); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("Could not find AWSLC_VERSION_NUMBER_STRING")); + } + + // ------------------------------------------------------------------------- + // PrebuiltBuilder library name tests + // ------------------------------------------------------------------------- + + #[test] + fn test_crypto_lib_name_no_prefix() { + let temp = tempfile::tempdir().unwrap(); + let lib_dir = temp.path().join("lib"); + std::fs::create_dir_all(&lib_dir).unwrap(); + std::fs::write(lib_dir.join("libcrypto.a"), b"").unwrap(); + + let builder = PrebuiltBuilder { + install_dir: temp.path().to_path_buf(), + include_dir: temp.path().join("include"), + lib_dir, + output_lib_type: OutputLibType::Static, + prefix: None, + }; + + assert_eq!(builder.crypto_lib_name(), "crypto"); + assert_eq!(builder.ssl_lib_name(), "ssl"); + } + + #[test] + fn test_crypto_lib_name_with_prefix() { + let temp = tempfile::tempdir().unwrap(); + let lib_dir = temp.path().join("lib"); + std::fs::create_dir_all(&lib_dir).unwrap(); + std::fs::write(lib_dir.join("libmy_prefix_crypto.a"), b"").unwrap(); + + let builder = PrebuiltBuilder { + install_dir: temp.path().to_path_buf(), + include_dir: temp.path().join("include"), + lib_dir, + output_lib_type: OutputLibType::Static, + prefix: Some("my_prefix".to_string()), + }; + + assert_eq!(builder.crypto_lib_name(), "my_prefix_crypto"); + assert_eq!(builder.ssl_lib_name(), "my_prefix_ssl"); + } + + // ------------------------------------------------------------------------- + // Library detection tests + // ------------------------------------------------------------------------- + + #[test] + fn test_find_static_lib_unix() { + let _env = setup_test_env(); + let temp = tempfile::tempdir().unwrap(); + let lib_dir = temp.path(); + std::fs::write(lib_dir.join("libcrypto.a"), b"").unwrap(); + + let result = PrebuiltBuilder::find_static_lib(lib_dir, "crypto"); + // Result depends on target platform, but should find the file on Unix-like systems + #[cfg(not(all(target_os = "windows", target_env = "msvc")))] + assert!(result.is_some()); + } + + #[test] + fn test_find_dynamic_lib_unix() { + let _env = setup_test_env(); + let temp = tempfile::tempdir().unwrap(); + let lib_dir = temp.path(); + std::fs::write(lib_dir.join("libcrypto.so"), b"").unwrap(); + + let result = PrebuiltBuilder::find_dynamic_lib(lib_dir, "crypto"); + // Result depends on target platform + #[cfg(target_os = "linux")] + assert!(result.is_some()); + } + + #[test] + fn test_detect_lib_type_static_preferred() { + let _env = setup_test_env(); + let temp = tempfile::tempdir().unwrap(); + let lib_dir = temp.path(); + // Create both static and dynamic libs + std::fs::write(lib_dir.join("libcrypto.a"), b"").unwrap(); + std::fs::write(lib_dir.join("libcrypto.so"), b"").unwrap(); + + let result = PrebuiltBuilder::detect_lib_type(lib_dir, &None); + // When both exist, the choice follows OutputLibType::default() + // (which reads the *_STATIC env var); either outcome is valid here. + #[cfg(not(all(target_os = "windows", target_env = "msvc")))] + assert!(result.is_ok()); + } + + #[test] + fn test_detect_lib_type_no_library() { + let _env = setup_test_env(); + let temp = tempfile::tempdir().unwrap(); + let lib_dir = temp.path(); + // No libraries created + + let result = PrebuiltBuilder::detect_lib_type(lib_dir, &None); + match result { + Err(msg) => assert!(msg.contains("No crypto library found")), + Ok(_) => panic!("expected Err when no library is present"), + } + } + + #[test] + fn test_detect_lib_type_prefixed() { + let _env = setup_test_env(); + let temp = tempfile::tempdir().unwrap(); + let lib_dir = temp.path(); + std::fs::write(lib_dir.join("libmy_prefix_crypto.a"), b"").unwrap(); + + let prefix = Some("my_prefix".to_string()); + let result = PrebuiltBuilder::detect_lib_type(lib_dir, &prefix); + + #[cfg(not(all(target_os = "windows", target_env = "msvc")))] + assert!(result.is_ok()); + } + + // ------------------------------------------------------------------------- + // PrebuiltBuilder::new tests + // ------------------------------------------------------------------------- + + #[test] + fn test_prebuilt_builder_new_lib64() { + let _env = setup_test_env(); + let temp = tempfile::tempdir().unwrap(); + let include_dir = temp.path().join("include"); + let lib64_dir = temp.path().join("lib64"); + std::fs::create_dir_all(&include_dir).unwrap(); + std::fs::create_dir_all(&lib64_dir).unwrap(); + std::fs::write(lib64_dir.join("libcrypto.a"), b"").unwrap(); + + let result = PrebuiltBuilder::new(temp.path().to_path_buf(), None); + + #[cfg(target_pointer_width = "64")] + { + // On 64-bit systems, should prefer lib64 + #[cfg(not(all(target_os = "windows", target_env = "msvc")))] + { + assert!(result.is_ok()); + let builder = result.unwrap(); + assert!(builder.lib_dir().ends_with("lib64")); + } + } + } + + #[test] + fn test_prebuilt_builder_new_missing_lib_dir() { + // This test doesn't need env setup as it fails before calling detect_lib_type + let temp = tempfile::tempdir().unwrap(); + let include_dir = temp.path().join("include"); + std::fs::create_dir_all(&include_dir).unwrap(); + // No lib or lib64 directory + + let result = PrebuiltBuilder::new(temp.path().to_path_buf(), None); + match result { + Err(msg) => assert!(msg.contains("Library directory not found")), + Ok(_) => panic!("expected Err for missing lib dir"), + } + } + + #[test] + fn test_prebuilt_builder_verify_library_exists() { + let _env = setup_test_env(); + let temp = tempfile::tempdir().unwrap(); + let lib_dir = temp.path().join("lib"); + std::fs::create_dir_all(&lib_dir).unwrap(); + std::fs::write(lib_dir.join("libcrypto.a"), b"").unwrap(); + + let builder = PrebuiltBuilder { + install_dir: temp.path().to_path_buf(), + include_dir: temp.path().join("include"), + lib_dir, + output_lib_type: OutputLibType::Static, + prefix: None, + }; + + // crypto should be found + #[cfg(not(all(target_os = "windows", target_env = "msvc")))] + assert!(builder.verify_library_exists("crypto").is_ok()); + + // ssl should not be found + #[cfg(not(all(target_os = "windows", target_env = "msvc")))] + assert!(builder.verify_library_exists("ssl").is_err()); + } + + #[test] + fn test_prefix_accessor() { + let temp = tempfile::tempdir().unwrap(); + let lib_dir = temp.path().join("lib"); + std::fs::create_dir_all(&lib_dir).unwrap(); + + let builder_no_prefix = PrebuiltBuilder { + install_dir: temp.path().to_path_buf(), + include_dir: temp.path().join("include"), + lib_dir: lib_dir.clone(), + output_lib_type: OutputLibType::Static, + prefix: None, + }; + assert_eq!(builder_no_prefix.prefix(), None); + + let builder_with_prefix = PrebuiltBuilder { + install_dir: temp.path().to_path_buf(), + include_dir: temp.path().join("include"), + lib_dir, + output_lib_type: OutputLibType::Static, + prefix: Some("test_prefix".to_string()), + }; + assert_eq!(builder_with_prefix.prefix(), Some("test_prefix")); + } +} diff --git a/scripts/tests/test_prebuilt.sh b/scripts/tests/test_prebuilt.sh new file mode 100755 index 000000000000..40e1c405cce4 --- /dev/null +++ b/scripts/tests/test_prebuilt.sh @@ -0,0 +1,184 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 OR ISC + +# Integration test script for prebuilt AWS-LC linking support. +# +# Usage: +# ./scripts/tests/test_prebuilt.sh +# +# Optional environment variables: +# PREFIXED_INSTALL_DIR Path to prefixed AWS-LC installation +# INSTALL_WITH_BINDINGS_DIR Path to installation with share/rust/aws_lc_bindings.rs + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +if [ -z "$1" ]; then + echo "Usage: $0 " + echo "" + echo "This script tests linking aws-lc-rs against a prebuilt AWS-LC installation." + echo "" + echo "Required:" + echo " AWS_LC_INSTALL_DIR Path to AWS-LC installation directory" + echo "" + echo "Optional environment variables:" + echo " PREFIXED_INSTALL_DIR Path to prefixed AWS-LC installation" + echo " INSTALL_WITH_BINDINGS_DIR Path to installation with share/rust/aws_lc_bindings.rs" + echo "" + echo "Example:" + echo " $0 /usr/local/aws-lc" + exit 1 +fi + +INSTALL_DIR="$1" + +cd "${REPO_ROOT}" + +echo "=== Testing prebuilt AWS-LC linking ===" +echo "Repository root: ${REPO_ROOT}" +echo "Install directory: ${INSTALL_DIR}" +echo "" + +# Validate install directory +if [ ! -d "$INSTALL_DIR" ]; then + echo "ERROR: Install directory not found: $INSTALL_DIR" + exit 1 +fi + +if [ ! -d "$INSTALL_DIR/include/openssl" ]; then + echo "ERROR: Headers not found at $INSTALL_DIR/include/openssl" + exit 1 +fi + +if [ ! -d "$INSTALL_DIR/lib" ] && [ ! -d "$INSTALL_DIR/lib64" ]; then + echo "ERROR: Library directory not found at $INSTALL_DIR/lib or $INSTALL_DIR/lib64" + exit 1 +fi + +# Clean previous builds (set SKIP_CLEAN=1 to speed up repeated local runs) +if [ "${SKIP_CLEAN:-0}" != "1" ]; then + echo "Cleaning previous builds..." + cargo clean +fi + +TESTS_PASSED=0 +TESTS_FAILED=0 +TESTS_SKIPPED=0 + +run_test() { + local test_name="$1" + local test_cmd="$2" + + echo "" + echo "=== Test: $test_name ===" + if eval "$test_cmd"; then + echo "SUCCESS: $test_name" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + echo "FAILED: $test_name" + TESTS_FAILED=$((TESTS_FAILED + 1)) + return 1 + fi +} + +skip_test() { + local test_name="$1" + local reason="$2" + echo "" + echo "=== Test: $test_name ===" + echo "SKIPPED: $reason" + TESTS_SKIPPED=$((TESTS_SKIPPED + 1)) +} + +# Test 1: Build with conventional bindings location (if present) +if [ -f "$INSTALL_DIR/share/rust/aws_lc_bindings.rs" ]; then + run_test "aws-lc-sys with conventional bindings location" \ + "AWS_LC_SYS_PREBUILT_INSTALL_DIR='$INSTALL_DIR' cargo build -p aws-lc-sys" +else + skip_test "aws-lc-sys with conventional bindings location" \ + "No share/rust/aws_lc_bindings.rs present" +fi + +# Clean between tests +cargo clean -p aws-lc-sys 2>/dev/null || true + +# Test 2: Build with bindgen feature (static linking) +run_test "aws-lc-sys with bindgen feature (static)" \ + "AWS_LC_SYS_STATIC=1 AWS_LC_SYS_PREBUILT_INSTALL_DIR='$INSTALL_DIR' cargo build -p aws-lc-sys --features bindgen" + +# Clean between tests +cargo clean -p aws-lc-sys -p aws-lc-rs 2>/dev/null || true + +# Test 3: Build aws-lc-rs with prebuilt aws-lc-sys +run_test "aws-lc-rs with prebuilt aws-lc-sys (static)" \ + "AWS_LC_SYS_STATIC=1 AWS_LC_SYS_PREBUILT_INSTALL_DIR='$INSTALL_DIR' cargo build -p aws-lc-rs --features aws-lc-sys/bindgen" + +# Test 4: Run aws-lc-rs tests with static linking +run_test "aws-lc-rs tests with prebuilt (static)" \ + "AWS_LC_SYS_STATIC=1 AWS_LC_SYS_PREBUILT_INSTALL_DIR='$INSTALL_DIR' cargo test -p aws-lc-rs --features aws-lc-sys/bindgen --lib" + +# Clean between tests +cargo clean -p aws-lc-sys -p aws-lc-rs 2>/dev/null || true + +# Test 5: Build with dynamic linking (requires DYLD_LIBRARY_PATH/LD_LIBRARY_PATH at runtime) +run_test "aws-lc-sys with bindgen feature (dynamic)" \ + "AWS_LC_SYS_STATIC=0 AWS_LC_SYS_PREBUILT_INSTALL_DIR='$INSTALL_DIR' cargo build -p aws-lc-sys --features bindgen" + +# Test 6: Run tests with dynamic linking +case "$(uname)" in + Darwin) + LIB_PATH_VAR="DYLD_LIBRARY_PATH" + ;; + *) + LIB_PATH_VAR="LD_LIBRARY_PATH" + ;; +esac + +LIB_DIR="$INSTALL_DIR/lib" +if [ -d "$INSTALL_DIR/lib64" ]; then + LIB_DIR="$INSTALL_DIR/lib64" +fi + +run_test "aws-lc-rs tests with prebuilt (dynamic)" \ + "${LIB_PATH_VAR}='${LIB_DIR}' AWS_LC_SYS_STATIC=0 AWS_LC_SYS_PREBUILT_INSTALL_DIR='$INSTALL_DIR' cargo test -p aws-lc-rs --features aws-lc-sys/bindgen --lib" + +# Test 7: Prefixed build (if available) +if [ -n "$PREFIXED_INSTALL_DIR" ] && [ -d "$PREFIXED_INSTALL_DIR" ]; then + cargo clean -p aws-lc-sys -p aws-lc-rs 2>/dev/null || true + run_test "aws-lc-sys with prefixed prebuilt" \ + "AWS_LC_SYS_STATIC=1 AWS_LC_SYS_PREBUILT_INSTALL_DIR='$PREFIXED_INSTALL_DIR' cargo build -p aws-lc-sys --features bindgen" +else + skip_test "aws-lc-sys with prefixed prebuilt" \ + "PREFIXED_INSTALL_DIR not set or directory doesn't exist" +fi + +# Test 8: Custom bindings location (if available) +if [ -n "$INSTALL_WITH_BINDINGS_DIR" ] && [ -f "$INSTALL_WITH_BINDINGS_DIR/share/rust/aws_lc_bindings.rs" ]; then + cargo clean -p aws-lc-sys 2>/dev/null || true + run_test "aws-lc-sys with custom bindings override" \ + "AWS_LC_SYS_PREBUILT_INSTALL_DIR='$INSTALL_WITH_BINDINGS_DIR' AWS_LC_SYS_PREBUILT_BINDINGS='$INSTALL_WITH_BINDINGS_DIR/share/rust/aws_lc_bindings.rs' cargo build -p aws-lc-sys" +else + skip_test "aws-lc-sys with custom bindings override" \ + "INSTALL_WITH_BINDINGS_DIR not set or bindings file doesn't exist" +fi + +# Summary +echo "" +echo "==========================================" +echo "Test Summary" +echo "==========================================" +echo "Passed: $TESTS_PASSED" +echo "Failed: $TESTS_FAILED" +echo "Skipped: $TESTS_SKIPPED" +echo "" + +if [ $TESTS_FAILED -gt 0 ]; then + echo "RESULT: Some tests failed!" + exit 1 +else + echo "RESULT: All tests passed!" + exit 0 +fi