# Building BoringSSL for Android from the source RPM on an offline RHEL 9 machine

This directory is a self-contained kit: the BoringSSL source RPM, every dependency needed
to build it, a build script, and this guide. Nothing here needs network access at build
time.

| File / directory | Purpose |
| --- | --- |
| `boringssl-0.20260813-2.1.src.rpm` | the source RPM (openSUSE Tumbleweed), BoringSSL 0.20260813 |
| [`build-deps/`](build-deps) | dependency data: RPMs with repository metadata, and the Android NDK |
| [`check-build-deps.sh`](check-build-deps.sh) | run **first** on the offline machine: verifies the copy is complete |
| [`build-boringssl-android-offline.sh`](build-boringssl-android-offline.sh) | the build: installs dependencies, unpacks the NDK, patches the source, cross-compiles |
| [`examples/jni/`](examples/jni) | a small JNI library, used by the script to prove the result links |
| `android-libs/` | **output**: static libraries per ABI plus headers |
| `build-logs/`, `rpmbuild/`, `build/`, `tools/` | logs, rpmbuild tree, CMake build trees, unpacked NDK |

## 1. What the source RPM contains

| Item | Value |
| --- | --- |
| Package | `boringssl-0.20260813-2.1`, vendor openSUSE, license OpenSSL |
| Sources | `boringssl-0.20260813.tar.xz`, `vendor.tar.xz` (vendored Go modules) |
| Patches | 5: `0001` architecture support, `0002` disable `-Werror`, `0003` library renaming + SOVERSION, `0004` lower the CMake minimum to 3.20, `curl-impersonate.patch` (TLS fingerprinting, 17 files) |
| BuildRequires | `cmake >= 3.0`, `fdupes`, `gcc11-c++`, `golang(API) >= 1.13` |
| SHA-256 | `334fbb460ca0688a11c9e0e05d1184650910eb18fcff827e5f6fe43e3e7a1be8` |

Two consequences matter for Android:

- **The libraries are renamed.** Patch `0003` sets `OUTPUT_NAME boringssl_crypto` and
  `boringssl_ssl`, so the archives are `libboringssl_crypto.a` and `libboringssl_ssl.a`,
  **not** upstream's `libcrypto.a` / `libssl.a`.
- **Only `%prep` is used.** The spec's `%build` targets a Linux distribution package
  (shared libraries in `/usr/lib64`, the `bssl` tool, a source subpackage). For Android the
  kit runs the spec's `%prep` to unpack and patch the source, then cross-compiles that tree
  with the NDK. The two BuildRequires that name openSUSE packages (`gcc11-c++`,
  `golang(API)`) are satisfied on RHEL 9 by `gcc-c++` and `golang`.

## 2. Why the dependency data is needed

An ordinary build of this SRPM reaches the network twice: for the `BuildRequires` RPMs, and
for the Android NDK, which is not packaged by any distribution. Both are captured here.

| Directory | Size | Contents |
| --- | --- | --- |
| `build-deps/rpms/` | 251 MB | 249 RPMs + `repodata/` + `SHA256SUMS`: `rpm-build`, `cmake`, `ninja-build`, `gcc-c++`, `golang`, `fdupes`, `unzip` and their full dependency closure |
| `build-deps/ndk/` | 633 MB | `android-ndk-r27d-linux.zip` (27.3.13750724), its `SHA256SUMS`, and Google's `repository2-3.xml` as provenance |
| `build-deps/INVENTORY.txt` | – | expected counts and checksums, used by `check-build-deps.sh` |
| `build-deps/logs/` | – | output of the capture step |

The NDK's published SHA-1 (`22105e41…`) and size (663,956,036 bytes) were taken from
Google's own SDK metadata and both matched on download.

## 3. Step 1 — Transfer and verify

Copy the whole `BoringSSL` directory to the offline machine with a method that preserves
everything, then check it:

```bash
rsync -aH --info=progress2 BoringSSL/ user@offline:/path/BoringSSL/
# or via removable media on a Linux filesystem (not FAT32/exFAT: the NDK zip is 633 MB
# and the RPM set holds thousands of files)
tar -C /path -cf - BoringSSL | tar -C /media/disk -xf -
```

```bash
cd BoringSSL
./check-build-deps.sh
```

It verifies the source RPM checksum, the RPM count and checksums, the presence of
`repodata/`, and the NDK's size and checksum.

## 4. Step 2 — Build

```bash
sudo ./build-boringssl-android-offline.sh
```

Options: `--abis "arm64-v8a x86_64"`, `--api 24`, `--output DIR`, `--skip-deps`,
`--with-rpm` (also rebuild the native RHEL 9 binary RPMs from the same SRPM).

What the script does, in order:

1. **Checks each build requirement against the installed system** with
   `dnf --disablerepo='*' repoquery --installed --whatprovides`, and installs **only what is
   missing** from `build-deps/rpms` (`--repofrompath … --nogpgcheck --nobest`). Installing
   only the gaps matters: the captured repository may hold newer builds than the RHEL system
   has, and asking dnf for those can drag unrelated upgrades into the transaction.
2. **Verifies and unpacks the NDK** into `tools/android-ndk-r27d` (skipped if already there).
3. **Unpacks and patches the source**: `rpm -i` puts the sources and spec into `rpmbuild/`,
   then `rpmbuild --nodeps -bp` runs the spec's `%prep`, applying all five patches.
   `--nodeps` is required because the spec's `BuildRequires` name openSUSE packages.
4. **Cross-compiles each ABI** with the NDK's CMake toolchain file:

   | Option | Reason |
   | --- | --- |
   | `CMAKE_TOOLCHAIN_FILE=…/android.toolchain.cmake` | selects the NDK clang, sysroot and linker |
   | `ANDROID_ABI` | `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` |
   | `ANDROID_PLATFORM=android-21` | minSdk of the libraries; must be ≤ your app's `minSdk` |
   | `ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON` | 16 KB page support, needed by recent devices |
   | `BUILD_SHARED_LIBS=OFF` | static archives, the usual choice for JNI |
   | `CMAKE_POSITION_INDEPENDENT_CODE=ON` | the archives are linked into a shared `.so` |
   | `BUILD_TESTING=OFF` | skips test binaries and their Go dependency |
   | `--target crypto ssl` | builds only the two libraries |

5. **Copies** the archives to `android-libs/<abi>/` and the headers once to
   `android-libs/include/openssl/`.
6. **Verifies**: prints each archive's CPU architecture, then builds
   [`examples/jni`](examples/jni) against the result for every ABI and prints the shared
   libraries the produced `.so` needs. Finally writes `android-libs/SHA256SUMS`.

## 5. Step 3 — Use the libraries in an Android project

Copy `android-libs/` into your app as `app/src/main/cpp/boringssl/`, then:

```cmake
cmake_minimum_required(VERSION 3.22)
# Declare CXX even if your own sources are C: BoringSSL is C++ and the target must be
# linked with the C++ driver.
project(myapp_native C CXX)

set(BSSL "${CMAKE_CURRENT_SOURCE_DIR}/boringssl")
add_library(boringssl_crypto STATIC IMPORTED)
set_target_properties(boringssl_crypto PROPERTIES
    IMPORTED_LOCATION "${BSSL}/${ANDROID_ABI}/libboringssl_crypto.a")
add_library(boringssl_ssl STATIC IMPORTED)
set_target_properties(boringssl_ssl PROPERTIES
    IMPORTED_LOCATION "${BSSL}/${ANDROID_ABI}/libboringssl_ssl.a")

add_library(myapp_native SHARED native-lib.c)
set_target_properties(myapp_native PROPERTIES LINKER_LANGUAGE CXX)
target_include_directories(myapp_native PRIVATE "${BSSL}/include")
target_link_libraries(myapp_native PRIVATE boringssl_ssl boringssl_crypto)   # ssl first
target_link_options(myapp_native PRIVATE "-Wl,--exclude-libs,ALL" "-Wl,-z,max-page-size=16384")
```

```kotlin
android {
    ndkVersion = "27.3.13750724"          // the NDK these libraries were built with
    defaultConfig {
        minSdk = 21                        // >= the --api the libraries were built for
        ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86") }
    }
}
```

Three rules, each of which causes a real failure when broken:

1. **Link with the C++ driver** (`project(... CXX)` plus `LINKER_LANGUAGE CXX`): BoringSSL
   is C++, and a C-only link fails with `undefined symbol:
   std::__ndk1::__libcpp_verbose_abort`.
2. **`boringssl_ssl` before `boringssl_crypto`**: ssl depends on crypto.
3. **Use the renamed libraries**: `boringssl_crypto` / `boringssl_ssl`, not `crypto` / `ssl`.

`--exclude-libs,ALL` keeps BoringSSL's symbols out of your library's dynamic symbol table,
so they cannot clash with the platform's own crypto libraries.

## 6. Doing it by hand

If you would rather not use the script, the same build in explicit commands:

```bash
cd BoringSSL
# 1. dependencies (only what is missing)
sudo dnf -y --disablerepo='*' --repofrompath=bssl,file://$PWD/build-deps/rpms \
    --enablerepo=bssl --nogpgcheck --nobest install rpm-build cmake ninja-build gcc-c++ golang fdupes unzip
# 2. NDK
unzip -q build-deps/ndk/android-ndk-r27d-linux.zip -d tools
# 3. unpack and patch the source
rpm -i --define "_topdir $PWD/rpmbuild" boringssl-0.20260813-2.1.src.rpm
rpmbuild --nodeps -bp --define "_topdir $PWD/rpmbuild" rpmbuild/SPECS/boringssl.spec
# 4. cross-compile one ABI
NDK=$PWD/tools/android-ndk-r27d
cmake -S rpmbuild/BUILD/boringssl-0.20260813 -B build/android-arm64-v8a -GNinja \
    -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-21 \
    -DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON -DCMAKE_BUILD_TYPE=Release \
    -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_TESTING=OFF
cmake --build build/android-arm64-v8a --parallel "$(nproc)" --target crypto ssl
```

The archives are then `build/android-arm64-v8a/libboringssl_{crypto,ssl}.a`, and the
headers are in `rpmbuild/BUILD/boringssl-0.20260813/include/openssl`.

## 7. Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| `undefined symbol: std::__ndk1::__libcpp_verbose_abort` | The consuming target is linked as C. Add `CXX` to `project(...)` and set `LINKER_LANGUAGE CXX`. |
| `cannot find -lcrypto` / `-lssl` | The distribution patch renames the libraries; link `boringssl_crypto` and `boringssl_ssl`. |
| Undefined BoringSSL symbols at link time | List `ssl` before `crypto`. |
| `Failed build dependencies: gcc11-c++ … golang(API)` | You ran rpmbuild without `--nodeps`. Those are openSUSE capability names; the RHEL 9 equivalents (`gcc-c++`, `golang`) are installed from `build-deps/rpms`. |
| dnf wants to upgrade unrelated packages such as `systemd` | Do not install the whole list blindly; install only missing capabilities, which is what the script does. |
| `NDK toolchain file not found` | `build-deps/ndk` did not arrive completely. Run `./check-build-deps.sh`. |
| `The operation would result in broken dependencies` | Same as above: install only what is missing, or add `--nobest`. |
| App crashes on devices with 16 KB pages | Build with `-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON` and link with `-Wl,-z,max-page-size=16384`. |
| CMake deprecation warnings from the NDK toolchain file | Harmless with CMake 3.31; the script passes `-Wno-dev -DCMAKE_WARN_DEPRECATED=OFF`. |

## 8. Re-capturing the dependency data

On a **connected** RHEL 9 machine of the same minor release:

```bash
# RPMs
dnf download --resolve --alldeps --arch x86_64,noarch --destdir build-deps/rpms \
    rpm-build 'cmake >= 3.20' ninja-build gcc-c++ golang fdupes unzip \
    gcc gcc-plugin-annobin annobin redhat-rpm-config cmake-rpm-macros go-srpm-macros
createrepo_c build-deps/rpms
(cd build-deps/rpms && sha256sum *.rpm > SHA256SUMS)

# NDK (check against Google's metadata)
curl -fLO https://dl.google.com/android/repository/repository2-3.xml
curl -fLO https://dl.google.com/android/repository/android-ndk-r27d-linux.zip
sha1sum android-ndk-r27d-linux.zip    # expect 22105e410cf29afcf163760cc95522b9fb981121
```

`gcc`, `annobin`, `redhat-rpm-config`, `cmake-rpm-macros` and `go-srpm-macros` must be named
explicitly: `dnf download --resolve` omits whatever is already installed on the capture
machine, and both `golang-bin` (which needs `/usr/bin/gcc`) and `cmake` (which needs
`cmake-rpm-macros` when `rpm-build` is present) would otherwise be uninstallable on a
minimal target.

## 9. Notes and limitations

- **Provenance.** The source RPM is an openSUSE package; only its sources and patches are
  used. The libraries produced here are not an openSUSE or RHEL binary package.
- **The captured RPMs are AlmaLinux 9.8 builds**, binary compatible with RHEL 9. For exact
  parity on genuine RHEL 9, re-capture them there (section 8). The build script installs
  only missing packages, so an existing RHEL system is not overwritten.
- **NDK licence.** The Android NDK is redistributed under Google's terms; keep
  `build-deps/ndk/repository2-3.xml` with it as provenance.
- **`curl-impersonate.patch`** changes TLS fingerprinting behaviour. Review whether you want
  it in a shipped app.
- **No stable ABI.** BoringSSL does not promise one; rebuild your native code whenever you
  rebuild BoringSSL.
- **Device testing is out of scope here.** The kit verifies architecture and linkability;
  run your own tests on a device or emulator.

## 10. Verified offline run (2026-09-18)

`build-boringssl-android-offline.sh` was run on AlmaLinux 9.8 x86_64 inside a network
namespace with **no connectivity** (`unshare -n`; the run's first line confirmed
`dl.google.com` was unreachable). It completed end to end:

| Stage | Result |
| --- | --- |
| Build requirements | all seven satisfied by the installed system, so nothing was installed |
| NDK | checksum verified, unpacked, revision 27.3.13750724 (r27d) |
| Source | `rpm -i` + `rpmbuild --nodeps -bp`: all five patches applied |
| Libraries | four ABIs built, headers copied (103 files) |
| Verification | architectures correct, JNI library linked for every ABI |

| ABI | `libboringssl_crypto.a` | `libboringssl_ssl.a` | Architecture | JNI `.so` needs |
| --- | --- | --- | --- | --- |
| arm64-v8a | 30 MB | 12 MB | AArch64 | `libm.so libdl.so libc.so` |
| armeabi-v7a | 20 MB | 7.5 MB | ARM | `libm.so libdl.so libc.so` |
| x86_64 | 29 MB | 12 MB | x86-64 | `libm.so libdl.so libc.so` |
| x86 | 19 MB | 7.4 MB | Intel 80386 | `libm.so libdl.so libc.so` |

The JNI test library needs only Android system libraries, which confirms the C++ runtime is
linked statically and no extra runtime has to be shipped.

**Not covered by this run:** executing anything on an Android device or emulator, and the
dependency-installation path (this host already had all seven requirements; only their
resolution from `build-deps/rpms` was verified separately, in an empty `--installroot`).
