Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/library/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 40 additions & 35 deletions pytensor/configdefaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
85 changes: 85 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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() == ""