diff --git a/.github/workflows/linuxbrew.yml b/.github/workflows/linuxbrew.yml index 51b0db1e..fce518f3 100644 --- a/.github/workflows/linuxbrew.yml +++ b/.github/workflows/linuxbrew.yml @@ -1,5 +1,5 @@ name: linuxbrew -on: [push, pull_request] +on: [push] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref_name != 'master' }} @@ -10,7 +10,7 @@ jobs: strategy: matrix: - python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python: ["3.14"] env: # For some unknown reason, linuxbrew tries to use "gcc-11" by default, which doesn't exist. @@ -28,17 +28,39 @@ jobs: - name: Install build dependencies run: | brew update - brew install python@${{ matrix.python }} gcc libxml2 libxmlsec1 pkg-config + brew install python@${{ matrix.python }} gcc libxml2 libxslt libxmlsec1 pkg-config echo "/home/linuxbrew/.linuxbrew/opt/python@${{ matrix.python }}/libexec/bin" >> $GITHUB_PATH + - name: Configure Homebrew toolchain + run: | + HOMEBREW_PREFIX="$(brew --prefix)" + LIBXML2_PREFIX="$(brew --prefix libxml2)" + LIBXSLT_PREFIX="$(brew --prefix libxslt)" + XMLSEC_PREFIX="$(brew --prefix libxmlsec1)" + OPENSSL_PREFIX="$(brew --prefix openssl@3)" + + { + echo "PKG_CONFIG_PATH=${LIBXML2_PREFIX}/lib/pkgconfig:${LIBXSLT_PREFIX}/lib/pkgconfig:${XMLSEC_PREFIX}/lib/pkgconfig:${OPENSSL_PREFIX}/lib/pkgconfig" + echo "CPPFLAGS=-I${LIBXML2_PREFIX}/include -I${LIBXSLT_PREFIX}/include -I${XMLSEC_PREFIX}/include/xmlsec1 -I${OPENSSL_PREFIX}/include" + echo "CFLAGS=-I${LIBXML2_PREFIX}/include -I${LIBXSLT_PREFIX}/include -I${XMLSEC_PREFIX}/include/xmlsec1 -I${OPENSSL_PREFIX}/include" + echo "LDFLAGS=-L${LIBXML2_PREFIX}/lib -L${LIBXSLT_PREFIX}/lib -L${XMLSEC_PREFIX}/lib -L${OPENSSL_PREFIX}/lib" + echo "LD_LIBRARY_PATH=${LIBXML2_PREFIX}/lib:${LIBXSLT_PREFIX}/lib:${XMLSEC_PREFIX}/lib:${OPENSSL_PREFIX}/lib" + echo "LIBRARY_PATH=${LIBXML2_PREFIX}/lib:${LIBXSLT_PREFIX}/lib:${XMLSEC_PREFIX}/lib:${OPENSSL_PREFIX}/lib" + echo "C_INCLUDE_PATH=${LIBXML2_PREFIX}/include:${LIBXSLT_PREFIX}/include:${XMLSEC_PREFIX}/include/xmlsec1:${OPENSSL_PREFIX}/include" + echo "XML2_CONFIG=${LIBXML2_PREFIX}/bin/xml2-config" + echo "XSLT_CONFIG=${LIBXSLT_PREFIX}/bin/xslt-config" + } >> $GITHUB_ENV + + echo "${LIBXML2_PREFIX}/bin" >> $GITHUB_PATH + echo "${LIBXSLT_PREFIX}/bin" >> $GITHUB_PATH + - name: Build wheel run: | python3 -m venv build_venv source build_venv/bin/activate - pip3 install --upgrade setuptools wheel build - export CFLAGS="-I$(brew --prefix)/include" - export LDFLAGS="-L$(brew --prefix)/lib" - python3 -m build + pip3 install --upgrade setuptools wheel build setuptools_scm pkgconfig + pip3 install --upgrade --no-binary=lxml -r requirements.txt + python3 -m build --no-isolation rm -rf build/ - name: Run tests diff --git a/.github/workflows/macosx.yml b/.github/workflows/macosx.yml index c9d8034e..6ab8b42f 100644 --- a/.github/workflows/macosx.yml +++ b/.github/workflows/macosx.yml @@ -1,5 +1,6 @@ name: macOS -on: [push, pull_request] +on: + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref_name != 'master' }} diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml index fe31b66e..ad8a04ab 100644 --- a/.github/workflows/manylinux.yml +++ b/.github/workflows/manylinux.yml @@ -1,5 +1,6 @@ name: manylinux -on: [push, pull_request] +on: + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref_name != 'master' }} diff --git a/.github/workflows/sdist.yml b/.github/workflows/sdist.yml index f48ca02c..18ac1c74 100644 --- a/.github/workflows/sdist.yml +++ b/.github/workflows/sdist.yml @@ -1,5 +1,6 @@ name: sdist -on: [push, pull_request] +on: + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref_name != 'master' }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 422b038e..fdf4630d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,18 +1,6 @@ name: Wheel build on: - release: - types: [created] - schedule: - # ┌───────────── minute (0 - 59) - # │ ┌───────────── hour (0 - 23) - # │ │ ┌───────────── day of the month (1 - 31) - # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) - # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) - # │ │ │ │ │ - - cron: "42 3 * * 4" - push: - pull_request: workflow_dispatch: concurrency: diff --git a/src/main.c b/src/main.c index 61eac139..2011b889 100644 --- a/src/main.c +++ b/src/main.c @@ -22,6 +22,7 @@ #define _PYXMLSEC_FREE_XMLSEC 1 #define _PYXMLSEC_FREE_CRYPTOLIB 2 #define _PYXMLSEC_FREE_ALL 3 +#define _PYXMLSEC_FREE_ALL_BUT_CRYPTOLIB 4 static int free_mode = _PYXMLSEC_FREE_NONE; @@ -44,6 +45,7 @@ static void PyXmlSec_Free(int what) { PYXMLSEC_DEBUGF("free resources %d", what); switch (what) { case _PYXMLSEC_FREE_ALL: + xmlSecCryptoShutdown(); xmlSecCryptoAppShutdown(); case _PYXMLSEC_FREE_CRYPTOLIB: #ifndef XMLSEC_NO_CRYPTO_DYNAMIC_LOADING @@ -51,6 +53,12 @@ static void PyXmlSec_Free(int what) { #endif case _PYXMLSEC_FREE_XMLSEC: xmlSecShutdown(); + break; + case _PYXMLSEC_FREE_ALL_BUT_CRYPTOLIB: + xmlSecCryptoShutdown(); + xmlSecCryptoAppShutdown(); + xmlSecShutdown(); + break; } free_mode = _PYXMLSEC_FREE_NONE; } @@ -94,7 +102,10 @@ static int PyXmlSec_Init(void) { // We thus reinstall our callback now. PyXmlSec_InstallErrorCallback(); - free_mode = _PYXMLSEC_FREE_ALL; + // Keep the dynamically loaded crypto backend resident for the lifetime of + // the process. Python-level constants cache xmlsec transform/keydata ids, + // and unloading the backend invalidates those pointers after shutdown/init. + free_mode = _PYXMLSEC_FREE_ALL_BUT_CRYPTOLIB; return 0; } @@ -113,8 +124,8 @@ static PyObject* PyXmlSec_PyInit(PyObject *self) { static char PyXmlSec_PyShutdown__doc__[] = \ "shutdown() -> None\n" "Shutdowns the library and cleanup any leftover resources.\n\n" - "This is called automatically upon interpreter termination and\n" - "should not need to be called explicitly."; + "This is not called automatically upon interpreter termination because\n" + "xmlsec-owned objects may still be finalized during Python shutdown."; static PyObject* PyXmlSec_PyShutdown(PyObject* self) { PyXmlSec_Free(free_mode); Py_RETURN_NONE; @@ -487,11 +498,6 @@ int PyXmlSec_EncModule_Init(PyObject* package); // templates management int PyXmlSec_TemplateModule_Init(PyObject* package); -static int PyXmlSec_PyClear(PyObject *self) { - PyXmlSec_Free(free_mode); - return 0; -} - static PyModuleDef PyXmlSecModule = { PyModuleDef_HEAD_INIT, STRINGIFY(MODULE_NAME), /* name of module */ @@ -501,7 +507,7 @@ static PyModuleDef PyXmlSecModule = { PyXmlSec_MainMethods, /* m_methods */ NULL, /* m_slots */ NULL, /* m_traverse */ - PyXmlSec_PyClear, /* m_clear */ + NULL, /* m_clear */ NULL, /* m_free */ }; diff --git a/tests/test_xmlsec.py b/tests/test_xmlsec.py index 52dce2b3..5d69d106 100644 --- a/tests/test_xmlsec.py +++ b/tests/test_xmlsec.py @@ -1,3 +1,8 @@ +import subprocess +import sys +import unittest +from pathlib import Path + import xmlsec from tests import base @@ -11,3 +16,33 @@ def test_reinitialize_module(self): """ xmlsec.shutdown() xmlsec.init() + + +class TestInterpreterShutdown(unittest.TestCase): + def test_interpreter_exit_with_live_xmlsec_objects(self): + key_path = Path(__file__).with_name('data') / 'rsakey.pem' + script = f""" +import xmlsec + +key = xmlsec.Key.from_file({str(key_path)!r}, format=xmlsec.constants.KeyDataFormatPem) +ctx = xmlsec.SignatureContext() +ctx.key = key +""" + proc = subprocess.run([sys.executable, '-c', script], capture_output=True, text=True) + self.assertEqual(proc.returncode, 0, proc.stderr) + + def test_reinitialize_module_preserves_constants(self): + script = """ +import xmlsec + +transform = xmlsec.constants.TransformExclC14N +keydata = xmlsec.constants.KeyDataAes + +xmlsec.shutdown() +xmlsec.init() + +assert transform.name == "exc-c14n" +assert keydata.name == "aes" +""" + proc = subprocess.run([sys.executable, '-c', script], capture_output=True, text=True) + self.assertEqual(proc.returncode, 0, proc.stderr)