setup.py 16.3 KB
Newer Older
wxchan's avatar
wxchan committed
1
# coding: utf-8
2
# pylint: disable=invalid-name, exec-used, C0111
wxchan's avatar
wxchan committed
3
4
"""Setup lightgbm package."""
from __future__ import absolute_import
5

Guolin Ke's avatar
Guolin Ke committed
6
import distutils
7
import io
8
import logging
9
import os
Guolin Ke's avatar
Guolin Ke committed
10
import shutil
11
import struct
12
import subprocess
13
14
import sys

15
from platform import system
16
from setuptools import find_packages, setup
17
18
19
from setuptools.command.install import install
from setuptools.command.install_lib import install_lib
from setuptools.command.sdist import sdist
20

21
22

def find_lib():
23
    libpath_py = os.path.join(CURRENT_DIR, 'lightgbm', 'libpath.py')
24
25
26
27
    libpath = {'__file__': libpath_py}
    exec(compile(open(libpath_py, "rb").read(), libpath_py, 'exec'), libpath, libpath)

    LIB_PATH = [os.path.relpath(path, CURRENT_DIR) for path in libpath['find_lib_path']()]
28
    logger.info("Installing lib_lightgbm from: %s" % LIB_PATH)
29
30
31
32
33
34
    return LIB_PATH


def copy_files(use_gpu=False):

    def copy_files_helper(folder_name):
35
        src = os.path.join(CURRENT_DIR, os.path.pardir, folder_name)
36
        if os.path.exists(src):
37
            dst = os.path.join(CURRENT_DIR, 'compile', folder_name)
38
            shutil.rmtree(dst, ignore_errors=True)
39
            distutils.dir_util.copy_tree(src, dst, verbose=0)
40
        else:
41
            raise Exception('Cannot copy {0} folder'.format(src))
42

43
    if not os.path.isfile(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt')):
44
45
        copy_files_helper('include')
        copy_files_helper('src')
46
47
48
        if not os.path.exists(os.path.join(CURRENT_DIR, "compile", "windows")):
            os.makedirs(os.path.join(CURRENT_DIR, "compile", "windows"))
        distutils.file_util.copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "windows", "LightGBM.sln"),
49
50
                                      os.path.join(CURRENT_DIR, "compile", "windows", "LightGBM.sln"),
                                      verbose=0)
51
        distutils.file_util.copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "windows", "LightGBM.vcxproj"),
52
53
                                      os.path.join(CURRENT_DIR, "compile", "windows", "LightGBM.vcxproj"),
                                      verbose=0)
Guolin Ke's avatar
Guolin Ke committed
54
        if use_gpu:
55
            copy_files_helper('compute')
56
        distutils.file_util.copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "CMakeLists.txt"),
57
58
                                      os.path.join(CURRENT_DIR, "compile", "CMakeLists.txt"),
                                      verbose=0)
59
        distutils.file_util.copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "LICENSE"),
60
61
                                      os.path.join(CURRENT_DIR, "LICENSE"),
                                      verbose=0)
wxchan's avatar
wxchan committed
62
63


64
def clear_path(path):
65
66
67
68
69
70
71
72
73
74
75
76
    if os.path.isdir(path):
        contents = os.listdir(path)
        for file_name in contents:
            file_path = os.path.join(path, file_name)
            if os.path.isfile(file_path):
                os.remove(file_path)
            else:
                shutil.rmtree(file_path)


def silent_call(cmd, raise_error=False, error_msg=''):
    try:
77
        output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
78
        with open(LOG_PATH, "ab") as log:
79
80
81
82
            log.write(output)
        return 0
    except Exception as err:
        if isinstance(err, subprocess.CalledProcessError):
83
            with open(LOG_PATH, "ab") as log:
84
                log.write(err.output)
85
        if raise_error:
86
            raise Exception("\n".join((error_msg, LOG_NOTICE)))
87
        return 1
88
89


90
def compile_cpp(use_mingw=False, use_gpu=False, use_mpi=False,
91
92
                use_hdfs=False, boost_root=None, boost_dir=None,
                boost_include_dir=None, boost_librarydir=None,
93
                opencl_include_dir=None, opencl_library=None,
94
95
                openmp_include_dir=None, openmp_library=None,
                nomp=False, bit32=False):
96

97
98
99
100
    if os.path.exists(os.path.join(CURRENT_DIR, "build_cpp")):
        shutil.rmtree(os.path.join(CURRENT_DIR, "build_cpp"))
    os.makedirs(os.path.join(CURRENT_DIR, "build_cpp"))
    os.chdir(os.path.join(CURRENT_DIR, "build_cpp"))
101

102
103
    logger.info("Starting to compile the library.")

104
    cmake_cmd = ["cmake", "../compile/"]
105
    if use_gpu:
106
        cmake_cmd.append("-DUSE_GPU=ON")
107
108
109
110
111
112
113
114
115
116
117
118
119
120
        if boost_root:
            cmake_cmd.append("-DBOOST_ROOT={0}".format(boost_root))
        if boost_dir:
            cmake_cmd.append("-DBoost_DIR={0}".format(boost_dir))
        if boost_include_dir:
            cmake_cmd.append("-DBoost_INCLUDE_DIR={0}".format(boost_include_dir))
        if boost_librarydir:
            cmake_cmd.append("-DBOOST_LIBRARYDIR={0}".format(boost_librarydir))
        if opencl_include_dir:
            cmake_cmd.append("-DOpenCL_INCLUDE_DIR={0}".format(opencl_include_dir))
        if opencl_library:
            cmake_cmd.append("-DOpenCL_LIBRARY={0}".format(opencl_library))
    if use_mpi:
        cmake_cmd.append("-DUSE_MPI=ON")
121
122
    if nomp:
        cmake_cmd.append("-DUSE_OPENMP=OFF")
123
124
    if use_hdfs:
        cmake_cmd.append("-DUSE_HDFS=ON")
125

126
    if system() in ('Windows', 'Microsoft'):
127
        if use_mingw:
128
129
            if use_mpi:
                raise Exception('MPI version cannot be compiled by MinGW due to the miss of MPI library in it')
130
131
            logger.info("Starting to compile with CMake and MinGW.")
            silent_call(cmake_cmd + ["-G", "MinGW Makefiles"], raise_error=True,
132
                        error_msg='Please install CMake and all required dependencies first')
133
134
            silent_call(["mingw32-make.exe", "_lightgbm"], raise_error=True,
                        error_msg='Please install MinGW first')
135
        else:
136
            status = 1
137
            lib_path = os.path.join(CURRENT_DIR, "compile", "windows", "x64", "DLL", "lib_lightgbm.dll")
138
            if not any((use_gpu, use_mpi, use_hdfs, nomp, bit32)):
139
                logger.info("Starting to compile with MSBuild from existing solution file.")
140
                platform_toolsets = ("v142", "v141", "v140")
141
                for pt in platform_toolsets:
142
143
                    status = silent_call(["MSBuild",
                                          os.path.join(CURRENT_DIR, "compile", "windows", "LightGBM.sln"),
144
145
146
147
148
149
                                          "/p:Configuration=DLL",
                                          "/p:Platform=x64",
                                          "/p:PlatformToolset={0}".format(pt)])
                    if status == 0 and os.path.exists(lib_path):
                        break
                    else:
150
                        clear_path(os.path.join(CURRENT_DIR, "compile", "windows", "x64"))
151
152
153
                if status != 0 or not os.path.exists(lib_path):
                    logger.warning("Compilation with MSBuild from existing solution file failed.")
            if status != 0 or not os.path.exists(lib_path):
154
                arch = "Win32" if bit32 else "x64"
155
                vs_versions = ("Visual Studio 16 2019", "Visual Studio 15 2017", "Visual Studio 14 2015")
156
157
                for vs in vs_versions:
                    logger.info("Starting to compile with %s." % vs)
158
                    status = silent_call(cmake_cmd + ["-G", vs, "-A", arch])
159
160
161
                    if status == 0:
                        break
                    else:
162
                        clear_path(os.path.join(CURRENT_DIR, "build_cpp"))
163
                if status != 0:
164
                    raise Exception("\n".join(('Please install Visual Studio or MS Build and all required dependencies first',
165
                                    LOG_NOTICE)))
166
167
                silent_call(["cmake", "--build", ".", "--target", "_lightgbm", "--config", "Release"], raise_error=True,
                            error_msg='Please install CMake first')
168
    else:  # Linux, Darwin (macOS), etc.
169
        logger.info("Starting to compile with CMake.")
170
        # Apple Clang with OpenMP
171
172
        if system() == 'Darwin' and not nomp and not (os.environ.get('CC', '').split('/')[-1].split('\\')[-1].startswith('gcc')
                                                      and os.environ.get('CXX', '').split('/')[-1].split('\\')[-1].startswith('g++')):
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
            def get_cmake_opts(openmp_include_dir, openmp_library):
                if openmp_include_dir and openmp_library:
                    return ['-DOpenMP_C_FLAGS=-Xpreprocessor -fopenmp -I{0}'.format(openmp_include_dir),
                            '-DOpenMP_C_LIB_NAMES=omp',
                            '-DOpenMP_CXX_FLAGS=-Xpreprocessor -fopenmp -I{0}'.format(openmp_include_dir),
                            '-DOpenMP_CXX_LIB_NAMES=omp',
                            '-DOpenMP_omp_LIBRARY={0}'.format(openmp_library)]
                else:
                    return []

            status = silent_call(cmake_cmd + get_cmake_opts(openmp_include_dir, openmp_library))
            status += silent_call(["make", "_lightgbm", "-j4"])
            if status != 0:
                logger.warning("Compilation failed.")
                logger.info("Starting to compile with Homebrew OpenMP paths guesses.")
                clear_path(os.path.join(CURRENT_DIR, "build_cpp"))
                status = silent_call(cmake_cmd + get_cmake_opts('/usr/local/opt/libomp/include',
                                                                '/usr/local/opt/libomp/lib/libomp.dylib'))
                status += silent_call(["make", "_lightgbm", "-j4"])
            if status != 0:
                logger.warning("Compilation failed.")
                logger.info("Starting to compile with MacPorts OpenMP paths guesses.")
                clear_path(os.path.join(CURRENT_DIR, "build_cpp"))
                silent_call(cmake_cmd + get_cmake_opts('/opt/local/include/libomp',
                                                       '/opt/local/lib/libomp/libomp.dylib'),
                            raise_error=True, error_msg='Please install CMake and all required dependencies first')
                silent_call(["make", "_lightgbm", "-j4"], raise_error=True,
                            error_msg='An error has occurred while building lightgbm library file')
        else:
            silent_call(cmake_cmd, raise_error=True, error_msg='Please install CMake and all required dependencies first')
            silent_call(["make", "_lightgbm", "-j4"], raise_error=True,
                        error_msg='An error has occurred while building lightgbm library file')
205
    os.chdir(CURRENT_DIR)
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221


class CustomInstallLib(install_lib):

    def install(self):
        outfiles = install_lib.install(self)
        src = find_lib()[0]
        dst = os.path.join(self.install_dir, 'lightgbm')
        dst, _ = self.copy_file(src, dst)
        outfiles.append(dst)
        return outfiles


class CustomInstall(install):

    user_options = install.user_options + [
222
223
224
        ('mingw', 'm', 'Compile with MinGW'),
        ('gpu', 'g', 'Compile GPU version'),
        ('mpi', None, 'Compile MPI version'),
225
        ('nomp', None, 'Compile version without OpenMP support'),
226
        ('hdfs', 'h', 'Compile HDFS version'),
227
        ('bit32', None, 'Compile 32-bit version'),
228
229
230
231
232
233
        ('precompile', 'p', 'Use precompiled library'),
        ('boost-root=', None, 'Boost preferred installation prefix'),
        ('boost-dir=', None, 'Directory with Boost package configuration file'),
        ('boost-include-dir=', None, 'Directory containing Boost headers'),
        ('boost-librarydir=', None, 'Preferred Boost library directory'),
        ('opencl-include-dir=', None, 'OpenCL include directory'),
234
235
236
        ('opencl-library=', None, 'Path to OpenCL library'),
        ('openmp-include-dir=', None, 'OpenMP include directory'),
        ('openmp-library=', None, 'Path to OpenMP library')
237
238
239
240
241
242
    ]

    def initialize_options(self):
        install.initialize_options(self)
        self.mingw = 0
        self.gpu = 0
243
244
245
246
247
248
        self.boost_root = None
        self.boost_dir = None
        self.boost_include_dir = None
        self.boost_librarydir = None
        self.opencl_include_dir = None
        self.opencl_library = None
249
250
        self.openmp_include_dir = None
        self.openmp_library = None
251
        self.mpi = 0
252
        self.hdfs = 0
253
        self.precompile = 0
254
        self.nomp = 0
255
        self.bit32 = 0
256
257

    def run(self):
258
259
260
261
262
263
264
        if (8 * struct.calcsize("P")) != 64:
            if self.bit32:
                logger.warning("You're installing 32-bit version. "
                               "This version is slow and untested, so use it on your own risk.")
            else:
                raise Exception("Cannot install LightGBM in 32-bit Python, "
                                "please use 64-bit Python instead.")
265
        open(LOG_PATH, 'wb').close()
266
267
        if not self.precompile:
            copy_files(use_gpu=self.gpu)
268
            compile_cpp(use_mingw=self.mingw, use_gpu=self.gpu, use_mpi=self.mpi,
269
                        use_hdfs=self.hdfs, boost_root=self.boost_root, boost_dir=self.boost_dir,
270
                        boost_include_dir=self.boost_include_dir, boost_librarydir=self.boost_librarydir,
271
                        opencl_include_dir=self.opencl_include_dir, opencl_library=self.opencl_library,
272
273
                        openmp_include_dir=self.openmp_include_dir, openmp_library=self.openmp_library,
                        nomp=self.nomp, bit32=self.bit32)
274
        install.run(self)
275
276
        if os.path.isfile(LOG_PATH):
            os.remove(LOG_PATH)
277
278
279
280
281


class CustomSdist(sdist):

    def run(self):
Guolin Ke's avatar
Guolin Ke committed
282
        copy_files(use_gpu=True)
283
284
285
286
287
288
289
        open(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt'), 'w').close()
        if os.path.exists(os.path.join(CURRENT_DIR, 'lightgbm', 'Release')):
            shutil.rmtree(os.path.join(CURRENT_DIR, 'lightgbm', 'Release'))
        if os.path.exists(os.path.join(CURRENT_DIR, 'lightgbm', 'windows', 'x64')):
            shutil.rmtree(os.path.join(CURRENT_DIR, 'lightgbm', 'windows', 'x64'))
        if os.path.isfile(os.path.join(CURRENT_DIR, 'lightgbm', 'lib_lightgbm.so')):
            os.remove(os.path.join(CURRENT_DIR, 'lightgbm', 'lib_lightgbm.so'))
290
        sdist.run(self)
291
292
        if os.path.isfile(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt')):
            os.remove(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt'))
293
294
295


if __name__ == "__main__":
296
297
298
299
300
    CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
    LOG_PATH = os.path.join(os.path.expanduser('~'), 'LightGBM_compilation.log')
    LOG_NOTICE = "The full version of error log was saved into {0}".format(LOG_PATH)
    if os.path.isfile(os.path.join(CURRENT_DIR, os.path.pardir, 'VERSION.txt')):
        distutils.file_util.copy_file(os.path.join(CURRENT_DIR, os.path.pardir, 'VERSION.txt'),
301
302
                                      os.path.join(CURRENT_DIR, 'lightgbm', 'VERSION.txt'),
                                      verbose=0)
303
304
    version = io.open(os.path.join(CURRENT_DIR, 'lightgbm', 'VERSION.txt'), encoding='utf-8').read().strip()
    readme = io.open(os.path.join(CURRENT_DIR, 'README.rst'), encoding='utf-8').read()
305

306
    sys.path.insert(0, CURRENT_DIR)
307

308
309
310
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger('LightGBM')

Guolin Ke's avatar
Guolin Ke committed
311
312
313
    setup(name='lightgbm',
          version=version,
          description='LightGBM Python Package',
314
          long_description=readme,
Guolin Ke's avatar
Guolin Ke committed
315
316
317
318
319
320
321
322
          install_requires=[
              'numpy',
              'scipy',
              'scikit-learn'
          ],
          maintainer='Guolin Ke',
          maintainer_email='guolin.ke@microsoft.com',
          zip_safe=False,
323
324
325
326
327
          cmdclass={
              'install': CustomInstall,
              'install_lib': CustomInstallLib,
              'sdist': CustomSdist,
          },
Guolin Ke's avatar
Guolin Ke committed
328
329
          packages=find_packages(),
          include_package_data=True,
330
          license='The MIT License (Microsoft)',
331
          url='https://github.com/microsoft/LightGBM',
332
333
334
335
336
337
338
339
340
341
342
343
344
          classifiers=['Development Status :: 5 - Production/Stable',
                       'Intended Audience :: Science/Research',
                       'License :: OSI Approved :: MIT License',
                       'Natural Language :: English',
                       'Operating System :: MacOS',
                       'Operating System :: Microsoft :: Windows',
                       'Operating System :: POSIX',
                       'Operating System :: Unix',
                       'Programming Language :: Python :: 2',
                       'Programming Language :: Python :: 2.7',
                       'Programming Language :: Python :: 3',
                       'Programming Language :: Python :: 3.5',
                       'Programming Language :: Python :: 3.6',
345
                       'Programming Language :: Python :: 3.7',
346
                       'Topic :: Scientific/Engineering :: Artificial Intelligence'])