diff --git a/.github/workflows/s390x-wheel.yml b/.github/workflows/s390x-wheel.yml new file mode 100644 index 000000000000..cd24c3e5cd59 --- /dev/null +++ b/.github/workflows/s390x-wheel.yml @@ -0,0 +1,68 @@ +name: s390x Wheel (experimental) + +permissions: + contents: read + +on: + workflow_dispatch: + inputs: + version: + description: PyPI version to build (leave empty to build from the checked-out ref) + required: false + type: string + openssl_version: + description: OpenSSL version for static linking + required: false + default: "3.4.6" + type: string + +env: + IMAGE: cryptography-s390x-builder + WHEELHOUSE: wheelhouse + +jobs: + s390x-wheel: + name: s390x manylinux wheel + runs-on: ubuntu-latest + timeout-minutes: 180 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@3b5aeead8caba617bcda9a68a66745a079fc368 # v3.6.0 + with: + platforms: all + + - name: Build s390x builder image + run: | + docker build --platform linux/s390x \ + -f docker/Dockerfile.s390x \ + -t "${IMAGE}" \ + . + + - name: Build cryptography wheel on s390x + env: + VERSION: ${{ github.event.inputs.version }} + OPENSSL_VERSION: ${{ github.event.inputs.openssl_version }} + run: | + mkdir -p "${WHEELHOUSE}" + args=( + --rm + --platform linux/s390x + -v "$PWD:/src:ro" + -v "$PWD/${WHEELHOUSE}:/wheelhouse" + -e SOURCE_DIR=/src + -e WHEELHOUSE=/wheelhouse + -e OPENSSL_VERSION="${OPENSSL_VERSION}" + ) + if [ -n "${VERSION}" ]; then + args+=(-e "VERSION=${VERSION}") + fi + docker run "${args[@]}" "${IMAGE}" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cryptography-s390x-wheel + path: wheelhouse/*.whl diff --git a/docker/Dockerfile.s390x b/docker/Dockerfile.s390x new file mode 100644 index 000000000000..d666e1ec397c --- /dev/null +++ b/docker/Dockerfile.s390x @@ -0,0 +1,63 @@ +# Minimal s390x wheel builder for cryptography. +# +# Derived from a proven UBI9-based builder; keeps only deps required to compile +# cryptography (Rust/maturin + static OpenSSL + auditwheel), not generic +# packages like Pillow/NumPy. +# +# Build (on any host with QEMU/binfmt for s390x): +# docker build --platform linux/s390x -f docker/Dockerfile.s390x -t cryptography-s390x-builder . +# +# Or use scripts/s390x/build.sh from the repository root. + +FROM registry.access.redhat.com/ubi9/ubi + +ENV LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + PYTHONUNBUFFERED=1 \ + CARGO_HOME=/root/.cargo \ + RUSTUP_HOME=/root/.rustup \ + PATH="/root/.cargo/bin:/usr/local/bin:${PATH}" + +WORKDIR /io + +# ---- cryptography build toolchain ---- +RUN dnf install -y --allowerasing \ + gcc gcc-c++ \ + make \ + git curl \ + tar gzip xz \ + autoconf automake libtool \ + pkgconf-pkg-config \ + python3.12 python3.12-devel python3.12-pip \ + openssl-devel \ + libffi-devel \ + zlib-devel \ + perl \ + && dnf clean all + +# auditwheel needs patchelf; not packaged on UBI9. +RUN git clone --depth 1 https://github.com/NixOS/patchelf.git /tmp/patchelf \ + && cd /tmp/patchelf \ + && ./bootstrap.sh \ + && ./configure \ + && make -j"$(nproc)" \ + && make install \ + && rm -rf /tmp/patchelf + +RUN python3.12 -m pip install --upgrade pip \ + && python3.12 -m pip install \ + uv \ + maturin \ + auditwheel \ + wheel \ + build + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain stable + +RUN mkdir -p /wheelhouse + +COPY scripts/s390x/build-wheel.sh /io/build-wheel.sh +RUN chmod +x /io/build-wheel.sh + +ENTRYPOINT ["/io/build-wheel.sh"] diff --git a/scripts/s390x/build-wheel.sh b/scripts/s390x/build-wheel.sh new file mode 100755 index 000000000000..a9f957b954ff --- /dev/null +++ b/scripts/s390x/build-wheel.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# Build a manylinux-compatible cryptography wheel on s390x. +# +# Runs inside docker/Dockerfile.s390x. Supports: +# - local checkout mounted at SOURCE_DIR (default /src) +# - PyPI sdist via VERSION when SOURCE_DIR is absent or empty +# +# Environment: +# SOURCE_DIR Path to cryptography source tree (optional) +# VERSION PyPI version to build when not using SOURCE_DIR +# PYTHON Python executable (default: python3.12) +# WHEELHOUSE Output directory (default: /wheelhouse) +# MANYLINUX_PLAT auditwheel platform tag (default: manylinux_2_34_s390x) +# OPENSSL_VERSION OpenSSL release to build statically (default: 3.4.6) +# OPENSSL_DIR Skip OpenSSL build when already provided +# OPENSSL_STATIC Set to 1 (default) for release-style wheels + +set -euo pipefail + +SOURCE_DIR="${SOURCE_DIR:-/src}" +WHEELHOUSE="${WHEELHOUSE:-/wheelhouse}" +PYTHON="${PYTHON:-python3.12}" +MANYLINUX_PLAT="${MANYLINUX_PLAT:-manylinux_2_34_s390x}" +OPENSSL_VERSION="${OPENSSL_VERSION:-3.4.6}" +OPENSSL_STATIC="${OPENSSL_STATIC:-1}" +BUILD_REQUIREMENTS="${BUILD_REQUIREMENTS:-.github/requirements/build-requirements.txt}" +WORK="${WORK:-/tmp/cryptography-s390x-build}" + +mkdir -p "$WHEELHOUSE" +rm -rf "$WORK" +mkdir -p "$WORK" +cd "$WORK" + +echo "=== cryptography s390x wheel build ===" +echo " python: $("$PYTHON" --version)" +echo " arch: $(uname -m)" +echo " wheelhouse: $WHEELHOUSE" +echo " manylinux plat: $MANYLINUX_PLAT" +echo " openssl static: $OPENSSL_STATIC" + +build_static_openssl() { + if [[ -n "${OPENSSL_DIR:-}" && -d "${OPENSSL_DIR}/include/openssl" ]]; then + echo "Using prebuilt OpenSSL at ${OPENSSL_DIR}" + return + fi + + local ossl_path="${WORK}/openssl" + echo "Building static OpenSSL ${OPENSSL_VERSION} -> ${ossl_path}" + + export TYPE=openssl + export VERSION="${OPENSSL_VERSION}" + export OSSL_PATH="${ossl_path}" + export CONFIG_FLAGS="${CONFIG_FLAGS:-no-shared no-ssl2 no-ssl3 no-comp no-hw no-engine no-tests}" + + if [[ -f "${SOURCE_DIR}/.github/bin/build_openssl.sh" ]]; then + bash "${SOURCE_DIR}/.github/bin/build_openssl.sh" + else + curl -fsSL \ + "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" \ + -o "openssl-${OPENSSL_VERSION}.tar.gz" + tar xzf "openssl-${OPENSSL_VERSION}.tar.gz" + pushd "openssl-${OPENSSL_VERSION}" + sed -i "s/^SHLIB_VERSION=.*/SHLIB_VERSION=100/" VERSION.dat + ./config ${CONFIG_FLAGS} -fPIC --prefix="${OSSL_PATH}" + make depend + make -j"$(nproc)" + make install_sw install_ssldirs + rm -rf "${OSSL_PATH}/bin" + popd + fi + + export OPENSSL_DIR="${ossl_path}" +} + +resolve_source_tree() { + if [[ -d "${SOURCE_DIR}" && -f "${SOURCE_DIR}/pyproject.toml" ]]; then + echo "Using local source tree: ${SOURCE_DIR}" + cp -a "${SOURCE_DIR}/." "${WORK}/cryptography-src/" + SRC="${WORK}/cryptography-src" + return + fi + + if [[ -z "${VERSION:-}" ]]; then + echo "ERROR: set SOURCE_DIR to a checkout or VERSION for a PyPI release." >&2 + exit 1 + fi + + echo "Downloading cryptography==${VERSION} from PyPI" + "$PYTHON" -m pip download --no-binary ":all:" --no-deps "cryptography==${VERSION}" + local archive + archive="$(ls cryptography-*.tar.gz | head -n1)" + tar xzf "${archive}" + SRC="$(find . -maxdepth 1 -type d -name 'cryptography-*' | head -n1)" +} + +build_wheel() { + local src="$1" + local constraints=() + local out="${WORK}/dist" + local python_bin + local py_config + + mkdir -p "$out" + + if [[ -f "${src}/${BUILD_REQUIREMENTS}" ]]; then + constraints=(--require-hashes --build-constraint="${src}/${BUILD_REQUIREMENTS}") + fi + + cd "$src" + + python_bin="$(readlink -f "$(command -v "$PYTHON")")" + py_config="$(command -v "$(basename "$python_bin")-config" || true)" + if [[ -x "$py_config" ]]; then + export CFLAGS="${CFLAGS:-} $("$py_config" --includes)" + export LDFLAGS="${LDFLAGS:-} $("$py_config" --ldflags)" + fi + + export OPENSSL_DIR="${OPENSSL_DIR:-}" + export OPENSSL_STATIC="${OPENSSL_STATIC}" + export PYO3_PYTHON="${python_bin}" + + if command -v uv >/dev/null 2>&1; then + uv build --wheel "${constraints[@]}" --python "${python_bin}" -o "$out" . + else + "$python_bin" -m build --wheel -o "$out" . + fi + + local wheel + wheel="$(ls "$out"/cryptography-*.whl | head -n1)" + echo "Built wheel: ${wheel}" + + auditwheel repair --plat "${MANYLINUX_PLAT}" "${wheel}" -w "$WHEELHOUSE" +} + +smoketest_wheel() { + local wheel + local src="${WORK}/cryptography-src" + wheel="$(ls "${WHEELHOUSE}"/cryptography-*.whl | tail -n1)" + + "$PYTHON" -m venv "${WORK}/venv" + # shellcheck disable=SC1091 + source "${WORK}/venv/bin/activate" + pip install -U pip + + if [[ -f "${src}/${BUILD_REQUIREMENTS}" ]]; then + pip install --require-hashes -r "${src}/${BUILD_REQUIREMENTS}" + else + pip install "cffi>=2.0.0" + fi + + pip install --no-index --no-deps "${wheel}" + + python - <<'EOF' +from cryptography.hazmat.backends.openssl.backend import backend + +print("Smoketest OK") +print("Loaded:", backend.openssl_version_text()) +EOF +} + +resolve_source_tree +build_static_openssl +build_wheel "${SRC}" +smoketest_wheel + +echo +echo "=== Wheel ready ===" +ls -lh "${WHEELHOUSE}"/cryptography-*.whl diff --git a/scripts/s390x/build.sh b/scripts/s390x/build.sh new file mode 100755 index 000000000000..47350309e789 --- /dev/null +++ b/scripts/s390x/build.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Build cryptography s390x wheels using docker + QEMU. +# +# Examples: +# # From this checkout (recommended while developing the contribution): +# ./scripts/s390x/build.sh +# +# # From a tagged PyPI release: +# VERSION=44.0.0 ./scripts/s390x/build.sh +# +# # Custom output directory: +# WHEELHOUSE=$PWD/wheelhouse ./scripts/s390x/build.sh +# +# Environment: +# IMAGE Docker image tag (default: cryptography-s390x-builder) +# SETUP_QEMU Run setup-qemu.sh first when 1 (default on non-s390x hosts) +# VERSION PyPI version when not building from checkout +# OPENSSL_VERSION Passed through to build-wheel.sh +# PYTHON Python executable inside the container + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +IMAGE="${IMAGE:-cryptography-s390x-builder}" +WHEELHOUSE="${WHEELHOUSE:-${ROOT}/wheelhouse}" +PYTHON="${PYTHON:-python3.12}" +HOST_ARCH="$(uname -m)" + +mkdir -p "$WHEELHOUSE" + +if [[ "${SETUP_QEMU:-}" == "1" || ( "${HOST_ARCH}" != "s390x" && "${SETUP_QEMU:-}" != "0" ) ]]; then + echo "==> Host arch is ${HOST_ARCH}; ensuring QEMU binfmt for s390x" + bash "${ROOT}/scripts/s390x/setup-qemu.sh" +fi + +echo "==> Building ${IMAGE} for linux/s390x" +docker build --platform linux/s390x \ + -f docker/Dockerfile.s390x \ + -t "${IMAGE}" \ + . + +RUN_ARGS=( + --rm + --platform linux/s390x + -v "${ROOT}:/src:ro" + -v "${WHEELHOUSE}:/wheelhouse" + -e "SOURCE_DIR=/src" + -e "WHEELHOUSE=/wheelhouse" + -e "PYTHON=${PYTHON}" +) + +if [[ -n "${VERSION:-}" ]]; then + RUN_ARGS+=(-e "VERSION=${VERSION}") +fi +if [[ -n "${OPENSSL_VERSION:-}" ]]; then + RUN_ARGS+=(-e "OPENSSL_VERSION=${OPENSSL_VERSION}") +fi +if [[ -n "${MANYLINUX_PLAT:-}" ]]; then + RUN_ARGS+=(-e "MANYLINUX_PLAT=${MANYLINUX_PLAT}") +fi + +echo "==> Running s390x wheel build" +docker run "${RUN_ARGS[@]}" "${IMAGE}" + +echo +echo "Done. Wheels:" +ls -lh "${WHEELHOUSE}"/cryptography-*.whl diff --git a/scripts/s390x/setup-qemu.sh b/scripts/s390x/setup-qemu.sh new file mode 100755 index 000000000000..4d616607df72 --- /dev/null +++ b/scripts/s390x/setup-qemu.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Register QEMU user-mode handlers so docker can run linux/s390x images +# on x86_64, aarch64, or macOS hosts. +# +# Usage: scripts/s390x/setup-qemu.sh + +set -euo pipefail + +if ! command -v docker >/dev/null 2>&1; then + echo "ERROR: docker is required." >&2 + exit 1 +fi + +echo "==> Installing binfmt handlers via tonistiigi/binfmt" +docker run --privileged --rm tonistiigi/binfmt --install all + +echo "==> Verifying s390x emulation" +docker run --rm --platform linux/s390x quay.io/pypa/manylinux_2_28_s390x uname -m + +echo "QEMU s390x is ready."