relenv 0.22.0__py3-none-any.whl → 0.22.2__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 (46) hide show
  1. relenv/__init__.py +1 -1
  2. relenv/__main__.py +1 -1
  3. relenv/build/__init__.py +21 -25
  4. relenv/build/common/__init__.py +1 -1
  5. relenv/build/common/_sysconfigdata_template.py +1 -1
  6. relenv/build/common/builder.py +1 -1
  7. relenv/build/common/builders.py +1 -1
  8. relenv/build/common/download.py +1 -1
  9. relenv/build/common/install.py +6 -2
  10. relenv/build/common/ui.py +1 -1
  11. relenv/build/darwin.py +1 -1
  12. relenv/build/linux.py +1 -1
  13. relenv/build/windows.py +1 -1
  14. relenv/buildenv.py +1 -1
  15. relenv/check.py +1 -1
  16. relenv/common.py +5 -4
  17. relenv/create.py +19 -27
  18. relenv/fetch.py +7 -5
  19. relenv/manifest.py +1 -1
  20. relenv/python-versions.json +347 -326
  21. relenv/pyversions.py +49 -1
  22. relenv/relocate.py +34 -2
  23. relenv/runtime.py +6 -1
  24. relenv/toolchain.py +1 -1
  25. {relenv-0.22.0.dist-info → relenv-0.22.2.dist-info}/METADATA +1 -1
  26. relenv-0.22.2.dist-info/RECORD +48 -0
  27. tests/__init__.py +1 -1
  28. tests/_pytest_typing.py +1 -1
  29. tests/conftest.py +1 -1
  30. tests/test_build.py +1 -1
  31. tests/test_common.py +64 -2
  32. tests/test_create.py +1 -1
  33. tests/test_downloads.py +1 -1
  34. tests/test_fips_photon.py +1 -1
  35. tests/test_module_imports.py +1 -1
  36. tests/test_pyversions_runtime.py +78 -1
  37. tests/test_relocate.py +86 -1
  38. tests/test_relocate_module.py +1 -1
  39. tests/test_runtime.py +167 -1
  40. tests/test_verify_build.py +7 -5
  41. relenv-0.22.0.dist-info/RECORD +0 -48
  42. {relenv-0.22.0.dist-info → relenv-0.22.2.dist-info}/WHEEL +0 -0
  43. {relenv-0.22.0.dist-info → relenv-0.22.2.dist-info}/entry_points.txt +0 -0
  44. {relenv-0.22.0.dist-info → relenv-0.22.2.dist-info}/licenses/LICENSE.md +0 -0
  45. {relenv-0.22.0.dist-info → relenv-0.22.2.dist-info}/licenses/NOTICE +0 -0
  46. {relenv-0.22.0.dist-info → relenv-0.22.2.dist-info}/top_level.txt +0 -0
relenv/pyversions.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
  Versions utility.
@@ -947,6 +947,54 @@ def python_versions(
947
947
  return {version: pyversions[str(version)] for version in versions}
948
948
 
949
949
 
950
+ def get_default_python_version() -> str:
951
+ """
952
+ Get the default Python version to use when none is specified.
953
+
954
+ :return: The default Python version string (e.g., "3.10.19")
955
+ """
956
+ # Default to latest 3.10 version
957
+ pyversions = python_versions("3.10")
958
+ if not pyversions:
959
+ raise RuntimeError("No 3.10 versions found")
960
+ latest = sorted(list(pyversions.keys()))[-1]
961
+ return str(latest)
962
+
963
+
964
+ def resolve_python_version(version_spec: str | None = None) -> str:
965
+ """
966
+ Resolve a Python version specification to a full version string.
967
+
968
+ If version_spec is None, returns the latest Python 3.10 version.
969
+ If version_spec is partial (e.g., "3.10"), returns the latest micro version.
970
+ If version_spec is full (e.g., "3.10.19"), returns it as-is after validation.
971
+
972
+ :param version_spec: Version specification (None, "3.10", or "3.10.19")
973
+ :return: Full version string (e.g., "3.10.19")
974
+ :raises RuntimeError: If the version is not found
975
+ """
976
+ if version_spec is None:
977
+ # Default to latest 3.10 version
978
+ return get_default_python_version()
979
+
980
+ requested = Version(version_spec)
981
+
982
+ if requested.micro is not None:
983
+ # Full version specified - validate it exists
984
+ pyversions = python_versions()
985
+ if requested not in pyversions:
986
+ raise RuntimeError(f"Unknown version {requested}")
987
+ return str(requested)
988
+ else:
989
+ # Partial version (major.minor) - get latest micro
990
+ pyversions = python_versions(version_spec)
991
+ if not pyversions:
992
+ raise RuntimeError(f"Unknown minor version {requested}")
993
+ # Return the latest version for this major.minor
994
+ latest = sorted(list(pyversions.keys()))[-1]
995
+ return str(latest)
996
+
997
+
950
998
  def setup_parser(
951
999
  subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
952
1000
  ) -> None:
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.
@@ -264,6 +264,34 @@ def is_in_dir(
264
264
  return os.path.realpath(filepath).startswith(os.path.realpath(directory) + os.sep)
265
265
 
266
266
 
267
+ def remove_rpath(path: str | os.PathLike[str]) -> bool:
268
+ """
269
+ Remove the rpath from a given ELF file.
270
+
271
+ :param path: The path to an ELF file
272
+ :type path: str
273
+
274
+ :return: True if successful, False otherwise
275
+ :rtype: bool
276
+ """
277
+ old_rpath = parse_rpath(path)
278
+ if not old_rpath:
279
+ # No RPATH to remove
280
+ return True
281
+
282
+ log.info("Remove RPATH from %s (was: %s)", path, old_rpath)
283
+ proc = subprocess.run(
284
+ ["patchelf", "--remove-rpath", path],
285
+ stderr=subprocess.PIPE,
286
+ stdout=subprocess.PIPE,
287
+ )
288
+
289
+ if proc.returncode:
290
+ log.error("Failed to remove RPATH from %s: %s", path, proc.stderr.decode())
291
+ return False
292
+ return True
293
+
294
+
267
295
  def patch_rpath(
268
296
  path: str | os.PathLike[str],
269
297
  new_rpath: str,
@@ -325,6 +353,7 @@ def handle_elf(
325
353
  root = libs
326
354
  proc = subprocess.run(["ldd", path], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
327
355
  needs_rpath = False
356
+
328
357
  for line in proc.stdout.decode().splitlines():
329
358
  if line.find("=>") == -1:
330
359
  log.debug("Skip ldd output line: %s", line)
@@ -371,7 +400,9 @@ def handle_elf(
371
400
  log.info("Adjust rpath of %s to %s", path, relpath)
372
401
  patch_rpath(path, relpath)
373
402
  else:
374
- log.info("Do not adjust rpath of %s", path)
403
+ # No relenv libraries are linked, so RPATH is not needed
404
+ # Remove any existing RPATH to avoid security/correctness issues
405
+ remove_rpath(path)
375
406
 
376
407
 
377
408
  def main(
@@ -420,6 +451,7 @@ def main(
420
451
  if path in processed:
421
452
  continue
422
453
  log.debug("Checking %s", path)
454
+
423
455
  if is_macho(path):
424
456
  log.info("Found Mach-O %s", path)
425
457
  _ = 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.
@@ -1270,3 +1270,8 @@ def bootstrap() -> None:
1270
1270
  setup_crossroot()
1271
1271
  install_cargo_config()
1272
1272
  sys.meta_path = [importer] + sys.meta_path
1273
+
1274
+ # For Python 3.13+, sysconfig became a package so the importer doesn't
1275
+ # intercept it. Manually wrap it here.
1276
+ if sys.version_info >= (3, 13):
1277
+ wrap_sysconfig("sysconfig")
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.0
3
+ Version: 0.22.2
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,48 @@
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=ZAOmSh74HYx9CAYA5mnz1EHqsDEa_pwjfctiyt44x00,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=axX8aDQhLwdQn55WRYc5JbQd9_qV9yw6Syue-7ieWh8,14571
10
+ relenv/pyversions.py,sha256=euMZcELZsM612c4D3PD9dad4ScFbh0S54dRnbf7BHJg,43032
11
+ relenv/relocate.py,sha256=gsLMbgvCYqHXGZ5ZDBUsvVEYrefken0D5v9bJ93j-qA,14003
12
+ relenv/runtime.py,sha256=-3CE8v_g6flBHqldMmYM4XCTGAYbDuui3JX-o6dI93M,40035
13
+ relenv/toolchain.py,sha256=vbL79grOsThEsstYpx1fZxo3EuliDj5UHcsTpEgdWXo,929
14
+ relenv/_resources/xz/config.h,sha256=2IofvMqyAa8xBaQb6QGStiUugMmjyUCq_Of5drh7fro,4117
15
+ relenv/_resources/xz/readme.md,sha256=aOs7hz9RhTnTwHUt6gbO-2Sl0xLKIJp8-w6Du4VIFgY,258
16
+ relenv/_scripts/install_vc_build.ps1,sha256=ir1bcz7rNOuFw4E5_AqBidiKS0SpPViXW7zaanRPCoM,7629
17
+ relenv/build/__init__.py,sha256=0Gre5oainAdkw4Bfc54V4Dwrjz4a5_hiICeEil9D-Cw,6157
18
+ relenv/build/darwin.py,sha256=n1ff-NLFlcN3_fqeJUOPnBC6HODT5Y-gvWjQ3UpdXsE,7136
19
+ relenv/build/linux.py,sha256=UD1b-bTri9lF3iaoibOFO2vzF-6GpubO38_65hvmqtY,26161
20
+ relenv/build/windows.py,sha256=-HO2MIxI3P8u3lde92KRBOM-zP4y9zyy8Xp7pULd3Es,16102
21
+ relenv/build/common/__init__.py,sha256=PUAaLAv_1zIGJcGb77jGhSZ1PdawJF6DH4RMRKamh7U,1004
22
+ relenv/build/common/_sysconfigdata_template.py,sha256=NaJkmEKIj8Uhf7Cs82FyrMd2CyZVh-Ri8mE3Pm8jxo0,1955
23
+ relenv/build/common/builder.py,sha256=vFujUxp_cq-miBZD6RLS3yta7158DbiQM0zZg593IDQ,31000
24
+ relenv/build/common/builders.py,sha256=2ajiN3gjjIsQ6TFuEvUCzizL_XKnvx0F9qeuak658ho,4772
25
+ relenv/build/common/download.py,sha256=oFmwYhdRlWvQMPju5ByjCTEsbxmH8ENFXilnZKY7c6U,10479
26
+ relenv/build/common/install.py,sha256=nXFxncnJQKlaell3Ps3LnjmVyGpS8KVuRWSMWrdjJHA,19299
27
+ relenv/build/common/ui.py,sha256=FebuZVSioSHGzTUyOSepFRkZzKN-SxchjXsM5tTLApI,14192
28
+ relenv-0.22.2.dist-info/licenses/LICENSE.md,sha256=T0SRk3vJM1YcAJjDz9vsX9gsCRatAVSBS7LeU0tklRM,9919
29
+ relenv-0.22.2.dist-info/licenses/NOTICE,sha256=Ns0AybPHBsgJKJJfjE6YnGgWEQQ9F7lQ6QNlYLlQT3E,548
30
+ tests/__init__.py,sha256=FdZLbBoaMwwgwYwDI9ozQnuT7FHQ7ae8MhQrsP3tVyQ,70
31
+ tests/_pytest_typing.py,sha256=cOGrU7it8H-2F198AT3bHf1Un8K3PzYvQgDs2PCmFb8,1204
32
+ tests/conftest.py,sha256=tccQCx1DLwftaJCnNLrBkyOmj-a_13nvayUG0KEkSzg,2628
33
+ tests/test_build.py,sha256=KR1jci5fWyA3yWAavnh3QowdPyYF_b0IURkhoe4YA0E,14992
34
+ tests/test_common.py,sha256=CJPpgP0euIOTFatZ2GmrR-TrJ1d2Hv_W8iaVpkNcuAc,18008
35
+ tests/test_create.py,sha256=iW26qyApMHTfOVa_X2LFsWeBIaaGo1pk8m1xaSOhGdA,6849
36
+ tests/test_downloads.py,sha256=A4caZk4HUdMPPS83toxx7FM6sXy4etxMqeKcDDx-gNc,3528
37
+ tests/test_fips_photon.py,sha256=Y39E1LUndDn3_w4Ir9gkYwxAIHlH8fWPvDI1GRNFh_g,1397
38
+ tests/test_module_imports.py,sha256=erwMhcRK-HMFjmVQoewQSLBtjes2KpZfiFAXgvK_Nt4,1354
39
+ tests/test_pyversions_runtime.py,sha256=CTlGySCCLTuk1I4KFftr2S37gnPvpYubNun8oIRv5BU,8978
40
+ tests/test_relocate.py,sha256=eWgocPhQoU9zAItdLN9yp45j0ura6IPsCmmkuLOOCGU,13402
41
+ tests/test_relocate_module.py,sha256=LslZSILgSAgLZ_wSepIlTFqb7AIkXTNGLWiPiNh5ffE,7802
42
+ tests/test_runtime.py,sha256=5anHdx92Je38XBb-_rDxwtAatgLk7U843s_ZFQ2YzTA,71990
43
+ tests/test_verify_build.py,sha256=2PyWNedqgc4RcvmicLhSnWr1GoFbVKh4W9tWUmPw_iQ,67827
44
+ relenv-0.22.2.dist-info/METADATA,sha256=WyqgcOFBqd_9VISxlNR-1Ga2gqyvM41fl0JYkHb6yvg,1360
45
+ relenv-0.22.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
+ relenv-0.22.2.dist-info/entry_points.txt,sha256=dO66nWPPWl8ALWWnZFlHKAo6mfPFuQid7purYWL2ddc,48
47
+ relenv-0.22.2.dist-info/top_level.txt,sha256=P4Ro6JLZE53ZdsQ76o2OzBcpb0MaVJmbfr0HAn9WF8M,13
48
+ relenv-0.22.2.dist-info/RECORD,,
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
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
 
@@ -464,10 +464,72 @@ def test_makepath_oserror() -> None:
464
464
  assert case == os.path.normcase(expected)
465
465
 
466
466
 
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 environment variable
490
+ monkeypatch.setenv("RELENV_DATA", str(data_dir))
491
+
492
+ # Patch DATA_DIR to reflect the new RELENV_DATA value
493
+ monkeypatch.setattr(relenv.common, "DATA_DIR", data_dir)
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
+ from relenv.common import toolchain_root_dir
524
+
525
+ result = toolchain_root_dir()
526
+ assert result == cache_dir
527
+
528
+
467
529
  def test_copyright_headers() -> None:
468
530
  """Verify all Python source files have the correct copyright header."""
469
531
  expected_header = (
470
- "# 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"
471
533
  )
472
534
 
473
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