setup.py 15.2 KB
Newer Older
wxchan's avatar
wxchan committed
1
2
# coding: utf-8
"""Setup lightgbm package."""
3
import logging
4
import struct
5
import subprocess
6
import sys
7
8
from os import chdir
from pathlib import Path
9
from platform import system
10
11
from shutil import copyfile, copytree, rmtree
from typing import List, Optional, Union
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 = CURRENT_DIR / 'lightgbm' / 'libpath.py'
40
    libpath = {'__file__': libpath_py}
41
    exec(compile(libpath_py.read_bytes(), libpath_py, 'exec'), libpath, libpath)
42

43
    LIB_PATH = libpath['find_lib_path']()  # type: ignore
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
51
52
53
54
55
56
    def copy_files_helper(folder_name: Union[str, Path]) -> None:
        src = CURRENT_DIR.parent / folder_name
        if src.is_dir():
            dst = CURRENT_DIR / 'compile' / folder_name
            if dst.is_dir():
                rmtree(dst)
            copytree(src, dst)
57
        else:
58
            raise Exception(f'Cannot copy {src} folder')
59

60
    if not IS_SOURCE_FLAG_PATH.is_file():
61
62
        copy_files_helper('include')
        copy_files_helper('src')
63
        for submodule in (CURRENT_DIR.parent / 'external_libs').iterdir():
64
65
            submodule_stem = submodule.stem
            if submodule_stem == 'compute' and not use_gpu:
66
                continue
67
            copy_files_helper(Path('external_libs') / submodule_stem)
68
69
70
71
72
73
74
75
76
        (CURRENT_DIR / "compile" / "windows").mkdir(parents=True, exist_ok=True)
        copyfile(CURRENT_DIR.parent / "windows" / "LightGBM.sln",
                 CURRENT_DIR / "compile" / "windows" / "LightGBM.sln")
        copyfile(CURRENT_DIR.parent / "windows" / "LightGBM.vcxproj",
                 CURRENT_DIR / "compile" / "windows" / "LightGBM.vcxproj")
        copyfile(CURRENT_DIR.parent / "LICENSE",
                 CURRENT_DIR / "LICENSE")
        copyfile(CURRENT_DIR.parent / "CMakeLists.txt",
                 CURRENT_DIR / "compile" / "CMakeLists.txt")
77
        if integrated_opencl:
78
79
80
81
82
83
84
85
86
87
            (CURRENT_DIR / "compile" / "cmake").mkdir(parents=True, exist_ok=True)
            copyfile(CURRENT_DIR.parent / "cmake" / "IntegratedOpenCL.cmake",
                     CURRENT_DIR / "compile" / "cmake" / "IntegratedOpenCL.cmake")


def clear_path(path: Path) -> None:
    if path.is_dir():
        for file_name in path.iterdir():
            if file_name.is_dir():
                rmtree(file_name)
88
            else:
89
                file_name.unlink()
90
91


92
def silent_call(cmd: List[str], raise_error: bool = False, error_msg: str = '') -> int:
93
    try:
94
        with open(LOG_PATH, "ab") as log:
95
            subprocess.check_call(cmd, stderr=log, stdout=log)
96
97
        return 0
    except Exception as err:
98
        if raise_error:
99
            raise Exception("\n".join((error_msg, LOG_NOTICE)))
100
        return 1
101
102


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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:
119
120
121
122
123
    build_dir = CURRENT_DIR / "build_cpp"
    rmtree(build_dir, ignore_errors=True)
    build_dir.mkdir(parents=True)
    original_dir = Path.cwd()
    chdir(build_dir)
124

125
126
    logger.info("Starting to compile the library.")

127
    cmake_cmd = ["cmake", str(CURRENT_DIR / "compile")]
128
129
130
    if integrated_opencl:
        use_gpu = False
        cmake_cmd.append("-D__INTEGRATE_OPENCL=ON")
131
    if use_gpu:
132
        cmake_cmd.append("-DUSE_GPU=ON")
133
        if boost_root:
134
            cmake_cmd.append(f"-DBOOST_ROOT={boost_root}")
135
        if boost_dir:
136
            cmake_cmd.append(f"-DBoost_DIR={boost_dir}")
137
        if boost_include_dir:
138
            cmake_cmd.append(f"-DBoost_INCLUDE_DIR={boost_include_dir}")
139
        if boost_librarydir:
140
            cmake_cmd.append(f"-DBOOST_LIBRARYDIR={boost_librarydir}")
141
        if opencl_include_dir:
142
            cmake_cmd.append(f"-DOpenCL_INCLUDE_DIR={opencl_include_dir}")
143
        if opencl_library:
144
            cmake_cmd.append(f"-DOpenCL_LIBRARY={opencl_library}")
145
146
    elif use_cuda:
        cmake_cmd.append("-DUSE_CUDA=ON")
147
148
    if use_mpi:
        cmake_cmd.append("-DUSE_MPI=ON")
149
150
    if nomp:
        cmake_cmd.append("-DUSE_OPENMP=OFF")
151
152
    if use_hdfs:
        cmake_cmd.append("-DUSE_HDFS=ON")
153

154
    if system() in {'Windows', 'Microsoft'}:
155
        if use_mingw:
156
157
            if use_mpi:
                raise Exception('MPI version cannot be compiled by MinGW due to the miss of MPI library in it')
158
159
            logger.info("Starting to compile with CMake and MinGW.")
            silent_call(cmake_cmd + ["-G", "MinGW Makefiles"], raise_error=True,
160
                        error_msg='Please install CMake and all required dependencies first')
161
            silent_call(["mingw32-make.exe", "_lightgbm", f"-I{build_dir}", "-j4"], raise_error=True,
162
                        error_msg='Please install MinGW first')
163
        else:
164
            status = 1
165
            lib_path = CURRENT_DIR / "compile" / "windows" / "x64" / "DLL" / "lib_lightgbm.dll"
166
            if not any((use_gpu, use_cuda, use_mpi, use_hdfs, nomp, bit32, integrated_opencl)):
167
                logger.info("Starting to compile with MSBuild from existing solution file.")
168
                platform_toolsets = ("v142", "v141", "v140")
169
                for pt in platform_toolsets:
170
                    status = silent_call(["MSBuild",
171
                                          str(CURRENT_DIR / "compile" / "windows" / "LightGBM.sln"),
172
173
                                          "/p:Configuration=DLL",
                                          "/p:Platform=x64",
174
                                          f"/p:PlatformToolset={pt}"])
175
                    if status == 0 and lib_path.is_file():
176
177
                        break
                    else:
178
179
                        clear_path(CURRENT_DIR / "compile" / "windows" / "x64")
                if status != 0 or not lib_path.is_file():
180
                    logger.warning("Compilation with MSBuild from existing solution file failed.")
181
            if status != 0 or not lib_path.is_file():
182
                arch = "Win32" if bit32 else "x64"
183
                vs_versions = ("Visual Studio 16 2019", "Visual Studio 15 2017", "Visual Studio 14 2015")
184
                for vs in vs_versions:
185
                    logger.info(f"Starting to compile with {vs} ({arch}).")
186
                    status = silent_call(cmake_cmd + ["-G", vs, "-A", arch])
187
188
189
                    if status == 0:
                        break
                    else:
190
                        clear_path(build_dir)
191
                if status != 0:
192
                    raise Exception("\n".join(('Please install Visual Studio or MS Build and all required dependencies first',
193
                                    LOG_NOTICE)))
194
                silent_call(["cmake", "--build", str(build_dir), "--target", "_lightgbm", "--config", "Release"], raise_error=True,
195
                            error_msg='Please install CMake first')
196
    else:  # Linux, Darwin (macOS), etc.
197
        logger.info("Starting to compile with CMake.")
198
        silent_call(cmake_cmd, raise_error=True, error_msg='Please install CMake and all required dependencies first')
199
        silent_call(["make", "_lightgbm", f"-I{build_dir}", "-j4"], raise_error=True,
200
                    error_msg='An error has occurred while building lightgbm library file')
201
    chdir(original_dir)
202
203
204
205


class CustomInstallLib(install_lib):

206
    def install(self) -> List[str]:
207
208
        outfiles = install_lib.install(self)
        src = find_lib()[0]
209
210
        dst = Path(self.install_dir) / 'lightgbm'
        dst, _ = self.copy_file(src, str(dst))
211
212
213
214
215
216
        outfiles.append(dst)
        return outfiles


class CustomInstall(install):

217
    user_options = install.user_options + LIGHTGBM_OPTIONS
218

219
    def initialize_options(self) -> None:
220
        install.initialize_options(self)
221
222
223
224
        self.mingw = False
        self.integrated_opencl = False
        self.gpu = False
        self.cuda = False
225
226
227
228
229
230
        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
231
232
233
234
235
        self.mpi = False
        self.hdfs = False
        self.precompile = False
        self.nomp = False
        self.bit32 = False
236

237
    def run(self) -> None:
238
239
240
241
242
243
244
        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.")
245
        LOG_PATH.touch()
246
        if not self.precompile:
247
            copy_files(integrated_opencl=self.integrated_opencl, use_gpu=self.gpu)
248
            compile_cpp(use_mingw=self.mingw, use_gpu=self.gpu, use_cuda=self.cuda, use_mpi=self.mpi,
249
                        use_hdfs=self.hdfs, boost_root=self.boost_root, boost_dir=self.boost_dir,
250
                        boost_include_dir=self.boost_include_dir, boost_librarydir=self.boost_librarydir,
251
                        opencl_include_dir=self.opencl_include_dir, opencl_library=self.opencl_library,
252
                        nomp=self.nomp, bit32=self.bit32, integrated_opencl=self.integrated_opencl)
253
        install.run(self)
254
255
        if LOG_PATH.is_file():
            LOG_PATH.unlink()
256
257


258
259
260
261
class CustomBdistWheel(bdist_wheel):

    user_options = bdist_wheel.user_options + LIGHTGBM_OPTIONS

262
    def initialize_options(self) -> None:
263
        bdist_wheel.initialize_options(self)
264
265
266
267
        self.mingw = False
        self.integrated_opencl = False
        self.gpu = False
        self.cuda = False
268
269
270
271
272
273
        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
274
275
276
277
278
        self.mpi = False
        self.hdfs = False
        self.precompile = False
        self.nomp = False
        self.bit32 = False
279

280
    def finalize_options(self) -> None:
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
        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


302
303
class CustomSdist(sdist):

304
    def run(self) -> None:
305
        copy_files(integrated_opencl=True, use_gpu=True)
306
307
308
309
310
311
        IS_SOURCE_FLAG_PATH.touch()
        rmtree(CURRENT_DIR / 'lightgbm' / 'Release', ignore_errors=True)
        rmtree(CURRENT_DIR / 'lightgbm' / 'windows' / 'x64', ignore_errors=True)
        lib_file = CURRENT_DIR / 'lightgbm' / 'lib_lightgbm.so'
        if lib_file.is_file():
            lib_file.unlink()
312
        sdist.run(self)
313
314
        if IS_SOURCE_FLAG_PATH.is_file():
            IS_SOURCE_FLAG_PATH.unlink()
315
316
317


if __name__ == "__main__":
318
319
    CURRENT_DIR = Path(__file__).absolute().parent
    LOG_PATH = Path.home() / 'LightGBM_compilation.log'
320
    LOG_NOTICE = f"The full version of error log was saved into {LOG_PATH}"
321
322
323
324
325
326
327
328
329
    IS_SOURCE_FLAG_PATH = CURRENT_DIR / '_IS_SOURCE_PACKAGE.txt'
    _version_src = CURRENT_DIR.parent / 'VERSION.txt'
    _version_dst = CURRENT_DIR / 'lightgbm' / 'VERSION.txt'
    if _version_src.is_file():
        copyfile(_version_src, _version_dst)
    version = _version_dst.read_text(encoding='utf-8').strip()
    readme = (CURRENT_DIR / 'README.rst').read_text(encoding='utf-8')

    sys.path.insert(0, str(CURRENT_DIR))
330

331
332
333
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger('LightGBM')

Guolin Ke's avatar
Guolin Ke committed
334
335
336
    setup(name='lightgbm',
          version=version,
          description='LightGBM Python Package',
337
          long_description=readme,
Guolin Ke's avatar
Guolin Ke committed
338
          install_requires=[
339
              'wheel',
Guolin Ke's avatar
Guolin Ke committed
340
341
              'numpy',
              'scipy',
342
              'scikit-learn!=0.22.0'
Guolin Ke's avatar
Guolin Ke committed
343
          ],
344
345
346
          extras_require={
              'dask': [
                  'dask[array]>=2.0.0',
347
                  'dask[dataframe]>=2.0.0',
348
349
350
351
                  'dask[distributed]>=2.0.0',
                  'pandas',
              ],
          },
352
353
          maintainer='Yu Shi',
          maintainer_email='yushi2@microsoft.com',
Guolin Ke's avatar
Guolin Ke committed
354
          zip_safe=False,
355
356
357
          cmdclass={
              'install': CustomInstall,
              'install_lib': CustomInstallLib,
358
              'bdist_wheel': CustomBdistWheel,
359
360
              'sdist': CustomSdist,
          },
Guolin Ke's avatar
Guolin Ke committed
361
362
          packages=find_packages(),
          include_package_data=True,
363
          license='The MIT License (Microsoft)',
364
          url='https://github.com/microsoft/LightGBM',
365
366
367
368
369
370
371
372
373
374
          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',
375
                       'Programming Language :: Python :: 3.7',
376
                       'Programming Language :: Python :: 3.8',
377
                       'Programming Language :: Python :: 3.9',
378
                       'Topic :: Scientific/Engineering :: Artificial Intelligence'])