#!/usr/bin/env python # -*- coding: utf-8 -*- # Setup script for PyPI; use CMakeFile.txt to build extension modules import contextlib import os import re import shutil import string import subprocess import sys import tempfile import setuptools.command.sdist DIR = os.path.abspath(os.path.dirname(__file__)) VERSION_REGEX = re.compile( r"^\s*#\s*define\s+PYBIND11_VERSION_([A-Z]+)\s+(.*)$", re.MULTILINE ) # PYBIND11_GLOBAL_SDIST will build a different sdist, with the python-headers # files, and the sys.prefix files (CMake and headers). global_sdist = os.environ.get("PYBIND11_GLOBAL_SDIST", False) setup_py = "tools/setup_global.py.in" if global_sdist else "tools/setup_main.py.in" extra_cmd = 'cmdclass["sdist"] = SDist\n' to_src = ( ("pyproject.toml", "tools/pyproject.toml"), ("setup.py", setup_py), ) # Read the listed version with open("pybind11/_version.py") as f: code = compile(f.read(), "pybind11/_version.py", "exec") loc = {} exec(code, loc) version = loc["__version__"] # Verify that the version matches the one in C++ with open("include/pybind11/detail/common.h") as f: matches = dict(VERSION_REGEX.findall(f.read())) cpp_version = "{MAJOR}.{MINOR}.{PATCH}".format(**matches) if version != cpp_version: msg = "Python version {} does not match C++ version {}!".format( version, cpp_version ) raise RuntimeError(msg) def get_and_replace(filename, binary=False, **opts): with open(filename, "rb" if binary else "r") as f: contents = f.read() # Replacement has to be done on text in Python 3 (both work in Python 2) if binary: return string.Template(contents.decode()).substitute(opts).encode() else: return string.Template(contents).substitute(opts) # Use our input files instead when making the SDist (and anything that depends # on it, like a wheel) class SDist(setuptools.command.sdist.sdist): def make_release_tree(self, base_dir, files): setuptools.command.sdist.sdist.make_release_tree(self, base_dir, files) for to, src in to_src: txt = get_and_replace(src, binary=True, version=version, extra_cmd="") dest = os.path.join(base_dir, to) # This is normally linked, so unlink before writing! os.unlink(dest) with open(dest, "wb") as f: f.write(txt) # Backport from Python 3 @contextlib.contextmanager def TemporaryDirectory(): # noqa: N802 "Prepare a temporary directory, cleanup when done" try: tmpdir = tempfile.mkdtemp() yield tmpdir finally: shutil.rmtree(tmpdir) # Remove the CMake install directory when done @contextlib.contextmanager def remove_output(*sources): try: yield finally: for src in sources: shutil.rmtree(src) with remove_output("pybind11/include", "pybind11/share"): # Generate the files if they are not present. with TemporaryDirectory() as tmpdir: cmd = ["cmake", "-S", ".", "-B", tmpdir] + [ "-DCMAKE_INSTALL_PREFIX=pybind11", "-DBUILD_TESTING=OFF", "-DPYBIND11_NOPYTHON=ON", ] cmake_opts = dict(cwd=DIR, stdout=sys.stdout, stderr=sys.stderr) subprocess.check_call(cmd, **cmake_opts) subprocess.check_call(["cmake", "--install", tmpdir], **cmake_opts) txt = get_and_replace(setup_py, version=version, extra_cmd=extra_cmd) code = compile(txt, setup_py, "exec") exec(code, {"SDist": SDist})