Skip to content
Merged
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
3 changes: 3 additions & 0 deletions relenv/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ def _toolchain_cache_root() -> Optional[pathlib.Path]:
if override.strip().lower() == "none":
return None
return pathlib.Path(override).expanduser()
# If RELENV_DATA is set, return None to use DATA_DIR/toolchain
if "RELENV_DATA" in os.environ:
return None
cache_home = os.environ.get("XDG_CACHE_HOME")
if cache_home:
base = pathlib.Path(cache_home)
Expand Down
62 changes: 62 additions & 0 deletions tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,68 @@ def test_makepath_oserror() -> None:
assert case == os.path.normcase(expected)


def test_toolchain_respects_relenv_data(
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""
Test that RELENV_DATA environment variable controls toolchain location.

This is a regression test for issue #XXX where RELENV_DATA was ignored
in version 0.20.2+ after toolchains were moved to a cache directory.
When RELENV_DATA is set (as in saltstack CI pipelines), the toolchain
should be found in $RELENV_DATA/toolchain, not in the cache directory.
"""
data_dir = tmp_path / "custom_data"
triplet = "x86_64-linux-gnu"
toolchain_path = data_dir / "toolchain" / triplet
toolchain_path.mkdir(parents=True)

# Patch sys.platform to simulate Linux
monkeypatch.setattr(sys, "platform", "linux")
monkeypatch.setattr(
relenv.common, "get_triplet", lambda machine=None, plat=None: triplet
)

# Set RELENV_DATA environment variable
monkeypatch.setenv("RELENV_DATA", str(data_dir))

# Patch DATA_DIR to reflect the new RELENV_DATA value
monkeypatch.setattr(relenv.common, "DATA_DIR", data_dir)

# Verify toolchain_root_dir returns DATA_DIR/toolchain
from relenv.common import toolchain_root_dir

result = toolchain_root_dir()
assert result == data_dir / "toolchain"


def test_toolchain_uses_cache_without_relenv_data(
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""
Test that toolchain uses cache directory when RELENV_DATA is not set.

When RELENV_DATA is not set, the toolchain should be stored in the
cache directory (~/.cache/relenv/toolchains or $XDG_CACHE_HOME/relenv/toolchains)
to allow reuse across different relenv environments.
"""
cache_dir = tmp_path / ".cache" / "relenv" / "toolchains"

# Patch sys.platform to simulate Linux
monkeypatch.setattr(sys, "platform", "linux")

# Remove RELENV_DATA if set
monkeypatch.delenv("RELENV_DATA", raising=False)

# Set XDG_CACHE_HOME to control cache location
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / ".cache"))

from relenv.common import toolchain_root_dir

result = toolchain_root_dir()
assert result == cache_dir


def test_copyright_headers() -> None:
"""Verify all Python source files have the correct copyright header."""
expected_header = (
Expand Down
166 changes: 166 additions & 0 deletions tests/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1821,3 +1821,169 @@ def __init__(self) -> None:
)
monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
assert relenv.runtime.load_openssl_provider("default") == 456


def test_sysconfig_wrapper_applied_for_python_313_plus(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Test that sysconfig wrapper is applied for Python 3.13+.

This is a regression test for Python 3.13 where sysconfig changed from
a single module to a package. The RelenvImporter no longer intercepts
the import automatically, so we must manually apply the wrapper.

Without this fix, Python 3.13+ would use the toolchain gcc with full path
even when RELENV_BUILDENV is not set, causing build failures with packages
like mysqlclient that compile native extensions.
"""
# Simulate Python 3.13+
fake_version = (3, 13, 0, "final", 0)
monkeypatch.setattr(relenv.runtime.sys, "version_info", fake_version)

# Track whether wrap_sysconfig was called
wrap_called = {"count": 0, "module_name": None}

def fake_wrap_sysconfig(name: str) -> ModuleType:
wrap_called["count"] += 1
wrap_called["module_name"] = name
return ModuleType("sysconfig")

monkeypatch.setattr(relenv.runtime, "wrap_sysconfig", fake_wrap_sysconfig)

# Mock other dependencies to avoid side effects
monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: pathlib.Path("/root"))
monkeypatch.setattr(relenv.runtime, "setup_openssl", lambda: None)
monkeypatch.setattr(relenv.runtime, "setup_crossroot", lambda: None)
monkeypatch.setattr(relenv.runtime, "install_cargo_config", lambda: None)
monkeypatch.setattr(
relenv.runtime.site, "execsitecustomize", lambda: None, raising=False
)
monkeypatch.setattr(
relenv.runtime, "wrapsitecustomize", lambda func: func, raising=False
)

# Mock importer
fake_importer = SimpleNamespace()
monkeypatch.setattr(relenv.runtime, "importer", fake_importer, raising=False)

# Clear sys.meta_path to avoid side effects
original_meta_path = sys.meta_path.copy()
monkeypatch.setattr(sys, "meta_path", [])

try:
# Execute the module initialization code at the end of runtime.py
# This simulates what happens when the runtime module is imported
exec(
"""
import sys
sys.RELENV = relenv_root()
setup_openssl()
site.execsitecustomize = wrapsitecustomize(site.execsitecustomize)
setup_crossroot()
install_cargo_config()
sys.meta_path = [importer] + sys.meta_path

# For Python 3.13+, sysconfig became a package so the importer doesn't
# intercept it. Manually wrap it here.
if sys.version_info >= (3, 13):
wrap_sysconfig("sysconfig")
""",
{
"sys": relenv.runtime.sys,
"relenv_root": relenv.runtime.relenv_root,
"setup_openssl": relenv.runtime.setup_openssl,
"site": relenv.runtime.site,
"wrapsitecustomize": relenv.runtime.wrapsitecustomize,
"setup_crossroot": relenv.runtime.setup_crossroot,
"install_cargo_config": relenv.runtime.install_cargo_config,
"importer": fake_importer,
"wrap_sysconfig": fake_wrap_sysconfig,
},
)

# Verify wrap_sysconfig was called for Python 3.13+
assert wrap_called["count"] == 1
assert wrap_called["module_name"] == "sysconfig"

finally:
# Restore original meta_path
monkeypatch.setattr(sys, "meta_path", original_meta_path)


def test_sysconfig_wrapper_not_applied_for_python_312(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Test that sysconfig wrapper is NOT applied for Python 3.12 and earlier.

For Python 3.12 and earlier, sysconfig is a single module file and the
RelenvImporter intercepts it automatically. We should not manually wrap
it to avoid double-wrapping.
"""
# Simulate Python 3.12
fake_version = (3, 12, 0, "final", 0)
monkeypatch.setattr(relenv.runtime.sys, "version_info", fake_version)

# Track whether wrap_sysconfig was called
wrap_called = {"count": 0}

def fake_wrap_sysconfig(name: str) -> ModuleType:
wrap_called["count"] += 1
return ModuleType("sysconfig")

monkeypatch.setattr(relenv.runtime, "wrap_sysconfig", fake_wrap_sysconfig)

# Mock other dependencies
monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: pathlib.Path("/root"))
monkeypatch.setattr(relenv.runtime, "setup_openssl", lambda: None)
monkeypatch.setattr(relenv.runtime, "setup_crossroot", lambda: None)
monkeypatch.setattr(relenv.runtime, "install_cargo_config", lambda: None)
monkeypatch.setattr(
relenv.runtime.site, "execsitecustomize", lambda: None, raising=False
)
monkeypatch.setattr(
relenv.runtime, "wrapsitecustomize", lambda func: func, raising=False
)

fake_importer = SimpleNamespace()
monkeypatch.setattr(relenv.runtime, "importer", fake_importer, raising=False)

original_meta_path = sys.meta_path.copy()
monkeypatch.setattr(sys, "meta_path", [])

try:
# Execute the module initialization code
exec(
"""
import sys
sys.RELENV = relenv_root()
setup_openssl()
site.execsitecustomize = wrapsitecustomize(site.execsitecustomize)
setup_crossroot()
install_cargo_config()
sys.meta_path = [importer] + sys.meta_path

# For Python 3.13+, sysconfig became a package so the importer doesn't
# intercept it. Manually wrap it here.
if sys.version_info >= (3, 13):
wrap_sysconfig("sysconfig")
""",
{
"sys": relenv.runtime.sys,
"relenv_root": relenv.runtime.relenv_root,
"setup_openssl": relenv.runtime.setup_openssl,
"site": relenv.runtime.site,
"wrapsitecustomize": relenv.runtime.wrapsitecustomize,
"setup_crossroot": relenv.runtime.setup_crossroot,
"install_cargo_config": relenv.runtime.install_cargo_config,
"importer": fake_importer,
"wrap_sysconfig": fake_wrap_sysconfig,
},
)

# Verify wrap_sysconfig was NOT called for Python 3.12
assert wrap_called["count"] == 0

finally:
monkeypatch.setattr(sys, "meta_path", original_meta_path)
Loading