3030get_requires_for_build_wheel = _build_meta .get_requires_for_build_wheel
3131get_requires_for_build_editable = _build_meta .get_requires_for_build_editable
3232
33+ # Note: There is no support guarantee for environment variables like
34+ # CUDA_PYTHON_TOOLCHAIN, etc. They may be removed or changed in the future.
35+
3336# Populated by _build_cuda_bindings(); consumed by setup.py.
3437_extensions = None
3538
@@ -80,6 +83,180 @@ def _get_cuda_path() -> str:
8083 return cuda_path
8184
8285
86+ # -----------------------------------------------------------------------
87+ # Toolchain selection
88+ #
89+ # The helpers below (down to the end-of-shared-block marker) are duplicated
90+ # verbatim in cuda_core/build_hooks.py. Keep them in sync. Only the
91+ # per-package _resolve_toolchain() flag assembly that follows is package-
92+ # specific (it differs because the two packages use different C++ standards
93+ # and opt levels).
94+
95+ # --- begin shared toolchain helpers (keep in sync) ---
96+ _TOOLCHAINS_LINUX = ("gnu" , "llvm" )
97+ _TOOLCHAINS_WINDOWS = ("msvc" ,)
98+ _TOOLCHAIN_COMPILERS = {
99+ "gnu" : ("gcc" , "g++" ),
100+ "llvm" : ("clang" , "clang++" ),
101+ "msvc" : (None , None ),
102+ }
103+
104+
105+ def _resolve_toolchain_name ():
106+ """Read CUDA_PYTHON_TOOLCHAIN, validate it, return (name, allowed, cc, cxx).
107+
108+ The default toolchain (gnu on Linux, msvc on Windows) is the first entry
109+ of the platform's allowed tuple. cc/cxx are the compiler binaries for the
110+ toolchain (None for msvc, which distutils discovers via the MSVC env).
111+ """
112+ if sys .platform == "win32" :
113+ platform_key , allowed = "win32" , _TOOLCHAINS_WINDOWS
114+ else :
115+ platform_key , allowed = "linux" , _TOOLCHAINS_LINUX
116+ name = os .environ .get ("CUDA_PYTHON_TOOLCHAIN" , allowed [0 ]).strip ().lower ()
117+ if name not in allowed :
118+ raise RuntimeError (
119+ f"CUDA_PYTHON_TOOLCHAIN={ name !r} is not supported on { platform_key } . Valid values: { ', ' .join (allowed )} ."
120+ )
121+ cc , cxx = _TOOLCHAIN_COMPILERS [name ]
122+ explicit = bool (os .environ .get ("CUDA_PYTHON_TOOLCHAIN" , "" ).strip ())
123+ return name , allowed , cc , cxx , explicit
124+
125+
126+ def _apply_toolchain_env (cc , cxx , explicit ):
127+ """Set CC/CXX/LDSHARED for an explicitly-chosen toolchain.
128+
129+ The default path (CUDA_PYTHON_TOOLCHAIN unset) intentionally
130+ does not touch the env, so an externally-set compiler (e.g.
131+ CC="sccache cc" in CI) keeps working. An explicit CUDA_PYTHON_TOOLCHAIN
132+ override (incl. =gnu) governs the compiler and overrides CC/CXX/LDSHARED.
133+ """
134+ if explicit and cc is not None :
135+ os .environ ["CC" ] = cc
136+ os .environ ["CXX" ] = cxx
137+ os .environ ["LDSHARED" ] = f"{ cxx } -shared"
138+
139+
140+ def _check_toolchain_available (name ):
141+ """Preflight: verify the selected toolchain's tools are on PATH.
142+
143+ No-op for the platform default (distutils discovers those). For llvm,
144+ probes clang, clang++, and ld.lld so a missing toolchain fails fast with a
145+ helpful message instead of a cryptic compile error.
146+ """
147+ if name != "llvm" :
148+ return
149+ tools = ("clang" , "clang++" , "ld.lld" )
150+ missing = [t for t in tools if shutil .which (t ) is None ]
151+ if missing :
152+ raise RuntimeError (
153+ f"CUDA_PYTHON_TOOLCHAIN=llvm but required tool(s) not found on PATH: "
154+ f"{ ', ' .join (missing )} . Install clang and lld "
155+ f"(e.g. `apt install clang lld` or `dnf install clang lld`) "
156+ f"or set CUDA_PYTHON_TOOLCHAIN=gnu."
157+ )
158+
159+
160+ # --- end shared toolchain helpers ---
161+
162+
163+ def _resolve_toolchain (debug = False , compile_for_coverage = False ):
164+ """Resolve the C/C++ toolchain from CUDA_PYTHON_TOOLCHAIN.
165+
166+ Returns (name, cc, cxx, extra_compile_args, extra_link_args). The default
167+ toolchain (gnu on Linux, msvc on Windows) reproduces the previous build
168+ behavior and does not touch CC/CXX/LDSHARED, so an externally-set compiler
169+ (e.g. CC="sccache cc") keeps working. A non-default toolchain (llvm on
170+ Linux) selects clang/clang++ and lld and sets CC/CXX/LDSHARED so distutils'
171+ customize_compiler picks them up.
172+ """
173+ name , _allowed , cc , cxx , explicit = _resolve_toolchain_name ()
174+
175+ extra_compile_args = []
176+ extra_link_args = []
177+
178+ if name == "msvc" :
179+ if debug :
180+ raise RuntimeError ("Debuggable builds are not supported on Windows." )
181+ else :
182+ # Common Linux compile flags.
183+ extra_compile_args += ["-std=c++14" , "-Wno-deprecated-declarations" ]
184+ # Compiler-specific flags.
185+ if name == "gnu" :
186+ extra_compile_args += ["-fpermissive" , "-fno-var-tracking-assignments" ]
187+ elif name == "llvm" :
188+ extra_link_args += ["-fuse-ld=lld" ]
189+ # Common Linux debug/opt flags.
190+ if debug :
191+ extra_compile_args += ["-g" , "-O0" , "-D _GLIBCXX_ASSERTIONS" ]
192+ else :
193+ extra_compile_args += ["-g0" , "-O3" ]
194+ extra_link_args += ["-Wl,--strip-all" ]
195+
196+ if compile_for_coverage :
197+ # CYTHON_TRACE_NOGIL indicates to trace nogil functions. It is not
198+ # related to free-threading builds.
199+ extra_compile_args += ["-DCYTHON_TRACE_NOGIL=1" , "-DCYTHON_USE_SYS_MONITORING=0" ]
200+
201+ _apply_toolchain_env (cc , cxx , explicit )
202+
203+ return name , cc , cxx , extra_compile_args , extra_link_args
204+
205+
206+ # -----------------------------------------------------------------------
207+ # Toolchain stamp
208+
209+ _BUILD_DIR = Path (__file__ ).parent / "build"
210+
211+
212+ def _abi_stamp_path (stem ):
213+ """Return a stamp path scoped to this interpreter's extension ABI."""
214+ extension_suffix = sysconfig .get_config_var ("EXT_SUFFIX" )
215+ if not extension_suffix :
216+ raise RuntimeError ("Python's EXT_SUFFIX build configuration is unavailable" )
217+ return _BUILD_DIR / f"{ stem } { extension_suffix } "
218+
219+
220+ # Records the toolchain of the last completed build for this extension ABI,
221+ # so setup.py can force build_ext when it changes. Written by
222+ # record_build_toolchain().
223+ _BUILD_TOOLCHAIN_STAMP = _abi_stamp_path (".build-toolchain" )
224+
225+ force_build_ext = False
226+
227+
228+ def _check_build_toolchain (toolchain ):
229+ """Set force_build_ext when the toolchain changed since the last build.
230+
231+ Setuptools' freshness check does not include the extension flags, so a
232+ stale .so compiled by a previous toolchain would otherwise be packaged.
233+ """
234+ global force_build_ext
235+
236+ try :
237+ previous = _BUILD_TOOLCHAIN_STAMP .read_text (encoding = "utf-8" ).strip ()
238+ except FileNotFoundError :
239+ previous = None
240+
241+ # A missing stamp means the last build's toolchain is unknown, so force too.
242+ # On a first build that costs nothing: there are no artifacts to reuse.
243+ if previous != toolchain :
244+ print (f"Toolchain of last build: { previous } (building { toolchain } ); forcing a full rebuild" )
245+ force_build_ext = True
246+
247+
248+ def record_build_toolchain () -> None :
249+ """Stamp the toolchain of the build that just completed.
250+
251+ setup.py calls this after build_ext succeeds, so that a build which failed
252+ partway through does not claim outputs it never produced. Re-derives the
253+ toolchain name from the environment rather than caching it in a global.
254+ """
255+ name , * _ = _resolve_toolchain_name ()
256+ _BUILD_TOOLCHAIN_STAMP .parent .mkdir (parents = True , exist_ok = True )
257+ _BUILD_TOOLCHAIN_STAMP .write_text (name + "\n " , encoding = "utf-8" )
258+
259+
83260# -----------------------------------------------------------------------
84261# Extension preparation helpers
85262
@@ -155,6 +332,17 @@ def _build_cuda_bindings(debug=False):
155332
156333 compile_for_coverage = bool (int (os .environ .get ("CUDA_PYTHON_COVERAGE" , "0" )))
157334
335+ # Resolve the C/C++ toolchain (CUDA_PYTHON_TOOLCHAIN). The default (gnu on
336+ # Linux, msvc on Windows) reproduces the previous build behavior and does
337+ # not touch CC/CXX, so an externally-set compiler (e.g. sccache) survives.
338+ toolchain , _cc , _cxx , extra_compile_args , extra_link_args = _resolve_toolchain (
339+ debug = debug , compile_for_coverage = compile_for_coverage
340+ )
341+ _check_toolchain_available (toolchain )
342+ extra_cythonize_kwargs = {}
343+ if debug and sys .platform != "win32" :
344+ extra_cythonize_kwargs ["gdb_debug" ] = True
345+
158346 # Prepare compile/link arguments
159347 include_path_list = [os .path .join (cuda_path , "include" )]
160348 include_dirs = [
@@ -167,31 +355,6 @@ def _build_cuda_bindings(debug=False):
167355 cudalib_subdirs = ["lib64" , "lib" ]
168356 library_dirs .extend (os .path .join (cuda_path , subdir ) for subdir in cudalib_subdirs )
169357
170- extra_compile_args = []
171- extra_link_args = []
172- extra_cythonize_kwargs = {}
173- if sys .platform == "win32" :
174- if debug :
175- raise RuntimeError ("Debuggable builds are not supported on Windows." )
176- else :
177- extra_compile_args += [
178- "-std=c++14" ,
179- "-fpermissive" ,
180- "-Wno-deprecated-declarations" ,
181- "-fno-var-tracking-assignments" ,
182- ]
183- if debug :
184- extra_cythonize_kwargs ["gdb_debug" ] = True
185- extra_compile_args += ["-g" , "-O0" ]
186- extra_compile_args += ["-D _GLIBCXX_ASSERTIONS" ]
187- else :
188- extra_compile_args += ["-g0" , "-O3" ]
189- extra_link_args += ["-Wl,--strip-all" ]
190- if compile_for_coverage :
191- # CYTHON_TRACE_NOGIL indicates to trace nogil functions. It is not
192- # related to free-threading builds.
193- extra_compile_args += ["-DCYTHON_TRACE_NOGIL=1" , "-DCYTHON_USE_SYS_MONITORING=0" ]
194-
195358 # Rename architecture-specific files
196359 dst_files = _rename_architecture_specific_files ()
197360
@@ -236,6 +399,10 @@ def get_static_libraries(f):
236399 if compile_for_coverage :
237400 cython_directives ["linetrace" ] = True
238401
402+ # Force a full rebuild when the toolchain changed since the last successful
403+ # build, so a stale .so from a previous toolchain is never packaged.
404+ _check_build_toolchain (toolchain )
405+
239406 _extensions = cythonize (
240407 extensions ,
241408 nthreads = nthreads ,
0 commit comments