fix(ci): rustls-everywhere — eliminate openssl-sys from build tree

# Root cause of the wheel-build cascade

We have shipped 5 release-pipeline hot-fixes in 12 hours, each
addressing a different symptom of the same architectural problem:

1. PR #363 — npm artifact downloads + tried `yum openssl-devel`
2. PR #367 — vendored OpenSSL in `headroom-proxy` + dropped Intel mac
3. PR #369 — Debian-cross perl install (`perl` not `libipc-cmd-perl`)
4. PR #370 — moved `openssl/vendored` from headroom-proxy to headroom-py
5. (this PR) — ELIMINATE OpenSSL entirely

Each fix exposed a different missing system package or feature flag in
a different build surface (manylinux x86_64 vs aarch64-cross-Debian vs
macOS Intel vs e2e/wrap Dockerfile vs e2e/init Dockerfile vs main
Dockerfile vs devcontainer). We were playing whack-a-mole because every
Cargo dep change to the OpenSSL surface required matching system-package
updates in 6+ different Dockerfiles and workflows, and the PR-level CI
didn't exercise all of them.

# Why this PR is the structural fix

`fastembed` exposes clean rustls feature flags:
- `hf-hub-rustls-tls`               (replaces default `hf-hub-native-tls`)
- `ort-download-binaries-rustls-tls` (replaces default `…native-tls`)

By disabling fastembed's default features and enabling the rustls
variants explicitly, we remove `native-tls` (and therefore `openssl-sys`,
`openssl`, `openssl-src`, perl modules, OpenSSL build-time deps,
vendored OpenSSL ~30s build cost) from the entire workspace dep tree.

Verified locally:

    $ cargo tree -p headroom-py -i openssl-sys
    error: package ID specification `openssl-sys` did not match any packages

    $ cargo tree -p headroom-py -i native-tls
    error: package ID specification `native-tls` did not match any packages

    $ cargo build --release -p headroom-py
    Finished `release` profile [optimized] target(s) in 25.57s

(Down from 1m+ with vendored OpenSSL.)

# Cleanups enabled by this change

- crates/headroom-py/Cargo.toml — dropped the `openssl/vendored`
  workaround from PR #370.
- crates/headroom-proxy/Cargo.toml — same dep removed.
- e2e/wrap/Dockerfile — dropped `yum install openssl-devel pkgconfig
  perl-IPC-Cmd`. Comment retained explaining why.
- e2e/init/Dockerfile — same.
- Dockerfile (main) — dropped `pkg-config libssl-dev` from apt-get.
- .devcontainer/Dockerfile — dropped `pkg-config libssl-dev`.
- .github/workflows/release.yml — removed the entire before-script-linux
  block (perl install probe + multi-package-manager dispatch + fail-loud
  assertion). No longer needed.

# Regression gate

Three new structural tests in tests/test_release_workflows.py:

- test_no_openssl_sys_in_wheel_build_tree — runs `cargo tree -p <crate>
  -i openssl-sys` for headroom-py / headroom-proxy / headroom-core. If
  openssl-sys reappears (a future native-tls enabler creeping in via a
  new dep), this fails AT PR TIME with an actionable message.
- test_no_native_tls_in_wheel_build_tree — same shape, native-tls is
  the proximate cause.
- test_fastembed_uses_rustls_features — checks the Cargo.toml so a
  future "let me bump fastembed and forget the features" doesn't
  silently re-introduce OpenSSL.

Plus two cleanup gates:
- test_dockerfiles_no_longer_install_openssl_devel
- test_release_yml_does_not_install_openssl_or_perl_for_wheels

All 13 release-workflow tests pass. `make ci-precheck` PASSED.

# What this teaches us about rollouts (per user's ultrathink ask)

The 5-fix cascade exposed three meta-problems:

1. PR checks don't block merges. PR #370 had docker-init-e2e,
   docker-wrap-e2e, docker-native-e2e all FAILED yet got merged.
   Branch protection should require these checks. Operator action
   needed (cannot fix in code).

2. Local validation is misleading. `cargo build -p headroom-py` from
   the workspace root used the workspace lockfile and looked green;
   CI did fresh resolution against headroom-py's manifest alone where
   the feature wasn't enabled. Lesson: verify structural invariants
   with `cargo tree -e features` before trusting that a build "works."

3. 6+ build surfaces with independent system-dep state. Every Cargo
   change required matching updates in 6 places. The structural answer
   (this PR) is to NOT depend on system OpenSSL at all. Where structural
   fixes are not possible, the answer is a single shared
   scripts/install-rust-build-deps.sh — but with this PR there's
   nothing left to install.
This commit is contained in:
chopratejas 2026-05-03 23:26:04 -07:00
parent a5c7f6fed9
commit 6f2c0a8400
10 changed files with 206 additions and 388 deletions

View file

@ -3,8 +3,9 @@ FROM mcr.microsoft.com/devcontainers/python:1-${VARIANT}
# Single-wheel architecture (post-#355): `uv sync` builds `headroom-ai`
# from the local pyproject.toml using maturin (declared in build-system).
# Maturin needs rust + cargo + pkg-config + libssl-dev (transitive
# openssl-sys via fastembed/hf-hub).
# Maturin needs rust + cargo. The rustls-everywhere refactor (PR #371)
# eliminated `openssl-sys` from our build tree, so this image no longer
# needs `pkg-config` or `libssl-dev`.
#
# Rust toolchain is provisioned via the official devcontainer feature
# (`ghcr.io/devcontainers/features/rust:1`) declared in devcontainer.json
@ -18,11 +19,6 @@ FROM mcr.microsoft.com/devcontainers/python:1-${VARIANT}
# base image's yarnpkg.com source has an expired GPG key that aborts
# the whole RUN with "NO_PUBKEY 62D54FD4003F6525". The maturin build
# doesn't need yarn.
RUN rm -f /etc/apt/sources.list.d/yarn.list && \
apt-get update && \
apt-get install -y --no-install-recommends \
pkg-config \
libssl-dev && \
rm -rf /var/lib/apt/lists/*
RUN rm -f /etc/apt/sources.list.d/yarn.list
RUN python -m pip install --no-cache-dir uv==0.6.17 'maturin>=1.5,<2.0'

View file

@ -226,48 +226,14 @@ jobs:
target: ${{ matrix.target }}
args: --release --out dist --interpreter python3.10 python3.11 python3.12 python3.13
manylinux: ${{ matrix.manylinux }}
# OpenSSL is vendored at the cargo level via headroom-proxy's
# `openssl = { features = ["vendored"] }` dep — that compiles
# OpenSSL from source as part of the cargo build, so we no
# longer need to install system OpenSSL in the manylinux
# container or via Homebrew on macOS. We DO need `IPC::Cmd`
# at build time because OpenSSL's vendored Configure script
# imports it (without it the build fails with "Can't locate
# IPC/Cmd.pm").
#
# `IPC::Cmd` has been a Perl core module since 5.10, so any
# working `perl` install provides it. The aarch64-cross
# container the maturin-action uses is Debian/Ubuntu-based
# (apt), the x86_64 native container is AlmaLinux 8 (yum/dnf).
# Probe the IPC::Cmd module directly first; only install when
# missing, using whichever package manager exists. The legacy
# `libipc-cmd-perl` alias on Debian is no longer in default
# sources — installing plain `perl` pulls IPC::Cmd via
# perl-modules-* on every Debian/Ubuntu version we'll see.
before-script-linux: |
set -euo pipefail
if perl -MIPC::Cmd -e 1 >/dev/null 2>&1; then
echo "IPC::Cmd already available"
exit 0
fi
if command -v dnf >/dev/null 2>&1; then
dnf install -y perl-IPC-Cmd
elif command -v yum >/dev/null 2>&1; then
yum install -y perl-IPC-Cmd
elif command -v apt-get >/dev/null 2>&1; then
apt-get update -qq
# `perl` is the meta-package that pulls perl-modules-* containing IPC::Cmd.
apt-get install -y --no-install-recommends perl
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache perl-utils
else
echo "::error::No supported package manager (dnf/yum/apt-get/apk) in container" >&2
exit 1
fi
# Final assertion — fail the build now (not 5 minutes later
# in the openssl-src compile step) if IPC::Cmd is still not
# importable. No silent fallback per project guideline.
perl -MIPC::Cmd -e 'print "IPC::Cmd loaded OK\n"'
# No before-script-linux needed: the rustls-everywhere refactor
# (PR #371) switched fastembed to `hf-hub-rustls-tls` +
# `ort-download-binaries-rustls-tls` features, which removed
# `openssl-sys` from our build tree entirely. The wheel build
# no longer needs system OpenSSL or any perl modules.
# `cargo tree -p headroom-py -i openssl-sys` returns "not
# found" — that's the regression gate (see test_release_workflows
# ::test_no_openssl_sys_in_wheel_build_tree).
env:
# PyO3 0.22 supports up to Python 3.13; allow forward-compat
# builds for 3.14+ until we bump PyO3 (tracked separately).

215
Cargo.lock generated
View file

@ -647,12 +647,6 @@ dependencies = [
"vsimd",
]
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bit-set"
version = "0.8.0"
@ -1008,16 +1002,6 @@ dependencies = [
"url",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@ -1238,16 +1222,6 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
[[package]]
name = "der"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b"
dependencies = [
"pem-rfc7468",
"zeroize",
]
[[package]]
name = "deranged"
version = "0.5.8"
@ -1547,21 +1521,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@ -1866,7 +1825,6 @@ dependencies = [
"humantime",
"hyper",
"hyper-util",
"openssl",
"pin-project-lite",
"proptest",
"reqwest",
@ -1892,7 +1850,6 @@ name = "headroom-py"
version = "0.1.0"
dependencies = [
"headroom-core",
"openssl",
"pyo3",
"serde_json",
]
@ -1946,7 +1903,6 @@ dependencies = [
"indicatif 0.18.4",
"libc",
"log",
"native-tls",
"rand 0.9.4",
"reqwest",
"serde",
@ -2092,22 +2048,6 @@ dependencies = [
"webpki-roots 1.0.7",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@ -2126,11 +2066,9 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@ -2695,23 +2633,6 @@ dependencies = [
"pxfm",
]
[[package]]
name = "native-tls"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ndarray"
version = "0.17.2"
@ -2897,60 +2818,12 @@ version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "openssl"
version = "0.10.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-src"
version = "300.6.0+3.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@ -3012,15 +2885,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "pem-rfc7468"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9"
dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@ -3582,7 +3446,6 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
@ -3591,12 +3454,9 @@ dependencies = [
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-tls",
"hyper-util",
"js-sys",
"log",
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite",
"quinn",
@ -3607,7 +3467,6 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tokio-util",
"tower",
@ -3805,7 +3664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"core-foundation 0.10.1",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
@ -4076,27 +3935,6 @@ dependencies = [
"syn",
]
[[package]]
name = "system-configuration"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
@ -4321,16 +4159,6 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
@ -4677,10 +4505,8 @@ checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
dependencies = [
"base64 0.22.1",
"cookie_store",
"der",
"flate2",
"log",
"native-tls",
"percent-encoding",
"rustls",
"rustls-pki-types",
@ -4689,7 +4515,6 @@ dependencies = [
"socks",
"ureq-proto",
"utf8-zero",
"webpki-root-certs",
"webpki-roots 1.0.7",
]
@ -4967,15 +4792,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
@ -5037,35 +4853,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"

View file

@ -14,18 +14,16 @@ ARG UV_VERSION
# build-essential / g++ for any C extension wheels uv may need to build
# from source. curl + ca-certificates are required by the rustup
# bootstrap below. pkg-config + libssl-dev for `openssl-sys` (transitive
# from fastembed/hf-hub/ureq → native-tls — the workspace `rustls-tls`
# pin loses to cargo feature unification). patchelf for maturin's
# wheel-link repair on linux.
# bootstrap below. patchelf for maturin's wheel-link repair on linux.
# No OpenSSL system deps required: the rustls-everywhere refactor
# eliminated `openssl-sys` from our build tree by switching fastembed
# to `hf-hub-rustls-tls` + `ort-download-binaries-rustls-tls`.
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
g++ \
curl \
ca-certificates \
pkg-config \
libssl-dev \
patchelf \
&& rm -rf /var/lib/apt/lists/*

View file

@ -53,7 +53,19 @@ flate2 = "1"
# int8-quantized ONNX) auto-downloads from HuggingFace Hub on first use.
# Same library + same model = byte-equal embeddings between Python and Rust
# (both call into ONNX Runtime over the identical ONNX file).
fastembed = "5"
#
# `default-features = false` + explicit rustls features eliminates the
# transitive `native-tls` → `openssl-sys` dependency that fastembed's
# default features pull in (via `hf-hub-native-tls` + `ort-download-
# binaries-native-tls`). Without this, every wheel-build surface
# (release matrix, e2e/wrap, e2e/init, devcontainer, ci.yml) needs
# system OpenSSL + perl modules for openssl-src vendored compile. Using
# rustls everywhere removes the entire OpenSSL build-deps surface.
fastembed = { version = "5", default-features = false, features = [
"hf-hub-rustls-tls",
"ort-download-binaries-rustls-tls",
"image-models",
] }
# `magika` is Google's ONNX-backed content classifier (Tier 1 of the
# Stage-3d ContentRouter detection arch). Bundled standard-model is
# loaded once per process via `OnceLock` and shared across calls. The

View file

@ -24,18 +24,6 @@ tracing = { workspace = true }
tracing-subscriber = { version = "0.3", features = ["json", "env-filter", "fmt"] }
reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls", "http2"] }
tokio-tungstenite = { version = "0.24", default-features = false, features = ["connect", "rustls-tls-webpki-roots"] }
# `hf-hub` (transitive via `fastembed`) hard-codes `native-tls` as a default
# feature; Cargo's feature unification then enables `openssl-sys` for the
# whole workspace despite our `reqwest`/`tokio-tungstenite`/`tokio-rustls`
# preferences. The wheel build matrix breaks on every Linux + Intel-macOS
# target where the manylinux container's system OpenSSL is too old or
# absent (manylinux2014 ships 1.0.2k; aarch64 cross sysroot has none).
# Enabling `openssl/vendored` instructs `openssl-sys` to compile OpenSSL
# from source as part of the cargo build, removing the system-OpenSSL
# dependency entirely. ~30s extra one-time build cost; works on every
# target uniformly. The crate is referenced here only to enable the
# feature via Cargo's unification — we don't use the API directly.
openssl = { version = "0.10", features = ["vendored"] }
clap = { workspace = true, features = ["derive", "env"] }
serde = { workspace = true }
serde_json = { workspace = true }

View file

@ -25,19 +25,3 @@ extension-module = ["pyo3/extension-module"]
headroom-core = { path = "../headroom-core" }
pyo3 = { workspace = true }
serde_json = { workspace = true }
# `headroom-core` pulls `openssl-sys` transitively via fastembed →
# hf-hub → ureq → native-tls. The wheel matrix's manylinux_2_28
# x86_64 container has no `openssl-devel`, and even with one the
# manylinux2014 fallback ships OpenSSL 1.0.2k which `openssl-sys 0.9`
# rejects. We force the `vendored` feature on `openssl` so cargo
# compiles OpenSSL from source as part of the wheel build, removing
# the system-OpenSSL dependency entirely.
#
# Why this dep lives HERE (the wheel-producing crate) rather than
# headroom-proxy: cargo's feature unification only applies to the
# resolution graph that's actually built. `maturin build --manifest-path
# crates/headroom-py/Cargo.toml` resolves from headroom-py's deps
# only — headroom-proxy's `openssl/vendored` enable doesn't propagate
# because headroom-py never depends on headroom-proxy. Pinning the
# vendored feature on headroom-py's own deps is the right fix.
openssl = { version = "0.10", features = ["vendored"] }

View file

@ -6,7 +6,8 @@
# 2.28 baseline) yields a wheel portable to any glibc 2.28+ runtime.
FROM quay.io/pypa/manylinux_2_28_x86_64 AS builder
RUN yum install -y openssl-devel pkgconfig perl-IPC-Cmd
# No OpenSSL system deps required (rustls-everywhere refactor). See
# e2e/wrap/Dockerfile for the full rationale.
ENV CARGO_HOME=/usr/local/cargo \
RUSTUP_HOME=/usr/local/rustup \

View file

@ -9,10 +9,15 @@
# on any glibc 2.28+ runtime, including bookworm.
FROM quay.io/pypa/manylinux_2_28_x86_64 AS builder
# `openssl-sys` (transitive via fastembed/hf-hub/ureq → native-tls) needs
# OpenSSL dev headers. perl-IPC-Cmd is required by openssl-sys's autotools
# probe.
RUN yum install -y openssl-devel pkgconfig perl-IPC-Cmd
# No OpenSSL system deps required. As of the rustls-everywhere refactor,
# all HTTP+TLS in our Rust crates goes through `rustls`. fastembed's
# `hf-hub-rustls-tls` + `ort-download-binaries-rustls-tls` features
# replace the default native-tls path; `cargo tree -p headroom-py -i
# openssl-sys` returns "not found", confirming this Dockerfile no
# longer needs `openssl-devel`, `pkgconfig`, or any perl modules
# (Time::Piece, IPC::Cmd) for the openssl-src vendored Configure
# script. The historical `yum install openssl-devel pkgconfig
# perl-IPC-Cmd` line is intentionally absent.
# Install rust toolchain into the manylinux container. Match the
# rust-toolchain.toml at repo root (1.95.0 + rustfmt + clippy) so cargo

View file

@ -69,103 +69,184 @@ def test_ci_commitlint_skips_default_github_merge_commits() -> None:
assert "!startsWith(github.event.head_commit.message, 'Merge pull request ')" in content
def test_headroom_py_vendors_openssl() -> None:
"""`hf-hub` (transitive via `fastembed`) hard-codes `native-tls` as a
default feature, forcing `openssl-sys` for the entire build graph
despite our `reqwest`/`tokio-tungstenite`/`tokio-rustls` preferences.
def test_no_openssl_sys_in_wheel_build_tree() -> None:
"""STRUCTURAL INVARIANT: openssl-sys must NOT appear in the wheel
build's resolved dependency graph.
Critically: this dep MUST live in `headroom-py/Cargo.toml` (the
wheel-producing crate) NOT in `headroom-proxy/Cargo.toml`. Cargo's
feature unification only applies to the resolution graph that's
actually built. `maturin build --manifest-path crates/headroom-py/
Cargo.toml` resolves headroom-py's deps only; headroom-py does NOT
depend on headroom-proxy, so a `vendored` feature enable in
headroom-proxy never propagates to the wheel build. The earlier
hot-fix put it in headroom-proxy and the wheel still failed with
"Could not find directory of OpenSSL installation" fixed by
moving the dep to headroom-py.
This is the load-bearing assertion for the entire build pipeline.
If openssl-sys is in the wheel-build resolution graph, every
Linux/macOS surface that builds from source needs system OpenSSL
+ perl modules + pkg-config and we've spent five hot-fixes
chasing whichever combination of perl modules / OpenSSL versions
/ pkg-config paths was missing in each manylinux/Dockerfile/
devcontainer surface. The cleanest fix is to NOT depend on
OpenSSL at all.
fastembed exposes `hf-hub-rustls-tls` and
`ort-download-binaries-rustls-tls` features that replace its
default `native-tls` path. With `default-features = false` plus
those rustls features enabled in headroom-core, our entire build
tree uses rustls and no crate pulls openssl-sys.
This test runs `cargo tree` (so it actually exercises the
resolved feature graph, not just declared Cargo.toml features).
A future refactor that adds a transitive native-tls user will
fail here, surfaced at PR time rather than 5 minutes into a CI
wheel-build error.
"""
py_cargo = (ROOT / "crates" / "headroom-py" / "Cargo.toml").read_text(encoding="utf-8")
import subprocess
# Guard against a future refactor that re-removes the vendored dep
# from headroom-py (would silently break the wheel build again).
assert 'openssl = { version = "0.10", features = ["vendored"] }' in py_cargo, (
"headroom-py/Cargo.toml must declare openssl with the vendored feature; "
"without it the manylinux wheel build cannot find system OpenSSL."
)
for crate in ("headroom-py", "headroom-proxy", "headroom-core"):
result = subprocess.run(
[
"cargo",
"tree",
"--target",
"x86_64-unknown-linux-gnu",
"-p",
crate,
"-i",
"openssl-sys",
],
cwd=str(ROOT),
capture_output=True,
text=True,
check=False,
)
# `cargo tree -i <pkg>` exits 101 with "did not match any
# packages" when the package is NOT in the tree — the GREEN
# case. Exit 0 with a tree of consumers means it IS pulled.
not_in_tree = result.returncode != 0 and "did not match any packages" in result.stderr
assert not_in_tree, (
f"openssl-sys is back in {crate}'s build tree:\n"
f"stdout:\n{result.stdout}\n"
f"stderr:\n{result.stderr}\n"
"Find the new native-tls user (likely a default-features=true "
"on a transitive crate) and disable it. Switching every "
"transitive HTTP+TLS consumer to rustls is the load-bearing "
"invariant that keeps wheel builds working without system "
"OpenSSL or perl modules."
)
def test_build_wheels_installs_perl_ipc_cmd_for_vendored_openssl() -> None:
"""OpenSSL's vendored `Configure` script needs `IPC::Cmd`. Without
it the build fails with `Can't locate IPC/Cmd.pm`. The before-script
must (a) probe the module first to skip a no-op install when the
container already has it, (b) cover RHEL family (dnf/yum) AND
Debian/Ubuntu (apt-get) AND musllinux (apk) since the maturin-action
uses different containers per target, and (c) fail loud with an
explicit `perl -MIPC::Cmd -e 1` assertion if the install path
didn't actually resolve the module — no silent fallback into a
later confusing openssl-src build error.
def test_no_native_tls_in_wheel_build_tree() -> None:
"""The dual of the openssl-sys gate: native-tls is the proximate
cause of openssl-sys being pulled. Catch it earlier with a more
specific error message so future debugging starts at the right
place.
"""
import subprocess
The previous shape used `libipc-cmd-perl` on the apt branch, which
is a deprecated alias and is not in the default sources of the
aarch64-cross container; that broke the aarch64 wheel build.
for crate in ("headroom-py", "headroom-proxy", "headroom-core"):
result = subprocess.run(
[
"cargo",
"tree",
"--target",
"x86_64-unknown-linux-gnu",
"-p",
crate,
"-i",
"native-tls",
],
cwd=str(ROOT),
capture_output=True,
text=True,
check=False,
)
not_in_tree = result.returncode != 0 and "did not match any packages" in result.stderr
assert not_in_tree, (
f"native-tls is back in {crate}'s build tree — likely some "
f"crate's `default-features = true` re-enabled native-tls "
f"transitively:\n{result.stdout}"
)
def test_fastembed_uses_rustls_features() -> None:
"""The mechanism that keeps openssl-sys out of the build is
fastembed's explicit rustls feature selection in headroom-core.
fastembed's default features include `hf-hub-native-tls` and
`ort-download-binaries-native-tls` both pull openssl-sys.
Disabling defaults and enabling the rustls equivalents removes
the OpenSSL surface entirely.
"""
cargo = (ROOT / "crates" / "headroom-core" / "Cargo.toml").read_text(encoding="utf-8")
assert "default-features = false" in cargo
assert '"hf-hub-rustls-tls"' in cargo
assert '"ort-download-binaries-rustls-tls"' in cargo
# `image-models` is in default; we re-enable it explicitly so we
# don't lose the image-embedding capability when defaults are off.
assert '"image-models"' in cargo
def test_dockerfiles_no_longer_install_openssl_devel() -> None:
"""Once openssl-sys is out of the build tree, every Dockerfile
that used to install `openssl-devel` / `libssl-dev` for the Rust
build can drop those packages. This test enforces the cleanup so
a future refactor doesn't carry the old packages forward "just
in case".
The check looks only at non-comment lines so explanatory comments
that mention the historical packages don't false-positive.
"""
targets = [
ROOT / "e2e" / "wrap" / "Dockerfile",
ROOT / "e2e" / "init" / "Dockerfile",
ROOT / "Dockerfile",
ROOT / ".devcontainer" / "Dockerfile",
]
forbidden = ["openssl-devel", "libssl-dev"]
for target in targets:
content = target.read_text(encoding="utf-8")
non_comment = "\n".join(
line for line in content.splitlines() if not line.lstrip().startswith("#")
)
for pkg in forbidden:
assert pkg not in non_comment, (
f"{target.relative_to(ROOT)} still installs {pkg!r} on a "
f"non-comment line. The rustls-everywhere refactor removed "
f"openssl-sys from the build tree; this package is no "
f"longer needed."
)
def test_release_yml_does_not_install_openssl_or_perl_for_wheels() -> None:
"""With openssl-sys out of the build tree (verified by
test_no_openssl_sys_in_wheel_build_tree), the previous
before-script-linux that installed perl-IPC-Cmd / perl /
perl-utils for the openssl-src vendored Configure script is
obsolete. Removing it speeds the wheel build and keeps the
Linux entry honest every package install we keep here
represents a hidden assumption about the manylinux container.
"""
content = (ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8")
assert "before-script-linux:" in content
# Pre-probe so we no-op when IPC::Cmd is already importable.
assert "perl -MIPC::Cmd -e 1" in content
# All three RHEL-family + Debian-family + Alpine package managers covered.
assert "dnf install -y perl-IPC-Cmd" in content
assert "yum install -y perl-IPC-Cmd" in content
# The plain `perl` meta-package pulls perl-modules-* on Debian/Ubuntu,
# which contains IPC::Cmd. The legacy `libipc-cmd-perl` alias must NOT
# be referenced as an installable target — it is no longer in default
# Debian/Ubuntu sources. We allow the string in YAML/shell comments
# (operator hints) by checking only non-comment lines.
assert "apt-get install -y --no-install-recommends perl" in content
assert "apk add --no-cache perl-utils" in content
non_comment_lines: list[str] = []
for raw in content.splitlines():
stripped = raw.lstrip()
# Drop YAML and shell `#` comments. Mid-line `#` comments would
# require a real YAML/shell tokenizer; the file's actual install
# commands are never on the same physical line as a comment.
if stripped.startswith("#"):
continue
non_comment_lines.append(raw)
code_only = "\n".join(non_comment_lines)
assert "libipc-cmd-perl" not in code_only, (
"libipc-cmd-perl must not appear as an apt-get target — it is a "
"deprecated alias not in default Debian/Ubuntu sources."
)
# Final fail-loud assertion (no silent fallback).
assert "perl -MIPC::Cmd -e 'print \"IPC::Cmd loaded OK" in content
def test_build_wheels_does_not_set_openssl_dir() -> None:
"""`OPENSSL_DIR` (and the related `OPENSSL_LIB_DIR` /
`OPENSSL_INCLUDE_DIR`) DEFEATS the `vendored` feature: openssl-sys
will use the system OpenSSL at that path instead of compiling from
source. The earlier macOS hot-fix exported these env vars; with
vendored enabled they must NOT be set, otherwise we silently
regress to the system-OpenSSL path that broke originally.
"""
content = (ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8")
# Locate the build-wheels job and assert it does not export OPENSSL_DIR.
bw_start = content.index("\n build-wheels:")
bw_end = content.index("\n collect-dist:")
body = content[bw_start:bw_end]
assert "OPENSSL_DIR" not in body, (
"build-wheels must not set OPENSSL_DIR — it disables openssl-sys's "
"vendored feature and reintroduces the system-OpenSSL dependency."
)
non_comment = "\n".join(line for line in body.splitlines() if not line.lstrip().startswith("#"))
# No legacy install commands or env vars must appear on a non-comment
# line. Each forbidden token represents an assumption about system
# OpenSSL that the rustls refactor removed.
forbidden = [
"openssl-devel",
"libssl-dev",
"perl-IPC-Cmd",
"libipc-cmd-perl",
"OPENSSL_DIR",
]
for token in forbidden:
assert token not in non_comment, (
f"release.yml build-wheels job still references {token!r} on "
f"a non-comment line. The rustls-everywhere refactor removed "
f"openssl-sys from the build tree; this command/env is now "
f"obsolete."
)
def test_build_wheels_matrix_excludes_intel_macos() -> None: