Skip to content

Commit 19c7050

Browse files
committed
Fix RELENV_DATA environment variable regression
Since version 0.20.2, toolchains ignored RELENV_DATA environment variable and always used ~/.cache/relenv/toolchains, breaking saltstack CI pipelines.
1 parent 209ad7e commit 19c7050

File tree

3 files changed

+236
-0
lines changed

3 files changed

+236
-0
lines changed

relenv/common.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ def _toolchain_cache_root() -> Optional[pathlib.Path]:
6868
if override.strip().lower() == "none":
6969
return None
7070
return pathlib.Path(override).expanduser()
71+
# If RELENV_DATA is set, return None to use DATA_DIR/toolchain
72+
if "RELENV_DATA" in os.environ:
73+
return None
7174
cache_home = os.environ.get("XDG_CACHE_HOME")
7275
if cache_home:
7376
base = pathlib.Path(cache_home)

tests/test_common.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,73 @@ def test_makepath_oserror() -> None:
464464
assert case == os.path.normcase(expected)
465465

466466

467+
def test_toolchain_respects_relenv_data(
468+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
469+
) -> None:
470+
"""
471+
Test that RELENV_DATA environment variable controls toolchain location.
472+
473+
This is a regression test for issue #XXX where RELENV_DATA was ignored
474+
in version 0.20.2+ after toolchains were moved to a cache directory.
475+
When RELENV_DATA is set (as in saltstack CI pipelines), the toolchain
476+
should be found in $RELENV_DATA/toolchain, not in the cache directory.
477+
"""
478+
data_dir = tmp_path / "custom_data"
479+
triplet = "x86_64-linux-gnu"
480+
toolchain_path = data_dir / "toolchain" / triplet
481+
toolchain_path.mkdir(parents=True)
482+
483+
# Patch sys.platform to simulate Linux
484+
monkeypatch.setattr(sys, "platform", "linux")
485+
monkeypatch.setattr(
486+
relenv.common, "get_triplet", lambda machine=None, plat=None: triplet
487+
)
488+
489+
# Set RELENV_DATA and reload the module to pick up the new DATA_DIR
490+
monkeypatch.setenv("RELENV_DATA", str(data_dir))
491+
import importlib
492+
493+
importlib.reload(relenv.common)
494+
495+
# Verify toolchain_root_dir returns DATA_DIR/toolchain
496+
from relenv.common import toolchain_root_dir
497+
498+
result = toolchain_root_dir()
499+
assert result == data_dir / "toolchain"
500+
501+
502+
def test_toolchain_uses_cache_without_relenv_data(
503+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
504+
) -> None:
505+
"""
506+
Test that toolchain uses cache directory when RELENV_DATA is not set.
507+
508+
When RELENV_DATA is not set, the toolchain should be stored in the
509+
cache directory (~/.cache/relenv/toolchains or $XDG_CACHE_HOME/relenv/toolchains)
510+
to allow reuse across different relenv environments.
511+
"""
512+
cache_dir = tmp_path / ".cache" / "relenv" / "toolchains"
513+
514+
# Patch sys.platform to simulate Linux
515+
monkeypatch.setattr(sys, "platform", "linux")
516+
517+
# Remove RELENV_DATA if set
518+
monkeypatch.delenv("RELENV_DATA", raising=False)
519+
520+
# Set XDG_CACHE_HOME to control cache location
521+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / ".cache"))
522+
523+
# Reload module to pick up environment changes
524+
import importlib
525+
526+
importlib.reload(relenv.common)
527+
528+
from relenv.common import toolchain_root_dir
529+
530+
result = toolchain_root_dir()
531+
assert result == cache_dir
532+
533+
467534
def test_copyright_headers() -> None:
468535
"""Verify all Python source files have the correct copyright header."""
469536
expected_header = (

tests/test_runtime.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1821,3 +1821,169 @@ def __init__(self) -> None:
18211821
)
18221822
monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
18231823
assert relenv.runtime.load_openssl_provider("default") == 456
1824+
1825+
1826+
def test_sysconfig_wrapper_applied_for_python_313_plus(
1827+
monkeypatch: pytest.MonkeyPatch,
1828+
) -> None:
1829+
"""
1830+
Test that sysconfig wrapper is applied for Python 3.13+.
1831+
1832+
This is a regression test for Python 3.13 where sysconfig changed from
1833+
a single module to a package. The RelenvImporter no longer intercepts
1834+
the import automatically, so we must manually apply the wrapper.
1835+
1836+
Without this fix, Python 3.13+ would use the toolchain gcc with full path
1837+
even when RELENV_BUILDENV is not set, causing build failures with packages
1838+
like mysqlclient that compile native extensions.
1839+
"""
1840+
# Simulate Python 3.13+
1841+
fake_version = (3, 13, 0, "final", 0)
1842+
monkeypatch.setattr(relenv.runtime.sys, "version_info", fake_version)
1843+
1844+
# Track whether wrap_sysconfig was called
1845+
wrap_called = {"count": 0, "module_name": None}
1846+
1847+
def fake_wrap_sysconfig(name: str) -> ModuleType:
1848+
wrap_called["count"] += 1
1849+
wrap_called["module_name"] = name
1850+
return ModuleType("sysconfig")
1851+
1852+
monkeypatch.setattr(relenv.runtime, "wrap_sysconfig", fake_wrap_sysconfig)
1853+
1854+
# Mock other dependencies to avoid side effects
1855+
monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: pathlib.Path("/root"))
1856+
monkeypatch.setattr(relenv.runtime, "setup_openssl", lambda: None)
1857+
monkeypatch.setattr(relenv.runtime, "setup_crossroot", lambda: None)
1858+
monkeypatch.setattr(relenv.runtime, "install_cargo_config", lambda: None)
1859+
monkeypatch.setattr(
1860+
relenv.runtime.site, "execsitecustomize", lambda: None, raising=False
1861+
)
1862+
monkeypatch.setattr(
1863+
relenv.runtime, "wrapsitecustomize", lambda func: func, raising=False
1864+
)
1865+
1866+
# Mock importer
1867+
fake_importer = SimpleNamespace()
1868+
monkeypatch.setattr(relenv.runtime, "importer", fake_importer, raising=False)
1869+
1870+
# Clear sys.meta_path to avoid side effects
1871+
original_meta_path = sys.meta_path.copy()
1872+
monkeypatch.setattr(sys, "meta_path", [])
1873+
1874+
try:
1875+
# Execute the module initialization code at the end of runtime.py
1876+
# This simulates what happens when the runtime module is imported
1877+
exec(
1878+
"""
1879+
import sys
1880+
sys.RELENV = relenv_root()
1881+
setup_openssl()
1882+
site.execsitecustomize = wrapsitecustomize(site.execsitecustomize)
1883+
setup_crossroot()
1884+
install_cargo_config()
1885+
sys.meta_path = [importer] + sys.meta_path
1886+
1887+
# For Python 3.13+, sysconfig became a package so the importer doesn't
1888+
# intercept it. Manually wrap it here.
1889+
if sys.version_info >= (3, 13):
1890+
wrap_sysconfig("sysconfig")
1891+
""",
1892+
{
1893+
"sys": relenv.runtime.sys,
1894+
"relenv_root": relenv.runtime.relenv_root,
1895+
"setup_openssl": relenv.runtime.setup_openssl,
1896+
"site": relenv.runtime.site,
1897+
"wrapsitecustomize": relenv.runtime.wrapsitecustomize,
1898+
"setup_crossroot": relenv.runtime.setup_crossroot,
1899+
"install_cargo_config": relenv.runtime.install_cargo_config,
1900+
"importer": fake_importer,
1901+
"wrap_sysconfig": fake_wrap_sysconfig,
1902+
},
1903+
)
1904+
1905+
# Verify wrap_sysconfig was called for Python 3.13+
1906+
assert wrap_called["count"] == 1
1907+
assert wrap_called["module_name"] == "sysconfig"
1908+
1909+
finally:
1910+
# Restore original meta_path
1911+
monkeypatch.setattr(sys, "meta_path", original_meta_path)
1912+
1913+
1914+
def test_sysconfig_wrapper_not_applied_for_python_312(
1915+
monkeypatch: pytest.MonkeyPatch,
1916+
) -> None:
1917+
"""
1918+
Test that sysconfig wrapper is NOT applied for Python 3.12 and earlier.
1919+
1920+
For Python 3.12 and earlier, sysconfig is a single module file and the
1921+
RelenvImporter intercepts it automatically. We should not manually wrap
1922+
it to avoid double-wrapping.
1923+
"""
1924+
# Simulate Python 3.12
1925+
fake_version = (3, 12, 0, "final", 0)
1926+
monkeypatch.setattr(relenv.runtime.sys, "version_info", fake_version)
1927+
1928+
# Track whether wrap_sysconfig was called
1929+
wrap_called = {"count": 0}
1930+
1931+
def fake_wrap_sysconfig(name: str) -> ModuleType:
1932+
wrap_called["count"] += 1
1933+
return ModuleType("sysconfig")
1934+
1935+
monkeypatch.setattr(relenv.runtime, "wrap_sysconfig", fake_wrap_sysconfig)
1936+
1937+
# Mock other dependencies
1938+
monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: pathlib.Path("/root"))
1939+
monkeypatch.setattr(relenv.runtime, "setup_openssl", lambda: None)
1940+
monkeypatch.setattr(relenv.runtime, "setup_crossroot", lambda: None)
1941+
monkeypatch.setattr(relenv.runtime, "install_cargo_config", lambda: None)
1942+
monkeypatch.setattr(
1943+
relenv.runtime.site, "execsitecustomize", lambda: None, raising=False
1944+
)
1945+
monkeypatch.setattr(
1946+
relenv.runtime, "wrapsitecustomize", lambda func: func, raising=False
1947+
)
1948+
1949+
fake_importer = SimpleNamespace()
1950+
monkeypatch.setattr(relenv.runtime, "importer", fake_importer, raising=False)
1951+
1952+
original_meta_path = sys.meta_path.copy()
1953+
monkeypatch.setattr(sys, "meta_path", [])
1954+
1955+
try:
1956+
# Execute the module initialization code
1957+
exec(
1958+
"""
1959+
import sys
1960+
sys.RELENV = relenv_root()
1961+
setup_openssl()
1962+
site.execsitecustomize = wrapsitecustomize(site.execsitecustomize)
1963+
setup_crossroot()
1964+
install_cargo_config()
1965+
sys.meta_path = [importer] + sys.meta_path
1966+
1967+
# For Python 3.13+, sysconfig became a package so the importer doesn't
1968+
# intercept it. Manually wrap it here.
1969+
if sys.version_info >= (3, 13):
1970+
wrap_sysconfig("sysconfig")
1971+
""",
1972+
{
1973+
"sys": relenv.runtime.sys,
1974+
"relenv_root": relenv.runtime.relenv_root,
1975+
"setup_openssl": relenv.runtime.setup_openssl,
1976+
"site": relenv.runtime.site,
1977+
"wrapsitecustomize": relenv.runtime.wrapsitecustomize,
1978+
"setup_crossroot": relenv.runtime.setup_crossroot,
1979+
"install_cargo_config": relenv.runtime.install_cargo_config,
1980+
"importer": fake_importer,
1981+
"wrap_sysconfig": fake_wrap_sysconfig,
1982+
},
1983+
)
1984+
1985+
# Verify wrap_sysconfig was NOT called for Python 3.12
1986+
assert wrap_called["count"] == 0
1987+
1988+
finally:
1989+
monkeypatch.setattr(sys, "meta_path", original_meta_path)

0 commit comments

Comments
 (0)