Skip to content

build: consolidate toolchain helpers via symlink - #2965

Closed
leofang wants to merge 17 commits into
NVIDIA:mainfrom
leofang:consolidate-build-hooks-symlink
Closed

leofang wants to merge 17 commits into
NVIDIA:mainfrom
leofang:consolidate-build-hooks-symlink

Conversation

@leofang

@leofang leofang commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Summary

Follow-up to #2903 addressing the second condition of its approval (review comment):

A follow-up PR will be sent (by me or Ralf J) to use symlink or other approaches to ensure we have a single source of truth for the build hook (and its tests).

Also closes the audit in #1882 — the two backends' flag sets had drifted for legacy reasons; this PR unifies them, with a per-project injection point for the small handful of genuinely-different bits.

The two build_hooks.py files now share a single canonical file (cuda_bindings/_build_shared.py) via a symlink at cuda_core/_build_shared.py, and each backend's own build_hooks.py is a thin shell that declares only what is genuinely per-package. All the mechanics — CUDA path resolution, toolchain resolution, flag set, stamp read/compare/write, Cython cache — live in one place.

What's in _build_shared.py

Path(__file__).parent inside the shared file resolves per-package because Python does not dereference symlinks in __file__, so _BUILD_DIR and the alias location land under each package's own directory when loaded from its symlink (verified empirically before committing).

What each build_hooks.py still owns

Genuinely per-package:

  • cuda_bindings/build_hooks.py: _BUILD_TOOLCHAIN_STAMP = _abi_stamp_path(".build-toolchain"), _current_toolchain_key() (re-derives from env for setup.py's post-build stamping), _tweak_flags (adds -Wno-deprecated-declarations on Linux, see below), _rename_architecture_specific_files, _prep_extensions, _build_cuda_bindings.
  • cuda_core/build_hooks.py: _BUILD_CONFIG_STAMP = _abi_stamp_path(".build-config"), _build_config_key(cuda_major, toolchain, debug, coverage), WARNINGS_AS_ERRORS (reads CUDA_PYTHON_WERROR; from cuda.core: eliminate build warnings; set option to build with warnings as errors #2966), _determine_cuda_major_version, _relativize_extension_sources, _extension_sources, _extension_depends, _build_cuda_core, _add_cython_include_paths_to_pth (from Cython .pth file support for pixi path dependencies #1562), plus get_requires_for_build_* that pins the cuda-bindings runtime dep.

Both files have a module-level __getattr__ that re-exports _build_shared.force_build_ext transparently, so setup.py's existing build_hooks.force_build_ext attribute read keeps working unchanged.

Per-project knobs on resolve_toolchain

Three knobs let each backend pick its own defaults without forking the shared code:

  • cxx_std (required kwarg) — each backend picks its own C++ standard. No shared default; the two backends have picked deliberately different values (see below), and forcing them onto a common default would either regress bindings or block core.
  • warnings_as_errors=False — opts into -Werror (Linux) or /WX with C4551/C4244 exemptions for Cython's utility code (MSVC). Each backend gates this on its own env var so the two backends' source cleanups stay on independent timelines.
  • tweak (optional callable) — post-hook that gets (name, extra_compile_args, extra_link_args) and returns the layered pair. Used for the small handful of flags a single backend needs but that don't belong in the shared set.

Current usage:

  • cuda-bindings: resolve_toolchain(cxx_std=14, ..., tweak=_tweak_flags). cxx_std=14 because raising to c++17 costs a measured ~15% on launch.launch_{256,512}_args from gcc's c++17 variadic-template expansion (the aggregate ~3% geomean is inside the pyperf --fast noise floor, but the outlier is real; measured on origin/main vs. this PR under identical CTK 13.4 / conda-forge toolchain). _tweak_flags adds -Wno-deprecated-declarations on Linux: cudaMemcpy*Array* and cudaGetDriverEntryPoint are deprecated but still supported, and the ~38 resulting warnings would otherwise break a future -Werror build (the flag is order-independent since it disables the warning class entirely, so it composes cleanly with warnings_as_errors=True when bindings eventually opts in). Bindings does not currently pass warnings_as_errors=True.
  • cuda-core: resolve_toolchain(cxx_std=17, ..., warnings_as_errors=WARNINGS_AS_ERRORS). c++17 is required by helper code under cuda/core/_cpp/ (structured bindings, if constexpr, etc.). WARNINGS_AS_ERRORS reads CUDA_PYTHON_WERROR at module load; CI's wheel builds set it (see cuda.core: eliminate build warnings; set option to build with warnings as errors #2966). No tweak.

Shared flag set

Per #1882, bindings and core carried different Linux and MSVC flags for legacy reasons. Both now go through _build_shared._build_flags:

  • Linux compile: -std=c++{cxx_std}, -g0 -O2 (opt) or -g -O0 -D _GLIBCXX_ASSERTIONS (debug)
  • Linux link: -fuse-ld=lld (llvm), -Wl,--strip-all (opt)
  • MSVC compile: /std:c++{cxx_std}, /O2 (modern setuptools' MSVCCompiler no longer forces /Ox, so we set the opt level explicitly for symmetry with Linux -O2)
  • Warnings-as-errors (when warnings_as_errors=True): -Werror on Linux; /WX /wd4551 /wd4244 on MSVC. The exemptions cover the two warning classes Cython's utility code emits in every module (C4551 "function call missing argument list" and C4244 narrowing from @cython.overflowcheck(True) helpers).
  • Coverage: -DCYTHON_TRACE_NOGIL=1 -DCYTHON_USE_SYS_MONITORING=0

Bindings loses -fpermissive, -fno-var-tracking-assignments, -O3 (and keeps -Wno-deprecated-declarations via its tweak). CI will surface any compilation regression if -fpermissive was actually load-bearing for the Cython-generated C++.

Windows contributors

The symlink now matters at clone time on Windows outside of WSL: without the right git config, the "symlink" lands as a text stub. Handled in two places:

  • CI: test-wheel-windows.yml, test-sdist-windows.yml, and build-wheel.yml gain a git config --global core.symlinks true step before checkout on Windows runners.
  • CONTRIBUTING.md: new top-level Development on Windows section documenting (1) Activate Developer Mode (matching CuPy's contribution guide), and (2) git config --global core.symlinks true. Includes an existing-clone recovery recipe, with a note that re-cloning is generally cleaner than trying to fix symlinks in place. The prior Pre-commit on Windows / Pre-commit lychee workaround subsections are folded in so all Windows-only setup lives in one place. cuda_bindings/docs/source/install.rst and cuda_core/docs/source/install.rst cross-link the new section from their Installing from Source notes.

Not touched: contribute.rst in either package (per @leofang, they're being refactored separately), and cuda_pathfinder/ (pure Python, no build_hooks.py).

What else changes

  • toolshed/check_build_hooks_sync.py and its .pre-commit-config.yaml entry — deleted. A single source of truth needs no drift check.
  • ruff.toml — one line: adds _build_shared to known-first-party so isort groups the import with cuda.* after third-party.
  • MANIFEST.in in both packages — include _build_shared.py so sdists carry the resolved content (setuptools' make_release_tree copies through symlinks).
  • lychee.toml (new, at repo root) — the four link-checker excludes previously inlined in .github/workflows/build-docs.yml move to a proper config file; the workflow now just points at --config ./lychee.toml. Includes the self-referencing #development-on-windows anchor exclusion needed because the section only lands with this PR.
  • cuda_python_test_helpers/build_shared.py (new) — shared test mixins for resolve_toolchain, _check_toolchain_available, and _abi_stamp_path. Both packages' test_build_hooks.py mix them in via subclass, so each shared assertion runs once in each package's env without duplicating the test source. Flag-content assertions (-std=c++14 vs c++17, presence of -Wno-deprecated-declarations) stay in the per-package test files since they encode the per-project choice.
  • Merge with main: absorbs #2966 (CUDA_PYTHON_WERROR=1 toggle for CI wheel builds). The auto-merge conflict lived in cuda_core/build_hooks.py — HEAD had deleted the whole in-file toolchain block, main had added a -Werror branch inside it. Resolved by hoisting the toggle into the shared flag set (see Per-project knobs above) so bindings can adopt it later with a one-line flip.
  • Test pruning — dropped tests whose behavior any successful CI wheel build already exercises (default no-touch, sccache preservation, default preflight noop, missing-stamp force, test_wheel_records_exact_prepared_config_after_success, etc.). Kept error paths, cross-configuration transitions, cross-Python-ABI scoping, failure paths, parametrized data variants, and llvm/edge cases wheel builds don't cover. Bindings 34 → 31, core 71 → 63.

Test plan

  • pytest cuda_bindings/tests/test_build_hooks.py --noconftest — 31 pass / 2 skip locally.
  • pytest cuda_core/tests/test_build_hooks.py --noconftest — 63 pass / 2 skip locally.
  • pytest-randomly run of both suites — no order-dependent failures (this PR fixes one that had been latent: monkey-patching build_hooks.force_build_ext created a real attribute that shadowed the __getattr__ fallback and poisoned later stamp tests; fixed by patching sys.modules["_build_shared"] instead).
  • ruff check + ruff format --check clean on all touched Python files.
  • toolshed/check_spdx.py clean.
  • python -m build --sdist for both packages: the extracted tarball contains _build_shared.py byte-identical to the canonical, and a from-scratch PEP 517 backend load (fresh sys.path) resolves every shared helper's __module__ to _build_shared.
  • cuda-bindings latency-benchmark suite on CTK 13.4 (baseline vs. this PR vs. this PR + -O3): confirmed -O3 → -O2 is not the source of the ~15% launch_{256,512}_args outlier; -std=c++14 → c++17 is. That's why bindings stays on cxx_std=14.
  • Full CI (waiting on /ok to test) — the flag change (drop -fpermissive etc.) makes CI the authoritative check.

Marked draft until CI confirms; ready for review otherwise.

-- Leo's bot

Introduces cuda_bindings/_toolchain_shared.py as the single source of truth
for the CUDA_PYTHON_TOOLCHAIN resolution helpers, with cuda_core mirroring
it via a symlink. Both build_hooks.py files now import the shared helpers
instead of carrying a byte-for-byte duplicated block, following up on the
approval condition in review comment #pullrequestreview-5355844534.

- New cuda_bindings/_toolchain_shared.py (canonical: constants,
  _resolve_toolchain_name, _apply_toolchain_env, _check_toolchain_available).
- cuda_core/_toolchain_shared.py is a symlink to ../cuda_bindings/. Both
  packages' pyproject.toml already sets backend-path = ["."], so
  `from _toolchain_shared import ...` at the top of each build_hooks.py
  resolves via each backend's own directory.
- Both MANIFEST.in include _toolchain_shared.py so sdists carry the
  resolved content: setuptools' make_release_tree copies through
  symlinks, so an extracted sdist gets a real 3099-byte file rather
  than a dangling link.
- Tests pre-load _toolchain_shared into sys.modules before executing
  build_hooks via importlib, so the shared import resolves without
  adding the package directory to sys.path (which would shadow the
  installed cuda.bindings / cuda.core for the rest of the test session).
- toolshed/check_build_hooks_sync.py and its pre-commit entry are gone;
  a single source of truth needs no drift check.

Windows contributors outside of WSL now need Git to be configured for
symlinks before cloning. A new "Development on Windows" section in
CONTRIBUTING.md covers Developer Mode + core.symlinks=true (and folds
in the existing lychee workaround). cuda_bindings and cuda_core install
docs cross-link to it from "Installing from Source".
@copy-pr-bot

copy-pr-bot Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added cuda.bindings Everything related to the cuda.bindings module cuda.core Everything related to the cuda.core module labels Sep 29, 2026
Match the CuPy contribution guide's phrasing and target URL so Windows
contributors land on the same official Microsoft page.
…_path into shared file

Approach A follow-up to the toolchain-block extraction: pull in the
remaining verbatim duplicates between the two build_hooks.py files, and
rename the shared file from _toolchain_shared.py to _build_shared.py to
reflect its broader scope. cuda_core/_build_shared.py remains a symlink
to the canonical cuda_bindings/_build_shared.py.

Moved into _build_shared.py:
- _import_get_cuda_path_or_home (PEP 517 pathfinder-shadowing workaround,
  the second helper the earlier PR flagged as prior art).
- _get_cuda_path (@functools.cache wrapper around the above).
- _BUILD_DIR and _abi_stamp_path (extension-ABI-scoped stamp helper).
  _BUILD_DIR relies on Python not resolving symlinks in __file__: when
  cuda_core imports the shared module through its symlink, Path(__file__)
  points at cuda_core/, so _BUILD_DIR is cuda_core/build/; imported from
  cuda_bindings, it's cuda_bindings/build/. Each package's stamps still
  land under its own build/ directory.

Both build_hooks.py files now import these names from _build_shared and
drop their local copies plus the now-unused import shutil (core),
import functools + from pathlib import Path (bindings), and
import sysconfig (core).

Tests patch shutil / sysconfig at their module level instead of via
build_hooks.shutil / build_hooks.sysconfig, since neither is needed on
build_hooks after the extraction. Both suites still pass (15 bindings,
49 core).

ruff.toml gets a T201 exemption for **/_build_shared.py so the print
diagnostics inherited from the previous build_hooks.py copies keep
working (build_hooks.py already has this exemption).
Resolves conflicts with NVIDIA#2933 (Cython generated-source cache). Ralf's
_cython_cache_path and _stable_cython_alias helpers were vendored in
both build_hooks.py files behind the # --- begin/end shared build helpers
markers his PR renamed. Moved both into cuda_bindings/_build_shared.py
alongside the toolchain block; both build_hooks.py now import them
via `from _build_shared import ...` (no vendoring, no sync check).

Conflict resolution:
- cuda_bindings/build_hooks.py, cuda_core/build_hooks.py: keep the
  from-_build_shared import structure; extend the import list with
  _cython_cache_path and _stable_cython_alias.
- cuda_bindings/_build_shared.py: add the two helpers plus the imports
  they need (contextlib, hashlib, uuid, warn).
- cuda_bindings/build_hooks.py: drop hashlib (moved), keep contextlib
  (still used for contextlib.suppress), re-add Path from pathlib for
  the Cython.__file__ path handling in _build_cuda_bindings.
- cuda_core/build_hooks.py: drop contextlib, hashlib, uuid, warn (all
  moved).
- .pre-commit-config.yaml: keep my removal of the check-build-hooks-sync
  entry (script is deleted).
- toolshed/check_build_hooks_sync.py: keep my deletion (single source
  of truth needs no drift check).
- Auto-merged test files needed no manual touch; both suites now include
  Ralf's cython_cache tests and pass (33 bindings, 71 core).

Verified after resolution:
- ruff check + format: clean.
- SPDX: clean.
- Sdists for both packages contain the resolved 8579-byte
  _build_shared.py matching the canonical.
Two follow-ups to the merge commit, both aimed at removing remaining
duplication between the two backends.

*1. Key-based stamp helpers in _build_shared.py*

Adds `check_build_key(stamp, get_key)` and `record_build_key(stamp,
get_key)` to the shared module. Each backend now supplies a package-
specific `get_key` callable and keeps a thin wrapper:

- cuda_bindings: key is the toolchain name (re-derived from env for
  `record_build_toolchain` via `_current_toolchain_key`); check gets
  `lambda: toolchain` since the caller already has it.
- cuda_core: key is the composite `cu{major}-{toolchain}-{opt|debug}
  [-cov]` string; both check and record wrap the value in `lambda: key`
  so the shared helper's signature stays uniform.

`force_build_ext` stays a module-level bool in each `build_hooks.py`:
the shared helper just returns True/False, and each wrapper flips its
own package's `force_build_ext`. That preserves `build_hooks.force_build_ext`
attribute access from setup.py without any `_build_shared` re-export
tricks.

*2. Shared-helper test mixins*

New file `cuda_python_test_helpers/build_shared.py` mirrors Ralf's
existing `cython_cache.py` mixin pattern (class attr `build_hooks = None`,
subclass sets it). Three mixins:

- `ResolveToolchainSharedMixin` — 5 tests: `test_default_does_not_touch_env`,
  `test_default_preserves_existing_cc`, `test_case_insensitive`,
  `test_invalid_value_raises`, `test_llvm_overrides_external_cc` (the
  latter was previously in cuda_core only; both packages exercise it now).
- `CheckToolchainAvailableSharedMixin` — 3 tests: default_is_noop,
  llvm_missing_tool_lists_install_hint, llvm_present_passes.
- `AbiStampPathMixin` — 1 test: stamp path scoped to `EXT_SUFFIX`, run
  against a fixed `.build-test` stem so it doesn't care whether the
  package stems its stamps as `.build-toolchain` or `.build-config`.

Left in each package's `test_build_hooks.py`:

- `test_llvm_sets_env_and_flags` / `test_gnu_sets_env_and_flags` /
  `test_gnu_keeps_gcc_only_flags` — flag-set assertions genuinely differ
  between bindings and core.
- Stamp-bookkeeping tests (`TestBuildToolchainStamp`, `TestBuildConfigStamp`)
  — bindings stamps just the toolchain name, core stamps the composite key.

Net: -~250 duplicated test lines, one added shared file. Bindings gains
one test (llvm_overrides_external_cc), so pass counts are 34 (bindings)
and 71 (core), up from 33 and 71.

All verifications green: ruff check + format clean, SPDX clean, both
sdists still resolve `_build_shared.py` to real content matching the
canonical.
…d_shared

Following on from the key-gen refactor: remove the thin
`_check_build_toolchain` / `record_build_toolchain` /
`_check_build_config` / `record_build_config` skeletons entirely so
call sites use the shared `check_build_key(stamp, get_key)` /
`record_build_key(stamp, get_key)` directly.

To make that safe:

- `force_build_ext` moves out of the two `build_hooks.py` files into
  `_build_shared.py`. `check_build_key` mutates it directly on a
  detected change.
- Each `build_hooks.py` grows a module-level `__getattr__` that
  re-exports `force_build_ext` from the shared module, so setup.py's
  existing `build_hooks.force_build_ext` attribute read keeps working
  unchanged.
- `cuda_bindings/setup.py`'s `build_ext.build_extensions` replaces
  `build_hooks.record_build_toolchain()` with
  `build_hooks.record_build_key(build_hooks._BUILD_TOOLCHAIN_STAMP,
   build_hooks._current_toolchain_key)`.
- `cuda_core/build_hooks.py`'s `_build_cuda_core` inlines what
  `_check_build_config` used to do: compute cuda_major and the config
  key, then call `check_build_key(_BUILD_CONFIG_STAMP, lambda: key)`.
  `build_wheel` / `build_editable` call `record_build_key(...)`
  directly instead of `record_build_config(key)`.
- Tests: the `stamp` fixtures now reset the shared force flag via
  `sys.modules["_build_shared"]`; `TestBuildToolchainStamp` and
  `TestBuildConfigStamp` call `check_build_key` / `record_build_key`
  directly; `TestBuildHookStamping` monkey-patches
  `build_hooks.record_build_key` (2-arg signature) instead of the
  removed named wrappers.

Pass counts unchanged (34 bindings, 71 core); ruff and SPDX clean;
sdists still resolve `_build_shared.py` to real content.
Per offline discussion: if a happy-path CI wheel build passes, the
build-system code it exercised is by definition working, so tests
asserting that same happy path are redundant. Keep only tests that
assert behavior a green wheel build would not surface — error paths,
cross-configuration transitions, cross-Python-ABI scoping, failure
paths, and data variants beyond the one CI uses.

Removed (behavior exercised by any green wheel build):

- Shared mixin: `test_default_does_not_touch_env`,
  `test_default_preserves_existing_cc`, `test_default_is_noop`.
- Per-package `test_gnu_sets_env_and_flags` (bindings and core).
- `test_cuda_path_is_resolved_before_importing_bindings` — namespace
  ordering; wheel build fails at import if wrong.
- `TestBuildToolchainStamp` / `TestBuildConfigStamp`
  `test_missing_stamp_forces_rebuild` — every clean-checkout CI build
  triggers the missing-stamp path.
- `TestBuildHookStamping.test_wheel_records_exact_prepared_config_after_success`
  + `test_editable_records_default_config_after_patch` — every green
  wheel/editable exercises the record path (failure-path guards below
  it are kept, since CI does not fail on purpose).
- `TestGeneratedSourceDirIsKeyed.test_dir_is_anchored_not_relative_to_cwd`,
  `TestSetuptoolsSourcePaths.test_absolute_sources_are_made_relative`,
  `TestExtensionDepends.test_headers_under_module_directories_only`,
  `TestForceReachesBuildExt.test_flag_set_forces_rebuild` — all
  implicit in every green wheel build.

Kept (not surfaced by any single successful CI wheel):

- Toolchain error paths (`test_case_insensitive`, `test_invalid_value_raises`,
  `test_llvm_missing_tool_lists_install_hint`).
- `test_llvm_present_passes` and `test_llvm_overrides_external_cc`
  (no llvm CI).
- All `test_changed_*_forces_rebuild` / `test_same_*_does_not_force`
  transitions.
- `test_stamp_path_is_scoped_to_extension_abi` (cross-Python-ABI).
- `test_failed_wheel_build_does_not_record_config`,
  `test_failed_editable_patch_does_not_record_config` (failure paths).
- Parametrized `TestGetCudaMajorVersion` data variants and error paths.
- `test_majors_use_different_dirs` (cross-CUDA-major).
- `TestExtensionSources` edge cases, `TestParallelSourceCompilation`
  hook edge cases.
- `test_gnu_keeps_gcc_only_flags` (bindings): regression guard for the
  `_is_clang` removal in NVIDIA#2903, catches a specific stale-flag scenario
  wheel builds would not.

Pass counts: bindings 34 → 29, core 71 → 59; 17 fewer test bodies
total, 213 lines removed.
Move the `from _build_shared import ...` block up next to the other
imports and register `_build_shared` as first-party in ruff.toml's
isort config so it sorts after third-party (setuptools, Cython) as
"ours". The `# noqa: E402` opt-out is no longer needed since the import
is no longer preceded by non-import statements.
Closes the audit in NVIDIA#1882. cuda_bindings' and cuda_core's
``_resolve_toolchain`` framework was identical (call
_resolve_toolchain_name, msvc-debug error, _apply_toolchain_env,
tuple return); only the ``extra_compile_args``/``extra_link_args``
choices differed, and NVIDIA#1882 traced that difference to legacy drift,
not intent. Both are now the same function in _build_shared.

Consolidations:

- New ``resolve_toolchain(debug, compile_for_coverage)`` in
  ``_build_shared.py`` handles the whole workflow (name resolution,
  msvc-debug guard, flag assembly, env application, return tuple).
- New ``_build_flags(name, debug, compile_for_coverage)`` in
  ``_build_shared.py`` carries the unified flag set. Both backends
  drop their local copies.
- Both ``build_hooks.py`` import ``resolve_toolchain`` and call it
  with just the two switches; no per-package callable is passed.

Flag choices are now the single set:

- Linux ``-std=c++17`` (bindings used ``c++14``)
- MSVC ``/std:c++17`` (bindings had no ``/std:`` at all)
- Non-debug ``-g0 -O2`` (bindings used ``-O3``)
- Debug ``-g -O0 -D _GLIBCXX_ASSERTIONS``
- llvm ``-fuse-ld=lld``
- Coverage ``-DCYTHON_TRACE_NOGIL=1 -DCYTHON_USE_SYS_MONITORING=0``
- Non-debug ``-Wl,--strip-all``

Bindings loses ``-Wno-deprecated-declarations``, ``-fpermissive``,
and ``-fno-var-tracking-assignments`` -- per NVIDIA#1882, these were
legacy leftover with no active justification. CI will surface any
compilation regressions if the Cython-generated C++ actually needs
``-fpermissive``.

Tests: the shared ``ResolveToolchainSharedMixin`` gains
``test_llvm_sets_env_and_flags`` (both backends now assert the same
flags). Per-package ``TestResolveToolchain`` classes are gone;
``test_gnu_keeps_gcc_only_flags`` (bindings) is deleted because the
flags it guarded are gone. Pass counts: 28 bindings (was 29), 59
core (unchanged).
@leofang

leofang commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

/ok to test 9828a2f

@leofang
leofang requested a review from juenglin September 29, 2026 21:14
@leofang leofang self-assigned this Sep 29, 2026
@leofang

leofang commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

@juenglin could you check if this PR works on your system?

@leofang
leofang requested a review from Andy-Jost September 29, 2026 21:16
…lf-referencing anchor

Windows GH runners don't inherit ``core.symlinks=true`` from the OS,
so ``actions/checkout`` materializes ``cuda_core/_build_shared.py`` as a
text stub containing ``../cuda_bindings/_build_shared.py``. Any
subsequent Python import of the file syntax-errors, which took out
every ``Test win-64`` job on this PR. Set ``git config --global
core.symlinks true`` before the checkout in each workflow that runs on
Windows:

- ``.github/workflows/test-wheel-windows.yml`` (Windows-only job, no if).
- ``.github/workflows/test-sdist-windows.yml`` (Windows-only, no if).
- ``.github/workflows/build-wheel.yml`` (cross-platform; gated with
  ``if: startsWith(inputs.host-platform, 'win')`` so win-64 and
  win-arm64 both pick it up, per the existing prefix-matching
  convention in that file).

Docs' lychee "Check rendered docs links" fails because
``install.rst`` cross-links
``https://github.com/NVIDIA/cuda-python/blob/main/CONTRIBUTING.md#development-on-windows``
and the anchor doesn't exist on ``main`` yet (only on this branch). The
error is self-resolving after merge, but until then, exclude the exact
URL in ``.github/workflows/build-docs.yml``'s lychee args. Safe to
remove the exclude after this PR lands.

-- Leo's bot
@github-actions github-actions Bot added the CI/CD CI/CD infrastructure label Sep 29, 2026
Replace the accretion of ``--exclude`` CLI flags in
``.github/workflows/build-docs.yml`` with a repo-root ``lychee.toml``.
The action still points at the file explicitly via ``--config`` so no
implicit discovery order matters. Each exclude gets a comment; the
``#development-on-windows`` anchor exclude added earlier in this PR
is marked as removable once main carries that section.
The four comment lines above the ``args:`` block in
``.github/workflows/build-docs.yml`` (PR-preview canonical URLs,
cuda-bindings docutils #id anchors + a TODO, Preferred Networks
crawler rejection) explained the excludes they sat above.  Now that
the excludes live in ``lychee.toml``, move the comments in beside
each rule.
@leofang

leofang commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

/ok to test 727f231

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor
Doc Preview CI
Preview removed because the pull request was closed or merged.

@leofang
leofang marked this pull request as ready for review September 29, 2026 23:57
@leofang leofang added this to the cuda.core 1.3.0 milestone Sep 29, 2026

@Andy-Jost Andy-Jost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for the two inline comments on cuda_core/tests/test_build_hooks.py: the stamp tests are order-dependent, and the force=True path through __getattr__ has no test. The comments on the shared mixin and CONTRIBUTING are small follow-ups.

One note with no single line to anchor: dropping -Wno-deprecated-declarations adds 38 warnings to the bindings build. In the linux-64 py3.12 build log: 18 each in _internal/runtime.cpp and _internal/runtime_ptds.cpp, one each in runtime.cpp and _internal/nvrtc.cpp, all cudaMemcpy*Array* and cudaGetDriverEntryPoint. Harmless today. #2966 adds -Werror in CI, and if that reaches the shared _build_flags, bindings stops building. Keep the flag for bindings, or account for it in #2966.

Comment thread cuda_core/tests/test_build_hooks.py
Comment thread cuda_core/tests/test_build_hooks.py
Comment thread cuda_python_test_helpers/cuda_python_test_helpers/build_shared.py Outdated
Comment thread CONTRIBUTING.md Outdated
resolve_toolchain gains a required keyword-only ``cxx_std`` argument and
an optional ``tweak`` post-hook. The shared ``_build_flags`` no longer
picks a default C++ standard — the two backends legitimately differ
(bindings stays on c++14 to avoid a c++17 variadic-template regression
on kernel-launch paths; core is on c++17), and there is no defensible
shared default. See NVIDIA#1882 for the audit.

- ``cuda_bindings/build_hooks.py`` adds ``_tweak_flags`` which layers
  ``-Wno-deprecated-declarations`` on Linux for the 38 warnings
  ``cudaMemcpy*Array*`` / ``cudaGetDriverEntryPoint`` produce in 13.4
  headers. Without it, once NVIDIA#2966 turns on ``-Werror``, bindings would
  stop building.
- Call site passes ``cxx_std=14, tweak=_tweak_flags``.
- ``cuda_core/build_hooks.py`` passes ``cxx_std=17`` (no tweak).

Also addresses Andy-Jost's line-anchored review comments:

- ``TestForceReachesBuildExt._finalized_build_ext`` now patches
  ``sys.modules["_build_shared"]`` instead of ``build_hooks``. Because
  ``build_hooks.force_build_ext`` is a module-level ``__getattr__``
  fallback, monkey-patching it on ``build_hooks`` creates a real
  attribute that shadows the fallback and persists across teardown —
  a poison state that made ``TestBuildConfigStamp`` order-dependent
  (reproduces under ``pytest -p randomly --randomly-seed=1``).
- Restores ``test_flag_set_forces_rebuild`` — the only direct check
  that ``force_build_ext=True`` reaches ``build_ext.force`` through
  the ``__getattr__`` re-export. Without it, a later
  ``from _build_shared import force_build_ext`` would silently snapshot
  ``False`` and forced rebuilds would stop with no failing test.
- Restores ``test_default_preserves_existing_cc`` in
  ``ResolveToolchainSharedMixin``. A green wheel build wouldn't catch
  a regression here — if the default toolchain overwrote ``CC``,
  sccache would silently stop and ``sccache-summary`` only warns.
- ``CONTRIBUTING.md`` recovery recipe now runs ``git config
  core.symlinks true`` (no ``--global``) inside the existing clone.
  ``git clone`` probes symlink support and writes
  ``core.symlinks=false`` into the *repo-local* config on failure;
  repo-local overrides ``--global``, so the previous recipe couldn't
  fix an existing clone.

Test bookkeeping:

- Per-package ``TestResolveToolchain.test_llvm_sets_env_and_flags``
  restored in both packages — bindings asserts ``-std=c++14`` +
  ``-Wno-deprecated-declarations``, core asserts ``-std=c++17`` and the
  absence of ``-Wno-deprecated-declarations``. Removed from the shared
  mixin because it now asserts package-specific flag content.
- Other shared-mixin resolve tests just pick ``cxx_std=17`` as a
  placeholder — they test env/name/error mechanics, not flag content.

Pass counts: bindings 29 (was 28), core 61 (was 59). Randomized-order
runs (``-p randomly --randomly-seed={1,2,4,42}``) all green.
…ws guide

Per review:
- Shared ``_build_flags`` / ``resolve_toolchain`` docstrings no longer
  explain the c++14/c++17 choice — those live in the call sites in
  ``cuda_bindings/build_hooks.py`` (why c++14: the launch_{256,512}_args
  variadic-template regression) and ``cuda_core/build_hooks.py`` (why
  c++17: c++17 features under ``cuda/core/_cpp/``). Reading the shared
  file now leads you to the call site for context, instead of asking
  the shared file to know about both consumers.
- ``CONTRIBUTING.md`` ends the Windows recovery recipe with a note that
  re-cloning after the Developer Mode + ``core.symlinks=true`` setup is
  usually simpler than patching an existing broken clone.
…-floor context

Per review: the 3% geomean and the 15% launch_{256,512}_args outlier are
both real but mean different things; the comment now names both so a
future reader isn't left wondering which one drove the choice.
Modern setuptools' MSVCCompiler no longer hard-codes /Ox on release
builds, so previously the effective Windows opt level was whatever
default cl.exe / distutils inferred. Emit /O2 from the shared
_build_flags for symmetry with Linux -O2 and to keep the effective
Windows flag set legible in one place.

Adds a shared-mixin regression guard (test_msvc_sets_std_and_opt,
skipped off-Windows) so a future refactor that dropped /O2 wouldn't
pass every Windows wheel build silently.
@leofang

leofang commented Sep 30, 2026

Copy link
Copy Markdown
Member Author

Keep the flag for bindings, or account for it in #2966.

This is now kept in commit 6477e98.

@leofang

leofang commented Sep 30, 2026

Copy link
Copy Markdown
Member Author

/ok to test 4949f56

@leofang
leofang requested a review from Andy-Jost September 30, 2026 16:55
@leofang leofang changed the title build: consolidate toolchain helpers via symlink (follow-up to #2903) build: consolidate toolchain helpers via symlink Sep 30, 2026

@Andy-Jost Andy-Jost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good

The auto-merge conflict was in cuda_core/build_hooks.py: HEAD had deleted
the whole in-file toolchain block (moved to _build_shared.py), while main
had added a -Werror / /WX branch inside that same block via NVIDIA#2966.

Resolved by taking HEAD's structural side and hoisting the toggle into
the shared code:

- _build_shared._build_flags and resolve_toolchain grow a
  warnings_as_errors=False keyword; the flag emission sits next to
  -O2/debug for a single legible flag path.
- cuda_core/build_hooks.py keeps its WARNINGS_AS_ERRORS constant (reads
  CUDA_PYTHON_WERROR) and threads it through the resolve_toolchain call
  site. No per-project _tweak_flags needed for this.
- Shared-mixin tests (test_warnings_as_errors_off_by_default,
  test_warnings_as_errors_on) guard both the default-off case and the
  exact tokens emitted per platform, so a future refactor can't silently
  disable CI's -Werror.

cuda_bindings does not currently pass warnings_as_errors=True; its
existing -Wno-deprecated-declarations tweak is order-independent from
-Werror and composes cleanly when it opts in later.
@juenglin

juenglin commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Closing this in favor of three smaller PRs that together re-implement it. I volunteered to take it over, and splitting it keeps each piece easier to review:

  1. build: require symlinks on Windows; move lychee excludes to lychee.toml #2992 (merged): require symlinks on Windows and move the lychee excludes to lychee.toml. This covers the core.symlinks CI steps and the "Development on Windows" section of CONTRIBUTING.md.
  2. build: unify compiler flags #2998 (merged): unify the compiler flags across cuda-bindings and cuda-core, closing the audit in cuda-core build_hooks.py missing compiler/linker flags present in cuda-bindings #1882.
  3. build: consolidate toolchain helpers #3000 (open): consolidate the toolchain helpers into a single _build_shared.py (symlinked into cuda_core), including the Cython cache, the rebuild stamps, the CUDA path lookup and the shared test mixins.

Andy's review feedback on this PR is carried over in #3000: the stamp tests patch _build_shared instead of build_hooks, the force=True path has a test, there is an sccache CC test, the CONTRIBUTING.md recovery steps use the repo-local core.symlinks setting, and bindings keeps -Wno-deprecated-declarations.

Thanks @leofang for the original work; the commits here were the starting point for the three PRs above.

@juenglin juenglin closed this Oct 2, 2026
@leofang
leofang deleted the consolidate-build-hooks-symlink branch October 2, 2026 23:25
github-actions Bot pushed a commit that referenced this pull request Oct 3, 2026
Removed preview folders for the following PRs:
- PR #2880
- PR #2917
- PR #2947
- PR #2965
- PR #2996
- PR #2998
- PR #3003
- PR #3005
- PR #3008
- PR #3010
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI/CD CI/CD infrastructure cuda.bindings Everything related to the cuda.bindings module cuda.core Everything related to the cuda.core module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants