diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 626d50355ab..09ad41bacfb 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -26,7 +26,31 @@ build_sdist = _build_meta.build_sdist get_requires_for_build_sdist = _build_meta.get_requires_for_build_sdist -COMPILE_FOR_COVERAGE = bool(int(os.environ.get("CUDA_PYTHON_COVERAGE", "0"))) + +def env_int(name: str, default: int) -> int: + """Read an integer build knob from the environment. + + Unset and empty mean the same thing. ``CUDA_PYTHON_COVERAGE= pip install .`` + (and the equivalent empty value in a Dockerfile ``ENV`` or a CI job spec) is + how a variable gets neutralised, and it must not abort the build -- a bare + ``int()`` here raises while the PEP 517 backend module is still being + imported, so the failure arrives before any build output. + + A value that is set to something non-integer is a typo worth stopping for: + silently ignoring ``CUDA_PYTHON_COVERAGE=yes`` would hand back a build with + no coverage instrumentation. It is reported with the variable's name rather + than as an anonymous ``invalid literal for int()``. + """ + raw = os.environ.get(name, "").strip() + if not raw: + return default + try: + return int(raw) + except ValueError: + raise ValueError(f"environment variable {name}={os.environ[name]!r} must be an integer") from None + + +COMPILE_FOR_COVERAGE = bool(env_int("CUDA_PYTHON_COVERAGE", 0)) # Please keep in sync with the copy in cuda_bindings/build_hooks.py. @@ -219,7 +243,9 @@ def get_sources(mod_name): for mod in module_names() ) - nthreads = int(os.environ.get("CUDA_PYTHON_PARALLEL_LEVEL", os.cpu_count() // 2)) + # ``os.cpu_count()`` is documented to return None when it cannot be + # determined; fall back to a serial build rather than a TypeError. + nthreads = env_int("CUDA_PYTHON_PARALLEL_LEVEL", (os.cpu_count() or 1) // 2) compile_time_env = {"CUDA_CORE_BUILD_MAJOR": int(_determine_cuda_major_version())} compiler_directives = {"embedsignature": True, "warn.deprecated.IF": False, "freethreading_compatible": True} _CythonOptions.warning_errors = True diff --git a/cuda_core/setup.py b/cuda_core/setup.py index e0d745b1f21..8f9a76d3a42 100644 --- a/cuda_core/setup.py +++ b/cuda_core/setup.py @@ -10,8 +10,10 @@ from setuptools.command.build_ext import build_ext as _build_ext from setuptools.command.build_py import build_py as _build_py -nthreads = int(os.environ.get("CUDA_PYTHON_PARALLEL_LEVEL", os.cpu_count() // 2)) -coverage_mode = bool(int(os.environ.get("CUDA_PYTHON_COVERAGE", "0"))) +# Shared with build_hooks so the two build entry points parse these knobs +# identically (see build_hooks.env_int for why a bare int() is not enough). +nthreads = build_hooks.env_int("CUDA_PYTHON_PARALLEL_LEVEL", (os.cpu_count() or 1) // 2) +coverage_mode = bool(build_hooks.env_int("CUDA_PYTHON_COVERAGE", 0)) _ROOT_DIR = Path(__file__).resolve().parent _AOTI_SHIM_DEF_FILE = _ROOT_DIR / "cuda" / "core" / "_include" / "aoti_shim.def" _AOTI_SHIM_LIB_FILE = _ROOT_DIR / "build" / "aoti_shim.lib" diff --git a/cuda_core/tests/test_build_hooks.py b/cuda_core/tests/test_build_hooks.py index c08ad4cd3c5..6bbca879afb 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -165,3 +165,49 @@ def test_missing_cuda_path_raises_error(self): pytest.raises(RuntimeError, match="CUDA_PATH or CUDA_HOME"), ): build_hooks._determine_cuda_major_version() + + +class TestEnvInt: + """Integer build knobs read from the environment.""" + + @pytest.mark.agent_authored(model="claude-opus-5") + @pytest.mark.parametrize( + ("value", "expected"), + [ + pytest.param(None, 7, id="unset"), + # `VAR= pip install .` is how a shell neutralizes a variable, and + # the same shape appears in Dockerfile ENV and CI job specs. + pytest.param("", 7, id="empty"), + pytest.param(" ", 7, id="whitespace-only"), + pytest.param("0", 0, id="zero"), + pytest.param("4", 4, id="positive"), + pytest.param(" 4 ", 4, id="padded"), + ], + ) + def test_env_int_reads_the_value_or_falls_back(self, value, expected): + env = {} if value is None else {"CUDA_PYTHON_TEST_KNOB": value} + with mock.patch.dict(os.environ, env, clear=True): + assert build_hooks.env_int("CUDA_PYTHON_TEST_KNOB", 7) == expected + + @pytest.mark.agent_authored(model="claude-opus-5") + def test_env_int_names_the_variable_for_a_non_integer(self): + with ( + mock.patch.dict(os.environ, {"CUDA_PYTHON_TEST_KNOB": "yes"}, clear=True), + pytest.raises(ValueError, match=r"CUDA_PYTHON_TEST_KNOB='yes' must be an integer"), + ): + build_hooks.env_int("CUDA_PYTHON_TEST_KNOB", 7) + + @pytest.mark.agent_authored(model="claude-opus-5") + @pytest.mark.parametrize("value", ["", " "], ids=["empty", "whitespace-only"]) + def test_backend_imports_with_an_empty_coverage_flag(self, value): + """An empty CUDA_PYTHON_COVERAGE must not break the PEP 517 backend. + + COMPILE_FOR_COVERAGE is evaluated at module scope, so a bare int() there + raised while the build backend was still being imported -- pip reported + `ValueError: invalid literal for int() with base 10: ''` before any + build output appeared. + """ + with mock.patch.dict(os.environ, {"CUDA_PYTHON_COVERAGE": value}, clear=True): + reloaded = _load_build_hooks() + + assert reloaded.COMPILE_FOR_COVERAGE is False