setup.py 16.5 KB
Newer Older
wxchan's avatar
wxchan committed
1
2
# coding: utf-8
"""Setup lightgbm package."""
3
import logging
4
5
import os
import struct
6
import subprocess
7
import sys
8
9
from distutils.dir_util import copy_tree, create_tree, remove_tree
from distutils.file_util import copy_file
10
from platform import system
11
from typing import List, Optional
12

13
from setuptools import find_packages, setup
14
15
16
from setuptools.command.install import install
from setuptools.command.install_lib import install_lib
from setuptools.command.sdist import sdist
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from wheel.bdist_wheel import bdist_wheel

LIGHTGBM_OPTIONS = [
    ('mingw', 'm', 'Compile with MinGW'),
    ('integrated-opencl', None, 'Compile integrated OpenCL version'),
    ('gpu', 'g', 'Compile GPU version'),
    ('cuda', None, 'Compile CUDA version'),
    ('mpi', None, 'Compile MPI version'),
    ('nomp', None, 'Compile version without OpenMP support'),
    ('hdfs', 'h', 'Compile HDFS version'),
    ('bit32', None, 'Compile 32-bit version'),
    ('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'),
    ('opencl-library=', None, 'Path to OpenCL library')
]
36

37

38
def find_lib() -> List[str]:
39
    libpath_py = os.path.join(CURRENT_DIR, 'lightgbm', 'libpath.py')
40
41
42
43
    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']()]
44
    logger.info(f"Installing lib_lightgbm from: {LIB_PATH}")
45
46
47
    return LIB_PATH


48
def copy_files(integrated_opencl: bool = False, use_gpu: bool = False) -> None:
49

50
    def copy_files_helper(folder_name: str) -> None:
51
        src = os.path.join(CURRENT_DIR, os.path.pardir, folder_name)
52
        if os.path.exists(src):
53
            dst = os.path.join(CURRENT_DIR, 'compile', folder_name)
54
55
56
57
58
59
60
            if os.path.exists(dst):
                if os.path.isdir:
                    # see https://github.com/pypa/distutils/pull/21
                    remove_tree(dst)
                else:
                    os.remove(dst)
            create_tree(src, dst, verbose=0)
61
            copy_tree(src, dst, verbose=0)
62
        else:
63
            raise Exception(f'Cannot copy {src} folder')
64

65
    if not os.path.isfile(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt')):
66
67
        copy_files_helper('include')
        copy_files_helper('src')
68
69
70
71
        for submodule in os.listdir(os.path.join(CURRENT_DIR, os.path.pardir, 'external_libs')):
            if submodule == 'compute' and not use_gpu:
                continue
            copy_files_helper(os.path.join('external_libs', submodule))
72
73
        if not os.path.exists(os.path.join(CURRENT_DIR, "compile", "windows")):
            os.makedirs(os.path.join(CURRENT_DIR, "compile", "windows"))
74
75
76
77
78
79
80
81
82
        copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "windows", "LightGBM.sln"),
                  os.path.join(CURRENT_DIR, "compile", "windows", "LightGBM.sln"),
                  verbose=0)
        copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "windows", "LightGBM.vcxproj"),
                  os.path.join(CURRENT_DIR, "compile", "windows", "LightGBM.vcxproj"),
                  verbose=0)
        copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "LICENSE"),
                  os.path.join(CURRENT_DIR, "LICENSE"),
                  verbose=0)
83
84
85
86
        copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "CMakeLists.txt"),
                  os.path.join(CURRENT_DIR, "compile", "CMakeLists.txt"),
                  verbose=0)
        if integrated_opencl:
87
88
89
90
            if not os.path.exists(os.path.join(CURRENT_DIR, "compile", "cmake")):
                os.makedirs(os.path.join(CURRENT_DIR, "compile", "cmake"))
            copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "cmake", "IntegratedOpenCL.cmake"),
                      os.path.join(CURRENT_DIR, "compile", "cmake", "IntegratedOpenCL.cmake"),
91
                      verbose=0)
wxchan's avatar
wxchan committed
92
93


94
def clear_path(path: str) -> None:
95
96
97
98
99
100
101
    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:
102
                remove_tree(file_path)
103
104


105
def silent_call(cmd: List[str], raise_error: bool = False, error_msg: str = '') -> int:
106
    try:
107
        with open(LOG_PATH, "ab") as log:
108
            subprocess.check_call(cmd, stderr=log, stdout=log)
109
110
        return 0
    except Exception as err:
111
        if raise_error:
112
            raise Exception("\n".join((error_msg, LOG_NOTICE)))
113
        return 1
114
115


116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def compile_cpp(
    use_mingw: bool = False,
    use_gpu: bool = False,
    use_cuda: bool = False,
    use_mpi: bool = False,
    use_hdfs: bool = False,
    boost_root: Optional[str] = None,
    boost_dir: Optional[str] = None,
    boost_include_dir: Optional[str] = None,
    boost_librarydir: Optional[str] = None,
    opencl_include_dir: Optional[str] = None,
    opencl_library: Optional[str] = None,
    nomp: bool = False,
    bit32: bool = False,
    integrated_opencl: bool = False
) -> None:
132

133
    if os.path.exists(os.path.join(CURRENT_DIR, "build_cpp")):
134
        remove_tree(os.path.join(CURRENT_DIR, "build_cpp"))
135
136
    os.makedirs(os.path.join(CURRENT_DIR, "build_cpp"))
    os.chdir(os.path.join(CURRENT_DIR, "build_cpp"))
137

138
139
    logger.info("Starting to compile the library.")

140
    cmake_cmd = ["cmake", "../compile/"]
141
142
143
    if integrated_opencl:
        use_gpu = False
        cmake_cmd.append("-D__INTEGRATE_OPENCL=ON")
144
    if use_gpu:
145
        cmake_cmd.append("-DUSE_GPU=ON")
146
        if boost_root:
147
            cmake_cmd.append(f"-DBOOST_ROOT={boost_root}")
148
        if boost_dir:
149
            cmake_cmd.append(f"-DBoost_DIR={boost_dir}")
150
        if boost_include_dir:
151
            cmake_cmd.append(f"-DBoost_INCLUDE_DIR={boost_include_dir}")
152
        if boost_librarydir:
153
            cmake_cmd.append(f"-DBOOST_LIBRARYDIR={boost_librarydir}")
154
        if opencl_include_dir:
155
            cmake_cmd.append(f"-DOpenCL_INCLUDE_DIR={opencl_include_dir}")
156
        if opencl_library:
157
            cmake_cmd.append(f"-DOpenCL_LIBRARY={opencl_library}")
158
159
    elif use_cuda:
        cmake_cmd.append("-DUSE_CUDA=ON")
160
161
    if use_mpi:
        cmake_cmd.append("-DUSE_MPI=ON")
162
163
    if nomp:
        cmake_cmd.append("-DUSE_OPENMP=OFF")
164
165
    if use_hdfs:
        cmake_cmd.append("-DUSE_HDFS=ON")
166

167
    if system() in {'Windows', 'Microsoft'}:
168
        if use_mingw:
169
170
            if use_mpi:
                raise Exception('MPI version cannot be compiled by MinGW due to the miss of MPI library in it')
171
172
            logger.info("Starting to compile with CMake and MinGW.")
            silent_call(cmake_cmd + ["-G", "MinGW Makefiles"], raise_error=True,
173
                        error_msg='Please install CMake and all required dependencies first')
174
175
            silent_call(["mingw32-make.exe", "_lightgbm"], raise_error=True,
                        error_msg='Please install MinGW first')
176
        else:
177
            status = 1
178
            lib_path = os.path.join(CURRENT_DIR, "compile", "windows", "x64", "DLL", "lib_lightgbm.dll")
179
            if not any((use_gpu, use_cuda, use_mpi, use_hdfs, nomp, bit32, integrated_opencl)):
180
                logger.info("Starting to compile with MSBuild from existing solution file.")
181
                platform_toolsets = ("v142", "v141", "v140")
182
                for pt in platform_toolsets:
183
184
                    status = silent_call(["MSBuild",
                                          os.path.join(CURRENT_DIR, "compile", "windows", "LightGBM.sln"),
185
186
                                          "/p:Configuration=DLL",
                                          "/p:Platform=x64",
187
                                          f"/p:PlatformToolset={pt}"])
188
189
190
                    if status == 0 and os.path.exists(lib_path):
                        break
                    else:
191
                        clear_path(os.path.join(CURRENT_DIR, "compile", "windows", "x64"))
192
193
194
                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):
195
                arch = "Win32" if bit32 else "x64"
196
                vs_versions = ("Visual Studio 16 2019", "Visual Studio 15 2017", "Visual Studio 14 2015")
197
                for vs in vs_versions:
198
                    logger.info(f"Starting to compile with {vs} ({arch}).")
199
                    status = silent_call(cmake_cmd + ["-G", vs, "-A", arch])
200
201
202
                    if status == 0:
                        break
                    else:
203
                        clear_path(os.path.join(CURRENT_DIR, "build_cpp"))
204
                if status != 0:
205
                    raise Exception("\n".join(('Please install Visual Studio or MS Build and all required dependencies first',
206
                                    LOG_NOTICE)))
207
208
                silent_call(["cmake", "--build", ".", "--target", "_lightgbm", "--config", "Release"], raise_error=True,
                            error_msg='Please install CMake first')
209
    else:  # Linux, Darwin (macOS), etc.
210
        logger.info("Starting to compile with CMake.")
211
212
213
        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')
214
    os.chdir(CURRENT_DIR)
215
216
217
218


class CustomInstallLib(install_lib):

219
    def install(self) -> List[str]:
220
221
222
223
224
225
226
227
228
229
        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):

230
    user_options = install.user_options + LIGHTGBM_OPTIONS
231

232
    def initialize_options(self) -> None:
233
234
        install.initialize_options(self)
        self.mingw = 0
235
        self.integrated_opencl = 0
236
        self.gpu = 0
237
        self.cuda = 0
238
239
240
241
242
243
244
        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
        self.mpi = 0
245
        self.hdfs = 0
246
        self.precompile = 0
247
        self.nomp = 0
248
        self.bit32 = 0
249

250
    def run(self) -> None:
251
252
253
254
255
256
257
        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.")
258
        open(LOG_PATH, 'wb').close()
259
        if not self.precompile:
260
            copy_files(integrated_opencl=self.integrated_opencl, use_gpu=self.gpu)
261
            compile_cpp(use_mingw=self.mingw, use_gpu=self.gpu, use_cuda=self.cuda, use_mpi=self.mpi,
262
                        use_hdfs=self.hdfs, boost_root=self.boost_root, boost_dir=self.boost_dir,
263
                        boost_include_dir=self.boost_include_dir, boost_librarydir=self.boost_librarydir,
264
                        opencl_include_dir=self.opencl_include_dir, opencl_library=self.opencl_library,
265
                        nomp=self.nomp, bit32=self.bit32, integrated_opencl=self.integrated_opencl)
266
        install.run(self)
267
268
        if os.path.isfile(LOG_PATH):
            os.remove(LOG_PATH)
269
270


271
272
273
274
class CustomBdistWheel(bdist_wheel):

    user_options = bdist_wheel.user_options + LIGHTGBM_OPTIONS

275
    def initialize_options(self) -> None:
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
        bdist_wheel.initialize_options(self)
        self.mingw = 0
        self.integrated_opencl = 0
        self.gpu = 0
        self.cuda = 0
        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
        self.mpi = 0
        self.hdfs = 0
        self.precompile = 0
        self.nomp = 0
        self.bit32 = 0

293
    def finalize_options(self) -> None:
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
        bdist_wheel.finalize_options(self)

        install = self.reinitialize_command('install')

        install.mingw = self.mingw
        install.integrated_opencl = self.integrated_opencl
        install.gpu = self.gpu
        install.cuda = self.cuda
        install.boost_root = self.boost_root
        install.boost_dir = self.boost_dir
        install.boost_include_dir = self.boost_include_dir
        install.boost_librarydir = self.boost_librarydir
        install.opencl_include_dir = self.opencl_include_dir
        install.opencl_library = self.opencl_library
        install.mpi = self.mpi
        install.hdfs = self.hdfs
        install.precompile = self.precompile
        install.nomp = self.nomp
        install.bit32 = self.bit32


315
316
class CustomSdist(sdist):

317
    def run(self) -> None:
318
        copy_files(integrated_opencl=True, use_gpu=True)
319
320
        open(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt'), 'w').close()
        if os.path.exists(os.path.join(CURRENT_DIR, 'lightgbm', 'Release')):
321
            remove_tree(os.path.join(CURRENT_DIR, 'lightgbm', 'Release'))
322
        if os.path.exists(os.path.join(CURRENT_DIR, 'lightgbm', 'windows', 'x64')):
323
            remove_tree(os.path.join(CURRENT_DIR, 'lightgbm', 'windows', 'x64'))
324
325
        if os.path.isfile(os.path.join(CURRENT_DIR, 'lightgbm', 'lib_lightgbm.so')):
            os.remove(os.path.join(CURRENT_DIR, 'lightgbm', 'lib_lightgbm.so'))
326
        sdist.run(self)
327
328
        if os.path.isfile(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt')):
            os.remove(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt'))
329
330
331


if __name__ == "__main__":
332
333
    CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
    LOG_PATH = os.path.join(os.path.expanduser('~'), 'LightGBM_compilation.log')
334
    LOG_NOTICE = f"The full version of error log was saved into {LOG_PATH}"
335
    if os.path.isfile(os.path.join(CURRENT_DIR, os.path.pardir, 'VERSION.txt')):
336
337
        copy_file(os.path.join(CURRENT_DIR, os.path.pardir, 'VERSION.txt'),
                  os.path.join(CURRENT_DIR, 'lightgbm', 'VERSION.txt'),
338
                  verbose=0)  # type:ignore
339
340
    version = open(os.path.join(CURRENT_DIR, 'lightgbm', 'VERSION.txt'), encoding='utf-8').read().strip()
    readme = open(os.path.join(CURRENT_DIR, 'README.rst'), encoding='utf-8').read()
341

342
    sys.path.insert(0, CURRENT_DIR)
343

344
345
346
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger('LightGBM')

Guolin Ke's avatar
Guolin Ke committed
347
348
349
    setup(name='lightgbm',
          version=version,
          description='LightGBM Python Package',
350
          long_description=readme,
Guolin Ke's avatar
Guolin Ke committed
351
          install_requires=[
352
              'wheel',
Guolin Ke's avatar
Guolin Ke committed
353
354
              'numpy',
              'scipy',
355
              'scikit-learn!=0.22.0'
Guolin Ke's avatar
Guolin Ke committed
356
          ],
357
358
359
          extras_require={
              'dask': [
                  'dask[array]>=2.0.0',
360
                  'dask[dataframe]>=2.0.0',
361
362
363
364
                  'dask[distributed]>=2.0.0',
                  'pandas',
              ],
          },
Guolin Ke's avatar
Guolin Ke committed
365
366
367
          maintainer='Guolin Ke',
          maintainer_email='guolin.ke@microsoft.com',
          zip_safe=False,
368
369
370
          cmdclass={
              'install': CustomInstall,
              'install_lib': CustomInstallLib,
371
              'bdist_wheel': CustomBdistWheel,
372
373
              'sdist': CustomSdist,
          },
Guolin Ke's avatar
Guolin Ke committed
374
375
          packages=find_packages(),
          include_package_data=True,
376
          license='The MIT License (Microsoft)',
377
          url='https://github.com/microsoft/LightGBM',
378
379
380
381
382
383
384
385
386
387
          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 :: 3',
                       'Programming Language :: Python :: 3.6',
388
                       'Programming Language :: Python :: 3.7',
389
                       'Programming Language :: Python :: 3.8',
390
                       'Programming Language :: Python :: 3.9',
391
                       'Topic :: Scientific/Engineering :: Artificial Intelligence'])