@@ -108,6 +108,14 @@ 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. Use the ``build_ext``
116+ from this module when you build more than one precompiled extension in
117+ one ``setup()``; it gives each extension its own copy of the library
118+ translation unit, so each gets its own object file.
111119 """
112120
113121 # flags are prepended, so that they can be further overridden, e.g. by
@@ -127,6 +135,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
127135 kwargs ["language" ] = "c++"
128136
129137 include_pybind11 = kwargs .pop ("include_pybind11" , True )
138+ precompile = kwargs .pop ("precompile" , False )
130139
131140 super ().__init__ (* args , ** kwargs )
132141
@@ -143,6 +152,40 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
143152 except ModuleNotFoundError :
144153 pass
145154
155+ self ._precompile_source : str | None = None
156+ if precompile :
157+ if not include_pybind11 :
158+ # The shipped sources must match the shipped headers; mixing
159+ # them with a different checkout gives confusing errors.
160+ msg = (
161+ "precompile=True compiles the sources of the installed "
162+ "pybind11 package, so it cannot be combined with "
163+ "include_pybind11=False. Instead, add "
164+ "src/pybind11_combined.cpp from your pybind11 checkout "
165+ "to sources and define PYBIND11_PRECOMPILED."
166+ )
167+ raise ValueError (msg )
168+ # No silent fallback: failing to precompile would quietly rebuild
169+ # everything inline, so a missing source tree is an error.
170+ try :
171+ import pybind11
172+
173+ combined = os .path .join (
174+ pybind11 .get_source_dir (), "pybind11_combined.cpp"
175+ )
176+ except (ImportError , AttributeError ) as err :
177+ msg = (
178+ "precompile=True requires an installed pybind11 package "
179+ "that provides the library sources"
180+ )
181+ raise ValueError (msg ) from err
182+ if not os .path .exists (combined ):
183+ msg = f"pybind11 library sources not found: { combined } "
184+ raise ValueError (msg )
185+ self ._precompile_source = combined
186+ self .sources .append (combined )
187+ self .define_macros .append (("PYBIND11_PRECOMPILED" , None ))
188+
146189 self .cxx_std = cxx_std
147190
148191 cflags = []
@@ -278,9 +321,30 @@ def build_extensions(self) -> None:
278321 for ext in self .extensions :
279322 if hasattr (ext , "_cxx_level" ) and ext ._cxx_level == 0 :
280323 ext .cxx_std = auto_cpp_level (self .compiler )
324+ self ._isolate_precompile_source (ext )
281325
282326 super ().build_extensions ()
283327
328+ def _isolate_precompile_source (self , ext : _Extension ) -> None :
329+ # Each precompiled extension needs its own combined source file:
330+ # setuptools maps a shared absolute source to one shared object file,
331+ # which races in parallel builds and can silently reuse an object
332+ # compiled with another extension's macros.
333+ src = getattr (ext , "_precompile_source" , None )
334+ if src is None :
335+ return
336+ dest_dir = Path (self .build_temp ) / "pybind11_precompile"
337+ dest_dir .mkdir (parents = True , exist_ok = True )
338+ dest = dest_dir / (ext .name .replace ("." , "_" ) + "_combined.cpp" )
339+ # A shim #include keeps the original's relative sibling includes valid
340+ contents = f'#include "{ Path (src ).resolve ().as_posix ()} "\n '
341+ if not dest .exists () or dest .read_text (encoding = "utf-8" ) != contents :
342+ dest .write_text (contents , encoding = "utf-8" )
343+ # Give the shim the original's mtime, so mtime-based recompile checks
344+ # follow the real source.
345+ shutil .copystat (src , dest )
346+ ext .sources [ext .sources .index (src )] = str (dest )
347+
284348
285349def intree_extensions (
286350 paths : Iterable [str ], package_dir : dict [str , str ] | None = None
0 commit comments