setup.py 9.97 KB
Newer Older
1
"""setup for the dlib project
Ehsan Azarnasab's avatar
Ehsan Azarnasab committed
2
3
 Copyright (C) 2015  Ehsan Azar (dashesy@linux.com)
 License: Boost Software License   See LICENSE.txt for the full license.
4

5
6
7
8
This file basically just uses CMake to compile the dlib python bindings project
located in the tools/python folder and then puts the outputs into standard
python packages.

9
10
11
12
To build the dlib:
    python setup.py build
To build and install:
    python setup.py install
13
To package the wheel (after pip installing twine and wheel):
14
    python setup.py bdist_wheel
Davis King's avatar
Davis King committed
15
To upload the binary wheel to PyPi
16
    twine upload dist/*.whl
Davis King's avatar
Davis King committed
17
To upload the source distribution to PyPi
Davis King's avatar
Davis King committed
18
19
    python setup.py sdist 
    twine upload dist/dlib-*.tar.gz
20
To exclude certain options in the cmake config use --no:
21
    for example:
22
    --no USE_AVX_INSTRUCTIONS: will set -DUSE_AVX_INSTRUCTIONS=no
23
Additional options:
24
25
    --compiler-flags: pass flags onto the compiler, e.g. --compiler-flags "-Os -Wall" passes -Os -Wall onto GCC.
    -G: Set the CMake generator.  E.g. -G "Visual Studio 14 2015"
26
    --clean: delete any previous build folders and rebuild.  You should do this if you change any build options
27
28
29
30
             by setting --compiler-flags or --no since the last time you ran a build.  This will
             ensure the changes take effect.
    --set: set arbitrary cmake options e.g. --set CUDA_HOST_COMPILER=/usr/bin/gcc-6.4.0
           passes -DCUDA_HOST_COMPILER=/usr/bin/gcc-6.4.0 to CMake.
31
32
"""
import os
33
import re
34
import sys
35
import shutil
36
import platform
37
import subprocess
38
import multiprocessing
39
from distutils import log
40
from math import ceil,floor
41

42
43
44
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
from distutils.version import LooseVersion
45

46

47
def get_extra_cmake_options():
48
    """read --clean, --no, --set, --compiler-flags, and -G options from the command line and add them as cmake switches.
Ehsan Azarnasab's avatar
Ehsan Azarnasab committed
49
    """
50
51
    _cmake_extra_options = []
    _clean_build_folder = False
52

Ehsan Azarnasab's avatar
Ehsan Azarnasab committed
53
    opt_key = None
54

55
    argv = [arg for arg in sys.argv]  # take a copy
56
    # parse command line options and consume those we care about
57
    for arg in argv:
58
59
        if opt_key == 'compiler-flags':
            _cmake_extra_options.append('-DCMAKE_CXX_FLAGS={arg}'.format(arg=arg.strip()))
60
61
        elif opt_key == 'G':
            _cmake_extra_options += ['-G', arg.strip()]
62
        elif opt_key == 'no':
63
            _cmake_extra_options.append('-D{arg}=no'.format(arg=arg.strip()))
64
65
        elif opt_key == 'set':
            _cmake_extra_options.append('-D{arg}'.format(arg=arg.strip()))
Ehsan Azarnasab's avatar
Ehsan Azarnasab committed
66
67
68

        if opt_key:
            sys.argv.remove(arg)
69
            opt_key = None
Ehsan Azarnasab's avatar
Ehsan Azarnasab committed
70
            continue
71

72
73
        if arg == '--clean':
            _clean_build_folder = True
74
75
76
            sys.argv.remove(arg)
            continue

77
78
79
80
81
        if arg == '--yes':
            print("The --yes options to dlib's setup.py don't do anything since all these options ")
            print("are on by default.  So --yes has been removed.  Do not give it to setup.py.")
            sys.exit(1)
        if arg in ['--no', '--set', '--compiler-flags']:
82
            opt_key = arg[2:].lower()
Ehsan Azarnasab's avatar
Ehsan Azarnasab committed
83
84
            sys.argv.remove(arg)
            continue
85
86
87
88
        if arg in ['-G']:
            opt_key = arg[1:]
            sys.argv.remove(arg)
            continue
89

90
    return _cmake_extra_options, _clean_build_folder
91

92
cmake_extra_options,clean_build_folder = get_extra_cmake_options()
93
94


95
96
97
98
class CMakeExtension(Extension):
    def __init__(self, name, sourcedir=''):
        Extension.__init__(self, name, sources=[])
        self.sourcedir = os.path.abspath(sourcedir)
Ehsan Azarnasab's avatar
Ehsan Azarnasab committed
99

100
101
102
103
104
105
106
107
108
109
def rmtree(name):
    """remove a directory and its subdirectories.
    """
    def remove_read_only(func, path, exc):
        excvalue = exc[1]
        if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES:
            os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
            func(path)
        else:
            raise
110

111
112
113
    if os.path.exists(name):
        log.info('Removing old directory {}'.format(name))
        shutil.rmtree(name, ignore_errors=False, onerror=remove_read_only)
114
115


116
class CMakeBuild(build_ext):
117

118
    def get_cmake_version(self):
119
        try:
120
            out = subprocess.check_output(['cmake', '--version'])
121
122
123
        except:
            sys.stderr.write("\nERROR: CMake must be installed to build dlib\n\n") 
            sys.exit(1)
124
        return re.search(r'version\s*([\d.]+)', out.decode()).group(1)
125

126
    def run(self):
Davis King's avatar
Davis King committed
127
        cmake_version = self.get_cmake_version()
128
        if platform.system() == "Windows":
Davis King's avatar
Davis King committed
129
            if LooseVersion(cmake_version) < '3.1.0':
130
131
                sys.stderr.write("\nERROR: CMake >= 3.1.0 is required on Windows\n\n")
                sys.exit(1)
132

133
134
        for ext in self.extensions:
            self.build_extension(ext)
135

136
137
    def build_extension(self, ext):
        extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
138

139
140
        cmake_args = ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + extdir,
                      '-DPYTHON_EXECUTABLE=' + sys.executable]
141

142
        cmake_args += cmake_extra_options 
143

144
145
        cfg = 'Debug' if self.debug else 'Release'
        build_args = ['--config', cfg]
146

147
148
149
150
        if platform.system() == "Windows":
            cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir)]
            if sys.maxsize > 2**32:
                cmake_args += ['-A', 'x64']
Davis King's avatar
Davis King committed
151
152
            # Do a parallel build
            build_args += ['--', '/m'] 
153
154
        else:
            cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg]
155
            # Do a parallel build
156
            build_args += ['--', '-j'+str(num_available_cpu_cores(2))]
157

158
        build_folder = os.path.abspath(self.build_temp)
159

160
161
162
163
        if clean_build_folder:
            rmtree(build_folder)
        if not os.path.exists(build_folder):
            os.makedirs(build_folder)
164

Davis King's avatar
Davis King committed
165
166
167
        cmake_setup = ['cmake', ext.sourcedir] + cmake_args
        cmake_build = ['cmake', '--build', '.'] + build_args

168
        print("Building extension for Python {}".format(sys.version.split('\n',1)[0]))
Davis King's avatar
Davis King committed
169
        print("Invoking CMake setup: '{}'".format(' '.join(cmake_setup)))
170
        sys.stdout.flush()
Davis King's avatar
Davis King committed
171
172
        subprocess.check_call(cmake_setup, cwd=build_folder)
        print("Invoking CMake build: '{}'".format(' '.join(cmake_build)))
173
        sys.stdout.flush()
Davis King's avatar
Davis King committed
174
        subprocess.check_call(cmake_build, cwd=build_folder)
175

176
def num_available_cpu_cores(ram_per_build_process_in_gb):
Davis King's avatar
Davis King committed
177
    if 'TRAVIS' in os.environ and os.environ['TRAVIS']=='true':
Davis King's avatar
Davis King committed
178
179
180
        # When building on travis-ci, just use 2 cores since travis-ci limits
        # you to that regardless of what the hardware might suggest.
        return 2 
181
182
183
    try:
        mem_bytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES')  
        mem_gib = mem_bytes/(1024.**3)
184
185
        num_cores = multiprocessing.cpu_count() 
        # make sure we have enough ram for each build process.
186
187
188
189
190
191
192
        mem_cores = int(floor(mem_gib/float(ram_per_build_process_in_gb)+0.5));
        # We are limited either by RAM or CPU cores.  So pick the limiting amount
        # and return that.
        return max(min(num_cores, mem_cores), 1)
    except ValueError:
        return 2 # just assume 2 if we can't get the os to tell us the right answer.

193

194
195
196
from setuptools.command.test import test as TestCommand
class PyTest(TestCommand):
    user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")]
197

198
199
    def initialize_options(self):
        TestCommand.initialize_options(self)
200
        self.pytest_args = '--ignore docs --ignore dlib'
201

202
203
204
205
206
207
    def run_tests(self):
        import shlex
        #import here, cause outside the eggs aren't loaded
        import pytest
        errno = pytest.main(shlex.split(self.pytest_args))
        sys.exit(errno)
208

209
210
211
212
213
214
215
def read_version_from_cmakelists(cmake_file):
    """Read version information
    """
    major = re.findall("set\(CPACK_PACKAGE_VERSION_MAJOR.*\"(.*)\"", open(cmake_file).read())[0]
    minor = re.findall("set\(CPACK_PACKAGE_VERSION_MINOR.*\"(.*)\"", open(cmake_file).read())[0]
    patch = re.findall("set\(CPACK_PACKAGE_VERSION_PATCH.*\"(.*)\"", open(cmake_file).read())[0]
    return major + '.' + minor + '.' + patch
216

217
218
219
220
def read_entire_file(fname):
    """Read text out of a file relative to setup.py.
    """
    return open(os.path.join(fname)).read()
221

222
223
setup(
    name='dlib',
224
    version=read_version_from_cmakelists('dlib/CMakeLists.txt'),
225
    description='A toolkit for making real world machine learning and data analysis applications',
226
    long_description='See http://dlib.net for documentation.',
227
228
229
230
    author='Davis King',
    author_email='davis@dlib.net',
    url='https://github.com/davisking/dlib',
    license='Boost Software License',
231
    ext_modules=[CMakeExtension('_dlib_pybind11','tools/python')],
232
    cmdclass=dict(build_ext=CMakeBuild, test=PyTest),
233
    zip_safe=False,
234
235
    # We need an older more-itertools version because v6 broke pytest (for everyone, not just dlib)
    tests_require=['pytest==3.8', 'more-itertools<6.0.0'],
236
    install_requires=['cmake'],
237
    packages=['dlib'],
238
    package_dir={'': 'tools/python'},
239
    keywords=['dlib', 'Computer Vision', 'Machine Learning'],
Ehsan Azarnasab's avatar
Ehsan Azarnasab committed
240
241
242
243
244
245
246
247
248
249
250
251
252
253
    classifiers=[
        'Development Status :: 5 - Production/Stable',
        'Intended Audience :: Science/Research',
        'Intended Audience :: Developers',
        'Operating System :: MacOS :: MacOS X',
        'Operating System :: POSIX',
        'Operating System :: POSIX :: Linux',
        'Operating System :: Microsoft',
        'Operating System :: Microsoft :: Windows',
        'Programming Language :: C++',
        'Programming Language :: Python',
        'Programming Language :: Python :: 2',
        'Programming Language :: Python :: 2.6',
        'Programming Language :: Python :: 2.7',
254
255
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.4',
Davis King's avatar
Davis King committed
256
257
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
Ehsan Azarnasab's avatar
Ehsan Azarnasab committed
258
        'Topic :: Scientific/Engineering',
Davis King's avatar
Davis King committed
259
        'Topic :: Scientific/Engineering :: Artificial Intelligence',
Ehsan Azarnasab's avatar
Ehsan Azarnasab committed
260
261
262
        'Topic :: Scientific/Engineering :: Image Recognition',
        'Topic :: Software Development',
    ],
263
)