Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 65 additions & 0 deletions .github/bin/build_pyemscripten_libsodium.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/bin/bash
#
# Cross-compile the bundled libsodium for wasm32-emscripten.
#
# Required env vars:
# SODIUM_SRC - absolute path to the bundled libsodium source tree
# SODIUM_PATH - absolute install prefix
#
# Optional env vars (mirror setup.py's build_clib contract):
# LIBSODIUM_MAKE_ARGS - args to `make` (default: -j$(nproc))
# SODIUM_INSTALL_MINIMAL - if non-empty, pass --enable-minimal
#
# Requires emsdk activated on PATH (emcc, emconfigure, emmake).
# Idempotent so a warm actions/cache hit short-circuits the rebuild.

set -euo pipefail

: "${SODIUM_SRC:?SODIUM_SRC must point at the bundled libsodium source tree}"
: "${SODIUM_PATH:?SODIUM_PATH must point at the install prefix}"

if [ -f "${SODIUM_PATH}/lib/libsodium.a" ]; then
echo "libsodium already built at ${SODIUM_PATH}; skipping rebuild."
exit 0
fi

# A fresh checkout on case-insensitive or unusual filesystems can lose +x.
chmod +x "${SODIUM_SRC}/configure" "${SODIUM_SRC}/autogen.sh" 2>/dev/null || true

mkdir -p "${SODIUM_PATH}"

NCORES="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)"
read -r -a MAKE_ARGS <<< "${LIBSODIUM_MAKE_ARGS:--j${NCORES}}"

CONFIGURE_EXTRA=()
if [ -n "${SODIUM_INSTALL_MINIMAL:-}" ]; then
CONFIGURE_EXTRA+=(--enable-minimal)
fi

pushd "${SODIUM_SRC}"

# The first six flags below also appear in setup.py:131-149 — keep both
# sites in sync. The remainder are emscripten-specific and would be
# rejected (or wrong) on a native build.
emconfigure ./configure \
--host=wasm32-unknown-emscripten \
--disable-shared \
--enable-static \
--with-pic \
--disable-asm \
--disable-pie \
--disable-ssp \
--disable-dependency-tracking \
--disable-debug \
--without-pthreads \
"${CONFIGURE_EXTRA[@]}" \
--prefix="${SODIUM_PATH}"

emmake make "${MAKE_ARGS[@]}"
emmake make install

popd

test -f "${SODIUM_PATH}/lib/libsodium.a"
test -f "${SODIUM_PATH}/include/sodium.h"
echo "libsodium cross-compiled successfully to ${SODIUM_PATH}"
91 changes: 90 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,98 @@ jobs:
shell: cmd
- uses: ./.github/actions/upload-coverage

pyemscripten:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Keep in lockstep with wheel-builder.yml::pyemscripten.
PYODIDE:
# TODO: switch to stable Pyodide 314.0 when released.
- VERSION: "314.0.0a2"
EMSDK: "5.0.3"
PYTHON: "3.14"
# PEP 783 platform tag emitted by pyodide-build for this xbuildenv.
WHEEL_TAG: "cp314-pyemscripten_2026_0_wasm32"
name: "${{ matrix.PYODIDE.WHEEL_TAG }} (Pyodide ${{ matrix.PYODIDE.VERSION }})"
env:
PYODIDE_VERSION: ${{ matrix.PYODIDE.VERSION }}
EMSDK_VERSION: ${{ matrix.PYODIDE.EMSDK }}
WHEEL_TAG: ${{ matrix.PYODIDE.WHEEL_TAG }}
steps:
- uses: actions/checkout@v6.0.2

- name: Setup python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.PYODIDE.PYTHON }}

- name: Install pyodide-build
# 0.34.1+ ships the PEP 783 `pyemscripten` platform name and the
# `xbuildenv install-emscripten` subcommand this job relies on.
run: python -m pip install "pyodide-build[resolve]==0.34.3"

- name: Install Pyodide xbuildenv (cross-build artifacts + patches)
# Explicit version arg — `pyodide xbuildenv install` does not read
# PYODIDE_VERSION; without this the matrix pin would not be
# enforced (selection falls back to latest host-Python-compatible).
run: pyodide xbuildenv install "${PYODIDE_VERSION}"

- name: Install Pyodide-pinned Emscripten SDK
# Pyodide patches emscripten; vanilla emsdk produces broken wheels.
run: pyodide xbuildenv install-emscripten

- name: Cache cross-compiled libsodium
id: sodium-cache
uses: actions/cache@v5
with:
path: /tmp/libsodium-pyodide
# min0/min1 partitions full vs --enable-minimal builds.
key: libsodium-pyodide-${{ env.PYODIDE_VERSION }}-${{ env.EMSDK_VERSION }}-min${{ matrix.PYODIDE.SODIUM_INSTALL_MINIMAL || '0' }}-${{ hashFiles('.github/bin/build_pyemscripten_libsodium.sh', 'src/libsodium/configure.ac') }}-${{ runner.os }}-${{ runner.arch }}-0

- name: Cross-compile libsodium for wasm32-emscripten
if: steps.sodium-cache.outputs.cache-hit != 'true'
env:
SODIUM_SRC: ${{ github.workspace }}/src/libsodium
SODIUM_PATH: /tmp/libsodium-pyodide
SODIUM_INSTALL_MINIMAL: ${{ matrix.PYODIDE.SODIUM_INSTALL_MINIMAL }}
run: |
set -e
# shellcheck disable=SC1091
source "$(pyodide config get emsdk_dir)/emsdk_env.sh"
bash .github/bin/build_pyemscripten_libsodium.sh

- name: Build pyemscripten wheel
env:
SODIUM_INSTALL: system
run: |
set -e
# shellcheck disable=SC1091
source "$(pyodide config get emsdk_dir)/emsdk_env.sh"
export CFLAGS="-I/tmp/libsodium-pyodide/include ${CFLAGS:-}"
export LDFLAGS="-L/tmp/libsodium-pyodide/lib ${LDFLAGS:-}"
mkdir -p wheelhouse
pyodide build .
# Match WHEEL_TAG exactly: catches silent xbuildenv ABI drift.
cp "dist/pynacl-${WHEEL_TAG}.whl" wheelhouse/ 2>/dev/null \
|| cp dist/pynacl-*-${WHEEL_TAG}.whl wheelhouse/

- name: Run tests
run: |
set -e
pyodide venv .venv-pyodide
# shellcheck disable=SC1091
source .venv-pyodide/bin/activate
# pytest-xdist omitted: no multiprocessing under Emscripten.
pip install pytest pytest-cov hypothesis
pip install wheelhouse/pynacl-*.whl
python -c "import nacl.signing; key = nacl.signing.SigningKey.generate(); signature = key.sign(b'test'); key.verify_key.verify(signature)"
pytest -p no:cacheprovider --cov=nacl --cov=tests tests/
- uses: ./.github/actions/upload-coverage

all-green:
runs-on: ubuntu-latest
needs: [linux, macos, windows]
needs: [linux, macos, windows, pyemscripten]
if: ${{ always() }}
timeout-minutes: 3
steps:
Expand Down
91 changes: 88 additions & 3 deletions .github/workflows/wheel-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
- run: auditwheel repair --plat ${{ matrix.MANYLINUX.NAME }} tmpwheelhouse/pynacl*.whl -w wheelhouse/
- run: .venv/bin/pip install pynacl --no-index -f wheelhouse/
- run: |
.venv/bin/python -c "import nacl.signing; key = nacl.signing.SigningKey.generate();signature = key.sign(b'test'); key.verify_key.verify(signature)"
.venv/bin/python -c "import nacl.signing; key = nacl.signing.SigningKey.generate(); signature = key.sign(b'test'); key.verify_key.verify(signature)"

- run: mkdir pynacl-wheelhouse
- run: mv wheelhouse/pynacl*.whl pynacl-wheelhouse/
Expand Down Expand Up @@ -159,7 +159,7 @@ jobs:
_PYTHON_HOST_PLATFORM: 'macosx-10.9-universal2'
- run: venv/bin/pip install -f wheelhouse --no-index pynacl
- run: |
venv/bin/python -c "import nacl.signing; key = nacl.signing.SigningKey.generate();signature = key.sign(b'test'); key.verify_key.verify(signature)"
venv/bin/python -c "import nacl.signing; key = nacl.signing.SigningKey.generate(); signature = key.sign(b'test'); key.verify_key.verify(signature)"

- run: mkdir pynacl-wheelhouse
- run: mv wheelhouse/pynacl*.whl pynacl-wheelhouse/
Expand Down Expand Up @@ -228,10 +228,95 @@ jobs:
run: pip install -f wheelhouse pynacl --no-index
- name: Test the installed wheel
run: |
python -c "import nacl.signing; key = nacl.signing.SigningKey.generate();signature = key.sign(b'test'); key.verify_key.verify(signature)"
python -c "import nacl.signing; key = nacl.signing.SigningKey.generate(); signature = key.sign(b'test'); key.verify_key.verify(signature)"
- run: mkdir pynacl-wheelhouse
- run: move wheelhouse\pynacl*.whl pynacl-wheelhouse\
- uses: actions/upload-artifact@v7
with:
name: "pynacl-${{ github.event.inputs.version }}-win-${{ matrix.WINDOWS.ARCH }}-${{ matrix.PYTHON.VERSION }}"
path: pynacl-wheelhouse\

pyemscripten:
needs: [sdist]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
PYODIDE:
# TODO: switch to stable Pyodide 314.0 when released. Keep these
# pins in lockstep with ci.yml::pyemscripten.
- VERSION: "314.0.0a2"
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_VERSION: ${{ matrix.PYODIDE.VERSION }}
EMSDK_VERSION: ${{ matrix.PYODIDE.EMSDK }}
WHEEL_TAG: ${{ matrix.PYODIDE.WHEEL_TAG }}
steps:
- name: Get build script and bundled libsodium from repository
# The sdist doesn't carry the cross-compile script or .github/bin,
# but CIBW_BEFORE_BUILD_PYODIDE and CIBW_TEST_COMMAND_PYODIDE need them.
uses: actions/checkout@v6.0.2
with:
ref: ${{ github.event.inputs.version || github.ref }}
persist-credentials: false
sparse-checkout: |
.github/bin/build_pyemscripten_libsodium.sh
src/libsodium
sparse-checkout-cone-mode: false

- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.PYODIDE.PYTHON }}

- uses: actions/download-artifact@v8.0.1
with:
name: pynacl-sdist

# 4.0.0rc1 is the first release carrying pypa/cibuildwheel#2812
# (cp314-pyodide_wasm32 / pyemscripten_2026_0_wasm32). Bump to the
# stable 4.0.0 tag once it lands.
- run: pip install "cibuildwheel==4.0.0rc1"
- run: mkdir wheelhouse

- name: Cache cross-compiled libsodium
# Key shape matches ci.yml::pyemscripten so both jobs share the cache.
uses: actions/cache@v5
with:
path: /tmp/libsodium-pyodide
key: libsodium-pyodide-${{ env.PYODIDE_VERSION }}-${{ env.EMSDK_VERSION }}-min${{ matrix.PYODIDE.SODIUM_INSTALL_MINIMAL || '0' }}-${{ hashFiles('.github/bin/build_pyemscripten_libsodium.sh', 'src/libsodium/configure.ac') }}-${{ runner.os }}-${{ runner.arch }}-0

- name: Build the wheel
run: cibuildwheel --platform pyodide --output-dir wheelhouse/ pynacl-*.tar.gz
env:
CIBW_BUILD: ${{ matrix.PYODIDE.CIBW_BUILD }}
CIBW_ENABLE: ${{ matrix.PYODIDE.CIBW_ENABLE }}
# BEFORE_BUILD (not BEFORE_ALL) — emsdk only lives on PATH
# inside cibuildwheel's build env, where BEFORE_BUILD runs.
# BEFORE_ALL would run on the host with no emconfigure available.
# The script's own libsodium.a-exists check makes the per-Python
# repeat cheap if the matrix ever grows.
CIBW_BEFORE_BUILD_PYODIDE: |
SODIUM_SRC=${{ github.workspace }}/src/libsodium \
SODIUM_PATH=/tmp/libsodium-pyodide \
SODIUM_INSTALL_MINIMAL=${{ matrix.PYODIDE.SODIUM_INSTALL_MINIMAL }} \
bash ${{ github.workspace }}/.github/bin/build_pyemscripten_libsodium.sh
CIBW_ENVIRONMENT_PYODIDE: >-
SODIUM_INSTALL=system
CFLAGS=-I/tmp/libsodium-pyodide/include
LDFLAGS=-L/tmp/libsodium-pyodide/lib
# Full pytest suite runs in ci.yml::pyemscripten — smoketest only here.
CIBW_TEST_COMMAND_PYODIDE: |
python -c "import nacl.signing; key = nacl.signing.SigningKey.generate(); signature = key.sign(b'test'); key.verify_key.verify(signature)"

- run: mkdir pynacl-wheelhouse
# Match WHEEL_TAG exactly: catches silent xbuildenv ABI drift.
- run: mv wheelhouse/pynacl-*-${WHEEL_TAG}.whl pynacl-wheelhouse/
- uses: actions/upload-artifact@v7
with:
name: "pynacl-${{ github.event.inputs.version }}-${{ matrix.PYODIDE.WHEEL_TAG }}"
path: pynacl-wheelhouse/
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ exclude_also = [
"@abc.abstractmethod",
"@typing.overload",
"if typing.TYPE_CHECKING",
# Pyodide branches never run under the coverage-collecting CI jobs.
'if sys.platform == "emscripten":',
]

[tool.coverage.html]
Expand All @@ -157,4 +159,7 @@ show_contexts = true
[tool.pytest.ini_options]
addopts = "-r s --capture=no"
console_output_style = "progress-even-when-capture-no"
markers = [
"skip_emscripten: this test is not executed under Emscripten/Pyodide",
]

3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ def run(self):
# Locate our configure script
configure = abshere("src/libsodium/configure")

# Run ./configure
# Mirrored by .github/bin/build_pyemscripten_libsodium.sh —
# keep in sync.
configure_flags = [
"--disable-shared",
"--enable-static",
Expand Down
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sys

import pytest


def pytest_runtest_setup(item: pytest.Item) -> None:
if sys.platform == "emscripten":
for marker in item.iter_markers(name="skip_emscripten"):
pytest.skip(
marker.kwargs.get("reason", "Skipped under Emscripten/Pyodide")
)
4 changes: 2 additions & 2 deletions tests/test_aead.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def test_variants_roundtrip_aad(
with pytest.raises(exc.CryptoError):
ct1 = bytearray(ct)
ct1[0] = ct1[0] ^ 0xFF
c.decrypt(ct1, aad, unonce, ukey)
c.decrypt(bytes(ct1), aad, unonce, ukey)


@given(
Expand Down Expand Up @@ -193,7 +193,7 @@ def test_variants_roundtrip_no_aad(
with pytest.raises(exc.CryptoError):
ct1 = bytearray(ct)
ct1[0] = ct1[0] ^ 0xFF
c.decrypt(ct1, aad, unonce, ukey)
c.decrypt(bytes(ct1), aad, unonce, ukey)


@pytest.mark.parametrize(
Expand Down
Loading