setup.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  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 setuptools.command.sdist
  13. DIR = os.path.abspath(os.path.dirname(__file__))
  14. VERSION_REGEX = re.compile(
  15. r"^\s*#\s*define\s+PYBIND11_VERSION_([A-Z]+)\s+(.*)$", re.MULTILINE
  16. )
  17. # PYBIND11_GLOBAL_SDIST will build a different sdist, with the python-headers
  18. # files, and the sys.prefix files (CMake and headers).
  19. global_sdist = os.environ.get("PYBIND11_GLOBAL_SDIST", False)
  20. setup_py = "tools/setup_global.py.in" if global_sdist else "tools/setup_main.py.in"
  21. extra_cmd = 'cmdclass["sdist"] = SDist\n'
  22. to_src = (
  23. ("pyproject.toml", "tools/pyproject.toml"),
  24. ("setup.py", setup_py),
  25. )
  26. # Read the listed version
  27. with open("pybind11/_version.py") as f:
  28. code = compile(f.read(), "pybind11/_version.py", "exec")
  29. loc = {}
  30. exec(code, loc)
  31. version = loc["__version__"]
  32. # Verify that the version matches the one in C++
  33. with open("include/pybind11/detail/common.h") as f:
  34. matches = dict(VERSION_REGEX.findall(f.read()))
  35. cpp_version = "{MAJOR}.{MINOR}.{PATCH}".format(**matches)
  36. if version != cpp_version:
  37. msg = "Python version {} does not match C++ version {}!".format(
  38. version, cpp_version
  39. )
  40. raise RuntimeError(msg)
  41. def get_and_replace(filename, binary=False, **opts):
  42. with open(filename, "rb" if binary else "r") as f:
  43. contents = f.read()
  44. # Replacement has to be done on text in Python 3 (both work in Python 2)
  45. if binary:
  46. return string.Template(contents.decode()).substitute(opts).encode()
  47. else:
  48. return string.Template(contents).substitute(opts)
  49. # Use our input files instead when making the SDist (and anything that depends
  50. # on it, like a wheel)
  51. class SDist(setuptools.command.sdist.sdist):
  52. def make_release_tree(self, base_dir, files):
  53. setuptools.command.sdist.sdist.make_release_tree(self, base_dir, files)
  54. for to, src in to_src:
  55. txt = get_and_replace(src, binary=True, version=version, extra_cmd="")
  56. dest = os.path.join(base_dir, to)
  57. # This is normally linked, so unlink before writing!
  58. os.unlink(dest)
  59. with open(dest, "wb") as f:
  60. f.write(txt)
  61. # Backport from Python 3
  62. @contextlib.contextmanager
  63. def TemporaryDirectory(): # noqa: N802
  64. "Prepare a temporary directory, cleanup when done"
  65. try:
  66. tmpdir = tempfile.mkdtemp()
  67. yield tmpdir
  68. finally:
  69. shutil.rmtree(tmpdir)
  70. # Remove the CMake install directory when done
  71. @contextlib.contextmanager
  72. def remove_output(*sources):
  73. try:
  74. yield
  75. finally:
  76. for src in sources:
  77. shutil.rmtree(src)
  78. with remove_output("pybind11/include", "pybind11/share"):
  79. # Generate the files if they are not present.
  80. with TemporaryDirectory() as tmpdir:
  81. cmd = ["cmake", "-S", ".", "-B", tmpdir] + [
  82. "-DCMAKE_INSTALL_PREFIX=pybind11",
  83. "-DBUILD_TESTING=OFF",
  84. "-DPYBIND11_NOPYTHON=ON",
  85. ]
  86. cmake_opts = dict(cwd=DIR, stdout=sys.stdout, stderr=sys.stderr)
  87. subprocess.check_call(cmd, **cmake_opts)
  88. subprocess.check_call(["cmake", "--install", tmpdir], **cmake_opts)
  89. txt = get_and_replace(setup_py, version=version, extra_cmd=extra_cmd)
  90. code = compile(txt, setup_py, "exec")
  91. exec(code, {"SDist": SDist})