setup.py 3.42 KB
Newer Older
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3

Dean Moldovan's avatar
Dean Moldovan committed
4
# Setup script for PyPI; use CMakeFile.txt to build extension modules
5

6
import contextlib
7
import os
8
9
10
11
12
13
import re
import shutil
import string
import subprocess
import sys
import tempfile
14

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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")
38
39
40
loc = {}
exec(code, loc)
version = loc["__version__"]
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

# 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})