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