Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
22f963e
pyodide wheel builds
greateggsgreg May 3, 2026
f2d01c6
Update wheel-builder.yml
greateggsgreg May 3, 2026
993c844
Bump to use pyemscripten cibuildwheel - Python 3.14
greateggsgreg May 3, 2026
57c8ea3
Update wheel-builder.yml - Run tests
greateggsgreg May 3, 2026
fcf84c8
Update wheel-builder.yml
greateggsgreg May 3, 2026
96cd750
Update wheel-builder.yml
greateggsgreg May 4, 2026
be30149
Update conftest.py - Added skip_emscripten
greateggsgreg May 4, 2026
9946a08
Update test_meta.py - Skip test use to emscripten not allowing subpro…
greateggsgreg May 4, 2026
661e5f5
Update pyproject.toml - Add skip_emscripten
greateggsgreg May 4, 2026
80168d0
Merge branch 'pyca:main' into pyodide-builds
greateggsgreg May 4, 2026
ebbc395
Merge pull request #1 from greateggsgreg/main
greateggsgreg May 4, 2026
969392d
Update wheel-builder.yml
greateggsgreg May 4, 2026
094f18e
Update wheel-builder.yml
greateggsgreg May 4, 2026
7f9a394
Merge branch 'pyca:main' into pyodide-builds
greateggsgreg May 4, 2026
c8e69a6
Migrate pyemscripten CI tests to ci.yml
greateggsgreg May 4, 2026
5a8bea7
Update wheel-builder.yml - pyemscripten just runs smoke tests on build
greateggsgreg May 4, 2026
2d47164
Update build_openssl.sh
greateggsgreg May 5, 2026
b25e36e
Update ci.yml
greateggsgreg May 5, 2026
fa544e6
Update wheel-builder.yml
greateggsgreg May 5, 2026
a2169d1
Merge branch 'pyca:main' into pyodide-builds
greateggsgreg May 5, 2026
a236786
Update ci.yml
greateggsgreg May 5, 2026
5e8ca1d
Update wheel-builder.yml
greateggsgreg May 5, 2026
5688b7e
Update conftest.py
greateggsgreg May 5, 2026
411b2a1
Update pyproject.toml
greateggsgreg May 5, 2026
bb4e8e6
Merge branch 'pyca:main' into pyodide-builds
greateggsgreg May 5, 2026
783278d
Merge branch 'pyca:main' into pyodide-builds
greateggsgreg May 9, 2026
089badf
Use upstream cibuildwheel main for pyodide wheel builds
greateggsgreg May 10, 2026
dfb4d79
Merge branch 'pyca:main' into pyodide-builds
greateggsgreg May 10, 2026
1af2b7c
Resolve PR comment re: pytest-pyodide
greateggsgreg May 10, 2026
041c097
Use pyodide xbuildenv install-emscripten to install an emscripten SDK…
greateggsgreg May 10, 2026
fe6daeb
Run tests with coverage
greateggsgreg May 10, 2026
f3ff55f
Pin cibuildwheel to 4.0.0rc1 for pyemscripten support
greateggsgreg May 15, 2026
ce45141
Merge branch 'main' into pyodide-builds
greateggsgreg May 15, 2026
a79f81b
Bump Pyodide to 314.0.0a2 in CI and wheel-builder matrices.
greateggsgreg May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/bin/build_openssl.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
#!/bin/bash
#
# Build a TLS library for use by cryptography's CI / wheel jobs.
#
# Required env vars (all branches):
# TYPE - one of: openssl, libressl, boringssl, aws-lc, pyemscripten
# VERSION - release version (TLS-impl branches) or git ref (e.g. boringssl)
# OSSL_PATH - absolute install prefix
#
# Per-TYPE extras:
# openssl - CONFIG_FLAGS : extra flags for ./config
# pyemscripten - emsdk must be activated on PATH (emcc, emconfigure,
# emmake). Cross-compiles OpenSSL VERSION for
# wasm32-emscripten. Idempotent: if libssl.a is already
# present at OSSL_PATH (e.g. restored from actions/cache)
# the build is skipped. Pyodide does not expose a
# downstream-linkable OpenSSL (its `_ssl` module is baked
# into pyodide.asm.wasm) so the cryptography wheel must
# link against its own.

set -e
set -x

Expand Down Expand Up @@ -71,4 +90,28 @@ elif [[ "${TYPE}" == "aws-lc" ]]; then
rm -rf "${OSSL_PATH:?}/bin"
popd # aws-lc
rm -rf aws-lc/
elif [[ "${TYPE}" == "pyemscripten" ]]; then
# Idempotency check: skip if libssl.a is already present (e.g. when
# actions/cache restored the install prefix in a prior step, or when
# cibuildwheel re-invokes CIBW_BEFORE_BUILD_PYODIDE on the same runner).
if [ -f "${OSSL_PATH}/lib/libssl.a" ] || [ -f "${OSSL_PATH}/lib64/libssl.a" ]; then
echo "OpenSSL already built at ${OSSL_PATH}; skipping rebuild."
exit 0
fi
curl -LO "https://github.com/openssl/openssl/releases/download/openssl-${VERSION}/openssl-${VERSION}.tar.gz"
tar zxf "openssl-${VERSION}.tar.gz"
pushd "openssl-${VERSION}"
# emconfigure sets CROSS_COMPILE=<emsdk>/em expecting Configure to append
# "cc"/"ar"/etc. -- but it also sets CC to the full emcc path, so OpenSSL
# ends up concatenating them. Override both with an empty cross-compile
# prefix and bare CC/AR/RANLIB names.
emconfigure ./Configure linux-generic32 \
no-shared no-asm no-engine no-dso no-tests no-srtp no-cms \
no-ui-console no-threads \
--cross-compile-prefix= \
CC=emcc AR=emar RANLIB=emranlib \
--prefix="${OSSL_PATH}"
emmake make -j"$(nproc)" build_libs
emmake make install_dev
popd
fi
148 changes: 147 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,152 @@ jobs:

- uses: ./.github/actions/upload-coverage

pyemscripten:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've taken a read through of this, and here is my concern: this is basically a significant amount of configuration, that none of hte pyca/cryptography knows anything about. We don't know when versions should be bumped, don't know what compatibility looks like, don't know how to debug. It duplicates a ton of our other CI code as well.

For this to be a realistic thing we were willing to maintain, we need to get this down much closer to "this is a new entry in the existing linux matrix that shares the vast majority of the code with the other CI workflows"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the read. Maintainability concerns are fair. Two paths I'd like your call on before cutting code:

Option A — Fold into the linux matrix

New row:

- VERSION: "3.14"
  NOXSESSION: "tests-pyemscripten"
  OPENSSL: {TYPE: "pyemscripten", VERSION: "3.6.2"}
  PYODIDE: {VERSION: "314.0.0a1", EMSDK: "5.0.3"}
  RUST: "nightly"
  RUST_TARGETS: "wasm32-unknown-emscripten"

Pros: checkout/setup-python/rust-toolchain/cache/upload-coverage reuse linux's steps; YAML shrinks; pyemscripten appears as a row in the linux matrix.
Cons: linux job grows 4-5 if: matrix.PYTHON.PYODIDE branches (install + Tests must diverge — nox can't drive a pyodide venv since uv has no wasm build).

Option B — Standalone job, shrunk via shared building blocks

  • New composite action .github/actions/setup-pyodide-build/ for the pyodide-build[resolve] + xbuildenv install + install-emscripten trio.
  • Existing build_openssl.sh (TYPE=pyemscripten already supported).
  • New .github/bin/test_pyemscripten.sh for pyodide build .pyodide venv → install wheel → pytest --cov. Same script runs locally.
  • Local reproducer (Containerfile + driver, already prototyped) so a maintainer can repro CI's pipeline with one command.

Pros: YAML shrinks; no conditional proliferation; unique logic in one action + one script, both grep-able and locally runnable.
Cons: still its own job rather than a row in the linux matrix.

I lean B — fewer conditionals, better local-debug story — but it's your call.

Independent of A vs B, on the maintainability gap: I'd add docs/development/pyemscripten.rst covering the Pyodide ↔ Emscripten ↔ Python compatibility triple (and where Pyodide pins it in Makefile.envs), bump cadence, and 4-5 failure-triage entries. I'll do this regardless of which option we go with.

What do you think?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one option could be to just use cibuildwheel in all workflow files, as we ensure in our Pyodide platform code that the right Emscripten toolchain is picked up, and we won't need to worry about the pyodide-build version, etc. All dependencies are pinned/constrained, too, making it more reliable and reducing the bespoke configuration involved. The only thing to pick up and update would be based on changes between cibuildwheel releases, i.e., which Pyodide ABIs are available to build and whether to enable or disable them as needed. That said, I'd recommend building only for the 2026 ABI, as the 314.0.0 stable release is coming soon (issue, milestone). The second alpha is out as of two days ago.

runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
PYODIDE:
- VERSION: "314.0.0a1"
EMSDK: "5.0.3"
PYTHON: "3.14"
name: "pyemscripten (Pyodide ${{ matrix.PYODIDE.VERSION }})"
env:
# Pyodide pins emscripten in xbuildenv/Makefile.envs.
PYODIDE_VERSION: ${{ matrix.PYODIDE.VERSION }}
EMSDK_VERSION: ${{ matrix.PYODIDE.EMSDK }}
# Must stay in lockstep with wheel-builder.yml::pyemscripten.
OPENSSL_VERSION: "3.6.2"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
timeout-minutes: 3
with:
persist-credentials: false

- name: Setup python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.PYODIDE.PYTHON }}
timeout-minutes: 3

- uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
# Pyodide's emscripten build passes nightly-only `-Z` flags
# (link-native-libraries, emscripten-wasm-eh).
# TODO: This should be switched to stable by the time pyodide 314.0 is released.
toolchain: nightly
target: wasm32-unknown-emscripten

- name: Cache Rust artifacts (wasm32-emscripten)
uses: ./.github/actions/cache
timeout-minutes: 2
with:
key: pyemscripten-${{ env.PYODIDE_VERSION }}-${{ env.EMSDK_VERSION }}-${{ env.OPENSSL_VERSION }}

- name: Clone test vectors
timeout-minutes: 2
uses: ./.github/actions/fetch-vectors

- name: Install pyodide-build
# `[resolve]` extra pulls in resolvelib + unearth, required by
# `pyodide build`.
run: python -m pip install "pyodide-build[resolve]"

- name: Install Pyodide xbuildenv (cross-build artifacts + patches)
run: pyodide xbuildenv install

- name: Cache emsdk install
id: emsdk-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
timeout-minutes: 2
with:
path: /opt/emsdk
key: emsdk-${{ env.PYODIDE_VERSION }}-${{ env.EMSDK_VERSION }}-${{ runner.os }}-${{ runner.arch }}-0

- name: Install emsdk (Pyodide-pinned)
# Pyodide's xbuildenv ships only patches, not emsdk binaries.
if: steps.emsdk-cache.outputs.cache-hit != 'true'
run: |
set -e
git clone --depth 1 https://github.com/emscripten-core/emsdk.git /opt/emsdk
cd /opt/emsdk
./emsdk install "${EMSDK_VERSION}"
./emsdk activate "${EMSDK_VERSION}"
Comment thread
greateggsgreg marked this conversation as resolved.
Outdated

- name: Cache cross-compiled OpenSSL
id: openssl-pyodide-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
timeout-minutes: 2
with:
path: /tmp/openssl-pyodide
key: openssl-pyodide-${{ env.PYODIDE_VERSION }}-${{ env.OPENSSL_VERSION }}-emscripten-${{ env.EMSDK_VERSION }}-${{ hashFiles('.github/bin/build_openssl.sh') }}-${{ runner.os }}-${{ runner.arch }}-0

- name: Cross-compile OpenSSL for wasm32-emscripten
# Pyodide does not expose a downstream-linkable OpenSSL (its `_ssl`
# module is baked into pyodide.asm.wasm), so we cross-compile our own.
if: steps.openssl-pyodide-cache.outputs.cache-hit != 'true'
env:
TYPE: pyemscripten
VERSION: ${{ env.OPENSSL_VERSION }}
OSSL_PATH: /tmp/openssl-pyodide
run: |
set -e
# shellcheck disable=SC1091
source /opt/emsdk/emsdk_env.sh
bash .github/bin/build_openssl.sh

- name: Build cryptography_vectors wheel
run: |
python -m pip install -r "${UV_REQUIREMENTS_PATH:-.github/requirements/uv-requirements.txt}"
uv build --wheel --no-build-isolation vectors/

- name: Build cryptography pyemscripten wheel
run: |
set -e
# shellcheck disable=SC1091
source /opt/emsdk/emsdk_env.sh
export OPENSSL_DIR=/tmp/openssl-pyodide
export OPENSSL_STATIC=1
export OPENSSL_NO_VENDOR=1
mkdir -p wheelhouse
# pyodide-build 0.34.x ignores --output-dir and writes to ./dist/.
pyodide build .
cp dist/cryptography-*pyemscripten*.whl wheelhouse/

- name: Create Pyodide venv and install wheel + test deps
run: |
set -e
pyodide venv .venv-pyodide
# shellcheck disable=SC1091
source .venv-pyodide/bin/activate
# pytest-xdist (no multiprocessing under Emscripten), pytest-cov
# (no Rust wasm coverage backend), and pytest-pyodide (host-side
# plugin; pulls playwright which has no wasm32 distribution) are
# deliberately omitted. pytest-benchmark satisfies pyproject.toml's
Comment thread
greateggsgreg marked this conversation as resolved.
Outdated
# --benchmark-disable addopt; certifi is needed by
# tests/bench/test_x509.py; bcrypt unblocks the bcrypt-gated cases
# in tests/hazmat/primitives/test_ssh.py (Pyodide ships a wasm
# wheel for it).
pip install pytest pytest-benchmark pretend certifi bcrypt
pip install vectors/dist/cryptography_vectors-*.whl
pip install wheelhouse/cryptography-*.whl
python -c "
from cryptography.hazmat.backends.openssl.backend import backend
print(f'Loaded: {backend.openssl_version_text()}')
print(f'Linked Against: {backend._ffi.string(backend._lib.OPENSSL_VERSION_TEXT).decode(\"ascii\")}')
"

- name: Run tests
run: |
set -e
# shellcheck disable=SC1091
source .venv-pyodide/bin/activate
pytest -p no:cacheprovider \
--wycheproof-root=wycheproof \
--x509-limbo-root=x509-limbo \
tests/

linux-downstream:
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -513,7 +659,7 @@ jobs:
all-green:
# https://github.community/t/is-it-possible-to-require-all-github-actions-tasks-to-pass-without-enumerating-them/117957/4?u=graingert
runs-on: ubuntu-latest
needs: [linux, alpine, distros, macos, windows, linux-downstream]
needs: [linux, alpine, distros, macos, windows, linux-downstream, pyemscripten]
if: ${{ always() }}
timeout-minutes: 3
steps:
Expand Down
95 changes: 95 additions & 0 deletions .github/workflows/wheel-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,98 @@ jobs:
with:
name: "cryptography-${{ github.event.inputs.version }}-${{ matrix.WINDOWS.WINDOWS }}-${{ matrix.PYTHON.VERSION }}-${{ matrix.PYTHON.ABI_VERSION }}"
path: wheelhouse\

pyemscripten:
needs: [sdist]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
PYODIDE:
- VERSION: "314.0.0a1"
EMSDK: "5.0.3"
PYTHON: "3.14"
CIBW_BUILD: "cp314-pyodide_wasm32"
CIBW_ENABLE: "pyodide-prerelease"
WHEEL_TAG: "cp314-pyemscripten_2026_0_wasm32"
name: "${{ matrix.PYODIDE.WHEEL_TAG }}"
env:
# Pyodide pins emscripten in xbuildenv/Makefile.envs.
PYODIDE_VERSION: ${{ matrix.PYODIDE.VERSION }}
EMSDK_VERSION: ${{ matrix.PYODIDE.EMSDK }}
# Must stay in lockstep with ci.yml::pyemscripten.
OPENSSL_VERSION: "3.6.2"
steps:
- name: Get OpenSSL build script from repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.inputs.version || github.ref }}
persist-credentials: false
sparse-checkout: |
.github/bin/build_openssl.sh
sparse-checkout-cone-mode: false

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.PYODIDE.PYTHON }}
timeout-minutes: 3

- uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
# Pyodide's emscripten build passes nightly-only `-Z` flags
# (link-native-libraries, emscripten-wasm-eh).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be possible to build on stable soon, since neither of these flags are needed any longer. It seems like it might still be passing link-native-libraries=yes somewhere which is the default but it's still nightly only.

Copy link
Copy Markdown
Contributor Author

@greateggsgreg greateggsgreg May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrote a TODO near this line and in ci.yml to make sure we validate if we still need nightly by the time pyodide 314.0 is released.

Also updated the issue task list to make sure we don't forget about this.

# TODO: This should be switched to stable by the time pyodide 314.0 is released.
toolchain: nightly
target: wasm32-unknown-emscripten

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: cryptography-sdist

# TODO: switch to released cibuildwheel once pypa/cibuildwheel#2812
# (which adds cp314-pyodide_wasm32 / pyemscripten_2026_0_wasm32) ships
# in a tagged release.
- run: pip install "cibuildwheel @ git+https://github.com/pypa/cibuildwheel.git@main"
- run: mkdir wheelhouse

- name: Cache cross-compiled OpenSSL
# Same key shape as ci.yml::pyemscripten so the cache is shared.
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
timeout-minutes: 2
with:
path: /tmp/openssl-pyodide
key: openssl-pyodide-${{ env.PYODIDE_VERSION }}-${{ env.OPENSSL_VERSION }}-emscripten-${{ env.EMSDK_VERSION }}-${{ hashFiles('.github/bin/build_openssl.sh') }}-${{ runner.os }}-${{ runner.arch }}-0

- name: Build the wheel
# Pyodide does not expose a downstream-linkable OpenSSL (its `_ssl`
# module is baked into pyodide.asm.wasm), so we cross-compile our
# own static libssl/libcrypto via .github/bin/build_openssl.sh
# (TYPE=pyemscripten branch).
run: cibuildwheel --platform pyodide --output-dir wheelhouse/ cryptography*.tar.gz
env:
CIBW_BUILD: ${{ matrix.PYODIDE.CIBW_BUILD }}
CIBW_ENABLE: ${{ matrix.PYODIDE.CIBW_ENABLE }}
CIBW_BEFORE_BUILD_PYODIDE: |
TYPE=pyemscripten \
VERSION="$OPENSSL_VERSION" \
OSSL_PATH=/tmp/openssl-pyodide \
bash ${{ github.workspace }}/.github/bin/build_openssl.sh
CIBW_ENVIRONMENT_PYODIDE: >-
OPENSSL_VERSION=$OPENSSL_VERSION
OPENSSL_DIR=/tmp/openssl-pyodide
OPENSSL_STATIC=1
OPENSSL_NO_VENDOR=1
# Smoketest only; the full pytest suite runs in ci.yml::pyemscripten.
CIBW_TEST_COMMAND_PYODIDE: |
python -c "
from cryptography.hazmat.backends.openssl.backend import backend
print(f'Loaded: {backend.openssl_version_text()}')
print(f'Linked Against: {backend._ffi.string(backend._lib.OPENSSL_VERSION_TEXT).decode(\"ascii\")}')
"

- run: |
echo "CRYPTOGRAPHY_WHEEL_NAME=$(basename $(ls wheelhouse/cryptography*.whl))" >> "$GITHUB_ENV"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "${{ env.CRYPTOGRAPHY_WHEEL_NAME }}"
path: wheelhouse/
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ addopts = "-r s --capture=no --strict-markers --benchmark-disable"
console_output_style = "progress-even-when-capture-no"
markers = [
"skip_fips: this test is not executed in FIPS mode",
"skip_emscripten: this test is not executed under Emscripten/Pyodide",
"supported: parametrized test requiring only_if and skip_message",
]

Expand Down Expand Up @@ -179,6 +180,8 @@ exclude_lines = [
"@abc.abstractmethod",
"@typing.overload",
"if typing.TYPE_CHECKING",
# coverage lacks Rust wasm backend
'if sys.platform == "emscripten":'
Comment thread
greateggsgreg marked this conversation as resolved.
Outdated
]

[tool.ruff]
Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# for complete details.

import contextlib
import sys

import pytest

Expand Down Expand Up @@ -35,6 +36,11 @@ def pytest_runtest_setup(item):
if openssl_backend._fips_enabled:
for marker in item.iter_markers(name="skip_fips"):
pytest.skip(marker.kwargs["reason"])
if sys.platform == "emscripten": # pragma: no cover
for marker in item.iter_markers(name="skip_emscripten"):
pytest.skip(
marker.kwargs.get("reason", "Skipped under Emscripten/Pyodide")
)


@pytest.fixture(autouse=True)
Expand Down
3 changes: 3 additions & 0 deletions tests/test_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ def find_all_modules() -> list[str]:
)


@pytest.mark.skip_emscripten(
reason="subprocess is unavailable under Emscripten/Pyodide"
)
@pytest.mark.parametrize("module", find_all_modules())
def test_no_circular_imports(module):
env = os.environ.copy()
Expand Down
Loading