setup.py 14 KB
Newer Older
wxchan's avatar
wxchan committed
1
# coding: utf-8
2
# pylint: disable=invalid-name, exec-used, C0111
wxchan's avatar
wxchan committed
3
4
"""Setup lightgbm package."""
from __future__ import absolute_import
5

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

15
from platform import system
16
from setuptools import find_packages, setup
17
18
19
from setuptools.command.install import install
from setuptools.command.install_lib import install_lib
from setuptools.command.sdist import sdist
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
            distutils.dir_util.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
48
        if not os.path.exists(os.path.join(CURRENT_DIR, "compile", "windows")):
            os.makedirs(os.path.join(CURRENT_DIR, "compile", "windows"))
        distutils.file_util.copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "windows", "LightGBM.sln"),
49
50
                                      os.path.join(CURRENT_DIR, "compile", "windows", "LightGBM.sln"),
                                      verbose=0)
51
        distutils.file_util.copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "windows", "LightGBM.vcxproj"),
52
53
                                      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
        distutils.file_util.copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "CMakeLists.txt"),
57
58
                                      os.path.join(CURRENT_DIR, "compile", "CMakeLists.txt"),
                                      verbose=0)
59
        distutils.file_util.copy_file(os.path.join(CURRENT_DIR, os.path.pardir, "LICENSE"),
60
61
                                      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
91
92
93
def compile_cpp(use_mingw=False, use_gpu=False, use_mpi=False, nomp=False,
                use_hdfs=False, boost_root=None, boost_dir=None,
                boost_include_dir=None, boost_librarydir=None,
                opencl_include_dir=None, opencl_library=None):
94

95
96
97
98
    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"))
99

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

102
    cmake_cmd = ["cmake", "../compile/"]
103
    if use_gpu:
104
        cmake_cmd.append("-DUSE_GPU=ON")
105
106
107
108
109
110
111
112
113
114
115
116
117
118
        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")
119
120
    if nomp:
        cmake_cmd.append("-DUSE_OPENMP=OFF")
121
122
123
124
125
126
127
128
129
130
131
132
133
    if system() == 'Darwin' and not nomp:
        cc = os.environ.get('CC')
        cxx = os.environ.get('CXX')
        if not (cc and cc.startswith('gcc') and cxx and cxx.startswith('g++')):
            # Apple Clang
            # https://github.com/Homebrew/homebrew-core/pull/20589
            cmake_cmd.extend([
                '-DOpenMP_C_FLAGS=-Xpreprocessor -fopenmp -I/usr/local/opt/libomp/include',
                '-DOpenMP_C_LIB_NAMES=omp',
                '-DOpenMP_CXX_FLAGS=-Xpreprocessor -fopenmp -I/usr/local/opt/libomp/include',
                '-DOpenMP_CXX_LIB_NAMES=omp',
                '-DOpenMP_omp_LIBRARY=/usr/local/opt/libomp/lib/libomp.dylib'
            ])
134
135
    if use_hdfs:
        cmake_cmd.append("-DUSE_HDFS=ON")
136

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


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 + [
200
201
202
        ('mingw', 'm', 'Compile with MinGW'),
        ('gpu', 'g', 'Compile GPU version'),
        ('mpi', None, 'Compile MPI version'),
203
        ('nomp', None, 'Compile version without OpenMP support'),
204
        ('hdfs', 'h', 'Compile HDFS version'),
205
206
207
208
209
210
211
        ('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')
212
213
214
215
216
217
    ]

    def initialize_options(self):
        install.initialize_options(self)
        self.mingw = 0
        self.gpu = 0
218
219
220
221
222
223
224
        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
225
        self.hdfs = 0
226
        self.precompile = 0
227
        self.nomp = 0
228
229

    def run(self):
230
        open(LOG_PATH, 'wb').close()
231
232
        if not self.precompile:
            copy_files(use_gpu=self.gpu)
233
234
            compile_cpp(use_mingw=self.mingw, use_gpu=self.gpu, use_mpi=self.mpi, nomp=self.nomp,
                        use_hdfs=self.hdfs, boost_root=self.boost_root, boost_dir=self.boost_dir,
235
236
                        boost_include_dir=self.boost_include_dir, boost_librarydir=self.boost_librarydir,
                        opencl_include_dir=self.opencl_include_dir, opencl_library=self.opencl_library)
237
        install.run(self)
238
239
        if os.path.isfile(LOG_PATH):
            os.remove(LOG_PATH)
240
241
242
243
244


class CustomSdist(sdist):

    def run(self):
Guolin Ke's avatar
Guolin Ke committed
245
        copy_files(use_gpu=True)
246
247
248
249
250
251
252
        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'))
253
        sdist.run(self)
254
255
        if os.path.isfile(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt')):
            os.remove(os.path.join(CURRENT_DIR, '_IS_SOURCE_PACKAGE.txt'))
256
257
258
259


if __name__ == "__main__":
    if (8 * struct.calcsize("P")) != 64:
260
        raise Exception('Cannot install LightGBM in 32-bit Python, please use 64-bit Python instead.')
261

262
263
264
265
266
    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')):
        distutils.file_util.copy_file(os.path.join(CURRENT_DIR, os.path.pardir, 'VERSION.txt'),
267
268
                                      os.path.join(CURRENT_DIR, 'lightgbm', 'VERSION.txt'),
                                      verbose=0)
269
270
    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()
271

272
    sys.path.insert(0, CURRENT_DIR)
273

274
275
276
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger('LightGBM')

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