From 967754b6ec084d3ee5baa73ad256cf19f0f231de Mon Sep 17 00:00:00 2001 From: Jim Jagielski Date: Fri, 11 Apr 2025 11:49:01 -0400 Subject: [PATCH 01/46] Ensure we don't use the wheel, which uses older versions of lxml2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 014bfe10..3490b822 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -lxml >= 3.8.0, !=4.7.0 +lxml >= 3.8.0, !=4.7.0 --no-binary=lxml From f3f9ba34af99f8c857f9c4f1fe95f8485a0e077f Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Sat, 5 Jul 2025 16:15:16 +0200 Subject: [PATCH 02/46] Revert "Ensure we don't use the wheel, which uses older versions of lxml2" This reverts commit 967754b6ec084d3ee5baa73ad256cf19f0f231de. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3490b822..014bfe10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -lxml >= 3.8.0, !=4.7.0 --no-binary=lxml +lxml >= 3.8.0, !=4.7.0 From 40b755f700b413684abca2d71d78a14c9740cfc7 Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Sat, 5 Jul 2025 16:16:24 +0200 Subject: [PATCH 03/46] Update library versions based on lxml 6.0.0 --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index af3fb57d..4303504a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -100,8 +100,8 @@ jobs: include: ${{ fromJson(needs.generate-wheels-matrix.outputs.include) }} env: - PYXMLSEC_LIBXML2_VERSION: 2.13.5 - PYXMLSEC_LIBXSLT_VERSION: 1.1.42 + PYXMLSEC_LIBXML2_VERSION: 2.14.4 + PYXMLSEC_LIBXSLT_VERSION: 1.1.43 steps: - name: Check out the repo From 6e691b36f95b9421ccacf64d6c5ec269bc0a9e43 Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Sat, 5 Jul 2025 16:31:28 +0200 Subject: [PATCH 04/46] Enable ripemd60 As suggested in https://github.com/xmlsec/python-xmlsec/issues/344#issuecomment-2894393505 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 92588ebc..c1cdce44 100644 --- a/setup.py +++ b/setup.py @@ -523,6 +523,7 @@ def prepare_static_build(self, build_platform): '--disable-shared', '--disable-gost', '--enable-md5', + '--enable-ripemd160', '--disable-crypto-dl', '--enable-static=yes', '--enable-shared=no', From d6ea8dec42a870a3971b15e83d65c5c08d5664ac Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Mon, 7 Jul 2025 00:46:52 +0200 Subject: [PATCH 05/46] Fix failing tests in sdist workflows --- .github/workflows/sdist.yml | 6 +++++- .github/workflows/wheels.yml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sdist.yml b/.github/workflows/sdist.yml index e987cdce..04f22c14 100644 --- a/.github/workflows/sdist.yml +++ b/.github/workflows/sdist.yml @@ -2,7 +2,11 @@ name: sdist on: [push, pull_request] jobs: sdist: - runs-on: ubuntu-latest + # Avoid Ubuntu 24.04 in sdist workflows, because it contains libxmlsec1-dev + # v1.2.39, which has a bug that causes tests/test_pkcs11.py to fail. + # (It thinks the softhsm engine has a public key instead of a private key.) + # libxmlsec1 <=1.2.33 or >=1.2.42 works. TODO: Try 26.04 when available. + runs-on: ubuntu-22.04 strategy: matrix: python: ["3.9", "3.10", "3.11", "3.12", "3.13"] diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4303504a..f9294fd3 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -19,7 +19,11 @@ permissions: {} jobs: sdist: - runs-on: ubuntu-latest + # Avoid Ubuntu 24.04 in sdist workflows, because it contains libxmlsec1-dev + # v1.2.39, which has a bug that causes tests/test_pkcs11.py to fail. + # (It thinks the softhsm engine has a public key instead of a private key.) + # libxmlsec1 <=1.2.33 or >=1.2.42 works. TODO: Try 26.04 when available. + runs-on: ubuntu-22.04 permissions: contents: write From bcd364cdb8e99f49172eaae67b2572ffaa7b4e61 Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Mon, 7 Jul 2025 01:09:15 +0200 Subject: [PATCH 06/46] Show stdout in setup.py, not only stderr --- setup.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/setup.py b/setup.py index c1cdce44..d5cf53aa 100644 --- a/setup.py +++ b/setup.py @@ -435,17 +435,17 @@ def prepare_static_build(self, build_platform): openssl_config_cmd.append(cross_compiling.triplet) else: openssl_config_cmd.insert(0, './config') - subprocess.check_output(openssl_config_cmd, cwd=str(openssl_dir), env=env) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(openssl_dir), env=env) - subprocess.check_output( + subprocess.check_call(openssl_config_cmd, cwd=str(openssl_dir), env=env) + subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(openssl_dir), env=env) + subprocess.check_call( ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install_sw'], cwd=str(openssl_dir), env=env ) self.info('Building zlib') zlib_dir = next(self.build_libs_dir.glob('zlib-*')) - subprocess.check_output(['./configure', prefix_arg], cwd=str(zlib_dir), env=env) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(zlib_dir), env=env) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(zlib_dir), env=env) + subprocess.check_call(['./configure', prefix_arg], cwd=str(zlib_dir), env=env) + subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(zlib_dir), env=env) + subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(zlib_dir), env=env) host_arg = "" if cross_compiling: @@ -453,7 +453,7 @@ def prepare_static_build(self, build_platform): self.info('Building libiconv') libiconv_dir = next(self.build_libs_dir.glob('libiconv-*')) - subprocess.check_output( + subprocess.check_call( [ './configure', prefix_arg, @@ -464,14 +464,14 @@ def prepare_static_build(self, build_platform): cwd=str(libiconv_dir), env=env, ) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libiconv_dir), env=env) - subprocess.check_output( + subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libiconv_dir), env=env) + subprocess.check_call( ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libiconv_dir), env=env ) self.info('Building LibXML2') libxml2_dir = next(self.build_libs_dir.glob('libxml2-*')) - subprocess.check_output( + subprocess.check_call( [ './configure', prefix_arg, @@ -486,14 +486,14 @@ def prepare_static_build(self, build_platform): cwd=str(libxml2_dir), env=env, ) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libxml2_dir), env=env) - subprocess.check_output( + subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libxml2_dir), env=env) + subprocess.check_call( ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libxml2_dir), env=env ) self.info('Building libxslt') libxslt_dir = next(self.build_libs_dir.glob('libxslt-*')) - subprocess.check_output( + subprocess.check_call( [ './configure', prefix_arg, @@ -507,8 +507,8 @@ def prepare_static_build(self, build_platform): cwd=str(libxslt_dir), env=env, ) - subprocess.check_output(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libxslt_dir), env=env) - subprocess.check_output( + subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libxslt_dir), env=env) + subprocess.check_call( ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libxslt_dir), env=env ) @@ -516,7 +516,7 @@ def prepare_static_build(self, build_platform): ldflags.append('-lpthread') env['LDFLAGS'] = ' '.join(ldflags) xmlsec1_dir = next(self.build_libs_dir.glob('xmlsec1-*')) - subprocess.check_output( + subprocess.check_call( [ './configure', prefix_arg, @@ -537,13 +537,13 @@ def prepare_static_build(self, build_platform): cwd=str(xmlsec1_dir), env=env, ) - subprocess.check_output( + subprocess.check_call( ['make', '-j{}'.format(multiprocessing.cpu_count() + 1)] + ['-I{}'.format(str(self.prefix_dir / 'include')), '-I{}'.format(str(self.prefix_dir / 'include' / 'libxml'))], cwd=str(xmlsec1_dir), env=env, ) - subprocess.check_output( + subprocess.check_call( ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(xmlsec1_dir), env=env ) From c63a4869f9facc60e810a01c3e1cd32d5d14a810 Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Mon, 7 Jul 2025 01:10:56 +0200 Subject: [PATCH 07/46] Download xmlsec1 releases from GitHub The server at www.aleksey.com (Cloudflare?) returns 403 Forbidden errors for some combination of: - missing User-Agent header - GitHub Actions CI runner IP address range - TLS settings (version, cipher suite) used by Python <= 3.9 --- setup.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index d5cf53aa..8383db4d 100644 --- a/setup.py +++ b/setup.py @@ -70,22 +70,18 @@ def latest_release_from_gnome_org_cache(url, lib_name): return '{}/{}'.format(url, latest_source) -def latest_release_from_github_api(repo): - api_url = 'https://api.github.com/repos/{}/releases'.format(repo) +def latest_release_json_from_github_api(repo): + api_url = 'https://api.github.com/repos/{}/releases/latest'.format(repo) # if we are running in CI, pass along the GH_TOKEN, so we don't get rate limited token = os.environ.get("GH_TOKEN") if token: log.info("Using GitHub token to avoid rate limiting") - api_releases = make_request(api_url, token, json_response=True) - releases = [r['tarball_url'] for r in api_releases if r['prerelease'] is False and r['draft'] is False] - if not releases: - raise DistutilsError('No release found for {}'.format(repo)) - return releases[0] + return make_request(api_url, token, json_response=True) def latest_openssl_release(): - return latest_release_from_github_api('openssl/openssl') + return latest_release_json_from_github_api('openssl/openssl')['tarball_url'] def latest_zlib_release(): @@ -105,7 +101,9 @@ def latest_libxslt_release(): def latest_xmlsec_release(): - return latest_release_from_html('https://www.aleksey.com/xmlsec/download/', re.compile('xmlsec1-(?P.*).tar.gz')) + assets = latest_release_json_from_github_api('lsh123/xmlsec')['assets'] + (tar_gz,) = [asset for asset in assets if asset['name'].endswith('.tar.gz')] + return tar_gz['browser_download_url'] class CrossCompileInfo: @@ -381,7 +379,7 @@ def prepare_static_build(self, build_platform): url = latest_xmlsec_release() self.info('{:10}: {}'.format('xmlsec1', 'PYXMLSEC_XMLSEC1_VERSION unset, downloading latest from {}'.format(url))) else: - url = 'https://www.aleksey.com/xmlsec/download/xmlsec1-{}.tar.gz'.format(self.xmlsec1_version) + url = 'https://github.com/lsh123/xmlsec/releases/download/{v}/xmlsec1-{v}.tar.gz'.format(v=self.xmlsec1_version) self.info( '{:10}: {}'.format( 'xmlsec1', 'PYXMLSEC_XMLSEC1_VERSION={}, downloading from {}'.format(self.xmlsec1_version, url) From 7f1e1d9a48bcf2475f34ad67d25c5a7c0a0b7700 Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Mon, 7 Jul 2025 01:14:49 +0200 Subject: [PATCH 08/46] Fix empty argument to ./configure This solves the warning: configure: WARNING: you should use --build, --host, --target --- setup.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 8383db4d..c70381d9 100644 --- a/setup.py +++ b/setup.py @@ -445,9 +445,9 @@ def prepare_static_build(self, build_platform): subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(zlib_dir), env=env) subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(zlib_dir), env=env) - host_arg = "" + host_arg = [] if cross_compiling: - host_arg = '--host={}'.format(cross_compiling.arch) + host_arg = ['--host={}'.format(cross_compiling.arch)] self.info('Building libiconv') libiconv_dir = next(self.build_libs_dir.glob('libiconv-*')) @@ -457,7 +457,7 @@ def prepare_static_build(self, build_platform): prefix_arg, '--disable-dependency-tracking', '--disable-shared', - host_arg, + *host_arg, ], cwd=str(libiconv_dir), env=env, @@ -479,7 +479,7 @@ def prepare_static_build(self, build_platform): '--without-python', '--with-iconv={}'.format(self.prefix_dir), '--with-zlib={}'.format(self.prefix_dir), - host_arg, + *host_arg, ], cwd=str(libxml2_dir), env=env, @@ -500,7 +500,7 @@ def prepare_static_build(self, build_platform): '--without-python', '--without-crypto', '--with-libxml-prefix={}'.format(self.prefix_dir), - host_arg, + *host_arg, ], cwd=str(libxslt_dir), env=env, @@ -530,7 +530,7 @@ def prepare_static_build(self, build_platform): '--with-openssl={}'.format(self.prefix_dir), '--with-libxml={}'.format(self.prefix_dir), '--with-libxslt={}'.format(self.prefix_dir), - host_arg, + *host_arg, ], cwd=str(xmlsec1_dir), env=env, From 9c11b8fbf07131dd4709e0d09af89db64f53af9f Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Mon, 7 Jul 2025 01:16:04 +0200 Subject: [PATCH 09/46] Update wheel availability based on lxml 6.0.0 --- pyproject.toml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c03f910..540f91c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,12 +54,10 @@ skip = [ "*-musllinux_i686", # LXML doesn't publish wheels for these platforms, which makes it # difficult for us to build wheels, so we exclude them. - "cp36-manylinux_aarch64", - "cp37-manylinux_aarch64", - "cp36-musllinux_aarch64", - "cp37-musllinux_aarch64", - "cp36-macosx*", - "cp37-macosx*", + "cp36-*", + "cp37-*", + "cp38-manylinux_aarch64", + "cp38-musllinux_aarch64", "cp38-macosx*", ] test-command = "pytest -v --color=yes {package}/tests" From c3f3df2a301ef3e225448fd417aba9b463519cb4 Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Mon, 7 Jul 2025 01:16:36 +0200 Subject: [PATCH 10/46] Fix linuxbrew workflow --- .github/workflows/linuxbrew.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/linuxbrew.yml b/.github/workflows/linuxbrew.yml index d8996fd6..c3ede39b 100644 --- a/.github/workflows/linuxbrew.yml +++ b/.github/workflows/linuxbrew.yml @@ -6,6 +6,9 @@ jobs: strategy: matrix: python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + env: + # For some unknown reason, linuxbrew tries to use "gcc-11" by default, which doesn't exist. + CC: gcc steps: - uses: actions/checkout@v3 - name: Install brew From 749da1146a87bb1d574af2df0ab012ac2bd50476 Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Mon, 7 Jul 2025 01:16:52 +0200 Subject: [PATCH 11/46] Fix macosx workflow --- .github/workflows/macosx.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/macosx.yml b/.github/workflows/macosx.yml index 1b82d22d..f639637e 100644 --- a/.github/workflows/macosx.yml +++ b/.github/workflows/macosx.yml @@ -26,6 +26,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | export PKG_CONFIG_PATH="$(brew --prefix)/opt/libxml2/lib/pkgconfig" + export PYXMLSEC_LIBXML2_VERSION="$(pkg-config --modversion libxml-2.0)" python -m build rm -rf build/ - name: Set environment variables From 4f213157de1fa123b77754f9723709a1ff460c6f Mon Sep 17 00:00:00 2001 From: Tomi Belan Date: Mon, 7 Jul 2025 01:17:13 +0200 Subject: [PATCH 12/46] Fix manylinux workflow --- .github/workflows/manylinux.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml index a44776b3..db8ea913 100644 --- a/.github/workflows/manylinux.yml +++ b/.github/workflows/manylinux.yml @@ -5,11 +5,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-abi: [cp36-cp36m, cp37-cp37m, cp38-cp38, cp39-cp39, cp310-cp310, cp311-cp311] + python-abi: [cp38-cp38, cp39-cp39, cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313] image: - manylinux2014_x86_64 - manylinux_2_28_x86_64 - - musllinux_1_1_x86_64 + - musllinux_1_2_x86_64 container: quay.io/pypa/${{ matrix.image }} steps: - uses: actions/checkout@v1 From 8f047912b4a00f5d2f42dc55691d241fbfdd2d53 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 00:16:28 +0000 Subject: [PATCH 13/46] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index c70381d9..8d3d4b94 100644 --- a/setup.py +++ b/setup.py @@ -463,9 +463,7 @@ def prepare_static_build(self, build_platform): env=env, ) subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libiconv_dir), env=env) - subprocess.check_call( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libiconv_dir), env=env - ) + subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libiconv_dir), env=env) self.info('Building LibXML2') libxml2_dir = next(self.build_libs_dir.glob('libxml2-*')) @@ -485,9 +483,7 @@ def prepare_static_build(self, build_platform): env=env, ) subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libxml2_dir), env=env) - subprocess.check_call( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libxml2_dir), env=env - ) + subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libxml2_dir), env=env) self.info('Building libxslt') libxslt_dir = next(self.build_libs_dir.glob('libxslt-*')) @@ -506,9 +502,7 @@ def prepare_static_build(self, build_platform): env=env, ) subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libxslt_dir), env=env) - subprocess.check_call( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libxslt_dir), env=env - ) + subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libxslt_dir), env=env) self.info('Building xmlsec1') ldflags.append('-lpthread') @@ -541,9 +535,7 @@ def prepare_static_build(self, build_platform): cwd=str(xmlsec1_dir), env=env, ) - subprocess.check_call( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(xmlsec1_dir), env=env - ) + subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(xmlsec1_dir), env=env) ext = self.ext_map['xmlsec'] ext.define_macros = [ From 3480dd30526faaf7764b5a674fa739dffa689a20 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 10:07:46 +0200 Subject: [PATCH 14/46] [pre-commit.ci] pre-commit autoupdate (#312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.3.0 → 25.1.0](https://github.com/psf/black/compare/24.3.0...25.1.0) - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) - [github.com/PyCQA/flake8: 7.0.0 → 7.3.0](https://github.com/PyCQA/flake8/compare/7.0.0...7.3.0) - [github.com/PyCQA/isort: 5.13.2 → 6.0.1](https://github.com/PyCQA/isort/compare/5.13.2...6.0.1) - [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.16.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.16.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 820778ef..a02d6fac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,14 +2,14 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 25.1.0 hooks: - id: black types: [] files: ^.*.pyi?$ exclude: ^doc/ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: no-commit-to-branch - id: trailing-whitespace @@ -25,17 +25,17 @@ repos: - id: pretty-format-json args: [--autofix] - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 + rev: 7.3.0 hooks: - id: flake8 exclude: ^setup.py$ additional_dependencies: [flake8-docstrings, flake8-bugbear, flake8-logging-format, flake8-builtins, flake8-eradicate, flake8-fixme, pep8-naming, flake8-pep3101, flake8-annotations-complexity,flake8-pyi] - repo: https://github.com/PyCQA/isort - rev: 5.13.2 + rev: 6.0.1 hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 + rev: v1.16.1 hooks: - id: mypy exclude: (setup.py|tests/.*.py|doc/.*) From 8f3d9247259116ed856bcc0e840c1ee644ccefa7 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Thu, 10 Jul 2025 12:34:08 +0200 Subject: [PATCH 15/46] Fix windows wheels and add ARM support (#354) Co-authored-by: Tomi Belan --- .github/workflows/wheels.yml | 2 +- setup.py | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index f9294fd3..66e6db8f 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -87,7 +87,7 @@ jobs: && cibuildwheel --print-build-identifiers --platform macos \ | jq -nRc '{"only": inputs, "os": "macos-latest"}' \ && cibuildwheel --print-build-identifiers --platform windows \ - | jq -nRc '{"only": inputs, "os": "windows-2019"}' + | jq -nRc '{"only": inputs, "os": "windows-2022"}' } | jq -sc ) echo "include=$MATRIX" diff --git a/setup.py b/setup.py index 8d3d4b94..7c4a2e63 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ import json import multiprocessing import os +import platform import re import subprocess import sys @@ -211,19 +212,21 @@ def run(self): super(build_ext, self).run() def prepare_static_build_win(self): - release_url = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2024.04.17/' - if sys.maxsize > 2147483647: # 2.0 GiB + release_url = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2025.07.10/' + if platform.machine() == 'ARM64': + suffix = 'win-arm64' + elif sys.maxsize > 2**32: # 2.0 GiB suffix = 'win64' else: suffix = 'win32' libs = [ - 'libxml2-2.11.7.{}.zip'.format(suffix), - 'libxslt-1.1.37.{}.zip'.format(suffix), - 'zlib-1.2.12.{}.zip'.format(suffix), - 'iconv-1.16-1.{}.zip'.format(suffix), - 'openssl-3.0.8.{}.zip'.format(suffix), - 'xmlsec-1.3.4.{}.zip'.format(suffix), + 'libxml2-2.11.9-3.{}.zip'.format(suffix), + 'libxslt-1.1.39.{}.zip'.format(suffix), + 'zlib-1.3.1.{}.zip'.format(suffix), + 'iconv-1.18-1.{}.zip'.format(suffix), + 'openssl-3.0.16.pl1.{}.zip'.format(suffix), + 'xmlsec-1.3.7.{}.zip'.format(suffix), ] for libfile in libs: From f7b8507b02fc17b7d96a1b4689ef11012cda8f00 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Thu, 10 Jul 2025 21:13:55 +0200 Subject: [PATCH 16/46] Add Windows ARM wheels support (#355) --- .github/workflows/wheels.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 66e6db8f..ae10888c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -87,7 +87,9 @@ jobs: && cibuildwheel --print-build-identifiers --platform macos \ | jq -nRc '{"only": inputs, "os": "macos-latest"}' \ && cibuildwheel --print-build-identifiers --platform windows \ - | jq -nRc '{"only": inputs, "os": "windows-2022"}' + | jq -nRc '{"only": inputs, "os": "windows-2022"}' \ + && cibuildwheel --print-build-identifiers --platform windows --archs ARM64 \ + | jq -nRc '{"only": inputs, "os": "windows-11-arm"}' } | jq -sc ) echo "include=$MATRIX" From f09c3c86015d2b97f56d96900cfc02e70aaf5518 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Mon, 14 Jul 2025 07:55:53 +0200 Subject: [PATCH 17/46] Convert README format from reStructuredText to markdown (#357) Markdown is much easier to manage and more GitHub compatible. Also updated the badges links and removed deprecated ones. --- .appveyor.yml | 64 -------------- README.md | 198 +++++++++++++++++++++++++++++++++++++++++++ README.rst | 229 -------------------------------------------------- setup.cfg | 2 +- setup.py | 2 +- 5 files changed, 200 insertions(+), 295 deletions(-) delete mode 100644 .appveyor.yml create mode 100644 README.md delete mode 100644 README.rst diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index ce025819..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,64 +0,0 @@ -environment: - matrix: - - python: 35 - - python: 35-x64 - - python: 36 - - python: 36-x64 - - python: 37 - - python: 37-x64 - - python: 38 - - python: 38-x64 - - python: 39 - python_version: 3.9.13 - - python: 39-x64 - python_version: 3.9.13 - - python: 310 - python_version: 3.10.6 - - python: 310-x64 - python_version: 3.10.6 - - python: 311 - python_version: 3.11.2 - - python: 311-x64 - python_version: 3.11.2 - -install: - - ps: | - # from https://github.com/appveyor/build-images/blob/27bde614bc60d7ef7a8bc46182f4d7582fa11b56/scripts/Windows/install_python.ps1#L88-L108 - function InstallPythonEXE($targetPath, $version) { - $urlPlatform = "" - if ($targetPath -match '-x64$') { - $urlPlatform = "-amd64" - } - Write-Host "Installing Python $version$urlPlatform to $($targetPath)..." -ForegroundColor Cyan - $downloadUrl = "https://www.python.org/ftp/python/$version/python-$version$urlPlatform.exe" - Write-Host "Downloading $($downloadUrl)..." - $exePath = "$env:TEMP\python-$version.exe" - (New-Object Net.WebClient).DownloadFile($downloadUrl, $exePath) - Write-Host "Installing..." - cmd /c start /wait $exePath /quiet TargetDir="$targetPath" Shortcuts=0 Include_launcher=1 InstallLauncherAllUsers=1 Include_debug=1 - Remove-Item $exePath - Write-Host "Installed Python $version" -ForegroundColor Green - } - if ( -not ( Test-Path -Path C:\\Python$env:PYTHON -PathType Container ) ) { - InstallPythonEXE C:\\Python$env:PYTHON $env:PYTHON_VERSION - } - - SET PATH=C:\\Python%PYTHON%;c:\\Python%PYTHON%\\scripts;%PATH% - - python -m pip install -U pip wheel setuptools - -build: off -build_script: - - python setup.py bdist_wheel - -test: off -test_script: - - pip install -r requirements-test.txt - - pip install xmlsec --only-binary=xmlsec --no-index --find-links=dist - - pytest -v --color=yes --junitxml=unittests.xml - - ps: Get-ChildItem dist\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } - -on_finish: - - ps: | - # archive test results at AppVeyor - $wc = New-Object 'System.Net.WebClient' - $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\unittests.xml)) - $LastExitCode = 0 diff --git a/README.md b/README.md new file mode 100644 index 00000000..6abd6a5a --- /dev/null +++ b/README.md @@ -0,0 +1,198 @@ +# python-xmlsec + +[![image](https://img.shields.io/pypi/v/xmlsec.svg?logo=python&logoColor=white)](https://pypi.python.org/pypi/xmlsec) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/xmlsec/python-xmlsec/master.svg)](https://results.pre-commit.ci/latest/github/xmlsec/python-xmlsec/master) +[![image](https://github.com/xmlsec/python-xmlsec/actions/workflows/manylinux.yml/badge.svg)](https://github.com/xmlsec/python-xmlsec/actions/workflows/manylinux.yml) +[![image](https://github.com/xmlsec/python-xmlsec/actions/workflows/macosx.yml/badge.svg)](https://github.com/xmlsec/python-xmlsec/actions/workflows/macosx.yml) +[![image](https://github.com/xmlsec/python-xmlsec/actions/workflows/linuxbrew.yml/badge.svg)](https://github.com/xmlsec/python-xmlsec/actions/workflows/linuxbrew.yml) +[![image](https://codecov.io/gh/xmlsec/python-xmlsec/branch/master/graph/badge.svg)](https://codecov.io/gh/xmlsec/python-xmlsec) +[![Documentation Status](https://img.shields.io/readthedocs/xmlsec/latest?logo=read-the-docs)](https://xmlsec.readthedocs.io/en/latest/?badge=latest) + +Python bindings for the [XML Security +Library](https://www.aleksey.com/xmlsec/). + +## Documentation + +Documentation for `xmlsec` can be found at +[xmlsec.readthedocs.io](https://xmlsec.readthedocs.io/). + +## Usage + +Check the +[examples](https://xmlsec.readthedocs.io/en/latest/examples.html) +section in the documentation to see various examples of signing and +verifying using the library. + +## Requirements + +- `libxml2 >= 2.9.1` +- `libxmlsec1 >= 1.2.33` + +## Install + +`xmlsec` is available on PyPI: + +``` bash +pip install xmlsec +``` + +Depending on your OS, you may need to install the required native +libraries first: + +### Linux (Debian) + +``` bash +apt-get install pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl +``` + +Note: There is no required version of LibXML2 for Ubuntu Precise, so you +need to download and install it manually. + +``` bash +wget http://xmlsoft.org/sources/libxml2-2.9.1.tar.gz +tar -xvf libxml2-2.9.1.tar.gz +cd libxml2-2.9.1 +./configure && make && make install +``` + +### Linux (CentOS) + +``` bash +yum install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel +``` + +### Linux (Fedora) + +``` bash +dnf install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel +``` + +### Mac + +``` bash +brew install libxml2 libxmlsec1 pkg-config +``` + +or + +``` bash +port install libxml2 xmlsec pkgconfig +``` + +### Alpine + +``` bash +apk add build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec +``` + +## Troubleshooting + +### Mac + +If you get any fatal errors about missing `.h` files, update your +`C_INCLUDE_PATH` environment variable to include the appropriate files +from the `libxml2` and `libxmlsec1` libraries. + +### Windows + +Starting with 1.3.7, prebuilt wheels are available for Windows, so +running `pip install xmlsec` should suffice. If you want to build from +source: + +1. Configure build environment, see + [wiki.python.org](https://wiki.python.org/moin/WindowsCompilers) for + more details. + +2. Install from source dist: + + ``` bash + pip install xmlsec --no-binary=xmlsec + ``` + +## Building from source + +1. Clone the `xmlsec` source code repository to your local computer. + + ``` bash + git clone https://github.com/xmlsec/python-xmlsec.git + ``` + +2. Change into the `python-xmlsec` root directory. + + ``` bash + cd /path/to/xmlsec + ``` + +3. Install the project and all its dependencies using `pip`. + + ``` bash + pip install . + ``` + +## Contributing + +### Setting up your environment + +1. Follow steps 1 and 2 of the [manual installation + instructions](#building-from-source). + +2. Initialize a virtual environment to develop in. This is done so as + to ensure every contributor is working with close-to-identical + versions of packages. + + ``` bash + mkvirtualenv xmlsec + ``` + + The `mkvirtualenv` command is available from `virtualenvwrapper` + package which can be installed by following + [link](http://virtualenvwrapper.readthedocs.org/en/latest/install.html#basic-installation). + +3. Activate the created virtual environment: + + ``` bash + workon xmlsec + ``` + +4. Install `xmlsec` in development mode with testing enabled. This will + download all dependencies required for running the unit tests. + + ``` bash + pip install -r requirements-test.txt + pip install -e "." + ``` + +### Running the test suite + +1. [Set up your environment](#setting-up-your-environment). + +2. Run the unit tests. + + ``` bash + pytest tests + ``` + +3. Tests configuration + + Env variable `PYXMLSEC_TEST_ITERATIONS` specifies number of test + iterations to detect memory leaks. + +### Reporting an issue + +Please attach the output of following information: + +- version of `xmlsec` +- version of `libxmlsec1` +- version of `libxml2` +- output from the command + + ``` bash + pkg-config --cflags xmlsec1 + ``` + +## License + +Unless otherwise noted, all files contained within this project are +licensed under the MIT open source license. See the included `LICENSE` +file or visit [opensource.org](http://opensource.org/licenses/MIT) for +more information. diff --git a/README.rst b/README.rst deleted file mode 100644 index bd3a1be9..00000000 --- a/README.rst +++ /dev/null @@ -1,229 +0,0 @@ -python-xmlsec -============= - -.. image:: https://img.shields.io/pypi/v/xmlsec.svg?logo=python&logoColor=white - :target: https://pypi.python.org/pypi/xmlsec -.. image:: https://results.pre-commit.ci/badge/github/xmlsec/python-xmlsec/master.svg - :target: https://results.pre-commit.ci/latest/github/xmlsec/python-xmlsec/master - :alt: pre-commit.ci status -.. image:: https://img.shields.io/appveyor/ci/hoefling/xmlsec/master.svg?logo=appveyor&logoColor=white&label=AppVeyor - :target: https://ci.appveyor.com/project/hoefling/xmlsec -.. image:: https://github.com/mehcode/python-xmlsec/actions/workflows/manylinux.yml/badge.svg - :target: https://github.com/mehcode/python-xmlsec/actions/workflows/manylinux.yml -.. image:: https://github.com/mehcode/python-xmlsec/actions/workflows/macosx.yml/badge.svg - :target: https://github.com/mehcode/python-xmlsec/actions/workflows/macosx.yml -.. image:: https://github.com/mehcode/python-xmlsec/actions/workflows/linuxbrew.yml/badge.svg - :target: https://github.com/mehcode/python-xmlsec/actions/workflows/linuxbrew.yml -.. image:: https://github.com/mehcode/python-xmlsec/actions/workflows/opensuse-tumbleweed.yml/badge.svg - :target: https://github.com/mehcode/python-xmlsec/actions/workflows/opensuse-tumbleweed.yml -.. image:: https://codecov.io/gh/xmlsec/python-xmlsec/branch/master/graph/badge.svg - :target: https://codecov.io/gh/xmlsec/python-xmlsec -.. image:: https://img.shields.io/readthedocs/xmlsec/latest?logo=read-the-docs - :target: https://xmlsec.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status - -Python bindings for the `XML Security Library `_. - -Documentation -************* - -A documentation for ``xmlsec`` can be found at `xmlsec.readthedocs.io `_. - -Usage -***** - -Check the `examples `_ section in the documentation to see various examples of signing and verifying using the library. - -Requirements -************ -- ``libxml2 >= 2.9.1`` -- ``libxmlsec1 >= 1.2.33`` - -Install -******* - -``xmlsec`` is available on PyPI: - -.. code-block:: bash - - pip install xmlsec - -Depending on your OS, you may need to install the required native -libraries first: - -Linux (Debian) -^^^^^^^^^^^^^^ - -.. code-block:: bash - - apt-get install pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl - - -Note: There is no required version of LibXML2 for Ubuntu Precise, -so you need to download and install it manually. - -.. code-block:: bash - - wget http://xmlsoft.org/sources/libxml2-2.9.1.tar.gz - tar -xvf libxml2-2.9.1.tar.gz - cd libxml2-2.9.1 - ./configure && make && make install - - -Linux (CentOS) -^^^^^^^^^^^^^^ - -.. code-block:: bash - - yum install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel - - -Linux (Fedora) -^^^^^^^^^^^^^^ - -.. code-block:: bash - - dnf install libxml2-devel xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel - - -Mac -^^^ - -.. code-block:: bash - - brew install libxml2 libxmlsec1 pkg-config - -or - -.. code-block:: bash - - port install libxml2 xmlsec pkgconfig - - -Alpine -^^^^^^ - -.. code-block:: bash - - apk add build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec - - -Troubleshooting -*************** - -Mac -^^^ - -If you get any fatal errors about missing ``.h`` files, update your -``C_INCLUDE_PATH`` environment variable to include the appropriate -files from the ``libxml2`` and ``libxmlsec1`` libraries. - - -Windows -^^^^^^^ - -Starting with 1.3.7, prebuilt wheels are available for Windows, -so running ``pip install xmlsec`` should suffice. If you want -to build from source: - -#. Configure build environment, see `wiki.python.org `_ for more details. - -#. Install from source dist: - - .. code-block:: bash - - pip install xmlsec --no-binary=xmlsec - - -Building from source -******************** - -#. Clone the ``xmlsec`` source code repository to your local computer. - - .. code-block:: bash - - git clone https://github.com/xmlsec/python-xmlsec.git - -#. Change into the ``python-xmlsec`` root directory. - - .. code-block:: bash - - cd /path/to/xmlsec - - -#. Install the project and all its dependencies using ``pip``. - - .. code-block:: bash - - pip install . - - -Contributing -************ - -Setting up your environment -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -#. Follow steps 1 and 2 of the `manual installation instructions <#building-from-source>`_. - - -#. Initialize a virtual environment to develop in. - This is done so as to ensure every contributor is working with - close-to-identicial versions of packages. - - .. code-block:: bash - - mkvirtualenv xmlsec - - The ``mkvirtualenv`` command is available from ``virtualenvwrapper`` package which can be installed by following `link `_. - -#. Activate the created virtual environment: - - .. code-block:: bash - - workon xmlsec - -#. Install ``xmlsec`` in development mode with testing enabled. - This will download all dependencies required for running the unit tests. - - .. code-block:: bash - - pip install -r requirements-test.txt - pip install -e "." - - -Running the test suite -^^^^^^^^^^^^^^^^^^^^^^ - -#. `Set up your environment <#setting-up-your-environment>`_. - -#. Run the unit tests. - - .. code-block:: bash - - pytest tests - -#. Tests configuration - - Env variable ``PYXMLSEC_TEST_ITERATIONS`` specifies number of - test iterations to detect memory leaks. - -Reporting an issue -^^^^^^^^^^^^^^^^^^ - -Please attach the output of following information: - -* version of ``xmlsec`` -* version of ``libxmlsec1`` -* version of ``libxml2`` -* output from the command - - .. code-block:: bash - - pkg-config --cflags xmlsec1 - -License -******* - -Unless otherwise noted, all files contained within this project are licensed under the MIT opensource license. -See the included ``LICENSE`` file or visit `opensource.org `_ for more information. diff --git a/setup.cfg b/setup.cfg index c090b4e8..7fee05f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -description_file = README.rst +description_file = README.md [bdist_rpm] release = 1 diff --git a/setup.py b/setup.py index 7c4a2e63..447cebc4 100644 --- a/setup.py +++ b/setup.py @@ -584,7 +584,7 @@ def prepare_static_build(self, build_platform): setup_reqs = ['setuptools_scm[toml]>=3.4', 'pkgconfig>=1.5.1', 'lxml>=3.8'] -with io.open('README.rst', encoding='utf-8') as f: +with io.open('README.md', encoding='utf-8') as f: long_desc = f.read() From 63ab5969bd65be570acb4bca070c46ace4c78b40 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Mon, 14 Jul 2025 10:27:30 +0200 Subject: [PATCH 18/46] Drop legacy platform support and improve CI efficiency (#358) Cancelled in-progress GitHub Actions using concurrency groups to reduce CI resource usage. Fully removed support for Python 3.8 and lower. Discontinued building wheels for Linux i686 and Windows 32-bit due to low usage based on PyPI download statistics. --- .github/workflows/linuxbrew.yml | 3 ++ .github/workflows/macosx.yml | 5 ++- .github/workflows/manylinux.yml | 5 ++- .github/workflows/sdist.yml | 3 ++ .github/workflows/wheels.yml | 4 ++ .travis.yml | 79 +++++++++++++++++---------------- pyproject.toml | 21 +++++---- 7 files changed, 68 insertions(+), 52 deletions(-) diff --git a/.github/workflows/linuxbrew.yml b/.github/workflows/linuxbrew.yml index c3ede39b..404321ea 100644 --- a/.github/workflows/linuxbrew.yml +++ b/.github/workflows/linuxbrew.yml @@ -1,5 +1,8 @@ name: linuxbrew on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: linuxbrew: runs-on: ubuntu-latest diff --git a/.github/workflows/macosx.yml b/.github/workflows/macosx.yml index f639637e..7961b5b9 100644 --- a/.github/workflows/macosx.yml +++ b/.github/workflows/macosx.yml @@ -1,5 +1,8 @@ -name: MacOS +name: macOS on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: macosx: runs-on: macos-latest diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml index db8ea913..9d578f05 100644 --- a/.github/workflows/manylinux.yml +++ b/.github/workflows/manylinux.yml @@ -1,11 +1,14 @@ name: manylinux on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: manylinux: runs-on: ubuntu-latest strategy: matrix: - python-abi: [cp38-cp38, cp39-cp39, cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313] + python-abi: [cp39-cp39, cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313] image: - manylinux2014_x86_64 - manylinux_2_28_x86_64 diff --git a/.github/workflows/sdist.yml b/.github/workflows/sdist.yml index 04f22c14..3f3eacf0 100644 --- a/.github/workflows/sdist.yml +++ b/.github/workflows/sdist.yml @@ -1,5 +1,8 @@ name: sdist on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: sdist: # Avoid Ubuntu 24.04 in sdist workflows, because it contains libxmlsec1-dev diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ae10888c..6b74c1b6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -15,6 +15,10 @@ on: pull_request: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: {} jobs: diff --git a/.travis.yml b/.travis.yml index 9e6ca540..9574a1c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,51 +1,52 @@ -dist: trusty -sudo: false +dist: focal language: python +travis: + auto_cancel: + push: true + pull_request: true + notifications: email: false -matrix: - include: - - python: 3.5 - - python: 3.6 - - python: 3.7 - dist: xenial - sudo: required - - python: 3.8 - dist: xenial - sudo: required - - python: 3.9 - dist: xenial - sudo: required - - python: 3.11 - dist: xenial - sudo: required + +python: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + env: global: - - CFLAGS=-coverage - - LDFLAGS=-coverage -lgcov - - PYXMLSEC_TEST_ITERATIONS=50 + - CFLAGS=-coverage + - LDFLAGS=-coverage -lgcov + - PYXMLSEC_TEST_ITERATIONS=50 addons: apt: packages: - - libssl-dev - - libxmlsec1 - - libxmlsec1-dev - - libxmlsec1-openssl - - libxslt1-dev - - pkg-config - - lcov + - libssl-dev + - libxmlsec1 + - libxmlsec1-dev + - libxmlsec1-openssl + - libxslt1-dev + - pkg-config + - lcov + install: -- travis_retry pip install --upgrade pip setuptools wheel -- travis_retry pip install coverage -r requirements-test.txt --upgrade --force-reinstall -- python setup.py bdist_wheel -- pip install xmlsec --only-binary=xmlsec --no-index --find-links=dist/ -script: coverage run -m pytest -v tests --color=yes + - travis_retry pip install --upgrade pip setuptools wheel + - travis_retry pip install coverage -r requirements-test.txt --upgrade --force-reinstall + - python setup.py bdist_wheel + - pip install xmlsec --only-binary=xmlsec --no-index --find-links=dist/ + +script: + - coverage run -m pytest -v tests --color=yes + after_success: -- lcov --capture --no-external --directory . --output-file coverage.info -- lcov --list coverage.info -- bash <(curl -s https://codecov.io/bash) -f coverage.info + - lcov --capture --no-external --directory . --output-file coverage.info + - lcov --list coverage.info + - bash <(curl -s https://codecov.io/bash) -f coverage.info + before_deploy: -- travis_retry pip install Sphinx -r doc/source/requirements.txt -- git apply --verbose --no-index --unsafe-paths --directory=$(python -c "import site; print(site.getsitepackages()[0])") doc/source/sphinx-pr-6916.diff -- sphinx-build -EWanb html doc/source build/sphinx + - travis_retry pip install Sphinx -r doc/source/requirements.txt + - git apply --verbose --no-index --unsafe-paths --directory=$(python -c "import site; print(site.getsitepackages()[0])") doc/source/sphinx-pr-6916.diff + - sphinx-build -EWanb html doc/source build/sphinx diff --git a/pyproject.toml b/pyproject.toml index 540f91c1..a0380dcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,18 +47,17 @@ known_third_party = ['lxml', 'pytest', '_pytest', 'hypothesis'] requires = ['setuptools>=42', 'wheel', 'setuptools_scm[toml]>=3.4', "pkgconfig>=1.5.1", "lxml>=3.8, !=4.7.0"] [tool.cibuildwheel] +build = [ + "cp39-*", + "cp310-*", + "cp311-*", + "cp312-*", + "cp313-*" +] build-verbosity = 1 build-frontend = "build" skip = [ - "pp*", - "*-musllinux_i686", - # LXML doesn't publish wheels for these platforms, which makes it - # difficult for us to build wheels, so we exclude them. - "cp36-*", - "cp37-*", - "cp38-manylinux_aarch64", - "cp38-musllinux_aarch64", - "cp38-macosx*", + "pp*", # Skips PyPy builds (pp38-*, pp39-*, etc.) ] test-command = "pytest -v --color=yes {package}/tests" before-test = "pip install -r requirements-test.txt" @@ -68,7 +67,7 @@ test-skip = "*-macosx_arm64" PYXMLSEC_STATIC_DEPS = "true" [tool.cibuildwheel.linux] -archs = ["x86_64", "aarch64", "i686"] +archs = ["x86_64", "aarch64"] environment-pass = [ "PYXMLSEC_LIBXML2_VERSION", "PYXMLSEC_LIBXSLT_VERSION", @@ -81,7 +80,7 @@ archs = ["x86_64", "arm64"] before-all = "brew install perl" [tool.cibuildwheel.windows] -archs = ["AMD64", "x86"] +archs = ["AMD64"] [[tool.cibuildwheel.overrides]] select = "*-manylinux*" From f43021900f9de3b9fb60f933720d38c06b885d84 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Mon, 14 Jul 2025 16:29:06 +0200 Subject: [PATCH 19/46] Migrate linting to Ruff and update CI settings (#359) Replace flake8, black, and isort with Ruff as the unified linter and formatter. - Applied auto-fixes from Ruff - Removed legacy configuration for flake8, black, and isort - Updated CI settings to prevent job cancelation on master --- .github/workflows/linuxbrew.yml | 2 +- .github/workflows/macosx.yml | 2 +- .github/workflows/manylinux.yml | 2 +- .github/workflows/sdist.yml | 2 +- .github/workflows/wheels.yml | 2 +- .pre-commit-config.yaml | 84 +++++++-------- doc/source/conf.py | 25 +++-- doc/source/examples/decrypt.py | 4 +- doc/source/examples/encrypt.py | 24 ++--- doc/source/examples/verify.py | 2 +- pyproject.toml | 88 +++++++++++----- requirements-test.txt | 6 +- requirements.txt | 2 +- setup.cfg | 19 ++-- setup.py | 178 ++++++++++++++------------------ src/xmlsec/constants.pyi | 7 +- tests/base.py | 32 +++--- tests/conftest.py | 3 +- tests/softhsm_setup.py | 26 ++--- tests/test_constants.py | 8 +- tests/test_doc_examples.py | 14 +-- tests/test_ds.py | 110 ++++++++++---------- tests/test_enc.py | 50 ++++----- tests/test_keys.py | 120 +++++++++++---------- tests/test_main.py | 12 +-- tests/test_pkcs11.py | 8 +- tests/test_templates.py | 88 ++++++++-------- tests/test_tree.py | 10 +- tests/test_type_stubs.py | 8 +- tests/test_xmlsec.py | 3 +- 30 files changed, 460 insertions(+), 481 deletions(-) diff --git a/.github/workflows/linuxbrew.yml b/.github/workflows/linuxbrew.yml index 404321ea..8a231047 100644 --- a/.github/workflows/linuxbrew.yml +++ b/.github/workflows/linuxbrew.yml @@ -2,7 +2,7 @@ name: linuxbrew on: [push, pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref_name != 'master' }} jobs: linuxbrew: runs-on: ubuntu-latest diff --git a/.github/workflows/macosx.yml b/.github/workflows/macosx.yml index 7961b5b9..44d214bc 100644 --- a/.github/workflows/macosx.yml +++ b/.github/workflows/macosx.yml @@ -2,7 +2,7 @@ name: macOS on: [push, pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref_name != 'master' }} jobs: macosx: runs-on: macos-latest diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml index 9d578f05..357e93eb 100644 --- a/.github/workflows/manylinux.yml +++ b/.github/workflows/manylinux.yml @@ -2,7 +2,7 @@ name: manylinux on: [push, pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref_name != 'master' }} jobs: manylinux: runs-on: ubuntu-latest diff --git a/.github/workflows/sdist.yml b/.github/workflows/sdist.yml index 3f3eacf0..3bdc9764 100644 --- a/.github/workflows/sdist.yml +++ b/.github/workflows/sdist.yml @@ -2,7 +2,7 @@ name: sdist on: [push, pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref_name != 'master' }} jobs: sdist: # Avoid Ubuntu 24.04 in sdist workflows, because it contains libxmlsec1-dev diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6b74c1b6..62d48b4b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -17,7 +17,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref_name != 'master' }} permissions: {} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a02d6fac..48abcc6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,48 +1,42 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/psf/black - rev: 25.1.0 - hooks: - - id: black - types: [] - files: ^.*.pyi?$ - exclude: ^doc/ -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: no-commit-to-branch - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files - - id: check-ast - - id: check-merge-conflict - - id: check-json - - id: detect-private-key - exclude: ^.*/rsakey.pem$ - - id: mixed-line-ending - - id: pretty-format-json - args: [--autofix] -- repo: https://github.com/PyCQA/flake8 - rev: 7.3.0 - hooks: - - id: flake8 - exclude: ^setup.py$ - additional_dependencies: [flake8-docstrings, flake8-bugbear, flake8-logging-format, flake8-builtins, flake8-eradicate, flake8-fixme, pep8-naming, flake8-pep3101, flake8-annotations-complexity,flake8-pyi] -- repo: https://github.com/PyCQA/isort - rev: 6.0.1 - hooks: - - id: isort -- repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.1 - hooks: - - id: mypy - exclude: (setup.py|tests/.*.py|doc/.*) - types: [] - files: ^.*.pyi?$ - additional_dependencies: [lxml-stubs,types-docutils] -- repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.10.0 - hooks: - - id: rst-backticks +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.3 + hooks: + - id: ruff + args: ["--fix"] + types: [python] + - id: ruff-format + types: [python] + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: no-commit-to-branch + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-ast + - id: check-merge-conflict + - id: check-json + - id: detect-private-key + exclude: ^.*/rsakey.pem$ + - id: mixed-line-ending + - id: pretty-format-json + args: [--autofix] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.16.1 + hooks: + - id: mypy + exclude: (setup.py|tests/.*.py|doc/.*) + types: [] + files: ^.*.pyi?$ + additional_dependencies: [lxml-stubs, types-docutils] + +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: rst-backticks diff --git a/doc/source/conf.py b/doc/source/conf.py index 35329050..900b79da 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -19,12 +19,12 @@ source_suffix = '.rst' master_doc = 'index' -project = u'python-xmlsec' -copyright = u'2020, Oleg Hoefling ' # noqa: A001 -author = u'Bulat Gaifullin ' +project = 'python-xmlsec' +copyright = '2020, Oleg Hoefling ' +author = 'Bulat Gaifullin ' release = importlib.metadata.version('xmlsec') parsed: Version = parse(release) -version = '{}.{}'.format(parsed.major, parsed.minor) +version = f'{parsed.major}.{parsed.minor}' exclude_patterns: list[str] = [] pygments_style = 'sphinx' @@ -39,19 +39,19 @@ ( master_doc, 'python-xmlsec.tex', - u'python-xmlsec Documentation', - u'Bulat Gaifullin \\textless{}gaifullinbf@gmail.com\\textgreater{}', + 'python-xmlsec Documentation', + 'Bulat Gaifullin \\textless{}gaifullinbf@gmail.com\\textgreater{}', 'manual', ) ] -man_pages = [(master_doc, 'python-xmlsec', u'python-xmlsec Documentation', [author], 1)] +man_pages = [(master_doc, 'python-xmlsec', 'python-xmlsec Documentation', [author], 1)] texinfo_documents = [ ( master_doc, 'python-xmlsec', - u'python-xmlsec Documentation', + 'python-xmlsec Documentation', author, 'python-xmlsec', 'One line description of project.', @@ -63,10 +63,10 @@ autodoc_docstring_signature = True -rst_prolog = ''' +rst_prolog = """ .. role:: xml(code) :language: xml -''' +""" # LXML crossref'ing stuff: # LXML doesn't have an intersphinx docs, @@ -75,8 +75,7 @@ def lxml_element_doc_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: Text) -> reference: - """ - Handle a missing reference only if it is a ``lxml.etree._Element`` ref. + """Handle a missing reference only if it is a ``lxml.etree._Element`` ref. We handle only :class:`lxml.etree._Element` and :class:`~lxml.etree._Element` nodes. """ @@ -85,7 +84,7 @@ def lxml_element_doc_reference(app: Sphinx, env: BuildEnvironment, node: pending and node.get('reftarget', None) == 'lxml.etree._Element' and contnode.astext() in ('lxml.etree._Element', '_Element') ): - reftitle = '(in lxml v{})'.format(lxml.__version__) # type: ignore[attr-defined] + reftitle = f'(in lxml v{lxml.__version__})' # type: ignore[attr-defined] newnode = reference('', '', internal=False, refuri=lxml_element_cls_doc_uri, reftitle=reftitle) newnode.append(contnode) return newnode diff --git a/doc/source/examples/decrypt.py b/doc/source/examples/decrypt.py index e107756f..cb474d22 100644 --- a/doc/source/examples/decrypt.py +++ b/doc/source/examples/decrypt.py @@ -6,7 +6,7 @@ key = xmlsec.Key.from_file('rsakey.pem', xmlsec.constants.KeyDataFormatPem) manager.add_key(key) enc_ctx = xmlsec.EncryptionContext(manager) -root = etree.parse("enc1-res.xml").getroot() -enc_data = xmlsec.tree.find_child(root, "EncryptedData", xmlsec.constants.EncNs) +root = etree.parse('enc1-res.xml').getroot() +enc_data = xmlsec.tree.find_child(root, 'EncryptedData', xmlsec.constants.EncNs) decrypted = enc_ctx.decrypt(enc_data) print(etree.tostring(decrypted)) diff --git a/doc/source/examples/encrypt.py b/doc/source/examples/encrypt.py index 98f63b6f..2a92264e 100644 --- a/doc/source/examples/encrypt.py +++ b/doc/source/examples/encrypt.py @@ -9,11 +9,11 @@ template, xmlsec.constants.TransformAes128Cbc, type=xmlsec.constants.TypeEncElement, - ns="xenc", + ns='xenc', ) xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) -key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns="dsig") +key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns='dsig') enc_key = xmlsec.template.add_encrypted_key(key_info, xmlsec.constants.TransformRsaOaep) xmlsec.template.encrypted_data_ensure_cipher_value(enc_key) data = template.find('./Data') @@ -24,20 +24,10 @@ manager.add_key(key) enc_ctx = xmlsec.EncryptionContext(manager) -enc_ctx.key = xmlsec.Key.generate( - xmlsec.constants.KeyDataAes, 128, xmlsec.constants.KeyDataTypeSession -) +enc_ctx.key = xmlsec.Key.generate(xmlsec.constants.KeyDataAes, 128, xmlsec.constants.KeyDataTypeSession) enc_data = enc_ctx.encrypt_xml(enc_data, data) -enc_method = xmlsec.tree.find_child( - enc_data, xmlsec.constants.NodeEncryptionMethod, xmlsec.constants.EncNs -) -key_info = xmlsec.tree.find_child( - enc_data, xmlsec.constants.NodeKeyInfo, xmlsec.constants.DSigNs -) -enc_method = xmlsec.tree.find_node( - key_info, xmlsec.constants.NodeEncryptionMethod, xmlsec.constants.EncNs -) -cipher_value = xmlsec.tree.find_node( - key_info, xmlsec.constants.NodeCipherValue, xmlsec.constants.EncNs -) +enc_method = xmlsec.tree.find_child(enc_data, xmlsec.constants.NodeEncryptionMethod, xmlsec.constants.EncNs) +key_info = xmlsec.tree.find_child(enc_data, xmlsec.constants.NodeKeyInfo, xmlsec.constants.DSigNs) +enc_method = xmlsec.tree.find_node(key_info, xmlsec.constants.NodeEncryptionMethod, xmlsec.constants.EncNs) +cipher_value = xmlsec.tree.find_node(key_info, xmlsec.constants.NodeCipherValue, xmlsec.constants.EncNs) print(etree.tostring(cipher_value)) diff --git a/doc/source/examples/verify.py b/doc/source/examples/verify.py index 808a53c2..c3240c99 100644 --- a/doc/source/examples/verify.py +++ b/doc/source/examples/verify.py @@ -5,7 +5,7 @@ with open('sign1-res.xml') as fp: template = etree.parse(fp).getroot() -xmlsec.tree.add_ids(template, ["ID"]) +xmlsec.tree.add_ids(template, ['ID']) signature_node = xmlsec.tree.find_node(template, xmlsec.constants.NodeSignature) # Create a digital signature context (no key manager is needed). ctx = xmlsec.SignatureContext() diff --git a/pyproject.toml b/pyproject.toml index a0380dcf..bb0c459e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +[build-system] +requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4", "pkgconfig>=1.5.1", "lxml>=3.8, !=4.7.0"] + [tool.mypy] files = ['src'] ignore_missing_imports = false @@ -19,32 +22,71 @@ warn_no_return = true no_implicit_reexport = true show_error_codes = true -[tool.black] -line_length = 130 -skip-string-normalization = true -target_version = ['py39'] -include = '\.pyi?$' -exclude = ''' +[tool.ruff] +# Maximum line length, same as your original Black + Flake8 config +line-length = 130 + +# Target Python version (used for autofixes and style rules) +target-version = "py39" -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.mypy_cache - | \.tox - | build - | dist - )/ -) -''' +# Directories and files to exclude from linting and formatting +exclude = [ + ".venv*", # virtual environments + ".git", # git directory + "build", # build output + "dist", # distribution packages + "libs", # vendor libraries + ".eggs", # setuptools egg folders + ".direnv*", # direnv environments + "*_pb2.pyi" # protobuf-generated type stubs +] -[tool.isort] -profile = 'black' -known_first_party = ['xmlsec'] -known_third_party = ['lxml', 'pytest', '_pytest', 'hypothesis'] +[tool.ruff.lint] +# Enable rule categories: +# E = pycodestyle (style issues, like indentation, whitespace, etc.) +# F = pyflakes (unused imports, undefined names) +# I = isort (import sorting) +# B = flake8-bugbear (common bugs & anti-patterns) +# UP = pyupgrade (auto-upgrade syntax for newer Python) +# SIM = flake8-simplify (simplifiable code patterns) +# RUF = Ruff-native rules (extra, performance-optimized checks) +select = ["E", "F", "I", "B", "UP", "SIM", "RUF"] +# TODO: Add more rule categories as needed, e.g.: +# D = pydocstyle (docstring format/style issues) -[build-system] -requires = ['setuptools>=42', 'wheel', 'setuptools_scm[toml]>=3.4', "pkgconfig>=1.5.1", "lxml>=3.8, !=4.7.0"] +[tool.ruff.lint.per-file-ignores] +"*.pyi" = [ + # Ignore formatting and import errors in stub files + "E301", # expected 1 blank line, found 0 + "E302", # expected 2 blank lines, found 1 + "E305", # expected 2 blank lines after class or function + "E501", # line too long + "E701", # multiple statements on one line + "F401", # unused import + "F811", # redefinition of unused name + "F822" # undefined name in `__all__` +] +"doc/source/conf.py" = [ + "D1" # missing docstring in public module/class/function +] +"doc/source/examples/*.py" = [ + "D1", # allow missing docstrings in examples + "E501" # allow long lines in code examples +] +"tests/*.py" = [ + "D1" # allow missing docstrings in test files +] + +[tool.ruff.format] +# Always use single quotes (e.g., 'text' instead of "text") +quote-style = "single" + +# Format code with or without trailing commas +# true = prefer trailing commas where valid +skip-magic-trailing-comma = false + +# Enforce Unix-style line endings (LF) +line-ending = "lf" [tool.cibuildwheel] build = [ diff --git a/requirements-test.txt b/requirements-test.txt index eb543402..52a31f00 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,5 @@ -r requirements.txt -pytest>=4.6.9 -lxml-stubs + +pytest==8.4.1 +lxml-stubs==0.5.1 +ruff[format]==0.12.3 diff --git a/requirements.txt b/requirements.txt index 014bfe10..7aa6718b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -lxml >= 3.8.0, !=4.7.0 +lxml==6.0.0 diff --git a/setup.cfg b/setup.cfg index 7fee05f9..8762c654 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,14 +12,11 @@ source-dir = doc/source build-dir = doc/build all_files = 1 -[upload_docs] -upload_dir = doc/build/html - -[flake8] -per-file-ignores = - *.pyi: E301, E302, E305, E501, E701, F401, F822 - doc/source/conf.py: D1 - doc/source/examples/*.py: D1, E501 - tests/*.py: D1 -exclude = .venv*,.git,*_pb2.pyi,build,dist,libs,.eggs,.direnv* -max-line-length = 130 +# [flake8] +# per-file-ignores = +# *.pyi: E301, E302, E305, E501, E701, F401, F822 +# doc/source/conf.py: D1 +# doc/source/examples/*.py: D1, E501 +# tests/*.py: D1 +# exclude = .venv*,.git,*_pb2.pyi,build,dist,libs,.eggs,.direnv* +# max-line-length = 130 diff --git a/setup.py b/setup.py index 447cebc4..94b49aa8 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ import contextlib import html.parser -import io import json import multiprocessing import os @@ -36,7 +35,7 @@ def handle_starttag(self, tag, attrs): def make_request(url, github_token=None, json_response=False): headers = {'User-Agent': 'https://github.com/xmlsec/python-xmlsec'} if github_token: - headers['authorization'] = "Bearer " + github_token + headers['authorization'] = 'Bearer ' + github_token request = Request(url, headers=headers) with contextlib.closing(urlopen(request)) as r: charset = r.headers.get_content_charset() or 'utf-8' @@ -60,24 +59,24 @@ def comp(text): return Version('0.0') latest = max(hrefs, key=comp) - return '{}/{}'.format(url, latest) + return f'{url}/{latest}' def latest_release_from_gnome_org_cache(url, lib_name): - cache_url = '{}/cache.json'.format(url) + cache_url = f'{url}/cache.json' cache = make_request(cache_url, json_response=True) latest_version = cache[2][lib_name][-1] latest_source = cache[1][lib_name][latest_version]['tar.xz'] - return '{}/{}'.format(url, latest_source) + return f'{url}/{latest_source}' def latest_release_json_from_github_api(repo): - api_url = 'https://api.github.com/repos/{}/releases/latest'.format(repo) + api_url = f'https://api.github.com/repos/{repo}/releases/latest' # if we are running in CI, pass along the GH_TOKEN, so we don't get rate limited - token = os.environ.get("GH_TOKEN") + token = os.environ.get('GH_TOKEN') if token: - log.info("Using GitHub token to avoid rate limiting") + log.info('Using GitHub token to avoid rate limiting') return make_request(api_url, token, json_response=True) @@ -115,7 +114,7 @@ def __init__(self, host, arch, compiler): @property def triplet(self): - return "{}-{}-{}".format(self.host, self.arch, self.compiler) + return f'{self.host}-{self.arch}-{self.compiler}' class build_ext(build_ext_orig): @@ -129,7 +128,7 @@ def run(self): self.size_opt = os.environ.get('PYXMLSEC_OPTIMIZE_SIZE', True) if self.static or sys.platform == 'win32': - self.info('starting static build on {}'.format(sys.platform)) + self.info(f'starting static build on {sys.platform}') buildroot = Path('build', 'tmp') self.prefix_dir = buildroot / 'prefix' @@ -145,19 +144,17 @@ def run(self): if sys.platform == 'win32': self.prepare_static_build_win() - elif 'linux' in sys.platform: - self.prepare_static_build(sys.platform) - elif 'darwin' in sys.platform: + elif 'linux' in sys.platform or 'darwin' in sys.platform: self.prepare_static_build(sys.platform) else: import pkgconfig try: config = pkgconfig.parse('xmlsec1') - except EnvironmentError: - raise DistutilsError('Unable to invoke pkg-config.') - except pkgconfig.PackageNotFoundError: - raise DistutilsError('xmlsec1 is not installed or not in path.') + except OSError as e: + raise DistutilsError('Unable to invoke pkg-config.') from e + except pkgconfig.PackageNotFoundError as e: + raise DistutilsError('xmlsec1 is not installed or not in path.') from e if config is None or not config.get('libraries'): raise DistutilsError('Bad or incomplete result returned from pkg-config.') @@ -178,7 +175,7 @@ def run(self): for key, value in ext.define_macros: if key == 'XMLSEC_CRYPTO' and not (value.startswith('"') and value.endswith('"')): ext.define_macros.remove((key, value)) - ext.define_macros.append((key, '"{0}"'.format(value))) + ext.define_macros.append((key, f'"{value}"')) break if sys.platform == 'win32': @@ -209,7 +206,7 @@ def run(self): else: ext.extra_compile_args.append('-Os') - super(build_ext, self).run() + super().run() def prepare_static_build_win(self): release_url = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2025.07.10/' @@ -221,21 +218,21 @@ def prepare_static_build_win(self): suffix = 'win32' libs = [ - 'libxml2-2.11.9-3.{}.zip'.format(suffix), - 'libxslt-1.1.39.{}.zip'.format(suffix), - 'zlib-1.3.1.{}.zip'.format(suffix), - 'iconv-1.18-1.{}.zip'.format(suffix), - 'openssl-3.0.16.pl1.{}.zip'.format(suffix), - 'xmlsec-1.3.7.{}.zip'.format(suffix), + f'libxml2-2.11.9-3.{suffix}.zip', + f'libxslt-1.1.39.{suffix}.zip', + f'zlib-1.3.1.{suffix}.zip', + f'iconv-1.18-1.{suffix}.zip', + f'openssl-3.0.16.pl1.{suffix}.zip', + f'xmlsec-1.3.7.{suffix}.zip', ] for libfile in libs: url = urljoin(release_url, libfile) destfile = self.libs_dir / libfile if destfile.is_file(): - self.info('Using local copy of "{}"'.format(url)) + self.info(f'Using local copy of "{url}"') else: - self.info('Retrieving "{}" to "{}"'.format(url, destfile)) + self.info(f'Retrieving "{url}" to "{destfile}"') urlcleanup() # work around FTP bug 27973 in Py2.7.12+ urlretrieve(url, str(destfile)) @@ -296,9 +293,9 @@ def prepare_static_build(self, build_platform): openssl_tar = self.libs_dir / 'openssl.tar.gz' if self.openssl_version is None: url = latest_openssl_release() - self.info('{:10}: {}'.format('OpenSSL', 'PYXMLSEC_OPENSSL_VERSION unset, downloading latest from {}'.format(url))) + self.info('{:10}: {}'.format('OpenSSL', f'PYXMLSEC_OPENSSL_VERSION unset, downloading latest from {url}')) else: - url = 'https://api.github.com/repos/openssl/openssl/tarball/openssl-{}'.format(self.openssl_version) + url = f'https://api.github.com/repos/openssl/openssl/tarball/openssl-{self.openssl_version}' self.info('{:10}: {} {}'.format('OpenSSL', 'version', self.openssl_version)) urlretrieve(url, str(openssl_tar)) @@ -309,12 +306,10 @@ def prepare_static_build(self, build_platform): zlib_tar = self.libs_dir / 'zlib.tar.gz' if self.zlib_version is None: url = latest_zlib_release() - self.info('{:10}: {}'.format('zlib', 'PYXMLSEC_ZLIB_VERSION unset, downloading latest from {}'.format(url))) + self.info('{:10}: {}'.format('zlib', f'PYXMLSEC_ZLIB_VERSION unset, downloading latest from {url}')) else: - url = 'https://zlib.net/fossils/zlib-{}.tar.gz'.format(self.zlib_version) - self.info( - '{:10}: {}'.format('zlib', 'PYXMLSEC_ZLIB_VERSION={}, downloading from {}'.format(self.zlib_version, url)) - ) + url = f'https://zlib.net/fossils/zlib-{self.zlib_version}.tar.gz' + self.info('{:10}: {}'.format('zlib', f'PYXMLSEC_ZLIB_VERSION={self.zlib_version}, downloading from {url}')) urlretrieve(url, str(zlib_tar)) # fetch libiconv @@ -324,13 +319,11 @@ def prepare_static_build(self, build_platform): libiconv_tar = self.libs_dir / 'libiconv.tar.gz' if self.libiconv_version is None: url = latest_libiconv_release() - self.info('{:10}: {}'.format('zlib', 'PYXMLSEC_LIBICONV_VERSION unset, downloading latest from {}'.format(url))) + self.info('{:10}: {}'.format('zlib', f'PYXMLSEC_LIBICONV_VERSION unset, downloading latest from {url}')) else: - url = 'https://ftp.gnu.org/pub/gnu/libiconv/libiconv-{}.tar.gz'.format(self.libiconv_version) + url = f'https://ftp.gnu.org/pub/gnu/libiconv/libiconv-{self.libiconv_version}.tar.gz' self.info( - '{:10}: {}'.format( - 'zlib', 'PYXMLSEC_LIBICONV_VERSION={}, downloading from {}'.format(self.libiconv_version, url) - ) + '{:10}: {}'.format('zlib', f'PYXMLSEC_LIBICONV_VERSION={self.libiconv_version}, downloading from {url}') ) urlretrieve(url, str(libiconv_tar)) @@ -340,16 +333,12 @@ def prepare_static_build(self, build_platform): self.info('{:10}: {}'.format('libxml2', 'source tar not found, downloading ...')) if self.libxml2_version is None: url = latest_libxml2_release() - self.info('{:10}: {}'.format('libxml2', 'PYXMLSEC_LIBXML2_VERSION unset, downloading latest from {}'.format(url))) + self.info('{:10}: {}'.format('libxml2', f'PYXMLSEC_LIBXML2_VERSION unset, downloading latest from {url}')) else: version_prefix, _ = self.libxml2_version.rsplit('.', 1) - url = 'https://download.gnome.org/sources/libxml2/{}/libxml2-{}.tar.xz'.format( - version_prefix, self.libxml2_version - ) + url = f'https://download.gnome.org/sources/libxml2/{version_prefix}/libxml2-{self.libxml2_version}.tar.xz' self.info( - '{:10}: {}'.format( - 'libxml2', 'PYXMLSEC_LIBXML2_VERSION={}, downloading from {}'.format(self.libxml2_version, url) - ) + '{:10}: {}'.format('libxml2', f'PYXMLSEC_LIBXML2_VERSION={self.libxml2_version}, downloading from {url}') ) libxml2_tar = self.libs_dir / 'libxml2.tar.xz' urlretrieve(url, str(libxml2_tar)) @@ -360,16 +349,12 @@ def prepare_static_build(self, build_platform): self.info('{:10}: {}'.format('libxslt', 'source tar not found, downloading ...')) if self.libxslt_version is None: url = latest_libxslt_release() - self.info('{:10}: {}'.format('libxslt', 'PYXMLSEC_LIBXSLT_VERSION unset, downloading latest from {}'.format(url))) + self.info('{:10}: {}'.format('libxslt', f'PYXMLSEC_LIBXSLT_VERSION unset, downloading latest from {url}')) else: version_prefix, _ = self.libxslt_version.rsplit('.', 1) - url = 'https://download.gnome.org/sources/libxslt/{}/libxslt-{}.tar.xz'.format( - version_prefix, self.libxslt_version - ) + url = f'https://download.gnome.org/sources/libxslt/{version_prefix}/libxslt-{self.libxslt_version}.tar.xz' self.info( - '{:10}: {}'.format( - 'libxslt', 'PYXMLSEC_LIBXSLT_VERSION={}, downloading from {}'.format(self.libxslt_version, url) - ) + '{:10}: {}'.format('libxslt', f'PYXMLSEC_LIBXSLT_VERSION={self.libxslt_version}, downloading from {url}') ) libxslt_tar = self.libs_dir / 'libxslt.tar.gz' urlretrieve(url, str(libxslt_tar)) @@ -380,26 +365,24 @@ def prepare_static_build(self, build_platform): self.info('{:10}: {}'.format('xmlsec1', 'source tar not found, downloading ...')) if self.xmlsec1_version is None: url = latest_xmlsec_release() - self.info('{:10}: {}'.format('xmlsec1', 'PYXMLSEC_XMLSEC1_VERSION unset, downloading latest from {}'.format(url))) + self.info('{:10}: {}'.format('xmlsec1', f'PYXMLSEC_XMLSEC1_VERSION unset, downloading latest from {url}')) else: - url = 'https://github.com/lsh123/xmlsec/releases/download/{v}/xmlsec1-{v}.tar.gz'.format(v=self.xmlsec1_version) + url = f'https://github.com/lsh123/xmlsec/releases/download/{self.xmlsec1_version}/xmlsec1-{self.xmlsec1_version}.tar.gz' self.info( - '{:10}: {}'.format( - 'xmlsec1', 'PYXMLSEC_XMLSEC1_VERSION={}, downloading from {}'.format(self.xmlsec1_version, url) - ) + '{:10}: {}'.format('xmlsec1', f'PYXMLSEC_XMLSEC1_VERSION={self.xmlsec1_version}, downloading from {url}') ) xmlsec1_tar = self.libs_dir / 'xmlsec1.tar.gz' urlretrieve(url, str(xmlsec1_tar)) for file in (openssl_tar, zlib_tar, libiconv_tar, libxml2_tar, libxslt_tar, xmlsec1_tar): - self.info('Unpacking {}'.format(file.name)) + self.info(f'Unpacking {file.name}') try: with tarfile.open(str(file)) as tar: tar.extractall(path=str(self.build_libs_dir)) - except EOFError: - raise DistutilsError('Bad {} downloaded; remove it and try again.'.format(file.name)) + except EOFError as e: + raise DistutilsError(f'Bad {file.name} downloaded; remove it and try again.') from e - prefix_arg = '--prefix={}'.format(self.prefix_dir) + prefix_arg = f'--prefix={self.prefix_dir}' env = os.environ.copy() cflags = [] @@ -416,14 +399,13 @@ def prepare_static_build(self, build_platform): arch = self.plat_name.rsplit('-', 1)[1] if arch != platform.machine() and arch in ('x86_64', 'arm64'): - self.info('Cross-compiling for {}'.format(arch)) - cflags.append('-arch {}'.format(arch)) - ldflags.append('-arch {}'.format(arch)) + self.info(f'Cross-compiling for {arch}') + cflags.append(f'-arch {arch}') + ldflags.append(f'-arch {arch}') cross_compiling = CrossCompileInfo('darwin64', arch, 'cc') - major_version, minor_version = tuple(map(int, platform.mac_ver()[0].split('.')[:2])) - if major_version >= 11: - if 'MACOSX_DEPLOYMENT_TARGET' not in env: - env['MACOSX_DEPLOYMENT_TARGET'] = "11.0" + major_version, _ = tuple(map(int, platform.mac_ver()[0].split('.')[:2])) + if major_version >= 11 and 'MACOSX_DEPLOYMENT_TARGET' not in env: + env['MACOSX_DEPLOYMENT_TARGET'] = '11.0' env['CFLAGS'] = ' '.join(cflags) env['LDFLAGS'] = ' '.join(ldflags) @@ -437,20 +419,18 @@ def prepare_static_build(self, build_platform): else: openssl_config_cmd.insert(0, './config') subprocess.check_call(openssl_config_cmd, cwd=str(openssl_dir), env=env) - subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(openssl_dir), env=env) - subprocess.check_call( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install_sw'], cwd=str(openssl_dir), env=env - ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(openssl_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install_sw'], cwd=str(openssl_dir), env=env) self.info('Building zlib') zlib_dir = next(self.build_libs_dir.glob('zlib-*')) subprocess.check_call(['./configure', prefix_arg], cwd=str(zlib_dir), env=env) - subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(zlib_dir), env=env) - subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(zlib_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(zlib_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(zlib_dir), env=env) host_arg = [] if cross_compiling: - host_arg = ['--host={}'.format(cross_compiling.arch)] + host_arg = [f'--host={cross_compiling.arch}'] self.info('Building libiconv') libiconv_dir = next(self.build_libs_dir.glob('libiconv-*')) @@ -465,8 +445,8 @@ def prepare_static_build(self, build_platform): cwd=str(libiconv_dir), env=env, ) - subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libiconv_dir), env=env) - subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libiconv_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libiconv_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libiconv_dir), env=env) self.info('Building LibXML2') libxml2_dir = next(self.build_libs_dir.glob('libxml2-*')) @@ -478,15 +458,15 @@ def prepare_static_build(self, build_platform): '--disable-shared', '--without-lzma', '--without-python', - '--with-iconv={}'.format(self.prefix_dir), - '--with-zlib={}'.format(self.prefix_dir), + f'--with-iconv={self.prefix_dir}', + f'--with-zlib={self.prefix_dir}', *host_arg, ], cwd=str(libxml2_dir), env=env, ) - subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libxml2_dir), env=env) - subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libxml2_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxml2_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxml2_dir), env=env) self.info('Building libxslt') libxslt_dir = next(self.build_libs_dir.glob('libxslt-*')) @@ -498,14 +478,14 @@ def prepare_static_build(self, build_platform): '--disable-shared', '--without-python', '--without-crypto', - '--with-libxml-prefix={}'.format(self.prefix_dir), + f'--with-libxml-prefix={self.prefix_dir}', *host_arg, ], cwd=str(libxslt_dir), env=env, ) - subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1)], cwd=str(libxslt_dir), env=env) - subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(libxslt_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxslt_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxslt_dir), env=env) self.info('Building xmlsec1') ldflags.append('-lpthread') @@ -524,21 +504,24 @@ def prepare_static_build(self, build_platform): '--enable-shared=no', '--enable-static-linking=yes', '--with-default-crypto=openssl', - '--with-openssl={}'.format(self.prefix_dir), - '--with-libxml={}'.format(self.prefix_dir), - '--with-libxslt={}'.format(self.prefix_dir), + f'--with-openssl={self.prefix_dir}', + f'--with-libxml={self.prefix_dir}', + f'--with-libxslt={self.prefix_dir}', *host_arg, ], cwd=str(xmlsec1_dir), env=env, ) + include_flags = [ + f'-I{self.prefix_dir / "include"}', + f'-I{self.prefix_dir / "include" / "libxml"}', + ] subprocess.check_call( - ['make', '-j{}'.format(multiprocessing.cpu_count() + 1)] - + ['-I{}'.format(str(self.prefix_dir / 'include')), '-I{}'.format(str(self.prefix_dir / 'include' / 'libxml'))], + ['make', f'-j{multiprocessing.cpu_count() + 1}', *include_flags], cwd=str(xmlsec1_dir), env=env, ) - subprocess.check_call(['make', '-j{}'.format(multiprocessing.cpu_count() + 1), 'install'], cwd=str(xmlsec1_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(xmlsec1_dir), env=env) ext = self.ext_map['xmlsec'] ext.define_macros = [ @@ -584,7 +567,7 @@ def prepare_static_build(self, build_platform): setup_reqs = ['setuptools_scm[toml]>=3.4', 'pkgconfig>=1.5.1', 'lxml>=3.8'] -with io.open('README.md', encoding='utf-8') as f: +with open('README.md', encoding='utf-8') as f: long_desc = f.read() @@ -596,10 +579,10 @@ def prepare_static_build(self, build_platform): long_description_content_type='text/markdown', ext_modules=[pyxmlsec], cmdclass={'build_ext': build_ext}, - python_requires='>=3.5', + python_requires='>=3.9', setup_requires=setup_reqs, install_requires=['lxml>=3.8'], - author="Bulat Gaifullin", + author='Bulat Gaifullin', author_email='support@mehcode.com', maintainer='Oleg Hoefling', maintainer_email='oleg.hoefling@gmail.com', @@ -619,12 +602,11 @@ def prepare_static_build(self, build_platform): 'Operating System :: OS Independent', 'Programming Language :: C', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Topic :: Text Processing :: Markup :: XML', 'Typing :: Typed', ], diff --git a/src/xmlsec/constants.pyi b/src/xmlsec/constants.pyi index 3c3ea94f..a9254ddd 100644 --- a/src/xmlsec/constants.pyi +++ b/src/xmlsec/constants.pyi @@ -1,10 +1,5 @@ import sys -from typing import NamedTuple - -if sys.version_info >= (3, 8): - from typing import Final -else: - from typing_extensions import Final +from typing import Final, NamedTuple class __KeyData(NamedTuple): # __KeyData type href: str diff --git a/tests/base.py b/tests/base.py index 48aef81d..1d21c89b 100644 --- a/tests/base.py +++ b/tests/base.py @@ -25,16 +25,16 @@ class TestMemoryLeaks(unittest.TestCase): iterations = test_iterations - data_dir = os.path.join(os.path.dirname(__file__), "data") + data_dir = os.path.join(os.path.dirname(__file__), 'data') def setUp(self): gc.disable() - self.addTypeEqualityFunc(etype, "assertXmlEqual") + self.addTypeEqualityFunc(etype, 'assertXmlEqual') xmlsec.enable_debug_trace(1) def run(self, result=None): # run first time - super(TestMemoryLeaks, self).run(result=result) + super().run(result=result) if self.iterations == 0: return @@ -43,7 +43,7 @@ def run(self, result=None): m_hits = 0 o_hits = 0 for _ in range(self.iterations): - super(TestMemoryLeaks, self).run(result=result) + super().run(result=result) m_usage_n = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss if m_usage_n > m_usage: m_usage = m_usage_n @@ -58,13 +58,13 @@ def run(self, result=None): if m_hits > int(self.iterations * 0.8): result.buffer = False try: - raise AssertionError("memory leak detected") + raise AssertionError('memory leak detected') except AssertionError: result.addError(self, sys.exc_info()) if o_hits > int(self.iterations * 0.8): result.buffer = False try: - raise AssertionError("unreferenced objects detected") + raise AssertionError('unreferenced objects detected') except AssertionError: result.addError(self, sys.exc_info()) @@ -74,7 +74,7 @@ def path(self, name): def load(self, name): """Load resource by name.""" - with open(self.path(name), "rb") as stream: + with open(self.path(name), 'rb') as stream: return stream.read() def load_xml(self, name, xpath=None): @@ -88,28 +88,26 @@ def load_xml(self, name, xpath=None): def dump(self, root): print(etree.tostring(root)) - def assertXmlEqual(self, first, second, msg=None): # noqa: N802 + def assertXmlEqual(self, first, second, msg=None): """Check equality of etree.roots.""" msg = msg or '' if first.tag != second.tag: - self.fail('Tags do not match: {} and {}. {}'.format(first.tag, second.tag, msg)) + self.fail(f'Tags do not match: {first.tag} and {second.tag}. {msg}') for name, value in first.attrib.items(): if second.attrib.get(name) != value: - self.fail('Attributes do not match: {}={!r}, {}={!r}. {}'.format(name, value, name, second.attrib.get(name), msg)) - for name in second.attrib.keys(): + self.fail(f'Attributes do not match: {name}={value!r}, {name}={second.attrib.get(name)!r}. {msg}') + for name in second.attrib: if name not in first.attrib: - self.fail('x2 has an attribute x1 is missing: {}. {}'.format(name, msg)) + self.fail(f'x2 has an attribute x1 is missing: {name}. {msg}') if not _xml_text_compare(first.text, second.text): - self.fail('text: {!r} != {!r}. {}'.format(first.text, second.text, msg)) + self.fail(f'text: {first.text!r} != {second.text!r}. {msg}') if not _xml_text_compare(first.tail, second.tail): - self.fail('tail: {!r} != {!r}. {}'.format(first.tail, second.tail, msg)) + self.fail(f'tail: {first.tail!r} != {second.tail!r}. {msg}') cl1 = sorted(first.getchildren(), key=lambda x: x.tag) cl2 = sorted(second.getchildren(), key=lambda x: x.tag) if len(cl1) != len(cl2): - self.fail('children length differs, {} != {}. {}'.format(len(cl1), len(cl2), msg)) - i = 0 + self.fail(f'children length differs, {len(cl1)} != {len(cl2)}. {msg}') for c1, c2 in zip(cl1, cl2): - i += 1 self.assertXmlEqual(c1, c2) diff --git a/tests/conftest.py b/tests/conftest.py index 675258c5..a65235d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,5 @@ def pytest_collection_modifyitems(items): - """ - Put the module init test first. + """Put the module init test first. This way, we implicitly check whether any subsequent test fails because of module reinitialization. """ diff --git a/tests/softhsm_setup.py b/tests/softhsm_setup.py index 247f1b18..3f5076d2 100644 --- a/tests/softhsm_setup.py +++ b/tests/softhsm_setup.py @@ -24,7 +24,7 @@ def find_alts(component_name, alts) -> str: for a in alts: if os.path.exists(a): return a - raise unittest.SkipTest('Required component is missing: {}'.format(component_name)) + raise unittest.SkipTest(f'Required component is missing: {component_name}') def run_cmd(args, softhsm_conf=None): @@ -101,7 +101,7 @@ def run_cmd(args, softhsm_conf=None): def _temp_file() -> str: - f = tempfile.NamedTemporaryFile(delete=False) + f = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 p11_test_files.append(f.name) return f.name @@ -123,24 +123,20 @@ def setup() -> None: if softhsm_version == 2: softhsm_db = _temp_dir() f.write( - """ + f""" # Generated by test -directories.tokendir = {} +directories.tokendir = {softhsm_db} objectstore.backend = file log.level = DEBUG -""".format( - softhsm_db - ) +""" ) else: softhsm_db = _temp_file() f.write( - """ + f""" # Generated by test -0:{} -""".format( - softhsm_db - ) +0:{softhsm_db} +""" ) logging.debug('Initializing the token') @@ -227,13 +223,13 @@ def setup() -> None: '[pkcs11_section]', 'engine_id = pkcs11', # dynamic_path, - "MODULE_PATH = {}".format(component_path['P11_MODULE']), + 'MODULE_PATH = {}'.format(component_path['P11_MODULE']), 'init = 0', ] ) ) - with open(openssl_conf, 'r') as f: + with open(openssl_conf) as f: logging.debug('-------- START DEBUG openssl_conf --------') logging.debug(f.readlines()) logging.debug('-------- END DEBUG openssl_conf --------') @@ -309,7 +305,7 @@ def setup() -> None: softhsm_conf=softhsm_conf, ) - # TODO: Should be teardowned in teardown # noqa: T101 + # TODO: Should be teardowned in teardown os.environ['SOFTHSM_CONF'] = softhsm_conf os.environ['SOFTHSM2_CONF'] = softhsm_conf diff --git a/tests/test_constants.py b/tests/test_constants.py index f79d19f0..2c39d5f6 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -21,22 +21,22 @@ def _constants(typename): @pytest.mark.parametrize('transform', _constants('__Transform'), ids=repr) def test_transform_str(transform): """Test string representation of ``xmlsec.constants.__Transform``.""" - assert str(transform) == '{}, {}'.format(transform.name, transform.href) + assert str(transform) == f'{transform.name}, {transform.href}' @pytest.mark.parametrize('transform', _constants('__Transform'), ids=repr) def test_transform_repr(transform): """Test raw string representation of ``xmlsec.constants.__Transform``.""" - assert repr(transform) == '__Transform({!r}, {!r}, {})'.format(transform.name, transform.href, transform.usage) + assert repr(transform) == f'__Transform({transform.name!r}, {transform.href!r}, {transform.usage})' @pytest.mark.parametrize('keydata', _constants('__KeyData'), ids=repr) def test_keydata_str(keydata): """Test string representation of ``xmlsec.constants.__KeyData``.""" - assert str(keydata) == '{}, {}'.format(keydata.name, keydata.href) + assert str(keydata) == f'{keydata.name}, {keydata.href}' @pytest.mark.parametrize('keydata', _constants('__KeyData'), ids=repr) def test_keydata_repr(keydata): """Test raw string representation of ``xmlsec.constants.__KeyData``.""" - assert repr(keydata) == '__KeyData({!r}, {!r})'.format(keydata.name, keydata.href) + assert repr(keydata) == f'__KeyData({keydata.name!r}, {keydata.href!r})' diff --git a/tests/test_doc_examples.py b/tests/test_doc_examples.py index 2fc490f3..7aa8e517 100644 --- a/tests/test_doc_examples.py +++ b/tests/test_doc_examples.py @@ -3,24 +3,17 @@ import contextlib import os import runpy -import sys +from pathlib import Path import pytest -if sys.version_info >= (3, 4): - from pathlib import Path -else: # python2.7 compat - from _pytest.pathlib import Path - - examples_dir = Path(__file__, '../../doc/source/examples').resolve() examples = sorted(examples_dir.glob('*.py')) @contextlib.contextmanager def cd(where_to): - """ - Temporarily change the working directory. + """Temporarily change the working directory. Restore the current working dir after exiting the context. """ @@ -34,8 +27,7 @@ def cd(where_to): @pytest.mark.parametrize('example', examples, ids=lambda p: p.name) def test_doc_example(example): - """ - Verify example scripts included in the docs are up to date. + """Verify example scripts included in the docs are up to date. Execute each script in :file:`docs/source/examples`, not raising any errors is good enough. diff --git a/tests/test_ds.py b/tests/test_ds.py index 38f0b25c..dd0657d3 100644 --- a/tests/test_ds.py +++ b/tests/test_ds.py @@ -25,13 +25,13 @@ def test_no_key(self): def test_del_key(self): ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) del ctx.key self.assertIsNone(ctx.key) def test_set_key(self): ctx = xmlsec.SignatureContext(manager=xmlsec.KeysManager()) - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) def test_set_key_bad_type(self): @@ -46,9 +46,9 @@ def test_set_invalid_key(self): def test_register_id(self): ctx = xmlsec.SignatureContext() - root = self.load_xml("sign_template.xml") - sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, "Id") - ctx.register_id(sign, "Id") + root = self.load_xml('sign_template.xml') + sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, 'Id') + ctx.register_id(sign, 'Id') def test_register_id_bad_args(self): ctx = xmlsec.SignatureContext() @@ -57,41 +57,41 @@ def test_register_id_bad_args(self): def test_register_id_with_namespace_without_attribute(self): ctx = xmlsec.SignatureContext() - root = self.load_xml("sign_template.xml") - sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, "Id") + root = self.load_xml('sign_template.xml') + sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, 'Id') with self.assertRaisesRegex(xmlsec.Error, 'missing attribute.'): - ctx.register_id(sign, "Id", id_ns='foo') + ctx.register_id(sign, 'Id', id_ns='foo') def test_sign_bad_args(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) with self.assertRaises(TypeError): ctx.sign('') def test_sign_fail(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) with self.assertRaisesRegex(xmlsec.Error, 'failed to sign'): ctx.sign(self.load_xml('sign1-in.xml')) def test_sign_case1(self): """Should sign a pre-constructed template file using a key from a PEM file.""" - root = self.load_xml("sign1-in.xml") + root = self.load_xml('sign1-in.xml') sign = xmlsec.tree.find_node(root, consts.NodeSignature) self.assertIsNotNone(sign) ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) - self.assertEqual(self.load_xml("sign1-out.xml"), root) + self.assertEqual(self.load_xml('sign1-out.xml'), root) def test_sign_case2(self): """Should sign a dynamicaly constructed template file using a key from a PEM file.""" - root = self.load_xml("sign2-in.xml") + root = self.load_xml('sign2-in.xml') sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1) self.assertIsNotNone(sign) root.append(sign) @@ -101,17 +101,17 @@ def test_sign_case2(self): xmlsec.template.add_key_name(ki) ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) - self.assertEqual(self.load_xml("sign2-out.xml"), root) + self.assertEqual(self.load_xml('sign2-out.xml'), root) def test_sign_case3(self): """Should sign a file using a dynamicaly created template, key from PEM and an X509 cert.""" - root = self.load_xml("sign3-in.xml") + root = self.load_xml('sign3-in.xml') sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1) self.assertIsNotNone(sign) root.append(sign) @@ -121,23 +121,23 @@ def test_sign_case3(self): xmlsec.template.add_x509_data(ki) ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.load_cert_from_file(self.path('rsacert.pem'), consts.KeyDataFormatPem) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) - self.assertEqual(self.load_xml("sign3-out.xml"), root) + self.assertEqual(self.load_xml('sign3-out.xml'), root) def test_sign_case4(self): """Should sign a file using a dynamically created template, key from PEM and an X509 cert with custom ns.""" - root = self.load_xml("sign4-in.xml") - xmlsec.tree.add_ids(root, ["ID"]) + root = self.load_xml('sign4-in.xml') + xmlsec.tree.add_ids(root, ['ID']) elem_id = root.get('ID', None) if elem_id: elem_id = '#' + elem_id - sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, ns="ds") + sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1, ns='ds') self.assertIsNotNone(sign) root.append(sign) ref = xmlsec.template.add_reference(sign, consts.TransformSha1, uri=elem_id) @@ -147,18 +147,18 @@ def test_sign_case4(self): xmlsec.template.add_x509_data(ki) ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.load_cert_from_file(self.path('rsacert.pem'), consts.KeyDataFormatPem) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) - self.assertEqual(self.load_xml("sign4-out.xml"), root) + self.assertEqual(self.load_xml('sign4-out.xml'), root) def test_sign_case5(self): """Should sign a file using a dynamicaly created template, key from PEM file and an X509 certificate.""" - root = self.load_xml("sign5-in.xml") + root = self.load_xml('sign5-in.xml') sign = xmlsec.template.create(root, consts.TransformExclC14N, consts.TransformRsaSha1) self.assertIsNotNone(sign) root.append(sign) @@ -175,11 +175,11 @@ def test_sign_case5(self): xmlsec.template.x509_issuer_serial_add_serial_number(x509_issuer_serial, '1') ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.load_cert_from_file(self.path('rsacert.pem'), consts.KeyDataFormatPem) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) if (1, 2, 36) <= xmlsec.get_libxmlsec_version() <= (1, 2, 37): @@ -190,7 +190,7 @@ def test_sign_case5(self): def test_sign_binary_bad_args(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) with self.assertRaises(TypeError): ctx.sign_binary(bytes=1, transform='') @@ -202,23 +202,23 @@ def test_sign_binary_no_key(self): @unittest.skipIf(not hasattr(consts, 'TransformXslt'), reason='XSLT transformations not enabled') def test_sign_binary_invalid_signature_method(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) with self.assertRaisesRegex(xmlsec.Error, 'incompatible signature method'): ctx.sign_binary(bytes=b'', transform=consts.TransformXslt) def test_sign_binary(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) - sign = ctx.sign_binary(self.load("sign6-in.bin"), consts.TransformRsaSha1) - self.assertEqual(self.load("sign6-out.bin"), sign) + sign = ctx.sign_binary(self.load('sign6-in.bin'), consts.TransformRsaSha1) + self.assertEqual(self.load('sign6-out.bin'), sign) def test_sign_binary_twice_not_possible(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) data = self.load('sign6-in.bin') ctx.sign_binary(data, consts.TransformRsaSha1) with self.assertRaisesRegex(xmlsec.Error, 'Signature context already used; it is designed for one use only.'): @@ -226,13 +226,13 @@ def test_sign_binary_twice_not_possible(self): def test_verify_bad_args(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) with self.assertRaises(TypeError): ctx.verify('') def test_verify_fail(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) with self.assertRaisesRegex(xmlsec.Error, 'failed to verify'): ctx.verify(self.load_xml('sign1-in.xml')) @@ -252,41 +252,41 @@ def test_verify_case_5(self): self.check_verify(5) def check_verify(self, i): - root = self.load_xml("sign{}-out.xml".format(i)) - xmlsec.tree.add_ids(root, ["ID"]) + root = self.load_xml(f'sign{i}-out.xml') + xmlsec.tree.add_ids(root, ['ID']) sign = xmlsec.tree.find_node(root, consts.NodeSignature) self.assertIsNotNone(sign) - self.assertEqual(consts.NodeSignature, sign.tag.partition("}")[2]) + self.assertEqual(consts.NodeSignature, sign.tag.partition('}')[2]) ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsapub.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsapub.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsapub.pem' - self.assertEqual("rsapub.pem", ctx.key.name) + self.assertEqual('rsapub.pem', ctx.key.name) ctx.verify(sign) def test_validate_binary_sign(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) - ctx.verify_binary(self.load("sign6-in.bin"), consts.TransformRsaSha1, self.load("sign6-out.bin")) + ctx.verify_binary(self.load('sign6-in.bin'), consts.TransformRsaSha1, self.load('sign6-out.bin')) def test_validate_binary_sign_fail(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) with self.assertRaises(xmlsec.Error): - ctx.verify_binary(self.load("sign6-in.bin"), consts.TransformRsaSha1, b"invalid") + ctx.verify_binary(self.load('sign6-in.bin'), consts.TransformRsaSha1, b'invalid') def test_enable_reference_transform(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) ctx.enable_reference_transform(consts.TransformRsaSha1) def test_enable_reference_transform_bad_args(self): @@ -301,7 +301,7 @@ def test_enable_reference_transform_bad_args(self): def test_enable_signature_transform(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) ctx.enable_signature_transform(consts.TransformRsaSha1) def test_enable_signature_transform_bad_args(self): @@ -316,12 +316,12 @@ def test_enable_signature_transform_bad_args(self): def test_set_enabled_key_data(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) ctx.set_enabled_key_data([consts.KeyDataAes]) def test_set_enabled_key_data_empty(self): ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) ctx.set_enabled_key_data([]) def test_set_enabled_key_data_bad_args(self): diff --git a/tests/test_enc.py b/tests/test_enc.py index 1788b4d6..41f78d74 100644 --- a/tests/test_enc.py +++ b/tests/test_enc.py @@ -28,18 +28,18 @@ def test_no_key(self): def test_get_key(self): ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) self.assertIsNone(ctx.key) - ctx.key = xmlsec.Key.from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatCertPem) + ctx.key = xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem) self.assertIsNotNone(ctx.key) def test_del_key(self): ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) - ctx.key = xmlsec.Key.from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatCertPem) + ctx.key = xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem) del ctx.key self.assertIsNone(ctx.key) def test_set_key(self): ctx = xmlsec.EncryptionContext(manager=xmlsec.KeysManager()) - ctx.key = xmlsec.Key.from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatCertPem) + ctx.key = xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem) self.assertIsNotNone(ctx.key) def test_set_key_bad_type(self): @@ -54,16 +54,16 @@ def test_set_invalid_key(self): def test_encrypt_xml(self): root = self.load_xml('enc1-in.xml') - enc_data = xmlsec.template.encrypted_data_create(root, consts.TransformAes128Cbc, type=consts.TypeEncElement, ns="xenc") + enc_data = xmlsec.template.encrypted_data_create(root, consts.TransformAes128Cbc, type=consts.TypeEncElement, ns='xenc') xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) - ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns="dsig") + ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns='dsig') ek = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep) xmlsec.template.encrypted_data_ensure_cipher_value(ek) data = root.find('./Data') self.assertIsNotNone(data) manager = xmlsec.KeysManager() - manager.add_key(xmlsec.Key.from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatCertPem)) + manager.add_key(xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem)) ctx = xmlsec.EncryptionContext(manager) ctx.key = xmlsec.Key.generate(consts.KeyDataAes, 128, consts.KeyDataTypeSession) @@ -73,12 +73,12 @@ def test_encrypt_xml(self): enc_method = xmlsec.tree.find_child(enc_data, consts.NodeEncryptionMethod, consts.EncNs) self.assertIsNotNone(enc_method) - self.assertEqual("http://www.w3.org/2001/04/xmlenc#aes128-cbc", enc_method.get("Algorithm")) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#aes128-cbc', enc_method.get('Algorithm')) ki = xmlsec.tree.find_child(enc_data, consts.NodeKeyInfo, consts.DSigNs) self.assertIsNotNone(ki) enc_method2 = xmlsec.tree.find_node(ki, consts.NodeEncryptionMethod, consts.EncNs) self.assertIsNotNone(enc_method2) - self.assertEqual("http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p", enc_method2.get("Algorithm")) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p', enc_method2.get('Algorithm')) cipher_value = xmlsec.tree.find_node(ki, consts.NodeCipherValue, consts.EncNs) self.assertIsNotNone(cipher_value) @@ -109,32 +109,32 @@ def test_encrypt_xml_fail(self): def test_encrypt_binary(self): root = self.load_xml('enc2-in.xml') enc_data = xmlsec.template.encrypted_data_create( - root, consts.TransformAes128Cbc, type=consts.TypeEncContent, ns="xenc", mime_type="binary/octet-stream" + root, consts.TransformAes128Cbc, type=consts.TypeEncContent, ns='xenc', mime_type='binary/octet-stream' ) xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) - ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns="dsig") + ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns='dsig') ek = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep) xmlsec.template.encrypted_data_ensure_cipher_value(ek) manager = xmlsec.KeysManager() - manager.add_key(xmlsec.Key.from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatCertPem)) + manager.add_key(xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem)) ctx = xmlsec.EncryptionContext(manager) ctx.key = xmlsec.Key.generate(consts.KeyDataAes, 128, consts.KeyDataTypeSession) encrypted = ctx.encrypt_binary(enc_data, b'test') self.assertIsNotNone(encrypted) - self.assertEqual("{{{}}}{}".format(consts.EncNs, consts.NodeEncryptedData), encrypted.tag) + self.assertEqual(f'{{{consts.EncNs}}}{consts.NodeEncryptedData}', encrypted.tag) enc_method = xmlsec.tree.find_child(enc_data, consts.NodeEncryptionMethod, consts.EncNs) self.assertIsNotNone(enc_method) - self.assertEqual("http://www.w3.org/2001/04/xmlenc#aes128-cbc", enc_method.get("Algorithm")) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#aes128-cbc', enc_method.get('Algorithm')) ki = xmlsec.tree.find_child(enc_data, consts.NodeKeyInfo, consts.DSigNs) self.assertIsNotNone(ki) enc_method2 = xmlsec.tree.find_node(ki, consts.NodeEncryptionMethod, consts.EncNs) self.assertIsNotNone(enc_method2) - self.assertEqual("http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p", enc_method2.get("Algorithm")) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p', enc_method2.get('Algorithm')) cipher_value = xmlsec.tree.find_node(ki, consts.NodeCipherValue, consts.EncNs) self.assertIsNotNone(cipher_value) @@ -151,15 +151,15 @@ def test_encrypt_binary_bad_template(self): def test_encrypt_uri(self): root = self.load_xml('enc2-in.xml') enc_data = xmlsec.template.encrypted_data_create( - root, consts.TransformAes128Cbc, type=consts.TypeEncContent, ns="xenc", mime_type="binary/octet-stream" + root, consts.TransformAes128Cbc, type=consts.TypeEncContent, ns='xenc', mime_type='binary/octet-stream' ) xmlsec.template.encrypted_data_ensure_cipher_value(enc_data) - ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns="dsig") + ki = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns='dsig') ek = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep) xmlsec.template.encrypted_data_ensure_cipher_value(ek) manager = xmlsec.KeysManager() - manager.add_key(xmlsec.Key.from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatCertPem)) + manager.add_key(xmlsec.Key.from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatCertPem)) ctx = xmlsec.EncryptionContext(manager) ctx.key = xmlsec.Key.generate(consts.KeyDataAes, 128, consts.KeyDataTypeSession) @@ -169,17 +169,17 @@ def test_encrypt_uri(self): encrypted = ctx.encrypt_binary(enc_data, 'file://' + tmpfile.name) self.assertIsNotNone(encrypted) - self.assertEqual("{{{}}}{}".format(consts.EncNs, consts.NodeEncryptedData), encrypted.tag) + self.assertEqual(f'{{{consts.EncNs}}}{consts.NodeEncryptedData}', encrypted.tag) enc_method = xmlsec.tree.find_child(enc_data, consts.NodeEncryptionMethod, consts.EncNs) self.assertIsNotNone(enc_method) - self.assertEqual("http://www.w3.org/2001/04/xmlenc#aes128-cbc", enc_method.get("Algorithm")) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#aes128-cbc', enc_method.get('Algorithm')) ki = xmlsec.tree.find_child(enc_data, consts.NodeKeyInfo, consts.DSigNs) self.assertIsNotNone(ki) enc_method2 = xmlsec.tree.find_node(ki, consts.NodeEncryptionMethod, consts.EncNs) self.assertIsNotNone(enc_method2) - self.assertEqual("http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p", enc_method2.get("Algorithm")) + self.assertEqual('http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p', enc_method2.get('Algorithm')) cipher_value = xmlsec.tree.find_node(ki, consts.NodeCipherValue, consts.EncNs) self.assertIsNotNone(cipher_value) @@ -205,7 +205,7 @@ def test_decrypt_key(self): self.assertIsNotNone(enc_key) manager = xmlsec.KeysManager() - manager.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) + manager.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) ctx = xmlsec.EncryptionContext(manager) keydata = ctx.decrypt(enc_key) ctx.reset() @@ -215,19 +215,19 @@ def test_decrypt_key(self): self.assertIsNotNone(enc_data) decrypted = ctx.decrypt(enc_data) self.assertIsNotNone(decrypted) - self.assertEqual(self.load_xml("enc3-in.xml"), decrypted) + self.assertEqual(self.load_xml('enc3-in.xml'), decrypted) def check_decrypt(self, i): - root = self.load_xml('enc{}-out.xml'.format(i)) + root = self.load_xml(f'enc{i}-out.xml') enc_data = xmlsec.tree.find_child(root, consts.NodeEncryptedData, consts.EncNs) self.assertIsNotNone(enc_data) manager = xmlsec.KeysManager() - manager.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) + manager.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) ctx = xmlsec.EncryptionContext(manager) decrypted = ctx.decrypt(enc_data) self.assertIsNotNone(decrypted) - self.assertEqual(self.load_xml("enc{}-in.xml".format(i)), root) + self.assertEqual(self.load_xml(f'enc{i}-in.xml'), root) def test_decrypt_bad_args(self): ctx = xmlsec.EncryptionContext() diff --git a/tests/test_keys.py b/tests/test_keys.py index 0d41abef..977ddf82 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -9,33 +9,32 @@ class TestKeys(base.TestMemoryLeaks): def test_key_from_memory(self): - key = xmlsec.Key.from_memory(self.load("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_memory(self.load('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) def test_key_from_memory_with_bad_args(self): with self.assertRaises(TypeError): - xmlsec.Key.from_memory(1, format="") + xmlsec.Key.from_memory(1, format='') def test_key_from_memory_invalid_data(self): with self.assertRaisesRegex(xmlsec.Error, '.*cannot load key.*'): xmlsec.Key.from_memory(b'foo', format=consts.KeyDataFormatPem) def test_key_from_file(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) def test_key_from_file_with_bad_args(self): with self.assertRaises(TypeError): - xmlsec.Key.from_file(1, format="") + xmlsec.Key.from_file(1, format='') def test_key_from_invalid_file(self): - with self.assertRaisesRegex(xmlsec.Error, '.*cannot read key.*'): - with tempfile.NamedTemporaryFile() as tmpfile: - tmpfile.write(b'foo') - xmlsec.Key.from_file(tmpfile.name, format=consts.KeyDataFormatPem) + with self.assertRaisesRegex(xmlsec.Error, '.*cannot read key.*'), tempfile.NamedTemporaryFile() as tmpfile: + tmpfile.write(b'foo') + xmlsec.Key.from_file(tmpfile.name, format=consts.KeyDataFormatPem) def test_key_from_fileobj(self): - with open(self.path("rsakey.pem"), "rb") as fobj: + with open(self.path('rsakey.pem'), 'rb') as fobj: key = xmlsec.Key.from_file(fobj, format=consts.KeyDataFormatPem) self.assertIsNotNone(key) @@ -51,71 +50,69 @@ def test_generate(self): def test_generate_with_bad_args(self): with self.assertRaises(TypeError): - xmlsec.Key.generate(klass="", size="", type="") + xmlsec.Key.generate(klass='', size='', type='') def test_generate_invalid_size(self): with self.assertRaisesRegex(xmlsec.Error, '.*cannot generate key.*'): xmlsec.Key.generate(klass=consts.KeyDataAes, size=0, type=consts.KeyDataTypeSession) def test_from_binary_file(self): - key = xmlsec.Key.from_binary_file(klass=consts.KeyDataDes, filename=self.path("deskey.bin")) + key = xmlsec.Key.from_binary_file(klass=consts.KeyDataDes, filename=self.path('deskey.bin')) self.assertIsNotNone(key) def test_from_binary_file_with_bad_args(self): with self.assertRaises(TypeError): - xmlsec.Key.from_binary_file(klass="", filename=1) + xmlsec.Key.from_binary_file(klass='', filename=1) def test_from_invalid_binary_file(self): - with self.assertRaisesRegex(xmlsec.Error, '.*cannot read key.*'): - with tempfile.NamedTemporaryFile() as tmpfile: - tmpfile.write(b'foo') - xmlsec.Key.from_binary_file(klass=consts.KeyDataDes, filename=tmpfile.name) + with self.assertRaisesRegex(xmlsec.Error, '.*cannot read key.*'), tempfile.NamedTemporaryFile() as tmpfile: + tmpfile.write(b'foo') + xmlsec.Key.from_binary_file(klass=consts.KeyDataDes, filename=tmpfile.name) def test_from_binary_data(self): - key = xmlsec.Key.from_binary_data(klass=consts.KeyDataDes, data=self.load("deskey.bin")) + key = xmlsec.Key.from_binary_data(klass=consts.KeyDataDes, data=self.load('deskey.bin')) self.assertIsNotNone(key) def test_from_binary_data_with_bad_args(self): with self.assertRaises(TypeError): - xmlsec.Key.from_binary_data(klass="", data=1) + xmlsec.Key.from_binary_data(klass='', data=1) def test_from_invalid_binary_data(self): with self.assertRaisesRegex(xmlsec.Error, '.*cannot read key.*'): xmlsec.Key.from_binary_data(klass=consts.KeyDataDes, data=b'') def test_load_cert_from_file(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) - key.load_cert_from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatPem) + key.load_cert_from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatPem) def test_load_cert_from_file_with_bad_args(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) with self.assertRaises(TypeError): - key.load_cert_from_file(1, format="") + key.load_cert_from_file(1, format='') def test_load_cert_from_invalid_file(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) - with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'): - with tempfile.NamedTemporaryFile() as tmpfile: - tmpfile.write(b'foo') - key.load_cert_from_file(tmpfile.name, format=consts.KeyDataFormatPem) + with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'), tempfile.NamedTemporaryFile() as tmpfile: + tmpfile.write(b'foo') + key.load_cert_from_file(tmpfile.name, format=consts.KeyDataFormatPem) def test_load_cert_from_fileobj(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) - with open(self.path("rsacert.pem"), "rb") as fobj: + with open(self.path('rsacert.pem'), 'rb') as fobj: key.load_cert_from_file(fobj, format=consts.KeyDataFormatPem) def test_load_cert_from_fileobj_with_bad_args(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) - with self.assertRaises(TypeError), open(self.path("rsacert.pem"), "rb") as fobj: + with self.assertRaises(TypeError), open(self.path('rsacert.pem'), 'rb') as fobj: key.load_cert_from_file(fobj, format='') def test_load_cert_from_invalid_fileobj(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) with tempfile.NamedTemporaryFile(delete=False) as tmpfile: tmpfile.write(b'foo') @@ -123,41 +120,41 @@ def test_load_cert_from_invalid_fileobj(self): key.load_cert_from_file(fp, format=consts.KeyDataFormatPem) def test_load_cert_from_memory(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) - key.load_cert_from_memory(self.load("rsacert.pem"), format=consts.KeyDataFormatPem) + key.load_cert_from_memory(self.load('rsacert.pem'), format=consts.KeyDataFormatPem) def test_load_cert_from_memory_with_bad_args(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) with self.assertRaises(TypeError): - key.load_cert_from_memory(1, format="") + key.load_cert_from_memory(1, format='') def test_load_cert_from_memory_invalid_data(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNotNone(key) with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'): key.load_cert_from_memory(b'', format=consts.KeyDataFormatPem) def test_get_name(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) self.assertIsNone(key.name) def test_get_name_invalid_key(self): key = xmlsec.Key() with self.assertRaisesRegex(ValueError, 'key is not ready'): - key.name + key.name # noqa: B018 def test_del_name(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) - key.name = "rsakey" + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + key.name = 'rsakey' del key.name self.assertIsNone(key.name) def test_set_name(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) - key.name = "rsakey" - self.assertEqual("rsakey", key.name) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) + key.name = 'rsakey' + self.assertEqual('rsakey', key.name) def test_set_name_invalid_key(self): key = xmlsec.Key() @@ -165,56 +162,55 @@ def test_set_name_invalid_key(self): key.name = 'foo' def test_copy(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) key2 = copy.copy(key) del key - key2.load_cert_from_file(self.path("rsacert.pem"), format=consts.KeyDataFormatPem) + key2.load_cert_from_file(self.path('rsacert.pem'), format=consts.KeyDataFormatPem) class TestKeysManager(base.TestMemoryLeaks): def test_add_key(self): - key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) mngr = xmlsec.KeysManager() mngr.add_key(key) def test_add_key_with_bad_args(self): mngr = xmlsec.KeysManager() with self.assertRaises(TypeError): - mngr.add_key("") + mngr.add_key('') def test_load_cert(self): mngr = xmlsec.KeysManager() - mngr.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) - mngr.load_cert(self.path("rsacert.pem"), format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) + mngr.load_cert(self.path('rsacert.pem'), format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) def test_load_cert_with_bad_args(self): mngr = xmlsec.KeysManager() - mngr.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) - with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'): - with tempfile.NamedTemporaryFile() as tmpfile: - tmpfile.write(b'foo') - mngr.load_cert(tmpfile.name, format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) + with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'), tempfile.NamedTemporaryFile() as tmpfile: + tmpfile.write(b'foo') + mngr.load_cert(tmpfile.name, format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) def test_load_invalid_cert(self): mngr = xmlsec.KeysManager() - mngr.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) with self.assertRaises(TypeError): - mngr.load_cert(1, format="", type="") + mngr.load_cert(1, format='', type='') def test_load_cert_from_memory(self): mngr = xmlsec.KeysManager() - mngr.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) - mngr.load_cert_from_memory(self.load("rsacert.pem"), format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) + mngr.load_cert_from_memory(self.load('rsacert.pem'), format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) def test_load_cert_from_memory_with_bad_args(self): mngr = xmlsec.KeysManager() - mngr.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) with self.assertRaises(TypeError): - mngr.load_cert_from_memory(1, format="", type="") + mngr.load_cert_from_memory(1, format='', type='') def test_load_cert_from_memory_invalid_data(self): mngr = xmlsec.KeysManager() - mngr.add_key(xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem)) + mngr.add_key(xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem)) with self.assertRaisesRegex(xmlsec.Error, '.*cannot load cert.*'): mngr.load_cert_from_memory(b'', format=consts.KeyDataFormatPem, type=consts.KeyDataTypeTrusted) diff --git a/tests/test_main.py b/tests/test_main.py index 3db18582..8f1501f2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -10,7 +10,7 @@ class TestBase64LineSize(base.TestMemoryLeaks): def tearDown(self): xmlsec.base64_default_line_size(64) - super(TestBase64LineSize, self).tearDown() + super().tearDown() def test_get_base64_default_line_size(self): self.assertEqual(xmlsec.base64_default_line_size(), 64) @@ -43,12 +43,12 @@ def setUp(self): xmlsec.cleanup_callbacks() def _sign_doc(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) - xmlsec.template.add_reference(sign, consts.TransformSha1, uri="cid:123456") + xmlsec.template.add_reference(sign, consts.TransformSha1, uri='cid:123456') ctx = xmlsec.SignatureContext() - ctx.key = xmlsec.Key.from_file(self.path("rsakey.pem"), format=consts.KeyDataFormatPem) + ctx.key = xmlsec.Key.from_file(self.path('rsakey.pem'), format=consts.KeyDataFormatPem) ctx.sign(sign) return sign @@ -78,7 +78,7 @@ def _register_match_callbacks(self): def _find(self, elem, *tags): try: return elem.xpath( - './' + '/'.join('xmldsig:{}'.format(tag) for tag in tags), + './' + '/'.join(f'xmldsig:{tag}' for tag in tags), namespaces={ 'xmldsig': 'http://www.w3.org/2000/09/xmldsig#', }, @@ -125,7 +125,7 @@ def match_cb(filename): self._verify_external_data_signature() self.assertEqual(bad_match_calls, 0) - @skipIf(sys.platform == "win32", "unclear behaviour on windows") + @skipIf(sys.platform == 'win32', 'unclear behaviour on windows') def test_failed_sign_because_default_callbacks(self): mismatch_calls = 0 diff --git a/tests/test_pkcs11.py b/tests/test_pkcs11.py index accd29ae..cba1a3f0 100644 --- a/tests/test_pkcs11.py +++ b/tests/test_pkcs11.py @@ -2,7 +2,7 @@ from tests import base from xmlsec import constants as consts -KEY_URL = "pkcs11;pkcs11:token=test;object=test;pin-value=secret1" +KEY_URL = 'pkcs11;pkcs11:token=test;object=test;pin-value=secret1' def setUpModule(): @@ -43,7 +43,7 @@ def test_sign_fail(self): def test_sign_case1(self): """Should sign a pre-constructed template file using a key from a pkcs11 engine.""" - root = self.load_xml("sign1-in.xml") + root = self.load_xml('sign1-in.xml') sign = xmlsec.tree.find_node(root, consts.NodeSignature) self.assertIsNotNone(sign) @@ -51,7 +51,7 @@ def test_sign_case1(self): ctx.key = xmlsec.Key.from_engine(KEY_URL) self.assertIsNotNone(ctx.key) ctx.key.name = 'rsakey.pem' - self.assertEqual("rsakey.pem", ctx.key.name) + self.assertEqual('rsakey.pem', ctx.key.name) ctx.sign(sign) - self.assertEqual(self.load_xml("sign1-out.xml"), root) + self.assertEqual(self.load_xml('sign1-out.xml'), root) diff --git a/tests/test_templates.py b/tests/test_templates.py index 3bae7e55..bbf7f42d 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -10,61 +10,61 @@ class TestTemplates(base.TestMemoryLeaks): def test_create(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create( - root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1, id="Id", ns="test" + root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1, id='Id', ns='test' ) - self.assertEqual("Id", sign.get("Id")) - self.assertEqual("test", sign.prefix) + self.assertEqual('Id', sign.get('Id')) + self.assertEqual('test', sign.prefix) def test_create_bad_args(self): with self.assertRaises(TypeError): xmlsec.template.create('', c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) def test_encrypt_data_create(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') enc = xmlsec.template.encrypted_data_create( - root, method=consts.TransformDes3Cbc, id="Id", type="Type", mime_type="MimeType", encoding="Encoding", ns="test" + root, method=consts.TransformDes3Cbc, id='Id', type='Type', mime_type='MimeType', encoding='Encoding', ns='test' ) - for a in ("Id", "Type", "MimeType", "Encoding"): + for a in ('Id', 'Type', 'MimeType', 'Encoding'): self.assertEqual(a, enc.get(a)) - self.assertEqual("test", enc.prefix) + self.assertEqual('test', enc.prefix) def test_ensure_key_info(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) - ki = xmlsec.template.ensure_key_info(sign, id="Id") - self.assertEqual("Id", ki.get("Id")) + ki = xmlsec.template.ensure_key_info(sign, id='Id') + self.assertEqual('Id', ki.get('Id')) def test_ensure_key_info_fail(self): with self.assertRaisesRegex(xmlsec.Error, 'cannot ensure key info.'): - xmlsec.template.ensure_key_info(etree.fromstring(b''), id="Id") + xmlsec.template.ensure_key_info(etree.fromstring(b''), id='Id') def test_ensure_key_info_bad_args(self): with self.assertRaises(TypeError): xmlsec.template.ensure_key_info('', id=0) def test_add_encrypted_key(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) ek = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep) - self.assertEqual(ek, xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeEncryptedKey, consts.EncNs)) - ek2 = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep, id="Id", type="Type", recipient="Recipient") - for a in ("Id", "Type", "Recipient"): + self.assertEqual(ek, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeEncryptedKey, consts.EncNs)) + ek2 = xmlsec.template.add_encrypted_key(ki, consts.TransformRsaOaep, id='Id', type='Type', recipient='Recipient') + for a in ('Id', 'Type', 'Recipient'): self.assertEqual(a, ek2.get(a)) def test_add_key_name(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) kn = xmlsec.template.add_key_name(ki) - self.assertEqual(kn, xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeKeyName, consts.DSigNs)) - kn2 = xmlsec.template.add_key_name(ki, name="name") - self.assertEqual("name", kn2.text) + self.assertEqual(kn, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeKeyName, consts.DSigNs)) + kn2 = xmlsec.template.add_key_name(ki, name='name') + self.assertEqual('name', kn2.text) def test_add_key_name_none(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) kn2 = xmlsec.template.add_key_name(ki, name=None) @@ -76,10 +76,10 @@ def test_add_key_name_bad_args(self): xmlsec.template.add_key_name('') def test_add_reference(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) - ref = xmlsec.template.add_reference(sign, consts.TransformSha1, id="Id", uri="URI", type="Type") - for a in ("Id", "URI", "Type"): + ref = xmlsec.template.add_reference(sign, consts.TransformSha1, id='Id', uri='URI', type='Type') + for a in ('Id', 'URI', 'Type'): self.assertEqual(a, ref.get(a)) def test_add_reference_bad_args(self): @@ -99,18 +99,18 @@ def test_add_transform_bad_args(self): xmlsec.template.add_transform(etree.Element('root'), '') def test_add_key_value(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) kv = xmlsec.template.add_key_value(ki) - self.assertEqual(kv, xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeKeyValue, consts.DSigNs)) + self.assertEqual(kv, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeKeyValue, consts.DSigNs)) def test_add_key_value_bad_args(self): with self.assertRaises(TypeError): xmlsec.template.add_key_value('') def test_add_x509_data(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) x509 = xmlsec.template.add_x509_data(ki) @@ -121,22 +121,22 @@ def test_add_x509_data(self): xmlsec.template.x509_data_add_subject_name(x509) xmlsec.template.x509_issuer_serial_add_issuer_name(issuer) xmlsec.template.x509_issuer_serial_add_serial_number(issuer) - self.assertEqual(x509, xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeX509Data, consts.DSigNs)) + self.assertEqual(x509, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeX509Data, consts.DSigNs)) def test_add_x509_data_bad_args(self): with self.assertRaises(TypeError): xmlsec.template.add_x509_data('') def test_x509_issuer_serial_add_issuer(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ki = xmlsec.template.ensure_key_info(sign) x509 = xmlsec.template.add_x509_data(ki) issuer = xmlsec.template.x509_data_add_issuer_serial(x509) - name = xmlsec.template.x509_issuer_serial_add_issuer_name(issuer, name="Name") - serial = xmlsec.template.x509_issuer_serial_add_serial_number(issuer, serial="Serial") - self.assertEqual("Name", name.text) - self.assertEqual("Serial", serial.text) + name = xmlsec.template.x509_issuer_serial_add_issuer_name(issuer, name='Name') + serial = xmlsec.template.x509_issuer_serial_add_serial_number(issuer, serial='Serial') + self.assertEqual('Name', name.text) + self.assertEqual('Serial', serial.text) def test_x509_issuer_serial_add_issuer_bad_args(self): with self.assertRaises(TypeError): @@ -175,23 +175,23 @@ def test_encrypted_data_create_bad_args(self): xmlsec.template.encrypted_data_create('', 0) def test_encrypted_data_ensure_cipher_value(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') enc = xmlsec.template.encrypted_data_create(root, method=consts.TransformDes3Cbc) cv = xmlsec.template.encrypted_data_ensure_cipher_value(enc) - self.assertEqual(cv, xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeCipherValue, consts.EncNs)) + self.assertEqual(cv, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeCipherValue, consts.EncNs)) def test_encrypted_data_ensure_cipher_value_bad_args(self): with self.assertRaises(TypeError): xmlsec.template.encrypted_data_ensure_cipher_value('') def test_encrypted_data_ensure_key_info(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') enc = xmlsec.template.encrypted_data_create(root, method=consts.TransformDes3Cbc) ki = xmlsec.template.encrypted_data_ensure_key_info(enc) - self.assertEqual(ki, xmlsec.tree.find_node(self.load_xml("enc_template.xml"), consts.NodeKeyInfo, consts.DSigNs)) - ki2 = xmlsec.template.encrypted_data_ensure_key_info(enc, id="Id", ns="test") - self.assertEqual("Id", ki2.get("Id")) - self.assertEqual("test", ki2.prefix) + self.assertEqual(ki, xmlsec.tree.find_node(self.load_xml('enc_template.xml'), consts.NodeKeyInfo, consts.DSigNs)) + ki2 = xmlsec.template.encrypted_data_ensure_key_info(enc, id='Id', ns='test') + self.assertEqual('Id', ki2.get('Id')) + self.assertEqual('test', ki2.prefix) def test_encrypted_data_ensure_key_info_bad_args(self): with self.assertRaises(TypeError): @@ -199,14 +199,14 @@ def test_encrypted_data_ensure_key_info_bad_args(self): @unittest.skipIf(not hasattr(consts, 'TransformXslt'), reason='XSLT transformations not enabled') def test_transform_add_c14n_inclusive_namespaces(self): - root = self.load_xml("doc.xml") + root = self.load_xml('doc.xml') sign = xmlsec.template.create(root, c14n_method=consts.TransformExclC14N, sign_method=consts.TransformRsaSha1) ref = xmlsec.template.add_reference(sign, consts.TransformSha1) trans1 = xmlsec.template.add_transform(ref, consts.TransformEnveloped) - xmlsec.template.transform_add_c14n_inclusive_namespaces(trans1, "default") + xmlsec.template.transform_add_c14n_inclusive_namespaces(trans1, 'default') trans2 = xmlsec.template.add_transform(ref, consts.TransformXslt) - xmlsec.template.transform_add_c14n_inclusive_namespaces(trans2, ["ns1", "ns2"]) - self.assertEqual(ref, xmlsec.tree.find_node(self.load_xml("sign_template.xml"), consts.NodeReference, consts.DSigNs)) + xmlsec.template.transform_add_c14n_inclusive_namespaces(trans2, ['ns1', 'ns2']) + self.assertEqual(ref, xmlsec.tree.find_node(self.load_xml('sign_template.xml'), consts.NodeReference, consts.DSigNs)) def test_transform_add_c14n_inclusive_namespaces_bad_args(self): with self.assertRaises(TypeError): diff --git a/tests/test_tree.py b/tests/test_tree.py index 4c79c8de..5e80a60a 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -6,7 +6,7 @@ class TestTree(base.TestMemoryLeaks): def test_find_child(self): - root = self.load_xml("sign_template.xml") + root = self.load_xml('sign_template.xml') si = xmlsec.tree.find_child(root, consts.NodeSignedInfo, consts.DSigNs) self.assertEqual(consts.NodeSignedInfo, si.tag.partition('}')[2]) self.assertIsNone(xmlsec.tree.find_child(root, consts.NodeReference)) @@ -17,7 +17,7 @@ def test_find_child_bad_args(self): xmlsec.tree.find_child('', 0, True) def test_find_parent(self): - root = self.load_xml("sign_template.xml") + root = self.load_xml('sign_template.xml') si = xmlsec.tree.find_child(root, consts.NodeSignedInfo, consts.DSigNs) self.assertIs(root, xmlsec.tree.find_parent(si, consts.NodeSignature)) self.assertIsNone(xmlsec.tree.find_parent(root, consts.NodeSignedInfo)) @@ -27,7 +27,7 @@ def test_find_parent_bad_args(self): xmlsec.tree.find_parent('', 0, True) def test_find_node(self): - root = self.load_xml("sign_template.xml") + root = self.load_xml('sign_template.xml') ref = xmlsec.tree.find_node(root, consts.NodeReference) self.assertEqual(consts.NodeReference, ref.tag.partition('}')[2]) self.assertIsNone(xmlsec.tree.find_node(root, consts.NodeReference, consts.EncNs)) @@ -37,8 +37,8 @@ def test_find_node_bad_args(self): xmlsec.tree.find_node('', 0, True) def test_add_ids(self): - root = self.load_xml("sign_template.xml") - xmlsec.tree.add_ids(root, ["id1", "id2", "id3"]) + root = self.load_xml('sign_template.xml') + xmlsec.tree.add_ids(root, ['id1', 'id2', 'id3']) def test_add_ids_bad_args(self): with self.assertRaises(TypeError): diff --git a/tests/test_type_stubs.py b/tests/test_type_stubs.py index 9ed8f1e2..82f7df7f 100644 --- a/tests/test_type_stubs.py +++ b/tests/test_type_stubs.py @@ -40,8 +40,7 @@ class __TransformNoHref(NamedTuple): # __Transform type def gen_constants_stub(): - """ - Generate contents of the file:`xmlsec/constants.pyi`. + """Generate contents of the file:`xmlsec/constants.pyi`. Simply load all constants at runtime, generate appropriate type hint for each constant type. @@ -53,7 +52,7 @@ def process_constant(name): type_name = type(obj).__name__ if type_name in ('__KeyData', '__Transform') and obj.href is None: type_name += 'NoHref' - return '{name}: Final[{type_name}]'.format(name=name, type_name=type_name) + return f'{name}: Final[{type_name}]' names = list(sorted(name for name in dir(xmlsec.constants) if not name.startswith('__'))) lines = [process_constant(name) for name in names] @@ -61,8 +60,7 @@ def process_constant(name): def test_xmlsec_constants_stub(request): - """ - Generate the stub file for :mod:`xmlsec.constants` from existing code. + """Generate the stub file for :mod:`xmlsec.constants` from existing code. Compare it against the existing stub :file:`xmlsec/constants.pyi`. """ diff --git a/tests/test_xmlsec.py b/tests/test_xmlsec.py index 303d7f8f..52dce2b3 100644 --- a/tests/test_xmlsec.py +++ b/tests/test_xmlsec.py @@ -4,8 +4,7 @@ class TestModule(base.TestMemoryLeaks): def test_reinitialize_module(self): - """ - This test doesn't explicitly verify anything, but will be invoked first in the suite. + """This test doesn't explicitly verify anything, but will be invoked first in the suite. So if the subsequent tests don't fail, we know that the ``init()``/``shutdown()`` function pair doesn't break anything. From a89f8043c1b54e82b0ae38fcbfa237a6358e0dac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:19:19 +0200 Subject: [PATCH 20/46] [pre-commit.ci] pre-commit autoupdate (#360) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.3 → v0.12.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.3...v0.12.4) - [github.com/pre-commit/mirrors-mypy: v1.16.1 → v1.17.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.16.1...v1.17.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48abcc6f..315bd1ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.3 + rev: v0.12.4 hooks: - id: ruff args: ["--fix"] @@ -28,7 +28,7 @@ repos: args: [--autofix] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.1 + rev: v1.17.0 hooks: - id: mypy exclude: (setup.py|tests/.*.py|doc/.*) From b2e4bd6be9b509a5f656a544199e8ca88b65106b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:07:58 +0200 Subject: [PATCH 21/46] [pre-commit.ci] pre-commit autoupdate (#363) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.4 → v0.12.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.4...v0.12.5) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 315bd1ad..0f038b24 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.4 + rev: v0.12.5 hooks: - id: ruff args: ["--fix"] From adb329ee06b08f270209687650531f70a121ff1f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:34:47 +0200 Subject: [PATCH 22/46] [pre-commit.ci] pre-commit autoupdate (#365) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f038b24..0e6d0afc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.5 + rev: v0.12.7 hooks: - id: ruff args: ["--fix"] @@ -28,7 +28,7 @@ repos: args: [--autofix] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.0 + rev: v1.17.1 hooks: - id: mypy exclude: (setup.py|tests/.*.py|doc/.*) From b70c0db61988c70c196dbc6bd719be4b92a2b4d1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:15:09 +0200 Subject: [PATCH 23/46] [pre-commit.ci] pre-commit autoupdate (#370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.7 → v0.12.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.7...v0.12.8) - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e6d0afc..14395966 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.12.8 hooks: - id: ruff args: ["--fix"] @@ -11,7 +11,7 @@ repos: types: [python] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: no-commit-to-branch - id: trailing-whitespace From 511d97b4fe99d81ab9cfdeff8405f1413d2f755c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:34:56 +0200 Subject: [PATCH 24/46] [pre-commit.ci] pre-commit autoupdate (#372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.8 → v0.12.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.8...v0.12.9) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14395966..ca70eb16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.8 + rev: v0.12.9 hooks: - id: ruff args: ["--fix"] From d7cc117147711eb33464cf35eddce1b4bb65cc4d Mon Sep 17 00:00:00 2001 From: Paul Zakin Date: Wed, 20 Aug 2025 04:25:25 -0400 Subject: [PATCH 25/46] Update Alpine linux installation document (#373) --- README.md | 2 +- doc/source/install.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6abd6a5a..60bde880 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ port install libxml2 xmlsec pkgconfig ### Alpine ``` bash -apk add build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec +apk add build-base openssl libffi-dev openssl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec ``` ## Troubleshooting diff --git a/doc/source/install.rst b/doc/source/install.rst index 834b9acb..c892a3ea 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -59,7 +59,7 @@ Alpine .. code-block:: bash - apk add build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec + apk add build-base openssl libffi-dev openssl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec Troubleshooting From 71ad25b101556de2824f98b9a63350a7c7568c88 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:07:18 +0200 Subject: [PATCH 26/46] [pre-commit.ci] pre-commit autoupdate (#374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.9 → v0.12.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.9...v0.12.10) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ca70eb16..deef08b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.9 + rev: v0.12.10 hooks: - id: ruff args: ["--fix"] From 5f82f6b4e7b9468ebc4fb7225fc6dcbaadcb3dce Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Mon, 1 Sep 2025 14:28:51 +0200 Subject: [PATCH 27/46] Bump libxml2 version to 2.14.5 for building wheels (#375) The goal is to keep it sync with lxml builds which in version 6.0.1 is using libxml2-2.14.5. --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 62d48b4b..f29019f0 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -110,7 +110,7 @@ jobs: include: ${{ fromJson(needs.generate-wheels-matrix.outputs.include) }} env: - PYXMLSEC_LIBXML2_VERSION: 2.14.4 + PYXMLSEC_LIBXML2_VERSION: 2.14.5 PYXMLSEC_LIBXSLT_VERSION: 1.1.43 steps: From 3933936ece71dc82bfdfa4ab35d80869e7af2f57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:39:28 +0200 Subject: [PATCH 28/46] [pre-commit.ci] pre-commit autoupdate (#376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.10 → v0.12.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.10...v0.12.11) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index deef08b2..46ee7c85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.10 + rev: v0.12.11 hooks: - id: ruff args: ["--fix"] From 699aa1e63c883adaa54c2e795446c529ec516ca7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:56:47 +0200 Subject: [PATCH 29/46] [pre-commit.ci] pre-commit autoupdate (#377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.11 → v0.12.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.11...v0.12.12) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46ee7c85..11bf9cc6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.11 + rev: v0.12.12 hooks: - id: ruff args: ["--fix"] From 5216e038615fc02466a51427a7f9f132e96bd884 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Fri, 12 Sep 2025 09:29:54 +0200 Subject: [PATCH 30/46] Add missing typing for id and ns parameters of template.create function (#378) --- src/xmlsec/template.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xmlsec/template.pyi b/src/xmlsec/template.pyi index a5199fc8..d1755fa2 100644 --- a/src/xmlsec/template.pyi +++ b/src/xmlsec/template.pyi @@ -15,7 +15,7 @@ def add_reference( ) -> _Element: ... def add_transform(node: _Element, transform: Transform) -> Any: ... def add_x509_data(node: _Element) -> _Element: ... -def create(node: _Element, c14n_method: Transform, sign_method: Transform) -> _Element: ... +def create(node: _Element, c14n_method: Transform, sign_method: Transform, id: str | None = ..., ns: str | None = ...) -> _Element: ... def encrypted_data_create( node: _Element, method: Transform, From 869e853d2396e1456bfc079d321ad38adadc71cd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 08:31:09 +0200 Subject: [PATCH 31/46] [pre-commit.ci] pre-commit autoupdate (#379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.12 → v0.13.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.12...v0.13.0) - [github.com/pre-commit/mirrors-mypy: v1.17.1 → v1.18.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.1...v1.18.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11bf9cc6..6403b4cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.12 + rev: v0.13.0 hooks: - id: ruff args: ["--fix"] @@ -28,7 +28,7 @@ repos: args: [--autofix] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v1.18.1 hooks: - id: mypy exclude: (setup.py|tests/.*.py|doc/.*) From 2075d146e849847257893958eb5a30cd1943f0b1 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Mon, 22 Sep 2025 21:09:30 +0200 Subject: [PATCH 32/46] Lock libxml2 to v2.14.6 in manylinux workflow (#380) It seems that xmlsec library build fails with libxml2 v2.15.0. Let's lock it for now until the issue is resolved in the main repository. --- .github/workflows/manylinux.yml | 1 + tests/softhsm_setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml index 357e93eb..550a8b34 100644 --- a/.github/workflows/manylinux.yml +++ b/.github/workflows/manylinux.yml @@ -32,6 +32,7 @@ jobs: - name: Build linux_x86_64 wheel env: PYXMLSEC_STATIC_DEPS: true + PYXMLSEC_LIBXML2_VERSION: 2.14.6 # Lock it to libxml2 2.14.6 until the issue with 2.15.x is resolved; e.g. https://github.com/lsh123/xmlsec/issues/948 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | /opt/python/${{ matrix.python-abi }}/bin/python -m build diff --git a/tests/softhsm_setup.py b/tests/softhsm_setup.py index 3f5076d2..d55c16fd 100644 --- a/tests/softhsm_setup.py +++ b/tests/softhsm_setup.py @@ -140,7 +140,7 @@ def setup() -> None: ) logging.debug('Initializing the token') - out, err = run_cmd( + _, _ = run_cmd( [ component_path['SOFTHSM'], '--slot', From 6786271f4b1d580194dd6f6121e0f4f0fbb5c3df Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 21:09:50 +0200 Subject: [PATCH 33/46] [pre-commit.ci] pre-commit autoupdate (#381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.0 → v0.13.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.0...v0.13.1) - [github.com/pre-commit/mirrors-mypy: v1.18.1 → v1.18.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.18.1...v1.18.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6403b4cc..5e38393e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.0 + rev: v0.13.1 hooks: - id: ruff args: ["--fix"] @@ -28,7 +28,7 @@ repos: args: [--autofix] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.1 + rev: v1.18.2 hooks: - id: mypy exclude: (setup.py|tests/.*.py|doc/.*) From e24a84c1503e5ca250f163a71bc366551d370604 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:16:38 +0200 Subject: [PATCH 34/46] [pre-commit.ci] pre-commit autoupdate (#382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.1 → v0.13.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.1...v0.13.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e38393e..2a20f7cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.1 + rev: v0.13.2 hooks: - id: ruff args: ["--fix"] From 11beda8c3fddf680c8d0e315bb20cbf4d2edfd0e Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Mon, 6 Oct 2025 21:13:49 +0200 Subject: [PATCH 35/46] Replace urlretrieve with streaming download using urlopen (#384) Updated setup.py to replace urllib.request.urlretrieve with a streaming download helper based on urlopen. This avoids the drawbacks of urlretrieve, such as lack of streaming support and limited control over requests. Using urlopen allows downloads to be written in chunks, reduces memory usage, and makes it easier to extend the logic with custom headers, retries, or checksum verification in the future. This improves the reliability and security of fetching dependency tarballs during the build process. --- .github/workflows/manylinux.yml | 2 +- setup.py | 30 ++++++++++++++++++++---------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml index 550a8b34..04645e84 100644 --- a/.github/workflows/manylinux.yml +++ b/.github/workflows/manylinux.yml @@ -32,7 +32,7 @@ jobs: - name: Build linux_x86_64 wheel env: PYXMLSEC_STATIC_DEPS: true - PYXMLSEC_LIBXML2_VERSION: 2.14.6 # Lock it to libxml2 2.14.6 until the issue with 2.15.x is resolved; e.g. https://github.com/lsh123/xmlsec/issues/948 + PYXMLSEC_LIBXML2_VERSION: 2.14.6 # Lock it to libxml2 2.14.6 to match it with lxml GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | /opt/python/${{ matrix.python-abi }}/bin/python -m build diff --git a/setup.py b/setup.py index 94b49aa8..014476ca 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ from distutils.version import StrictVersion as Version from pathlib import Path from urllib.parse import urljoin -from urllib.request import Request, urlcleanup, urlopen, urlretrieve +from urllib.request import Request, urlcleanup, urlopen from setuptools import Extension, setup from setuptools.command.build_ext import build_ext as build_ext_orig @@ -106,6 +106,16 @@ def latest_xmlsec_release(): return tar_gz['browser_download_url'] +def download_lib(url, filename): + req = Request(url, headers={'User-Agent': 'python-xmlsec build'}) + with urlopen(req) as r, open(filename, 'wb') as f: + while True: + chunk = r.read(8192) + if not chunk: + break + f.write(chunk) + + class CrossCompileInfo: def __init__(self, host, arch, compiler): self.host = host @@ -234,7 +244,7 @@ def prepare_static_build_win(self): else: self.info(f'Retrieving "{url}" to "{destfile}"') urlcleanup() # work around FTP bug 27973 in Py2.7.12+ - urlretrieve(url, str(destfile)) + download_lib(url, str(destfile)) for p in self.libs_dir.glob('*.zip'): with zipfile.ZipFile(str(p)) as f: @@ -297,7 +307,7 @@ def prepare_static_build(self, build_platform): else: url = f'https://api.github.com/repos/openssl/openssl/tarball/openssl-{self.openssl_version}' self.info('{:10}: {} {}'.format('OpenSSL', 'version', self.openssl_version)) - urlretrieve(url, str(openssl_tar)) + download_lib(url, str(openssl_tar)) # fetch zlib zlib_tar = next(self.libs_dir.glob('zlib*.tar.gz'), None) @@ -310,7 +320,7 @@ def prepare_static_build(self, build_platform): else: url = f'https://zlib.net/fossils/zlib-{self.zlib_version}.tar.gz' self.info('{:10}: {}'.format('zlib', f'PYXMLSEC_ZLIB_VERSION={self.zlib_version}, downloading from {url}')) - urlretrieve(url, str(zlib_tar)) + download_lib(url, str(zlib_tar)) # fetch libiconv libiconv_tar = next(self.libs_dir.glob('libiconv*.tar.gz'), None) @@ -319,13 +329,13 @@ def prepare_static_build(self, build_platform): libiconv_tar = self.libs_dir / 'libiconv.tar.gz' if self.libiconv_version is None: url = latest_libiconv_release() - self.info('{:10}: {}'.format('zlib', f'PYXMLSEC_LIBICONV_VERSION unset, downloading latest from {url}')) + self.info('{:10}: {}'.format('libiconv', f'PYXMLSEC_LIBICONV_VERSION unset, downloading latest from {url}')) else: url = f'https://ftp.gnu.org/pub/gnu/libiconv/libiconv-{self.libiconv_version}.tar.gz' self.info( - '{:10}: {}'.format('zlib', f'PYXMLSEC_LIBICONV_VERSION={self.libiconv_version}, downloading from {url}') + '{:10}: {}'.format('libiconv', f'PYXMLSEC_LIBICONV_VERSION={self.libiconv_version}, downloading from {url}') ) - urlretrieve(url, str(libiconv_tar)) + download_lib(url, str(libiconv_tar)) # fetch libxml2 libxml2_tar = next(self.libs_dir.glob('libxml2*.tar.xz'), None) @@ -341,7 +351,7 @@ def prepare_static_build(self, build_platform): '{:10}: {}'.format('libxml2', f'PYXMLSEC_LIBXML2_VERSION={self.libxml2_version}, downloading from {url}') ) libxml2_tar = self.libs_dir / 'libxml2.tar.xz' - urlretrieve(url, str(libxml2_tar)) + download_lib(url, str(libxml2_tar)) # fetch libxslt libxslt_tar = next(self.libs_dir.glob('libxslt*.tar.gz'), None) @@ -357,7 +367,7 @@ def prepare_static_build(self, build_platform): '{:10}: {}'.format('libxslt', f'PYXMLSEC_LIBXSLT_VERSION={self.libxslt_version}, downloading from {url}') ) libxslt_tar = self.libs_dir / 'libxslt.tar.gz' - urlretrieve(url, str(libxslt_tar)) + download_lib(url, str(libxslt_tar)) # fetch xmlsec1 xmlsec1_tar = next(self.libs_dir.glob('xmlsec1*.tar.gz'), None) @@ -372,7 +382,7 @@ def prepare_static_build(self, build_platform): '{:10}: {}'.format('xmlsec1', f'PYXMLSEC_XMLSEC1_VERSION={self.xmlsec1_version}, downloading from {url}') ) xmlsec1_tar = self.libs_dir / 'xmlsec1.tar.gz' - urlretrieve(url, str(xmlsec1_tar)) + download_lib(url, str(xmlsec1_tar)) for file in (openssl_tar, zlib_tar, libiconv_tar, libxml2_tar, libxslt_tar, xmlsec1_tar): self.info(f'Unpacking {file.name}') From 8d51c090a633cc52c178e7e89781696be4669d7b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:47:10 +0200 Subject: [PATCH 36/46] [pre-commit.ci] pre-commit autoupdate (#385) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a20f7cb..bbb7e9eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.2 + rev: v0.13.3 hooks: - id: ruff args: ["--fix"] From e88d9ce1e2f7b757923c07c53658176c29b7ebb5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:27:37 +0200 Subject: [PATCH 37/46] [pre-commit.ci] pre-commit autoupdate (#386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.3 → v0.14.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.3...v0.14.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bbb7e9eb..94fa772b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.0 hooks: - id: ruff args: ["--fix"] From a241c56750f40cacd90090a58ed8277dc5f3254a Mon Sep 17 00:00:00 2001 From: ffgan Date: Thu, 16 Oct 2025 19:39:06 +0800 Subject: [PATCH 38/46] build riscv64 wheel (#387) --- .github/workflows/wheels.yml | 4 ++-- doc/source/requirements.txt | 2 +- pyproject.toml | 5 +++-- requirements-test.txt | 2 +- requirements.txt | 2 +- setup.py | 16 ++++++++++++++-- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index f29019f0..d9cad9ce 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -77,7 +77,7 @@ jobs: - uses: actions/checkout@v4 - name: Install cibuildwheel # Nb. keep cibuildwheel version pin consistent with job below - run: pipx install cibuildwheel==2.21.3 + run: pipx install cibuildwheel==3.1.4 - id: set-matrix # Once we have the windows build figured out, it can be added here # by updating the matrix to include windows builds as well. @@ -126,7 +126,7 @@ jobs: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.21.3 + uses: pypa/cibuildwheel@v3.1.4 with: only: ${{ matrix.only }} env: diff --git a/doc/source/requirements.txt b/doc/source/requirements.txt index 6b78694a..ffb2b6d3 100644 --- a/doc/source/requirements.txt +++ b/doc/source/requirements.txt @@ -1,4 +1,4 @@ -lxml>=3.8 +lxml==6.0.2 importlib_metadata;python_version < '3.8' packaging Sphinx>=3 diff --git a/pyproject.toml b/pyproject.toml index bb0c459e..2b2d2d04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4", "pkgconfig>=1.5.1", "lxml>=3.8, !=4.7.0"] +requires = ["setuptools==80.9.0", "wheel", "setuptools_scm[toml]>=3.4", "pkgconfig>=1.5.1", "lxml==6.0.2"] [tool.mypy] files = ['src'] @@ -100,6 +100,7 @@ build-verbosity = 1 build-frontend = "build" skip = [ "pp*", # Skips PyPy builds (pp38-*, pp39-*, etc.) + "*musllinux_riscv64" # maturin and ruff currently don’t support the musl + riscv64 target ] test-command = "pytest -v --color=yes {package}/tests" before-test = "pip install -r requirements-test.txt" @@ -109,7 +110,7 @@ test-skip = "*-macosx_arm64" PYXMLSEC_STATIC_DEPS = "true" [tool.cibuildwheel.linux] -archs = ["x86_64", "aarch64"] +archs = ["x86_64", "aarch64", "riscv64"] environment-pass = [ "PYXMLSEC_LIBXML2_VERSION", "PYXMLSEC_LIBXSLT_VERSION", diff --git a/requirements-test.txt b/requirements-test.txt index 52a31f00..ad135d97 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,4 +2,4 @@ pytest==8.4.1 lxml-stubs==0.5.1 -ruff[format]==0.12.3 +ruff[format]==0.13.0 diff --git a/requirements.txt b/requirements.txt index 7aa6718b..8221c374 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -lxml==6.0.0 +lxml==6.0.2 diff --git a/setup.py b/setup.py index 014476ca..624169c4 100644 --- a/setup.py +++ b/setup.py @@ -404,9 +404,8 @@ def prepare_static_build(self, build_platform): ldflags.append(env['LDFLAGS']) cross_compiling = False - if build_platform == 'darwin': - import platform + if build_platform == 'darwin': arch = self.plat_name.rsplit('-', 1)[1] if arch != platform.machine() and arch in ('x86_64', 'arm64'): self.info(f'Cross-compiling for {arch}') @@ -423,6 +422,19 @@ def prepare_static_build(self, build_platform): self.info('Building OpenSSL') openssl_dir = next(self.build_libs_dir.glob('openssl-*')) openssl_config_cmd = [prefix_arg, 'no-shared', '-fPIC', '--libdir=lib'] + if platform.machine() == 'riscv64': + # add `no-asm` flag for openssl while build for riscv64 + + # on openssl 3.5.2, When building for riscv64, ASM code is used by default. + # However, this version of the assembly code will report during the build: + + # relocation truncated to fit: R_RISCV_JAL against symbol `AES_set_encrypt_key' defined in .text section in /project/build/tmp/prefix/lib/libcrypto.a(libcrypto-lib-aes-riscv64.o) # noqa: E501 + + # This [line] (https://github.com/openssl/openssl/blob/0893a62353583343eb712adef6debdfbe597c227/crypto/aes/asm/aes-riscv64.pl#L1069) of code cannot be completed normally because the jump address of the static link library used by xmlsec is too large. # noqa: E501 + # may be we can consider filing a related issue with OpenSSL later. + # However, for now, for convenience, we can simply disable ASM for riscv64. + # This will result in some performance degradation, but this is acceptable. + openssl_config_cmd.append('no-asm') if cross_compiling: openssl_config_cmd.insert(0, './Configure') openssl_config_cmd.append(cross_compiling.triplet) From c8b1b6b611f0f7d3f710031f2a1597eb8c2ad4fa Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Fri, 17 Oct 2025 09:28:43 +0200 Subject: [PATCH 39/46] Refactor packaging build helpers into dedicated modules (#388) Split the old monolithic `setup.py` build logic into focused helper modules so that static builds are coordinated by dedicated utilities rather than one giant script. Additional changes: - Use fixed library versions - Use GNU mirror FTP for downloading related libraries --- .github/workflows/wheels.yml | 2 +- .pre-commit-config.yaml | 2 +- build_support/__init__.py | 0 build_support/build_ext.py | 86 +++++ build_support/network.py | 29 ++ build_support/releases.py | 77 +++++ build_support/static_build.py | 453 ++++++++++++++++++++++++++ setup.py | 587 +--------------------------------- 8 files changed, 651 insertions(+), 585 deletions(-) create mode 100644 build_support/__init__.py create mode 100644 build_support/build_ext.py create mode 100644 build_support/network.py create mode 100644 build_support/releases.py create mode 100644 build_support/static_build.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d9cad9ce..1d4564a6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -110,7 +110,7 @@ jobs: include: ${{ fromJson(needs.generate-wheels-matrix.outputs.include) }} env: - PYXMLSEC_LIBXML2_VERSION: 2.14.5 + PYXMLSEC_LIBXML2_VERSION: 2.14.6 PYXMLSEC_LIBXSLT_VERSION: 1.1.43 steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94fa772b..8fb6f59f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: rev: v1.18.2 hooks: - id: mypy - exclude: (setup.py|tests/.*.py|doc/.*) + exclude: (setup.py|tests|build_support/.*.py|doc/.*) types: [] files: ^.*.pyi?$ additional_dependencies: [lxml-stubs, types-docutils] diff --git a/build_support/__init__.py b/build_support/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/build_support/build_ext.py b/build_support/build_ext.py new file mode 100644 index 00000000..c4fb5bc9 --- /dev/null +++ b/build_support/build_ext.py @@ -0,0 +1,86 @@ +import os +import sys +from distutils import log +from distutils.errors import DistutilsError + +from setuptools.command.build_ext import build_ext as build_ext_orig + +from .static_build import CrossCompileInfo, StaticBuildHelper + + +class build_ext(build_ext_orig): + def info(self, message): + self.announce(message, level=log.INFO) + + def run(self): + ext = self.ext_map['xmlsec'] + self.debug = os.environ.get('PYXMLSEC_ENABLE_DEBUG', False) + self.static = os.environ.get('PYXMLSEC_STATIC_DEPS', False) + self.size_opt = os.environ.get('PYXMLSEC_OPTIMIZE_SIZE', True) + + if self.static or sys.platform == 'win32': + helper = StaticBuildHelper(self) + helper.prepare(sys.platform) + else: + import pkgconfig + + try: + config = pkgconfig.parse('xmlsec1') + except OSError as error: + raise DistutilsError('Unable to invoke pkg-config.') from error + except pkgconfig.PackageNotFoundError as error: + raise DistutilsError('xmlsec1 is not installed or not in path.') from error + + if config is None or not config.get('libraries'): + raise DistutilsError('Bad or incomplete result returned from pkg-config.') + + ext.define_macros.extend(config['define_macros']) + ext.include_dirs.extend(config['include_dirs']) + ext.library_dirs.extend(config['library_dirs']) + ext.libraries.extend(config['libraries']) + + import lxml + + ext.include_dirs.extend(lxml.get_include()) + + ext.define_macros.extend( + [('MODULE_NAME', self.distribution.metadata.name), ('MODULE_VERSION', self.distribution.metadata.version)] + ) + for key, value in ext.define_macros: + if key == 'XMLSEC_CRYPTO' and not (value.startswith('"') and value.endswith('"')): + ext.define_macros.remove((key, value)) + ext.define_macros.append((key, f'"{value}"')) + break + + if sys.platform == 'win32': + ext.extra_compile_args.append('/Zi') + else: + ext.extra_compile_args.extend( + [ + '-g', + '-std=c99', + '-fPIC', + '-fno-strict-aliasing', + '-Wno-error=declaration-after-statement', + '-Werror=implicit-function-declaration', + ] + ) + + if self.debug: + ext.define_macros.append(('PYXMLSEC_ENABLE_DEBUG', '1')) + if sys.platform == 'win32': + ext.extra_compile_args.append('/Od') + else: + ext.extra_compile_args.append('-Wall') + ext.extra_compile_args.append('-O0') + else: + if self.size_opt: + if sys.platform == 'win32': + ext.extra_compile_args.append('/Os') + else: + ext.extra_compile_args.append('-Os') + + super().run() + + +__all__ = ('CrossCompileInfo', 'build_ext') diff --git a/build_support/network.py b/build_support/network.py new file mode 100644 index 00000000..7ac0bb5e --- /dev/null +++ b/build_support/network.py @@ -0,0 +1,29 @@ +import contextlib +import json +from urllib.request import Request, urlopen + +DEFAULT_USER_AGENT = 'https://github.com/xmlsec/python-xmlsec' +DOWNLOAD_USER_AGENT = 'python-xmlsec build' + + +def make_request(url, github_token=None, json_response=False): + headers = {'User-Agent': DEFAULT_USER_AGENT} + if github_token: + headers['authorization'] = 'Bearer ' + github_token + request = Request(url, headers=headers) + with contextlib.closing(urlopen(request)) as response: + charset = response.headers.get_content_charset() or 'utf-8' + content = response.read().decode(charset) + if json_response: + return json.loads(content) + return content + + +def download_lib(url, filename): + request = Request(url, headers={'User-Agent': DOWNLOAD_USER_AGENT}) + with urlopen(request) as response, open(filename, 'wb') as target: + while True: + chunk = response.read(8192) + if not chunk: + break + target.write(chunk) diff --git a/build_support/releases.py b/build_support/releases.py new file mode 100644 index 00000000..089162e2 --- /dev/null +++ b/build_support/releases.py @@ -0,0 +1,77 @@ +import html.parser +import os +import re +from distutils import log +from distutils.version import StrictVersion as Version + +from .network import make_request + + +class HrefCollector(html.parser.HTMLParser): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.hrefs = [] + + def handle_starttag(self, tag, attrs): + if tag == 'a': + for name, value in attrs: + if name == 'href': + self.hrefs.append(value) + + +def latest_release_from_html(url, matcher): + content = make_request(url) + collector = HrefCollector() + collector.feed(content) + hrefs = collector.hrefs + + def comp(text): + try: + return Version(matcher.match(text).groupdict()['version']) + except (AttributeError, ValueError): + return Version('0.0') + + latest = max(hrefs, key=comp) + return f'{url}/{latest}' + + +def latest_release_from_gnome_org_cache(url, lib_name): + cache_url = f'{url}/cache.json' + cache = make_request(cache_url, json_response=True) + latest_version = cache[2][lib_name][-1] + latest_source = cache[1][lib_name][latest_version]['tar.xz'] + return f'{url}/{latest_source}' + + +def latest_release_json_from_github_api(repo): + api_url = f'https://api.github.com/repos/{repo}/releases/latest' + token = os.environ.get('GH_TOKEN') + if token: + log.info('Using GitHub token to avoid rate limiting') + return make_request(api_url, token, json_response=True) + + +def latest_openssl_release(): + return latest_release_json_from_github_api('openssl/openssl')['tarball_url'] + + +def latest_zlib_release(): + return latest_release_from_html('https://zlib.net/fossils', re.compile('zlib-(?P.*).tar.gz')) + + +def latest_libiconv_release(): + return latest_release_from_html('https://ftpmirror.gnu.org/libiconv', re.compile('libiconv-(?P.*).tar.gz')) + + +def latest_libxml2_release(): + return latest_release_from_gnome_org_cache('https://download.gnome.org/sources/libxml2', 'libxml2') + + +def latest_libxslt_release(): + return latest_release_from_gnome_org_cache('https://download.gnome.org/sources/libxslt', 'libxslt') + + +def latest_xmlsec_release(): + assets = latest_release_json_from_github_api('lsh123/xmlsec')['assets'] + (tar_gz,) = [asset for asset in assets if asset['name'].endswith('.tar.gz')] + return tar_gz['browser_download_url'] diff --git a/build_support/static_build.py b/build_support/static_build.py new file mode 100644 index 00000000..4a0b98e8 --- /dev/null +++ b/build_support/static_build.py @@ -0,0 +1,453 @@ +import multiprocessing +import os +import platform +import subprocess +import sys +import tarfile +import zipfile +from distutils.errors import DistutilsError +from pathlib import Path +from urllib.parse import urljoin +from urllib.request import urlcleanup + +from .network import download_lib +from .releases import ( + latest_libiconv_release, + latest_libxml2_release, + latest_libxslt_release, + latest_openssl_release, + latest_xmlsec_release, + latest_zlib_release, +) + + +class CrossCompileInfo: + def __init__(self, host, arch, compiler): + self.host = host + self.arch = arch + self.compiler = compiler + + @property + def triplet(self): + return f'{self.host}-{self.arch}-{self.compiler}' + + +class StaticBuildHelper: + def __init__(self, builder): + self.builder = builder + self.ext = builder.ext_map['xmlsec'] + self.info = builder.info + self._prepare_directories() + + def prepare(self, platform_name): + self.info(f'starting static build on {sys.platform}') + if platform_name == 'win32': + self._prepare_windows_build() + elif 'linux' in platform_name or 'darwin' in platform_name: + self._prepare_unix_build(platform_name) + else: + raise DistutilsError(f'Unsupported static build platform: {platform_name}') + + def _prepare_directories(self): + buildroot = Path('build', 'tmp') + + prefix_dir = buildroot / 'prefix' + prefix_dir.mkdir(parents=True, exist_ok=True) + self.prefix_dir = prefix_dir.absolute() + + build_libs_dir = buildroot / 'libs' + build_libs_dir.mkdir(exist_ok=True) + self.build_libs_dir = build_libs_dir + + libs_dir = Path(os.environ.get('PYXMLSEC_LIBS_DIR', 'libs')) + libs_dir.mkdir(exist_ok=True) + self.libs_dir = libs_dir + self.info('{:20} {}'.format('Lib sources in:', self.libs_dir.absolute())) + + self.builder.prefix_dir = self.prefix_dir + self.builder.build_libs_dir = self.build_libs_dir + self.builder.libs_dir = self.libs_dir + + def _prepare_windows_build(self): + release_url = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2025.07.10/' + if platform.machine() == 'ARM64': + suffix = 'win-arm64' + elif sys.maxsize > 2**32: + suffix = 'win64' + else: + suffix = 'win32' + + libs = [ + f'libxml2-2.11.9-3.{suffix}.zip', + f'libxslt-1.1.39.{suffix}.zip', + f'zlib-1.3.1.{suffix}.zip', + f'iconv-1.18-1.{suffix}.zip', + f'openssl-3.0.16.pl1.{suffix}.zip', + f'xmlsec-1.3.7.{suffix}.zip', + ] + + for libfile in libs: + url = urljoin(release_url, libfile) + destfile = self.libs_dir / libfile + if destfile.is_file(): + self.info(f'Using local copy of "{url}"') + else: + self.info(f'Retrieving "{url}" to "{destfile}"') + urlcleanup() + download_lib(url, str(destfile)) + + for package in self.libs_dir.glob('*.zip'): + with zipfile.ZipFile(str(package)) as archive: + destdir = self.build_libs_dir + archive.extractall(path=str(destdir)) + + self.ext.define_macros = [ + ('XMLSEC_CRYPTO', '\\"openssl\\"'), + ('__XMLSEC_FUNCTION__', '__FUNCTION__'), + ('XMLSEC_NO_GOST', '1'), + ('XMLSEC_NO_XKMS', '1'), + ('XMLSEC_NO_CRYPTO_DYNAMIC_LOADING', '1'), + ('XMLSEC_CRYPTO_OPENSSL', '1'), + ('UNICODE', '1'), + ('_UNICODE', '1'), + ('LIBXML_ICONV_ENABLED', 1), + ('LIBXML_STATIC', '1'), + ('LIBXSLT_STATIC', 1), + ('XMLSEC_STATIC', 1), + ('inline', '__inline'), + ] + self.ext.libraries = [ + 'libxmlsec_a', + 'libxmlsec-openssl_a', + 'libcrypto', + 'iconv_a', + 'libxslt_a', + 'libexslt_a', + 'libxml2_a', + 'zlib', + 'WS2_32', + 'Advapi32', + 'User32', + 'Gdi32', + 'Crypt32', + ] + self.ext.library_dirs = [str(path.absolute()) for path in self.build_libs_dir.rglob('lib')] + + includes = [path for path in self.build_libs_dir.rglob('include') if path.is_dir()] + includes.append(next(path / 'xmlsec' for path in includes if (path / 'xmlsec').is_dir())) + self.ext.include_dirs = [str(path.absolute()) for path in includes] + + def _prepare_unix_build(self, build_platform): + self._capture_version_overrides() + archives = self._ensure_source_archives() + self._extract_archives(archives) + + env, prefix_arg, ldflags, cross_compile = self._prepare_build_environment(build_platform) + self._build_dependencies(env, prefix_arg, ldflags, cross_compile) + self._configure_extension_for_static(build_platform) + + def _capture_version_overrides(self): + builder = self.builder + builder.openssl_version = os.environ.get('PYXMLSEC_OPENSSL_VERSION', '3.6.0') + builder.libiconv_version = os.environ.get('PYXMLSEC_LIBICONV_VERSION', '1.18') + builder.libxml2_version = os.environ.get('PYXMLSEC_LIBXML2_VERSION', '2.14.6') + builder.libxslt_version = os.environ.get('PYXMLSEC_LIBXSLT_VERSION', '1.1.43') + builder.zlib_version = os.environ.get('PYXMLSEC_ZLIB_VERSION', '1.3.1') + builder.xmlsec1_version = os.environ.get('PYXMLSEC_XMLSEC1_VERSION', '1.3.8') + + def _ensure_source_archives(self): + return [ + self._ensure_source( + name='OpenSSL', + glob='openssl*.tar.gz', + filename='openssl.tar.gz', + version=self.builder.openssl_version, + env_label='PYXMLSEC_OPENSSL_VERSION', + default_url=latest_openssl_release, + version_url=lambda v: f'https://api.github.com/repos/openssl/openssl/tarball/openssl-{v}', + ), + self._ensure_source( + name='zlib', + glob='zlib*.tar.gz', + filename='zlib.tar.gz', + version=self.builder.zlib_version, + env_label='PYXMLSEC_ZLIB_VERSION', + default_url=latest_zlib_release, + version_url=lambda v: f'https://zlib.net/fossils/zlib-{v}.tar.gz', + ), + self._ensure_source( + name='libiconv', + glob='libiconv*.tar.gz', + filename='libiconv.tar.gz', + version=self.builder.libiconv_version, + env_label='PYXMLSEC_LIBICONV_VERSION', + default_url=latest_libiconv_release, + version_url=lambda v: f'https://ftpmirror.gnu.org/libiconv/libiconv-{v}.tar.gz', + ), + self._ensure_source( + name='libxml2', + glob='libxml2*.tar.xz', + filename='libxml2.tar.xz', + version=self.builder.libxml2_version, + env_label='PYXMLSEC_LIBXML2_VERSION', + default_url=latest_libxml2_release, + version_url=lambda v: self._libxml_related_url('libxml2', v), + ), + self._ensure_source( + name='libxslt', + glob='libxslt*.tar.xz', + filename='libxslt.tar.xz', + version=self.builder.libxslt_version, + env_label='PYXMLSEC_LIBXSLT_VERSION', + default_url=latest_libxslt_release, + version_url=lambda v: self._libxml_related_url('libxslt', v), + ), + self._ensure_source( + name='xmlsec1', + glob='xmlsec1*.tar.gz', + filename='xmlsec1.tar.gz', + version=self.builder.xmlsec1_version, + env_label='PYXMLSEC_XMLSEC1_VERSION', + default_url=latest_xmlsec_release, + version_url=lambda v: f'https://github.com/lsh123/xmlsec/releases/download/{v}/xmlsec1-{v}.tar.gz', + ), + ] + + def _ensure_source(self, name, glob, filename, version, env_label, default_url, version_url): + archive = next(self.libs_dir.glob(glob), None) + if archive is not None: + return archive + + self.info('{:10}: {}'.format(name, 'source tar not found, downloading ...')) + archive = self.libs_dir / filename + if version is None: + url = default_url() + self.info('{:10}: {}'.format(name, f'{env_label} unset, downloading latest from {url}')) + else: + url = version_url(version) + self.info('{:10}: {}'.format(name, f'{env_label}={version}, downloading from {url}')) + download_lib(url, str(archive)) + return archive + + def _libxml_related_url(self, lib_name, version): + version_prefix, _ = version.rsplit('.', 1) + return f'https://download.gnome.org/sources/{lib_name}/{version_prefix}/{lib_name}-{version}.tar.xz' + + def _extract_archives(self, archives): + for archive in archives: + self.info(f'Unpacking {archive.name}') + try: + with tarfile.open(str(archive)) as tar: + tar.extractall(path=str(self.build_libs_dir)) + except EOFError as error: + raise DistutilsError(f'Bad {archive.name} downloaded; remove it and try again.') from error + + def _prepare_build_environment(self, build_platform): + prefix_arg = f'--prefix={self.prefix_dir}' + env = os.environ.copy() + + cflags = [] + if env.get('CFLAGS'): + cflags.append(env['CFLAGS']) + cflags.append('-fPIC') + + ldflags = [] + if env.get('LDFLAGS'): + ldflags.append(env['LDFLAGS']) + + cross_compile = None + if build_platform == 'darwin': + arch = self.builder.plat_name.rsplit('-', 1)[1] + if arch != platform.machine() and arch in ('x86_64', 'arm64'): + self.info(f'Cross-compiling for {arch}') + cflags.append(f'-arch {arch}') + ldflags.append(f'-arch {arch}') + cross_compile = CrossCompileInfo('darwin64', arch, 'cc') + major_version, _ = tuple(map(int, platform.mac_ver()[0].split('.')[:2])) + if major_version >= 11 and 'MACOSX_DEPLOYMENT_TARGET' not in env: + env['MACOSX_DEPLOYMENT_TARGET'] = '11.0' + + env['CFLAGS'] = ' '.join(cflags) + env['LDFLAGS'] = ' '.join(ldflags) + return env, prefix_arg, ldflags, cross_compile + + def _build_dependencies(self, env, prefix_arg, ldflags, cross_compile): + self._build_openssl(env, prefix_arg, cross_compile) + self._build_zlib(env, prefix_arg) + + host_arg = [f'--host={cross_compile.arch}'] if cross_compile else [] + self._build_libiconv(env, prefix_arg, host_arg) + self._build_libxml2(env, prefix_arg, host_arg) + self._build_libxslt(env, prefix_arg, host_arg) + + ldflags.append('-lpthread') + env['LDFLAGS'] = ' '.join(ldflags) + self._build_xmlsec1(env, prefix_arg, host_arg) + + def _build_openssl(self, env, prefix_arg, cross_compile): + self.info('Building OpenSSL') + openssl_dir = next(self.build_libs_dir.glob('openssl-*')) + openssl_config_cmd = [prefix_arg, 'no-shared', '-fPIC', '--libdir=lib'] + if platform.machine() == 'riscv64': + # openssl(riscv64): disable ASM to avoid R_RISCV_JAL relocation failure on 3.5.2 + # OpenSSL 3.5.2 enables RISC-V64 AES assembly by default. When we statically + # link libcrypto alongside xmlsec, the AES asm path triggers a link-time error: + # relocation truncated to fit: R_RISCV_JAL against symbol `AES_set_encrypt_key' + # in .../libcrypto.a(libcrypto-lib-aes-riscv64.o) + # This appears to stem from a long-range jump emitted by the AES asm generator + # (see aes-riscv64.pl around L1069), which can exceed the JAL reach when objects + # end up far apart in the final static link. + # As a pragmatic workaround, disable ASM on riscv64 (pass `no-asm`) so the + # portable C implementation is used. This unblocks the build at the cost of + # some crypto performance on riscv64 only. + # Refs: + # - https://github.com/openssl/openssl/blob/0893a62/crypto/aes/asm/aes-riscv64.pl#L1069 + openssl_config_cmd.append('no-asm') + if cross_compile: + openssl_config_cmd.insert(0, './Configure') + openssl_config_cmd.append(cross_compile.triplet) + else: + openssl_config_cmd.insert(0, './config') + subprocess.check_call(openssl_config_cmd, cwd=str(openssl_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(openssl_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install_sw'], cwd=str(openssl_dir), env=env) + + def _build_zlib(self, env, prefix_arg): + self.info('Building zlib') + zlib_dir = next(self.build_libs_dir.glob('zlib-*')) + subprocess.check_call(['./configure', prefix_arg], cwd=str(zlib_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(zlib_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(zlib_dir), env=env) + + def _build_libiconv(self, env, prefix_arg, host_arg): + self.info('Building libiconv') + libiconv_dir = next(self.build_libs_dir.glob('libiconv-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-dependency-tracking', + '--disable-shared', + *host_arg, + ], + cwd=str(libiconv_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libiconv_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libiconv_dir), env=env) + + def _build_libxml2(self, env, prefix_arg, host_arg): + self.info('Building LibXML2') + libxml2_dir = next(self.build_libs_dir.glob('libxml2-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-dependency-tracking', + '--disable-shared', + '--without-lzma', + '--without-python', + f'--with-iconv={self.prefix_dir}', + f'--with-zlib={self.prefix_dir}', + *host_arg, + ], + cwd=str(libxml2_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxml2_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxml2_dir), env=env) + + def _build_libxslt(self, env, prefix_arg, host_arg): + self.info('Building libxslt') + libxslt_dir = next(self.build_libs_dir.glob('libxslt-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-dependency-tracking', + '--disable-shared', + '--without-python', + '--without-crypto', + f'--with-libxml-prefix={self.prefix_dir}', + *host_arg, + ], + cwd=str(libxslt_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxslt_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxslt_dir), env=env) + + def _build_xmlsec1(self, env, prefix_arg, host_arg): + self.info('Building xmlsec1') + xmlsec1_dir = next(self.build_libs_dir.glob('xmlsec1-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-shared', + '--disable-gost', + '--enable-md5', + '--enable-ripemd160', + '--disable-crypto-dl', + '--enable-static=yes', + '--enable-shared=no', + '--enable-static-linking=yes', + '--with-default-crypto=openssl', + f'--with-openssl={self.prefix_dir}', + f'--with-libxml={self.prefix_dir}', + f'--with-libxslt={self.prefix_dir}', + *host_arg, + ], + cwd=str(xmlsec1_dir), + env=env, + ) + include_flags = [ + f'-I{self.prefix_dir / "include"}', + f'-I{self.prefix_dir / "include" / "libxml"}', + ] + subprocess.check_call( + ['make', f'-j{multiprocessing.cpu_count() + 1}', *include_flags], + cwd=str(xmlsec1_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(xmlsec1_dir), env=env) + + def _configure_extension_for_static(self, build_platform): + self.ext.define_macros = [ + ('__XMLSEC_FUNCTION__', '__func__'), + ('XMLSEC_NO_SIZE_T', None), + ('XMLSEC_NO_GOST', '1'), + ('XMLSEC_NO_GOST2012', '1'), + ('XMLSEC_NO_XKMS', '1'), + ('XMLSEC_CRYPTO', '\\"openssl\\"'), + ('XMLSEC_NO_CRYPTO_DYNAMIC_LOADING', '1'), + ('XMLSEC_CRYPTO_OPENSSL', '1'), + ('LIBXML_ICONV_ENABLED', 1), + ('LIBXML_STATIC', 1), + ('LIBXSLT_STATIC', 1), + ('XMLSEC_STATIC', 1), + ('inline', '__inline'), + ('UNICODE', '1'), + ('_UNICODE', '1'), + ] + + self.ext.include_dirs.append(str(self.prefix_dir / 'include')) + self.ext.include_dirs.extend([str(path.absolute()) for path in (self.prefix_dir / 'include').iterdir() if path.is_dir()]) + + self.ext.library_dirs = [] + if build_platform == 'linux': + self.ext.libraries = ['m', 'rt'] + extra_objects = [ + 'libxmlsec1.a', + 'libxslt.a', + 'libxml2.a', + 'libz.a', + 'libxmlsec1-openssl.a', + 'libcrypto.a', + 'libiconv.a', + 'libxmlsec1.a', + ] + self.ext.extra_objects = [str(self.prefix_dir / 'lib' / obj) for obj in extra_objects] + + +__all__ = ('CrossCompileInfo', 'StaticBuildHelper') diff --git a/setup.py b/setup.py index 624169c4..4b3d93f7 100644 --- a/setup.py +++ b/setup.py @@ -1,596 +1,17 @@ -import contextlib -import html.parser -import json -import multiprocessing -import os -import platform -import re -import subprocess -import sys -import tarfile -import zipfile -from distutils import log -from distutils.errors import DistutilsError -from distutils.version import StrictVersion as Version from pathlib import Path -from urllib.parse import urljoin -from urllib.request import Request, urlcleanup, urlopen from setuptools import Extension, setup -from setuptools.command.build_ext import build_ext as build_ext_orig - - -class HrefCollector(html.parser.HTMLParser): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.hrefs = [] - - def handle_starttag(self, tag, attrs): - if tag == 'a': - for name, value in attrs: - if name == 'href': - self.hrefs.append(value) - - -def make_request(url, github_token=None, json_response=False): - headers = {'User-Agent': 'https://github.com/xmlsec/python-xmlsec'} - if github_token: - headers['authorization'] = 'Bearer ' + github_token - request = Request(url, headers=headers) - with contextlib.closing(urlopen(request)) as r: - charset = r.headers.get_content_charset() or 'utf-8' - content = r.read().decode(charset) - if json_response: - return json.loads(content) - else: - return content - - -def latest_release_from_html(url, matcher): - content = make_request(url) - collector = HrefCollector() - collector.feed(content) - hrefs = collector.hrefs - - def comp(text): - try: - return Version(matcher.match(text).groupdict()['version']) - except (AttributeError, ValueError): - return Version('0.0') - - latest = max(hrefs, key=comp) - return f'{url}/{latest}' - - -def latest_release_from_gnome_org_cache(url, lib_name): - cache_url = f'{url}/cache.json' - cache = make_request(cache_url, json_response=True) - latest_version = cache[2][lib_name][-1] - latest_source = cache[1][lib_name][latest_version]['tar.xz'] - return f'{url}/{latest_source}' - - -def latest_release_json_from_github_api(repo): - api_url = f'https://api.github.com/repos/{repo}/releases/latest' - - # if we are running in CI, pass along the GH_TOKEN, so we don't get rate limited - token = os.environ.get('GH_TOKEN') - if token: - log.info('Using GitHub token to avoid rate limiting') - return make_request(api_url, token, json_response=True) - - -def latest_openssl_release(): - return latest_release_json_from_github_api('openssl/openssl')['tarball_url'] - - -def latest_zlib_release(): - return latest_release_from_html('https://zlib.net/fossils', re.compile('zlib-(?P.*).tar.gz')) - - -def latest_libiconv_release(): - return latest_release_from_html('https://ftp.gnu.org/pub/gnu/libiconv', re.compile('libiconv-(?P.*).tar.gz')) - - -def latest_libxml2_release(): - return latest_release_from_gnome_org_cache('https://download.gnome.org/sources/libxml2', 'libxml2') - - -def latest_libxslt_release(): - return latest_release_from_gnome_org_cache('https://download.gnome.org/sources/libxslt', 'libxslt') - - -def latest_xmlsec_release(): - assets = latest_release_json_from_github_api('lsh123/xmlsec')['assets'] - (tar_gz,) = [asset for asset in assets if asset['name'].endswith('.tar.gz')] - return tar_gz['browser_download_url'] - - -def download_lib(url, filename): - req = Request(url, headers={'User-Agent': 'python-xmlsec build'}) - with urlopen(req) as r, open(filename, 'wb') as f: - while True: - chunk = r.read(8192) - if not chunk: - break - f.write(chunk) - - -class CrossCompileInfo: - def __init__(self, host, arch, compiler): - self.host = host - self.arch = arch - self.compiler = compiler - - @property - def triplet(self): - return f'{self.host}-{self.arch}-{self.compiler}' - - -class build_ext(build_ext_orig): - def info(self, message): - self.announce(message, level=log.INFO) - - def run(self): - ext = self.ext_map['xmlsec'] - self.debug = os.environ.get('PYXMLSEC_ENABLE_DEBUG', False) - self.static = os.environ.get('PYXMLSEC_STATIC_DEPS', False) - self.size_opt = os.environ.get('PYXMLSEC_OPTIMIZE_SIZE', True) - - if self.static or sys.platform == 'win32': - self.info(f'starting static build on {sys.platform}') - buildroot = Path('build', 'tmp') - - self.prefix_dir = buildroot / 'prefix' - self.prefix_dir.mkdir(parents=True, exist_ok=True) - self.prefix_dir = self.prefix_dir.absolute() - - self.build_libs_dir = buildroot / 'libs' - self.build_libs_dir.mkdir(exist_ok=True) - - self.libs_dir = Path(os.environ.get('PYXMLSEC_LIBS_DIR', 'libs')) - self.libs_dir.mkdir(exist_ok=True) - self.info('{:20} {}'.format('Lib sources in:', self.libs_dir.absolute())) - - if sys.platform == 'win32': - self.prepare_static_build_win() - elif 'linux' in sys.platform or 'darwin' in sys.platform: - self.prepare_static_build(sys.platform) - else: - import pkgconfig - - try: - config = pkgconfig.parse('xmlsec1') - except OSError as e: - raise DistutilsError('Unable to invoke pkg-config.') from e - except pkgconfig.PackageNotFoundError as e: - raise DistutilsError('xmlsec1 is not installed or not in path.') from e - - if config is None or not config.get('libraries'): - raise DistutilsError('Bad or incomplete result returned from pkg-config.') - - ext.define_macros.extend(config['define_macros']) - ext.include_dirs.extend(config['include_dirs']) - ext.library_dirs.extend(config['library_dirs']) - ext.libraries.extend(config['libraries']) - - import lxml - - ext.include_dirs.extend(lxml.get_include()) - - ext.define_macros.extend( - [('MODULE_NAME', self.distribution.metadata.name), ('MODULE_VERSION', self.distribution.metadata.version)] - ) - # escape the XMLSEC_CRYPTO macro value, see mehcode/python-xmlsec#141 - for key, value in ext.define_macros: - if key == 'XMLSEC_CRYPTO' and not (value.startswith('"') and value.endswith('"')): - ext.define_macros.remove((key, value)) - ext.define_macros.append((key, f'"{value}"')) - break - - if sys.platform == 'win32': - ext.extra_compile_args.append('/Zi') - else: - ext.extra_compile_args.extend( - [ - '-g', - '-std=c99', - '-fPIC', - '-fno-strict-aliasing', - '-Wno-error=declaration-after-statement', - '-Werror=implicit-function-declaration', - ] - ) - - if self.debug: - ext.define_macros.append(('PYXMLSEC_ENABLE_DEBUG', '1')) - if sys.platform == 'win32': - ext.extra_compile_args.append('/Od') - else: - ext.extra_compile_args.append('-Wall') - ext.extra_compile_args.append('-O0') - else: - if self.size_opt: - if sys.platform == 'win32': - ext.extra_compile_args.append('/Os') - else: - ext.extra_compile_args.append('-Os') - - super().run() - - def prepare_static_build_win(self): - release_url = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2025.07.10/' - if platform.machine() == 'ARM64': - suffix = 'win-arm64' - elif sys.maxsize > 2**32: # 2.0 GiB - suffix = 'win64' - else: - suffix = 'win32' - - libs = [ - f'libxml2-2.11.9-3.{suffix}.zip', - f'libxslt-1.1.39.{suffix}.zip', - f'zlib-1.3.1.{suffix}.zip', - f'iconv-1.18-1.{suffix}.zip', - f'openssl-3.0.16.pl1.{suffix}.zip', - f'xmlsec-1.3.7.{suffix}.zip', - ] - - for libfile in libs: - url = urljoin(release_url, libfile) - destfile = self.libs_dir / libfile - if destfile.is_file(): - self.info(f'Using local copy of "{url}"') - else: - self.info(f'Retrieving "{url}" to "{destfile}"') - urlcleanup() # work around FTP bug 27973 in Py2.7.12+ - download_lib(url, str(destfile)) - - for p in self.libs_dir.glob('*.zip'): - with zipfile.ZipFile(str(p)) as f: - destdir = self.build_libs_dir - f.extractall(path=str(destdir)) - - ext = self.ext_map['xmlsec'] - ext.define_macros = [ - ('XMLSEC_CRYPTO', '\\"openssl\\"'), - ('__XMLSEC_FUNCTION__', '__FUNCTION__'), - ('XMLSEC_NO_GOST', '1'), - ('XMLSEC_NO_XKMS', '1'), - ('XMLSEC_NO_CRYPTO_DYNAMIC_LOADING', '1'), - ('XMLSEC_CRYPTO_OPENSSL', '1'), - ('UNICODE', '1'), - ('_UNICODE', '1'), - ('LIBXML_ICONV_ENABLED', 1), - ('LIBXML_STATIC', '1'), - ('LIBXSLT_STATIC', '1'), - ('XMLSEC_STATIC', '1'), - ('inline', '__inline'), - ] - ext.libraries = [ - 'libxmlsec_a', - 'libxmlsec-openssl_a', - 'libcrypto', - 'iconv_a', - 'libxslt_a', - 'libexslt_a', - 'libxml2_a', - 'zlib', - 'WS2_32', - 'Advapi32', - 'User32', - 'Gdi32', - 'Crypt32', - ] - ext.library_dirs = [str(p.absolute()) for p in self.build_libs_dir.rglob('lib')] - - includes = [p for p in self.build_libs_dir.rglob('include') if p.is_dir()] - includes.append(next(p / 'xmlsec' for p in includes if (p / 'xmlsec').is_dir())) - ext.include_dirs = [str(p.absolute()) for p in includes] - - def prepare_static_build(self, build_platform): - self.openssl_version = os.environ.get('PYXMLSEC_OPENSSL_VERSION') - self.libiconv_version = os.environ.get('PYXMLSEC_LIBICONV_VERSION') - self.libxml2_version = os.environ.get('PYXMLSEC_LIBXML2_VERSION') - self.libxslt_version = os.environ.get('PYXMLSEC_LIBXSLT_VERSION') - self.zlib_version = os.environ.get('PYXMLSEC_ZLIB_VERSION') - self.xmlsec1_version = os.environ.get('PYXMLSEC_XMLSEC1_VERSION') - - # fetch openssl - openssl_tar = next(self.libs_dir.glob('openssl*.tar.gz'), None) - if openssl_tar is None: - self.info('{:10}: {}'.format('OpenSSL', 'source tar not found, downloading ...')) - openssl_tar = self.libs_dir / 'openssl.tar.gz' - if self.openssl_version is None: - url = latest_openssl_release() - self.info('{:10}: {}'.format('OpenSSL', f'PYXMLSEC_OPENSSL_VERSION unset, downloading latest from {url}')) - else: - url = f'https://api.github.com/repos/openssl/openssl/tarball/openssl-{self.openssl_version}' - self.info('{:10}: {} {}'.format('OpenSSL', 'version', self.openssl_version)) - download_lib(url, str(openssl_tar)) - - # fetch zlib - zlib_tar = next(self.libs_dir.glob('zlib*.tar.gz'), None) - if zlib_tar is None: - self.info('{:10}: {}'.format('zlib', 'source not found, downloading ...')) - zlib_tar = self.libs_dir / 'zlib.tar.gz' - if self.zlib_version is None: - url = latest_zlib_release() - self.info('{:10}: {}'.format('zlib', f'PYXMLSEC_ZLIB_VERSION unset, downloading latest from {url}')) - else: - url = f'https://zlib.net/fossils/zlib-{self.zlib_version}.tar.gz' - self.info('{:10}: {}'.format('zlib', f'PYXMLSEC_ZLIB_VERSION={self.zlib_version}, downloading from {url}')) - download_lib(url, str(zlib_tar)) - - # fetch libiconv - libiconv_tar = next(self.libs_dir.glob('libiconv*.tar.gz'), None) - if libiconv_tar is None: - self.info('{:10}: {}'.format('libiconv', 'source not found, downloading ...')) - libiconv_tar = self.libs_dir / 'libiconv.tar.gz' - if self.libiconv_version is None: - url = latest_libiconv_release() - self.info('{:10}: {}'.format('libiconv', f'PYXMLSEC_LIBICONV_VERSION unset, downloading latest from {url}')) - else: - url = f'https://ftp.gnu.org/pub/gnu/libiconv/libiconv-{self.libiconv_version}.tar.gz' - self.info( - '{:10}: {}'.format('libiconv', f'PYXMLSEC_LIBICONV_VERSION={self.libiconv_version}, downloading from {url}') - ) - download_lib(url, str(libiconv_tar)) - - # fetch libxml2 - libxml2_tar = next(self.libs_dir.glob('libxml2*.tar.xz'), None) - if libxml2_tar is None: - self.info('{:10}: {}'.format('libxml2', 'source tar not found, downloading ...')) - if self.libxml2_version is None: - url = latest_libxml2_release() - self.info('{:10}: {}'.format('libxml2', f'PYXMLSEC_LIBXML2_VERSION unset, downloading latest from {url}')) - else: - version_prefix, _ = self.libxml2_version.rsplit('.', 1) - url = f'https://download.gnome.org/sources/libxml2/{version_prefix}/libxml2-{self.libxml2_version}.tar.xz' - self.info( - '{:10}: {}'.format('libxml2', f'PYXMLSEC_LIBXML2_VERSION={self.libxml2_version}, downloading from {url}') - ) - libxml2_tar = self.libs_dir / 'libxml2.tar.xz' - download_lib(url, str(libxml2_tar)) - - # fetch libxslt - libxslt_tar = next(self.libs_dir.glob('libxslt*.tar.gz'), None) - if libxslt_tar is None: - self.info('{:10}: {}'.format('libxslt', 'source tar not found, downloading ...')) - if self.libxslt_version is None: - url = latest_libxslt_release() - self.info('{:10}: {}'.format('libxslt', f'PYXMLSEC_LIBXSLT_VERSION unset, downloading latest from {url}')) - else: - version_prefix, _ = self.libxslt_version.rsplit('.', 1) - url = f'https://download.gnome.org/sources/libxslt/{version_prefix}/libxslt-{self.libxslt_version}.tar.xz' - self.info( - '{:10}: {}'.format('libxslt', f'PYXMLSEC_LIBXSLT_VERSION={self.libxslt_version}, downloading from {url}') - ) - libxslt_tar = self.libs_dir / 'libxslt.tar.gz' - download_lib(url, str(libxslt_tar)) - - # fetch xmlsec1 - xmlsec1_tar = next(self.libs_dir.glob('xmlsec1*.tar.gz'), None) - if xmlsec1_tar is None: - self.info('{:10}: {}'.format('xmlsec1', 'source tar not found, downloading ...')) - if self.xmlsec1_version is None: - url = latest_xmlsec_release() - self.info('{:10}: {}'.format('xmlsec1', f'PYXMLSEC_XMLSEC1_VERSION unset, downloading latest from {url}')) - else: - url = f'https://github.com/lsh123/xmlsec/releases/download/{self.xmlsec1_version}/xmlsec1-{self.xmlsec1_version}.tar.gz' - self.info( - '{:10}: {}'.format('xmlsec1', f'PYXMLSEC_XMLSEC1_VERSION={self.xmlsec1_version}, downloading from {url}') - ) - xmlsec1_tar = self.libs_dir / 'xmlsec1.tar.gz' - download_lib(url, str(xmlsec1_tar)) - - for file in (openssl_tar, zlib_tar, libiconv_tar, libxml2_tar, libxslt_tar, xmlsec1_tar): - self.info(f'Unpacking {file.name}') - try: - with tarfile.open(str(file)) as tar: - tar.extractall(path=str(self.build_libs_dir)) - except EOFError as e: - raise DistutilsError(f'Bad {file.name} downloaded; remove it and try again.') from e - - prefix_arg = f'--prefix={self.prefix_dir}' - - env = os.environ.copy() - cflags = [] - if env.get('CFLAGS'): - cflags.append(env['CFLAGS']) - cflags.append('-fPIC') - ldflags = [] - if env.get('LDFLAGS'): - ldflags.append(env['LDFLAGS']) - - cross_compiling = False - - if build_platform == 'darwin': - arch = self.plat_name.rsplit('-', 1)[1] - if arch != platform.machine() and arch in ('x86_64', 'arm64'): - self.info(f'Cross-compiling for {arch}') - cflags.append(f'-arch {arch}') - ldflags.append(f'-arch {arch}') - cross_compiling = CrossCompileInfo('darwin64', arch, 'cc') - major_version, _ = tuple(map(int, platform.mac_ver()[0].split('.')[:2])) - if major_version >= 11 and 'MACOSX_DEPLOYMENT_TARGET' not in env: - env['MACOSX_DEPLOYMENT_TARGET'] = '11.0' - - env['CFLAGS'] = ' '.join(cflags) - env['LDFLAGS'] = ' '.join(ldflags) - - self.info('Building OpenSSL') - openssl_dir = next(self.build_libs_dir.glob('openssl-*')) - openssl_config_cmd = [prefix_arg, 'no-shared', '-fPIC', '--libdir=lib'] - if platform.machine() == 'riscv64': - # add `no-asm` flag for openssl while build for riscv64 - - # on openssl 3.5.2, When building for riscv64, ASM code is used by default. - # However, this version of the assembly code will report during the build: - - # relocation truncated to fit: R_RISCV_JAL against symbol `AES_set_encrypt_key' defined in .text section in /project/build/tmp/prefix/lib/libcrypto.a(libcrypto-lib-aes-riscv64.o) # noqa: E501 - - # This [line] (https://github.com/openssl/openssl/blob/0893a62353583343eb712adef6debdfbe597c227/crypto/aes/asm/aes-riscv64.pl#L1069) of code cannot be completed normally because the jump address of the static link library used by xmlsec is too large. # noqa: E501 - # may be we can consider filing a related issue with OpenSSL later. - # However, for now, for convenience, we can simply disable ASM for riscv64. - # This will result in some performance degradation, but this is acceptable. - openssl_config_cmd.append('no-asm') - if cross_compiling: - openssl_config_cmd.insert(0, './Configure') - openssl_config_cmd.append(cross_compiling.triplet) - else: - openssl_config_cmd.insert(0, './config') - subprocess.check_call(openssl_config_cmd, cwd=str(openssl_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(openssl_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install_sw'], cwd=str(openssl_dir), env=env) - - self.info('Building zlib') - zlib_dir = next(self.build_libs_dir.glob('zlib-*')) - subprocess.check_call(['./configure', prefix_arg], cwd=str(zlib_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(zlib_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(zlib_dir), env=env) - - host_arg = [] - if cross_compiling: - host_arg = [f'--host={cross_compiling.arch}'] - - self.info('Building libiconv') - libiconv_dir = next(self.build_libs_dir.glob('libiconv-*')) - subprocess.check_call( - [ - './configure', - prefix_arg, - '--disable-dependency-tracking', - '--disable-shared', - *host_arg, - ], - cwd=str(libiconv_dir), - env=env, - ) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libiconv_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libiconv_dir), env=env) - - self.info('Building LibXML2') - libxml2_dir = next(self.build_libs_dir.glob('libxml2-*')) - subprocess.check_call( - [ - './configure', - prefix_arg, - '--disable-dependency-tracking', - '--disable-shared', - '--without-lzma', - '--without-python', - f'--with-iconv={self.prefix_dir}', - f'--with-zlib={self.prefix_dir}', - *host_arg, - ], - cwd=str(libxml2_dir), - env=env, - ) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxml2_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxml2_dir), env=env) - - self.info('Building libxslt') - libxslt_dir = next(self.build_libs_dir.glob('libxslt-*')) - subprocess.check_call( - [ - './configure', - prefix_arg, - '--disable-dependency-tracking', - '--disable-shared', - '--without-python', - '--without-crypto', - f'--with-libxml-prefix={self.prefix_dir}', - *host_arg, - ], - cwd=str(libxslt_dir), - env=env, - ) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxslt_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxslt_dir), env=env) - - self.info('Building xmlsec1') - ldflags.append('-lpthread') - env['LDFLAGS'] = ' '.join(ldflags) - xmlsec1_dir = next(self.build_libs_dir.glob('xmlsec1-*')) - subprocess.check_call( - [ - './configure', - prefix_arg, - '--disable-shared', - '--disable-gost', - '--enable-md5', - '--enable-ripemd160', - '--disable-crypto-dl', - '--enable-static=yes', - '--enable-shared=no', - '--enable-static-linking=yes', - '--with-default-crypto=openssl', - f'--with-openssl={self.prefix_dir}', - f'--with-libxml={self.prefix_dir}', - f'--with-libxslt={self.prefix_dir}', - *host_arg, - ], - cwd=str(xmlsec1_dir), - env=env, - ) - include_flags = [ - f'-I{self.prefix_dir / "include"}', - f'-I{self.prefix_dir / "include" / "libxml"}', - ] - subprocess.check_call( - ['make', f'-j{multiprocessing.cpu_count() + 1}', *include_flags], - cwd=str(xmlsec1_dir), - env=env, - ) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(xmlsec1_dir), env=env) - - ext = self.ext_map['xmlsec'] - ext.define_macros = [ - ('__XMLSEC_FUNCTION__', '__func__'), - ('XMLSEC_NO_SIZE_T', None), - ('XMLSEC_NO_GOST', '1'), - ('XMLSEC_NO_GOST2012', '1'), - ('XMLSEC_NO_XKMS', '1'), - ('XMLSEC_CRYPTO', '\\"openssl\\"'), - ('XMLSEC_NO_CRYPTO_DYNAMIC_LOADING', '1'), - ('XMLSEC_CRYPTO_OPENSSL', '1'), - ('LIBXML_ICONV_ENABLED', 1), - ('LIBXML_STATIC', '1'), - ('LIBXSLT_STATIC', '1'), - ('XMLSEC_STATIC', '1'), - ('inline', '__inline'), - ('UNICODE', '1'), - ('_UNICODE', '1'), - ] - - ext.include_dirs.append(str(self.prefix_dir / 'include')) - ext.include_dirs.extend([str(p.absolute()) for p in (self.prefix_dir / 'include').iterdir() if p.is_dir()]) - - ext.library_dirs = [] - if build_platform == 'linux': - ext.libraries = ['m', 'rt'] - extra_objects = [ - 'libxmlsec1.a', - 'libxslt.a', - 'libxml2.a', - 'libz.a', - 'libxmlsec1-openssl.a', - 'libcrypto.a', - 'libiconv.a', - 'libxmlsec1.a', - ] - ext.extra_objects = [str(self.prefix_dir / 'lib' / o) for o in extra_objects] +from build_support.build_ext import build_ext src_root = Path(__file__).parent / 'src' -sources = [str(p.absolute()) for p in src_root.rglob('*.c')] +sources = [str(path.absolute()) for path in src_root.rglob('*.c')] pyxmlsec = Extension('xmlsec', sources=sources) setup_reqs = ['setuptools_scm[toml]>=3.4', 'pkgconfig>=1.5.1', 'lxml>=3.8'] -with open('README.md', encoding='utf-8') as f: - long_desc = f.read() +with open('README.md', encoding='utf-8') as readme: + long_desc = readme.read() setup( From 47f3e00c2e351ad663a753e68c7c13b780627ef7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:12:56 +0200 Subject: [PATCH 40/46] [pre-commit.ci] pre-commit autoupdate (#389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.0 → v0.14.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.0...v0.14.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8fb6f59f..3dc8df2d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.0 + rev: v0.14.1 hooks: - id: ruff args: ["--fix"] From c42bf925d7eb7acdab0a892e326c7690fb009de1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:52:15 +0100 Subject: [PATCH 41/46] [pre-commit.ci] pre-commit autoupdate (#391) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.1 → v0.14.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.1...v0.14.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3dc8df2d..2a91f0ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.1 + rev: v0.14.2 hooks: - id: ruff args: ["--fix"] From 1bb232462fcb2b3b726a3b5443f30f34e34b9a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Escolano?= Date: Fri, 31 Oct 2025 15:54:13 +0100 Subject: [PATCH 42/46] Add support for Python 3.14 (#393) --- .github/workflows/linuxbrew.yml | 2 +- .github/workflows/macosx.yml | 2 +- .github/workflows/manylinux.yml | 2 +- .github/workflows/sdist.yml | 2 +- .travis.yml | 1 + pyproject.toml | 3 ++- setup.py | 1 + 7 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/linuxbrew.yml b/.github/workflows/linuxbrew.yml index 8a231047..1c6d9543 100644 --- a/.github/workflows/linuxbrew.yml +++ b/.github/workflows/linuxbrew.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] env: # For some unknown reason, linuxbrew tries to use "gcc-11" by default, which doesn't exist. CC: gcc diff --git a/.github/workflows/macosx.yml b/.github/workflows/macosx.yml index 44d214bc..e2e2a0df 100644 --- a/.github/workflows/macosx.yml +++ b/.github/workflows/macosx.yml @@ -8,7 +8,7 @@ jobs: runs-on: macos-latest strategy: matrix: - python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] static_deps: ["static", ""] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml index 04645e84..d1c205d7 100644 --- a/.github/workflows/manylinux.yml +++ b/.github/workflows/manylinux.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-abi: [cp39-cp39, cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313] + python-abi: [cp39-cp39, cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313, cp314-cp314] image: - manylinux2014_x86_64 - manylinux_2_28_x86_64 diff --git a/.github/workflows/sdist.yml b/.github/workflows/sdist.yml index 3bdc9764..ecc53c31 100644 --- a/.github/workflows/sdist.yml +++ b/.github/workflows/sdist.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v3 - name: Set up Python diff --git a/.travis.yml b/.travis.yml index 9574a1c7..8d3ca07e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ python: - "3.11" - "3.12" - "3.13" + - "3.14" env: global: diff --git a/pyproject.toml b/pyproject.toml index 2b2d2d04..7c7b4bf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,8 @@ build = [ "cp310-*", "cp311-*", "cp312-*", - "cp313-*" + "cp313-*", + "cp314-*" ] build-verbosity = 1 build-frontend = "build" diff --git a/setup.py b/setup.py index 4b3d93f7..4100a52b 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Topic :: Text Processing :: Markup :: XML', 'Typing :: Typed', ], From 60a0788e4359de80591a1c78bbf161325a7b1078 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:26:11 +0100 Subject: [PATCH 43/46] [pre-commit.ci] pre-commit autoupdate (#394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.2 → v0.14.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.2...v0.14.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a91f0ab..49a3a0da 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.2 + rev: v0.14.3 hooks: - id: ruff args: ["--fix"] From 47d4201def5d9d131b4f4a29088d677fab8a19f4 Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Mon, 10 Nov 2025 11:12:09 +0100 Subject: [PATCH 44/46] Bump xmlsec1 library to v1.3.9 (#398) --- build_support/static_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_support/static_build.py b/build_support/static_build.py index 4a0b98e8..09e2039a 100644 --- a/build_support/static_build.py +++ b/build_support/static_build.py @@ -153,7 +153,7 @@ def _capture_version_overrides(self): builder.libxml2_version = os.environ.get('PYXMLSEC_LIBXML2_VERSION', '2.14.6') builder.libxslt_version = os.environ.get('PYXMLSEC_LIBXSLT_VERSION', '1.1.43') builder.zlib_version = os.environ.get('PYXMLSEC_ZLIB_VERSION', '1.3.1') - builder.xmlsec1_version = os.environ.get('PYXMLSEC_XMLSEC1_VERSION', '1.3.8') + builder.xmlsec1_version = os.environ.get('PYXMLSEC_XMLSEC1_VERSION', '1.3.9') def _ensure_source_archives(self): return [ From 8d98ff24fbf03f530c3178e013ba42c8afc0d26f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 13:11:53 +0100 Subject: [PATCH 45/46] [pre-commit.ci] pre-commit autoupdate (#399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.3 → v0.14.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.3...v0.14.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49a3a0da..aca65390 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 + rev: v0.14.4 hooks: - id: ruff args: ["--fix"] From 6a61caea84d283f27903b2bab5f84fd043ad293c Mon Sep 17 00:00:00 2001 From: Amin Solhizadeh Date: Mon, 16 Feb 2026 16:00:13 +0100 Subject: [PATCH 46/46] Refactor static dependency builder and cache dependency archives in CI (#401) Extract static dependency preparation into a reusable builder and wire CI to reuse downloaded archives across jobs/platforms. This reduces duplicated build logic, improves maintainability, and speeds up wheel/sdist workflows. - Introduce `build_support/lib_xmlsec_dependency_builder.py`: - Centralize dependency version resolution, source/archive download, extract, and build steps for OpenSSL, zlib, libiconv, libxml2, libxslt, and xmlsec1. - Support both Unix and Windows dependency preparation paths. - Preserve cross-compilation handling via `CrossCompileInfo` (now a dataclass). - Expose resolved library versions for callers. - Add `build_libs_xmlsec.py` CLI: - Provide a standalone entrypoint to prepare dependencies. - Support `--download-only`, custom `--libs-dir`, custom `--buildroot`, and target platform/plat-name overrides. - Refactor `build_support/static_build.py`: - Delegate dependency preparation to `LibXmlsecDependencyBuilder`. - Keep extension configuration focused on platform-specific compiler/linker flags and include/lib wiring. - Preserve static-link behavior while removing duplicated dependency logic. - Update `build_support/build_ext.py`: - Initialize build flags (`PYXMLSEC_ENABLE_DEBUG`, `PYXMLSEC_STATIC_DEPS`, `PYXMLSEC_OPTIMIZE_SIZE`) in `__init__`. - Keep build flow unchanged, but use the refactored static helper path. - Modernize packaging metadata: - Move project metadata from `setup.py` into PEP 621 fields in `pyproject.toml` (`[project]`, `[project.urls]`, `[tool.setuptools]`). - Simplify `setup.py` to extension setup only. - Delete legacy `setup.cfg`. - Relax build-system pins and align build requirements with setuptools_scm>=8. - Bump `ruff[format]` in `requirements-test.txt` to `0.14.4`. - Add reusable dependency-cache workflow: - New `.github/workflows/cache_libs.yml` workflow_call job that downloads and caches `libs/*.{xz,gz,zip}` per OS/arch/version inputs. - Export cache/version outputs for downstream jobs. - Validate expected Windows archive filenames. - Rework wheel/manylinux CI to consume cached libs: - `manylinux.yml` now depends on `cache_libs`, restores cache, and runs build + test inside container via new script `.github/scripts/manylinux_build_and_test.sh`. - Script sets `PYTHONPATH` and `PYXMLSEC_LIBS_DIR` explicitly so isolated PEP 517 builds can import local helpers and reuse cached archives. - `wheels.yml` now depends on `cache_libs`, restores cache before cibuildwheel, updates action versions, and refreshes matrix generation (`generate_wheels_matrix`) including ARM Linux runner mapping. - `sdist.yml` installs `setuptools_scm>=8` during build deps setup. - Use cached libs for macOS static build - Minor workflow hygiene updates: - Normalize formatting and small ordering/conditional tweaks in `linuxbrew.yml` and `macosx.yml`. --- .github/scripts/manylinux_build_and_test.sh | 52 +++ .github/workflows/cache_libs.yml | 134 ++++++ .github/workflows/linuxbrew.yml | 8 + .github/workflows/macosx.yml | 48 +- .github/workflows/manylinux.yml | 74 ++-- .github/workflows/sdist.yml | 13 +- .github/workflows/wheels.yml | 65 +-- build_libs_xmlsec.py | 55 +++ build_support/build_ext.py | 11 +- .../lib_xmlsec_dependency_builder.py | 417 ++++++++++++++++++ build_support/static_build.py | 378 +--------------- pyproject.toml | 74 +++- requirements-test.txt | 2 +- setup.cfg | 22 - setup.py | 48 +- 15 files changed, 895 insertions(+), 506 deletions(-) create mode 100644 .github/scripts/manylinux_build_and_test.sh create mode 100644 .github/workflows/cache_libs.yml create mode 100644 build_libs_xmlsec.py create mode 100644 build_support/lib_xmlsec_dependency_builder.py delete mode 100644 setup.cfg diff --git a/.github/scripts/manylinux_build_and_test.sh b/.github/scripts/manylinux_build_and_test.sh new file mode 100644 index 00000000..01c6867f --- /dev/null +++ b/.github/scripts/manylinux_build_and_test.sh @@ -0,0 +1,52 @@ +#!/bin/sh +set -eu + +: "${PY_ABI:?PY_ABI is required}" +: "${MANYLINUX_IMAGE:?MANYLINUX_IMAGE is required}" + +# Make local build helpers importable for isolated PEP 517 backend subprocesses. +export PYTHONPATH="$PWD${PYTHONPATH:+:${PYTHONPATH}}" +# Ensure dependency archives are read from the restored workspace cache even in isolated builds. +export PYXMLSEC_LIBS_DIR="$PWD/libs" + +# Step: Install system build dependencies (manylinux only) +echo "== [container] Step: Install system build dependencies (manylinux only) ==" +case "$MANYLINUX_IMAGE" in + manylinux*) + yum install -y perl-core + ;; +esac + +# Step: Install python build dependencies +echo "== [container] Step: Install python build dependencies ==" +/opt/python/${PY_ABI}/bin/pip install --upgrade pip setuptools wheel build 'setuptools_scm>=8' + +# Step: Set environment variables +echo "== [container] Step: Set environment variables ==" +PKGVER=$(/opt/python/${PY_ABI}/bin/python setup.py --version) +echo "PKGVER=$PKGVER" + +# Step: Build linux_x86_64 wheel +echo "== [container] Step: Build linux_x86_64 wheel ==" +/opt/python/${PY_ABI}/bin/python -m build + +# Step: Label manylinux wheel +echo "== [container] Step: Label manylinux wheel ==" +ls -la dist/ +auditwheel show dist/xmlsec-${PKGVER}-${PY_ABI}-linux_x86_64.whl +auditwheel repair dist/xmlsec-${PKGVER}-${PY_ABI}-linux_x86_64.whl +ls -la wheelhouse/ +auditwheel show wheelhouse/xmlsec-${PKGVER}-${PY_ABI}-*${MANYLINUX_IMAGE}*.whl + +# Step: Install test dependencies +echo "== [container] Step: Install test dependencies ==" +/opt/python/${PY_ABI}/bin/pip install --upgrade -r requirements-test.txt +/opt/python/${PY_ABI}/bin/pip install xmlsec --only-binary=xmlsec --no-index --find-links=wheelhouse/ + +# Step: Run tests +echo "== [container] Step: Run tests ==" +/opt/python/${PY_ABI}/bin/pytest -v --color=yes + +# Step: Fix mounted workspace file ownership on host +echo "== [container] Step: Fix mounted workspace file ownership on host ==" +chown -R "${HOST_UID}:${HOST_GID}" dist wheelhouse build libs || true diff --git a/.github/workflows/cache_libs.yml b/.github/workflows/cache_libs.yml new file mode 100644 index 00000000..77c83353 --- /dev/null +++ b/.github/workflows/cache_libs.yml @@ -0,0 +1,134 @@ +name: Cache library dependencies + +on: + workflow_call: + inputs: + LIBICONV_VERSION: + default: "1.18" + required: false + type: string + LIBXML2_VERSION: + default: "2.14.6" + required: false + type: string + LIBXSLT_VERSION: + default: "1.1.43" + required: false + type: string + OPENSSL_VERSION: + default: "3.6.0" + required: false + type: string + XMLSEC1_VERSION: + default: "1.3.9" + required: false + type: string + ZLIB_VERSION: + default: "1.3.1" + required: false + type: string + WIN_LIBICONV_VERSION: + default: "1.18-1" + required: false + type: string + WIN_LIBXML2_VERSION: + default: "2.11.9-3" + required: false + type: string + WIN_LIBXSLT_VERSION: + default: "1.1.39" + required: false + type: string + WIN_OPENSSL_VERSION: + default: "3.0.16.pl1" + required: false + type: string + WIN_XMLSEC1_VERSION: + default: "1.3.7" + required: false + type: string + WIN_ZLIB_VERSION: + default: "1.3.1" + required: false + type: string + + outputs: + LIBICONV_VERSION: + value: ${{ inputs.LIBICONV_VERSION }} + LIBXML2_VERSION: + value: ${{ inputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: + value: ${{ inputs.LIBXSLT_VERSION }} + OPENSSL_VERSION: + value: ${{ inputs.OPENSSL_VERSION }} + XMLSEC1_VERSION: + value: ${{ inputs.XMLSEC1_VERSION }} + ZLIB_VERSION: + value: ${{ inputs.ZLIB_VERSION }} + WIN_LIBICONV_VERSION: + value: ${{ inputs.WIN_LIBICONV_VERSION }} + WIN_LIBXML2_VERSION: + value: ${{ inputs.WIN_LIBXML2_VERSION }} + WIN_LIBXSLT_VERSION: + value: ${{ inputs.WIN_LIBXSLT_VERSION }} + WIN_OPENSSL_VERSION: + value: ${{ inputs.WIN_OPENSSL_VERSION }} + WIN_XMLSEC1_VERSION: + value: ${{ inputs.WIN_XMLSEC1_VERSION }} + WIN_ZLIB_VERSION: + value: ${{ inputs.WIN_ZLIB_VERSION }} + +jobs: + cache_libs: + strategy: + fail-fast: false + matrix: + os: + - "ubuntu-22.04" + - "ubuntu-22.04-arm" + - "macos-latest" + - "windows-2022" + - "windows-11-arm" + + runs-on: ${{ matrix.os }} + + env: + LIBICONV_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_LIBICONV_VERSION || inputs.LIBICONV_VERSION }} + LIBXML2_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_LIBXML2_VERSION || inputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_LIBXSLT_VERSION || inputs.LIBXSLT_VERSION }} + OPENSSL_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_OPENSSL_VERSION || inputs.OPENSSL_VERSION }} + XMLSEC1_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_XMLSEC1_VERSION || inputs.XMLSEC1_VERSION }} + ZLIB_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_ZLIB_VERSION || inputs.ZLIB_VERSION }} + + steps: + - uses: actions/checkout@v6 + + - name: Cache [libs] + uses: actions/cache@v4.3.0 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} + + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Install setuptools shim + run: python -m pip install --upgrade pip setuptools + + - name: Download latest libraries + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python build_libs_xmlsec.py --download-only + + - name: Check Windows library versions + if: ${{ contains(matrix.os, 'windows-') }} + run: | + bash -c ' + for file in libs/iconv-${{ inputs.WIN_LIBICONV_VERSION }}.*.zip libs/libxml2-${{ inputs.WIN_LIBXML2_VERSION }}.*.zip libs/libxslt-${{ inputs.WIN_LIBXSLT_VERSION }}.*.zip libs/openssl-${{ inputs.WIN_OPENSSL_VERSION }}.*.zip libs/xmlsec-${{ inputs.WIN_XMLSEC1_VERSION }}.*.zip libs/zlib-${{ inputs.WIN_ZLIB_VERSION }}.*.zip; do + [[ -f "$file" ]] || { echo "MISSING: $file" ; exit 1; } + done + ' diff --git a/.github/workflows/linuxbrew.yml b/.github/workflows/linuxbrew.yml index 1c6d9543..51b0db1e 100644 --- a/.github/workflows/linuxbrew.yml +++ b/.github/workflows/linuxbrew.yml @@ -3,27 +3,34 @@ on: [push, pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref_name != 'master' }} + jobs: linuxbrew: runs-on: ubuntu-latest + strategy: matrix: python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + env: # For some unknown reason, linuxbrew tries to use "gcc-11" by default, which doesn't exist. CC: gcc + steps: - uses: actions/checkout@v3 + - name: Install brew run: | sudo apt install -y build-essential procps curl file git /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH + - name: Install build dependencies run: | brew update brew install python@${{ matrix.python }} gcc libxml2 libxmlsec1 pkg-config echo "/home/linuxbrew/.linuxbrew/opt/python@${{ matrix.python }}/libexec/bin" >> $GITHUB_PATH + - name: Build wheel run: | python3 -m venv build_venv @@ -33,6 +40,7 @@ jobs: export LDFLAGS="-L$(brew --prefix)/lib" python3 -m build rm -rf build/ + - name: Run tests run: | python3 -m venv test_venv diff --git a/.github/workflows/macosx.yml b/.github/workflows/macosx.yml index e2e2a0df..522a2a0a 100644 --- a/.github/workflows/macosx.yml +++ b/.github/workflows/macosx.yml @@ -3,23 +3,47 @@ on: [push, pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref_name != 'master' }} + jobs: + cache_libs: + uses: ./.github/workflows/cache_libs.yml + secrets: inherit + macosx: + needs: cache_libs runs-on: macos-latest + + env: + LIBXML2_VERSION: ${{ needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ needs.cache_libs.outputs.LIBXSLT_VERSION }} + strategy: matrix: python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] static_deps: ["static", ""] + steps: - - uses: actions/checkout@v3 - - name: Setup Python - uses: actions/setup-python@v4 + - uses: actions/checkout@v5.0.0 + + - name: Cache [libs] + id: cache-libs + uses: actions/cache/restore@v4.3.0 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} + + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} + - name: Install build dependencies run: | pip install --upgrade pip setuptools wheel build brew install libxml2 libxmlsec1 pkg-config + - name: Build macosx_x86_64 wheel env: CC: clang @@ -29,26 +53,38 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | export PKG_CONFIG_PATH="$(brew --prefix)/opt/libxml2/lib/pkgconfig" - export PYXMLSEC_LIBXML2_VERSION="$(pkg-config --modversion libxml-2.0)" + export PYXMLSEC_LIBS_DIR="$PWD/libs" python -m build rm -rf build/ + - name: Set environment variables shell: bash run: | echo "PKGVER=$(python setup.py --version)" >> $GITHUB_ENV echo "LLVM_PROFILE_FILE=pyxmlsec.profraw" >> $GITHUB_ENV - - name: Install test dependencies + + - name: Install test dependencies (static only) + if: matrix.static_deps == 'static' + run: | + pip install coverage --upgrade -r requirements-test.txt + pip install xmlsec --only-binary=xmlsec --no-index --find-links=dist/ + + + - name: Install test dependencies (non-static only) + if: matrix.static_deps != 'static' run: | export PKG_CONFIG_PATH="$(brew --prefix)/opt/libxml2/lib/pkgconfig" pip install coverage --upgrade --no-binary=lxml -r requirements-test.txt pip install xmlsec --only-binary=xmlsec --no-index --find-links=dist/ echo "PYXMLSEC_LIBFILE=$(python -c 'import xmlsec; print(xmlsec.__file__)')" >> $GITHUB_ENV + - name: Run tests run: | coverage run -m pytest -v --color=yes + - name: Report coverage to codecov + if: matrix.static_deps != 'static' run: | /Library/Developer/CommandLineTools/usr/bin/llvm-profdata merge -sparse ${{ env.LLVM_PROFILE_FILE }} -output pyxmlsec.profdata /Library/Developer/CommandLineTools/usr/bin/llvm-cov show ${{ env.PYXMLSEC_LIBFILE }} --arch=$(uname -m) --instr-profile=pyxmlsec.profdata src > coverage.txt bash <(curl -s https://codecov.io/bash) -f coverage.txt - if: matrix.static_deps != 'static' diff --git a/.github/workflows/manylinux.yml b/.github/workflows/manylinux.yml index d1c205d7..ff04f9ae 100644 --- a/.github/workflows/manylinux.yml +++ b/.github/workflows/manylinux.yml @@ -3,9 +3,20 @@ on: [push, pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref_name != 'master' }} + jobs: + cache_libs: + uses: ./.github/workflows/cache_libs.yml + secrets: inherit + manylinux: + needs: cache_libs runs-on: ubuntu-latest + + env: + LIBXML2_VERSION: ${{ needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ needs.cache_libs.outputs.LIBXSLT_VERSION }} + strategy: matrix: python-abi: [cp39-cp39, cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313, cp314-cp314] @@ -13,40 +24,39 @@ jobs: - manylinux2014_x86_64 - manylinux_2_28_x86_64 - musllinux_1_2_x86_64 - container: quay.io/pypa/${{ matrix.image }} + steps: - - uses: actions/checkout@v1 - - name: Install python build dependencies - run: | - # https://github.com/actions/runner/issues/2033 - chown -R $(id -u):$(id -g) $PWD - /opt/python/${{ matrix.python-abi }}/bin/pip install --upgrade pip setuptools wheel build - - name: Install system build dependencies (manylinux) - run: | - yum install -y perl-core - if: contains(matrix.image, 'manylinux') - - name: Set environment variables - shell: bash - run: | - echo "PKGVER=$(/opt/python/${{ matrix.python-abi }}/bin/python setup.py --version)" >> $GITHUB_ENV - - name: Build linux_x86_64 wheel + - uses: actions/checkout@v5.0.0 + with: + fetch-depth: 0 + + - name: Cache [libs] + uses: actions/cache/restore@v4.3.0 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} + + # Keep this job on the host runner so JS-based actions (for example actions/cache) + # can run, then execute build/test inside the target manylinux/musllinux container. + - name: Build and test in container env: PYXMLSEC_STATIC_DEPS: true - PYXMLSEC_LIBXML2_VERSION: 2.14.6 # Lock it to libxml2 2.14.6 to match it with lxml GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PY_ABI: ${{ matrix.python-abi }} + MANYLINUX_IMAGE: ${{ matrix.image }} run: | - /opt/python/${{ matrix.python-abi }}/bin/python -m build - - name: Label manylinux wheel - run: | - ls -la dist/ - auditwheel show dist/xmlsec-${{ env.PKGVER }}-${{ matrix.python-abi }}-linux_x86_64.whl - auditwheel repair dist/xmlsec-${{ env.PKGVER }}-${{ matrix.python-abi }}-linux_x86_64.whl - ls -la wheelhouse/ - auditwheel show wheelhouse/xmlsec-${{ env.PKGVER }}-${{ matrix.python-abi }}-*${{ matrix.image }}*.whl - - name: Install test dependencies - run: | - /opt/python/${{ matrix.python-abi }}/bin/pip install --upgrade -r requirements-test.txt - /opt/python/${{ matrix.python-abi }}/bin/pip install xmlsec --only-binary=xmlsec --no-index --find-links=wheelhouse/ - - name: Run tests - run: | - /opt/python/${{ matrix.python-abi }}/bin/pytest -v --color=yes + set -euxo pipefail + docker run --rm \ + -v "$PWD:$PWD" \ + -w "$PWD" \ + -e GH_TOKEN \ + -e PYXMLSEC_STATIC_DEPS \ + -e PY_ABI \ + -e MANYLINUX_IMAGE \ + -e HOST_UID="$(id -u)" \ + -e HOST_GID="$(id -g)" \ + "quay.io/pypa/${MANYLINUX_IMAGE}" \ + sh .github/scripts/manylinux_build_and_test.sh diff --git a/.github/workflows/sdist.yml b/.github/workflows/sdist.yml index ecc53c31..f48ca02c 100644 --- a/.github/workflows/sdist.yml +++ b/.github/workflows/sdist.yml @@ -3,6 +3,7 @@ on: [push, pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref_name != 'master' }} + jobs: sdist: # Avoid Ubuntu 24.04 in sdist workflows, because it contains libxmlsec1-dev @@ -10,27 +11,33 @@ jobs: # (It thinks the softhsm engine has a public key instead of a private key.) # libxmlsec1 <=1.2.33 or >=1.2.42 works. TODO: Try 26.04 when available. runs-on: ubuntu-22.04 + strategy: matrix: python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + steps: - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 + + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} + - name: Install build dependencies run: | - pip install --upgrade pip setuptools wheel + pip install --upgrade pip setuptools wheel 'setuptools_scm>=8' + - name: Package source dist run: | python setup.py sdist + - name: Install test dependencies run: | sudo apt-get update sudo apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl opensc softhsm2 libengine-pkcs11-openssl pip install --upgrade -r requirements-test.txt --no-binary lxml pip install dist/xmlsec-$(python setup.py --version).tar.gz + - name: Run tests run: | pytest -v --color=yes diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 1d4564a6..fa80a645 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -22,6 +22,10 @@ concurrency: permissions: {} jobs: + cache_libs: + uses: ./.github/workflows/cache_libs.yml + secrets: inherit + sdist: # Avoid Ubuntu 24.04 in sdist workflows, because it contains libxmlsec1-dev # v1.2.39, which has a bug that causes tests/test_pkcs11.py to fail. @@ -33,18 +37,18 @@ jobs: contents: write steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v6.0.0 with: - python-version: "3.x" + python-version: '3.14' - name: Install build dependencies run: | - pip install --upgrade pip setuptools wheel + pip install --upgrade pip setuptools wheel setuptools_scm>=8 pkgconfig>=1.5.1 - name: Package source dist run: python setup.py sdist @@ -60,34 +64,35 @@ jobs: run: pytest -v --color=yes - name: Upload sdist - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v5.0.0 with: name: sdist path: dist/*.tar.gz - generate-wheels-matrix: + generate_wheels_matrix: # Create a matrix of all architectures & versions to build. # This enables the next step to run cibuildwheel in parallel. # From https://iscinumpy.dev/post/cibuildwheel-2-10-0/#only-210 name: Generate wheels matrix - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + outputs: include: ${{ steps.set-matrix.outputs.include }} + steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5.0.0 + - name: Install cibuildwheel - # Nb. keep cibuildwheel version pin consistent with job below - run: pipx install cibuildwheel==3.1.4 + # N.B. Keep cibuildwheel version pin consistent with "build_wheels" job below. + run: pipx install cibuildwheel==3.3 + - id: set-matrix - # Once we have the windows build figured out, it can be added here - # by updating the matrix to include windows builds as well. - # See example here: - # https://github.com/lxml/lxml/blob/3ccc7d583e325ceb0ebdf8fc295bbb7fc8cd404d/.github/workflows/wheels.yml#L95C1-L106C51 run: | MATRIX=$( { cibuildwheel --print-build-identifiers --platform linux \ - | jq -nRc '{"only": inputs, "os": "ubuntu-latest"}' \ + | jq -nRc '{"only": inputs, "os": "ubuntu-22.04"}' \ + | sed -e '/aarch64/s|ubuntu-22.04|ubuntu-22.04-arm|' \ && cibuildwheel --print-build-identifiers --platform macos \ | jq -nRc '{"only": inputs, "os": "macos-latest"}' \ && cibuildwheel --print-build-identifiers --platform windows \ @@ -101,38 +106,48 @@ jobs: build_wheels: name: Build for ${{ matrix.only }} - needs: generate-wheels-matrix + needs: [cache_libs, generate_wheels_matrix] runs-on: ${{ matrix.os }} + env: + LIBXML2_VERSION: ${{ contains(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_LIBXML2_VERSION || needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ contains(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_LIBXSLT_VERSION || needs.cache_libs.outputs.LIBXSLT_VERSION }} + strategy: fail-fast: false matrix: - include: ${{ fromJson(needs.generate-wheels-matrix.outputs.include) }} - - env: - PYXMLSEC_LIBXML2_VERSION: 2.14.6 - PYXMLSEC_LIBXSLT_VERSION: 1.1.43 + include: ${{ fromJson(needs.generate_wheels_matrix.outputs.include) }} steps: - name: Check out the repo - uses: actions/checkout@v4 + uses: actions/checkout@v5.0.0 with: fetch-depth: 0 + - name: Cache [libs] + uses: actions/cache/restore@v4.3.0 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} + - name: Set up QEMU if: runner.os == 'Linux' - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v3.7.0 with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v3.1.4 + uses: pypa/cibuildwheel@v3.3.0 with: only: ${{ matrix.only }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/upload-artifact@v4.3.1 + - name: Upload wheels + uses: actions/upload-artifact@v5.0.0 with: path: ./wheelhouse/*.whl name: xmlsec-wheel-${{ matrix.only }} diff --git a/build_libs_xmlsec.py b/build_libs_xmlsec.py new file mode 100644 index 00000000..a4a7e603 --- /dev/null +++ b/build_libs_xmlsec.py @@ -0,0 +1,55 @@ +import argparse +import os +import sys +from pathlib import Path + +from build_support.lib_xmlsec_dependency_builder import LibXmlsecDependencyBuilder + + +def _console_info(message): + print(message) + + +def main(argv=None): + parser = argparse.ArgumentParser(description='Download and build static dependency libraries for python-xmlsec.') + parser.add_argument( + '--platform', + default=sys.platform, + help='Target platform (default: current interpreter platform).', + ) + parser.add_argument( + '--plat-name', + default=os.environ.get('PYXMLSEC_PLAT_NAME'), + help='Target platform tag for cross-compiling (for example macosx-11.0-arm64).', + ) + parser.add_argument( + '--libs-dir', + default=os.environ.get('PYXMLSEC_LIBS_DIR', 'libs'), + help='Directory where source/binary archives are stored.', + ) + parser.add_argument( + '--buildroot', + default=Path('build', 'tmp'), + type=Path, + help='Build root for extracted/build artifacts.', + ) + parser.add_argument( + '--download-only', + action='store_true', + help='Only download dependency archives; do not extract/build.', + ) + + args = parser.parse_args(argv) + builder = LibXmlsecDependencyBuilder( + platform_name=args.platform, + info=_console_info, + libs_dir=Path(args.libs_dir), + buildroot=args.buildroot, + plat_name=args.plat_name, + ) + builder.prepare(download_only=args.download_only) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/build_support/build_ext.py b/build_support/build_ext.py index c4fb5bc9..262acd97 100644 --- a/build_support/build_ext.py +++ b/build_support/build_ext.py @@ -9,15 +9,18 @@ class build_ext(build_ext_orig): - def info(self, message): - self.announce(message, level=log.INFO) + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) - def run(self): - ext = self.ext_map['xmlsec'] self.debug = os.environ.get('PYXMLSEC_ENABLE_DEBUG', False) self.static = os.environ.get('PYXMLSEC_STATIC_DEPS', False) self.size_opt = os.environ.get('PYXMLSEC_OPTIMIZE_SIZE', True) + def info(self, message) -> None: + self.announce(message, level=log.INFO) + + def run(self) -> None: + ext = self.ext_map['xmlsec'] if self.static or sys.platform == 'win32': helper = StaticBuildHelper(self) helper.prepare(sys.platform) diff --git a/build_support/lib_xmlsec_dependency_builder.py b/build_support/lib_xmlsec_dependency_builder.py new file mode 100644 index 00000000..00b519dd --- /dev/null +++ b/build_support/lib_xmlsec_dependency_builder.py @@ -0,0 +1,417 @@ +import multiprocessing +import os +import platform +import subprocess +import sys +import tarfile +import zipfile +from dataclasses import dataclass +from distutils.errors import DistutilsError +from pathlib import Path +from typing import ClassVar +from urllib.parse import urljoin +from urllib.request import urlcleanup + +from .network import download_lib +from .releases import ( + latest_libiconv_release, + latest_libxml2_release, + latest_libxslt_release, + latest_openssl_release, + latest_xmlsec_release, + latest_zlib_release, +) + + +@dataclass +class CrossCompileInfo: + host: str + arch: str + compiler: str + + @property + def triplet(self) -> str: + return f'{self.host}-{self.arch}-{self.compiler}' + + +class LibXmlsecDependencyBuilder: + WINDOWS_LIBS_DOWNLOAD_RELEASE_URL = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2025.07.10/' + LIB_VERSION_ENV_VARS: ClassVar[dict[str, str]] = { + 'libiconv_version': 'PYXMLSEC_LIBICONV_VERSION', + 'libxml2_version': 'PYXMLSEC_LIBXML2_VERSION', + 'libxslt_version': 'PYXMLSEC_LIBXSLT_VERSION', + 'openssl_version': 'PYXMLSEC_OPENSSL_VERSION', + 'xmlsec1_version': 'PYXMLSEC_XMLSEC1_VERSION', + 'zlib_version': 'PYXMLSEC_ZLIB_VERSION', + } + UNIX_DEFAULT_LIB_VERSIONS: ClassVar[dict[str, str]] = { + 'libiconv_version': '1.18', + 'libxml2_version': '2.14.6', # Make sure it matches with lxml + 'libxslt_version': '1.1.43', + 'openssl_version': '3.6.0', + 'xmlsec1_version': '1.3.9', + 'zlib_version': '1.3.1', + } + WINDOWS_DEFAULT_LIB_VERSIONS: ClassVar[dict[str, str]] = { + 'libiconv_version': '1.18-1', + 'libxml2_version': '2.11.9-3', # Make sure it matches with lxml + 'libxslt_version': '1.1.39', + 'openssl_version': '3.0.16.pl1', + 'xmlsec1_version': '1.3.7', + 'zlib_version': '1.3.1', + } + + def __init__(self, platform_name, info=None, libs_dir=None, buildroot=None, plat_name=None): + self.platform_name = platform_name + self.info = info or print + self.plat_name = plat_name + + self._prepare_directories(libs_dir=libs_dir, buildroot=buildroot) + self._set_library_versions(build_platform=platform_name) + + @property + def versions(self): + return {attr: getattr(self, attr) for attr in self.LIB_VERSION_ENV_VARS} + + def prepare(self, download_only=False): + self.info(f'preparing dependency build on {self.platform_name}') + if self.platform_name == 'win32': + self._prepare_windows_build(download_only=download_only) + elif 'linux' in self.platform_name or 'darwin' in self.platform_name: + self._prepare_unix_build(build_platform=self.platform_name, download_only=download_only) + else: + raise DistutilsError(f'Unsupported static build platform: {self.platform_name}') + + def _prepare_directories(self, libs_dir=None, buildroot=None): + buildroot_path = Path(buildroot) if buildroot else Path('build', 'tmp') + + prefix_dir = buildroot_path / 'prefix' + prefix_dir.mkdir(parents=True, exist_ok=True) + self.prefix_dir = prefix_dir.absolute() + + build_libs_dir = buildroot_path / 'libs' + build_libs_dir.mkdir(parents=True, exist_ok=True) + self.build_libs_dir = build_libs_dir + + libs_root = libs_dir if libs_dir is not None else os.environ.get('PYXMLSEC_LIBS_DIR', 'libs') + libs_dir_path = Path(libs_root) + libs_dir_path.mkdir(parents=True, exist_ok=True) + self.libs_dir = libs_dir_path + + self.info('{:20} {}'.format('Lib sources in:', self.libs_dir.absolute())) + + def _set_library_versions(self, build_platform): + defaults = self.UNIX_DEFAULT_LIB_VERSIONS + if build_platform == 'win32': + defaults = self.WINDOWS_DEFAULT_LIB_VERSIONS + + for version_attr, env_var in self.LIB_VERSION_ENV_VARS.items(): + setattr(self, version_attr, os.environ.get(env_var, defaults[version_attr])) + + def _prepare_windows_build(self, download_only=False): + if platform.machine() == 'ARM64': + suffix = 'win-arm64' + elif sys.maxsize > 2**32: + suffix = 'win64' + else: + suffix = 'win32' + + libs = [ + f'libxml2-{self.libxml2_version}.{suffix}.zip', + f'libxslt-{self.libxslt_version}.{suffix}.zip', + f'zlib-{self.zlib_version}.{suffix}.zip', + f'iconv-{self.libiconv_version}.{suffix}.zip', + f'openssl-{self.openssl_version}.{suffix}.zip', + f'xmlsec-{self.xmlsec1_version}.{suffix}.zip', + ] + + for libfile in libs: + url = urljoin(self.WINDOWS_LIBS_DOWNLOAD_RELEASE_URL, libfile) + destfile = self.libs_dir / libfile + if destfile.is_file(): + self.info(f'Using local copy of "{url}"') + else: + self.info(f'Retrieving "{url}" to "{destfile}"') + urlcleanup() + download_lib(url, str(destfile)) + + if download_only: + return + + for package in self.libs_dir.glob('*.zip'): + with zipfile.ZipFile(str(package)) as archive: + archive.extractall(path=str(self.build_libs_dir)) + + def _prepare_unix_build(self, build_platform, download_only=False): + archives = self._ensure_source_archives() + if download_only: + return + + self._extract_archives(archives) + + env, prefix_arg, ldflags, cross_compile = self._prepare_build_environment(build_platform) + self._build_dependencies(env, prefix_arg, ldflags, cross_compile) + + def _ensure_source_archives(self): + return [ + self._ensure_source( + name='OpenSSL', + glob='openssl*.tar.gz', + filename='openssl.tar.gz', + version=self.openssl_version, + env_label='PYXMLSEC_OPENSSL_VERSION', + default_url=latest_openssl_release, + version_url=lambda v: f'https://api.github.com/repos/openssl/openssl/tarball/openssl-{v}', + ), + self._ensure_source( + name='zlib', + glob='zlib*.tar.gz', + filename='zlib.tar.gz', + version=self.zlib_version, + env_label='PYXMLSEC_ZLIB_VERSION', + default_url=latest_zlib_release, + version_url=lambda v: f'https://zlib.net/fossils/zlib-{v}.tar.gz', + ), + self._ensure_source( + name='libiconv', + glob='libiconv*.tar.gz', + filename='libiconv.tar.gz', + version=self.libiconv_version, + env_label='PYXMLSEC_LIBICONV_VERSION', + default_url=latest_libiconv_release, + version_url=lambda v: f'https://ftpmirror.gnu.org/libiconv/libiconv-{v}.tar.gz', + ), + self._ensure_source( + name='libxml2', + glob='libxml2*.tar.xz', + filename='libxml2.tar.xz', + version=self.libxml2_version, + env_label='PYXMLSEC_LIBXML2_VERSION', + default_url=latest_libxml2_release, + version_url=lambda v: self._libxml_related_url('libxml2', v), + ), + self._ensure_source( + name='libxslt', + glob='libxslt*.tar.xz', + filename='libxslt.tar.xz', + version=self.libxslt_version, + env_label='PYXMLSEC_LIBXSLT_VERSION', + default_url=latest_libxslt_release, + version_url=lambda v: self._libxml_related_url('libxslt', v), + ), + self._ensure_source( + name='xmlsec1', + glob='xmlsec1*.tar.gz', + filename='xmlsec1.tar.gz', + version=self.xmlsec1_version, + env_label='PYXMLSEC_XMLSEC1_VERSION', + default_url=latest_xmlsec_release, + version_url=lambda v: f'https://github.com/lsh123/xmlsec/releases/download/{v}/xmlsec1-{v}.tar.gz', + ), + ] + + def _ensure_source(self, name, glob, filename, version, env_label, default_url, version_url): + archive = next(self.libs_dir.glob(glob), None) + if archive is not None: + return archive + + self.info('{:10}: {}'.format(name, 'source tar not found, downloading ...')) + archive = self.libs_dir / filename + if version is None: + url = default_url() + self.info('{:10}: {}'.format(name, f'{env_label} unset, downloading latest from {url}')) + else: + url = version_url(version) + self.info('{:10}: {}'.format(name, f'{env_label}={version}, downloading from {url}')) + download_lib(url, str(archive)) + return archive + + def _libxml_related_url(self, lib_name, version): + version_prefix, _ = version.rsplit('.', 1) + return f'https://download.gnome.org/sources/{lib_name}/{version_prefix}/{lib_name}-{version}.tar.xz' + + def _extract_archives(self, archives): + for archive in archives: + self.info(f'Unpacking {archive.name}') + try: + with tarfile.open(str(archive)) as tar: + if sys.version_info >= (3, 12): + tar.extractall(path=str(self.build_libs_dir), filter='data') + else: + tar.extractall(path=str(self.build_libs_dir)) + except EOFError as error: + raise DistutilsError(f'Bad {archive.name} downloaded; remove it and try again.') from error + + def _prepare_build_environment(self, build_platform): + prefix_arg = f'--prefix={self.prefix_dir}' + env = os.environ.copy() + + cflags = [] + if env.get('CFLAGS'): + cflags.append(env['CFLAGS']) + cflags.append('-fPIC') + + ldflags = [] + if env.get('LDFLAGS'): + ldflags.append(env['LDFLAGS']) + + cross_compile = None + if build_platform == 'darwin': + if self.plat_name: + arch = self.plat_name.rsplit('-', 1)[1] + if arch != platform.machine() and arch in ('x86_64', 'arm64'): + self.info(f'Cross-compiling for {arch}') + cflags.append(f'-arch {arch}') + ldflags.append(f'-arch {arch}') + cross_compile = CrossCompileInfo('darwin64', arch, 'cc') + major_version, _ = tuple(map(int, platform.mac_ver()[0].split('.')[:2])) + if major_version >= 11 and 'MACOSX_DEPLOYMENT_TARGET' not in env: + env['MACOSX_DEPLOYMENT_TARGET'] = '11.0' + + env['CFLAGS'] = ' '.join(cflags) + env['LDFLAGS'] = ' '.join(ldflags) + return env, prefix_arg, ldflags, cross_compile + + def _build_dependencies(self, env, prefix_arg, ldflags, cross_compile): + self._build_openssl(env, prefix_arg, cross_compile) + self._build_zlib(env, prefix_arg) + + host_arg = [f'--host={cross_compile.arch}'] if cross_compile else [] + self._build_libiconv(env, prefix_arg, host_arg) + self._build_libxml2(env, prefix_arg, host_arg) + self._build_libxslt(env, prefix_arg, host_arg) + + ldflags.append('-lpthread') + env['LDFLAGS'] = ' '.join(ldflags) + self._build_xmlsec1(env, prefix_arg, host_arg) + + def _build_openssl(self, env, prefix_arg, cross_compile): + self.info('Building OpenSSL') + openssl_dir = next(self.build_libs_dir.glob('openssl-*')) + openssl_config_cmd = [prefix_arg, 'no-shared', '-fPIC', '--libdir=lib'] + if platform.machine() == 'riscv64': + # openssl(riscv64): disable ASM to avoid R_RISCV_JAL relocation failure on 3.5.2 + # OpenSSL 3.5.2 enables RISC-V64 AES assembly by default. When we statically + # link libcrypto alongside xmlsec, the AES asm path triggers a link-time error: + # relocation truncated to fit: R_RISCV_JAL against symbol `AES_set_encrypt_key' + # in .../libcrypto.a(libcrypto-lib-aes-riscv64.o) + # This appears to stem from a long-range jump emitted by the AES asm generator + # (see aes-riscv64.pl around L1069), which can exceed the JAL reach when objects + # end up far apart in the final static link. + # As a pragmatic workaround, disable ASM on riscv64 (pass `no-asm`) so the + # portable C implementation is used. This unblocks the build at the cost of + # some crypto performance on riscv64 only. + # Refs: + # - https://github.com/openssl/openssl/blob/0893a62/crypto/aes/asm/aes-riscv64.pl#L1069 + openssl_config_cmd.append('no-asm') + if cross_compile: + openssl_config_cmd.insert(0, './Configure') + openssl_config_cmd.append(cross_compile.triplet) + else: + openssl_config_cmd.insert(0, './config') + subprocess.check_call(openssl_config_cmd, cwd=str(openssl_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(openssl_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install_sw'], cwd=str(openssl_dir), env=env) + + def _build_zlib(self, env, prefix_arg): + self.info('Building zlib') + zlib_dir = next(self.build_libs_dir.glob('zlib-*')) + subprocess.check_call(['./configure', prefix_arg], cwd=str(zlib_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(zlib_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(zlib_dir), env=env) + + def _build_libiconv(self, env, prefix_arg, host_arg): + self.info('Building libiconv') + libiconv_dir = next(self.build_libs_dir.glob('libiconv-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-dependency-tracking', + '--disable-shared', + *host_arg, + ], + cwd=str(libiconv_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libiconv_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libiconv_dir), env=env) + + def _build_libxml2(self, env, prefix_arg, host_arg): + self.info('Building LibXML2') + libxml2_dir = next(self.build_libs_dir.glob('libxml2-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-dependency-tracking', + '--disable-shared', + '--without-lzma', + '--without-python', + f'--with-iconv={self.prefix_dir}', + f'--with-zlib={self.prefix_dir}', + *host_arg, + ], + cwd=str(libxml2_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxml2_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxml2_dir), env=env) + + def _build_libxslt(self, env, prefix_arg, host_arg): + self.info('Building libxslt') + libxslt_dir = next(self.build_libs_dir.glob('libxslt-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-dependency-tracking', + '--disable-shared', + '--without-python', + '--without-crypto', + f'--with-libxml-prefix={self.prefix_dir}', + *host_arg, + ], + cwd=str(libxslt_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxslt_dir), env=env) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxslt_dir), env=env) + + def _build_xmlsec1(self, env, prefix_arg, host_arg): + self.info('Building xmlsec1') + xmlsec1_dir = next(self.build_libs_dir.glob('xmlsec1-*')) + subprocess.check_call( + [ + './configure', + prefix_arg, + '--disable-shared', + '--disable-gost', + '--enable-md5', + '--enable-ripemd160', + '--disable-crypto-dl', + '--enable-static=yes', + '--enable-shared=no', + '--enable-static-linking=yes', + '--with-default-crypto=openssl', + f'--with-openssl={self.prefix_dir}', + f'--with-libxml={self.prefix_dir}', + f'--with-libxslt={self.prefix_dir}', + *host_arg, + ], + cwd=str(xmlsec1_dir), + env=env, + ) + include_flags = [ + f'-I{self.prefix_dir / "include"}', + f'-I{self.prefix_dir / "include" / "libxml"}', + ] + subprocess.check_call( + ['make', f'-j{multiprocessing.cpu_count() + 1}', *include_flags], + cwd=str(xmlsec1_dir), + env=env, + ) + subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(xmlsec1_dir), env=env) + + +__all__ = ('CrossCompileInfo', 'LibXmlsecDependencyBuilder') diff --git a/build_support/static_build.py b/build_support/static_build.py index 09e2039a..34d06ae6 100644 --- a/build_support/static_build.py +++ b/build_support/static_build.py @@ -1,35 +1,7 @@ -import multiprocessing -import os -import platform -import subprocess import sys -import tarfile -import zipfile from distutils.errors import DistutilsError -from pathlib import Path -from urllib.parse import urljoin -from urllib.request import urlcleanup -from .network import download_lib -from .releases import ( - latest_libiconv_release, - latest_libxml2_release, - latest_libxslt_release, - latest_openssl_release, - latest_xmlsec_release, - latest_zlib_release, -) - - -class CrossCompileInfo: - def __init__(self, host, arch, compiler): - self.host = host - self.arch = arch - self.compiler = compiler - - @property - def triplet(self): - return f'{self.host}-{self.arch}-{self.compiler}' +from .lib_xmlsec_dependency_builder import CrossCompileInfo, LibXmlsecDependencyBuilder class StaticBuildHelper: @@ -37,70 +9,35 @@ def __init__(self, builder): self.builder = builder self.ext = builder.ext_map['xmlsec'] self.info = builder.info - self._prepare_directories() def prepare(self, platform_name): self.info(f'starting static build on {sys.platform}') - if platform_name == 'win32': - self._prepare_windows_build() - elif 'linux' in platform_name or 'darwin' in platform_name: - self._prepare_unix_build(platform_name) - else: - raise DistutilsError(f'Unsupported static build platform: {platform_name}') - - def _prepare_directories(self): - buildroot = Path('build', 'tmp') - - prefix_dir = buildroot / 'prefix' - prefix_dir.mkdir(parents=True, exist_ok=True) - self.prefix_dir = prefix_dir.absolute() - - build_libs_dir = buildroot / 'libs' - build_libs_dir.mkdir(exist_ok=True) - self.build_libs_dir = build_libs_dir + deps_builder = LibXmlsecDependencyBuilder( + platform_name=platform_name, + info=self.info, + plat_name=getattr(self.builder, 'plat_name', None), + ) + deps_builder.prepare() - libs_dir = Path(os.environ.get('PYXMLSEC_LIBS_DIR', 'libs')) - libs_dir.mkdir(exist_ok=True) - self.libs_dir = libs_dir - self.info('{:20} {}'.format('Lib sources in:', self.libs_dir.absolute())) + self.prefix_dir = deps_builder.prefix_dir + self.build_libs_dir = deps_builder.build_libs_dir + self.libs_dir = deps_builder.libs_dir self.builder.prefix_dir = self.prefix_dir self.builder.build_libs_dir = self.build_libs_dir self.builder.libs_dir = self.libs_dir - def _prepare_windows_build(self): - release_url = 'https://github.com/mxamin/python-xmlsec-win-binaries/releases/download/2025.07.10/' - if platform.machine() == 'ARM64': - suffix = 'win-arm64' - elif sys.maxsize > 2**32: - suffix = 'win64' - else: - suffix = 'win32' + for version_attr, value in deps_builder.versions.items(): + setattr(self.builder, version_attr, value) - libs = [ - f'libxml2-2.11.9-3.{suffix}.zip', - f'libxslt-1.1.39.{suffix}.zip', - f'zlib-1.3.1.{suffix}.zip', - f'iconv-1.18-1.{suffix}.zip', - f'openssl-3.0.16.pl1.{suffix}.zip', - f'xmlsec-1.3.7.{suffix}.zip', - ] - - for libfile in libs: - url = urljoin(release_url, libfile) - destfile = self.libs_dir / libfile - if destfile.is_file(): - self.info(f'Using local copy of "{url}"') - else: - self.info(f'Retrieving "{url}" to "{destfile}"') - urlcleanup() - download_lib(url, str(destfile)) - - for package in self.libs_dir.glob('*.zip'): - with zipfile.ZipFile(str(package)) as archive: - destdir = self.build_libs_dir - archive.extractall(path=str(destdir)) + if platform_name == 'win32': + self._configure_windows_extension_for_static() + elif 'linux' in platform_name or 'darwin' in platform_name: + self._configure_unix_extension_for_static(platform_name) + else: + raise DistutilsError(f'Unsupported static build platform: {platform_name}') + def _configure_windows_extension_for_static(self): self.ext.define_macros = [ ('XMLSEC_CRYPTO', '\\"openssl\\"'), ('__XMLSEC_FUNCTION__', '__FUNCTION__'), @@ -137,282 +74,7 @@ def _prepare_windows_build(self): includes.append(next(path / 'xmlsec' for path in includes if (path / 'xmlsec').is_dir())) self.ext.include_dirs = [str(path.absolute()) for path in includes] - def _prepare_unix_build(self, build_platform): - self._capture_version_overrides() - archives = self._ensure_source_archives() - self._extract_archives(archives) - - env, prefix_arg, ldflags, cross_compile = self._prepare_build_environment(build_platform) - self._build_dependencies(env, prefix_arg, ldflags, cross_compile) - self._configure_extension_for_static(build_platform) - - def _capture_version_overrides(self): - builder = self.builder - builder.openssl_version = os.environ.get('PYXMLSEC_OPENSSL_VERSION', '3.6.0') - builder.libiconv_version = os.environ.get('PYXMLSEC_LIBICONV_VERSION', '1.18') - builder.libxml2_version = os.environ.get('PYXMLSEC_LIBXML2_VERSION', '2.14.6') - builder.libxslt_version = os.environ.get('PYXMLSEC_LIBXSLT_VERSION', '1.1.43') - builder.zlib_version = os.environ.get('PYXMLSEC_ZLIB_VERSION', '1.3.1') - builder.xmlsec1_version = os.environ.get('PYXMLSEC_XMLSEC1_VERSION', '1.3.9') - - def _ensure_source_archives(self): - return [ - self._ensure_source( - name='OpenSSL', - glob='openssl*.tar.gz', - filename='openssl.tar.gz', - version=self.builder.openssl_version, - env_label='PYXMLSEC_OPENSSL_VERSION', - default_url=latest_openssl_release, - version_url=lambda v: f'https://api.github.com/repos/openssl/openssl/tarball/openssl-{v}', - ), - self._ensure_source( - name='zlib', - glob='zlib*.tar.gz', - filename='zlib.tar.gz', - version=self.builder.zlib_version, - env_label='PYXMLSEC_ZLIB_VERSION', - default_url=latest_zlib_release, - version_url=lambda v: f'https://zlib.net/fossils/zlib-{v}.tar.gz', - ), - self._ensure_source( - name='libiconv', - glob='libiconv*.tar.gz', - filename='libiconv.tar.gz', - version=self.builder.libiconv_version, - env_label='PYXMLSEC_LIBICONV_VERSION', - default_url=latest_libiconv_release, - version_url=lambda v: f'https://ftpmirror.gnu.org/libiconv/libiconv-{v}.tar.gz', - ), - self._ensure_source( - name='libxml2', - glob='libxml2*.tar.xz', - filename='libxml2.tar.xz', - version=self.builder.libxml2_version, - env_label='PYXMLSEC_LIBXML2_VERSION', - default_url=latest_libxml2_release, - version_url=lambda v: self._libxml_related_url('libxml2', v), - ), - self._ensure_source( - name='libxslt', - glob='libxslt*.tar.xz', - filename='libxslt.tar.xz', - version=self.builder.libxslt_version, - env_label='PYXMLSEC_LIBXSLT_VERSION', - default_url=latest_libxslt_release, - version_url=lambda v: self._libxml_related_url('libxslt', v), - ), - self._ensure_source( - name='xmlsec1', - glob='xmlsec1*.tar.gz', - filename='xmlsec1.tar.gz', - version=self.builder.xmlsec1_version, - env_label='PYXMLSEC_XMLSEC1_VERSION', - default_url=latest_xmlsec_release, - version_url=lambda v: f'https://github.com/lsh123/xmlsec/releases/download/{v}/xmlsec1-{v}.tar.gz', - ), - ] - - def _ensure_source(self, name, glob, filename, version, env_label, default_url, version_url): - archive = next(self.libs_dir.glob(glob), None) - if archive is not None: - return archive - - self.info('{:10}: {}'.format(name, 'source tar not found, downloading ...')) - archive = self.libs_dir / filename - if version is None: - url = default_url() - self.info('{:10}: {}'.format(name, f'{env_label} unset, downloading latest from {url}')) - else: - url = version_url(version) - self.info('{:10}: {}'.format(name, f'{env_label}={version}, downloading from {url}')) - download_lib(url, str(archive)) - return archive - - def _libxml_related_url(self, lib_name, version): - version_prefix, _ = version.rsplit('.', 1) - return f'https://download.gnome.org/sources/{lib_name}/{version_prefix}/{lib_name}-{version}.tar.xz' - - def _extract_archives(self, archives): - for archive in archives: - self.info(f'Unpacking {archive.name}') - try: - with tarfile.open(str(archive)) as tar: - tar.extractall(path=str(self.build_libs_dir)) - except EOFError as error: - raise DistutilsError(f'Bad {archive.name} downloaded; remove it and try again.') from error - - def _prepare_build_environment(self, build_platform): - prefix_arg = f'--prefix={self.prefix_dir}' - env = os.environ.copy() - - cflags = [] - if env.get('CFLAGS'): - cflags.append(env['CFLAGS']) - cflags.append('-fPIC') - - ldflags = [] - if env.get('LDFLAGS'): - ldflags.append(env['LDFLAGS']) - - cross_compile = None - if build_platform == 'darwin': - arch = self.builder.plat_name.rsplit('-', 1)[1] - if arch != platform.machine() and arch in ('x86_64', 'arm64'): - self.info(f'Cross-compiling for {arch}') - cflags.append(f'-arch {arch}') - ldflags.append(f'-arch {arch}') - cross_compile = CrossCompileInfo('darwin64', arch, 'cc') - major_version, _ = tuple(map(int, platform.mac_ver()[0].split('.')[:2])) - if major_version >= 11 and 'MACOSX_DEPLOYMENT_TARGET' not in env: - env['MACOSX_DEPLOYMENT_TARGET'] = '11.0' - - env['CFLAGS'] = ' '.join(cflags) - env['LDFLAGS'] = ' '.join(ldflags) - return env, prefix_arg, ldflags, cross_compile - - def _build_dependencies(self, env, prefix_arg, ldflags, cross_compile): - self._build_openssl(env, prefix_arg, cross_compile) - self._build_zlib(env, prefix_arg) - - host_arg = [f'--host={cross_compile.arch}'] if cross_compile else [] - self._build_libiconv(env, prefix_arg, host_arg) - self._build_libxml2(env, prefix_arg, host_arg) - self._build_libxslt(env, prefix_arg, host_arg) - - ldflags.append('-lpthread') - env['LDFLAGS'] = ' '.join(ldflags) - self._build_xmlsec1(env, prefix_arg, host_arg) - - def _build_openssl(self, env, prefix_arg, cross_compile): - self.info('Building OpenSSL') - openssl_dir = next(self.build_libs_dir.glob('openssl-*')) - openssl_config_cmd = [prefix_arg, 'no-shared', '-fPIC', '--libdir=lib'] - if platform.machine() == 'riscv64': - # openssl(riscv64): disable ASM to avoid R_RISCV_JAL relocation failure on 3.5.2 - # OpenSSL 3.5.2 enables RISC-V64 AES assembly by default. When we statically - # link libcrypto alongside xmlsec, the AES asm path triggers a link-time error: - # relocation truncated to fit: R_RISCV_JAL against symbol `AES_set_encrypt_key' - # in .../libcrypto.a(libcrypto-lib-aes-riscv64.o) - # This appears to stem from a long-range jump emitted by the AES asm generator - # (see aes-riscv64.pl around L1069), which can exceed the JAL reach when objects - # end up far apart in the final static link. - # As a pragmatic workaround, disable ASM on riscv64 (pass `no-asm`) so the - # portable C implementation is used. This unblocks the build at the cost of - # some crypto performance on riscv64 only. - # Refs: - # - https://github.com/openssl/openssl/blob/0893a62/crypto/aes/asm/aes-riscv64.pl#L1069 - openssl_config_cmd.append('no-asm') - if cross_compile: - openssl_config_cmd.insert(0, './Configure') - openssl_config_cmd.append(cross_compile.triplet) - else: - openssl_config_cmd.insert(0, './config') - subprocess.check_call(openssl_config_cmd, cwd=str(openssl_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(openssl_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install_sw'], cwd=str(openssl_dir), env=env) - - def _build_zlib(self, env, prefix_arg): - self.info('Building zlib') - zlib_dir = next(self.build_libs_dir.glob('zlib-*')) - subprocess.check_call(['./configure', prefix_arg], cwd=str(zlib_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(zlib_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(zlib_dir), env=env) - - def _build_libiconv(self, env, prefix_arg, host_arg): - self.info('Building libiconv') - libiconv_dir = next(self.build_libs_dir.glob('libiconv-*')) - subprocess.check_call( - [ - './configure', - prefix_arg, - '--disable-dependency-tracking', - '--disable-shared', - *host_arg, - ], - cwd=str(libiconv_dir), - env=env, - ) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libiconv_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libiconv_dir), env=env) - - def _build_libxml2(self, env, prefix_arg, host_arg): - self.info('Building LibXML2') - libxml2_dir = next(self.build_libs_dir.glob('libxml2-*')) - subprocess.check_call( - [ - './configure', - prefix_arg, - '--disable-dependency-tracking', - '--disable-shared', - '--without-lzma', - '--without-python', - f'--with-iconv={self.prefix_dir}', - f'--with-zlib={self.prefix_dir}', - *host_arg, - ], - cwd=str(libxml2_dir), - env=env, - ) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxml2_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxml2_dir), env=env) - - def _build_libxslt(self, env, prefix_arg, host_arg): - self.info('Building libxslt') - libxslt_dir = next(self.build_libs_dir.glob('libxslt-*')) - subprocess.check_call( - [ - './configure', - prefix_arg, - '--disable-dependency-tracking', - '--disable-shared', - '--without-python', - '--without-crypto', - f'--with-libxml-prefix={self.prefix_dir}', - *host_arg, - ], - cwd=str(libxslt_dir), - env=env, - ) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}'], cwd=str(libxslt_dir), env=env) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(libxslt_dir), env=env) - - def _build_xmlsec1(self, env, prefix_arg, host_arg): - self.info('Building xmlsec1') - xmlsec1_dir = next(self.build_libs_dir.glob('xmlsec1-*')) - subprocess.check_call( - [ - './configure', - prefix_arg, - '--disable-shared', - '--disable-gost', - '--enable-md5', - '--enable-ripemd160', - '--disable-crypto-dl', - '--enable-static=yes', - '--enable-shared=no', - '--enable-static-linking=yes', - '--with-default-crypto=openssl', - f'--with-openssl={self.prefix_dir}', - f'--with-libxml={self.prefix_dir}', - f'--with-libxslt={self.prefix_dir}', - *host_arg, - ], - cwd=str(xmlsec1_dir), - env=env, - ) - include_flags = [ - f'-I{self.prefix_dir / "include"}', - f'-I{self.prefix_dir / "include" / "libxml"}', - ] - subprocess.check_call( - ['make', f'-j{multiprocessing.cpu_count() + 1}', *include_flags], - cwd=str(xmlsec1_dir), - env=env, - ) - subprocess.check_call(['make', f'-j{multiprocessing.cpu_count() + 1}', 'install'], cwd=str(xmlsec1_dir), env=env) - - def _configure_extension_for_static(self, build_platform): + def _configure_unix_extension_for_static(self, build_platform): self.ext.define_macros = [ ('__XMLSEC_FUNCTION__', '__func__'), ('XMLSEC_NO_SIZE_T', None), diff --git a/pyproject.toml b/pyproject.toml index 7c7b4bf3..89ace626 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,56 @@ [build-system] -requires = ["setuptools==80.9.0", "wheel", "setuptools_scm[toml]>=3.4", "pkgconfig>=1.5.1", "lxml==6.0.2"] +requires = ["setuptools", "wheel", "setuptools_scm>=8", "pkgconfig>=1.5.1", "lxml>=3.8"] +[project] +name = "xmlsec" +dynamic = ["version"] +description = "Python bindings for the XML Security Library" +readme = {file = "README.md", content-type = "text/markdown"} +requires-python = ">=3.9" +dependencies = ["lxml>=3.8"] +keywords = ["xmlsec"] +authors = [ + {name = "Bulat Gaifullin", email = "support@mehcode.com"} +] +maintainers = [ + {name = "Oleg Hoefling", email = "oleg.hoefling@gmail.com"}, + {name = "Amin Solhizadeh", email = "amin.solhizadeh@gmail.com"} +] +license = "MIT" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: C", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Text Processing :: Markup :: XML", + "Typing :: Typed" +] + +[project.urls] +Documentation = "https://xmlsec.readthedocs.io" +Source = "https://github.com/xmlsec/python-xmlsec" +Changelog = "https://github.com/xmlsec/python-xmlsec/releases" + +# setuptools +[tool.setuptools] +zip-safe = false +packages = ["xmlsec"] +package-dir = {"" = "src"} + +[tool.setuptools.package-data] +xmlsec = ["py.typed", "*.pyi"] + +[tool.setuptools_scm] + +# mypy [tool.mypy] files = ['src'] ignore_missing_imports = false @@ -22,6 +72,19 @@ warn_no_return = true no_implicit_reexport = true show_error_codes = true +# TODO: Remove this override after adding full type annotations in build tooling modules. +[[tool.mypy.overrides]] +module = [ + "build_support.network", + "build_support.releases", + "build_support.lib_xmlsec_dependency_builder", + "build_libs_xmlsec" +] +disallow_untyped_calls = false +disallow_untyped_defs = false +disable_error_code = ["attr-defined"] + +# ruff [tool.ruff] # Maximum line length, same as your original Black + Flake8 config line-length = 130 @@ -88,6 +151,7 @@ skip-magic-trailing-comma = false # Enforce Unix-style line endings (LF) line-ending = "lf" +# cibuildwheel [tool.cibuildwheel] build = [ "cp39-*", @@ -98,6 +162,7 @@ build = [ "cp314-*" ] build-verbosity = 1 +environment = {PYXMLSEC_STATIC_DEPS="true"} build-frontend = "build" skip = [ "pp*", # Skips PyPy builds (pp38-*, pp39-*, etc.) @@ -105,23 +170,16 @@ skip = [ ] test-command = "pytest -v --color=yes {package}/tests" before-test = "pip install -r requirements-test.txt" -test-skip = "*-macosx_arm64" - -[tool.cibuildwheel.environment] -PYXMLSEC_STATIC_DEPS = "true" [tool.cibuildwheel.linux] archs = ["x86_64", "aarch64", "riscv64"] environment-pass = [ - "PYXMLSEC_LIBXML2_VERSION", - "PYXMLSEC_LIBXSLT_VERSION", "PYXMLSEC_STATIC_DEPS", "GH_TOKEN" ] [tool.cibuildwheel.macos] archs = ["x86_64", "arm64"] -before-all = "brew install perl" [tool.cibuildwheel.windows] archs = ["AMD64"] diff --git a/requirements-test.txt b/requirements-test.txt index ad135d97..70fe9703 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,4 +2,4 @@ pytest==8.4.1 lxml-stubs==0.5.1 -ruff[format]==0.13.0 +ruff[format]==0.14.4 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 8762c654..00000000 --- a/setup.cfg +++ /dev/null @@ -1,22 +0,0 @@ -[metadata] -description_file = README.md - -[bdist_rpm] -release = 1 -build_requires = pkg-config xmlsec1-devel libxml2-devel xmlsec1-openssl-devel -group = Development/Libraries -requires = xmlsec1 xmlsec1-openssl - -[build_sphinx] -source-dir = doc/source -build-dir = doc/build -all_files = 1 - -# [flake8] -# per-file-ignores = -# *.pyi: E301, E302, E305, E501, E701, F401, F822 -# doc/source/conf.py: D1 -# doc/source/examples/*.py: D1, E501 -# tests/*.py: D1 -# exclude = .venv*,.git,*_pb2.pyi,build,dist,libs,.eggs,.direnv* -# max-line-length = 130 diff --git a/setup.py b/setup.py index 4100a52b..946855f1 100644 --- a/setup.py +++ b/setup.py @@ -5,57 +5,11 @@ from build_support.build_ext import build_ext src_root = Path(__file__).parent / 'src' -sources = [str(path.absolute()) for path in src_root.rglob('*.c')] +sources = [str(path.relative_to(Path(__file__).parent)) for path in src_root.rglob('*.c')] pyxmlsec = Extension('xmlsec', sources=sources) -setup_reqs = ['setuptools_scm[toml]>=3.4', 'pkgconfig>=1.5.1', 'lxml>=3.8'] - - -with open('README.md', encoding='utf-8') as readme: - long_desc = readme.read() setup( - name='xmlsec', - use_scm_version=True, - description='Python bindings for the XML Security Library', - long_description=long_desc, - long_description_content_type='text/markdown', ext_modules=[pyxmlsec], cmdclass={'build_ext': build_ext}, - python_requires='>=3.9', - setup_requires=setup_reqs, - install_requires=['lxml>=3.8'], - author='Bulat Gaifullin', - author_email='support@mehcode.com', - maintainer='Oleg Hoefling', - maintainer_email='oleg.hoefling@gmail.com', - url='https://github.com/mehcode/python-xmlsec', - project_urls={ - 'Documentation': 'https://xmlsec.readthedocs.io', - 'Source': 'https://github.com/mehcode/python-xmlsec', - 'Changelog': 'https://github.com/mehcode/python-xmlsec/releases', - }, - license='MIT', - keywords=['xmlsec'], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: C', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', - 'Programming Language :: Python :: 3.14', - 'Topic :: Text Processing :: Markup :: XML', - 'Typing :: Typed', - ], - zip_safe=False, - packages=['xmlsec'], - package_dir={'': 'src'}, - package_data={'xmlsec': ['py.typed', '*.pyi']}, )