Skip to content

Commit ed76bfe

Browse files
feat: replace PyPI opencv wheels with ffmpeg-free builds in Dockerfiles (#4336)
## Summary - After `uv sync`, the Dockerfile downloads a source-built `opencv-contrib-python-headless` wheel (compiled with `WITH_FFMPEG=OFF`, `ENABLE_CONTRIB=1`, `ENABLE_HEADLESS=1`) from the GitHub release and substitutes it for all PyPI opencv-python variants - The contrib-headless variant is a strict superset of the cv2 API (core + contrib modules, no GUI), so a single wheel replaces `opencv-python`, `opencv-python-headless`, and `opencv-contrib-python`, eliminating 14 bundled ffmpeg CVEs - Validated end-to-end: PaddleOCR model load + detection + recognition on a real document image succeeds with the substituted wheel on wolfi-base arm64 ## Dependencies > **Depends on #4335** — the GHA workflow that builds and publishes the opencv wheels must be merged and run first so the GitHub release exists for the Dockerfile to download from. ✅ Merged and release published. ## Test plan - [x] Merge and run the workflow in #4335 to create the wheel release - [x] Local smoke test: PaddleOCR OCR on a real document image with substituted wheel (wolfi-base arm64) - [ ] Build the main `Dockerfile` and verify opencv imports work without `.libs` - [ ] Confirm no ffmpeg-related CVEs remain in a container scan 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes a core transitive native dependency in the Docker image (OpenCV), which could affect OCR/inference behavior or break builds if the wheel/arch tag mismatch occurs, despite hash verification. > > **Overview** > **Hardens the Docker image against ffmpeg CVEs** by replacing all PyPI `opencv-*` packages after `uv sync` with a downloaded, hash-verified `opencv-contrib-python-headless` wheel built with `WITH_FFMPEG=OFF`. > > The Docker build now selects the wheel by architecture (`x86_64`/`aarch64`), uninstalls any existing OpenCV variants, installs the pinned wheel version, and bumps the library version/changelog to `0.22.22`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a58f842. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3ac4443 commit ed76bfe

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.22.22
2+
3+
### Security
4+
5+
- **Replace PyPI opencv wheels with ffmpeg-free builds in Docker image**: After `uv sync`, the Dockerfile now substitutes all PyPI opencv-python variants with a source-built `opencv-contrib-python-headless` wheel compiled with `WITH_FFMPEG=OFF`, eliminating 14 bundled ffmpeg CVEs. The contrib-headless variant is a strict superset of the cv2 API (core + contrib modules, no GUI) so a single wheel replaces `opencv-python`, `opencv-python-headless`, and `opencv-contrib-python`.
6+
17
## 0.22.21
28

39
### Enhancements

Dockerfile

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,42 @@ RUN uv sync --locked --all-extras --no-group dev --no-group lint --no-group test
7777
uv run --no-sync $PYTHON -c "from unstructured.partition.model_init import initialize; initialize()" && \
7878
uv run --no-sync $PYTHON -c "from unstructured_inference.models.tables import UnstructuredTableTransformerModel; model = UnstructuredTableTransformerModel(); model.initialize('microsoft/table-transformer-structure-recognition')"
7979

80+
# Replace PyPI opencv wheels (which bundle vulnerable ffmpeg 5.1.x with 14 CVEs)
81+
# with a source-built opencv-contrib-python-headless wheel compiled with
82+
# WITH_FFMPEG=OFF + ENABLE_CONTRIB=1 + ENABLE_HEADLESS=1.
83+
#
84+
# The contrib-headless variant is a strict superset of the cv2 API exposed by
85+
# opencv-python, opencv-python-headless, and opencv-contrib-python (all of
86+
# which are pulled in transitively by unstructured-paddleocr / unstructured-
87+
# inference). One wheel can therefore replace all three. Because the wheel's
88+
# metadata name only matches opencv-contrib-python-headless, we have to
89+
# uninstall the other variants first - `uv pip install --reinstall-package`
90+
# would silently no-op for the non-matching names.
91+
#
92+
# See: https://github.com/opencv/opencv-python/issues/1212
93+
#
94+
# Note: uv.lock resolves opencv packages to 4.13.0.92, but our wheel is pinned
95+
# to 4.12.0.88 because 4.13.0.92 has no sdist on PyPI — our GHA workflow
96+
# (build-opencv-wheels.yml) compiles from source and requires an sdist.
97+
# Bump this when a newer version publishes an sdist.
98+
ARG OPENCV_WHEEL_TAG=opencv-4.12.0.88
99+
ARG OPENCV_WHEEL_VERSION=4.12.0.88
100+
# SHA-256 hashes of the wheels we built in build-opencv-wheels.yml.
101+
# Update these when bumping OPENCV_WHEEL_VERSION.
102+
ARG OPENCV_SHA256_aarch64=498fbb787dbfe7d6bc853ddad4ea1154e8fbefbfafd05aafb417f576e27850d5
103+
ARG OPENCV_SHA256_x86_64=50545ffc1efabf06cd70894b65a7fbca56786f560f452bf67a42c1bbd7a85961
104+
RUN ARCH=$(uname -m) && \
105+
WHEEL="opencv_contrib_python_headless-${OPENCV_WHEEL_VERSION}-cp312-cp312-linux_${ARCH}.whl" && \
106+
wget -q -O /tmp/"${WHEEL}" \
107+
"https://github.com/Unstructured-IO/unstructured/releases/download/${OPENCV_WHEEL_TAG}/${WHEEL}" && \
108+
EXPECTED=$(eval echo "\$OPENCV_SHA256_${ARCH}") && \
109+
echo "${EXPECTED} /tmp/${WHEEL}" | sha256sum -c - && \
110+
uv pip uninstall \
111+
opencv-python opencv-python-headless \
112+
opencv-contrib-python opencv-contrib-python-headless && \
113+
uv pip install --no-deps /tmp/"${WHEEL}" && \
114+
rm /tmp/"${WHEEL}"
115+
80116
ENV PATH="/app/.venv/bin:${PATH}"
81117
ENV HF_HUB_OFFLINE=1
82118

unstructured/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.22.21" # pragma: no cover
1+
__version__ = "0.22.22" # pragma: no cover

0 commit comments

Comments
 (0)