|
| 1 | +import os |
| 2 | +import re |
| 3 | +import subprocess |
| 4 | +import sys |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | +from setuptools import Extension, setup |
| 8 | +from setuptools.command.build_ext import build_ext |
| 9 | + |
| 10 | +# Convert distutils Windows platform specifiers to CMake -A arguments |
| 11 | +PLAT_TO_CMAKE = { |
| 12 | + "win32": "Win32", |
| 13 | + "win-amd64": "x64", |
| 14 | + "win-arm32": "ARM", |
| 15 | + "win-arm64": "ARM64", |
| 16 | +} |
| 17 | + |
| 18 | + |
| 19 | +# A CMakeExtension needs a sourcedir instead of a file list. |
| 20 | +# The name must be the _single_ output extension from the CMake build. |
| 21 | +# If you need multiple extensions, see scikit-build. |
| 22 | +class CMakeExtension(Extension): |
| 23 | + def __init__(self, name: str, sourcedir: str = "") -> None: |
| 24 | + super().__init__(name, sources=[]) |
| 25 | + self.sourcedir = os.fspath(Path(sourcedir).resolve()) |
| 26 | + |
| 27 | + |
| 28 | +class CMakeBuild(build_ext): |
| 29 | + def build_extension(self, ext: CMakeExtension) -> None: |
| 30 | + # Must be in this form due to bug in .resolve() only fixed in Python 3.10+ |
| 31 | + ext_fullpath = Path.cwd() / self.get_ext_fullpath(ext.name) |
| 32 | + extdir = ext_fullpath.parent.resolve() |
| 33 | + |
| 34 | + # Using this requires trailing slash for auto-detection & inclusion of |
| 35 | + # auxiliary "native" libs |
| 36 | + |
| 37 | + debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug |
| 38 | + cfg = "Debug" if debug else "Release" |
| 39 | + |
| 40 | + # CMake lets you override the generator - we need to check this. |
| 41 | + # Can be set with Conda-Build, for example. |
| 42 | + cmake_generator = os.environ.get("CMAKE_GENERATOR", "") |
| 43 | + |
| 44 | + # Set Python_EXECUTABLE instead if you use PYBIND11_FINDPYTHON |
| 45 | + # EXAMPLE_VERSION_INFO shows you how to pass a value into the C++ code |
| 46 | + # from Python. |
| 47 | + cmake_args = [ |
| 48 | + f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}", |
| 49 | + f"-DPYTHON_EXECUTABLE={sys.executable}", |
| 50 | + f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm |
| 51 | + ] |
| 52 | + build_args = [] |
| 53 | + # Adding CMake arguments set as environment variable |
| 54 | + # (needed e.g. to build for ARM OSx on conda-forge) |
| 55 | + if "CMAKE_ARGS" in os.environ: |
| 56 | + cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item] |
| 57 | + |
| 58 | + # In this example, we pass in the version to C++. You might not need to. |
| 59 | + cmake_args += [f"-DEXAMPLE_VERSION_INFO={self.distribution.get_version()}"] |
| 60 | + |
| 61 | + if self.compiler.compiler_type != "msvc": |
| 62 | + # Using Ninja-build since it a) is available as a wheel and b) |
| 63 | + # multithreads automatically. MSVC would require all variables be |
| 64 | + # exported for Ninja to pick it up, which is a little tricky to do. |
| 65 | + # Users can override the generator with CMAKE_GENERATOR in CMake |
| 66 | + # 3.15+. |
| 67 | + if not cmake_generator or cmake_generator == "Ninja": |
| 68 | + try: |
| 69 | + import ninja |
| 70 | + |
| 71 | + ninja_executable_path = Path(ninja.BIN_DIR) / "ninja" |
| 72 | + cmake_args += [ |
| 73 | + "-GNinja", |
| 74 | + f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}", |
| 75 | + ] |
| 76 | + except ImportError: |
| 77 | + pass |
| 78 | + |
| 79 | + else: |
| 80 | + # Single config generators are handled "normally" |
| 81 | + single_config = any(x in cmake_generator for x in {"NMake", "Ninja"}) |
| 82 | + |
| 83 | + # CMake allows an arch-in-generator style for backward compatibility |
| 84 | + contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"}) |
| 85 | + |
| 86 | + # Specify the arch if using MSVC generator, but only if it doesn't |
| 87 | + # contain a backward-compatibility arch spec already in the |
| 88 | + # generator name. |
| 89 | + if not single_config and not contains_arch: |
| 90 | + cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]] |
| 91 | + |
| 92 | + # Multi-config generators have a different way to specify configs |
| 93 | + if not single_config: |
| 94 | + cmake_args += [ |
| 95 | + f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}" |
| 96 | + ] |
| 97 | + build_args += ["--config", cfg] |
| 98 | + |
| 99 | + if sys.platform.startswith("darwin"): |
| 100 | + # Cross-compile support for macOS - respect ARCHFLAGS if set |
| 101 | + archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", "")) |
| 102 | + if archs: |
| 103 | + cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))] |
| 104 | + |
| 105 | + # Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level |
| 106 | + # across all generators. |
| 107 | + if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ: |
| 108 | + # self.parallel is a Python 3 only way to set parallel jobs by hand |
| 109 | + # using -j in the build_ext call, not supported by pip or PyPA-build. |
| 110 | + if hasattr(self, "parallel") and self.parallel: |
| 111 | + # CMake 3.12+ only. |
| 112 | + build_args += [f"-j{self.parallel}"] |
| 113 | + |
| 114 | + build_temp = Path(self.build_temp) / ext.name |
| 115 | + if not build_temp.exists(): |
| 116 | + build_temp.mkdir(parents=True) |
| 117 | + |
| 118 | + subprocess.run( |
| 119 | + ["cmake", ext.sourcedir, *cmake_args], cwd=build_temp, check=True |
| 120 | + ) |
| 121 | + subprocess.run( |
| 122 | + ["cmake", "--build", ".", *build_args], cwd=build_temp, check=True |
| 123 | + ) |
| 124 | + |
| 125 | + |
| 126 | +# The information here can also be placed in setup.cfg - better separation of |
| 127 | +# logic and declaration, and simpler if you include description/version in a file. |
| 128 | +setup( |
| 129 | + name="renderpy", |
| 130 | + version="0.0.1", |
| 131 | + author="YC Liu", |
| 132 | + |
| 133 | + description="Simple python rasterizer implemented by OpenGL and C++", |
| 134 | + long_description="", |
| 135 | + ext_modules=[CMakeExtension("cmake_example")], |
| 136 | + cmdclass={"build_ext": CMakeBuild}, |
| 137 | + zip_safe=False, |
| 138 | + extras_require={"test": ["pytest>=6.0"]}, |
| 139 | + python_requires=">=3.7", |
| 140 | +) |
0 commit comments