1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3 4# Setup script for PyPI; use CMakeFile.txt to build extension modules 5 6import contextlib 7import os 8import re 9import shutil 10import string 11import subprocess 12import sys 13import tempfile 14 15import setuptools.command.sdist 16 17DIR = os.path.abspath(os.path.dirname(__file__)) 18VERSION_REGEX = re.compile( 19 r"^\s*#\s*define\s+PYBIND11_VERSION_([A-Z]+)\s+(.*)$", re.MULTILINE 20) 21 22# PYBIND11_GLOBAL_SDIST will build a different sdist, with the python-headers 23# files, and the sys.prefix files (CMake and headers). 24 25global_sdist = os.environ.get("PYBIND11_GLOBAL_SDIST", False) 26 27setup_py = "tools/setup_global.py.in" if global_sdist else "tools/setup_main.py.in" 28extra_cmd = 'cmdclass["sdist"] = SDist\n' 29 30to_src = ( 31 ("pyproject.toml", "tools/pyproject.toml"), 32 ("setup.py", setup_py), 33) 34 35# Read the listed version 36with open("pybind11/_version.py") as f: 37 code = compile(f.read(), "pybind11/_version.py", "exec") 38loc = {} 39exec(code, loc) 40version = loc["__version__"] 41 42# Verify that the version matches the one in C++ 43with open("include/pybind11/detail/common.h") as f: 44 matches = dict(VERSION_REGEX.findall(f.read())) 45cpp_version = "{MAJOR}.{MINOR}.{PATCH}".format(**matches) 46if version != cpp_version: 47 msg = "Python version {} does not match C++ version {}!".format( 48 version, cpp_version 49 ) 50 raise RuntimeError(msg) 51 52 53def get_and_replace(filename, binary=False, **opts): 54 with open(filename, "rb" if binary else "r") as f: 55 contents = f.read() 56 # Replacement has to be done on text in Python 3 (both work in Python 2) 57 if binary: 58 return string.Template(contents.decode()).substitute(opts).encode() 59 else: 60 return string.Template(contents).substitute(opts) 61 62 63# Use our input files instead when making the SDist (and anything that depends 64# on it, like a wheel) 65class SDist(setuptools.command.sdist.sdist): 66 def make_release_tree(self, base_dir, files): 67 setuptools.command.sdist.sdist.make_release_tree(self, base_dir, files) 68 69 for to, src in to_src: 70 txt = get_and_replace(src, binary=True, version=version, extra_cmd="") 71 72 dest = os.path.join(base_dir, to) 73 74 # This is normally linked, so unlink before writing! 75 os.unlink(dest) 76 with open(dest, "wb") as f: 77 f.write(txt) 78 79 80# Backport from Python 3 81@contextlib.contextmanager 82def TemporaryDirectory(): # noqa: N802 83 "Prepare a temporary directory, cleanup when done" 84 try: 85 tmpdir = tempfile.mkdtemp() 86 yield tmpdir 87 finally: 88 shutil.rmtree(tmpdir) 89 90 91# Remove the CMake install directory when done 92@contextlib.contextmanager 93def remove_output(*sources): 94 try: 95 yield 96 finally: 97 for src in sources: 98 shutil.rmtree(src) 99 100 101with remove_output("pybind11/include", "pybind11/share"): 102 # Generate the files if they are not present. 103 with TemporaryDirectory() as tmpdir: 104 cmd = ["cmake", "-S", ".", "-B", tmpdir] + [ 105 "-DCMAKE_INSTALL_PREFIX=pybind11", 106 "-DBUILD_TESTING=OFF", 107 "-DPYBIND11_NOPYTHON=ON", 108 ] 109 cmake_opts = dict(cwd=DIR, stdout=sys.stdout, stderr=sys.stderr) 110 subprocess.check_call(cmd, **cmake_opts) 111 subprocess.check_call(["cmake", "--install", tmpdir], **cmake_opts) 112 113 txt = get_and_replace(setup_py, version=version, extra_cmd=extra_cmd) 114 code = compile(txt, setup_py, "exec") 115 exec(code, {"SDist": SDist}) 116