relenv 0.21.2__py3-none-any.whl → 0.22.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. relenv/__init__.py +14 -2
  2. relenv/__main__.py +12 -6
  3. relenv/_resources/xz/config.h +148 -0
  4. relenv/_resources/xz/readme.md +4 -0
  5. relenv/build/__init__.py +28 -30
  6. relenv/build/common/__init__.py +50 -0
  7. relenv/build/common/_sysconfigdata_template.py +72 -0
  8. relenv/build/common/builder.py +907 -0
  9. relenv/build/common/builders.py +163 -0
  10. relenv/build/common/download.py +324 -0
  11. relenv/build/common/install.py +609 -0
  12. relenv/build/common/ui.py +432 -0
  13. relenv/build/darwin.py +128 -14
  14. relenv/build/linux.py +292 -74
  15. relenv/build/windows.py +123 -169
  16. relenv/buildenv.py +48 -17
  17. relenv/check.py +10 -5
  18. relenv/common.py +489 -165
  19. relenv/create.py +147 -7
  20. relenv/fetch.py +16 -4
  21. relenv/manifest.py +15 -7
  22. relenv/python-versions.json +329 -0
  23. relenv/pyversions.py +817 -30
  24. relenv/relocate.py +101 -55
  25. relenv/runtime.py +452 -282
  26. relenv/toolchain.py +9 -3
  27. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/METADATA +1 -1
  28. relenv-0.22.0.dist-info/RECORD +48 -0
  29. tests/__init__.py +2 -0
  30. tests/_pytest_typing.py +45 -0
  31. tests/conftest.py +42 -36
  32. tests/test_build.py +426 -9
  33. tests/test_common.py +311 -48
  34. tests/test_create.py +149 -6
  35. tests/test_downloads.py +19 -15
  36. tests/test_fips_photon.py +6 -3
  37. tests/test_module_imports.py +44 -0
  38. tests/test_pyversions_runtime.py +177 -0
  39. tests/test_relocate.py +45 -39
  40. tests/test_relocate_module.py +257 -0
  41. tests/test_runtime.py +1802 -6
  42. tests/test_verify_build.py +477 -34
  43. relenv/build/common.py +0 -1707
  44. relenv-0.21.2.dist-info/RECORD +0 -35
  45. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/WHEEL +0 -0
  46. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/entry_points.txt +0 -0
  47. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/licenses/LICENSE.md +0 -0
  48. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/licenses/NOTICE +0 -0
  49. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,19 @@
1
1
  # Copyright 2022-2025 Broadcom.
2
- # SPDX-License-Identifier: Apache-2
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # mypy: ignore-errors
3
4
  """
4
5
  Verify relenv builds.
5
6
  """
6
7
  import json
7
8
  import os
8
9
  import pathlib
10
+ import shlex
9
11
  import shutil
10
12
  import subprocess
11
13
  import sys
12
14
  import textwrap
13
15
  import time
16
+ import uuid
14
17
 
15
18
  import packaging
16
19
  import pytest
@@ -65,6 +68,48 @@ def _install_ppbt(pexec):
65
68
  assert p.returncode == 0, "Failed to extract toolchain"
66
69
 
67
70
 
71
+ def _setup_buildenv(pyexec, env):
72
+ """
73
+ Setup build environment variables for compiling C extensions.
74
+
75
+ On Linux, this calls 'relenv buildenv --json' to get the proper compiler
76
+ flags and paths to use the relenv toolchain and bundled libraries instead
77
+ of system libraries.
78
+
79
+ :param pyexec: Path to the relenv Python executable
80
+ :param env: Environment dictionary to update with buildenv variables
81
+ """
82
+ if sys.platform != "linux":
83
+ return
84
+
85
+ p = subprocess.run(
86
+ [
87
+ str(pyexec),
88
+ "-m",
89
+ "relenv",
90
+ "buildenv",
91
+ "--json",
92
+ ],
93
+ capture_output=True,
94
+ )
95
+ try:
96
+ buildenv = json.loads(p.stdout)
97
+ except json.JSONDecodeError:
98
+ assert False, f"Failed to decode json: {p.stdout.decode()} {p.stderr.decode()}"
99
+
100
+ for k in buildenv:
101
+ env[k] = buildenv[k]
102
+
103
+
104
+ @pytest.fixture(autouse=True)
105
+ def _clear_ssl_env(monkeypatch: pytest.MonkeyPatch) -> None:
106
+ """
107
+ Ensure preceding tests do not leave stale certificate paths behind.
108
+ """
109
+ monkeypatch.delenv("SSL_CERT_FILE", raising=False)
110
+ monkeypatch.delenv("SSL_CERT_DIR", raising=False)
111
+
112
+
68
113
  @pytest.fixture(scope="module")
69
114
  def arch():
70
115
  return build_arch()
@@ -320,27 +365,7 @@ def test_pip_install_salt_w_package_requirements(
320
365
 
321
366
  _install_ppbt(pyexec)
322
367
  env = os.environ.copy()
323
-
324
- # if sys.platform == "linux":
325
- # p = subprocess.run(
326
- # [
327
- # pyexec,
328
- # "-m",
329
- # "relenv",
330
- # "buildenv",
331
- # "--json",
332
- # ],
333
- # capture_output=True,
334
- # )
335
- # try:
336
- # buildenv = json.loads(p.stdout)
337
- # except json.JSONDecodeError:
338
- # assert (
339
- # False
340
- # ), f"Failed to decode json: {p.stdout.decode()} {p.stderr.decode()}"
341
- # for k in buildenv:
342
- # env[k] = buildenv[k]
343
-
368
+ _setup_buildenv(pyexec, env)
344
369
  env["RELENV_BUILDENV"] = "yes"
345
370
  env["USE_STATIC_REQUIREMENTS"] = "1"
346
371
  p = subprocess.run(
@@ -413,7 +438,15 @@ def test_pip_install_salt_w_package_requirements(
413
438
  "26.4.0",
414
439
  ],
415
440
  )
416
- def test_pip_install_pyzmq(pipexec, pyexec, pyzmq_version, build_version, arch, build):
441
+ def test_pip_install_pyzmq(
442
+ pipexec,
443
+ pyexec,
444
+ pyzmq_version,
445
+ build_version,
446
+ arch,
447
+ build,
448
+ tmp_path: pathlib.Path,
449
+ ) -> None:
417
450
 
418
451
  if pyzmq_version == "23.2.0" and "3.12" in build_version:
419
452
  pytest.xfail(f"{pyzmq_version} does not install on 3.12")
@@ -483,6 +516,157 @@ def test_pip_install_pyzmq(pipexec, pyexec, pyzmq_version, build_version, arch,
483
516
  env["ZMQ_PREFIX"] = "bundled"
484
517
  env["RELENV_BUILDENV"] = "yes"
485
518
  env["USE_STATIC_REQUIREMENTS"] = "1"
519
+
520
+ if sys.platform == "linux":
521
+ fake_bsd_root = tmp_path / "fake_libbsd"
522
+ (fake_bsd_root / "bsd").mkdir(parents=True, exist_ok=True)
523
+ (fake_bsd_root / "bsd" / "string.h").write_text(
524
+ textwrap.dedent(
525
+ """\
526
+ #ifndef RELENV_FAKE_BSD_STRING_H
527
+ #define RELENV_FAKE_BSD_STRING_H
528
+
529
+ #include <stddef.h>
530
+
531
+ #ifdef __cplusplus
532
+ extern "C" {
533
+ #endif
534
+
535
+ size_t strlcpy(char *dst, const char *src, size_t siz);
536
+ size_t strlcat(char *dst, const char *src, size_t siz);
537
+
538
+ #ifdef __cplusplus
539
+ }
540
+ #endif
541
+
542
+ #endif /* RELENV_FAKE_BSD_STRING_H */
543
+ """
544
+ )
545
+ )
546
+ (fake_bsd_root / "string.c").write_text(
547
+ textwrap.dedent(
548
+ """\
549
+ #include <stddef.h>
550
+ #include <string.h>
551
+
552
+ static size_t relenv_strlen(const char *s) {
553
+ size_t len = 0;
554
+ if (s == NULL) {
555
+ return 0;
556
+ }
557
+ while (s[len] != '\\0') {
558
+ ++len;
559
+ }
560
+ return len;
561
+ }
562
+
563
+ static size_t relenv_strnlen(const char *s, size_t maxlen) {
564
+ size_t len = 0;
565
+ if (s == NULL) {
566
+ return 0;
567
+ }
568
+ while (len < maxlen && s[len] != '\\0') {
569
+ ++len;
570
+ }
571
+ return len;
572
+ }
573
+
574
+ size_t strlcpy(char *dst, const char *src, size_t siz) {
575
+ size_t src_len = relenv_strlen(src);
576
+ if (siz == 0 || dst == NULL) {
577
+ return src_len;
578
+ }
579
+ size_t copy = src_len;
580
+ if (copy >= siz) {
581
+ copy = siz - 1;
582
+ }
583
+ if (copy > 0 && src != NULL) {
584
+ memcpy(dst, src, copy);
585
+ }
586
+ dst[copy] = '\\0';
587
+ return src_len;
588
+ }
589
+
590
+ size_t strlcat(char *dst, const char *src, size_t siz) {
591
+ size_t dst_len = relenv_strnlen(dst, siz);
592
+ size_t src_len = relenv_strlen(src);
593
+ size_t initial_len = dst_len;
594
+ if (dst == NULL || dst_len >= siz) {
595
+ return initial_len + src_len;
596
+ }
597
+ size_t space = (siz > dst_len + 1) ? siz - dst_len - 1 : 0;
598
+ size_t copy = 0;
599
+ if (space > 0 && src != NULL) {
600
+ copy = src_len;
601
+ if (copy > space) {
602
+ copy = space;
603
+ }
604
+ if (copy > 0) {
605
+ memcpy(dst + dst_len, src, copy);
606
+ }
607
+ dst_len += copy;
608
+ }
609
+ dst[dst_len] = '\\0';
610
+ return initial_len + src_len;
611
+ }
612
+ """
613
+ )
614
+ )
615
+ include_flag = f"-I{fake_bsd_root}"
616
+ for key in ("CFLAGS", "CXXFLAGS", "CPPFLAGS"):
617
+ env[key] = " ".join(filter(None, [env.get(key, ""), include_flag])).strip()
618
+ env["CPATH"] = ":".join(
619
+ filter(None, [str(fake_bsd_root), env.get("CPATH", "")])
620
+ )
621
+ for key in ("C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH"):
622
+ env[key] = ":".join(filter(None, [str(fake_bsd_root), env.get(key, "")]))
623
+ cc_value = env.get("CC")
624
+ if cc_value:
625
+ cc_args = shlex.split(cc_value)
626
+ else:
627
+ cc_path = shutil.which("cc") or shutil.which("gcc")
628
+ assert cc_path, "C compiler not found for libbsd shim"
629
+ cc_args = [cc_path]
630
+ obj_path = fake_bsd_root / "string.o"
631
+ compile_result = subprocess.run(
632
+ cc_args
633
+ + [
634
+ "-c",
635
+ "-O2",
636
+ "-fPIC",
637
+ "-o",
638
+ str(obj_path),
639
+ str(fake_bsd_root / "string.c"),
640
+ ],
641
+ env=env,
642
+ )
643
+ assert compile_result.returncode == 0, "Failed to compile libbsd shim"
644
+ ar_value = env.get("AR")
645
+ if ar_value:
646
+ ar_args = shlex.split(ar_value)
647
+ else:
648
+ ar_path = shutil.which("ar")
649
+ assert ar_path, "Archiver not found for libbsd shim"
650
+ ar_args = [ar_path]
651
+ libbsd_static = fake_bsd_root / "libbsd.a"
652
+ archive_result = subprocess.run(
653
+ ar_args + ["rcs", str(libbsd_static), str(obj_path)],
654
+ env=env,
655
+ )
656
+ assert archive_result.returncode == 0, "Failed to archive libbsd shim"
657
+ lib_dir_flag = f"-L{fake_bsd_root}"
658
+ env["LDFLAGS"] = " ".join(
659
+ filter(None, [lib_dir_flag, env.get("LDFLAGS", "")])
660
+ ).strip()
661
+ env["LIBS"] = " ".join(filter(None, ["-lbsd", env.get("LIBS", "")])).strip()
662
+ env["LIBRARY_PATH"] = ":".join(
663
+ filter(None, [str(fake_bsd_root), env.get("LIBRARY_PATH", "")])
664
+ )
665
+ env["ac_cv_func_strlcpy"] = "yes"
666
+ env["ac_cv_func_strlcat"] = "yes"
667
+ env["ac_cv_have_decl_strlcpy"] = "yes"
668
+ env["ac_cv_have_decl_strlcat"] = "yes"
669
+
486
670
  p = subprocess.run(
487
671
  [
488
672
  str(pipexec),
@@ -826,6 +1010,7 @@ def test_shebangs(pipexec, build, minor_version):
826
1010
  def test_moving_pip_installed_c_extentions(pipexec, pyexec, build, minor_version):
827
1011
  _install_ppbt(pyexec)
828
1012
  env = os.environ.copy()
1013
+ _setup_buildenv(pyexec, env)
829
1014
  env["RELENV_DEBUG"] = "yes"
830
1015
  env["RELENV_BUILDENV"] = "yes"
831
1016
  p = subprocess.run(
@@ -857,11 +1042,6 @@ def test_cryptography_rpath(
857
1042
  pyexec, pipexec, build, minor_version, cryptography_version
858
1043
  ):
859
1044
  _install_ppbt(pyexec)
860
- # log.warn("Extract ppbt")
861
- # p = subprocess.run(
862
- # [pyexec, "-c", "import ppbt; ppbt.extract()"],
863
- # )
864
- # assert p.returncode == 0
865
1045
 
866
1046
  def find_library(path, search):
867
1047
  for root, dirs, files in os.walk(path):
@@ -870,6 +1050,7 @@ def test_cryptography_rpath(
870
1050
  return fname
871
1051
 
872
1052
  env = os.environ.copy()
1053
+ _setup_buildenv(pyexec, env)
873
1054
  env["RELENV_BUILDENV"] = "yes"
874
1055
  p = subprocess.run(
875
1056
  [
@@ -1155,7 +1336,7 @@ def test_install_python_ldap(pipexec, pyexec, build):
1155
1336
  tar xvf cyrus-sasl-{saslver}.tar.gz
1156
1337
  cd cyrus-sasl-{saslver}
1157
1338
  ./configure --prefix=$RELENV_PATH
1158
- make
1339
+ make -j"$(nproc)"
1159
1340
  make install
1160
1341
  cd ..
1161
1342
 
@@ -1164,7 +1345,7 @@ def test_install_python_ldap(pipexec, pyexec, build):
1164
1345
  tar xvf openldap-{ldapver}.tgz
1165
1346
  cd openldap-{ldapver}
1166
1347
  ./configure --prefix=$RELENV_PATH
1167
- make
1348
+ make -j"$(nproc)"
1168
1349
  make install
1169
1350
  cd ..
1170
1351
 
@@ -1184,6 +1365,7 @@ def test_install_python_ldap(pipexec, pyexec, build):
1184
1365
 
1185
1366
  subprocess.run(["/usr/bin/bash", "buildscript.sh"], check=True)
1186
1367
  env = os.environ.copy()
1368
+ _setup_buildenv(pyexec, env)
1187
1369
  env["RELENV_DEBUG"] = "yes"
1188
1370
  env["RELENV_BUILDENV"] = "yes"
1189
1371
 
@@ -1215,9 +1397,16 @@ def test_install_with_target_shebang(pipexec, build, minor_version):
1215
1397
  check=True,
1216
1398
  env=env,
1217
1399
  )
1218
- shebang = pathlib.Path(extras / "bin" / "cowsay").open().readlines()[2].strip()
1400
+ exec_line = ""
1401
+ for line in pathlib.Path(extras / "bin" / "cowsay").read_text().splitlines():
1402
+ stripped = line.strip()
1403
+ if not stripped or stripped.startswith("#"):
1404
+ continue
1405
+ if stripped.startswith('"exec"'):
1406
+ exec_line = stripped
1407
+ break
1219
1408
  assert (
1220
- shebang
1409
+ exec_line
1221
1410
  == '"exec" "$(dirname "$(readlink -f "$0")")/../../bin/python{}" "$0" "$@"'.format(
1222
1411
  minor_version
1223
1412
  )
@@ -1764,7 +1953,7 @@ def test_install_editable_package_in_extras(
1764
1953
  def rockycontainer(build):
1765
1954
  if not shutil.which("docker"):
1766
1955
  pytest.skip(reason="No docker binary found")
1767
- name = "rocky10"
1956
+ name = f"rocky10-{uuid.uuid4().hex}"
1768
1957
  subprocess.run(
1769
1958
  [
1770
1959
  "docker",
@@ -1815,10 +2004,35 @@ def rockycontainer(build):
1815
2004
 
1816
2005
 
1817
2006
  @pytest.mark.skip_on_windows
1818
- def test_no_openssl_binary(rockycontainer, pipexec, pyexec):
2007
+ def test_no_openssl_binary(rockycontainer, pipexec, pyexec, build):
1819
2008
  _install_ppbt(pyexec)
1820
2009
  env = os.environ.copy()
2010
+ _setup_buildenv(pyexec, env)
1821
2011
  env["RELENV_BUILDENV"] = "yes"
2012
+ if sys.platform == "linux":
2013
+ toolchain_path = pathlib.Path(env["TOOLCHAIN_PATH"])
2014
+ triplet = env["TRIPLET"]
2015
+ sysroot_lib = toolchain_path / triplet / "sysroot" / "lib"
2016
+ sysroot_lib.mkdir(parents=True, exist_ok=True)
2017
+ bz2_sources = sorted(
2018
+ (pathlib.Path(build) / "lib").glob("libbz2.so*"),
2019
+ key=lambda p: len(p.name),
2020
+ )
2021
+ if not bz2_sources:
2022
+ pytest.fail(
2023
+ "libbz2.so not found in relenv build; cryptography build cannot proceed"
2024
+ )
2025
+ for bz2_source in bz2_sources:
2026
+ target = sysroot_lib / bz2_source.name
2027
+ if target.exists() or target.is_symlink():
2028
+ if target.is_symlink():
2029
+ try:
2030
+ if target.readlink() == bz2_source:
2031
+ continue
2032
+ except OSError:
2033
+ pass
2034
+ target.unlink()
2035
+ target.symlink_to(bz2_source)
1822
2036
  proc = subprocess.run(
1823
2037
  [
1824
2038
  str(pipexec),
@@ -1882,3 +2096,232 @@ def test_install_setuptools_25_2_to_25_3(pipexec, build, minor_version, pip_vers
1882
2096
  ],
1883
2097
  check=True,
1884
2098
  )
2099
+
2100
+
2101
+ def test_expat_version(pyexec):
2102
+ """
2103
+ Verify that the build contains the correct expat version.
2104
+
2105
+ This validates that update_expat() successfully updated the bundled
2106
+ expat library to match the version in python-versions.json.
2107
+
2108
+ Works on all platforms: Linux, Darwin (macOS), and Windows.
2109
+ """
2110
+ from relenv.build.common import get_dependency_version
2111
+
2112
+ # Map sys.platform to relenv platform names
2113
+ platform_map = {
2114
+ "linux": "linux",
2115
+ "darwin": "darwin",
2116
+ "win32": "win32",
2117
+ }
2118
+ platform = platform_map.get(sys.platform)
2119
+ if not platform:
2120
+ pytest.skip(f"Unknown platform: {sys.platform}")
2121
+
2122
+ # Get expected version from python-versions.json
2123
+ expat_info = get_dependency_version("expat", platform)
2124
+ if not expat_info:
2125
+ pytest.skip(f"No expat version defined in python-versions.json for {platform}")
2126
+
2127
+ expected_version = expat_info["version"]
2128
+
2129
+ # Get actual version from the build
2130
+ proc = subprocess.run(
2131
+ [str(pyexec), "-c", "import pyexpat; print(pyexpat.EXPAT_VERSION)"],
2132
+ capture_output=True,
2133
+ check=True,
2134
+ )
2135
+
2136
+ actual_version_str = proc.stdout.decode().strip()
2137
+ # Format is "expat_X_Y_Z", extract version
2138
+ assert actual_version_str.startswith(
2139
+ "expat_"
2140
+ ), f"Unexpected EXPAT_VERSION format: {actual_version_str}"
2141
+
2142
+ # Convert "expat_2_7_3" -> "2.7.3"
2143
+ actual_version = actual_version_str.replace("expat_", "").replace("_", ".")
2144
+
2145
+ assert actual_version == expected_version, (
2146
+ f"Expat version mismatch on {platform}: expected {expected_version}, "
2147
+ f"found {actual_version} (from {actual_version_str})"
2148
+ )
2149
+
2150
+
2151
+ def test_sqlite_version(pyexec):
2152
+ """
2153
+ Verify that the build contains the correct SQLite version.
2154
+
2155
+ This validates that SQLite was built with the version specified
2156
+ in python-versions.json.
2157
+
2158
+ Works on all platforms: Linux, Darwin (macOS), and Windows.
2159
+ """
2160
+ from relenv.build.common import get_dependency_version
2161
+
2162
+ # Map sys.platform to relenv platform names
2163
+ platform_map = {
2164
+ "linux": "linux",
2165
+ "darwin": "darwin",
2166
+ "win32": "win32",
2167
+ }
2168
+ platform = platform_map.get(sys.platform)
2169
+ if not platform:
2170
+ pytest.skip(f"Unknown platform: {sys.platform}")
2171
+
2172
+ # Get expected version from python-versions.json
2173
+ sqlite_info = get_dependency_version("sqlite", platform)
2174
+ if not sqlite_info:
2175
+ pytest.skip(f"No sqlite version defined in python-versions.json for {platform}")
2176
+
2177
+ expected_version = sqlite_info["version"]
2178
+
2179
+ # Get actual version from the build
2180
+ proc = subprocess.run(
2181
+ [str(pyexec), "-c", "import sqlite3; print(sqlite3.sqlite_version)"],
2182
+ capture_output=True,
2183
+ check=True,
2184
+ )
2185
+
2186
+ actual_version = proc.stdout.decode().strip()
2187
+
2188
+ # SQLite version in JSON is like "3.50.4.0" but runtime shows "3.50.4"
2189
+ # So we need to handle both formats
2190
+ if expected_version.count(".") == 3:
2191
+ # Remove trailing .0 for comparison
2192
+ expected_version = ".".join(expected_version.split(".")[:3])
2193
+
2194
+ assert actual_version == expected_version, (
2195
+ f"SQLite version mismatch on {platform}: expected {expected_version}, "
2196
+ f"found {actual_version}"
2197
+ )
2198
+
2199
+
2200
+ def test_openssl_version(pyexec):
2201
+ """
2202
+ Verify that the build contains the correct OpenSSL version.
2203
+
2204
+ This validates that OpenSSL was built with the version specified
2205
+ in python-versions.json.
2206
+
2207
+ Works on all platforms: Linux, Darwin (macOS), and Windows.
2208
+ """
2209
+ import re
2210
+
2211
+ from relenv.build.common import get_dependency_version
2212
+
2213
+ # Map sys.platform to relenv platform names
2214
+ platform_map = {
2215
+ "linux": "linux",
2216
+ "darwin": "darwin",
2217
+ "win32": "win32",
2218
+ }
2219
+ platform = platform_map.get(sys.platform)
2220
+ if not platform:
2221
+ pytest.skip(f"Unknown platform: {sys.platform}")
2222
+
2223
+ # Get expected version from python-versions.json
2224
+ openssl_info = get_dependency_version("openssl", platform)
2225
+ if not openssl_info:
2226
+ pytest.skip(
2227
+ f"No openssl version defined in python-versions.json for {platform}"
2228
+ )
2229
+
2230
+ expected_version = openssl_info["version"]
2231
+
2232
+ # Get actual version from the build
2233
+ proc = subprocess.run(
2234
+ [str(pyexec), "-c", "import ssl; print(ssl.OPENSSL_VERSION)"],
2235
+ capture_output=True,
2236
+ check=True,
2237
+ )
2238
+
2239
+ actual_version_str = proc.stdout.decode().strip()
2240
+ # Format is "OpenSSL 3.5.4 30 Sep 2025"
2241
+ # Extract version number
2242
+ match = re.search(r"OpenSSL (\d+\.\d+\.\d+)", actual_version_str)
2243
+ if not match:
2244
+ pytest.fail(f"Could not parse OpenSSL version from: {actual_version_str}")
2245
+
2246
+ actual_version = match.group(1)
2247
+
2248
+ assert actual_version == expected_version, (
2249
+ f"OpenSSL version mismatch on {platform}: expected {expected_version}, "
2250
+ f"found {actual_version} (from {actual_version_str})"
2251
+ )
2252
+
2253
+
2254
+ def test_xz_lzma_functionality(pyexec):
2255
+ """
2256
+ Verify that the lzma module works correctly.
2257
+
2258
+ This is especially important for Windows builds which use a config.h
2259
+ compatibility shim from XZ 5.4.7 to support newer XZ versions that
2260
+ removed MSBuild support.
2261
+
2262
+ If this test fails, it indicates that the config.h in
2263
+ relenv/_resources/xz/config.h is no longer compatible with the
2264
+ current XZ version being used.
2265
+
2266
+ Works on all platforms: Linux, Darwin (macOS), and Windows.
2267
+ """
2268
+ # Test that lzma module loads and basic compression works
2269
+ test_code = """
2270
+ import lzma
2271
+ import sys
2272
+
2273
+ # Test basic compression/decompression
2274
+ test_data = b"Hello, World! " * 100
2275
+ compressed = lzma.compress(test_data)
2276
+ decompressed = lzma.decompress(compressed)
2277
+
2278
+ if test_data != decompressed:
2279
+ print("ERROR: Decompressed data does not match original", file=sys.stderr)
2280
+ sys.exit(1)
2281
+
2282
+ # Verify compression actually happened
2283
+ if len(compressed) >= len(test_data):
2284
+ print("ERROR: Compression did not reduce size", file=sys.stderr)
2285
+ sys.exit(2)
2286
+
2287
+ # Test different formats (skip FORMAT_RAW as it requires explicit filters)
2288
+ for fmt in [lzma.FORMAT_XZ, lzma.FORMAT_ALONE]:
2289
+ try:
2290
+ data = lzma.compress(test_data, format=fmt)
2291
+ result = lzma.decompress(data)
2292
+ if result != test_data:
2293
+ print(f"ERROR: Format {fmt} failed round-trip", file=sys.stderr)
2294
+ sys.exit(3)
2295
+ except Exception as e:
2296
+ print(f"ERROR: Format {fmt} raised exception: {e}", file=sys.stderr)
2297
+ sys.exit(4)
2298
+
2299
+ # Test streaming compression/decompression
2300
+ import io
2301
+ output = io.BytesIO()
2302
+ with lzma.LZMAFile(output, "w") as f:
2303
+ f.write(test_data)
2304
+
2305
+ compressed_stream = output.getvalue()
2306
+ input_stream = io.BytesIO(compressed_stream)
2307
+ with lzma.LZMAFile(input_stream, "r") as f:
2308
+ decompressed_stream = f.read()
2309
+
2310
+ if decompressed_stream != test_data:
2311
+ print("ERROR: Streaming compression/decompression failed", file=sys.stderr)
2312
+ sys.exit(5)
2313
+
2314
+ print("OK")
2315
+ """
2316
+
2317
+ proc = subprocess.run(
2318
+ [str(pyexec), "-c", test_code],
2319
+ capture_output=True,
2320
+ check=False,
2321
+ )
2322
+
2323
+ assert proc.returncode == 0, (
2324
+ f"LZMA functionality test failed (exit code {proc.returncode}): "
2325
+ f"{proc.stderr.decode()}"
2326
+ )
2327
+ assert proc.stdout.decode().strip() == "OK"