diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 63debd131..b5d4ce218 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 81707f7ce..706199727 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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). diff --git a/Cargo.lock b/Cargo.lock index d6d879088..5dceb1ed8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Dockerfile b/Dockerfile index 5a1eac949..9ed5233d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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/* diff --git a/crates/headroom-core/Cargo.toml b/crates/headroom-core/Cargo.toml index 49463817b..2ec4cead8 100644 --- a/crates/headroom-core/Cargo.toml +++ b/crates/headroom-core/Cargo.toml @@ -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 diff --git a/crates/headroom-proxy/Cargo.toml b/crates/headroom-proxy/Cargo.toml index 6277c7095..c35039ecf 100644 --- a/crates/headroom-proxy/Cargo.toml +++ b/crates/headroom-proxy/Cargo.toml @@ -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 } diff --git a/crates/headroom-py/Cargo.toml b/crates/headroom-py/Cargo.toml index 95ccb27a6..f509213ad 100644 --- a/crates/headroom-py/Cargo.toml +++ b/crates/headroom-py/Cargo.toml @@ -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"] } diff --git a/e2e/init/Dockerfile b/e2e/init/Dockerfile index a855fcf65..0a2d0b15b 100644 --- a/e2e/init/Dockerfile +++ b/e2e/init/Dockerfile @@ -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 \ diff --git a/e2e/wrap/Dockerfile b/e2e/wrap/Dockerfile index 1881ff2e6..6bc55356a 100644 --- a/e2e/wrap/Dockerfile +++ b/e2e/wrap/Dockerfile @@ -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 diff --git a/tests/test_release_workflows.py b/tests/test_release_workflows.py index 3207c4f32..1337bb279 100644 --- a/tests/test_release_workflows.py +++ b/tests/test_release_workflows.py @@ -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 ` 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: