setup.py 13.9 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 rmtree
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
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
50
51
52
53
def clear_path(path: Path) -> None:
    if path.is_dir():
        for file_name in path.iterdir():
            if file_name.is_dir():
                rmtree(file_name)
54
            else:
55
                file_name.unlink()
56
57


58
def silent_call(cmd: List[str], raise_error: bool = False, error_msg: str = '') -> int:
59
    try:
60
        with open(LOG_PATH, "ab") as log:
61
            subprocess.check_call(cmd, stderr=log, stdout=log)
62
        return 0
63
    except Exception:
64
        if raise_error:
65
            raise Exception("\n".join((error_msg, LOG_NOTICE)))
66
        return 1
67
68


69
70
71
72
73
74
75
76
77
78
79
80
81
82
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,
83
84
    integrated_opencl: bool = False,
    time_costs: bool = False
85
) -> None:
86
87
88
89
90
    build_dir = CURRENT_DIR / "build_cpp"
    rmtree(build_dir, ignore_errors=True)
    build_dir.mkdir(parents=True)
    original_dir = Path.cwd()
    chdir(build_dir)
91

92
93
    logger.info("Starting to compile the library.")

94
    cmake_cmd = ["cmake", str(CURRENT_DIR / "compile"), "-D__BUILD_FOR_PYTHON=ON"]
95
96
97
    if integrated_opencl:
        use_gpu = False
        cmake_cmd.append("-D__INTEGRATE_OPENCL=ON")
98
    if use_gpu:
99
        cmake_cmd.append("-DUSE_GPU=ON")
100
        if boost_root:
101
            cmake_cmd.append(f"-DBOOST_ROOT={boost_root}")
102
        if boost_dir:
103
            cmake_cmd.append(f"-DBoost_DIR={boost_dir}")
104
        if boost_include_dir:
105
            cmake_cmd.append(f"-DBoost_INCLUDE_DIR={boost_include_dir}")
106
        if boost_librarydir:
107
            cmake_cmd.append(f"-DBOOST_LIBRARYDIR={boost_librarydir}")
108
        if opencl_include_dir:
109
            cmake_cmd.append(f"-DOpenCL_INCLUDE_DIR={opencl_include_dir}")
110
        if opencl_library:
111
            cmake_cmd.append(f"-DOpenCL_LIBRARY={opencl_library}")
112
113
    elif use_cuda:
        cmake_cmd.append("-DUSE_CUDA=ON")
114
115
    if use_mpi:
        cmake_cmd.append("-DUSE_MPI=ON")
116
117
    if nomp:
        cmake_cmd.append("-DUSE_OPENMP=OFF")
118
119
    if use_hdfs:
        cmake_cmd.append("-DUSE_HDFS=ON")
120
121
    if time_costs:
        cmake_cmd.append("-DUSE_TIMETAG=ON")
122

123
    if system() in {'Windows', 'Microsoft'}:
124
        if use_mingw:
125
126
            if use_mpi:
                raise Exception('MPI version cannot be compiled by MinGW due to the miss of MPI library in it')
127
            logger.info("Starting to compile with CMake and MinGW.")
128
129
            # ref: https://stackoverflow.com/a/45104058/3986677
            silent_call(cmake_cmd + ["-G", "MinGW Makefiles", "-DCMAKE_SH=CMAKE_SH-NOTFOUND"], raise_error=True,
130
                        error_msg='Please install CMake and all required dependencies first')
131
            silent_call(["mingw32-make.exe", "_lightgbm", f"-I{build_dir}", "-j4"], raise_error=True,
132
                        error_msg='Please install MinGW first')
133
        else:
134
            status = 1
135
            lib_path = CURRENT_DIR / "compile" / "windows" / "x64" / "DLL" / "lib_lightgbm.dll"
136
            if not any((use_gpu, use_cuda, use_mpi, use_hdfs, nomp, bit32, integrated_opencl)):
137
                logger.info("Starting to compile with MSBuild from existing solution file.")
138
                platform_toolsets = ("v143", "v142", "v141", "v140")
139
                for pt in platform_toolsets:
140
                    status = silent_call(["MSBuild",
141
                                          str(CURRENT_DIR / "compile" / "windows" / "LightGBM.sln"),
142
143
                                          "/p:Configuration=DLL",
                                          "/p:Platform=x64",
144
                                          f"/p:PlatformToolset={pt}"])
145
                    if status == 0 and lib_path.is_file():
146
147
                        break
                    else:
148
149
                        clear_path(CURRENT_DIR / "compile" / "windows" / "x64")
                if status != 0 or not lib_path.is_file():
150
                    logger.warning("Compilation with MSBuild from existing solution file failed.")
151
            if status != 0 or not lib_path.is_file():
152
                arch = "Win32" if bit32 else "x64"
153
154
155
156
157
158
                vs_versions = (
                    "Visual Studio 17 2022",
                    "Visual Studio 16 2019",
                    "Visual Studio 15 2017",
                    "Visual Studio 14 2015"
                )
159
                for vs in vs_versions:
160
                    logger.info(f"Starting to compile with {vs} ({arch}).")
161
                    status = silent_call(cmake_cmd + ["-G", vs, "-A", arch])
162
163
164
                    if status == 0:
                        break
                    else:
165
                        clear_path(build_dir)
166
                if status != 0:
167
                    raise Exception("\n".join(('Please install Visual Studio or MS Build and all required dependencies first',
168
                                    LOG_NOTICE)))
169
                silent_call(["cmake", "--build", str(build_dir), "--target", "_lightgbm", "--config", "Release"], raise_error=True,
170
                            error_msg='Please install CMake first')
171
    else:  # Linux, Darwin (macOS), etc.
172
        logger.info("Starting to compile with CMake.")
173
        silent_call(cmake_cmd, raise_error=True, error_msg='Please install CMake and all required dependencies first')
174
        silent_call(["make", "_lightgbm", f"-I{build_dir}", "-j4"], raise_error=True,
175
                    error_msg='An error has occurred while building lightgbm library file')
176
    chdir(original_dir)
177
178
179
180


class CustomInstallLib(install_lib):

181
    def install(self) -> List[str]:
182
183
        outfiles = install_lib.install(self)
        src = find_lib()[0]
184
185
        dst = Path(self.install_dir) / 'lightgbm'
        dst, _ = self.copy_file(src, str(dst))
186
187
188
189
190
191
        outfiles.append(dst)
        return outfiles


class CustomInstall(install):

192
    user_options = install.user_options + LIGHTGBM_OPTIONS
193

194
    def initialize_options(self) -> None:
195
        install.initialize_options(self)
196
197
198
199
        self.mingw = False
        self.integrated_opencl = False
        self.gpu = False
        self.cuda = False
200
201
202
203
204
205
        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
206
207
208
        self.mpi = False
        self.hdfs = False
        self.precompile = False
209
        self.time_costs = False
210
211
        self.nomp = False
        self.bit32 = False
212

213
    def run(self) -> None:
214
215
216
217
218
219
220
        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.")
221
        LOG_PATH.touch()
222
        if not self.precompile:
223
            compile_cpp(use_mingw=self.mingw, use_gpu=self.gpu, use_cuda=self.cuda, use_mpi=self.mpi,
224
                        use_hdfs=self.hdfs, boost_root=self.boost_root, boost_dir=self.boost_dir,
225
                        boost_include_dir=self.boost_include_dir, boost_librarydir=self.boost_librarydir,
226
                        opencl_include_dir=self.opencl_include_dir, opencl_library=self.opencl_library,
227
228
                        nomp=self.nomp, bit32=self.bit32, integrated_opencl=self.integrated_opencl,
                        time_costs=self.time_costs)
229
        install.run(self)
230
231
        if LOG_PATH.is_file():
            LOG_PATH.unlink()
232
233


234
235
236
237
class CustomBdistWheel(bdist_wheel):

    user_options = bdist_wheel.user_options + LIGHTGBM_OPTIONS

238
    def initialize_options(self) -> None:
239
        bdist_wheel.initialize_options(self)
240
241
242
243
        self.mingw = False
        self.integrated_opencl = False
        self.gpu = False
        self.cuda = False
244
245
246
247
248
249
        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
250
251
252
        self.mpi = False
        self.hdfs = False
        self.precompile = False
253
        self.time_costs = False
254
255
        self.nomp = False
        self.bit32 = False
256

257
    def finalize_options(self) -> None:
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
        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
275
        install.time_costs = self.time_costs
276
277
278
279
        install.nomp = self.nomp
        install.bit32 = self.bit32


280
281
class CustomSdist(sdist):

282
    def run(self) -> None:
283
284
285
286
287
288
        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()
289
        sdist.run(self)
290
291
        if IS_SOURCE_FLAG_PATH.is_file():
            IS_SOURCE_FLAG_PATH.unlink()
292
293
294


if __name__ == "__main__":
295
296
    CURRENT_DIR = Path(__file__).absolute().parent
    LOG_PATH = Path.home() / 'LightGBM_compilation.log'
297
    LOG_NOTICE = f"The full version of error log was saved into {LOG_PATH}"
298
    IS_SOURCE_FLAG_PATH = CURRENT_DIR / '_IS_SOURCE_PACKAGE.txt'
299
300
    _version_file = CURRENT_DIR / 'lightgbm' / 'VERSION.txt'
    version = _version_file.read_text(encoding='utf-8').strip()
301
302
303
    readme = (CURRENT_DIR / 'README.rst').read_text(encoding='utf-8')

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

305
306
307
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger('LightGBM')

Guolin Ke's avatar
Guolin Ke committed
308
309
310
    setup(name='lightgbm',
          version=version,
          description='LightGBM Python Package',
311
          long_description=readme,
312
          long_description_content_type='text/x-rst',
313
          python_requires='>=3.6',
Guolin Ke's avatar
Guolin Ke committed
314
          install_requires=[
315
              'wheel',
Guolin Ke's avatar
Guolin Ke committed
316
317
              'numpy',
              'scipy',
318
              'scikit-learn!=0.22.0'
Guolin Ke's avatar
Guolin Ke committed
319
          ],
320
321
322
          extras_require={
              'dask': [
                  'dask[array]>=2.0.0',
323
                  'dask[dataframe]>=2.0.0',
324
325
326
327
                  'dask[distributed]>=2.0.0',
                  'pandas',
              ],
          },
328
329
          maintainer='Yu Shi',
          maintainer_email='yushi2@microsoft.com',
Guolin Ke's avatar
Guolin Ke committed
330
          zip_safe=False,
331
332
333
          cmdclass={
              'install': CustomInstall,
              'install_lib': CustomInstallLib,
334
              'bdist_wheel': CustomBdistWheel,
335
336
              'sdist': CustomSdist,
          },
Guolin Ke's avatar
Guolin Ke committed
337
338
          packages=find_packages(),
          include_package_data=True,
339
          license='The MIT License (Microsoft)',
340
          url='https://github.com/microsoft/LightGBM',
341
342
343
344
345
346
347
348
349
          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',
350
                       'Programming Language :: Python :: 3.7',
351
                       'Programming Language :: Python :: 3.8',
352
                       'Programming Language :: Python :: 3.9',
353
                       'Programming Language :: Python :: 3.10',
354
355
                       'Topic :: Scientific/Engineering :: Artificial Intelligence',
                       'Typing :: Typed'])