setup.py 15.6 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
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'),
29
    ('time-costs', None, 'Output time costs for different internal routines'),
30
31
32
33
34
35
36
    ('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')
]
37

38

39
def find_lib() -> List[str]:
40
    libpath_py = CURRENT_DIR / 'lightgbm' / 'libpath.py'
41
    libpath = {'__file__': libpath_py}
42
    exec(compile(libpath_py.read_bytes(), libpath_py, 'exec'), libpath, libpath)
43

44
    LIB_PATH = libpath['find_lib_path']()  # type: ignore
45
    logger.info(f"Installing lib_lightgbm from: {LIB_PATH}")
46
47
48
    return LIB_PATH


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

51
52
53
54
55
56
57
    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)
58
        else:
59
            raise Exception(f'Cannot copy {src} folder')
60

61
    if not IS_SOURCE_FLAG_PATH.is_file():
62
63
        copy_files_helper('include')
        copy_files_helper('src')
64
        for submodule in (CURRENT_DIR.parent / 'external_libs').iterdir():
65
66
            submodule_stem = submodule.stem
            if submodule_stem == 'compute' and not use_gpu:
67
                continue
68
            copy_files_helper(Path('external_libs') / submodule_stem)
69
70
71
72
73
74
75
76
77
        (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")
78
        if integrated_opencl:
79
80
81
82
83
84
85
86
87
88
            (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)
89
            else:
90
                file_name.unlink()
91
92


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


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

127
128
    logger.info("Starting to compile the library.")

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

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


class CustomInstallLib(install_lib):

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


class CustomInstall(install):

226
    user_options = install.user_options + LIGHTGBM_OPTIONS
227

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

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


269
270
271
272
class CustomBdistWheel(bdist_wheel):

    user_options = bdist_wheel.user_options + LIGHTGBM_OPTIONS

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

292
    def finalize_options(self) -> None:
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
        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
310
        install.time_costs = self.time_costs
311
312
313
314
        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
321
322
323
324
        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()
325
        sdist.run(self)
326
327
        if IS_SOURCE_FLAG_PATH.is_file():
            IS_SOURCE_FLAG_PATH.unlink()
328
329
330


if __name__ == "__main__":
331
332
    CURRENT_DIR = Path(__file__).absolute().parent
    LOG_PATH = Path.home() / 'LightGBM_compilation.log'
333
    LOG_NOTICE = f"The full version of error log was saved into {LOG_PATH}"
334
335
336
337
338
339
340
341
342
    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))
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,
351
          python_requires='>=3.6',
Guolin Ke's avatar
Guolin Ke committed
352
          install_requires=[
353
              'wheel',
Guolin Ke's avatar
Guolin Ke committed
354
355
              'numpy',
              'scipy',
356
              'scikit-learn!=0.22.0'
Guolin Ke's avatar
Guolin Ke committed
357
          ],
358
359
360
          extras_require={
              'dask': [
                  'dask[array]>=2.0.0',
361
                  'dask[dataframe]>=2.0.0',
362
363
364
365
                  'dask[distributed]>=2.0.0',
                  'pandas',
              ],
          },
366
367
          maintainer='Yu Shi',
          maintainer_email='yushi2@microsoft.com',
Guolin Ke's avatar
Guolin Ke committed
368
          zip_safe=False,
369
370
371
          cmdclass={
              'install': CustomInstall,
              'install_lib': CustomInstallLib,
372
              'bdist_wheel': CustomBdistWheel,
373
374
              'sdist': CustomSdist,
          },
Guolin Ke's avatar
Guolin Ke committed
375
376
          packages=find_packages(),
          include_package_data=True,
377
          license='The MIT License (Microsoft)',
378
          url='https://github.com/microsoft/LightGBM',
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',
388
                       'Programming Language :: Python :: 3.7',
389
                       'Programming Language :: Python :: 3.8',
390
                       'Programming Language :: Python :: 3.9',
391
                       'Programming Language :: Python :: 3.10',
392
                       'Topic :: Scientific/Engineering :: Artificial Intelligence'])