relenv 0.22.1__py3-none-any.whl → 0.22.3__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 (50) hide show
  1. relenv/__init__.py +1 -1
  2. relenv/__main__.py +1 -1
  3. relenv/_resources/xz/crc32_table.c +22 -0
  4. relenv/_resources/xz/crc64_table.c +33 -0
  5. relenv/_resources/xz/readme.md +13 -4
  6. relenv/build/__init__.py +21 -25
  7. relenv/build/common/__init__.py +1 -1
  8. relenv/build/common/_sysconfigdata_template.py +1 -1
  9. relenv/build/common/builder.py +1 -1
  10. relenv/build/common/builders.py +1 -1
  11. relenv/build/common/download.py +1 -1
  12. relenv/build/common/install.py +6 -2
  13. relenv/build/common/ui.py +1 -1
  14. relenv/build/darwin.py +1 -1
  15. relenv/build/linux.py +1 -1
  16. relenv/build/windows.py +9 -1
  17. relenv/buildenv.py +1 -1
  18. relenv/check.py +1 -1
  19. relenv/common.py +2 -4
  20. relenv/create.py +19 -27
  21. relenv/fetch.py +7 -5
  22. relenv/manifest.py +1 -1
  23. relenv/python-versions.json +43 -0
  24. relenv/pyversions.py +49 -1
  25. relenv/relocate.py +118 -4
  26. relenv/runtime.py +29 -20
  27. relenv/toolchain.py +1 -1
  28. {relenv-0.22.1.dist-info → relenv-0.22.3.dist-info}/METADATA +1 -1
  29. relenv-0.22.3.dist-info/RECORD +51 -0
  30. {relenv-0.22.1.dist-info → relenv-0.22.3.dist-info}/WHEEL +1 -1
  31. tests/__init__.py +1 -1
  32. tests/_pytest_typing.py +1 -1
  33. tests/conftest.py +1 -1
  34. tests/test_build.py +3 -7
  35. tests/test_common.py +2 -2
  36. tests/test_create.py +1 -1
  37. tests/test_downloads.py +1 -1
  38. tests/test_fips_photon.py +1 -1
  39. tests/test_module_imports.py +1 -1
  40. tests/test_pyversions_runtime.py +78 -1
  41. tests/test_relocate.py +86 -1
  42. tests/test_relocate_module.py +1 -1
  43. tests/test_relocate_tools.py +152 -0
  44. tests/test_runtime.py +73 -1
  45. tests/test_verify_build.py +10 -5
  46. relenv-0.22.1.dist-info/RECORD +0 -48
  47. {relenv-0.22.1.dist-info → relenv-0.22.3.dist-info}/entry_points.txt +0 -0
  48. {relenv-0.22.1.dist-info → relenv-0.22.3.dist-info}/licenses/LICENSE.md +0 -0
  49. {relenv-0.22.1.dist-info → relenv-0.22.3.dist-info}/licenses/NOTICE +0 -0
  50. {relenv-0.22.1.dist-info → relenv-0.22.3.dist-info}/top_level.txt +0 -0
relenv/relocate.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  """
4
4
  A script to ensure the proper rpaths are in place for the relenv environment.
@@ -11,6 +11,7 @@ import os as _os
11
11
  import pathlib
12
12
  import shutil as _shutil
13
13
  import subprocess as _subprocess
14
+ import sys as _sys
14
15
  from typing import Optional
15
16
 
16
17
  log = logging.getLogger(__name__)
@@ -18,6 +19,7 @@ log = logging.getLogger(__name__)
18
19
  os = _os
19
20
  shutil = _shutil
20
21
  subprocess = _subprocess
22
+ sys = _sys
21
23
 
22
24
  __all__ = [
23
25
  "is_macho",
@@ -70,6 +72,83 @@ LC_ID_DYLIB = "LC_ID_DYLIB"
70
72
  LC_LOAD_DYLIB = "LC_LOAD_DYLIB"
71
73
  LC_RPATH = "LC_RPATH"
72
74
 
75
+ # Cache for readelf binary path
76
+ _READELF_BINARY: Optional[str] = None
77
+
78
+ # Cache for patchelf binary path
79
+ _PATCHELF_BINARY: Optional[str] = None
80
+
81
+
82
+ def _get_readelf_binary() -> str:
83
+ """
84
+ Get the path to readelf binary, preferring toolchain version.
85
+
86
+ Returns the cached value if already computed. On Linux, prefers the
87
+ toolchain's readelf over the system version. Falls back to "readelf"
88
+ from PATH if toolchain is unavailable.
89
+
90
+ :return: Path to readelf binary
91
+ :rtype: str
92
+ """
93
+ global _READELF_BINARY
94
+ if _READELF_BINARY is not None:
95
+ return _READELF_BINARY
96
+
97
+ # Only Linux has the toolchain with readelf
98
+ if sys.platform == "linux":
99
+ try:
100
+ from relenv.common import get_toolchain, get_triplet
101
+
102
+ toolchain = get_toolchain()
103
+ if toolchain:
104
+ triplet = get_triplet()
105
+ toolchain_readelf = toolchain / "bin" / f"{triplet}-readelf"
106
+ if toolchain_readelf.exists():
107
+ _READELF_BINARY = str(toolchain_readelf)
108
+ return _READELF_BINARY
109
+ except Exception:
110
+ # Fall through to system readelf
111
+ pass
112
+
113
+ # Fall back to system readelf
114
+ _READELF_BINARY = "readelf"
115
+ return _READELF_BINARY
116
+
117
+
118
+ def _get_patchelf_binary() -> str:
119
+ """
120
+ Get the path to patchelf binary, preferring toolchain version.
121
+
122
+ Returns the cached value if already computed. On Linux, prefers the
123
+ toolchain's patchelf over the system version. Falls back to "patchelf"
124
+ from PATH if toolchain is unavailable.
125
+
126
+ :return: Path to patchelf binary
127
+ :rtype: str
128
+ """
129
+ global _PATCHELF_BINARY
130
+ if _PATCHELF_BINARY is not None:
131
+ return _PATCHELF_BINARY
132
+
133
+ # Only Linux has the toolchain with patchelf
134
+ if sys.platform == "linux":
135
+ try:
136
+ from relenv.common import get_toolchain
137
+
138
+ toolchain = get_toolchain()
139
+ if toolchain:
140
+ toolchain_patchelf = toolchain / "bin" / "patchelf"
141
+ if toolchain_patchelf.exists():
142
+ _PATCHELF_BINARY = str(toolchain_patchelf)
143
+ return _PATCHELF_BINARY
144
+ except Exception:
145
+ # Fall through to system patchelf
146
+ pass
147
+
148
+ # Fall back to system patchelf
149
+ _PATCHELF_BINARY = "patchelf"
150
+ return _PATCHELF_BINARY
151
+
73
152
 
74
153
  def is_macho(path: str | os.PathLike[str]) -> bool:
75
154
  """
@@ -192,8 +271,9 @@ def parse_rpath(path: str | os.PathLike[str]) -> list[str]:
192
271
  :return: The RPATH's found.
193
272
  :rtype: list
194
273
  """
274
+ readelf = _get_readelf_binary()
195
275
  proc = subprocess.run(
196
- ["readelf", "-d", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE
276
+ [readelf, "-d", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE
197
277
  )
198
278
  return parse_readelf_d(proc.stdout.decode())
199
279
 
@@ -264,6 +344,35 @@ def is_in_dir(
264
344
  return os.path.realpath(filepath).startswith(os.path.realpath(directory) + os.sep)
265
345
 
266
346
 
347
+ def remove_rpath(path: str | os.PathLike[str]) -> bool:
348
+ """
349
+ Remove the rpath from a given ELF file.
350
+
351
+ :param path: The path to an ELF file
352
+ :type path: str
353
+
354
+ :return: True if successful, False otherwise
355
+ :rtype: bool
356
+ """
357
+ old_rpath = parse_rpath(path)
358
+ if not old_rpath:
359
+ # No RPATH to remove
360
+ return True
361
+
362
+ log.info("Remove RPATH from %s (was: %s)", path, old_rpath)
363
+ patchelf = _get_patchelf_binary()
364
+ proc = subprocess.run(
365
+ [patchelf, "--remove-rpath", path],
366
+ stderr=subprocess.PIPE,
367
+ stdout=subprocess.PIPE,
368
+ )
369
+
370
+ if proc.returncode:
371
+ log.error("Failed to remove RPATH from %s: %s", path, proc.stderr.decode())
372
+ return False
373
+ return True
374
+
375
+
267
376
  def patch_rpath(
268
377
  path: str | os.PathLike[str],
269
378
  new_rpath: str,
@@ -291,8 +400,9 @@ def patch_rpath(
291
400
  if new_rpath not in old_rpath:
292
401
  patched_rpath = ":".join([new_rpath] + old_rpath)
293
402
  log.info("Set RPATH=%s %s", patched_rpath, path)
403
+ patchelf = _get_patchelf_binary()
294
404
  proc = subprocess.run(
295
- ["patchelf", "--force-rpath", "--set-rpath", patched_rpath, path],
405
+ [patchelf, "--force-rpath", "--set-rpath", patched_rpath, path],
296
406
  stderr=subprocess.PIPE,
297
407
  stdout=subprocess.PIPE,
298
408
  )
@@ -325,6 +435,7 @@ def handle_elf(
325
435
  root = libs
326
436
  proc = subprocess.run(["ldd", path], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
327
437
  needs_rpath = False
438
+
328
439
  for line in proc.stdout.decode().splitlines():
329
440
  if line.find("=>") == -1:
330
441
  log.debug("Skip ldd output line: %s", line)
@@ -371,7 +482,9 @@ def handle_elf(
371
482
  log.info("Adjust rpath of %s to %s", path, relpath)
372
483
  patch_rpath(path, relpath)
373
484
  else:
374
- log.info("Do not adjust rpath of %s", path)
485
+ # No relenv libraries are linked, so RPATH is not needed
486
+ # Remove any existing RPATH to avoid security/correctness issues
487
+ remove_rpath(path)
375
488
 
376
489
 
377
490
  def main(
@@ -420,6 +533,7 @@ def main(
420
533
  if path in processed:
421
534
  continue
422
535
  log.debug("Checking %s", path)
536
+
423
537
  if is_macho(path):
424
538
  log.info("Found Mach-O %s", path)
425
539
  _ = handle_macho(path, libs_dir, rpath_only)
relenv/runtime.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  """
4
4
  This code is run when initializing the python interperter in a Relenv environment.
@@ -384,25 +384,30 @@ def install_wheel_wrapper(func: Callable[..., Any]) -> Callable[..., Any]:
384
384
  direct_url,
385
385
  requested,
386
386
  )
387
- plat = pathlib.Path(scheme.platlib)
388
- rootdir = relenv_root()
389
- with open(plat / info_dir / "RECORD") as fp:
390
- for line in fp.readlines():
391
- file = plat / line.split(",", 1)[0]
392
- if not file.exists():
393
- debug(f"Relenv - File not found {file}")
394
- continue
395
- if relocate().is_elf(file):
396
- debug(f"Relenv - Found elf {file}")
397
- relocate().handle_elf(plat / file, rootdir / "lib", True, rootdir)
398
- elif relocate().is_macho(file):
399
- otool_bin = shutil.which("otool")
400
- if otool_bin:
401
- relocate().handle_macho(str(plat / file), str(rootdir), True)
402
- else:
403
- debug(
404
- "The otool command is not available, please run `xcode-select --install`"
387
+ if "RELENV_BUILDENV" in os.environ:
388
+ plat = pathlib.Path(scheme.platlib)
389
+ rootdir = relenv_root()
390
+ with open(plat / info_dir / "RECORD") as fp:
391
+ for line in fp.readlines():
392
+ file = plat / line.split(",", 1)[0]
393
+ if not file.exists():
394
+ debug(f"Relenv - File not found {file}")
395
+ continue
396
+ if relocate().is_elf(file):
397
+ debug(f"Relenv - Found elf {file}")
398
+ relocate().handle_elf(
399
+ plat / file, rootdir / "lib", True, rootdir
405
400
  )
401
+ elif relocate().is_macho(file):
402
+ otool_bin = shutil.which("otool")
403
+ if otool_bin:
404
+ relocate().handle_macho(
405
+ str(plat / file), str(rootdir), True
406
+ )
407
+ else:
408
+ debug(
409
+ "The otool command is not available, please run `xcode-select --install`"
410
+ )
406
411
 
407
412
  return wrapper
408
413
 
@@ -1022,7 +1027,11 @@ def install_cargo_config() -> None:
1022
1027
  cargo_home = dirs.data / "cargo"
1023
1028
  triplet = common().get_triplet()
1024
1029
 
1025
- toolchain = common().get_toolchain()
1030
+ try:
1031
+ toolchain = common().get_toolchain()
1032
+ except PermissionError:
1033
+ pass
1034
+
1026
1035
  if not toolchain:
1027
1036
  debug("Unable to set CARGO_HOME ppbt package not installed")
1028
1037
  return
relenv/toolchain.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  """
4
4
  The ``relenv toolchain`` command.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: relenv
3
- Version: 0.22.1
3
+ Version: 0.22.3
4
4
  Project-URL: Source Code, https://github.com/saltstack/relative-environment-for-python
5
5
  Project-URL: Documentation, https://relenv.readthedocs.io/en/latest/
6
6
  Project-URL: Changelog, https://relenv.readthedocs.io/en/latest/changelog.html
@@ -0,0 +1,51 @@
1
+ relenv/__init__.py,sha256=LmJZKtCS7hgyl9VIIT4qiIXj8YX1fJKY0qJUi2UtwgA,325
2
+ relenv/__main__.py,sha256=gLR88_GYmdpyuppoaCq-W0SPetEOQFisQglcQZSGROg,1538
3
+ relenv/buildenv.py,sha256=I7ZyWxO6RZveaCQtqwBPpUW1XdMLwmuPfww5dfzRgwM,4120
4
+ relenv/check.py,sha256=m3KkTS6LQkvRwexVqqGQ4_A7GQU-dAmcF8EQDu_K67c,1133
5
+ relenv/common.py,sha256=zkNmhZr-Dp9-aMPSO2FNiF9vrDqGOCVQgDjIClaDalA,35826
6
+ relenv/create.py,sha256=6Q3m1MkeYyeH48UM6wYhvTlhHRRA73ZA34LfvNe0Ylk,7934
7
+ relenv/fetch.py,sha256=cnkPJiPlttiWOm7Olt4R23RAu_IzzcUsq1H-yHoPbow,2791
8
+ relenv/manifest.py,sha256=xyMHWao_I3j7gCyweES3I50830efacxZI3KumuPfyvU,1057
9
+ relenv/python-versions.json,sha256=vRDqf2sKys1zCnJBzZCCtaUnn5d_Ow6-vj3XcX8y2hE,15827
10
+ relenv/pyversions.py,sha256=euMZcELZsM612c4D3PD9dad4ScFbh0S54dRnbf7BHJg,43032
11
+ relenv/relocate.py,sha256=7usm94iC68AsbFl4IJ6iL_6p4Cb_HUeygAdfLrelCwU,16503
12
+ relenv/runtime.py,sha256=BQ96USuzT04umQjlnDt0johYTsfSVg1IMrfVx8xyfdI,40326
13
+ relenv/toolchain.py,sha256=vbL79grOsThEsstYpx1fZxo3EuliDj5UHcsTpEgdWXo,929
14
+ relenv/_resources/xz/config.h,sha256=2IofvMqyAa8xBaQb6QGStiUugMmjyUCq_Of5drh7fro,4117
15
+ relenv/_resources/xz/crc32_table.c,sha256=L7Loi256KVn8QDuteZVGh5lhH2spTCIDOEc_7tnSJqs,641
16
+ relenv/_resources/xz/crc64_table.c,sha256=--hRinyLqzfn6qTTQfRYBu5osdtucwJjH-lFRE4_xzY,1058
17
+ relenv/_resources/xz/readme.md,sha256=Esbs4NhnCwCpCfk_iRc9AgeiKoqW--HnSME7THjYivA,577
18
+ relenv/_scripts/install_vc_build.ps1,sha256=ir1bcz7rNOuFw4E5_AqBidiKS0SpPViXW7zaanRPCoM,7629
19
+ relenv/build/__init__.py,sha256=0Gre5oainAdkw4Bfc54V4Dwrjz4a5_hiICeEil9D-Cw,6157
20
+ relenv/build/darwin.py,sha256=n1ff-NLFlcN3_fqeJUOPnBC6HODT5Y-gvWjQ3UpdXsE,7136
21
+ relenv/build/linux.py,sha256=UD1b-bTri9lF3iaoibOFO2vzF-6GpubO38_65hvmqtY,26161
22
+ relenv/build/windows.py,sha256=xJ6hoH-MjhMxkS6_Pj5piUGtX41tTRcLkHJuUPWsuQA,16508
23
+ relenv/build/common/__init__.py,sha256=PUAaLAv_1zIGJcGb77jGhSZ1PdawJF6DH4RMRKamh7U,1004
24
+ relenv/build/common/_sysconfigdata_template.py,sha256=NaJkmEKIj8Uhf7Cs82FyrMd2CyZVh-Ri8mE3Pm8jxo0,1955
25
+ relenv/build/common/builder.py,sha256=vFujUxp_cq-miBZD6RLS3yta7158DbiQM0zZg593IDQ,31000
26
+ relenv/build/common/builders.py,sha256=2ajiN3gjjIsQ6TFuEvUCzizL_XKnvx0F9qeuak658ho,4772
27
+ relenv/build/common/download.py,sha256=oFmwYhdRlWvQMPju5ByjCTEsbxmH8ENFXilnZKY7c6U,10479
28
+ relenv/build/common/install.py,sha256=nXFxncnJQKlaell3Ps3LnjmVyGpS8KVuRWSMWrdjJHA,19299
29
+ relenv/build/common/ui.py,sha256=FebuZVSioSHGzTUyOSepFRkZzKN-SxchjXsM5tTLApI,14192
30
+ relenv-0.22.3.dist-info/licenses/LICENSE.md,sha256=T0SRk3vJM1YcAJjDz9vsX9gsCRatAVSBS7LeU0tklRM,9919
31
+ relenv-0.22.3.dist-info/licenses/NOTICE,sha256=Ns0AybPHBsgJKJJfjE6YnGgWEQQ9F7lQ6QNlYLlQT3E,548
32
+ tests/__init__.py,sha256=FdZLbBoaMwwgwYwDI9ozQnuT7FHQ7ae8MhQrsP3tVyQ,70
33
+ tests/_pytest_typing.py,sha256=cOGrU7it8H-2F198AT3bHf1Un8K3PzYvQgDs2PCmFb8,1204
34
+ tests/conftest.py,sha256=tccQCx1DLwftaJCnNLrBkyOmj-a_13nvayUG0KEkSzg,2628
35
+ tests/test_build.py,sha256=WpcO5_C1hxflgz5B0j5mB4v6ZVYZe-t8VOQRiQUUJvI,14827
36
+ tests/test_common.py,sha256=CJPpgP0euIOTFatZ2GmrR-TrJ1d2Hv_W8iaVpkNcuAc,18008
37
+ tests/test_create.py,sha256=iW26qyApMHTfOVa_X2LFsWeBIaaGo1pk8m1xaSOhGdA,6849
38
+ tests/test_downloads.py,sha256=A4caZk4HUdMPPS83toxx7FM6sXy4etxMqeKcDDx-gNc,3528
39
+ tests/test_fips_photon.py,sha256=Y39E1LUndDn3_w4Ir9gkYwxAIHlH8fWPvDI1GRNFh_g,1397
40
+ tests/test_module_imports.py,sha256=erwMhcRK-HMFjmVQoewQSLBtjes2KpZfiFAXgvK_Nt4,1354
41
+ tests/test_pyversions_runtime.py,sha256=CTlGySCCLTuk1I4KFftr2S37gnPvpYubNun8oIRv5BU,8978
42
+ tests/test_relocate.py,sha256=eWgocPhQoU9zAItdLN9yp45j0ura6IPsCmmkuLOOCGU,13402
43
+ tests/test_relocate_module.py,sha256=LslZSILgSAgLZ_wSepIlTFqb7AIkXTNGLWiPiNh5ffE,7802
44
+ tests/test_relocate_tools.py,sha256=bOHFlkebeZFfH0ZF0rg2Agd0unI-7GFHxkzmPL3reIw,5622
45
+ tests/test_runtime.py,sha256=QBIiX89JBJHoNAGqghZFtH6iLzvtKbQyqonNzYWK7gE,74285
46
+ tests/test_verify_build.py,sha256=F34nBwEvUif7XTpLOTD9tSIGz2kzf3egvelpcajnLc0,67927
47
+ relenv-0.22.3.dist-info/METADATA,sha256=6vmSfsbuLB0DSSmXkCIYgzRN-znakGbKDRH3Ds08-a8,1360
48
+ relenv-0.22.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
49
+ relenv-0.22.3.dist-info/entry_points.txt,sha256=dO66nWPPWl8ALWWnZFlHKAo6mfPFuQid7purYWL2ddc,48
50
+ relenv-0.22.3.dist-info/top_level.txt,sha256=P4Ro6JLZE53ZdsQ76o2OzBcpb0MaVJmbfr0HAn9WF8M,13
51
+ relenv-0.22.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
tests/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
tests/_pytest_typing.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  """
4
4
  Typed helper wrappers for common pytest decorators so mypy understands them.
tests/conftest.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  #
4
4
  import logging
tests/test_build.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  import hashlib
4
4
  import logging
@@ -154,8 +154,8 @@ def test_get_dependency_version_sqlite_all_platforms() -> None:
154
154
  def test_get_dependency_version_xz_all_platforms() -> None:
155
155
  """Test getting XZ version for various platforms."""
156
156
  # XZ 5.5.0+ removed MSBuild support, so Windows uses a fallback version
157
- # and XZ is not in JSON for win32
158
- for platform in ["linux", "darwin"]:
157
+ # BUT we now have XZ 5.8.2 in python-versions.json for win32 too
158
+ for platform in ["linux", "darwin", "win32"]:
159
159
  result = get_dependency_version("xz", platform)
160
160
  assert result is not None, f"XZ should be available for {platform}"
161
161
  assert isinstance(result, dict)
@@ -166,10 +166,6 @@ def test_get_dependency_version_xz_all_platforms() -> None:
166
166
  assert "xz" in result["url"].lower()
167
167
  assert isinstance(result["sha256"], str)
168
168
 
169
- # Windows should return None (uses hardcoded fallback in windows.py)
170
- result = get_dependency_version("xz", "win32")
171
- assert result is None, "XZ should not be in JSON for win32 (uses fallback)"
172
-
173
169
 
174
170
  def test_get_dependency_version_nonexistent() -> None:
175
171
  """Test that nonexistent dependency returns None."""
tests/test_common.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  from __future__ import annotations
4
4
 
@@ -529,7 +529,7 @@ def test_toolchain_uses_cache_without_relenv_data(
529
529
  def test_copyright_headers() -> None:
530
530
  """Verify all Python source files have the correct copyright header."""
531
531
  expected_header = (
532
- "# Copyright 2022-2025 Broadcom.\n" "# SPDX-License-Identifier: Apache-2.0\n"
532
+ "# Copyright 2022-2026 Broadcom.\n" "# SPDX-License-Identifier: Apache-2.0\n"
533
533
  )
534
534
 
535
535
  # Find all Python files in relenv/ and tests/
tests/test_create.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  #
4
4
  import os
tests/test_downloads.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  import pathlib
4
4
  import subprocess
tests/test_fips_photon.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  #
4
4
  import os
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  #
4
4
  from __future__ import annotations
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  #
4
4
  from __future__ import annotations
@@ -175,3 +175,80 @@ def test_detect_xz_versions(monkeypatch: pytest.MonkeyPatch) -> None:
175
175
  assert "5.6.3" in versions
176
176
  # Verify sorting (latest first)
177
177
  assert versions[0] == "5.8.1"
178
+
179
+
180
+ def test_resolve_python_version_none_defaults_to_latest_310() -> None:
181
+ """Test that None resolves to the latest 3.10 version."""
182
+ result = pyversions.resolve_python_version(None)
183
+ assert result.startswith("3.10.")
184
+ # Verify it's a valid version in the registry
185
+ versions = pyversions.python_versions("3.10")
186
+ assert pyversions.Version(result) in versions
187
+ # Verify it's the latest 3.10 version
188
+ latest = sorted(list(versions.keys()))[-1]
189
+ assert result == str(latest)
190
+
191
+
192
+ def test_resolve_python_version_partial_minor() -> None:
193
+ """Test that partial versions (3.10) resolve to latest micro."""
194
+ result = pyversions.resolve_python_version("3.10")
195
+ assert result.startswith("3.10.")
196
+ # Verify it resolves to the latest micro version
197
+ versions = pyversions.python_versions("3.10")
198
+ latest = sorted(list(versions.keys()))[-1]
199
+ assert result == str(latest)
200
+
201
+
202
+ def test_resolve_python_version_different_minors() -> None:
203
+ """Test resolution works for different minor versions."""
204
+ result_311 = pyversions.resolve_python_version("3.11")
205
+ assert result_311.startswith("3.11.")
206
+
207
+ result_313 = pyversions.resolve_python_version("3.13")
208
+ assert result_313.startswith("3.13.")
209
+
210
+ # Verify they're different
211
+ assert result_311 != result_313
212
+
213
+ # Verify each is the latest for its minor version
214
+ versions_311 = pyversions.python_versions("3.11")
215
+ latest_311 = sorted(list(versions_311.keys()))[-1]
216
+ assert result_311 == str(latest_311)
217
+
218
+ versions_313 = pyversions.python_versions("3.13")
219
+ latest_313 = sorted(list(versions_313.keys()))[-1]
220
+ assert result_313 == str(latest_313)
221
+
222
+
223
+ def test_resolve_python_version_full_version() -> None:
224
+ """Test that full versions are validated and returned as-is."""
225
+ # Get any valid version from the registry
226
+ all_versions = pyversions.python_versions()
227
+ some_version = str(next(iter(all_versions)))
228
+
229
+ result = pyversions.resolve_python_version(some_version)
230
+ assert result == some_version
231
+
232
+
233
+ def test_resolve_python_version_invalid_full_version() -> None:
234
+ """Test that invalid full versions raise RuntimeError."""
235
+ with pytest.raises(RuntimeError, match="Unknown version"):
236
+ pyversions.resolve_python_version("3.10.999")
237
+
238
+
239
+ def test_resolve_python_version_invalid_minor_version() -> None:
240
+ """Test that invalid minor versions raise RuntimeError."""
241
+ with pytest.raises(RuntimeError, match="Unknown minor version"):
242
+ pyversions.resolve_python_version("3.99")
243
+
244
+
245
+ def test_resolve_python_version_consistency() -> None:
246
+ """Test that resolve_python_version is idempotent for full versions."""
247
+ # Get a valid full version from the registry
248
+ all_versions = pyversions.python_versions()
249
+ some_version = str(next(iter(all_versions)))
250
+
251
+ # Resolving a full version twice should give the same result
252
+ first = pyversions.resolve_python_version(some_version)
253
+ second = pyversions.resolve_python_version(first)
254
+ assert first == second
tests/test_relocate.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  import pathlib
4
4
  import shutil
@@ -16,6 +16,7 @@ from relenv.relocate import (
16
16
  main,
17
17
  parse_readelf_d,
18
18
  patch_rpath,
19
+ remove_rpath,
19
20
  )
20
21
 
21
22
  pytestmark = [
@@ -276,3 +277,87 @@ def test_handle_elf_rpath_only(tmp_path: pathlib.Path) -> None:
276
277
  assert not (proj.libs_dir / "fake.so.2").exists()
277
278
  assert patch_rpath_mock.call_count == 1
278
279
  patch_rpath_mock.assert_called_with(str(pybin), "$ORIGIN/../lib")
280
+
281
+
282
+ def test_remove_rpath_with_existing_rpath(tmp_path: pathlib.Path) -> None:
283
+ """Test that remove_rpath removes an existing RPATH."""
284
+ path = str(tmp_path / "test.so")
285
+ with patch("subprocess.run", return_value=MagicMock(returncode=0)):
286
+ with patch(
287
+ "relenv.relocate.parse_rpath",
288
+ return_value=["/some/absolute/path"],
289
+ ):
290
+ assert remove_rpath(path) is True
291
+
292
+
293
+ def test_remove_rpath_no_existing_rpath(tmp_path: pathlib.Path) -> None:
294
+ """Test that remove_rpath succeeds when there's no RPATH to remove."""
295
+ path = str(tmp_path / "test.so")
296
+ with patch("relenv.relocate.parse_rpath", return_value=[]):
297
+ assert remove_rpath(path) is True
298
+
299
+
300
+ def test_remove_rpath_failed(tmp_path: pathlib.Path) -> None:
301
+ """Test that remove_rpath returns False when patchelf fails."""
302
+ path = str(tmp_path / "test.so")
303
+ with patch("subprocess.run", return_value=MagicMock(returncode=1)):
304
+ with patch(
305
+ "relenv.relocate.parse_rpath",
306
+ return_value=["/some/absolute/path"],
307
+ ):
308
+ assert remove_rpath(path) is False
309
+
310
+
311
+ def test_handle_elf_removes_rpath_when_no_relenv_libs(tmp_path: pathlib.Path) -> None:
312
+ """Test that handle_elf removes RPATH for binaries linking only to system libs."""
313
+ proj = LinuxProject(tmp_path / "proj")
314
+ module = proj.add_simple_elf("array.so", "lib", "python3.10", "lib-dynload")
315
+
316
+ # ldd output showing only system libraries
317
+ ldd_ret = """
318
+ linux-vdso.so.1 => linux-vdso.so.1 (0x0123456789)
319
+ libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x0123456789)
320
+ libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0123456789)
321
+ """.encode()
322
+
323
+ with proj:
324
+ with patch("subprocess.run", return_value=MagicMock(stdout=ldd_ret)):
325
+ with patch("relenv.relocate.remove_rpath") as remove_rpath_mock:
326
+ with patch("relenv.relocate.patch_rpath") as patch_rpath_mock:
327
+ handle_elf(
328
+ str(module), str(proj.libs_dir), True, str(proj.root_dir)
329
+ )
330
+ # Should remove RPATH, not patch it
331
+ assert remove_rpath_mock.call_count == 1
332
+ assert patch_rpath_mock.call_count == 0
333
+ remove_rpath_mock.assert_called_with(str(module))
334
+
335
+
336
+ def test_handle_elf_sets_rpath_when_relenv_libs_present(tmp_path: pathlib.Path) -> None:
337
+ """Test that handle_elf sets RPATH for binaries linking to relenv libs."""
338
+ proj = LinuxProject(tmp_path / "proj")
339
+ module = proj.add_simple_elf("_ssl.so", "lib", "python3.10", "lib-dynload")
340
+ libssl = proj.libs_dir / "libssl.so.3"
341
+ libssl.touch()
342
+
343
+ # ldd output showing relenv-built library
344
+ ldd_ret = """
345
+ linux-vdso.so.1 => linux-vdso.so.1 (0x0123456789)
346
+ libssl.so.3 => {libssl} (0x0123456789)
347
+ libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x0123456789)
348
+ libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0123456789)
349
+ """.format(
350
+ libssl=libssl
351
+ ).encode()
352
+
353
+ with proj:
354
+ with patch("subprocess.run", return_value=MagicMock(stdout=ldd_ret)):
355
+ with patch("relenv.relocate.remove_rpath") as remove_rpath_mock:
356
+ with patch("relenv.relocate.patch_rpath") as patch_rpath_mock:
357
+ handle_elf(
358
+ str(module), str(proj.libs_dir), True, str(proj.root_dir)
359
+ )
360
+ # Should patch RPATH, not remove it
361
+ assert patch_rpath_mock.call_count == 1
362
+ assert remove_rpath_mock.call_count == 0
363
+ patch_rpath_mock.assert_called_with(str(module), "$ORIGIN/../..")
@@ -1,4 +1,4 @@
1
- # Copyright 2022-2025 Broadcom.
1
+ # Copyright 2022-2026 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  #
4
4
  from __future__ import annotations