Skip to content

Commit 84f20e8

Browse files
committed
build(bindings): force rebuild on toolchain change via .build-toolchain stamp
cuda.bindings had no build-ext stamp at all and never forced rebuilds. Add a minimal toolchain-only stamp mirroring cuda.core's CUDA-major stamp pattern: _check_build_toolchain() sets force_build_ext when the toolchain changed since the last successful build; record_build_toolchain() writes the stamp after build_ext succeeds. setup.py reads force_build_ext in build_extensions and calls record_build_toolchain after a successful build. The cythonize build_dir is left flat (build/cython) -- the .pyx->.cpp step is toolchain-independent, so keying it would only force regenerating identical sources. Only the compiled .so is at risk, and force_build_ext fixes that. Addresses rwgk's P1 #1 (toolchain changes silently reuse binaries from the previous toolchain) for cuda.bindings: a gnu build followed by llvm in the same checkout now recompiles.
1 parent 69c23e5 commit 84f20e8

3 files changed

Lines changed: 106 additions & 0 deletions

File tree

‎cuda_bindings/build_hooks.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,54 @@ def _resolve_toolchain(debug=False, compile_for_coverage=False):
207207
return name, cc, cxx, extra_compile_args, extra_link_args
208208

209209

210+
# -----------------------------------------------------------------------
211+
# Toolchain stamp
212+
213+
_BUILD_DIR = Path(__file__).parent / "build"
214+
215+
# Records the toolchain of the last completed build, so setup.py can force
216+
# build_ext when it changes. Written by record_build_toolchain().
217+
_BUILD_TOOLCHAIN_STAMP = _BUILD_DIR / ".build-toolchain"
218+
219+
force_build_ext = False
220+
# Set by _check_build_toolchain; read by record_build_toolchain.
221+
_last_toolchain = None
222+
223+
224+
def _check_build_toolchain(toolchain):
225+
"""Set force_build_ext when the toolchain changed since the last build.
226+
227+
Setuptools' freshness check does not include the extension flags, so a
228+
stale .so compiled by a previous toolchain would otherwise be packaged.
229+
"""
230+
global force_build_ext, _last_toolchain
231+
232+
_last_toolchain = toolchain
233+
try:
234+
previous = _BUILD_TOOLCHAIN_STAMP.read_text(encoding="utf-8").strip()
235+
except FileNotFoundError:
236+
previous = None
237+
238+
# A missing stamp means the last build's toolchain is unknown, so force too.
239+
# On a first build that costs nothing: there are no artifacts to reuse.
240+
if previous != toolchain:
241+
print(f"Toolchain of last build: {previous} (building {toolchain}); forcing a full rebuild")
242+
force_build_ext = True
243+
244+
245+
def record_build_toolchain() -> None:
246+
"""Stamp the toolchain of the build that just completed.
247+
248+
setup.py calls this after build_ext succeeds, so that a build which failed
249+
partway through does not claim outputs it never produced.
250+
"""
251+
global _last_toolchain
252+
if _last_toolchain is None:
253+
return # build never reached _check_build_toolchain (e.g. metadata-only)
254+
_BUILD_TOOLCHAIN_STAMP.parent.mkdir(parents=True, exist_ok=True)
255+
_BUILD_TOOLCHAIN_STAMP.write_text(_last_toolchain + "\n", encoding="utf-8")
256+
257+
210258
# -----------------------------------------------------------------------
211259
# Extension preparation helpers
212260

@@ -349,6 +397,10 @@ def get_static_libraries(f):
349397
if compile_for_coverage:
350398
cython_directives["linetrace"] = True
351399

400+
# Force a full rebuild when the toolchain changed since the last successful
401+
# build, so a stale .so from a previous toolchain is never packaged.
402+
_check_build_toolchain(toolchain)
403+
352404
_extensions = cythonize(
353405
extensions,
354406
nthreads=nthreads,

‎cuda_bindings/setup.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ class build_ext(_build_ext):
2727
def build_extensions(self):
2828
if nthreads > 0:
2929
self.parallel = nthreads
30+
# A stale .so from a previous toolchain looks perfectly fresh;
31+
# see build_hooks._check_build_toolchain().
32+
if build_hooks.force_build_ext:
33+
self.force = True
3034
super().build_extensions()
35+
build_hooks.record_build_toolchain()
3136

3237

3338
class build_py(_build_py):

‎cuda_bindings/tests/test_build_hooks.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,52 @@ def fake_which(name):
135135
def test_llvm_present_passes(self, monkeypatch):
136136
monkeypatch.setattr(build_hooks.shutil, "which", lambda name: "/bin/" + name)
137137
build_hooks._check_toolchain_available("llvm")
138+
139+
140+
@pytest.fixture
141+
def stamp(tmp_path, monkeypatch):
142+
"""Redirect the toolchain stamp to a scratch path."""
143+
scratch = tmp_path / "build" / ".build-toolchain"
144+
monkeypatch.setattr(build_hooks, "_BUILD_TOOLCHAIN_STAMP", scratch)
145+
monkeypatch.setattr(build_hooks, "force_build_ext", False)
146+
monkeypatch.setattr(build_hooks, "_last_toolchain", None)
147+
monkeypatch.delenv("CUDA_PYTHON_TOOLCHAIN", raising=False)
148+
return scratch
149+
150+
151+
def _write_stamp(stamp, toolchain):
152+
stamp.parent.mkdir(parents=True, exist_ok=True)
153+
stamp.write_text(toolchain + "\n")
154+
155+
156+
class TestBuildToolchainStamp:
157+
"""Tests for _check_build_toolchain() and record_build_toolchain()."""
158+
159+
@pytest.mark.agent_authored(model="glm-5.2")
160+
def test_missing_stamp_forces_rebuild(self, stamp):
161+
assert build_hooks._check_build_toolchain("gnu") is None
162+
assert build_hooks.force_build_ext is True
163+
164+
@pytest.mark.agent_authored(model="glm-5.2")
165+
def test_same_toolchain_does_not_force(self, stamp):
166+
_write_stamp(stamp, "gnu")
167+
build_hooks._check_build_toolchain("gnu")
168+
assert build_hooks.force_build_ext is False
169+
170+
@pytest.mark.agent_authored(model="glm-5.2")
171+
def test_changed_toolchain_forces_rebuild(self, stamp):
172+
_write_stamp(stamp, "gnu")
173+
build_hooks._check_build_toolchain("llvm")
174+
assert build_hooks.force_build_ext is True
175+
176+
@pytest.mark.agent_authored(model="glm-5.2")
177+
def test_record_writes_stamp(self, stamp):
178+
build_hooks._check_build_toolchain("gnu")
179+
build_hooks.record_build_toolchain()
180+
assert stamp.read_text().strip() == "gnu"
181+
182+
@pytest.mark.agent_authored(model="glm-5.2")
183+
def test_record_without_check_is_noop(self, stamp, monkeypatch):
184+
monkeypatch.setattr(build_hooks, "_last_toolchain", None)
185+
build_hooks.record_build_toolchain()
186+
assert not stamp.exists()

0 commit comments

Comments
 (0)