diff --git a/doc/library/config.rst b/doc/library/config.rst index 2194579a1e..4204c34a8e 100644 --- a/doc/library/config.rst +++ b/doc/library/config.rst @@ -512,6 +512,10 @@ import ``pytensor`` and print the config variable, as in: disables C++ compilation when it is not. On Darwin systems (e.g. Mac OS X), it looks for ``clang++`` and uses that when available. + The ``CXX`` environment variable is respected and takes precedence over + the automatic detection. This is useful for conda-forge environments where + the compiler is configured via environment variables. + PyTensor prints a warning if it detects that no compiler is present. This value can point to any compiler binary (full path or not), but things may diff --git a/pytensor/configdefaults.py b/pytensor/configdefaults.py index 7698c5d441..b2244f1f01 100644 --- a/pytensor/configdefaults.py +++ b/pytensor/configdefaults.py @@ -311,47 +311,52 @@ def add_compile_configvars(): in_c_key=False, ) - param = "g++" + # Check if CXX environment variable is set (for conda-forge compatibility) + param = os.environ.get("CXX", "").strip() + rc = 0 if param else 1 # Assume success if CXX is set - # Test whether or not g++ is present: disable C code if it is not. - try: - rc = call_subprocess_Popen(["g++", "-v"]) - except OSError: - rc = 1 + if not param: + param = "g++" - # Anaconda on Windows has mingw-w64 packages including GCC, but it may not be on PATH. - if rc != 0: - if sys.platform == "win32": - mingw_w64_gcc = Path(sys.executable).parent / "Library/mingw-w64/bin/g++" + # Test whether or not g++ is present: disable C code if it is not. + try: + rc = call_subprocess_Popen(["g++", "-v"]) + except OSError: + rc = 1 + + # Anaconda on Windows has mingw-w64 packages including GCC, but it may not be on PATH. + if rc != 0: + if sys.platform == "win32": + mingw_w64_gcc = Path(sys.executable).parent / "Library/mingw-w64/bin/g++" + try: + rc = call_subprocess_Popen([str(mingw_w64_gcc), "-v"]) + if rc == 0: + maybe_add_to_os_environ_pathlist("PATH", mingw_w64_gcc.parent) + except OSError: + rc = 1 + if rc != 0: + _logger.warning( + "g++ not available, if using conda: `conda install gxx`" + ) + + if rc != 0: + param = "" + + # On Mac/FreeBSD we test for 'clang++' and use it by default + if sys.platform == "darwin" or sys.platform.startswith("freebsd"): try: - rc = call_subprocess_Popen([str(mingw_w64_gcc), "-v"]) + rc = call_subprocess_Popen(["clang++", "-v"]) if rc == 0: - maybe_add_to_os_environ_pathlist("PATH", mingw_w64_gcc.parent) + param = "clang++" except OSError: - rc = 1 - if rc != 0: - _logger.warning( - "g++ not available, if using conda: `conda install gxx`" - ) - - if rc != 0: - param = "" + pass - # On Mac/FreeBSD we test for 'clang++' and use it by default - if sys.platform == "darwin" or sys.platform.startswith("freebsd"): - try: - rc = call_subprocess_Popen(["clang++", "-v"]) - if rc == 0: - param = "clang++" - except OSError: - pass - - # Try to find the full compiler path from the name - if param != "": - newp = which(param) - if newp is not None: - param = newp - del newp + # Try to find the full compiler path from the name + if param != "": + newp = which(param) + if newp is not None: + param = newp + del newp # to support path that includes spaces, we need to wrap it with double quotes on Windows if param and os.name == "nt": diff --git a/tests/test_config.py b/tests/test_config.py index 2dd3c32180..c9bb3e6df1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -281,3 +281,88 @@ class TestConfigHelperFunctions: def test_short_platform(self, release, platform, answer): o = short_platform(release, platform) assert o == answer, (o, answer) + + +def test_cxx_env_variable(monkeypatch, tmp_path): + """Test that the CXX environment variable is respected with correct priority. + + Priority order (highest to lowest): + 1. Explicitly set PyTensor configuration (via PYTENSOR_FLAGS or config file) + 2. CXX environment variable + 3. Which-style search (g++, clang++, etc.) + """ + import subprocess + import sys + + # Test script that imports pytensor and prints config.cxx + test_script = tmp_path / "test_cxx.py" + test_script.write_text( + "import sys; sys.path.insert(0, '.'); " + "from pytensor import config; " + "print(config.cxx)" + ) + + # Test 1: No CXX, no explicit config - should use which-style search + result = subprocess.run( + [sys.executable, str(test_script)], + cwd="/home/runner/work/pytensor/pytensor", + capture_output=True, + text=True, + ) + default_cxx = result.stdout.strip() + # Just verify we got something (could be empty if no compiler available) + assert result.returncode == 0 + + # Test 2: CXX set, no explicit config - should use CXX + custom_compiler = "/usr/bin/custom-g++" + result = subprocess.run( + [sys.executable, str(test_script)], + cwd="/home/runner/work/pytensor/pytensor", + capture_output=True, + text=True, + env={**subprocess.os.environ, "CXX": custom_compiler}, + ) + assert result.returncode == 0 + assert result.stdout.strip() == custom_compiler + + # Test 3: CXX set to empty string - should fall back to default + result = subprocess.run( + [sys.executable, str(test_script)], + cwd="/home/runner/work/pytensor/pytensor", + capture_output=True, + text=True, + env={**subprocess.os.environ, "CXX": ""}, + ) + assert result.returncode == 0 + assert result.stdout.strip() == default_cxx + + # Test 4: Both CXX and explicit config - explicit config should win + explicit_compiler = "/usr/bin/explicit-g++" + result = subprocess.run( + [sys.executable, str(test_script)], + cwd="/home/runner/work/pytensor/pytensor", + capture_output=True, + text=True, + env={ + **subprocess.os.environ, + "CXX": custom_compiler, + "PYTENSOR_FLAGS": f"cxx={explicit_compiler}", + }, + ) + assert result.returncode == 0 + assert result.stdout.strip() == explicit_compiler + + # Test 5: CXX set and explicit empty config - explicit empty should win + result = subprocess.run( + [sys.executable, str(test_script)], + cwd="/home/runner/work/pytensor/pytensor", + capture_output=True, + text=True, + env={ + **subprocess.os.environ, + "CXX": custom_compiler, + "PYTENSOR_FLAGS": "cxx=", + }, + ) + assert result.returncode == 0 + assert result.stdout.strip() == ""