setup.py 4.7 KB

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