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 = ("v143", "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
184
185
186
187
188
                vs_versions = (
                    "Visual Studio 17 2022",
                    "Visual Studio 16 2019",
                    "Visual Studio 15 2017",
                    "Visual Studio 14 2015"
                )
189
                for vs in vs_versions:
190
                    logger.info(f"Starting to compile with {vs} ({arch}).")
191
                    status = silent_call(cmake_cmd + ["-G", vs, "-A", arch])
192
193
194
                    if status == 0:
                        break
                    else:
195
                        clear_path(build_dir)
196
                if status != 0:
197
                    raise Exception("\n".join(('Please install Visual Studio or MS Build and all required dependencies first',
198
                                    LOG_NOTICE)))
199
                silent_call(["cmake", "--build", str(build_dir), "--target", "_lightgbm", "--config", "Release"], raise_error=True,
200
                            error_msg='Please install CMake first')
201
    else:  # Linux, Darwin (macOS), etc.
202
        logger.info("Starting to compile with CMake.")
203
        silent_call(cmake_cmd, raise_error=True, error_msg='Please install CMake and all required dependencies first')
204
        silent_call(["make", "_lightgbm", f"-I{build_dir}", "-j4"], raise_error=True,
205
                    error_msg='An error has occurred while building lightgbm library file')
206
    chdir(original_dir)
207
208
209
210


class CustomInstallLib(install_lib):

211
    def install(self) -> List[str]:
212
213
        outfiles = install_lib.install(self)
        src = find_lib()[0]
214
215
        dst = Path(self.install_dir) / 'lightgbm'
        dst, _ = self.copy_file(src, str(dst))
216
217
218
219
220
221
        outfiles.append(dst)
        return outfiles


class CustomInstall(install):

222
    user_options = install.user_options + LIGHTGBM_OPTIONS
223

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

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


263
264
265
266
class CustomBdistWheel(bdist_wheel):

    user_options = bdist_wheel.user_options + LIGHTGBM_OPTIONS

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

285
    def finalize_options(self) -> None:
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
        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


307
308
class CustomSdist(sdist):

309
    def run(self) -> None:
310
        copy_files(integrated_opencl=True, use_gpu=True)
311
312
313
314
315
316
        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()
317
        sdist.run(self)
318
319
        if IS_SOURCE_FLAG_PATH.is_file():
            IS_SOURCE_FLAG_PATH.unlink()
320
321
322


if __name__ == "__main__":
323
324
    CURRENT_DIR = Path(__file__).absolute().parent
    LOG_PATH = Path.home() / 'LightGBM_compilation.log'
325
    LOG_NOTICE = f"The full version of error log was saved into {LOG_PATH}"
326
327
328
329
330
331
332
333
334
    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))
335

336
337
338
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger('LightGBM')

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