Skip to content

Commit ebed7aa

Browse files
committed
feat: expose the precompiled-mode sources to non-CMake builds
Adds pybind11.get_source_dir() / python -m pybind11 --srcdir, a srcdir variable in pybind11.pc, and Pybind11Extension(precompile=True) which compiles src/pybind11_combined.cpp into the extension and defines PYBIND11_PRECOMPILED (hard error if the sources are missing). Assisted-by: ClaudeCode:claude-fable-5
1 parent ce07ef9 commit ebed7aa

8 files changed

Lines changed: 131 additions & 1 deletion

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ if(PYBIND11_INSTALL)
410410
endif()
411411
endif()
412412
join_paths(includedir_for_pc_file "\${prefix}" "${CMAKE_INSTALL_INCLUDEDIR}")
413+
join_paths(srcdir_for_pc_file "\${prefix}" "${CMAKE_INSTALL_DATAROOTDIR}/pybind11/src")
413414
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/tools/pybind11.pc.in"
414415
"${CMAKE_CURRENT_BINARY_DIR}/pybind11.pc" @ONLY)
415416
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pybind11.pc"

pybind11/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88

99

1010
from ._version import __version__, version_info
11-
from .commands import get_cmake_dir, get_include, get_pkgconfig_dir
11+
from .commands import get_cmake_dir, get_include, get_pkgconfig_dir, get_source_dir
1212

1313
__all__ = (
1414
"__version__",
1515
"get_cmake_dir",
1616
"get_include",
1717
"get_pkgconfig_dir",
18+
"get_source_dir",
1819
"version_info",
1920
)

pybind11/__main__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
get_include_dirs,
1818
get_ldflags,
1919
get_pkgconfig_dir,
20+
get_source_dir,
2021
)
2122

2223

@@ -50,6 +51,12 @@ def main() -> None:
5051
action="store_true",
5152
help="Print the pkgconfig directory, ideal for setting $PKG_CONFIG_PATH.",
5253
)
54+
parser.add_argument(
55+
"--srcdir",
56+
action="store_true",
57+
help="Print the directory containing the library sources for the optional"
58+
" precompiled mode.",
59+
)
5360
parser.add_argument(
5461
"--extension-suffix",
5562
action="store_true",
@@ -101,6 +108,8 @@ def main() -> None:
101108
print(quote(get_cmake_dir()))
102109
if args.pkgconfigdir:
103110
print(quote(get_pkgconfig_dir()))
111+
if args.srcdir:
112+
print(quote(get_source_dir()))
104113
if args.extension_suffix:
105114
print(ext_suffix)
106115

pybind11/commands.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,24 @@ def get_include(user: bool = False) -> str: # noqa: ARG001
5252
return installed_path if os.path.exists(installed_path) else source_path
5353

5454

55+
def get_source_dir() -> str:
56+
"""
57+
Return the path to the pybind11 library sources, for the optional
58+
precompiled mode. Compile ``pybind11_combined.cpp`` (or the individual
59+
``.cpp`` files) with ``PYBIND11_PRECOMPILED`` defined, and define that
60+
macro for every translation unit that includes pybind11.
61+
"""
62+
installed_path = os.path.join(DIR, "share", "pybind11", "src")
63+
source_path = os.path.join(os.path.dirname(DIR), "src")
64+
if os.path.exists(installed_path):
65+
return installed_path
66+
if os.path.exists(source_path):
67+
return source_path
68+
69+
msg = "pybind11 library sources not found (pybind11 not installed?)"
70+
raise ImportError(msg)
71+
72+
5573
def get_cmake_dir() -> str:
5674
"""
5775
Return the path to the pybind11 CMake module directory.

pybind11/setup_helpers.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ class Pybind11Extension(_Extension):
108108
109109
If you want to add pybind11 headers manually, for example for an exact
110110
git checkout, then set ``include_pybind11=False``.
111+
112+
Set ``precompile=True`` to compile the pybind11 library sources into the
113+
extension (one extra translation unit) instead of instantiating everything
114+
inline in every file; this usually builds faster. Requires an installed
115+
pybind11 package that ships the library sources.
111116
"""
112117

113118
# flags are prepended, so that they can be further overridden, e.g. by
@@ -127,6 +132,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
127132
kwargs["language"] = "c++"
128133

129134
include_pybind11 = kwargs.pop("include_pybind11", True)
135+
precompile = kwargs.pop("precompile", False)
130136

131137
super().__init__(*args, **kwargs)
132138

@@ -143,6 +149,27 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
143149
except ModuleNotFoundError:
144150
pass
145151

152+
if precompile:
153+
# No silent fallback: failing to precompile would quietly rebuild
154+
# everything inline, so a missing source tree is an error.
155+
try:
156+
import pybind11
157+
158+
combined = os.path.join(
159+
pybind11.get_source_dir(), "pybind11_combined.cpp"
160+
)
161+
except (ImportError, AttributeError) as err:
162+
msg = (
163+
"precompile=True requires an installed pybind11 package "
164+
"that provides the library sources"
165+
)
166+
raise ValueError(msg) from err
167+
if not os.path.exists(combined):
168+
msg = f"pybind11 library sources not found: {combined}"
169+
raise ValueError(msg)
170+
self.sources.append(combined)
171+
self.define_macros.append(("PYBIND11_PRECOMPILED", None))
172+
146173
self.cxx_std = cxx_std
147174

148175
cflags = []

tests/extra_python_package/test_files.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
PKGCONFIG = """\
3434
prefix=${{pcfiledir}}/../../
3535
includedir=${{prefix}}/include
36+
srcdir=${{prefix}}/share/pybind11/src
3637
3738
Name: pybind11
3839
Description: Seamless operability between C++11 and Python

tests/extra_setuptools/test_setuphelper.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,78 @@ def test_simple_setup_py(monkeypatch, tmpdir, parallel, std):
110110
)
111111

112112

113+
def test_precompile_setup_py(monkeypatch, tmpdir):
114+
monkeypatch.chdir(tmpdir)
115+
monkeypatch.syspath_prepend(MAIN_DIR)
116+
117+
(tmpdir / "setup.py").write_text(
118+
dedent(
119+
f"""\
120+
import sys
121+
sys.path.append({MAIN_DIR!r})
122+
123+
from setuptools import setup
124+
from pybind11.setup_helpers import Pybind11Extension
125+
126+
ext_modules = [
127+
Pybind11Extension(
128+
"precompile_setup",
129+
sorted(["main.cpp"]),
130+
cxx_std=17,
131+
precompile=True,
132+
),
133+
]
134+
135+
setup(
136+
name="precompile_setup_package",
137+
ext_modules=ext_modules,
138+
)
139+
"""
140+
),
141+
encoding="ascii",
142+
)
143+
144+
(tmpdir / "main.cpp").write_text(
145+
dedent(
146+
"""\
147+
#include <pybind11/pybind11.h>
148+
149+
#ifndef PYBIND11_PRECOMPILED
150+
# error "expected PYBIND11_PRECOMPILED to be defined"
151+
#endif
152+
153+
int f(int x) {
154+
return x * 3;
155+
}
156+
PYBIND11_MODULE(precompile_setup, m, pybind11::mod_gil_used()) {
157+
m.def("f", &f);
158+
}
159+
"""
160+
),
161+
encoding="ascii",
162+
)
163+
164+
subprocess.check_call(
165+
[sys.executable, "setup.py", "build_ext", "--inplace"],
166+
stdout=sys.stdout,
167+
stderr=sys.stderr,
168+
)
169+
170+
(tmpdir / "test.py").write_text(
171+
dedent(
172+
"""\
173+
import precompile_setup
174+
assert precompile_setup.f(3) == 9
175+
"""
176+
),
177+
encoding="ascii",
178+
)
179+
180+
subprocess.check_call(
181+
[sys.executable, "test.py"], stdout=sys.stdout, stderr=sys.stderr
182+
)
183+
184+
113185
def test_intree_extensions(monkeypatch, tmpdir):
114186
monkeypatch.syspath_prepend(MAIN_DIR)
115187

tools/pybind11.pc.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
prefix=@prefix_for_pc_file@
22
includedir=@includedir_for_pc_file@
3+
srcdir=@srcdir_for_pc_file@
34

45
Name: @PROJECT_NAME@
56
Description: Seamless operability between C++11 and Python

0 commit comments

Comments
 (0)