diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index e50dc0bb72..3d6d1e39fd 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -32,9 +32,6 @@ jobs: - name: Add matchers run: echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json" - uses: pre-commit/action@v3.0.1 - with: - # Slow hooks are marked with manual - slow is okay here, run them too - extra_args: --hook-stage manual --all-files clang-tidy: # When making changes here, please also review the "Clang-Tidy" section diff --git a/.gitignore b/.gitignore index 4daf6119c3..0efef67295 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,8 @@ pybind11Targets.cmake .ipynb_checkpoints/ tests/main.cpp CMakeUserPresents.json + +/Python +/tmp* +.ruby-version +.*cache*/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 34354898b6..b45781b65e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -108,15 +108,6 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal -# Checks the manifest for missing files (native support) -- repo: https://github.com/mgedmin/check-manifest - rev: "0.50" - hooks: - - id: check-manifest - # This is a slow hook, so only run this if --hook-stage manual is passed - stages: [manual] - additional_dependencies: [cmake, ninja] - # Check for spelling # Use tools/codespell_ignore_lines_from_errors.py # to rebuild .codespell-ignore-lines diff --git a/CMakeLists.txt b/CMakeLists.txt index d570112dd1..e824383b1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,13 @@ if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) message(STATUS "CMake ${CMAKE_VERSION}") + if(DEFINED SKBUILD AND DEFINED $ENV{PYBIND11_GLOBAL_PREFIX}) + message( + FATAL_ERROR + "PYBIND11_GLOBAL_PREFIX is not supported, use nox -s build_global or a pybind11-global SDist instead." + ) + endif() + if(CMAKE_CXX_STANDARD) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -248,6 +255,9 @@ elseif(USE_PYTHON_INCLUDE_DIR AND DEFINED PYTHON_INCLUDE_DIR) endif() if(PYBIND11_INSTALL) + if(DEFINED SKBUILD_PROJECT_NAME AND SKBUILD_PROJECT_NAME STREQUAL "pybind11_global") + install(DIRECTORY ${pybind11_INCLUDE_DIR}/pybind11 DESTINATION "${SKBUILD_HEADERS_DIR}") + endif() install(DIRECTORY ${pybind11_INCLUDE_DIR}/pybind11 DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) set(PYBIND11_CMAKECONFIG_INSTALL_DIR "${CMAKE_INSTALL_DATAROOTDIR}/cmake/${PROJECT_NAME}" @@ -314,6 +324,17 @@ if(PYBIND11_INSTALL) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pybind11.pc" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig/") + # When building a wheel, include __init__.py's for modules + # (see https://github.com/pybind/pybind11/pull/5552) + if(DEFINED SKBUILD_PROJECT_NAME AND SKBUILD_PROJECT_NAME STREQUAL "pybind11") + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/empty") + file(TOUCH "${CMAKE_CURRENT_BINARY_DIR}/empty/__init__.py") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/empty/__init__.py" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/empty/__init__.py" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig/") + endif() + # Uninstall target if(PYBIND11_MASTER_PROJECT) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake_uninstall.cmake.in" diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 7ce83c5527..0000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -prune tests -recursive-include pybind11/include/pybind11 *.h -recursive-include pybind11 *.py -recursive-include pybind11 py.typed -include pybind11/share/cmake/pybind11/*.cmake -include LICENSE README.rst SECURITY.md pyproject.toml setup.py setup.cfg diff --git a/docs/conf.py b/docs/conf.py index e5cba03826..a8758200f4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -69,9 +69,10 @@ # built documents. # Read the listed version -with open("../pybind11/_version.py") as f: - code = compile(f.read(), "../pybind11/_version.py", "exec") -loc = {} +version_file = DIR.parent / "pybind11/_version.py" +with version_file.open(encoding="utf-8") as f: + code = compile(f.read(), version_file, "exec") +loc = {"__file__": str(version_file)} exec(code, loc) # The full version, including alpha/beta/rc tags. diff --git a/noxfile.py b/noxfile.py index 773687fb0a..778025229c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,6 +7,13 @@ from __future__ import annotations import argparse +import contextlib +import os +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Generator import nox @@ -99,13 +106,46 @@ def make_changelog(session: nox.Session) -> None: @nox.session(reuse_venv=True, default=False) def build(session: nox.Session) -> None: """ - Build SDists and wheels. + Build SDist and wheel. """ session.install("build") session.log("Building normal files") session.run("python", "-m", "build", *session.posargs) - session.log("Building pybind11-global files (PYBIND11_GLOBAL_SDIST=1)") - session.run( - "python", "-m", "build", *session.posargs, env={"PYBIND11_GLOBAL_SDIST": "1"} - ) + + +@contextlib.contextmanager +def preserve_file(filename: Path) -> Generator[str, None, None]: + """ + Causes a file to be stored and preserved when the context manager exits. + """ + old_stat = filename.stat() + old_file = filename.read_text(encoding="utf-8") + try: + yield old_file + finally: + filename.write_text(old_file, encoding="utf-8") + os.utime(filename, (old_stat.st_atime, old_stat.st_mtime)) + + +@nox.session(reuse_venv=True) +def build_global(session: nox.Session) -> None: + """ + Build global SDist and wheel. + """ + + installer = ["--installer=uv"] if session.venv_backend == "uv" else [] + session.install("build", "tomlkit") + session.log("Building pybind11-global files") + pyproject = Path("pyproject.toml") + with preserve_file(pyproject): + newer_txt = session.run("python", "tools/make_global.py", silent=True) + assert isinstance(newer_txt, str) + pyproject.write_text(newer_txt, encoding="utf-8") + session.run( + "python", + "-m", + "build", + *installer, + *session.posargs, + ) diff --git a/pybind11/_version.py b/pybind11/_version.py index 2dfe67616d..d31cadb0a6 100644 --- a/pybind11/_version.py +++ b/pybind11/_version.py @@ -1,5 +1,28 @@ +# This file will be replaced in the wheel with a hard-coded version. This only +# exists to allow running directly from source without installing (not +# recommended, but supported). + from __future__ import annotations +import re +from pathlib import Path + +DIR = Path(__file__).parent.resolve() + +input_file = DIR.parent / "include/pybind11/detail/common.h" +regex = re.compile( + r""" +\#define \s+ PYBIND11_VERSION_MAJOR \s+ (?P\d+) .*? +\#define \s+ PYBIND11_VERSION_MINOR \s+ (?P\d+) .*? +\#define \s+ PYBIND11_VERSION_PATCH \s+ (?P\S+) +""", + re.MULTILINE | re.DOTALL | re.VERBOSE, +) + +match = regex.search(input_file.read_text(encoding="utf-8")) +assert match, "Unable to find version in pybind11/detail/common.h" +__version__ = "{major}.{minor}.{patch}".format(**match.groupdict()) + def _to_int(s: str) -> int | str: try: @@ -8,5 +31,4 @@ def _to_int(s: str) -> int | str: return s -__version__ = "3.0.0.dev1" version_info = tuple(_to_int(s) for s in __version__.split(".")) diff --git a/pyproject.toml b/pyproject.toml index 38deff474a..2d9ba0ba45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,25 +1,106 @@ [build-system] -requires = ["setuptools>=42", "cmake>=3.18", "ninja"] -build-backend = "setuptools.build_meta" +requires = ["scikit-build-core >=0.11.0"] +build-backend = "scikit_build_core.build" +[project] +name = "pybind11" +description = "Seamless operability between C++11 and Python" +authors = [{name = "Wenzel Jakob", email = "wenzel.jakob@epfl.ch"}] +license = "BSD-3-Clause" +license-files = ["LICENSE"] +readme = "README.rst" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", + "Programming Language :: C++", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: C++", + "Topic :: Software Development :: Libraries :: Python Modules", +] +keywords = [ + "C++11", + "Python bindings", +] +dynamic = ["version"] +requires-python = ">=3.8" -[tool.check-manifest] -ignore = [ - "tests/**", - "docs/**", - "tools/**", - "include/**", - ".*", - "pybind11/include/**", - "pybind11/share/**", - "CMakeLists.txt", - "noxfile.py", +[project.urls] +Homepage = "https://github.com/pybind/pybind11" +Documentation = "https://pybind11.readthedocs.io/" +"Bug Tracker" = "https://github.com/pybind/pybind11/issues" +Discussions = "https://github.com/pybind/pybind11/discussions" +Changelog = "https://pybind11.readthedocs.io/en/latest/changelog.html" +Chat = "https://gitter.im/pybind/Lobby" + +[project.optional-dependencies] +global = ["pybind11-global"] # TODO: pin + +[project.scripts] +pybind11-config = "pybind11.__main__:main" + +[project.entry-points."pipx.run"] +pybind11 = "pybind11.__main__:main" + +[project.entry-points.pkg_config] +pybind11 = "pybind11.share.pkgconfig" + + +[tool.scikit-build] +minimum-version = "build-system.requires" +sdist.exclude = [ + "/docs/**", + "/.**", ] +wheel.install-dir = "pybind11" +wheel.platlib = false + +[tool.scikit-build.cmake.define] +BUILD_TESTING = false +PYBIND11_NOPYTHON = true +prefix_for_pc_file = "${pcfiledir}/../../" + +[tool.scikit-build.metadata.version] +provider = "scikit_build_core.metadata.regex" +input = "include/pybind11/detail/common.h" +regex = '''(?sx) +\#define \s+ PYBIND11_VERSION_MAJOR \s+ (?P\d+) .*? +\#define \s+ PYBIND11_VERSION_MINOR \s+ (?P\d+) .*? +\#define \s+ PYBIND11_VERSION_PATCH \s+ (?P\S+) +''' +result = "{major}.{minor}.{patch}" # Can't use tool.uv.sources with requirements.txt [tool.uv] index-strategy = "unsafe-best-match" +[[tool.scikit-build.generate]] +path = "pybind11/_version.py" +template = ''' +from __future__ import annotations + + +def _to_int(s: str) -> int | str: + try: + return int(s) + except ValueError: + return s + + +__version__ = "$version" +version_info = tuple(_to_int(s) for s in __version__.split(".")) +''' + + [tool.mypy] files = ["pybind11"] python_version = "3.8" @@ -28,7 +109,7 @@ enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] warn_unreachable = true [[tool.mypy.overrides]] -module = ["ghapi.*"] +module = ["ghapi.*", "tomlkit"] # tomlkit has types, but not very helpful ignore_missing_imports = true @@ -45,10 +126,10 @@ messages_control.disable = [ "protected-access", "missing-module-docstring", "unused-argument", # covered by Ruff ARG + "consider-using-f-string", # triggers in _version.py incorrectly ] [tool.ruff] -target-version = "py38" src = ["src"] [tool.ruff.lint] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index bb5b744aae..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,42 +0,0 @@ -[metadata] -long_description = file: README.rst -long_description_content_type = text/x-rst -description = Seamless operability between C++11 and Python -author = Wenzel Jakob -author_email = wenzel.jakob@epfl.ch -url = https://github.com/pybind/pybind11 -license = BSD - -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - Topic :: Software Development :: Libraries :: Python Modules - Topic :: Utilities - Programming Language :: C++ - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 - License :: OSI Approved :: BSD License - Programming Language :: Python :: Implementation :: PyPy - Programming Language :: Python :: Implementation :: CPython - Programming Language :: C++ - Topic :: Software Development :: Libraries :: Python Modules - -keywords = - C++11 - Python bindings - -project_urls = - Documentation = https://pybind11.readthedocs.io/ - Bug Tracker = https://github.com/pybind/pybind11/issues - Discussions = https://github.com/pybind/pybind11/discussions - Changelog = https://pybind11.readthedocs.io/en/latest/changelog.html - Chat = https://gitter.im/pybind/Lobby - -[options] -python_requires = >=3.8 -zip_safe = False diff --git a/setup.py b/setup.py deleted file mode 100644 index a5af04e18e..0000000000 --- a/setup.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env python3 - -# Setup script for PyPI; use CMakeFile.txt to build extension modules -from __future__ import annotations - -import contextlib -import os -import re -import shutil -import string -import subprocess -import sys -from collections.abc import Generator -from pathlib import Path -from tempfile import TemporaryDirectory - -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") - -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) -> 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): - 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) -> Generator[None, None, 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", - "-Dprefix_for_pc_file=${pcfiledir}/../../", - ] - 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, - ) - - # pkgconf-pypi needs pybind11/share/pkgconfig to be importable - Path("pybind11/share/__init__.py").touch() - Path("pybind11/share/pkgconfig/__init__.py").touch() - - txt = get_and_replace(setup_py, version=version, extra_cmd=extra_cmd) - code = compile(txt, setup_py, "exec") - exec(code, {"SDist": SDist}) diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 017b52f6a9..6ecf26ac0d 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -3,16 +3,23 @@ import contextlib import os import shutil -import string import subprocess import sys import tarfile import zipfile +from pathlib import Path +from typing import Generator # These tests must be run explicitly -DIR = os.path.abspath(os.path.dirname(__file__)) -MAIN_DIR = os.path.dirname(os.path.dirname(DIR)) +DIR = Path(__file__).parent.resolve() +MAIN_DIR = DIR.parent.parent + + +# Newer pytest has global path setting, but keeping old pytest for now +sys.path.append(str(MAIN_DIR / "tools")) + +from make_global import get_global # noqa: E402 HAS_UV = shutil.which("uv") is not None UV_ARGS = ["--installer=uv"] if HAS_UV else [] @@ -120,44 +127,44 @@ } headers = main_headers | conduit_headers | detail_headers | eigen_headers | stl_headers -src_files = headers | cmake_files | pkgconfig_files -all_files = src_files | py_files - +generated_files = cmake_files | pkgconfig_files +all_files = headers | generated_files | py_files sdist_files = { - "pybind11", - "pybind11/include", - "pybind11/include/pybind11", - "pybind11/include/pybind11/conduit", - "pybind11/include/pybind11/detail", - "pybind11/include/pybind11/eigen", - "pybind11/include/pybind11/stl", - "pybind11/share", - "pybind11/share/cmake", - "pybind11/share/cmake/pybind11", - "pybind11/share/pkgconfig", "pyproject.toml", - "setup.cfg", - "setup.py", "LICENSE", - "MANIFEST.in", "README.rst", "PKG-INFO", "SECURITY.md", } -local_sdist_files = { - ".egg-info", - ".egg-info/PKG-INFO", - ".egg-info/SOURCES.txt", - ".egg-info/dependency_links.txt", - ".egg-info/not-zip-safe", - ".egg-info/top_level.txt", -} + +@contextlib.contextmanager +def preserve_file(filename: Path) -> Generator[str, None, None]: + old_stat = filename.stat() + old_file = filename.read_text(encoding="utf-8") + try: + yield old_file + finally: + filename.write_text(old_file, encoding="utf-8") + os.utime(filename, (old_stat.st_atime, old_stat.st_mtime)) + + +@contextlib.contextmanager +def build_global() -> Generator[None, None, None]: + """ + Build global SDist and wheel. + """ + + pyproject = MAIN_DIR / "pyproject.toml" + with preserve_file(pyproject): + newer_txt = get_global() + pyproject.write_text(newer_txt, encoding="utf-8") + yield def read_tz_file(tar: tarfile.TarFile, name: str) -> bytes: - start = tar.getnames()[0] + "/" + start = tar.getnames()[0].split("/")[0] + "/" inner_file = tar.extractfile(tar.getmember(f"{start}{name}")) assert inner_file with contextlib.closing(inner_file) as f: @@ -179,93 +186,43 @@ def test_build_sdist(monkeypatch, tmpdir): (sdist,) = tmpdir.visit("*.tar.gz") with tarfile.open(str(sdist), "r:gz") as tar: - start = tar.getnames()[0] + "/" - version = start[9:-1] simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]} - setup_py = read_tz_file(tar, "setup.py") pyproject_toml = read_tz_file(tar, "pyproject.toml") - pkgconfig = read_tz_file(tar, "pybind11/share/pkgconfig/pybind11.pc") - cmake_cfg = read_tz_file( - tar, "pybind11/share/cmake/pybind11/pybind11Config.cmake" - ) - - assert ( - 'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")' - in cmake_cfg.decode("utf-8") - ) - - files = {f"pybind11/{n}" for n in all_files} - files |= sdist_files - files |= {f"pybind11{n}" for n in local_sdist_files} - files.add("pybind11.egg-info/entry_points.txt") - files.add("pybind11.egg-info/requires.txt") - assert simpler == files - - with open(os.path.join(MAIN_DIR, "tools", "setup_main.py.in"), "rb") as f: - contents = ( - string.Template(f.read().decode("utf-8")) - .substitute(version=version, extra_cmd="") - .encode("utf-8") - ) - assert setup_py == contents - with open(os.path.join(MAIN_DIR, "tools", "pyproject.toml"), "rb") as f: - contents = f.read() - assert pyproject_toml == contents + files = headers | sdist_files + assert files <= simpler - simple_version = ".".join(version.split(".")[:3]) - pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version).encode("utf-8") - assert normalize_line_endings(pkgconfig) == pkgconfig_expected + assert b'name = "pybind11"' in pyproject_toml def test_build_global_dist(monkeypatch, tmpdir): monkeypatch.chdir(MAIN_DIR) - monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1") - subprocess.run( - [sys.executable, "-m", "build", "--sdist", "--outdir", str(tmpdir), *UV_ARGS], - check=True, - ) + with build_global(): + subprocess.run( + [ + sys.executable, + "-m", + "build", + "--sdist", + "--outdir", + str(tmpdir), + *UV_ARGS, + ], + check=True, + ) (sdist,) = tmpdir.visit("*.tar.gz") with tarfile.open(str(sdist), "r:gz") as tar: - start = tar.getnames()[0] + "/" - version = start[16:-1] simpler = {n.split("/", 1)[-1] for n in tar.getnames()[1:]} - setup_py = read_tz_file(tar, "setup.py") pyproject_toml = read_tz_file(tar, "pyproject.toml") - pkgconfig = read_tz_file(tar, "pybind11/share/pkgconfig/pybind11.pc") - cmake_cfg = read_tz_file( - tar, "pybind11/share/cmake/pybind11/pybind11Config.cmake" - ) - assert ( - 'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")' - in cmake_cfg.decode("utf-8") - ) + files = headers | sdist_files + assert files <= simpler - files = {f"pybind11/{n}" for n in all_files} - files |= sdist_files - files |= {f"pybind11_global{n}" for n in local_sdist_files} - assert simpler == files - - with open(os.path.join(MAIN_DIR, "tools", "setup_global.py.in"), "rb") as f: - contents = ( - string.Template(f.read().decode()) - .substitute(version=version, extra_cmd="") - .encode("utf-8") - ) - assert setup_py == contents - - with open(os.path.join(MAIN_DIR, "tools", "pyproject.toml"), "rb") as f: - contents = f.read() - assert pyproject_toml == contents - - simple_version = ".".join(version.split(".")[:3]) - pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version).encode("utf-8") - assert normalize_line_endings(pkgconfig) == pkgconfig_expected + assert b'name = "pybind11-global"' in pyproject_toml def tests_build_wheel(monkeypatch, tmpdir): @@ -280,47 +237,79 @@ def tests_build_wheel(monkeypatch, tmpdir): files = {f"pybind11/{n}" for n in all_files} files |= { - "dist-info/LICENSE", + "dist-info/licenses/LICENSE", "dist-info/METADATA", "dist-info/RECORD", "dist-info/WHEEL", "dist-info/entry_points.txt", - "dist-info/top_level.txt", } with zipfile.ZipFile(str(wheel)) as z: names = z.namelist() + share = zipfile.Path(z, "pybind11/share") + pkgconfig = (share / "pkgconfig/pybind11.pc").read_text(encoding="utf-8") + cmakeconfig = (share / "cmake/pybind11/pybind11Config.cmake").read_text( + encoding="utf-8" + ) trimmed = {n for n in names if "dist-info" not in n} trimmed |= {f"dist-info/{n.split('/', 1)[-1]}" for n in names if "dist-info" in n} + assert files == trimmed + assert 'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")' in cmakeconfig + + version = wheel.basename.split("-")[1] + simple_version = ".".join(version.split(".")[:3]) + pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version) + assert pkgconfig_expected == pkgconfig + def tests_build_global_wheel(monkeypatch, tmpdir): monkeypatch.chdir(MAIN_DIR) - monkeypatch.setenv("PYBIND11_GLOBAL_SDIST", "1") - - subprocess.run( - [sys.executable, "-m", "build", "--wheel", "--outdir", str(tmpdir), *UV_ARGS], - check=True, - ) + with build_global(): + subprocess.run( + [ + sys.executable, + "-m", + "build", + "--wheel", + "--outdir", + str(tmpdir), + *UV_ARGS, + ], + check=True, + ) (wheel,) = tmpdir.visit("*.whl") - files = {f"data/data/{n}" for n in src_files} + files = {f"data/data/{n}" for n in headers} files |= {f"data/headers/{n[8:]}" for n in headers} + files |= {f"data/data/{n}" for n in generated_files} files |= { - "dist-info/LICENSE", + "dist-info/licenses/LICENSE", "dist-info/METADATA", "dist-info/WHEEL", - "dist-info/top_level.txt", "dist-info/RECORD", } with zipfile.ZipFile(str(wheel)) as z: names = z.namelist() + beginning = names[0].split("/", 1)[0].rsplit(".", 1)[0] + + share = zipfile.Path(z, f"{beginning}.data/data/share") + pkgconfig = (share / "pkgconfig/pybind11.pc").read_text(encoding="utf-8") + cmakeconfig = (share / "cmake/pybind11/pybind11Config.cmake").read_text( + encoding="utf-8" + ) - beginning = names[0].split("/", 1)[0].rsplit(".", 1)[0] trimmed = {n[len(beginning) + 1 :] for n in names} assert files == trimmed + + assert 'set(pybind11_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")' in cmakeconfig + + version = wheel.basename.split("-")[1] + simple_version = ".".join(version.split(".")[:3]) + pkgconfig_expected = PKGCONFIG.format(VERSION=simple_version) + assert pkgconfig_expected == pkgconfig diff --git a/tests/requirements.txt b/tests/requirements.txt index 96c0cbcee8..6e3a260b19 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -15,3 +15,4 @@ scipy~=1.5.4; platform_python_implementation=="CPython" and python_version<"3.10 scipy~=1.8.0; platform_python_implementation=="CPython" and python_version=="3.10" and sys_platform!="win32" scipy~=1.11.1; platform_python_implementation=="CPython" and python_version>="3.11" and python_version<"3.13" and sys_platform!="win32" scipy~=1.15.2; platform_python_implementation=="CPython" and python_version=="3.13" and sys_platform!="win32" +tomlkit diff --git a/tools/make_global.py b/tools/make_global.py new file mode 100755 index 0000000000..5d12df4b2c --- /dev/null +++ b/tools/make_global.py @@ -0,0 +1,33 @@ +#!/usr/bin/env -S uv run -q + +# /// script +# dependencies = ["tomlkit"] +# /// +from __future__ import annotations + +from pathlib import Path + +import tomlkit + +DIR = Path(__file__).parent.resolve() +PYPROJECT = DIR.parent / "pyproject.toml" + + +def get_global() -> str: + pyproject = tomlkit.parse(PYPROJECT.read_text()) + del pyproject["tool"]["scikit-build"]["generate"] + del pyproject["project"]["optional-dependencies"] + del pyproject["project"]["entry-points"] + del pyproject["project"]["scripts"] + pyproject["project"]["name"] = "pybind11-global" + pyproject["tool"]["scikit-build"]["experimental"] = True + pyproject["tool"]["scikit-build"]["wheel"]["install-dir"] = "/data" + pyproject["tool"]["scikit-build"]["wheel"]["packages"] = [] + + result = tomlkit.dumps(pyproject) + assert isinstance(result, str) + return result + + +if __name__ == "__main__": + print(get_global()) diff --git a/tools/pyproject.toml b/tools/pyproject.toml deleted file mode 100644 index 8fe2f47af9..0000000000 --- a/tools/pyproject.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build-system] -requires = ["setuptools>=42", "wheel"] -build-backend = "setuptools.build_meta" diff --git a/tools/setup_global.py.in b/tools/setup_global.py.in deleted file mode 100644 index 99b8a2b29e..0000000000 --- a/tools/setup_global.py.in +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -# Setup script for pybind11-global (in the sdist or in tools/setup_global.py in the repository) -# This package is targeted for easy use from CMake. - -import glob -import os -import re - -# Setuptools has to be before distutils -from setuptools import setup - -from distutils.command.install_headers import install_headers - -class InstallHeadersNested(install_headers): - def run(self): - headers = self.distribution.headers or [] - for header in headers: - # Remove pybind11/include/ - short_header = header.split("/", 2)[-1] - - dst = os.path.join(self.install_dir, os.path.dirname(short_header)) - self.mkpath(dst) - (out, _) = self.copy_file(header, dst) - self.outfiles.append(out) - - -main_headers = glob.glob("pybind11/include/pybind11/*.h") -conduit_headers = sum([glob.glob(f"pybind11/include/pybind11/conduit/*.{ext}") - for ext in ("h", "txt")], []) -detail_headers = glob.glob("pybind11/include/pybind11/detail/*.h") -eigen_headers = glob.glob("pybind11/include/pybind11/eigen/*.h") -stl_headers = glob.glob("pybind11/include/pybind11/stl/*.h") -cmake_files = glob.glob("pybind11/share/cmake/pybind11/*.cmake") -pkgconfig_files = glob.glob("pybind11/share/pkgconfig/*.pc") -headers = main_headers + conduit_headers + detail_headers + eigen_headers + stl_headers - -cmdclass = {"install_headers": InstallHeadersNested} -$extra_cmd - -# This will _not_ affect installing from wheels, -# only building wheels or installing from SDist. -# Primarily intended on Windows, where this is sometimes -# customized (for example, conda-forge uses Library/) -base = os.environ.get("PYBIND11_GLOBAL_PREFIX", "") - -# Must have a separator -if base and not base.endswith("/"): - base += "/" - -setup( - name="pybind11_global", - version="$version", - packages=[], - headers=headers, - data_files=[ - (base + "share/cmake/pybind11", cmake_files), - (base + "share/pkgconfig", pkgconfig_files), - (base + "include/pybind11", main_headers), - (base + "include/pybind11/conduit", conduit_headers), - (base + "include/pybind11/detail", detail_headers), - (base + "include/pybind11/eigen", eigen_headers), - (base + "include/pybind11/stl", stl_headers), - ], - cmdclass=cmdclass, -) diff --git a/tools/setup_main.py.in b/tools/setup_main.py.in deleted file mode 100644 index e04dc82049..0000000000 --- a/tools/setup_main.py.in +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 - -# Setup script (in the sdist or in tools/setup_main.py in the repository) - -from setuptools import setup - -cmdclass = {} -$extra_cmd - -setup( - name="pybind11", - version="$version", - download_url='https://github.com/pybind/pybind11/tarball/v$version', - packages=[ - "pybind11", - "pybind11.include.pybind11", - "pybind11.include.pybind11.conduit", - "pybind11.include.pybind11.detail", - "pybind11.include.pybind11.eigen", - "pybind11.include.pybind11.stl", - "pybind11.share", - "pybind11.share.cmake.pybind11", - "pybind11.share.pkgconfig", - ], - package_data={ - "pybind11": ["py.typed"], - "pybind11.include.pybind11": ["*.h"], - "pybind11.include.pybind11.conduit": ["*.h", "*.txt"], - "pybind11.include.pybind11.detail": ["*.h"], - "pybind11.include.pybind11.eigen": ["*.h"], - "pybind11.include.pybind11.stl": ["*.h"], - "pybind11.share.cmake.pybind11": ["*.cmake"], - "pybind11.share.pkgconfig": ["*.pc"], - }, - extras_require={ - "global": ["pybind11_global==$version"] - }, - entry_points={ - "console_scripts": [ - "pybind11-config = pybind11.__main__:main", - ], - "pipx.run": [ - "pybind11 = pybind11.__main__:main", - ], - "pkg_config": [ - "pybind11 = pybind11.share.pkgconfig", - ], - }, - cmdclass=cmdclass -)