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