setup.py 16.3 KB
Newer Older
wxchan's avatar
wxchan committed
1
2
3
# coding: utf-8
"""Setup lightgbm package."""
from __future__ import absolute_import
4

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

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

20
21

def find_lib():
22
    libpath_py = os.path.join(CURRENT_DIR, 'lightgbm', 'libpath.py')
23
24
25
26
    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']()]
27
    logger.info("Installing lib_lightgbm from: %s" % LIB_PATH)
28
29
30
31
32
33
    return LIB_PATH


def copy_files(use_gpu=False):

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

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


63
def clear_path(path):
64
65
66
67
68
69
70
71
72
73
74
75
    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:
76
        output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
77
        with open(LOG_PATH, "ab") as log:
78
79
80
81
            log.write(output)
        return 0
    except Exception as err:
        if isinstance(err, subprocess.CalledProcessError):
82
            with open(LOG_PATH, "ab") as log:
83
                log.write(err.output)
84
        if raise_error:
85
            raise Exception("\n".join((error_msg, LOG_NOTICE)))
86
        return 1
87
88


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

96
97
98
99
    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"))
100

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

103
    cmake_cmd = ["cmake", "../compile/"]
104
    if use_gpu:
105
        cmake_cmd.append("-DUSE_GPU=ON")
106
107
108
109
110
111
112
113
114
115
116
117
118
119
        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")
120
121
    if nomp:
        cmake_cmd.append("-DUSE_OPENMP=OFF")
122
123
    if use_hdfs:
        cmake_cmd.append("-DUSE_HDFS=ON")
124

125
    if system() in ('Windows', 'Microsoft'):
126
        if use_mingw:
127
128
            if use_mpi:
                raise Exception('MPI version cannot be compiled by MinGW due to the miss of MPI library in it')
129
130
            logger.info("Starting to compile with CMake and MinGW.")
            silent_call(cmake_cmd + ["-G", "MinGW Makefiles"], raise_error=True,
131
                        error_msg='Please install CMake and all required dependencies first')
132
133
            silent_call(["mingw32-make.exe", "_lightgbm"], raise_error=True,
                        error_msg='Please install MinGW first')
134
        else:
135
            status = 1
136
            lib_path = os.path.join(CURRENT_DIR, "compile", "windows", "x64", "DLL", "lib_lightgbm.dll")
137
            if not any((use_gpu, use_mpi, use_hdfs, nomp, bit32)):
138
                logger.info("Starting to compile with MSBuild from existing solution file.")
139
                platform_toolsets = ("v142", "v141", "v140")
140
                for pt in platform_toolsets:
141
142
                    status = silent_call(["MSBuild",
                                          os.path.join(CURRENT_DIR, "compile", "windows", "LightGBM.sln"),
143
144
145
146
147
148
                                          "/p:Configuration=DLL",
                                          "/p:Platform=x64",
                                          "/p:PlatformToolset={0}".format(pt)])
                    if status == 0 and os.path.exists(lib_path):
                        break
                    else:
149
                        clear_path(os.path.join(CURRENT_DIR, "compile", "windows", "x64"))
150
151
152
                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):
153
                arch = "Win32" if bit32 else "x64"
154
                vs_versions = ("Visual Studio 16 2019", "Visual Studio 15 2017", "Visual Studio 14 2015")
155
156
                for vs in vs_versions:
                    logger.info("Starting to compile with %s." % vs)
157
                    status = silent_call(cmake_cmd + ["-G", vs, "-A", arch])
158
159
160
                    if status == 0:
                        break
                    else:
161
                        clear_path(os.path.join(CURRENT_DIR, "build_cpp"))
162
                if status != 0:
163
                    raise Exception("\n".join(('Please install Visual Studio or MS Build and all required dependencies first',
164
                                    LOG_NOTICE)))
165
166
                silent_call(["cmake", "--build", ".", "--target", "_lightgbm", "--config", "Release"], raise_error=True,
                            error_msg='Please install CMake first')
167
    else:  # Linux, Darwin (macOS), etc.
168
        logger.info("Starting to compile with CMake.")
169
        # Apple Clang with OpenMP
170
171
        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++')):
172
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
            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')
204
    os.chdir(CURRENT_DIR)
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220


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 + [
221
222
223
        ('mingw', 'm', 'Compile with MinGW'),
        ('gpu', 'g', 'Compile GPU version'),
        ('mpi', None, 'Compile MPI version'),
224
        ('nomp', None, 'Compile version without OpenMP support'),
225
        ('hdfs', 'h', 'Compile HDFS version'),
226
        ('bit32', None, 'Compile 32-bit version'),
227
228
229
230
231
232
        ('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'),
233
234
235
        ('opencl-library=', None, 'Path to OpenCL library'),
        ('openmp-include-dir=', None, 'OpenMP include directory'),
        ('openmp-library=', None, 'Path to OpenMP library')
236
237
238
239
240
241
    ]

    def initialize_options(self):
        install.initialize_options(self)
        self.mingw = 0
        self.gpu = 0
242
243
244
245
246
247
        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
248
249
        self.openmp_include_dir = None
        self.openmp_library = None
250
        self.mpi = 0
251
        self.hdfs = 0
252
        self.precompile = 0
253
        self.nomp = 0
254
        self.bit32 = 0
255
256

    def run(self):
257
258
259
260
261
262
263
        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.")
264
        open(LOG_PATH, 'wb').close()
265
266
        if not self.precompile:
            copy_files(use_gpu=self.gpu)
267
            compile_cpp(use_mingw=self.mingw, use_gpu=self.gpu, use_mpi=self.mpi,
268
                        use_hdfs=self.hdfs, boost_root=self.boost_root, boost_dir=self.boost_dir,
269
                        boost_include_dir=self.boost_include_dir, boost_librarydir=self.boost_librarydir,
270
                        opencl_include_dir=self.opencl_include_dir, opencl_library=self.opencl_library,
271
272
                        openmp_include_dir=self.openmp_include_dir, openmp_library=self.openmp_library,
                        nomp=self.nomp, bit32=self.bit32)
273
        install.run(self)
274
275
        if os.path.isfile(LOG_PATH):
            os.remove(LOG_PATH)
276
277
278
279
280


class CustomSdist(sdist):

    def run(self):
Guolin Ke's avatar
Guolin Ke committed
281
        copy_files(use_gpu=True)
282
283
284
285
286
287
288
        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'))
289
        sdist.run(self)
290
291
        if os.path.isfile(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt')):
            os.remove(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt'))
292
293
294


if __name__ == "__main__":
295
296
297
298
299
    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'),
300
301
                                      os.path.join(CURRENT_DIR, 'lightgbm', 'VERSION.txt'),
                                      verbose=0)
302
303
    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()
304

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

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

Guolin Ke's avatar
Guolin Ke committed
310
311
312
    setup(name='lightgbm',
          version=version,
          description='LightGBM Python Package',
313
          long_description=readme,
Guolin Ke's avatar
Guolin Ke committed
314
315
316
          install_requires=[
              'numpy',
              'scipy',
317
              'scikit-learn<=0.21.3'
Guolin Ke's avatar
Guolin Ke committed
318
319
320
321
          ],
          maintainer='Guolin Ke',
          maintainer_email='guolin.ke@microsoft.com',
          zip_safe=False,
322
323
324
325
326
          cmdclass={
              'install': CustomInstall,
              'install_lib': CustomInstallLib,
              'sdist': CustomSdist,
          },
Guolin Ke's avatar
Guolin Ke committed
327
328
          packages=find_packages(),
          include_package_data=True,
329
          license='The MIT License (Microsoft)',
330
          url='https://github.com/microsoft/LightGBM',
331
332
333
334
335
336
337
338
339
340
341
342
343
          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',
344
                       'Programming Language :: Python :: 3.7',
345
                       'Topic :: Scientific/Engineering :: Artificial Intelligence'])