150 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Python
		
	
	
		
		
			
		
	
	
			150 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Python
		
	
	
| 
								 | 
							
								#!/usr/bin/env python3
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Setup script for PyPI; use CMakeFile.txt to build extension modules
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import contextlib
							 | 
						||
| 
								 | 
							
								import os
							 | 
						||
| 
								 | 
							
								import re
							 | 
						||
| 
								 | 
							
								import shutil
							 | 
						||
| 
								 | 
							
								import string
							 | 
						||
| 
								 | 
							
								import subprocess
							 | 
						||
| 
								 | 
							
								import sys
							 | 
						||
| 
								 | 
							
								from pathlib import Path
							 | 
						||
| 
								 | 
							
								from tempfile import TemporaryDirectory
							 | 
						||
| 
								 | 
							
								from typing import Dict, Iterator, List, Union
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import setuptools.command.sdist
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								DIR = Path(__file__).parent.absolute()
							 | 
						||
| 
								 | 
							
								VERSION_REGEX = re.compile(
							 | 
						||
| 
								 | 
							
								    r"^\s*#\s*define\s+PYBIND11_VERSION_([A-Z]+)\s+(.*)$", re.MULTILINE
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								VERSION_FILE = Path("pybind11/_version.py")
							 | 
						||
| 
								 | 
							
								COMMON_FILE = Path("include/pybind11/detail/common.h")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def build_expected_version_hex(matches: Dict[str, str]) -> str:
							 | 
						||
| 
								 | 
							
								    patch_level_serial = matches["PATCH"]
							 | 
						||
| 
								 | 
							
								    serial = None
							 | 
						||
| 
								 | 
							
								    major = int(matches["MAJOR"])
							 | 
						||
| 
								 | 
							
								    minor = int(matches["MINOR"])
							 | 
						||
| 
								 | 
							
								    flds = patch_level_serial.split(".")
							 | 
						||
| 
								 | 
							
								    if flds:
							 | 
						||
| 
								 | 
							
								        patch = int(flds[0])
							 | 
						||
| 
								 | 
							
								        if len(flds) == 1:
							 | 
						||
| 
								 | 
							
								            level = "0"
							 | 
						||
| 
								 | 
							
								            serial = 0
							 | 
						||
| 
								 | 
							
								        elif len(flds) == 2:
							 | 
						||
| 
								 | 
							
								            level_serial = flds[1]
							 | 
						||
| 
								 | 
							
								            for level in ("a", "b", "c", "dev"):
							 | 
						||
| 
								 | 
							
								                if level_serial.startswith(level):
							 | 
						||
| 
								 | 
							
								                    serial = int(level_serial[len(level) :])
							 | 
						||
| 
								 | 
							
								                    break
							 | 
						||
| 
								 | 
							
								    if serial is None:
							 | 
						||
| 
								 | 
							
								        msg = f'Invalid PYBIND11_VERSION_PATCH: "{patch_level_serial}"'
							 | 
						||
| 
								 | 
							
								        raise RuntimeError(msg)
							 | 
						||
| 
								 | 
							
								    version_hex_str = f"{major:02x}{minor:02x}{patch:02x}{level[:1]}{serial:x}"
							 | 
						||
| 
								 | 
							
								    return f"0x{version_hex_str.upper()}"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# 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 = Path(
							 | 
						||
| 
								 | 
							
								    "tools/setup_global.py.in" if global_sdist else "tools/setup_main.py.in"
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								extra_cmd = 'cmdclass["sdist"] = SDist\n'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								to_src = (
							 | 
						||
| 
								 | 
							
								    (Path("pyproject.toml"), Path("tools/pyproject.toml")),
							 | 
						||
| 
								 | 
							
								    (Path("setup.py"), setup_py),
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Read the listed version
							 | 
						||
| 
								 | 
							
								loc: Dict[str, str] = {}
							 | 
						||
| 
								 | 
							
								code = compile(VERSION_FILE.read_text(encoding="utf-8"), "pybind11/_version.py", "exec")
							 | 
						||
| 
								 | 
							
								exec(code, loc)
							 | 
						||
| 
								 | 
							
								version = loc["__version__"]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Verify that the version matches the one in C++
							 | 
						||
| 
								 | 
							
								matches = dict(VERSION_REGEX.findall(COMMON_FILE.read_text(encoding="utf8")))
							 | 
						||
| 
								 | 
							
								cpp_version = "{MAJOR}.{MINOR}.{PATCH}".format(**matches)
							 | 
						||
| 
								 | 
							
								if version != cpp_version:
							 | 
						||
| 
								 | 
							
								    msg = f"Python version {version} does not match C++ version {cpp_version}!"
							 | 
						||
| 
								 | 
							
								    raise RuntimeError(msg)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								version_hex = matches.get("HEX", "MISSING")
							 | 
						||
| 
								 | 
							
								exp_version_hex = build_expected_version_hex(matches)
							 | 
						||
| 
								 | 
							
								if version_hex != exp_version_hex:
							 | 
						||
| 
								 | 
							
								    msg = f"PYBIND11_VERSION_HEX {version_hex} does not match expected value {exp_version_hex}!"
							 | 
						||
| 
								 | 
							
								    raise RuntimeError(msg)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# TODO: use literals & overload (typing extensions or Python 3.8)
							 | 
						||
| 
								 | 
							
								def get_and_replace(
							 | 
						||
| 
								 | 
							
								    filename: Path, binary: bool = False, **opts: str
							 | 
						||
| 
								 | 
							
								) -> Union[bytes, str]:
							 | 
						||
| 
								 | 
							
								    if binary:
							 | 
						||
| 
								 | 
							
								        contents = filename.read_bytes()
							 | 
						||
| 
								 | 
							
								        return string.Template(contents.decode()).substitute(opts).encode()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return string.Template(filename.read_text()).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):  # type: ignore[misc]
							 | 
						||
| 
								 | 
							
								    def make_release_tree(self, base_dir: str, files: List[str]) -> None:
							 | 
						||
| 
								 | 
							
								        super().make_release_tree(base_dir, files)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        for to, src in to_src:
							 | 
						||
| 
								 | 
							
								            txt = get_and_replace(src, binary=True, version=version, extra_cmd="")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            dest = Path(base_dir) / to
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            # This is normally linked, so unlink before writing!
							 | 
						||
| 
								 | 
							
								            dest.unlink()
							 | 
						||
| 
								 | 
							
								            dest.write_bytes(txt)  # type: ignore[arg-type]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Remove the CMake install directory when done
							 | 
						||
| 
								 | 
							
								@contextlib.contextmanager
							 | 
						||
| 
								 | 
							
								def remove_output(*sources: str) -> Iterator[None]:
							 | 
						||
| 
								 | 
							
								    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",
							 | 
						||
| 
								 | 
							
								        ]
							 | 
						||
| 
								 | 
							
								        if "CMAKE_ARGS" in os.environ:
							 | 
						||
| 
								 | 
							
								            fcommand = [
							 | 
						||
| 
								 | 
							
								                c
							 | 
						||
| 
								 | 
							
								                for c in os.environ["CMAKE_ARGS"].split()
							 | 
						||
| 
								 | 
							
								                if "DCMAKE_INSTALL_PREFIX" not in c
							 | 
						||
| 
								 | 
							
								            ]
							 | 
						||
| 
								 | 
							
								            cmd += fcommand
							 | 
						||
| 
								 | 
							
								        subprocess.run(cmd, check=True, cwd=DIR, stdout=sys.stdout, stderr=sys.stderr)
							 | 
						||
| 
								 | 
							
								        subprocess.run(
							 | 
						||
| 
								 | 
							
								            ["cmake", "--install", tmpdir],
							 | 
						||
| 
								 | 
							
								            check=True,
							 | 
						||
| 
								 | 
							
								            cwd=DIR,
							 | 
						||
| 
								 | 
							
								            stdout=sys.stdout,
							 | 
						||
| 
								 | 
							
								            stderr=sys.stderr,
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    txt = get_and_replace(setup_py, version=version, extra_cmd=extra_cmd)
							 | 
						||
| 
								 | 
							
								    code = compile(txt, setup_py, "exec")
							 | 
						||
| 
								 | 
							
								    exec(code, {"SDist": SDist})
							 |