setup.py 15.6 KB
Newer Older
1 2 3 4 5 6 7 8
import io
import os
import os.path
import sys
import runpy
import subprocess
import re
import sysconfig
O
Olli-Pekka Heinisuo 已提交
9 10
import skbuild
from skbuild import cmaker
11

12

13 14
def main():
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
15

O
Olli-Pekka Heinisuo 已提交
16
    CI_BUILD = os.environ.get("CI_BUILD", "False")
O
Olli-Pekka Heinisuo 已提交
17
    is_CI_build = True if CI_BUILD == "1" else False
18
    cmake_source_dir = "opencv"
O
Olli-Pekka Heinisuo 已提交
19
    minimum_supported_numpy = "1.13.1"
O
Olli-Pekka Heinisuo 已提交
20 21
    build_contrib = get_build_env_var_by_name("contrib")
    build_headless = get_build_env_var_by_name("headless")
22
    build_java = 'ON' if get_build_env_var_by_name("java") else 'OFF'
23

24
    if sys.version_info[:2] >= (3, 6):
O
Olli-Pekka Heinisuo 已提交
25
        minimum_supported_numpy = "1.13.3"
26 27
    if sys.version_info[:2] >= (3, 7):
        minimum_supported_numpy = "1.14.5"
28 29
    if sys.version_info[:2] >= (3, 8):
        minimum_supported_numpy = "1.17.3"
30

O
Olli-Pekka Heinisuo 已提交
31
    numpy_version = "numpy>=%s" % minimum_supported_numpy
32 33

    python_version = cmaker.CMaker.get_python_version()
O
Olli-Pekka Heinisuo 已提交
34 35 36 37 38 39
    python_lib_path = cmaker.CMaker.get_python_library(python_version).replace(
        "\\", "/"
    )
    python_include_dir = cmaker.CMaker.get_python_include_dir(python_version).replace(
        "\\", "/"
    )
40

41
    if os.path.exists(".git"):
O
Olli-Pekka Heinisuo 已提交
42
        import pip._internal.vcs.git as git
O
Olli-Pekka Heinisuo 已提交
43

O
Olli-Pekka Heinisuo 已提交
44
        g = git.Git()  # NOTE: pip API's are internal, this has to be refactored
45

46
        g.run_command(["submodule", "sync"])
47 48 49
        g.run_command(
            ["submodule", "update", "--init", "--recursive", cmake_source_dir]
        )
50

51
        if build_contrib:
52 53 54
            g.run_command(
                ["submodule", "update", "--init", "--recursive", "opencv_contrib"]
            )
O
Olli-Pekka Heinisuo 已提交
55

56 57
    package_version, build_contrib, build_headless = get_and_set_info(
        build_contrib, build_headless, is_CI_build
O
Olli-Pekka Heinisuo 已提交
58
    )
O
Olli-Pekka Heinisuo 已提交
59

60
    # https://stackoverflow.com/questions/1405913/python-32bit-or-64bit-mode
61
    x64 = sys.maxsize > 2 ** 32
62

O
Olli-Pekka Heinisuo 已提交
63 64
    package_name = "opencv-python"

65
    if build_contrib and not build_headless:
O
Olli-Pekka Heinisuo 已提交
66 67
        package_name = "opencv-contrib-python"

O
Olli-Pekka Heinisuo 已提交
68
    if build_contrib and build_headless:
O
Olli-Pekka Heinisuo 已提交
69 70
        package_name = "opencv-contrib-python-headless"

71
    if build_headless and not build_contrib:
O
Olli-Pekka Heinisuo 已提交
72 73
        package_name = "opencv-python-headless"

74
    long_description = io.open("README.md", encoding="utf-8").read()
O
Olli-Pekka Heinisuo 已提交
75

76
    packages = ["cv2", "cv2.data"]
77

78
    package_data = {
O
Olli-Pekka Heinisuo 已提交
79
        "cv2": ["*%s" % sysconfig.get_config_vars().get("SO"), "version.py"]
80 81 82
        + (["*.dll"] if os.name == "nt" else [])
        + ["LICENSE.txt", "LICENSE-3RD-PARTY.txt"],
        "cv2.data": ["*.xml"],
83
    }
84 85 86

    # Files from CMake output to copy to package.
    # Path regexes with forward slashes relative to CMake install dir.
87
    rearrange_cmake_output_data = {
88 89 90 91 92 93
        "cv2": (
            [r"bin/opencv_videoio_ffmpeg\d{3}%s\.dll" % ("_64" if x64 else "")]
            if os.name == "nt"
            else []
        )
        +
94 95 96
        # In Windows, in python/X.Y/<arch>/; in Linux, in just python/X.Y/.
        # Naming conventions vary so widely between versions and OSes
        # had to give up on checking them.
O
Olli-Pekka Heinisuo 已提交
97 98 99 100
        [
            "python/cv2[^/]*%(ext)s"
            % {"ext": re.escape(sysconfig.get_config_var("EXT_SUFFIX"))}
        ],
101 102 103
        "cv2.data": [  # OPENCV_OTHER_INSTALL_PATH
            ("etc" if os.name == "nt" else "share/opencv4") + r"/haarcascades/.*\.xml"
        ],
104 105
    }

106 107
    # Files in sourcetree outside package dir that should be copied to package.
    # Raw paths relative to sourcetree root.
108
    files_outside_package_dir = {"cv2": ["LICENSE.txt", "LICENSE-3RD-PARTY.txt"]}
109

O
Olli-Pekka Heinisuo 已提交
110 111 112 113 114 115
    ci_cmake_generator = (
        ["-G", "Visual Studio 14" + (" Win64" if x64 else "")]
        if os.name == "nt"
        else ["-G", "Unix Makefiles"]
    )

116
    cmake_args = (
O
Olli-Pekka Heinisuo 已提交
117
        (ci_cmake_generator if is_CI_build else [])
118 119
        + [
            # skbuild inserts PYTHON_* vars. That doesn't satisfy opencv build scripts in case of Py3
O
Olli-Pekka Heinisuo 已提交
120
            "-DPYTHON3_EXECUTABLE=%s" % sys.executable,
121 122
            "-DPYTHON3_INCLUDE_DIR=%s" % python_include_dir,
            "-DPYTHON3_LIBRARY=%s" % python_lib_path,
123 124
            "-DBUILD_opencv_python3=ON",
            "-DBUILD_opencv_python2=OFF",
125 126
            # Disable the Java build by default as it is not needed
            "-DBUILD_opencv_java=%s" % build_java,
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
            # When off, adds __init__.py and a few more helper .py's. We use our own helper files with a different structure.
            "-DOPENCV_SKIP_PYTHON_LOADER=ON",
            # Relative dir to install the built module to in the build tree.
            # The default is generated from sysconfig, we'd rather have a constant for simplicity
            "-DOPENCV_PYTHON3_INSTALL_PATH=python",
            # Otherwise, opencv scripts would want to install `.pyd' right into site-packages,
            # and skbuild bails out on seeing that
            "-DINSTALL_CREATE_DISTRIB=ON",
            # See opencv/CMakeLists.txt for options and defaults
            "-DBUILD_opencv_apps=OFF",
            "-DBUILD_SHARED_LIBS=OFF",
            "-DBUILD_TESTS=OFF",
            "-DBUILD_PERF_TESTS=OFF",
            "-DBUILD_DOCS=OFF",
        ]
        + (
            ["-DOPENCV_EXTRA_MODULES_PATH=" + os.path.abspath("opencv_contrib/modules")]
            if build_contrib
            else []
        )
    )
148

O
Olli-Pekka Heinisuo 已提交
149 150 151 152
    if build_headless:
        # it seems that cocoa cannot be disabled so on macOS the package is not truly headless
        cmake_args.append("-DWITH_WIN32UI=OFF")
        cmake_args.append("-DWITH_QT=OFF")
O
Olli-Pekka Heinisuo 已提交
153 154 155 156 157 158
        cmake_args.append("-DWITH_GTK=OFF")
        if is_CI_build:
            cmake_args.append(
                "-DWITH_MSMF=OFF"
            )  # see: https://github.com/skvark/opencv-python/issues/263

159 160 161
    if sys.platform.startswith("linux") and not x64 and "bdist_wheel" in sys.argv:
        subprocess.check_call("patch -p0 < patches/patchOpenEXR", shell=True)

162
    # OS-specific components during CI builds
O
Olli-Pekka Heinisuo 已提交
163
    if is_CI_build:
O
Olli-Pekka Heinisuo 已提交
164

O
Olli-Pekka Heinisuo 已提交
165 166 167 168 169 170 171 172 173
        if not build_headless and "bdist_wheel" in sys.argv:
            cmake_args.append("-DWITH_QT=5")
            subprocess.check_call("patch -p1 < patches/patchQtPlugins", shell=True)

            if sys.platform.startswith("linux"):
                rearrange_cmake_output_data["cv2.qt.plugins.platforms"] = [
                    (r"lib/qt/plugins/platforms/libqxcb\.so")
                ]
            if sys.platform == "darwin":
174 175 176
                rearrange_cmake_output_data["cv2.qt.plugins.platforms"] = [
                    (r"lib/qt/plugins/platforms/libqcocoa\.dylib")
                ]
O
Olli-Pekka Heinisuo 已提交
177 178 179 180 181

        if sys.platform.startswith("linux"):
            cmake_args.append("-DWITH_V4L=ON")
            cmake_args.append("-DWITH_LAPACK=ON")
            cmake_args.append("-DENABLE_PRECOMPILED_HEADERS=OFF")
182

183 184 185
    # https://github.com/scikit-build/scikit-build/issues/479
    if "CMAKE_ARGS" in os.environ:
        import shlex
186

187 188
        cmake_args.extend(shlex.split(os.environ["CMAKE_ARGS"]))
        del shlex
O
Olli-Pekka Heinisuo 已提交
189

190
    # works via side effect
191 192 193
    RearrangeCMakeOutput(
        rearrange_cmake_output_data, files_outside_package_dir, package_data.keys()
    )
194 195 196 197

    skbuild.setup(
        name=package_name,
        version=package_version,
198 199 200
        url="https://github.com/skvark/opencv-python",
        license="MIT",
        description="Wrapper package for OpenCV python bindings.",
201
        long_description=long_description,
202
        long_description_content_type="text/markdown",
203 204 205 206
        packages=packages,
        package_data=package_data,
        maintainer="Olli-Pekka Heinisuo",
        ext_modules=EmptyListWithLength(),
O
Olli-Pekka Heinisuo 已提交
207
        install_requires=numpy_version,
208
        classifiers=[
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
            "Development Status :: 5 - Production/Stable",
            "Environment :: Console",
            "Intended Audience :: Developers",
            "Intended Audience :: Education",
            "Intended Audience :: Information Technology",
            "Intended Audience :: Science/Research",
            "License :: OSI Approved :: MIT License",
            "Operating System :: MacOS",
            "Operating System :: Microsoft :: Windows",
            "Operating System :: POSIX",
            "Operating System :: Unix",
            "Programming Language :: Python",
            "Programming Language :: Python :: 3",
            "Programming Language :: Python :: 3.5",
            "Programming Language :: Python :: 3.6",
            "Programming Language :: Python :: 3.7",
            "Programming Language :: Python :: 3.8",
            "Programming Language :: C++",
            "Programming Language :: Python :: Implementation :: CPython",
            "Topic :: Scientific/Engineering",
            "Topic :: Scientific/Engineering :: Image Recognition",
            "Topic :: Software Development",
231 232 233
        ],
        cmake_args=cmake_args,
        cmake_source_dir=cmake_source_dir,
234
    )
235 236 237


class RearrangeCMakeOutput(object):
238 239 240 241
    """
        Patch SKBuild logic to only take files related to the Python package
        and construct a file hierarchy that SKBuild expects (see below)
    """
242

243 244 245 246 247
    _setuptools_wrap = None

    # Have to wrap a function reference, or it's converted
    # into an instance method on attr assignment
    import argparse
248

O
Olli-Pekka Heinisuo 已提交
249
    wraps = argparse.Namespace(_classify_installed_files=None)
250 251 252 253 254 255 256 257
    del argparse

    package_paths_re = None
    packages = None
    files_outside_package = None

    def __init__(self, package_paths_re, files_outside_package, packages):
        cls = self.__class__
O
Olli-Pekka Heinisuo 已提交
258
        assert not cls.wraps._classify_installed_files, "Singleton object"
259 260 261
        import skbuild.setuptools_wrap

        cls._setuptools_wrap = skbuild.setuptools_wrap
262 263 264 265 266 267
        cls.wraps._classify_installed_files = (
            cls._setuptools_wrap._classify_installed_files
        )
        cls._setuptools_wrap._classify_installed_files = (
            self._classify_installed_files_override
        )
268 269 270 271

        cls.package_paths_re = package_paths_re
        cls.files_outside_package = files_outside_package
        cls.packages = packages
272

273 274
    def __del__(self):
        cls = self.__class__
275 276 277
        cls._setuptools_wrap._classify_installed_files = (
            cls.wraps._classify_installed_files
        )
O
Olli-Pekka Heinisuo 已提交
278
        cls.wraps._classify_installed_files = None
279 280
        cls._setuptools_wrap = None

281 282 283 284 285 286 287 288 289 290 291 292 293
    def _classify_installed_files_override(
        self,
        install_paths,
        package_data,
        package_prefixes,
        py_modules,
        new_py_modules,
        scripts,
        new_scripts,
        data_files,
        cmake_source_dir,
        cmake_install_reldir,
    ):
294
        """
295 296 297 298 299 300 301 302 303
            From all CMake output, we're only interested in a few files
            and must place them into CMake install dir according
            to Python conventions for SKBuild to find them:
                package\
                    file
                    subpackage\
                        etc.
        """

304
        cls = self.__class__
305

306 307
        # 'relpath'/'reldir' = relative to CMAKE_INSTALL_DIR/cmake_install_dir
        # 'path'/'dir' = relative to sourcetree root
308 309 310 311 312 313 314 315 316
        cmake_install_dir = os.path.join(
            cls._setuptools_wrap.CMAKE_INSTALL_DIR(), cmake_install_reldir
        )
        install_relpaths = [
            os.path.relpath(p, cmake_install_dir) for p in install_paths
        ]
        fslash_install_relpaths = [
            p.replace(os.path.sep, "/") for p in install_relpaths
        ]
317 318 319 320
        relpaths_zip = list(zip(fslash_install_relpaths, install_relpaths))
        del install_relpaths, fslash_install_relpaths

        final_install_relpaths = []
O
Olli-Pekka Heinisuo 已提交
321

322
        print("Copying files from CMake output")
323

324
        for package_name, relpaths_re in cls.package_paths_re.items():
325
            package_dest_reldir = package_name.replace(".", os.path.sep)
326 327
            for relpath_re in relpaths_re:
                found = False
328
                r = re.compile(relpath_re + "$")
329 330
                for fslash_relpath, relpath in relpaths_zip:
                    m = r.match(fslash_relpath)
331 332
                    if not m:
                        continue
333 334
                    found = True
                    new_install_relpath = os.path.join(
335 336
                        package_dest_reldir, os.path.basename(relpath)
                    )
337 338 339
                    cls._setuptools_wrap._copy_file(
                        os.path.join(cmake_install_dir, relpath),
                        os.path.join(cmake_install_dir, new_install_relpath),
340 341
                        hide_listing=False,
                    )
342 343 344
                    final_install_relpaths.append(new_install_relpath)
                    del m, fslash_relpath, new_install_relpath
                else:
345 346
                    if not found:
                        raise Exception("Not found: '%s'" % relpath_re)
347
                del r, found
O
Olli-Pekka Heinisuo 已提交
348

349 350 351
        del relpaths_zip

        print("Copying files from non-default sourcetree locations")
352

353
        for package_name, paths in cls.files_outside_package.items():
354
            package_dest_reldir = package_name.replace(".", os.path.sep)
355 356
            for path in paths:
                new_install_relpath = os.path.join(
357 358 359 360 361
                    package_dest_reldir,
                    # Don't yet have a need to copy
                    # to subdirectories of package dir
                    os.path.basename(path),
                )
362
                cls._setuptools_wrap._copy_file(
363 364 365
                    path,
                    os.path.join(cmake_install_dir, new_install_relpath),
                    hide_listing=False,
366 367 368
                )
                final_install_relpaths.append(new_install_relpath)

369 370 371
        final_install_paths = [
            os.path.join(cmake_install_dir, p) for p in final_install_relpaths
        ]
372

O
Olli-Pekka Heinisuo 已提交
373
        return (cls.wraps._classify_installed_files)(
374
            final_install_paths,
375 376 377 378 379 380
            package_data,
            package_prefixes,
            py_modules,
            new_py_modules,
            scripts,
            new_scripts,
381
            data_files,
382
            # To get around a check that prepends source dir to paths and breaks package detection code.
383 384
            cmake_source_dir="",
            cmake_install_dir=cmake_install_reldir,
385
        )
386

O
Olli-Pekka Heinisuo 已提交
387

388
def get_and_set_info(contrib, headless, ci_build):
O
Olli-Pekka Heinisuo 已提交
389 390 391 392
    # cv2/version.py should be generated by running find_version.py
    version = {}
    here = os.path.abspath(os.path.dirname(__file__))
    version_file = os.path.join(here, "cv2", "version.py")
393

O
Olli-Pekka Heinisuo 已提交
394
    # generate a fresh version.py always when Git repository exists
395
    # (in sdists the version.py file already exists)
O
Olli-Pekka Heinisuo 已提交
396 397
    if os.path.exists(".git"):
        old_args = sys.argv.copy()
398
        sys.argv = ["", str(contrib), str(headless), str(ci_build)]
O
Olli-Pekka Heinisuo 已提交
399 400
        runpy.run_path("find_version.py", run_name="__main__")
        sys.argv = old_args
401

O
Olli-Pekka Heinisuo 已提交
402 403
    with open(version_file) as fp:
        exec(fp.read(), version)
404

O
Olli-Pekka Heinisuo 已提交
405 406
    return version["opencv_version"], version["contrib"], version["headless"]

407

O
Olli-Pekka Heinisuo 已提交
408 409 410
def get_build_env_var_by_name(flag_name):
    flag_set = False

411
    try:
412
        flag_set = bool(int(os.getenv("ENABLE_" + flag_name.upper(), None)))
413 414 415
    except Exception:
        pass

O
Olli-Pekka Heinisuo 已提交
416
    if not flag_set:
417
        try:
O
Olli-Pekka Heinisuo 已提交
418
            flag_set = bool(int(open(flag_name + ".enabled").read(1)))
419 420
        except Exception:
            pass
O
Olli-Pekka Heinisuo 已提交
421 422

    return flag_set
423

O
Olli-Pekka Heinisuo 已提交
424

425 426
# This creates a list which is empty but returns a length of 1.
# Should make the wheel a binary distribution and platlib compliant.
O
Olli-Pekka Heinisuo 已提交
427 428 429 430
class EmptyListWithLength(list):
    def __len__(self):
        return 1

431

432
if __name__ == "__main__":
433
    main()