relenv 0.21.2__py3-none-any.whl → 0.22.1__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 +492 -165
  19. relenv/create.py +147 -7
  20. relenv/fetch.py +16 -4
  21. relenv/manifest.py +15 -7
  22. relenv/python-versions.json +350 -0
  23. relenv/pyversions.py +817 -30
  24. relenv/relocate.py +101 -55
  25. relenv/runtime.py +457 -282
  26. relenv/toolchain.py +9 -3
  27. {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/METADATA +1 -1
  28. relenv-0.22.1.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 +373 -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 +1968 -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.1.dist-info}/WHEEL +0 -0
  46. {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/entry_points.txt +0 -0
  47. {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/licenses/LICENSE.md +0 -0
  48. {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/licenses/NOTICE +0 -0
  49. {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/top_level.txt +0 -0
tests/test_runtime.py CHANGED
@@ -1,27 +1,1989 @@
1
- # Copyright 2023-2025 Broadcom.
1
+ # Copyright 2022-2025 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  #
4
+ from __future__ import annotations
5
+
4
6
  import importlib
7
+ import json
8
+ import os
9
+ import pathlib
5
10
  import sys
11
+ from types import ModuleType, SimpleNamespace
12
+ from typing import Optional
13
+
14
+ import pytest
6
15
 
7
16
  import relenv.runtime
8
17
 
18
+ # mypy: ignore-errors
19
+
20
+
21
+ def _raise(exc: Exception):
22
+ raise exc
23
+
24
+
25
+ def test_path_import_failure(
26
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
27
+ ) -> None:
28
+ monkeypatch.setattr(
29
+ importlib.util, "spec_from_file_location", lambda *args, **kwargs: None
30
+ )
31
+ with pytest.raises(ImportError):
32
+ relenv.runtime.path_import("demo", tmp_path / "demo.py")
33
+
34
+
35
+ def test_path_import_success(tmp_path: pathlib.Path) -> None:
36
+ module_file = tmp_path / "mod.py"
37
+ module_file.write_text("value = 42\n")
38
+ mod = relenv.runtime.path_import("temp_mod", module_file)
39
+ assert mod.value == 42
40
+ assert sys.modules["temp_mod"] is mod
41
+
42
+
43
+ def test_debug_print(
44
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
45
+ ) -> None:
46
+ monkeypatch.setenv("RELENV_DEBUG", "1")
47
+ relenv.runtime.debug("hello")
48
+ out = capsys.readouterr().out
49
+ assert "hello" in out
50
+ monkeypatch.delenv("RELENV_DEBUG", raising=False)
51
+
52
+
53
+ def test_pushd_changes_directory(tmp_path: pathlib.Path) -> None:
54
+ original = os.getcwd()
55
+ with relenv.runtime.pushd(tmp_path):
56
+ assert os.getcwd() == str(tmp_path)
57
+ assert os.getcwd() == original
58
+
9
59
 
10
- def test_importer():
11
- def mywrapper(name):
60
+ def test_relenv_root_windows(monkeypatch: pytest.MonkeyPatch) -> None:
61
+ module_dir = pathlib.Path(relenv.runtime.__file__).resolve().parent
62
+ fake_sys = SimpleNamespace(platform="win32")
63
+ monkeypatch.setattr(relenv.runtime, "sys", fake_sys)
64
+ expected = module_dir.parent.parent.parent
65
+ assert relenv.runtime.relenv_root() == expected
66
+
67
+
68
+ def test_get_major_version() -> None:
69
+ result = relenv.runtime.get_major_version()
70
+ major, minor = result.split(".")
71
+ assert major.isdigit() and minor.isdigit()
72
+
73
+
74
+ def test_importer() -> None:
75
+ def mywrapper(name: str) -> ModuleType:
12
76
  mod = importlib.import_module(name)
13
- mod.__test_case__ = True
77
+ mod.__test_case__ = True # type: ignore[attr-defined]
14
78
  return mod
15
79
 
16
80
  importer = relenv.runtime.RelenvImporter(
17
81
  wrappers=[
18
- relenv.runtime.Wrapper("pip._internal.locations", mywrapper),
82
+ relenv.runtime.Wrapper(
83
+ "pip._internal.locations",
84
+ mywrapper,
85
+ ),
19
86
  ]
20
87
  )
21
88
 
22
89
  sys.meta_path = [importer] + sys.meta_path
23
90
 
24
- import pip._internal.locations
91
+ import pip._internal.locations # type: ignore[import]
25
92
 
26
93
  assert hasattr(pip._internal.locations, "__test_case__")
27
94
  assert pip._internal.locations.__test_case__ is True
95
+
96
+
97
+ def test_set_env_if_not_set(
98
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
99
+ ) -> None:
100
+ env_name = "RELENV_TEST_ENV"
101
+ monkeypatch.delenv(env_name, raising=False)
102
+ relenv.runtime.set_env_if_not_set(env_name, "value")
103
+ assert os.environ[env_name] == "value"
104
+
105
+ monkeypatch.setenv(env_name, "other")
106
+ relenv.runtime.set_env_if_not_set(env_name, "value")
107
+ captured = capsys.readouterr()
108
+ assert "Warning:" in captured.out
109
+
110
+
111
+ def test_get_config_var_wrapper_with_env(monkeypatch: pytest.MonkeyPatch) -> None:
112
+ monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: pathlib.Path("/root"))
113
+ monkeypatch.setenv("RELENV_PIP_DIR", "1")
114
+ wrapped = relenv.runtime.get_config_var_wrapper(lambda name: "/orig")
115
+ assert wrapped("BINDIR") == pathlib.Path("/root")
116
+ monkeypatch.delenv("RELENV_PIP_DIR", raising=False)
117
+
118
+
119
+ def test_system_sysconfig_uses_system_python(monkeypatch: pytest.MonkeyPatch) -> None:
120
+ monkeypatch.setattr(relenv.runtime, "_SYSTEM_CONFIG_VARS", None, raising=False)
121
+
122
+ original_exists = pathlib.Path.exists
123
+
124
+ def fake_exists(path: pathlib.Path) -> bool:
125
+ return str(path) == "/usr/bin/python3"
126
+
127
+ monkeypatch.setattr(pathlib.Path, "exists", fake_exists)
128
+ expected = {"AR": "ar"}
129
+ completed = SimpleNamespace(stdout=json.dumps(expected).encode(), returncode=0)
130
+ monkeypatch.setattr(
131
+ relenv.runtime.subprocess, "run", lambda *args, **kwargs: completed
132
+ )
133
+
134
+ result = relenv.runtime.system_sysconfig()
135
+ assert result["AR"] == "ar"
136
+
137
+ monkeypatch.setattr(pathlib.Path, "exists", original_exists)
138
+
139
+
140
+ def test_system_sysconfig_cached(monkeypatch: pytest.MonkeyPatch) -> None:
141
+ cache = {"AR": "cached"}
142
+ monkeypatch.setattr(relenv.runtime, "_SYSTEM_CONFIG_VARS", cache, raising=False)
143
+ result = relenv.runtime.system_sysconfig()
144
+ assert result is cache
145
+ monkeypatch.setattr(relenv.runtime, "_SYSTEM_CONFIG_VARS", None, raising=False)
146
+
147
+
148
+ def test_system_sysconfig_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
149
+ monkeypatch.setattr(relenv.runtime, "_SYSTEM_CONFIG_VARS", None, raising=False)
150
+ monkeypatch.setattr(pathlib.Path, "exists", lambda _path: False)
151
+ result = relenv.runtime.system_sysconfig()
152
+ assert result == relenv.runtime.CONFIG_VARS_DEFAULTS
153
+
154
+
155
+ def test_install_cargo_config_creates_file(
156
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
157
+ ) -> None:
158
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux")
159
+ data_dir = tmp_path / "data"
160
+ data_dir.mkdir()
161
+ toolchain_dir = tmp_path / "toolchain" / "x86_64-linux-gnu"
162
+ (toolchain_dir / "sysroot" / "lib").mkdir(parents=True)
163
+ (toolchain_dir / "bin").mkdir(parents=True)
164
+ (toolchain_dir / "bin" / "x86_64-linux-gnu-gcc").touch()
165
+
166
+ class StubDirs:
167
+ def __init__(self, data: pathlib.Path) -> None:
168
+ self.data = data
169
+
170
+ stub_dirs = StubDirs(data_dir)
171
+ stub_common = SimpleNamespace(
172
+ DATA_DIR=tmp_path,
173
+ work_dirs=lambda: stub_dirs,
174
+ get_triplet=lambda: "x86_64-linux-gnu",
175
+ get_toolchain=lambda: toolchain_dir,
176
+ )
177
+
178
+ monkeypatch.setattr(relenv.runtime, "common", lambda: stub_common)
179
+ relenv.runtime.install_cargo_config()
180
+ config_path = data_dir / "cargo" / "config.toml"
181
+ assert config_path.exists()
182
+ assert "x86_64-unknown-linux-gnu" in config_path.read_text()
183
+
184
+
185
+ def test_build_shebang_value_error(monkeypatch: pytest.MonkeyPatch) -> None:
186
+ monkeypatch.setattr(
187
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
188
+ )
189
+ monkeypatch.setattr(
190
+ relenv.runtime,
191
+ "common",
192
+ lambda: SimpleNamespace(
193
+ relative_interpreter=lambda *args, **kwargs: _raise(ValueError("boom"))
194
+ ),
195
+ )
196
+
197
+ called = {"count": 0}
198
+
199
+ def original(self: object, *args: object, **kwargs: object) -> bytes: # type: ignore[override]
200
+ called["count"] += 1
201
+ return b"orig"
202
+
203
+ wrapped = relenv.runtime._build_shebang(original)
204
+ result = wrapped(SimpleNamespace(target_dir="/tmp/dir"))
205
+ assert result == b"orig"
206
+ assert called["count"] == 1
207
+
208
+
209
+ def test_build_shebang_windows(monkeypatch: pytest.MonkeyPatch) -> None:
210
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "win32", raising=False)
211
+ monkeypatch.setattr(
212
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
213
+ )
214
+ monkeypatch.setattr(
215
+ relenv.runtime,
216
+ "common",
217
+ lambda: SimpleNamespace(
218
+ relative_interpreter=lambda *args: pathlib.Path("python.exe")
219
+ ),
220
+ )
221
+
222
+ def original(self: object) -> bytes: # type: ignore[override]
223
+ return b""
224
+
225
+ wrapped = relenv.runtime._build_shebang(original)
226
+ result = wrapped(SimpleNamespace(target_dir="/tmp/dir"))
227
+ assert result.startswith(b"#!") and result.endswith(b"\r\n")
228
+
229
+
230
+ def test_get_config_var_wrapper_bindir(monkeypatch: pytest.MonkeyPatch) -> None:
231
+ monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: pathlib.Path("/root"))
232
+ wrapped = relenv.runtime.get_config_var_wrapper(lambda name: "/orig")
233
+ result = wrapped("BINDIR")
234
+ assert result == pathlib.Path("/root/Scripts")
235
+
236
+
237
+ def test_get_config_var_wrapper_other(
238
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
239
+ ) -> None:
240
+ monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: pathlib.Path("/root"))
241
+ result = relenv.runtime.get_config_var_wrapper(lambda name: "value")("OTHER")
242
+ assert result == "value"
243
+
244
+
245
+ def test_system_sysconfig_json_error(monkeypatch: pytest.MonkeyPatch) -> None:
246
+ monkeypatch.setattr(relenv.runtime, "_SYSTEM_CONFIG_VARS", None, raising=False)
247
+ monkeypatch.setattr(
248
+ pathlib.Path, "exists", lambda self: str(self) == "/usr/bin/python3"
249
+ )
250
+ monkeypatch.setattr(
251
+ relenv.runtime.subprocess,
252
+ "run",
253
+ lambda *args, **kwargs: SimpleNamespace(stdout=b"invalid", returncode=0),
254
+ )
255
+
256
+ def fake_loads(_data: bytes) -> dict:
257
+ raise json.JSONDecodeError("msg", "doc", 0)
258
+
259
+ monkeypatch.setattr(relenv.runtime.json, "loads", fake_loads)
260
+ result = relenv.runtime.system_sysconfig()
261
+ assert result == relenv.runtime.CONFIG_VARS_DEFAULTS
262
+
263
+
264
+ def test_get_paths_wrapper_updates_scripts(monkeypatch: pytest.MonkeyPatch) -> None:
265
+ def original_get_paths(
266
+ *, scheme: Optional[str], vars: Optional[dict[str, str]], expand: bool
267
+ ) -> dict[str, str]:
268
+ return {"scripts": "/original/scripts"}
269
+
270
+ wrapped = relenv.runtime.get_paths_wrapper(original_get_paths, "default")
271
+ monkeypatch.setenv("RELENV_PIP_DIR", "/tmp/scripts")
272
+ monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: pathlib.Path("/relroot"))
273
+
274
+ result = wrapped()
275
+ expected_root = os.fspath(pathlib.Path("/relroot"))
276
+ assert result["scripts"] == expected_root
277
+ assert relenv.runtime.sys.exec_prefix == expected_root
278
+
279
+ monkeypatch.delenv("RELENV_PIP_DIR", raising=False)
280
+
281
+
282
+ def test_get_config_vars_wrapper_updates(monkeypatch: pytest.MonkeyPatch) -> None:
283
+ module = ModuleType("sysconfig")
284
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
285
+
286
+ def original() -> dict[str, str]:
287
+ return {
288
+ key: "orig"
289
+ for key in (
290
+ "AR",
291
+ "CC",
292
+ "CFLAGS",
293
+ "CPPFLAGS",
294
+ "CXX",
295
+ "LIBDEST",
296
+ "SCRIPTDIR",
297
+ "BLDSHARED",
298
+ "LDFLAGS",
299
+ "LDCXXSHARED",
300
+ "LDSHARED",
301
+ )
302
+ }
303
+
304
+ monkeypatch.setattr(
305
+ relenv.runtime,
306
+ "system_sysconfig",
307
+ lambda: {
308
+ key: "sys"
309
+ for key in (
310
+ "AR",
311
+ "CC",
312
+ "CFLAGS",
313
+ "CPPFLAGS",
314
+ "CXX",
315
+ "LIBDEST",
316
+ "SCRIPTDIR",
317
+ "BLDSHARED",
318
+ "LDFLAGS",
319
+ "LDCXXSHARED",
320
+ "LDSHARED",
321
+ )
322
+ },
323
+ )
324
+ wrapped = relenv.runtime.get_config_vars_wrapper(original, module)
325
+ result = wrapped()
326
+ assert module._CONFIG_VARS["AR"] == "sys"
327
+ assert result == {
328
+ key: "orig"
329
+ for key in (
330
+ "AR",
331
+ "CC",
332
+ "CFLAGS",
333
+ "CPPFLAGS",
334
+ "CXX",
335
+ "LIBDEST",
336
+ "SCRIPTDIR",
337
+ "BLDSHARED",
338
+ "LDFLAGS",
339
+ "LDCXXSHARED",
340
+ "LDSHARED",
341
+ )
342
+ }
343
+
344
+
345
+ def test_get_config_vars_wrapper_buildenv_skip(monkeypatch: pytest.MonkeyPatch) -> None:
346
+ module = ModuleType("sysconfig")
347
+ monkeypatch.setenv("RELENV_BUILDENV", "1")
348
+ marker = object()
349
+ wrapped = relenv.runtime.get_config_vars_wrapper(lambda: marker, module)
350
+ assert wrapped() is marker
351
+ monkeypatch.delenv("RELENV_BUILDENV", raising=False)
352
+
353
+
354
+ def test_finalize_options_wrapper_appends_include(
355
+ monkeypatch: pytest.MonkeyPatch,
356
+ ) -> None:
357
+ class Dummy:
358
+ def __init__(self) -> None:
359
+ self.include_dirs: list[str] = []
360
+
361
+ def original(self: Dummy, *args: object, **kwargs: object) -> None:
362
+ self.include_dirs.append("existing")
363
+
364
+ wrapped = relenv.runtime.finalize_options_wrapper(original)
365
+ dummy = Dummy()
366
+ monkeypatch.setenv("RELENV_BUILDENV", "1")
367
+ monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: pathlib.Path("/relroot"))
368
+ wrapped(dummy)
369
+ expected_include = os.fspath(pathlib.Path("/relroot") / "include")
370
+ assert dummy.include_dirs == ["existing", expected_include]
371
+ monkeypatch.delenv("RELENV_BUILDENV", raising=False)
372
+
373
+
374
+ def test_install_wheel_wrapper_processes_record(
375
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
376
+ ) -> None:
377
+ plat_dir = tmp_path / "plat"
378
+ info_dir = plat_dir / "demo.dist-info"
379
+ info_dir.mkdir(parents=True)
380
+ record = info_dir / "RECORD"
381
+ record.write_text("libdemo.so,,\n")
382
+ binary = plat_dir / "libdemo.so"
383
+ binary.touch()
384
+
385
+ handled: list[tuple[pathlib.Path, pathlib.Path]] = []
386
+ monkeypatch.setattr(
387
+ relenv.runtime,
388
+ "relocate",
389
+ lambda: SimpleNamespace(
390
+ is_elf=lambda path: path.name.endswith(".so"),
391
+ is_macho=lambda path: False,
392
+ handle_elf=lambda file, lib_dir, fix, root: handled.append((file, lib_dir)),
393
+ handle_macho=lambda *args, **kwargs: None,
394
+ ),
395
+ )
396
+
397
+ wheel_utils = ModuleType("pip._internal.utils.wheel")
398
+ wheel_utils.parse_wheel = lambda _zf, _name: ("demo.dist-info", {})
399
+ monkeypatch.setitem(sys.modules, wheel_utils.__name__, wheel_utils)
400
+
401
+ class DummyZip:
402
+ def __init__(self, path: pathlib.Path) -> None:
403
+ self.path = path
404
+
405
+ def __enter__(self) -> DummyZip:
406
+ return self
407
+
408
+ def __exit__(self, *exc: object) -> bool:
409
+ return False
410
+
411
+ monkeypatch.setattr("zipfile.ZipFile", DummyZip)
412
+
413
+ install_module = ModuleType("pip._internal.operations.install.wheel")
414
+
415
+ def original_install(*_args: object, **_kwargs: object) -> str:
416
+ return "original"
417
+
418
+ install_module.install_wheel = original_install # type: ignore[attr-defined]
419
+ monkeypatch.setitem(sys.modules, install_module.__name__, install_module)
420
+
421
+ wrapped_module = relenv.runtime.wrap_pip_install_wheel(install_module.__name__)
422
+
423
+ scheme = SimpleNamespace(
424
+ platlib=str(plat_dir),
425
+ )
426
+ wrapped_module.install_wheel(
427
+ "demo",
428
+ tmp_path / "wheel.whl",
429
+ scheme,
430
+ "desc",
431
+ None,
432
+ None,
433
+ None,
434
+ None,
435
+ )
436
+
437
+ assert handled and handled[0][0].name == "libdemo.so"
438
+
439
+
440
+ def test_install_wheel_wrapper_missing_file(
441
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
442
+ ) -> None:
443
+ plat_dir = tmp_path / "plat"
444
+ info_dir = plat_dir / "demo.dist-info"
445
+ info_dir.mkdir(parents=True)
446
+ (info_dir / "RECORD").write_text("missing.so,,\n")
447
+ (info_dir / "WHEEL").write_text("Wheel-Version: 1.0\n")
448
+ import zipfile
449
+
450
+ wheel_path = tmp_path / "demo_missing.whl"
451
+ with zipfile.ZipFile(wheel_path, "w") as zf:
452
+ zf.writestr("demo.dist-info/RECORD", "missing.so,,\n")
453
+ zf.writestr("demo.dist-info/WHEEL", "Wheel-Version: 1.0\n")
454
+
455
+ monkeypatch.setattr(
456
+ relenv.runtime,
457
+ "relocate",
458
+ lambda: SimpleNamespace(is_elf=lambda path: False, is_macho=lambda path: False),
459
+ )
460
+ module_utils = ModuleType("pip._internal.utils.wheel.missing")
461
+ module_utils.parse_wheel = lambda zf, name: ("demo.dist-info", {})
462
+ wheel_module = ModuleType("pip._internal.operations.install.wheel.missing")
463
+ wheel_module.install_wheel = lambda *args, **kwargs: None # type: ignore[attr-defined]
464
+ monkeypatch.setitem(sys.modules, module_utils.__name__, module_utils)
465
+ monkeypatch.setitem(sys.modules, wheel_module.__name__, wheel_module)
466
+ monkeypatch.setattr(
467
+ relenv.runtime.importlib,
468
+ "import_module",
469
+ lambda name: wheel_module if name == wheel_module.__name__ else module_utils,
470
+ )
471
+ scheme = SimpleNamespace(platlib=str(plat_dir))
472
+ relenv.runtime.wrap_pip_install_wheel(wheel_module.__name__).install_wheel(
473
+ "demo", wheel_path, scheme, "desc", None, None, None, None
474
+ )
475
+
476
+
477
+ def test_install_wheel_wrapper_macho_with_otool(
478
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
479
+ ) -> None:
480
+ plat_dir = tmp_path / "plat"
481
+ info_dir = plat_dir / "demo.dist-info"
482
+ info_dir.mkdir(parents=True)
483
+ (plat_dir / "libmach.dylib").touch()
484
+ (info_dir / "RECORD").write_text("libmach.dylib,,\n")
485
+ (info_dir / "WHEEL").write_text("Wheel-Version: 1.0\n")
486
+ import zipfile
487
+
488
+ wheel_path = tmp_path / "demo_otool.whl"
489
+ with zipfile.ZipFile(wheel_path, "w") as zf:
490
+ zf.writestr("demo.dist-info/RECORD", "libmach.dylib,,\n")
491
+ zf.writestr("demo.dist-info/WHEEL", "Wheel-Version: 1.0\n")
492
+
493
+ monkeypatch.setattr(
494
+ relenv.runtime,
495
+ "relocate",
496
+ lambda: SimpleNamespace(
497
+ is_elf=lambda path: False,
498
+ is_macho=lambda path: True,
499
+ handle_macho=lambda *args, **kwargs: None,
500
+ ),
501
+ )
502
+ monkeypatch.setattr(relenv.runtime.shutil, "which", lambda cmd: "/usr/bin/otool")
503
+ module_utils = ModuleType("pip._internal.utils.wheel.otool")
504
+ module_utils.parse_wheel = lambda zf, name: ("demo.dist-info", {})
505
+ wheel_module = ModuleType("pip._internal.operations.install.wheel.otool")
506
+ wheel_module.install_wheel = lambda *args, **kwargs: None # type: ignore[attr-defined]
507
+ monkeypatch.setitem(sys.modules, module_utils.__name__, module_utils)
508
+ monkeypatch.setitem(sys.modules, wheel_module.__name__, wheel_module)
509
+ monkeypatch.setattr(
510
+ relenv.runtime.importlib,
511
+ "import_module",
512
+ lambda name: wheel_module if name == wheel_module.__name__ else module_utils,
513
+ )
514
+ scheme = SimpleNamespace(platlib=str(plat_dir))
515
+ relenv.runtime.wrap_pip_install_wheel(wheel_module.__name__).install_wheel(
516
+ "demo", wheel_path, scheme, "desc", None, None, None, None
517
+ )
518
+
519
+
520
+ def test_install_wheel_wrapper_macho_without_otool(
521
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
522
+ ) -> None:
523
+ plat_dir = tmp_path / "plat"
524
+ info_dir = plat_dir / "demo.dist-info"
525
+ info_dir.mkdir(parents=True)
526
+ (plat_dir / "libmach.dylib").touch()
527
+ (info_dir / "RECORD").write_text("libmach.dylib,,\n")
528
+ (info_dir / "WHEEL").write_text("Wheel-Version: 1.0\n")
529
+ import zipfile
530
+
531
+ wheel_path = tmp_path / "demo_no_otool.whl"
532
+ with zipfile.ZipFile(wheel_path, "w") as zf:
533
+ zf.writestr("demo.dist-info/RECORD", "libmach.dylib,,\n")
534
+ zf.writestr("demo.dist-info/WHEEL", "Wheel-Version: 1.0\n")
535
+
536
+ monkeypatch.setattr(
537
+ relenv.runtime,
538
+ "relocate",
539
+ lambda: SimpleNamespace(
540
+ is_elf=lambda path: False,
541
+ is_macho=lambda path: True,
542
+ handle_macho=lambda *args, **kwargs: _raise(
543
+ AssertionError("unexpected macho")
544
+ ),
545
+ ),
546
+ )
547
+ monkeypatch.setattr(relenv.runtime.shutil, "which", lambda cmd: None)
548
+ messages: list[str] = []
549
+ monkeypatch.setattr(relenv.runtime, "debug", lambda msg: messages.append(str(msg)))
550
+ module_utils = ModuleType("pip._internal.utils.wheel.no_otool")
551
+ module_utils.parse_wheel = lambda zf, name: ("demo.dist-info", {})
552
+ wheel_module = ModuleType("pip._internal.operations.install.wheel.no_otool")
553
+ wheel_module.install_wheel = lambda *args, **kwargs: None # type: ignore[attr-defined]
554
+ monkeypatch.setitem(sys.modules, module_utils.__name__, module_utils)
555
+ monkeypatch.setitem(sys.modules, wheel_module.__name__, wheel_module)
556
+ monkeypatch.setattr(
557
+ relenv.runtime.importlib,
558
+ "import_module",
559
+ lambda name: wheel_module if name == wheel_module.__name__ else module_utils,
560
+ )
561
+ scheme = SimpleNamespace(platlib=str(plat_dir))
562
+ relenv.runtime.wrap_pip_install_wheel(wheel_module.__name__).install_wheel(
563
+ "demo", wheel_path, scheme, "desc", None, None, None, None
564
+ )
565
+ assert any("otool command is not available" in msg for msg in messages)
566
+
567
+
568
+ def test_install_legacy_wrapper_prefix(
569
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
570
+ ) -> None:
571
+ pkg_dir = tmp_path / "pkg"
572
+ pkg_dir.mkdir()
573
+ (pkg_dir / "PKG-INFO").write_text("Version: 1.0\nName: demo\n")
574
+ sitepack = (
575
+ tmp_path
576
+ / "prefix"
577
+ / "lib"
578
+ / f"python{relenv.runtime.get_major_version()}"
579
+ / "site-packages"
580
+ )
581
+ egg_dir = sitepack / "demo-1.0.egg-info"
582
+ egg_dir.mkdir(parents=True)
583
+ (egg_dir / "installed-files.txt").write_text("missing.so\n")
584
+ scheme = SimpleNamespace(
585
+ purelib=str(tmp_path / "pure"), platlib=str(tmp_path / "pure")
586
+ )
587
+ module = ModuleType("pip._internal.operations.install.legacy.prefix")
588
+ module.install = lambda *args, **kwargs: None # type: ignore[attr-defined]
589
+ monkeypatch.setitem(sys.modules, module.__name__, module)
590
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: module)
591
+ monkeypatch.setattr(
592
+ relenv.runtime,
593
+ "relocate",
594
+ lambda: SimpleNamespace(is_elf=lambda path: False, is_macho=lambda path: False),
595
+ )
596
+ wrapper = relenv.runtime.wrap_pip_install_legacy(module.__name__)
597
+ wrapper.install(
598
+ None,
599
+ None,
600
+ str(sitepack.parent.parent.parent),
601
+ None,
602
+ str(sitepack.parent.parent.parent),
603
+ False,
604
+ False,
605
+ scheme,
606
+ str(pkg_dir / "setup.py"),
607
+ False,
608
+ "demo",
609
+ None,
610
+ pkg_dir,
611
+ "demo",
612
+ )
613
+
614
+
615
+ def test_install_legacy_wrapper_no_egginfo(
616
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
617
+ ) -> None:
618
+ pkg_dir = tmp_path / "pkg"
619
+ pkg_dir.mkdir()
620
+ (pkg_dir / "PKG-INFO").write_text("Name: demo\nVersion: 1.0\n")
621
+ scheme = SimpleNamespace(purelib=str(tmp_path / "pure"))
622
+ module = ModuleType("pip._internal.operations.install.legacy.none")
623
+ module.install = lambda *args, **kwargs: None # type: ignore[attr-defined]
624
+ monkeypatch.setitem(sys.modules, module.__name__, module)
625
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: module)
626
+ wrapper = relenv.runtime.wrap_pip_install_legacy(module.__name__)
627
+ wrapper.install(
628
+ None,
629
+ None,
630
+ None,
631
+ None,
632
+ None,
633
+ False,
634
+ False,
635
+ scheme,
636
+ str(pkg_dir / "setup.py"),
637
+ False,
638
+ "demo",
639
+ None,
640
+ pkg_dir,
641
+ "demo",
642
+ )
643
+
644
+
645
+ def test_install_legacy_wrapper_file_missing(
646
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
647
+ ) -> None:
648
+ pkg_dir = tmp_path / "pkg"
649
+ pkg_dir.mkdir()
650
+ (pkg_dir / "PKG-INFO").write_text("Name: demo\nVersion: 1.0\n")
651
+ egg_dir = tmp_path / "pure" / "demo-1.0.egg-info"
652
+ egg_dir.mkdir(parents=True)
653
+ (egg_dir / "installed-files.txt").write_text("missing.so\n")
654
+ scheme = SimpleNamespace(
655
+ purelib=str(tmp_path / "pure"), platlib=str(tmp_path / "pure")
656
+ )
657
+ module = ModuleType("pip._internal.operations.install.legacy.missing")
658
+ module.install = lambda *args, **kwargs: None # type: ignore[attr-defined]
659
+ monkeypatch.setitem(sys.modules, module.__name__, module)
660
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: module)
661
+ monkeypatch.setattr(
662
+ relenv.runtime,
663
+ "relocate",
664
+ lambda: SimpleNamespace(is_elf=lambda path: False, is_macho=lambda path: False),
665
+ )
666
+ wrapper = relenv.runtime.wrap_pip_install_legacy(module.__name__)
667
+ wrapper.install(
668
+ None,
669
+ None,
670
+ None,
671
+ None,
672
+ None,
673
+ False,
674
+ False,
675
+ scheme,
676
+ str(pkg_dir / "setup.py"),
677
+ False,
678
+ "demo",
679
+ None,
680
+ pkg_dir,
681
+ "demo",
682
+ )
683
+
684
+
685
+ def test_install_legacy_wrapper_handles_elf(
686
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
687
+ ) -> None:
688
+ pkg_dir = tmp_path / "pkg"
689
+ pkg_dir.mkdir()
690
+ (pkg_dir / "PKG-INFO").write_text("Name: demo\nVersion: 1.0\n")
691
+ egg_dir = tmp_path / "pure" / "demo-1.0.egg-info"
692
+ egg_dir.mkdir(parents=True)
693
+ binary = tmp_path / "pure" / "libdemo.so"
694
+ binary.parent.mkdir(parents=True, exist_ok=True)
695
+ binary.write_bytes(b"")
696
+ (egg_dir / "installed-files.txt").write_text(f"{binary}\n")
697
+ scheme = SimpleNamespace(
698
+ purelib=str(tmp_path / "pure"), platlib=str(tmp_path / "pure")
699
+ )
700
+ module = ModuleType("pip._internal.operations.install.legacy.elf")
701
+ module.install = lambda *args, **kwargs: None # type: ignore[attr-defined]
702
+ monkeypatch.setitem(sys.modules, module.__name__, module)
703
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: module)
704
+ handled: list[tuple[pathlib.Path, pathlib.Path, bool, pathlib.Path]] = []
705
+
706
+ def fake_relocate() -> SimpleNamespace:
707
+ return SimpleNamespace(
708
+ is_elf=lambda path: path == binary,
709
+ is_macho=lambda path: False,
710
+ handle_elf=lambda *args: handled.append(args),
711
+ )
712
+
713
+ monkeypatch.setattr(relenv.runtime, "relocate", fake_relocate)
714
+ monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: tmp_path)
715
+ wrapper = relenv.runtime.wrap_pip_install_legacy(module.__name__)
716
+ wrapper.install(
717
+ None,
718
+ None,
719
+ None,
720
+ None,
721
+ None,
722
+ False,
723
+ False,
724
+ scheme,
725
+ str(pkg_dir / "setup.py"),
726
+ False,
727
+ "demo",
728
+ None,
729
+ pkg_dir,
730
+ "demo",
731
+ )
732
+ assert handled and handled[0][0] == binary
733
+
734
+
735
+ def test_wrap_sysconfig_legacy(monkeypatch: pytest.MonkeyPatch) -> None:
736
+ module = ModuleType("sysconfig")
737
+
738
+ def get_config_var(name: str) -> str:
739
+ return name
740
+
741
+ def get_config_vars() -> dict[str, str]:
742
+ return relenv.runtime.CONFIG_VARS_DEFAULTS.copy()
743
+
744
+ def get_paths(**kwargs: object) -> dict[str, str]:
745
+ return {"scripts": "/tmp"}
746
+
747
+ def default_scheme() -> str:
748
+ return "legacy"
749
+
750
+ module.get_config_var = get_config_var
751
+ module.get_config_vars = get_config_vars
752
+ module.get_paths = get_paths
753
+ module._get_default_scheme = default_scheme # type: ignore[attr-defined]
754
+ monkeypatch.setitem(sys.modules, "sysconfig.legacy", module)
755
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: module)
756
+ wrapped = relenv.runtime.wrap_sysconfig("sysconfig.legacy")
757
+ assert wrapped is module
758
+
759
+
760
+ def test_wrap_pip_distlib_scripts(monkeypatch: pytest.MonkeyPatch) -> None:
761
+ class ScriptMaker:
762
+ def __init__(self) -> None:
763
+ self.target_dir = "/tmp/dir"
764
+
765
+ def _build_shebang(self, target: str) -> bytes:
766
+ return b"orig"
767
+
768
+ module = ModuleType("pip._vendor.distlib.scripts")
769
+ module.ScriptMaker = ScriptMaker
770
+ monkeypatch.setitem(sys.modules, module.__name__, module)
771
+ wrapped = relenv.runtime.wrap_pip_distlib_scripts(module.__name__)
772
+ monkeypatch.setattr(
773
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
774
+ )
775
+ monkeypatch.setattr(
776
+ relenv.runtime,
777
+ "common",
778
+ lambda: SimpleNamespace(
779
+ relative_interpreter=lambda *args, **kwargs: _raise(ValueError("boom"))
780
+ ),
781
+ )
782
+ result = wrapped.ScriptMaker()._build_shebang("target")
783
+ assert result == b"orig"
784
+
785
+
786
+ def test_wrap_distutils_command(monkeypatch: pytest.MonkeyPatch) -> None:
787
+ class BuildExt:
788
+ def finalize_options(self) -> None:
789
+ return None
790
+
791
+ module = ModuleType("distutils.command.build_ext")
792
+ module.build_ext = BuildExt
793
+ monkeypatch.setitem(sys.modules, module.__name__, module)
794
+ wrapped = relenv.runtime.wrap_distutils_command(module.__name__)
795
+ dummy = wrapped.build_ext()
796
+ monkeypatch.setenv("RELENV_BUILDENV", "1")
797
+ monkeypatch.setattr(relenv.runtime, "relenv_root", lambda: pathlib.Path("/rel"))
798
+ dummy.include_dirs = []
799
+ wrapped.build_ext.finalize_options(dummy)
800
+ expected_include = os.fspath(pathlib.Path("/rel") / "include")
801
+ assert expected_include in dummy.include_dirs
802
+ monkeypatch.delenv("RELENV_BUILDENV", raising=False)
803
+
804
+
805
+ def test_wrap_pip_build_wheel_sets_env(
806
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
807
+ ) -> None:
808
+ relenv.runtime.TARGET.TARGET = False
809
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
810
+ toolchain = tmp_path / "toolchain" / "trip"
811
+ (toolchain / "sysroot" / "lib").mkdir(parents=True)
812
+ toolchain.mkdir(parents=True, exist_ok=True)
813
+ base_dir = tmp_path
814
+ set_calls: list[tuple[str, str]] = []
815
+ monkeypatch.setattr(
816
+ relenv.runtime,
817
+ "set_env_if_not_set",
818
+ lambda name, value: set_calls.append((name, value)),
819
+ )
820
+ stub_common = SimpleNamespace(
821
+ DATA_DIR=base_dir,
822
+ get_triplet=lambda: "trip",
823
+ get_toolchain=lambda: toolchain,
824
+ )
825
+ monkeypatch.setattr(relenv.runtime, "common", lambda: stub_common)
826
+
827
+ class DummyModule(ModuleType):
828
+ def build_wheel_pep517(self, *args: object, **kwargs: object) -> str: # type: ignore[override]
829
+ return "built"
830
+
831
+ module_name = "pip._internal.operations.build.wheel"
832
+ dummy = DummyModule(module_name)
833
+ monkeypatch.setitem(sys.modules, module_name, dummy)
834
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: dummy)
835
+
836
+ monkeypatch.setattr(
837
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
838
+ )
839
+ wrapped = relenv.runtime.wrap_pip_build_wheel(module_name)
840
+ result = wrapped.build_wheel_pep517("backend", {}, {})
841
+ assert result == "built"
842
+ assert any(name == "CARGO_HOME" for name, _ in set_calls)
843
+
844
+
845
+ def test_wrap_pip_build_wheel_toolchain_missing(
846
+ monkeypatch: pytest.MonkeyPatch,
847
+ ) -> None:
848
+ relenv.runtime.TARGET.TARGET = False
849
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
850
+ stub_common = SimpleNamespace(
851
+ DATA_DIR=pathlib.Path("/data"),
852
+ get_triplet=lambda: "trip",
853
+ get_toolchain=lambda: None,
854
+ )
855
+ monkeypatch.setattr(relenv.runtime, "common", lambda: stub_common)
856
+ module_name = "pip._internal.operations.build.none"
857
+ module = ModuleType(module_name)
858
+ module.build_wheel_pep517 = lambda *args, **kwargs: "built" # type: ignore[attr-defined]
859
+ monkeypatch.setitem(sys.modules, module_name, module)
860
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: module)
861
+
862
+ wrapped = relenv.runtime.wrap_pip_build_wheel(module_name)
863
+ assert wrapped.build_wheel_pep517("backend", {}, {}) == "built"
864
+
865
+
866
+ def test_wrap_pip_build_wheel_non_linux(monkeypatch: pytest.MonkeyPatch) -> None:
867
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "darwin", raising=False)
868
+ module_name = "pip._internal.operations.build.nonlinux"
869
+ module = ModuleType(module_name)
870
+ module.build_wheel_pep517 = lambda *args, **kwargs: "built" # type: ignore[attr-defined]
871
+ monkeypatch.setitem(sys.modules, module_name, module)
872
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: module)
873
+ wrapped = relenv.runtime.wrap_pip_build_wheel(module_name)
874
+ assert wrapped.build_wheel_pep517("backend", {}, {}) == "built"
875
+
876
+
877
+ def test_wrap_cmd_install_updates_target(monkeypatch: pytest.MonkeyPatch) -> None:
878
+ relenv.runtime.TARGET.TARGET = False
879
+ relenv.runtime.TARGET.PATH = None
880
+ relenv.runtime.TARGET.IGNORE = False
881
+
882
+ fake_module = ModuleType("pip._internal.commands.install")
883
+
884
+ class FakeInstallCommand:
885
+ def run(self, options: SimpleNamespace, args: list[str]) -> str:
886
+ options.ran = True
887
+ return "ran"
888
+
889
+ def _handle_target_dir(
890
+ self, target_dir: str, target_temp_dir: str, upgrade: bool
891
+ ) -> str:
892
+ return "handled"
893
+
894
+ fake_module.InstallCommand = FakeInstallCommand
895
+ monkeypatch.setitem(sys.modules, fake_module.__name__, fake_module)
896
+
897
+ status_module = ModuleType("pip._internal.cli.status_codes")
898
+ status_module.SUCCESS = 0
899
+ monkeypatch.setitem(sys.modules, status_module.__name__, status_module)
900
+
901
+ original_import = relenv.runtime.importlib.import_module
902
+
903
+ def fake_import_module(name: str) -> ModuleType:
904
+ if name == fake_module.__name__:
905
+ return fake_module
906
+ return original_import(name)
907
+
908
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", fake_import_module)
909
+
910
+ wrapped = relenv.runtime.wrap_cmd_install(fake_module.__name__)
911
+ options = SimpleNamespace(
912
+ use_user_site=False, target_dir="/tmp/target", ignore_installed=True
913
+ )
914
+ command = wrapped.InstallCommand()
915
+ result = command.run(options, [])
916
+
917
+ assert result == "ran"
918
+ assert relenv.runtime.TARGET.TARGET is True
919
+ assert relenv.runtime.TARGET.PATH == "/tmp/target"
920
+ assert relenv.runtime.TARGET.IGNORE is True
921
+ assert command._handle_target_dir("a", "b", True) == 0
922
+
923
+
924
+ def test_wrap_cmd_install_no_user_site(monkeypatch: pytest.MonkeyPatch) -> None:
925
+ relenv.runtime.TARGET.TARGET = False
926
+ fake_module = ModuleType("pip._internal.commands.install.skip")
927
+
928
+ class InstallCommand:
929
+ def run(self, options: SimpleNamespace, args: list[str]) -> str:
930
+ return "ran"
931
+
932
+ fake_module.InstallCommand = InstallCommand
933
+ monkeypatch.setitem(sys.modules, fake_module.__name__, fake_module)
934
+
935
+ module_status = ModuleType("pip._internal.cli.status_codes")
936
+ module_status.SUCCESS = 0
937
+ monkeypatch.setitem(sys.modules, module_status.__name__, module_status)
938
+
939
+ monkeypatch.setattr(
940
+ relenv.runtime.importlib,
941
+ "import_module",
942
+ lambda name: fake_module if name == fake_module.__name__ else module_status,
943
+ )
944
+
945
+ wrapped = relenv.runtime.wrap_cmd_install(fake_module.__name__)
946
+ options = SimpleNamespace(
947
+ use_user_site=True, target_dir=None, ignore_installed=False
948
+ )
949
+ result = wrapped.InstallCommand().run(options, [])
950
+ assert result == "ran"
951
+ assert relenv.runtime.TARGET.TARGET is False
952
+
953
+
954
+ def test_wrap_locations_applies_target(monkeypatch: pytest.MonkeyPatch) -> None:
955
+ relenv.runtime.TARGET.TARGET = True
956
+ relenv.runtime.TARGET.INSTALL = True
957
+ relenv.runtime.TARGET.PATH = "/target/path"
958
+
959
+ scheme_module = ModuleType("pip._internal.models.scheme")
960
+
961
+ class Scheme:
962
+ def __init__(
963
+ self,
964
+ platlib: str,
965
+ purelib: str,
966
+ headers: str,
967
+ scripts: str,
968
+ data: str,
969
+ ) -> None:
970
+ self.platlib = platlib
971
+ self.purelib = purelib
972
+ self.headers = headers
973
+ self.scripts = scripts
974
+ self.data = data
975
+
976
+ scheme_module.Scheme = Scheme
977
+ monkeypatch.setitem(sys.modules, scheme_module.__name__, scheme_module)
978
+
979
+ fake_locations = ModuleType("pip._internal.locations")
980
+
981
+ class OriginalScheme:
982
+ platlib = "/original/plat"
983
+ purelib = "/original/pure"
984
+ headers = "headers"
985
+ scripts = "scripts"
986
+ data = "data"
987
+
988
+ def get_scheme(
989
+ dist_name: str,
990
+ user: bool = False,
991
+ home: str | None = None,
992
+ root: str | None = None,
993
+ isolated: bool = False,
994
+ prefix: str | None = None,
995
+ ) -> OriginalScheme:
996
+ return OriginalScheme()
997
+
998
+ fake_locations.get_scheme = get_scheme # type: ignore[attr-defined]
999
+ monkeypatch.setitem(sys.modules, fake_locations.__name__, fake_locations)
1000
+
1001
+ original_import = relenv.runtime.importlib.import_module
1002
+
1003
+ def fake_import_module(name: str) -> ModuleType:
1004
+ if name == fake_locations.__name__:
1005
+ return fake_locations
1006
+ if name == scheme_module.__name__:
1007
+ return scheme_module
1008
+ return original_import(name)
1009
+
1010
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", fake_import_module)
1011
+
1012
+ wrapped = relenv.runtime.wrap_locations(fake_locations.__name__)
1013
+ scheme = wrapped.get_scheme("dist")
1014
+ assert scheme.platlib == "/target/path"
1015
+ assert scheme.purelib == "/target/path"
1016
+ assert scheme.headers == "headers"
1017
+ assert scheme.scripts == "scripts"
1018
+ assert scheme.data == "data"
1019
+
1020
+
1021
+ def test_wrap_locations_without_target(monkeypatch: pytest.MonkeyPatch) -> None:
1022
+ relenv.runtime.TARGET.TARGET = False
1023
+ fake_module = ModuleType("pip._internal.locations.plain")
1024
+
1025
+ class OriginalScheme:
1026
+ def __init__(self) -> None:
1027
+ self.platlib = "/plat"
1028
+
1029
+ fake_module.get_scheme = lambda *args, **kwargs: OriginalScheme()
1030
+ monkeypatch.setitem(sys.modules, fake_module.__name__, fake_module)
1031
+ monkeypatch.setattr(
1032
+ relenv.runtime.importlib, "import_module", lambda name: fake_module
1033
+ )
1034
+
1035
+ wrapped = relenv.runtime.wrap_locations(fake_module.__name__)
1036
+ scheme = wrapped.get_scheme("dist")
1037
+ assert scheme.platlib == "/plat"
1038
+
1039
+
1040
+ def test_wrap_req_command_honors_ignore(monkeypatch: pytest.MonkeyPatch) -> None:
1041
+ relenv.runtime.TARGET.TARGET = True
1042
+ relenv.runtime.TARGET.IGNORE = True
1043
+
1044
+ fake_module = ModuleType("pip._internal.cli.req_command")
1045
+
1046
+ class RequirementCommand:
1047
+ def _build_package_finder(
1048
+ self,
1049
+ options: SimpleNamespace,
1050
+ session: object,
1051
+ target_python: object | None = None,
1052
+ ignore_requires_python: object | None = None,
1053
+ ) -> bool:
1054
+ return options.ignore_installed
1055
+
1056
+ fake_module.RequirementCommand = RequirementCommand
1057
+ monkeypatch.setitem(sys.modules, fake_module.__name__, fake_module)
1058
+
1059
+ original_import = relenv.runtime.importlib.import_module
1060
+
1061
+ def fake_import_module(name: str) -> ModuleType:
1062
+ if name == fake_module.__name__:
1063
+ return fake_module
1064
+ return original_import(name)
1065
+
1066
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", fake_import_module)
1067
+
1068
+ wrapped = relenv.runtime.wrap_req_command(fake_module.__name__)
1069
+ command = wrapped.RequirementCommand()
1070
+ options = SimpleNamespace(ignore_installed=False)
1071
+ result = command._build_package_finder(options, object())
1072
+ assert options.ignore_installed is True
1073
+ assert result is True
1074
+
1075
+
1076
+ def test_wrap_req_command_without_target(monkeypatch: pytest.MonkeyPatch) -> None:
1077
+ relenv.runtime.TARGET.TARGET = False
1078
+ fake_module = ModuleType("pip._internal.cli.req_command.plain")
1079
+
1080
+ class RequirementCommand:
1081
+ def _build_package_finder(
1082
+ self,
1083
+ options: SimpleNamespace,
1084
+ session: object,
1085
+ target_python: object | None = None,
1086
+ ignore_requires_python: object | None = None,
1087
+ ) -> bool:
1088
+ return options.ignore_installed
1089
+
1090
+ fake_module.RequirementCommand = RequirementCommand
1091
+ monkeypatch.setitem(sys.modules, fake_module.__name__, fake_module)
1092
+ monkeypatch.setattr(
1093
+ relenv.runtime.importlib, "import_module", lambda name: fake_module
1094
+ )
1095
+
1096
+ wrapped = relenv.runtime.wrap_req_command(fake_module.__name__)
1097
+ options = SimpleNamespace(ignore_installed=False)
1098
+ result = wrapped.RequirementCommand()._build_package_finder(options, object())
1099
+ assert result is False
1100
+
1101
+
1102
+ def test_wrap_req_install_sets_target_home(monkeypatch: pytest.MonkeyPatch) -> None:
1103
+ relenv.runtime.TARGET.TARGET = True
1104
+ relenv.runtime.TARGET.PATH = "/target/path"
1105
+
1106
+ fake_module = ModuleType("pip._internal.req.req_install")
1107
+
1108
+ class InstallRequirement:
1109
+ def install(
1110
+ self,
1111
+ install_options: object,
1112
+ global_options: object,
1113
+ root: object,
1114
+ home: object,
1115
+ prefix: object,
1116
+ warn_script_location: bool,
1117
+ use_user_site: bool,
1118
+ pycompile: bool,
1119
+ ) -> tuple[object, object]:
1120
+ return install_options, home
1121
+
1122
+ fake_module.InstallRequirement = InstallRequirement
1123
+ monkeypatch.setitem(sys.modules, fake_module.__name__, fake_module)
1124
+
1125
+ original_import = relenv.runtime.importlib.import_module
1126
+
1127
+ def fake_import_module(name: str) -> ModuleType:
1128
+ if name == fake_module.__name__:
1129
+ return fake_module
1130
+ return original_import(name)
1131
+
1132
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", fake_import_module)
1133
+
1134
+ wrapped = relenv.runtime.wrap_req_install(fake_module.__name__)
1135
+ installer = wrapped.InstallRequirement()
1136
+ _, home = installer.install(None, None, None, None, None, True, False, True)
1137
+ assert home == relenv.runtime.TARGET.PATH
1138
+
1139
+
1140
+ def test_wrap_req_install_short_signature(monkeypatch: pytest.MonkeyPatch) -> None:
1141
+ relenv.runtime.TARGET.TARGET = True
1142
+ relenv.runtime.TARGET.PATH = "/another/path"
1143
+
1144
+ module_name = "pip._internal.req.req_install.short"
1145
+ short_module = ModuleType(module_name)
1146
+
1147
+ class InstallRequirement:
1148
+ def install(
1149
+ self,
1150
+ global_options: object = None,
1151
+ root: object = None,
1152
+ home: object = None,
1153
+ prefix: object = None,
1154
+ warn_script_location: bool = True,
1155
+ use_user_site: bool = False,
1156
+ pycompile: bool = True,
1157
+ ) -> tuple[object, object]:
1158
+ return global_options, home
1159
+
1160
+ short_module.InstallRequirement = InstallRequirement
1161
+ monkeypatch.setitem(sys.modules, module_name, short_module)
1162
+
1163
+ original_import = relenv.runtime.importlib.import_module
1164
+
1165
+ def fake_import_module(name: str) -> ModuleType:
1166
+ if name == module_name:
1167
+ return short_module
1168
+ return original_import(name)
1169
+
1170
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", fake_import_module)
1171
+
1172
+ wrapped = relenv.runtime.wrap_req_install(module_name)
1173
+ installer = wrapped.InstallRequirement()
1174
+ _, home = installer.install()
1175
+ assert home == relenv.runtime.TARGET.PATH
1176
+
1177
+
1178
+ def test_wrap_req_install_no_target(monkeypatch: pytest.MonkeyPatch) -> None:
1179
+ relenv.runtime.TARGET.TARGET = False
1180
+ module_name = "pip._internal.req.req_install.none"
1181
+ module = ModuleType(module_name)
1182
+
1183
+ class InstallRequirement:
1184
+ def install(
1185
+ self,
1186
+ install_options: object,
1187
+ global_options: object,
1188
+ root: object,
1189
+ home: object,
1190
+ prefix: object,
1191
+ warn_script_location: bool,
1192
+ use_user_site: bool,
1193
+ pycompile: bool,
1194
+ ) -> str:
1195
+ return "installed"
1196
+
1197
+ module.InstallRequirement = InstallRequirement
1198
+ monkeypatch.setitem(sys.modules, module_name, module)
1199
+ monkeypatch.setattr(relenv.runtime.importlib, "import_module", lambda name: module)
1200
+
1201
+ wrapped = relenv.runtime.wrap_req_install(module_name)
1202
+ installer = wrapped.InstallRequirement()
1203
+ result = installer.install(None, None, None, None, None, True, False, True)
1204
+ assert result == "installed"
1205
+
1206
+
1207
+ def test_wrapsitecustomize_sanitizes_sys_path(monkeypatch: pytest.MonkeyPatch) -> None:
1208
+ sanitized = ["sanitized/path"]
1209
+ monkeypatch.setattr(
1210
+ relenv.runtime,
1211
+ "common",
1212
+ lambda: SimpleNamespace(sanitize_sys_path=lambda _: sanitized),
1213
+ )
1214
+ monkeypatch.setattr(
1215
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1216
+ )
1217
+
1218
+ def original() -> None:
1219
+ pass
1220
+
1221
+ wrapped = relenv.runtime.wrapsitecustomize(original)
1222
+ wrapped()
1223
+ assert relenv.runtime.site.ENABLE_USER_SITE is False
1224
+ assert relenv.runtime.sys.path == sanitized
1225
+
1226
+
1227
+ def test_install_cargo_config_toolchain_none(monkeypatch: pytest.MonkeyPatch) -> None:
1228
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1229
+ stub_common = SimpleNamespace(
1230
+ DATA_DIR=pathlib.Path("/data"),
1231
+ work_dirs=lambda: SimpleNamespace(data=pathlib.Path("/data")),
1232
+ get_triplet=lambda: "trip",
1233
+ get_toolchain=lambda: None,
1234
+ )
1235
+ monkeypatch.setattr(relenv.runtime, "common", lambda: stub_common)
1236
+ relenv.runtime.install_cargo_config()
1237
+
1238
+
1239
+ def test_install_cargo_config_toolchain_missing_dir(
1240
+ monkeypatch: pytest.MonkeyPatch,
1241
+ ) -> None:
1242
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1243
+ toolchain = SimpleNamespace(exists=lambda: False)
1244
+ stub_common = SimpleNamespace(
1245
+ DATA_DIR=pathlib.Path("/data"),
1246
+ work_dirs=lambda: SimpleNamespace(data=pathlib.Path("/data")),
1247
+ get_triplet=lambda: "trip",
1248
+ get_toolchain=lambda: toolchain,
1249
+ )
1250
+ monkeypatch.setattr(relenv.runtime, "common", lambda: stub_common)
1251
+ relenv.runtime.install_cargo_config()
1252
+
1253
+
1254
+ def test_install_cargo_config_non_linux(monkeypatch: pytest.MonkeyPatch) -> None:
1255
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "darwin", raising=False)
1256
+ relenv.runtime.install_cargo_config()
1257
+
1258
+
1259
+ def test_install_cargo_config_alt_triplet(
1260
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
1261
+ ) -> None:
1262
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1263
+ data_dir = tmp_path / "data"
1264
+ data_dir.mkdir()
1265
+ toolchain_dir = tmp_path / "toolchain" / "aarch"
1266
+ (toolchain_dir / "sysroot" / "lib").mkdir(parents=True)
1267
+ (toolchain_dir / "bin").mkdir(parents=True)
1268
+ (toolchain_dir / "bin" / "aarch-gcc").touch()
1269
+ stub_common = SimpleNamespace(
1270
+ DATA_DIR=tmp_path,
1271
+ work_dirs=lambda: SimpleNamespace(data=data_dir),
1272
+ get_triplet=lambda: "aarch",
1273
+ get_toolchain=lambda: toolchain_dir,
1274
+ )
1275
+ monkeypatch.setattr(relenv.runtime, "common", lambda: stub_common)
1276
+ relenv.runtime.install_cargo_config()
1277
+ assert (data_dir / "cargo" / "config.toml").exists()
1278
+
1279
+
1280
+ def test_setup_openssl_windows(monkeypatch: pytest.MonkeyPatch) -> None:
1281
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "win32", raising=False)
1282
+ relenv.runtime.setup_openssl()
1283
+
1284
+
1285
+ def test_setup_openssl_without_binary(
1286
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
1287
+ ) -> None:
1288
+ monkeypatch.setattr(relenv.runtime.sys, "RELENV", tmp_path, raising=False)
1289
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux")
1290
+ monkeypatch.setattr(relenv.runtime.shutil, "which", lambda _: None)
1291
+
1292
+ modules_dirs: list[str] = []
1293
+ monkeypatch.setattr(relenv.runtime, "set_openssl_modules_dir", modules_dirs.append)
1294
+ providers: list[str] = []
1295
+
1296
+ def fail_provider(name: str) -> int:
1297
+ providers.append(name)
1298
+ return 0
1299
+
1300
+ monkeypatch.setattr(relenv.runtime, "load_openssl_provider", fail_provider)
1301
+
1302
+ monkeypatch.delenv("OPENSSL_MODULES", raising=False)
1303
+ monkeypatch.delenv("SSL_CERT_DIR", raising=False)
1304
+ monkeypatch.delenv("SSL_CERT_FILE", raising=False)
1305
+
1306
+ relenv.runtime.setup_openssl()
1307
+ assert modules_dirs[-1].endswith("ossl-modules")
1308
+ assert providers == ["default", "legacy"]
1309
+
1310
+
1311
+ def test_setup_openssl_with_system_binary(
1312
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
1313
+ ) -> None:
1314
+ monkeypatch.setattr(relenv.runtime.sys, "RELENV", tmp_path, raising=False)
1315
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux")
1316
+ monkeypatch.setattr(relenv.runtime.shutil, "which", lambda _: "/usr/bin/openssl")
1317
+
1318
+ module_calls: list[str] = []
1319
+ monkeypatch.setattr(relenv.runtime, "set_openssl_modules_dir", module_calls.append)
1320
+
1321
+ providers: list[str] = []
1322
+ monkeypatch.setattr(
1323
+ relenv.runtime,
1324
+ "load_openssl_provider",
1325
+ lambda name: providers.append(name) or 1,
1326
+ )
1327
+
1328
+ def fake_run(args: list[str], **kwargs: object) -> SimpleNamespace:
1329
+ if args[:2] == ["/usr/bin/openssl", "version"]:
1330
+ if "-m" in args:
1331
+ return SimpleNamespace(
1332
+ returncode=0, stdout='MODULESDIR: "/usr/lib/ssl"'
1333
+ )
1334
+ if "-d" in args:
1335
+ return SimpleNamespace(returncode=0, stdout='OPENSSLDIR: "/etc/ssl"')
1336
+ return SimpleNamespace(returncode=1, stdout="", stderr="error")
1337
+
1338
+ monkeypatch.setattr(relenv.runtime.subprocess, "run", fake_run)
1339
+
1340
+ certs_dir = pathlib.Path("/etc/ssl/certs")
1341
+ monkeypatch.setattr(
1342
+ pathlib.Path,
1343
+ "exists",
1344
+ lambda self: str(self)
1345
+ in (str(certs_dir), str(tmp_path / "lib" / "libcrypto.so")),
1346
+ )
1347
+
1348
+ monkeypatch.delenv("OPENSSL_MODULES", raising=False)
1349
+ monkeypatch.delenv("SSL_CERT_DIR", raising=False)
1350
+ monkeypatch.delenv("SSL_CERT_FILE", raising=False)
1351
+
1352
+ relenv.runtime.setup_openssl()
1353
+
1354
+ assert module_calls[0] == "/usr/lib/ssl"
1355
+ assert module_calls[-1].endswith("ossl-modules")
1356
+ assert {"default", "legacy"} <= set(providers)
1357
+ assert os.environ["SSL_CERT_DIR"] == str(certs_dir)
1358
+ monkeypatch.delenv("SSL_CERT_DIR", raising=False)
1359
+ monkeypatch.delenv("SSL_CERT_FILE", raising=False)
1360
+
1361
+
1362
+ def test_relenv_importer_loading(monkeypatch: pytest.MonkeyPatch) -> None:
1363
+ loaded: list[str] = []
1364
+
1365
+ def wrapper(name: str) -> ModuleType:
1366
+ mod = ModuleType(name)
1367
+ mod.loaded = True # type: ignore[attr-defined]
1368
+ loaded.append(name)
1369
+ return mod
1370
+
1371
+ wrapper_obj = relenv.runtime.Wrapper("pkg.sub", wrapper, matcher="startswith")
1372
+ importer = relenv.runtime.RelenvImporter([wrapper_obj])
1373
+ assert importer.find_module("pkg.sub.module") is importer
1374
+ wrapper_obj.loading = False
1375
+ spec = importer.find_spec("pkg.sub.module")
1376
+ assert spec is not None
1377
+ module = importer.load_module("pkg.sub.module")
1378
+ assert getattr(module, "loaded", False)
1379
+ assert loaded == ["pkg.sub.module"]
1380
+ importer.create_module(spec)
1381
+ importer.exec_module(module)
1382
+
1383
+
1384
+ def test_relenv_importer_defaults() -> None:
1385
+ importer = relenv.runtime.RelenvImporter()
1386
+ assert importer.wrappers == set()
1387
+ assert importer._loads == {}
1388
+
1389
+
1390
+ def test_install_cargo_config_toolchain_missing(
1391
+ monkeypatch: pytest.MonkeyPatch,
1392
+ ) -> None:
1393
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1394
+ stub_common = SimpleNamespace(
1395
+ DATA_DIR=pathlib.Path("/data"),
1396
+ work_dirs=lambda: SimpleNamespace(data=pathlib.Path("/data")),
1397
+ get_triplet=lambda: "trip",
1398
+ get_toolchain=lambda: None,
1399
+ )
1400
+ monkeypatch.setattr(relenv.runtime, "common", lambda: stub_common)
1401
+ relenv.runtime.install_cargo_config()
1402
+
1403
+
1404
+ def test_bootstrap(monkeypatch: pytest.MonkeyPatch) -> None:
1405
+ calls: list[str] = []
1406
+ monkeypatch.setattr(
1407
+ relenv.runtime, "relenv_root", lambda: pathlib.Path("/relbootstrap")
1408
+ )
1409
+ monkeypatch.setattr(relenv.runtime, "setup_openssl", lambda: calls.append("ssl"))
1410
+ monkeypatch.setattr(
1411
+ relenv.runtime.site, "execsitecustomize", lambda: None, raising=False
1412
+ )
1413
+ monkeypatch.setattr(
1414
+ relenv.runtime, "setup_crossroot", lambda: calls.append("cross")
1415
+ )
1416
+ monkeypatch.setattr(
1417
+ relenv.runtime, "install_cargo_config", lambda: calls.append("cargo")
1418
+ )
1419
+ monkeypatch.setattr(
1420
+ relenv.runtime.warnings, "filterwarnings", lambda *args, **kwargs: None
1421
+ )
1422
+ original_meta = list(relenv.runtime.sys.meta_path)
1423
+ relenv.runtime.bootstrap()
1424
+ assert relenv.runtime.sys.RELENV == pathlib.Path("/relbootstrap")
1425
+ assert calls == ["ssl", "cross", "cargo"]
1426
+ assert relenv.runtime.sys.meta_path[0] is relenv.runtime.importer
1427
+ relenv.runtime.sys.meta_path = original_meta
1428
+
1429
+
1430
+ def test_common_path_import_invoked(monkeypatch: pytest.MonkeyPatch) -> None:
1431
+ monkeypatch.delattr(relenv.runtime.common, "common", raising=False)
1432
+ sentinel = ModuleType("cached.common")
1433
+ monkeypatch.setattr(relenv.runtime, "path_import", lambda name, path: sentinel)
1434
+ result = relenv.runtime.common()
1435
+ assert result is sentinel
1436
+
1437
+
1438
+ def test_relocate_path_import_invoked(monkeypatch: pytest.MonkeyPatch) -> None:
1439
+ monkeypatch.delattr(relenv.runtime.relocate, "relocate", raising=False)
1440
+ sentinel = ModuleType("cached.relocate")
1441
+ monkeypatch.setattr(relenv.runtime, "path_import", lambda name, path: sentinel)
1442
+ result = relenv.runtime.relocate()
1443
+ assert result is sentinel
1444
+
1445
+
1446
+ def test_buildenv_path_import_invoked(monkeypatch: pytest.MonkeyPatch) -> None:
1447
+ monkeypatch.delattr(relenv.runtime.buildenv, "builenv", raising=False)
1448
+ monkeypatch.delattr(relenv.runtime.buildenv, "buildenv", raising=False)
1449
+ sentinel = ModuleType("cached.buildenv")
1450
+ monkeypatch.setattr(relenv.runtime, "path_import", lambda name, path: sentinel)
1451
+ result = relenv.runtime.buildenv()
1452
+ assert result is sentinel
1453
+
1454
+
1455
+ def test_common_cached(monkeypatch: pytest.MonkeyPatch) -> None:
1456
+ count = {"calls": 0}
1457
+
1458
+ def loader(name: str, path: str) -> ModuleType:
1459
+ count["calls"] += 1
1460
+ return ModuleType(name)
1461
+
1462
+ monkeypatch.setattr(relenv.runtime, "path_import", loader)
1463
+ module1 = relenv.runtime.common()
1464
+ module2 = relenv.runtime.common()
1465
+ assert module1 is module2
1466
+ assert count["calls"] == 0
1467
+
1468
+
1469
+ def test_relocate_cached(monkeypatch: pytest.MonkeyPatch) -> None:
1470
+ module = ModuleType("relenv.relocate.cached")
1471
+ monkeypatch.setattr(relenv.runtime, "_RELOCATE", module, raising=False)
1472
+ result = relenv.runtime.relocate()
1473
+ assert result is module
1474
+
1475
+
1476
+ def test_buildenv_cached(monkeypatch: pytest.MonkeyPatch) -> None:
1477
+ module = ModuleType("relenv.buildenv.cached")
1478
+ monkeypatch.setattr(relenv.runtime, "_BUILDENV", module, raising=False)
1479
+ result = relenv.runtime.buildenv()
1480
+ assert result is module
1481
+
1482
+
1483
+ def test_build_shebang_target(monkeypatch: pytest.MonkeyPatch) -> None:
1484
+ relenv.runtime.TARGET.TARGET = True
1485
+ relenv.runtime.TARGET.PATH = "/target"
1486
+ monkeypatch.setattr(
1487
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1488
+ )
1489
+ monkeypatch.setattr(
1490
+ relenv.runtime,
1491
+ "common",
1492
+ lambda: SimpleNamespace(
1493
+ relative_interpreter=lambda *args: pathlib.Path("bin/python"),
1494
+ format_shebang=lambda path: f"#!{path}",
1495
+ ),
1496
+ )
1497
+
1498
+ def original(self: object) -> bytes: # type: ignore[override]
1499
+ return b""
1500
+
1501
+ result = relenv.runtime._build_shebang(original)(
1502
+ SimpleNamespace(target_dir="/tmp/scripts")
1503
+ )
1504
+ shebang = result.decode().strip()
1505
+ assert shebang.startswith("#!")
1506
+ path_part = shebang[2:]
1507
+ expected_suffix = os.fspath(pathlib.Path("bin") / "python")
1508
+ normalized = path_part.replace("\\", "/")
1509
+ assert normalized.endswith(expected_suffix.replace("\\", "/"))
1510
+ relenv.runtime.TARGET.TARGET = False
1511
+ relenv.runtime.TARGET.PATH = None
1512
+
1513
+
1514
+ def test_build_shebang_linux(monkeypatch: pytest.MonkeyPatch) -> None:
1515
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1516
+ monkeypatch.setattr(
1517
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1518
+ )
1519
+
1520
+ class StubCommon:
1521
+ @staticmethod
1522
+ def relative_interpreter(_relenv, _scripts, _exec):
1523
+ return pathlib.Path("bin/python")
1524
+
1525
+ @staticmethod
1526
+ def format_shebang(path: pathlib.Path) -> str:
1527
+ return f"#!{path}"
1528
+
1529
+ monkeypatch.setattr(relenv.runtime, "common", lambda: StubCommon())
1530
+
1531
+ def original(self: object) -> bytes: # type: ignore[override]
1532
+ return b""
1533
+
1534
+ result = relenv.runtime._build_shebang(original)(
1535
+ SimpleNamespace(target_dir="/tmp/dir")
1536
+ )
1537
+ shebang = result.decode().strip()
1538
+ assert shebang.startswith("#!")
1539
+ path_part = shebang[2:]
1540
+ # Use PurePosixPath since we're testing Linux behavior
1541
+ expected = os.fspath(pathlib.PurePosixPath("/") / "bin" / "python")
1542
+ assert path_part == expected
1543
+
1544
+
1545
+ def test_setup_openssl_version_error(monkeypatch: pytest.MonkeyPatch) -> None:
1546
+ monkeypatch.setattr(
1547
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1548
+ )
1549
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1550
+ monkeypatch.setattr(relenv.runtime.shutil, "which", lambda _: "/usr/bin/openssl")
1551
+ monkeypatch.setattr(relenv.runtime, "set_openssl_modules_dir", lambda path: None)
1552
+ monkeypatch.setattr(relenv.runtime, "load_openssl_provider", lambda name: 1)
1553
+
1554
+ def fake_run(args: list[str], **kwargs: object) -> SimpleNamespace:
1555
+ return SimpleNamespace(returncode=1, stdout="", stderr="err")
1556
+
1557
+ monkeypatch.setattr(relenv.runtime.subprocess, "run", fake_run)
1558
+ relenv.runtime.setup_openssl()
1559
+
1560
+
1561
+ def test_setup_openssl_parse_error(monkeypatch: pytest.MonkeyPatch) -> None:
1562
+ monkeypatch.setattr(
1563
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1564
+ )
1565
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1566
+ monkeypatch.setattr(relenv.runtime.shutil, "which", lambda _: "/usr/bin/openssl")
1567
+ monkeypatch.setattr(relenv.runtime, "load_openssl_provider", lambda name: 1)
1568
+ monkeypatch.setattr(relenv.runtime, "set_openssl_modules_dir", lambda path: None)
1569
+ monkeypatch.delenv("OPENSSL_MODULES", raising=False)
1570
+ monkeypatch.delenv("SSL_CERT_DIR", raising=False)
1571
+ monkeypatch.delenv("SSL_CERT_FILE", raising=False)
1572
+
1573
+ def fake_run(args: list[str], **kwargs: object) -> SimpleNamespace:
1574
+ if "-m" in args:
1575
+ return SimpleNamespace(returncode=0, stdout="invalid", stderr="")
1576
+ return SimpleNamespace(returncode=0, stdout='OPENSSLDIR: "/etc/ssl"', stderr="")
1577
+
1578
+ monkeypatch.setattr(relenv.runtime.subprocess, "run", fake_run)
1579
+ relenv.runtime.setup_openssl()
1580
+
1581
+
1582
+ def test_setup_openssl_cert_dir_error(monkeypatch: pytest.MonkeyPatch) -> None:
1583
+ monkeypatch.setattr(
1584
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1585
+ )
1586
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1587
+ monkeypatch.setattr(relenv.runtime.shutil, "which", lambda _: "/usr/bin/openssl")
1588
+ monkeypatch.setattr(relenv.runtime, "load_openssl_provider", lambda name: 1)
1589
+ monkeypatch.setattr(relenv.runtime, "set_openssl_modules_dir", lambda path: None)
1590
+
1591
+ def fake_run(args: list[str], **kwargs: object) -> SimpleNamespace:
1592
+ if "-m" in args:
1593
+ return SimpleNamespace(
1594
+ returncode=0, stdout='MODULESDIR: "/usr/lib"', stderr=""
1595
+ )
1596
+ return SimpleNamespace(returncode=1, stdout="", stderr="error")
1597
+
1598
+ monkeypatch.setattr(relenv.runtime.subprocess, "run", fake_run)
1599
+ monkeypatch.delenv("SSL_CERT_DIR", raising=False)
1600
+ relenv.runtime.setup_openssl()
1601
+
1602
+
1603
+ def test_setup_openssl_cert_dir_parse_error(monkeypatch: pytest.MonkeyPatch) -> None:
1604
+ monkeypatch.setattr(
1605
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1606
+ )
1607
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1608
+ monkeypatch.setattr(relenv.runtime.shutil, "which", lambda _: "/usr/bin/openssl")
1609
+ monkeypatch.setattr(relenv.runtime, "load_openssl_provider", lambda name: 1)
1610
+ monkeypatch.setattr(relenv.runtime, "set_openssl_modules_dir", lambda path: None)
1611
+ monkeypatch.delenv("OPENSSL_MODULES", raising=False)
1612
+ monkeypatch.delenv("SSL_CERT_DIR", raising=False)
1613
+ monkeypatch.delenv("SSL_CERT_FILE", raising=False)
1614
+
1615
+ def fake_run(args: list[str], **kwargs: object) -> SimpleNamespace:
1616
+ if "-m" in args:
1617
+ return SimpleNamespace(
1618
+ returncode=0, stdout='MODULESDIR: "/usr/lib"', stderr=""
1619
+ )
1620
+ return SimpleNamespace(returncode=0, stdout="invalid", stderr="")
1621
+
1622
+ monkeypatch.setattr(relenv.runtime.subprocess, "run", fake_run)
1623
+ relenv.runtime.setup_openssl()
1624
+
1625
+
1626
+ def test_setup_openssl_cert_file(
1627
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
1628
+ ) -> None:
1629
+ monkeypatch.setattr(
1630
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1631
+ )
1632
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1633
+ monkeypatch.setattr(relenv.runtime.shutil, "which", lambda _: "/usr/bin/openssl")
1634
+ cert_dir = tmp_path / "etc" / "ssl"
1635
+ cert_dir.mkdir(parents=True)
1636
+ cert_file = cert_dir / "cert.pem"
1637
+ cert_file.write_text("cert")
1638
+
1639
+ def fake_run(args: list[str], **kwargs: object) -> SimpleNamespace:
1640
+ if "-m" in args:
1641
+ return SimpleNamespace(
1642
+ returncode=0, stdout='MODULESDIR: "{}"'.format(cert_dir), stderr=""
1643
+ )
1644
+ return SimpleNamespace(
1645
+ returncode=0, stdout='OPENSSLDIR: "{}"'.format(cert_dir), stderr=""
1646
+ )
1647
+
1648
+ monkeypatch.setattr(relenv.runtime.subprocess, "run", fake_run)
1649
+ monkeypatch.setenv("OPENSSL_MODULES", "")
1650
+ monkeypatch.delenv("SSL_CERT_DIR", raising=False)
1651
+ monkeypatch.delenv("SSL_CERT_FILE", raising=False)
1652
+ monkeypatch.setattr(relenv.runtime, "set_openssl_modules_dir", lambda path: None)
1653
+ monkeypatch.setattr(relenv.runtime, "load_openssl_provider", lambda name: 1)
1654
+ relenv.runtime.setup_openssl()
1655
+ assert os.environ["SSL_CERT_FILE"] == os.fspath(cert_file)
1656
+ monkeypatch.delenv("OPENSSL_MODULES", raising=False)
1657
+ monkeypatch.delenv("SSL_CERT_FILE", raising=False)
1658
+
1659
+
1660
+ def test_set_openssl_modules_dir(monkeypatch: pytest.MonkeyPatch) -> None:
1661
+ called = {}
1662
+
1663
+ class FakeLib:
1664
+ def __init__(self) -> None:
1665
+ self.OSSL_PROVIDER_set_default_search_path = (
1666
+ lambda ctx, path: called.update({"path": path}) or 1
1667
+ )
1668
+
1669
+ monkeypatch.setattr(relenv.runtime.ctypes, "CDLL", lambda path: FakeLib())
1670
+ monkeypatch.setattr(
1671
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1672
+ )
1673
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "darwin", raising=False)
1674
+ relenv.runtime.set_openssl_modules_dir("/mods")
1675
+ assert called["path"] == b"/mods"
1676
+
1677
+
1678
+ def test_load_openssl_provider(monkeypatch: pytest.MonkeyPatch) -> None:
1679
+ class FakeLib:
1680
+ def __init__(self) -> None:
1681
+ self.OSSL_PROVIDER_load = lambda ctx, name: 123
1682
+
1683
+ monkeypatch.setattr(relenv.runtime.ctypes, "CDLL", lambda path: FakeLib())
1684
+ monkeypatch.setattr(
1685
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1686
+ )
1687
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "darwin", raising=False)
1688
+ assert relenv.runtime.load_openssl_provider("default") == 123
1689
+
1690
+
1691
+ def test_setup_crossroot(
1692
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
1693
+ ) -> None:
1694
+ monkeypatch.setenv("RELENV_CROSS", str(tmp_path))
1695
+ original_path = sys.path[:]
1696
+ try:
1697
+ relenv.runtime.setup_crossroot()
1698
+ assert sys.prefix == str(tmp_path.resolve())
1699
+ assert str(tmp_path / "lib") in sys.path[0]
1700
+ finally:
1701
+ sys.path = original_path
1702
+ monkeypatch.delenv("RELENV_CROSS", raising=False)
1703
+
1704
+
1705
+ def test_setup_openssl_provider_failure(monkeypatch: pytest.MonkeyPatch) -> None:
1706
+ monkeypatch.setattr(
1707
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1708
+ )
1709
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1710
+ monkeypatch.setattr(relenv.runtime.shutil, "which", lambda _: "/usr/bin/openssl")
1711
+ order: list[str] = []
1712
+ monkeypatch.setattr(
1713
+ relenv.runtime, "set_openssl_modules_dir", lambda path: order.append(path)
1714
+ )
1715
+ providers: list[str] = []
1716
+ monkeypatch.setattr(
1717
+ relenv.runtime,
1718
+ "load_openssl_provider",
1719
+ lambda name: providers.append(name) or 0,
1720
+ )
1721
+ monkeypatch.setattr(
1722
+ relenv.runtime.subprocess,
1723
+ "run",
1724
+ lambda args, **kwargs: SimpleNamespace(
1725
+ returncode=0,
1726
+ stdout='MODULESDIR: "/usr/lib"'
1727
+ if "-m" in args
1728
+ else 'OPENSSLDIR: "/etc/ssl"',
1729
+ stderr="",
1730
+ ),
1731
+ )
1732
+ monkeypatch.delenv("OPENSSL_MODULES", raising=False)
1733
+ monkeypatch.delenv("SSL_CERT_DIR", raising=False)
1734
+ monkeypatch.delenv("SSL_CERT_FILE", raising=False)
1735
+ relenv.runtime.setup_openssl()
1736
+ assert order[0] == "/usr/lib"
1737
+ assert order[-1].endswith("ossl-modules")
1738
+ assert providers == ["fips", "default", "legacy"]
1739
+ monkeypatch.delenv("SSL_CERT_DIR", raising=False)
1740
+ monkeypatch.delenv("SSL_CERT_FILE", raising=False)
1741
+
1742
+
1743
+ def test_wrapsitecustomize_import_error(monkeypatch: pytest.MonkeyPatch) -> None:
1744
+ def original() -> None:
1745
+ pass
1746
+
1747
+ class CustomError(ImportError):
1748
+ def __init__(self) -> None:
1749
+ super().__init__("other")
1750
+ self.name = "other"
1751
+
1752
+ import builtins
1753
+
1754
+ orig_import = builtins.__import__
1755
+
1756
+ def fake_import(
1757
+ name: str,
1758
+ globals: Optional[dict] = None,
1759
+ locals: Optional[dict] = None,
1760
+ fromlist=(),
1761
+ level: int = 0,
1762
+ ):
1763
+ if name == "sitecustomize":
1764
+ raise CustomError()
1765
+ return orig_import(name, globals, locals, fromlist, level)
1766
+
1767
+ monkeypatch.setattr(builtins, "__import__", fake_import)
1768
+ monkeypatch.setattr(
1769
+ relenv.runtime,
1770
+ "common",
1771
+ lambda: SimpleNamespace(sanitize_sys_path=lambda paths: paths),
1772
+ )
1773
+ wrapped = relenv.runtime.wrapsitecustomize(original)
1774
+ with pytest.raises(ImportError):
1775
+ wrapped()
1776
+
1777
+
1778
+ def test_wrapsitecustomize_skip(monkeypatch: pytest.MonkeyPatch) -> None:
1779
+ def original() -> None:
1780
+ pass
1781
+
1782
+ fake_module = ModuleType("sitecustomize")
1783
+ fake_module.__file__ = "/tmp/pip-build-env/sitecustomize.py"
1784
+ monkeypatch.setitem(sys.modules, "sitecustomize", fake_module)
1785
+ monkeypatch.setattr(
1786
+ relenv.runtime,
1787
+ "common",
1788
+ lambda: SimpleNamespace(sanitize_sys_path=lambda paths: paths),
1789
+ )
1790
+ wrapped = relenv.runtime.wrapsitecustomize(original)
1791
+ monkeypatch.setattr(relenv.runtime, "debug", lambda msg: None)
1792
+ wrapped()
1793
+
1794
+
1795
+ def test_set_openssl_modules_dir_linux(monkeypatch: pytest.MonkeyPatch) -> None:
1796
+ called = {}
1797
+
1798
+ class FakeLib:
1799
+ def __init__(self) -> None:
1800
+ self.OSSL_PROVIDER_set_default_search_path = (
1801
+ lambda ctx, path: called.update({"path": path}) or 1
1802
+ )
1803
+
1804
+ monkeypatch.setattr(relenv.runtime.ctypes, "CDLL", lambda path: FakeLib())
1805
+ monkeypatch.setattr(
1806
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1807
+ )
1808
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1809
+ relenv.runtime.set_openssl_modules_dir("/mods")
1810
+ assert called["path"] == b"/mods"
1811
+
1812
+
1813
+ def test_load_openssl_provider_linux(monkeypatch: pytest.MonkeyPatch) -> None:
1814
+ class FakeLib:
1815
+ def __init__(self) -> None:
1816
+ self.OSSL_PROVIDER_load = lambda ctx, name: 456
1817
+
1818
+ monkeypatch.setattr(relenv.runtime.ctypes, "CDLL", lambda path: FakeLib())
1819
+ monkeypatch.setattr(
1820
+ relenv.runtime.sys, "RELENV", pathlib.Path("/rel"), raising=False
1821
+ )
1822
+ monkeypatch.setattr(relenv.runtime.sys, "platform", "linux", raising=False)
1823
+ 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)