diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d9cad9c..1d4564a 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 94fa772..8fb6f59 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 0000000..e69de29 diff --git a/build_support/build_ext.py b/build_support/build_ext.py new file mode 100644 index 0000000..c4fb5bc --- /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 0000000..7ac0bb5 --- /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 0000000..089162e --- /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 0000000..4a0b98e --- /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 624169c..4b3d93f 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(