From fe0244f3a55ddf5d7d4995aa7673d6218a47a9da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:41:03 +0000 Subject: [PATCH 1/3] Initial plan From aa372bb9c81d48ea121256af18e1cb9c26f03dc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:54:52 +0000 Subject: [PATCH 2/3] Respect CXX environment variable for compiler selection Co-authored-by: maresb <15216687+maresb@users.noreply.github.com> --- doc/library/config.rst | 4 ++ pytensor/configdefaults.py | 75 ++++++++++++++++++++------------------ tests/test_config.py | 48 ++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 35 deletions(-) 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..6cd746a510 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -281,3 +281,51 @@ 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.""" + 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 without CXX set (should use default) + 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 with CXX set to a custom compiler + 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 with 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 From ff2bde9c14711c26b014442c57f63d2de40be7a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 08:11:03 +0000 Subject: [PATCH 3/3] Add comprehensive tests for CXX priority order Tests now verify that the priority order is: 1. Explicit PyTensor config (highest) 2. CXX environment variable (middle) 3. Which-style search (fallback) Co-authored-by: maresb <15216687+maresb@users.noreply.github.com> --- tests/test_config.py | 45 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 6cd746a510..c9bb3e6df1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -284,7 +284,13 @@ def test_short_platform(self, release, platform, answer): def test_cxx_env_variable(monkeypatch, tmp_path): - """Test that the CXX environment variable is respected.""" + """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 @@ -296,7 +302,7 @@ def test_cxx_env_variable(monkeypatch, tmp_path): "print(config.cxx)" ) - # Test without CXX set (should use default) + # 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", @@ -307,7 +313,7 @@ def test_cxx_env_variable(monkeypatch, tmp_path): # Just verify we got something (could be empty if no compiler available) assert result.returncode == 0 - # Test with CXX set to a custom compiler + # Test 2: CXX set, no explicit config - should use CXX custom_compiler = "/usr/bin/custom-g++" result = subprocess.run( [sys.executable, str(test_script)], @@ -319,7 +325,7 @@ def test_cxx_env_variable(monkeypatch, tmp_path): assert result.returncode == 0 assert result.stdout.strip() == custom_compiler - # Test with CXX set to empty string (should fall back to default) + # 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", @@ -329,3 +335,34 @@ def test_cxx_env_variable(monkeypatch, tmp_path): ) 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() == ""