1import errno 2import os 3import os.path 4import platform 5import shutil 6import subprocess 7import tarfile 8from distutils import log 9from distutils.command.build_clib import build_clib as _build_clib 10from distutils.command.build_ext import build_ext as _build_ext 11from distutils.errors import DistutilsError 12from io import BytesIO 13import sys 14 15from setuptools import Distribution as _Distribution, setup, find_packages, __version__ as setuptools_version 16from setuptools.command.develop import develop as _develop 17from setuptools.command.egg_info import egg_info as _egg_info 18from setuptools.command.sdist import sdist as _sdist 19 20try: 21 from wheel.bdist_wheel import bdist_wheel as _bdist_wheel 22except ImportError: 23 _bdist_wheel = None 24 pass 25 26 27sys.path.append(os.path.abspath(os.path.dirname(__file__))) 28from setup_support import absolute, build_flags, detect_dll, has_system_lib # noqa: E402 29 30 31BUILDING_FOR_WINDOWS = detect_dll() 32 33MAKE = 'gmake' if platform.system() in ['DragonFly', 'FreeBSD', 'OpenBSD'] else 'make' 34 35# IMPORTANT: keep in sync with .github/workflows/build.yml 36# 37# Version of libsecp256k1 to download if none exists in the `libsecp256k1` directory 38UPSTREAM_REF = os.getenv('COINCURVE_UPSTREAM_REF') or 'f2d9aeae6d5a7c7fbbba8bbb38b1849b784beef7' 39 40LIB_TARBALL_URL = f'https://github.com/bitcoin-core/secp256k1/archive/{UPSTREAM_REF}.tar.gz' 41 42 43# We require setuptools >= 3.3 44if [int(i) for i in setuptools_version.split('.', 2)[:2]] < [3, 3]: 45 raise SystemExit( 46 'Your setuptools version ({}) is too old to correctly install this ' 47 'package. Please upgrade to a newer version (>= 3.3).'.format(setuptools_version) 48 ) 49 50 51def download_library(command): 52 if command.dry_run: 53 return 54 libdir = absolute('libsecp256k1') 55 if os.path.exists(os.path.join(libdir, 'autogen.sh')): 56 # Library already downloaded 57 return 58 if not os.path.exists(libdir): 59 command.announce('downloading libsecp256k1 source code', level=log.INFO) 60 try: 61 import requests 62 63 r = requests.get(LIB_TARBALL_URL, stream=True) 64 status_code = r.status_code 65 if status_code == 200: 66 content = BytesIO(r.raw.read()) 67 content.seek(0) 68 with tarfile.open(fileobj=content) as tf: 69 dirname = tf.getnames()[0].partition('/')[0] 70 tf.extractall() 71 shutil.move(dirname, libdir) 72 else: 73 raise SystemExit('Unable to download secp256k1 library: HTTP-Status: %d', status_code) 74 except requests.exceptions.RequestException as e: 75 raise SystemExit('Unable to download secp256k1 library: %s', str(e)) 76 77 78class egg_info(_egg_info): 79 def run(self): 80 # Ensure library has been downloaded (sdist might have been skipped) 81 download_library(self) 82 83 _egg_info.run(self) 84 85 86class sdist(_sdist): 87 def run(self): 88 download_library(self) 89 _sdist.run(self) 90 91 92if _bdist_wheel: 93 94 class bdist_wheel(_bdist_wheel): 95 def run(self): 96 download_library(self) 97 _bdist_wheel.run(self) 98 99 100else: 101 bdist_wheel = None 102 103 104class build_clib(_build_clib): 105 def initialize_options(self): 106 _build_clib.initialize_options(self) 107 self.build_flags = None 108 109 def finalize_options(self): 110 _build_clib.finalize_options(self) 111 if self.build_flags is None: 112 self.build_flags = {'include_dirs': [], 'library_dirs': [], 'define': []} 113 114 def get_source_files(self): 115 # Ensure library has been downloaded (sdist might have been skipped) 116 download_library(self) 117 118 return [ 119 absolute(os.path.join(root, filename)) 120 for root, _, filenames in os.walk(absolute('libsecp256k1')) 121 for filename in filenames 122 ] 123 124 def build_libraries(self, libraries): 125 raise Exception('build_libraries') 126 127 def check_library_list(self, libraries): 128 raise Exception('check_library_list') 129 130 def get_library_names(self): 131 return build_flags('libsecp256k1', 'l', os.path.abspath(self.build_temp)) 132 133 def run(self): 134 if has_system_lib(): 135 log.info('Using system library') 136 return 137 138 build_temp = os.path.abspath(self.build_temp) 139 140 try: 141 os.makedirs(build_temp) 142 except OSError as e: 143 if e.errno != errno.EEXIST: 144 raise 145 146 if not os.path.exists(absolute('libsecp256k1')): 147 # library needs to be downloaded 148 self.get_source_files() 149 150 if not os.path.exists(absolute('libsecp256k1/configure')): 151 # configure script hasn't been generated yet 152 autogen = absolute('libsecp256k1/autogen.sh') 153 os.chmod(absolute(autogen), 0o755) 154 subprocess.check_call([autogen], cwd=absolute('libsecp256k1')) 155 156 for filename in [ 157 'libsecp256k1/configure', 158 'libsecp256k1/build-aux/compile', 159 'libsecp256k1/build-aux/config.guess', 160 'libsecp256k1/build-aux/config.sub', 161 'libsecp256k1/build-aux/depcomp', 162 'libsecp256k1/build-aux/install-sh', 163 'libsecp256k1/build-aux/missing', 164 'libsecp256k1/build-aux/test-driver', 165 ]: 166 try: 167 os.chmod(absolute(filename), 0o755) 168 except OSError as e: 169 # some of these files might not exist depending on autoconf version 170 if e.errno != errno.ENOENT: 171 # If the error isn't 'No such file or directory' something 172 # else is wrong and we want to know about it 173 raise 174 175 cmd = [ 176 absolute('libsecp256k1/configure'), 177 '--disable-shared', 178 '--enable-static', 179 '--disable-dependency-tracking', 180 '--with-pic', 181 '--enable-module-recovery', 182 '--prefix', 183 os.path.abspath(self.build_clib), 184 '--enable-experimental', 185 '--enable-module-ecdh', 186 '--enable-benchmark=no', 187 '--enable-tests=no', 188 '--enable-openssl-tests=no', 189 '--enable-exhaustive-tests=no', 190 ] 191 192 log.debug('Running configure: {}'.format(' '.join(cmd))) 193 subprocess.check_call(cmd, cwd=build_temp) 194 195 subprocess.check_call([MAKE], cwd=build_temp) 196 subprocess.check_call([MAKE, 'install'], cwd=build_temp) 197 198 self.build_flags['include_dirs'].extend(build_flags('libsecp256k1', 'I', build_temp)) 199 self.build_flags['library_dirs'].extend(build_flags('libsecp256k1', 'L', build_temp)) 200 if not has_system_lib(): 201 self.build_flags['define'].append(('CFFI_ENABLE_RECOVERY', None)) 202 else: 203 pass 204 205 206class build_ext(_build_ext): 207 def run(self): 208 if self.distribution.has_c_libraries(): 209 _build_clib = self.get_finalized_command('build_clib') 210 self.include_dirs.append(os.path.join(_build_clib.build_clib, 'include')) 211 self.include_dirs.extend(_build_clib.build_flags['include_dirs']) 212 213 self.library_dirs.insert(0, os.path.join(_build_clib.build_clib, 'lib')) 214 self.library_dirs.extend(_build_clib.build_flags['library_dirs']) 215 216 self.define = _build_clib.build_flags['define'] 217 218 return _build_ext.run(self) 219 220 221class develop(_develop): 222 def run(self): 223 if not has_system_lib(): 224 raise DistutilsError( 225 "This library is not usable in 'develop' mode when using the " 226 'bundled libsecp256k1. See README for details.' 227 ) 228 _develop.run(self) 229 230 231package_data = {'coincurve': ['py.typed']} 232 233if BUILDING_FOR_WINDOWS: 234 235 class Distribution(_Distribution): 236 def is_pure(self): 237 return False 238 239 package_data['coincurve'].append('libsecp256k1.dll') 240 setup_kwargs = dict() 241else: 242 243 class Distribution(_Distribution): 244 def has_c_libraries(self): 245 return not has_system_lib() 246 247 setup_kwargs = dict( 248 setup_requires=['cffi>=1.3.0', 'requests'], 249 ext_package='coincurve', 250 cffi_modules=['_cffi_build/build.py:ffi'], 251 cmdclass={ 252 'build_clib': build_clib, 253 'build_ext': build_ext, 254 'develop': develop, 255 'egg_info': egg_info, 256 'sdist': sdist, 257 'bdist_wheel': bdist_wheel, 258 }, 259 ) 260 261 262setup( 263 name='coincurve', 264 version='16.0.0', 265 266 description='Cross-platform Python CFFI bindings for libsecp256k1', 267 long_description=open('README.md', 'r').read(), 268 long_description_content_type='text/markdown', 269 author_email='Ofek Lev <oss@ofek.dev>', 270 license='MIT OR Apache-2.0', 271 272 python_requires='>=3.6', 273 install_requires=['asn1crypto', 'cffi>=1.3.0'], 274 275 packages=find_packages(exclude=('_cffi_build', '_cffi_build.*', 'libsecp256k1', 'tests')), 276 package_data=package_data, 277 278 distclass=Distribution, 279 zip_safe=False, 280 281 project_urls={ 282 'Documentation': 'https://ofek.dev/coincurve/', 283 'Issues': 'https://github.com/ofek/coincurve/issues', 284 'Source': 'https://github.com/ofek/coincurve', 285 }, 286 keywords=[ 287 'secp256k1', 288 'crypto', 289 'elliptic curves', 290 'bitcoin', 291 'ethereum', 292 'cryptocurrency', 293 ], 294 classifiers=[ 295 'Development Status :: 5 - Production/Stable', 296 'Intended Audience :: Developers', 297 'License :: OSI Approved :: MIT License', 298 'License :: OSI Approved :: Apache Software License', 299 'Natural Language :: English', 300 'Operating System :: OS Independent', 301 'Programming Language :: Python :: 3', 302 'Programming Language :: Python :: 3.6', 303 'Programming Language :: Python :: 3.7', 304 'Programming Language :: Python :: 3.8', 305 'Programming Language :: Python :: 3.9', 306 'Programming Language :: Python :: 3.10', 307 'Programming Language :: Python :: Implementation :: CPython', 308 'Programming Language :: Python :: Implementation :: PyPy', 309 'Topic :: Software Development :: Libraries', 310 'Topic :: Security :: Cryptography', 311 ], 312 **setup_kwargs 313) 314