setup.py 13.5 KB
Newer Older
wxchan's avatar
wxchan committed
1
2
3
# coding: utf-8
"""Setup lightgbm package."""
from __future__ import absolute_import
4

5
import io
6
import logging
7
import os
Guolin Ke's avatar
Guolin Ke committed
8
import shutil
9
import struct
10
import subprocess
11
12
import sys

13
from platform import system
14
from setuptools import find_packages, setup
15
16
17
from setuptools.command.install import install
from setuptools.command.install_lib import install_lib
from setuptools.command.sdist import sdist
18
19
from distutils.dir_util import copy_tree
from distutils.file_util import copy_file
20

21
22

def find_lib():
23
    libpath_py = os.path.join(CURRENT_DIR, 'lightgbm', 'libpath.py')
24
25
26
27
    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']()]
28
    logger.info("Installing lib_lightgbm from: %s" % LIB_PATH)
29
30
31
32
33
34
    return LIB_PATH


def copy_files(use_gpu=False):

    def copy_files_helper(folder_name):
35
        src = os.path.join(CURRENT_DIR, os.path.pardir, folder_name)
36
        if os.path.exists(src):
37
            dst = os.path.join(CURRENT_DIR, 'compile', folder_name)
38
            shutil.rmtree(dst, ignore_errors=True)
39
            copy_tree(src, dst, verbose=0)
40
        else:
41
            raise Exception('Cannot copy {0} folder'.format(src))
42

43
    if not os.path.isfile(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt')):
44
45
        copy_files_helper('include')
        copy_files_helper('src')
46
47
        if not os.path.exists(os.path.join(CURRENT_DIR, "compile", "windows")):
            os.makedirs(os.path.join(CURRENT_DIR, "compile", "windows"))
48
49
50
51
52
53
        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)
Guolin Ke's avatar
Guolin Ke committed
54
        if use_gpu:
55
            copy_files_helper('compute')
56
57
58
59
60
61
        copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "CMakeLists.txt"),
                  os.path.join(CURRENT_DIR, "compile", "CMakeLists.txt"),
                  verbose=0)
        copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "LICENSE"),
                  os.path.join(CURRENT_DIR, "LICENSE"),
                  verbose=0)
wxchan's avatar
wxchan committed
62
63


64
def clear_path(path):
65
66
67
68
69
70
71
72
73
74
75
76
    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:
                shutil.rmtree(file_path)


def silent_call(cmd, raise_error=False, error_msg=''):
    try:
77
        output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
78
        with open(LOG_PATH, "ab") as log:
79
80
81
82
            log.write(output)
        return 0
    except Exception as err:
        if isinstance(err, subprocess.CalledProcessError):
83
            with open(LOG_PATH, "ab") as log:
84
                log.write(err.output)
85
        if raise_error:
86
            raise Exception("\n".join((error_msg, LOG_NOTICE)))
87
        return 1
88
89


90
def compile_cpp(use_mingw=False, use_gpu=False, use_mpi=False,
91
92
                use_hdfs=False, boost_root=None, boost_dir=None,
                boost_include_dir=None, boost_librarydir=None,
93
                opencl_include_dir=None, opencl_library=None,
94
                nomp=False, bit32=False):
95

96
97
98
99
    if os.path.exists(os.path.join(CURRENT_DIR, "build_cpp")):
        shutil.rmtree(os.path.join(CURRENT_DIR, "build_cpp"))
    os.makedirs(os.path.join(CURRENT_DIR, "build_cpp"))
    os.chdir(os.path.join(CURRENT_DIR, "build_cpp"))
100

101
102
    logger.info("Starting to compile the library.")

103
    cmake_cmd = ["cmake", "../compile/"]
104
    if use_gpu:
105
        cmake_cmd.append("-DUSE_GPU=ON")
106
107
108
109
110
111
112
113
114
115
116
117
118
119
        if boost_root:
            cmake_cmd.append("-DBOOST_ROOT={0}".format(boost_root))
        if boost_dir:
            cmake_cmd.append("-DBoost_DIR={0}".format(boost_dir))
        if boost_include_dir:
            cmake_cmd.append("-DBoost_INCLUDE_DIR={0}".format(boost_include_dir))
        if boost_librarydir:
            cmake_cmd.append("-DBOOST_LIBRARYDIR={0}".format(boost_librarydir))
        if opencl_include_dir:
            cmake_cmd.append("-DOpenCL_INCLUDE_DIR={0}".format(opencl_include_dir))
        if opencl_library:
            cmake_cmd.append("-DOpenCL_LIBRARY={0}".format(opencl_library))
    if use_mpi:
        cmake_cmd.append("-DUSE_MPI=ON")
120
121
    if nomp:
        cmake_cmd.append("-DUSE_OPENMP=OFF")
122
123
    if use_hdfs:
        cmake_cmd.append("-DUSE_HDFS=ON")
124

125
    if system() in {'Windows', 'Microsoft'}:
126
        if use_mingw:
127
128
            if use_mpi:
                raise Exception('MPI version cannot be compiled by MinGW due to the miss of MPI library in it')
129
130
            logger.info("Starting to compile with CMake and MinGW.")
            silent_call(cmake_cmd + ["-G", "MinGW Makefiles"], raise_error=True,
131
                        error_msg='Please install CMake and all required dependencies first')
132
133
            silent_call(["mingw32-make.exe", "_lightgbm"], raise_error=True,
                        error_msg='Please install MinGW first')
134
        else:
135
            status = 1
136
            lib_path = os.path.join(CURRENT_DIR, "compile", "windows", "x64", "DLL", "lib_lightgbm.dll")
137
            if not any((use_gpu, use_mpi, use_hdfs, nomp, bit32)):
138
                logger.info("Starting to compile with MSBuild from existing solution file.")
139
                platform_toolsets = ("v142", "v141", "v140")
140
                for pt in platform_toolsets:
141
142
                    status = silent_call(["MSBuild",
                                          os.path.join(CURRENT_DIR, "compile", "windows", "LightGBM.sln"),
143
144
145
146
147
148
                                          "/p:Configuration=DLL",
                                          "/p:Platform=x64",
                                          "/p:PlatformToolset={0}".format(pt)])
                    if status == 0 and os.path.exists(lib_path):
                        break
                    else:
149
                        clear_path(os.path.join(CURRENT_DIR, "compile", "windows", "x64"))
150
151
152
                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):
153
                arch = "Win32" if bit32 else "x64"
154
                vs_versions = ("Visual Studio 16 2019", "Visual Studio 15 2017", "Visual Studio 14 2015")
155
156
                for vs in vs_versions:
                    logger.info("Starting to compile with %s." % vs)
157
                    status = silent_call(cmake_cmd + ["-G", vs, "-A", arch])
158
159
160
                    if status == 0:
                        break
                    else:
161
                        clear_path(os.path.join(CURRENT_DIR, "build_cpp"))
162
                if status != 0:
163
                    raise Exception("\n".join(('Please install Visual Studio or MS Build and all required dependencies first',
164
                                    LOG_NOTICE)))
165
166
                silent_call(["cmake", "--build", ".", "--target", "_lightgbm", "--config", "Release"], raise_error=True,
                            error_msg='Please install CMake first')
167
    else:  # Linux, Darwin (macOS), etc.
168
        logger.info("Starting to compile with CMake.")
169
170
171
        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')
172
    os.chdir(CURRENT_DIR)
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188


class CustomInstallLib(install_lib):

    def install(self):
        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):

    user_options = install.user_options + [
189
190
191
        ('mingw', 'm', 'Compile with MinGW'),
        ('gpu', 'g', 'Compile GPU version'),
        ('mpi', None, 'Compile MPI version'),
192
        ('nomp', None, 'Compile version without OpenMP support'),
193
        ('hdfs', 'h', 'Compile HDFS version'),
194
        ('bit32', None, 'Compile 32-bit version'),
195
196
197
198
199
200
        ('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'),
201
        ('opencl-library=', None, 'Path to OpenCL library')
202
203
204
205
206
207
    ]

    def initialize_options(self):
        install.initialize_options(self)
        self.mingw = 0
        self.gpu = 0
208
209
210
211
212
213
214
        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
215
        self.hdfs = 0
216
        self.precompile = 0
217
        self.nomp = 0
218
        self.bit32 = 0
219
220

    def run(self):
221
222
223
224
225
226
227
        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.")
228
        open(LOG_PATH, 'wb').close()
229
230
        if not self.precompile:
            copy_files(use_gpu=self.gpu)
231
            compile_cpp(use_mingw=self.mingw, use_gpu=self.gpu, use_mpi=self.mpi,
232
                        use_hdfs=self.hdfs, boost_root=self.boost_root, boost_dir=self.boost_dir,
233
                        boost_include_dir=self.boost_include_dir, boost_librarydir=self.boost_librarydir,
234
                        opencl_include_dir=self.opencl_include_dir, opencl_library=self.opencl_library,
235
                        nomp=self.nomp, bit32=self.bit32)
236
        install.run(self)
237
238
        if os.path.isfile(LOG_PATH):
            os.remove(LOG_PATH)
239
240
241
242
243


class CustomSdist(sdist):

    def run(self):
Guolin Ke's avatar
Guolin Ke committed
244
        copy_files(use_gpu=True)
245
246
247
248
249
250
251
        open(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt'), 'w').close()
        if os.path.exists(os.path.join(CURRENT_DIR, 'lightgbm', 'Release')):
            shutil.rmtree(os.path.join(CURRENT_DIR, 'lightgbm', 'Release'))
        if os.path.exists(os.path.join(CURRENT_DIR, 'lightgbm', 'windows', 'x64')):
            shutil.rmtree(os.path.join(CURRENT_DIR, 'lightgbm', 'windows', 'x64'))
        if os.path.isfile(os.path.join(CURRENT_DIR, 'lightgbm', 'lib_lightgbm.so')):
            os.remove(os.path.join(CURRENT_DIR, 'lightgbm', 'lib_lightgbm.so'))
252
        sdist.run(self)
253
254
        if os.path.isfile(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt')):
            os.remove(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt'))
255
256
257


if __name__ == "__main__":
258
259
260
261
    CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
    LOG_PATH = os.path.join(os.path.expanduser('~'), 'LightGBM_compilation.log')
    LOG_NOTICE = "The full version of error log was saved into {0}".format(LOG_PATH)
    if os.path.isfile(os.path.join(CURRENT_DIR, os.path.pardir, 'VERSION.txt')):
262
263
264
        copy_file(os.path.join(CURRENT_DIR, os.path.pardir, 'VERSION.txt'),
                  os.path.join(CURRENT_DIR, 'lightgbm', 'VERSION.txt'),
                  verbose=0)
265
266
    version = io.open(os.path.join(CURRENT_DIR, 'lightgbm', 'VERSION.txt'), encoding='utf-8').read().strip()
    readme = io.open(os.path.join(CURRENT_DIR, 'README.rst'), encoding='utf-8').read()
267

268
    sys.path.insert(0, CURRENT_DIR)
269

270
271
272
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger('LightGBM')

Guolin Ke's avatar
Guolin Ke committed
273
274
275
    setup(name='lightgbm',
          version=version,
          description='LightGBM Python Package',
276
          long_description=readme,
Guolin Ke's avatar
Guolin Ke committed
277
278
279
          install_requires=[
              'numpy',
              'scipy',
280
              'scikit-learn!=0.22.0'
Guolin Ke's avatar
Guolin Ke committed
281
282
283
284
          ],
          maintainer='Guolin Ke',
          maintainer_email='guolin.ke@microsoft.com',
          zip_safe=False,
285
286
287
288
289
          cmdclass={
              'install': CustomInstall,
              'install_lib': CustomInstallLib,
              'sdist': CustomSdist,
          },
Guolin Ke's avatar
Guolin Ke committed
290
291
          packages=find_packages(),
          include_package_data=True,
292
          license='The MIT License (Microsoft)',
293
          url='https://github.com/microsoft/LightGBM',
294
295
296
297
298
299
300
301
302
303
304
305
          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 :: 2',
                       'Programming Language :: Python :: 2.7',
                       'Programming Language :: Python :: 3',
                       'Programming Language :: Python :: 3.6',
306
                       'Programming Language :: Python :: 3.7',
307
                       'Programming Language :: Python :: 3.8',
308
                       'Topic :: Scientific/Engineering :: Artificial Intelligence'])