You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

165 lines
4.9 KiB

  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 io
  6. import os
  7. import re
  8. import shutil
  9. import string
  10. import subprocess
  11. import sys
  12. import tempfile
  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 (
  43. "0x"
  44. + "{:02x}{:02x}{:02x}{}{:x}".format(
  45. major, minor, patch, level[:1], serial
  46. ).upper()
  47. )
  48. # PYBIND11_GLOBAL_SDIST will build a different sdist, with the python-headers
  49. # files, and the sys.prefix files (CMake and headers).
  50. global_sdist = os.environ.get("PYBIND11_GLOBAL_SDIST", False)
  51. setup_py = "tools/setup_global.py.in" if global_sdist else "tools/setup_main.py.in"
  52. extra_cmd = 'cmdclass["sdist"] = SDist\n'
  53. to_src = (
  54. ("pyproject.toml", "tools/pyproject.toml"),
  55. ("setup.py", setup_py),
  56. )
  57. # Read the listed version
  58. with open("pybind11/_version.py") as f:
  59. code = compile(f.read(), "pybind11/_version.py", "exec")
  60. loc = {}
  61. exec(code, loc)
  62. version = loc["__version__"]
  63. # Verify that the version matches the one in C++
  64. with io.open("include/pybind11/detail/common.h", encoding="utf8") as f:
  65. matches = dict(VERSION_REGEX.findall(f.read()))
  66. cpp_version = "{MAJOR}.{MINOR}.{PATCH}".format(**matches)
  67. if version != cpp_version:
  68. msg = "Python version {} does not match C++ version {}!".format(
  69. version, cpp_version
  70. )
  71. raise RuntimeError(msg)
  72. version_hex = matches.get("HEX", "MISSING")
  73. expected_version_hex = build_expected_version_hex(matches)
  74. if version_hex != expected_version_hex:
  75. msg = "PYBIND11_VERSION_HEX {} does not match expected value {}!".format(
  76. version_hex,
  77. expected_version_hex,
  78. )
  79. raise RuntimeError(msg)
  80. def get_and_replace(filename, binary=False, **opts):
  81. with open(filename, "rb" if binary else "r") as f:
  82. contents = f.read()
  83. # Replacement has to be done on text in Python 3 (both work in Python 2)
  84. if binary:
  85. return string.Template(contents.decode()).substitute(opts).encode()
  86. else:
  87. return string.Template(contents).substitute(opts)
  88. # Use our input files instead when making the SDist (and anything that depends
  89. # on it, like a wheel)
  90. class SDist(setuptools.command.sdist.sdist):
  91. def make_release_tree(self, base_dir, files):
  92. setuptools.command.sdist.sdist.make_release_tree(self, base_dir, files)
  93. for to, src in to_src:
  94. txt = get_and_replace(src, binary=True, version=version, extra_cmd="")
  95. dest = os.path.join(base_dir, to)
  96. # This is normally linked, so unlink before writing!
  97. os.unlink(dest)
  98. with open(dest, "wb") as f:
  99. f.write(txt)
  100. # Backport from Python 3
  101. @contextlib.contextmanager
  102. def TemporaryDirectory(): # noqa: N802
  103. "Prepare a temporary directory, cleanup when done"
  104. try:
  105. tmpdir = tempfile.mkdtemp()
  106. yield tmpdir
  107. finally:
  108. shutil.rmtree(tmpdir)
  109. # Remove the CMake install directory when done
  110. @contextlib.contextmanager
  111. def remove_output(*sources):
  112. try:
  113. yield
  114. finally:
  115. for src in sources:
  116. shutil.rmtree(src)
  117. with remove_output("pybind11/include", "pybind11/share"):
  118. # Generate the files if they are not present.
  119. with TemporaryDirectory() as tmpdir:
  120. cmd = ["cmake", "-S", ".", "-B", tmpdir] + [
  121. "-DCMAKE_INSTALL_PREFIX=pybind11",
  122. "-DBUILD_TESTING=OFF",
  123. "-DPYBIND11_NOPYTHON=ON",
  124. ]
  125. if "CMAKE_ARGS" in os.environ:
  126. fcommand = [
  127. c
  128. for c in os.environ["CMAKE_ARGS"].split()
  129. if "DCMAKE_INSTALL_PREFIX" not in c
  130. ]
  131. cmd += fcommand
  132. cmake_opts = dict(cwd=DIR, stdout=sys.stdout, stderr=sys.stderr)
  133. subprocess.check_call(cmd, **cmake_opts)
  134. subprocess.check_call(["cmake", "--install", tmpdir], **cmake_opts)
  135. txt = get_and_replace(setup_py, version=version, extra_cmd=extra_cmd)
  136. code = compile(txt, setup_py, "exec")
  137. exec(code, {"SDist": SDist})