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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. relenv/__init__.py +14 -2
  2. relenv/__main__.py +12 -6
  3. relenv/_resources/xz/config.h +148 -0
  4. relenv/_resources/xz/readme.md +4 -0
  5. relenv/build/__init__.py +28 -30
  6. relenv/build/common/__init__.py +50 -0
  7. relenv/build/common/_sysconfigdata_template.py +72 -0
  8. relenv/build/common/builder.py +907 -0
  9. relenv/build/common/builders.py +163 -0
  10. relenv/build/common/download.py +324 -0
  11. relenv/build/common/install.py +609 -0
  12. relenv/build/common/ui.py +432 -0
  13. relenv/build/darwin.py +128 -14
  14. relenv/build/linux.py +292 -74
  15. relenv/build/windows.py +123 -169
  16. relenv/buildenv.py +48 -17
  17. relenv/check.py +10 -5
  18. relenv/common.py +489 -165
  19. relenv/create.py +147 -7
  20. relenv/fetch.py +16 -4
  21. relenv/manifest.py +15 -7
  22. relenv/python-versions.json +329 -0
  23. relenv/pyversions.py +817 -30
  24. relenv/relocate.py +101 -55
  25. relenv/runtime.py +452 -282
  26. relenv/toolchain.py +9 -3
  27. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/METADATA +1 -1
  28. relenv-0.22.0.dist-info/RECORD +48 -0
  29. tests/__init__.py +2 -0
  30. tests/_pytest_typing.py +45 -0
  31. tests/conftest.py +42 -36
  32. tests/test_build.py +426 -9
  33. tests/test_common.py +311 -48
  34. tests/test_create.py +149 -6
  35. tests/test_downloads.py +19 -15
  36. tests/test_fips_photon.py +6 -3
  37. tests/test_module_imports.py +44 -0
  38. tests/test_pyversions_runtime.py +177 -0
  39. tests/test_relocate.py +45 -39
  40. tests/test_relocate_module.py +257 -0
  41. tests/test_runtime.py +1802 -6
  42. tests/test_verify_build.py +477 -34
  43. relenv/build/common.py +0 -1707
  44. relenv-0.21.2.dist-info/RECORD +0 -35
  45. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/WHEEL +0 -0
  46. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/entry_points.txt +0 -0
  47. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/licenses/LICENSE.md +0 -0
  48. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/licenses/NOTICE +0 -0
  49. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/top_level.txt +0 -0
tests/test_relocate.py CHANGED
@@ -1,8 +1,9 @@
1
1
  # Copyright 2022-2025 Broadcom.
2
- # SPDX-License-Identifier: Apache-2
2
+ # SPDX-License-Identifier: Apache-2.0
3
3
  import pathlib
4
4
  import shutil
5
5
  from textwrap import dedent
6
+ from typing import Any
6
7
  from unittest.mock import MagicMock, call, patch
7
8
 
8
9
  import pytest
@@ -23,103 +24,108 @@ pytestmark = [
23
24
 
24
25
 
25
26
  class BaseProject:
26
- def __init__(self, root_dir):
27
+ def __init__(self, root_dir: pathlib.Path) -> None:
27
28
  self.root_dir = root_dir
28
29
  self.libs_dir = self.root_dir / "lib"
29
30
 
30
- def make_project(self):
31
+ def make_project(self) -> None:
31
32
  self.root_dir.mkdir(parents=True, exist_ok=True)
32
33
  self.libs_dir.mkdir(parents=True, exist_ok=True)
33
34
 
34
- def destroy_project(self):
35
+ def destroy_project(self) -> None:
35
36
  # Make sure the project is torn down properly
36
- if pathlib.Path(self.root_dir).exists():
37
+ if self.root_dir.exists():
37
38
  shutil.rmtree(self.root_dir, ignore_errors=True)
38
39
 
39
- def add_file(self, name, contents, *relpath, binary=False):
40
+ def add_file(
41
+ self,
42
+ name: str,
43
+ contents: bytes | str,
44
+ *relpath: str,
45
+ binary: bool = False,
46
+ ) -> pathlib.Path:
40
47
  file_path = (self.root_dir / pathlib.Path(*relpath) / name).resolve()
41
48
  file_path.parent.mkdir(parents=True, exist_ok=True)
42
49
  if binary:
43
- file_path.write_bytes(contents)
50
+ data = contents if isinstance(contents, bytes) else contents.encode()
51
+ file_path.write_bytes(data)
44
52
  else:
45
- file_path.write_text(contents)
53
+ text = contents.decode() if isinstance(contents, bytes) else contents
54
+ file_path.write_text(text)
46
55
  return file_path
47
56
 
48
- def __enter__(self):
57
+ def __enter__(self) -> "BaseProject":
49
58
  self.make_project()
50
59
  return self
51
60
 
52
- def __exit__(self, *exc):
61
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
53
62
  self.destroy_project()
54
63
 
55
64
 
56
65
  class LinuxProject(BaseProject):
57
- def add_simple_elf(self, name, *relpath):
66
+ def add_simple_elf(self, name: str, *relpath: str) -> pathlib.Path:
58
67
  return self.add_file(name, b"\x7f\x45\x4c\x46", *relpath, binary=True)
59
68
 
60
69
 
61
- def test_is_macho_true(tmp_path):
70
+ def test_is_macho_true(tmp_path: pathlib.Path) -> None:
62
71
  lib_path = tmp_path / "test.dylib"
63
72
  lib_path.write_bytes(b"\xcf\xfa\xed\xfe")
64
73
  assert is_macho(lib_path) is True
65
74
 
66
75
 
67
- def test_is_macho_false(tmp_path):
76
+ def test_is_macho_false(tmp_path: pathlib.Path) -> None:
68
77
  lib_path = tmp_path / "test.dylib"
69
78
  lib_path.write_bytes(b"\xcf\xfa\xed\xfa")
70
79
  assert is_macho(lib_path) is False
71
80
 
72
81
 
73
- def test_is_macho_not_a_file(tmp_path):
82
+ def test_is_macho_not_a_file(tmp_path: pathlib.Path) -> None:
74
83
  with pytest.raises(IsADirectoryError):
75
84
  assert is_macho(tmp_path) is False
76
85
 
77
86
 
78
- def test_is_macho_file_does_not_exist(tmp_path):
87
+ def test_is_macho_file_does_not_exist(tmp_path: pathlib.Path) -> None:
79
88
  lib_path = tmp_path / "test.dylib"
80
89
  with pytest.raises(FileNotFoundError):
81
90
  assert is_macho(lib_path) is False
82
91
 
83
92
 
84
- def test_is_elf_true(tmp_path):
93
+ def test_is_elf_true(tmp_path: pathlib.Path) -> None:
85
94
  lib_path = tmp_path / "test.so"
86
95
  lib_path.write_bytes(b"\x7f\x45\x4c\x46")
87
96
  assert is_elf(lib_path) is True
88
97
 
89
98
 
90
- def test_is_elf_false(tmp_path):
99
+ def test_is_elf_false(tmp_path: pathlib.Path) -> None:
91
100
  lib_path = tmp_path / "test.so"
92
101
  lib_path.write_bytes(b"\xcf\xfa\xed\xfa")
93
102
  assert is_elf(lib_path) is False
94
103
 
95
104
 
96
- def test_is_elf_not_a_file(tmp_path):
105
+ def test_is_elf_not_a_file(tmp_path: pathlib.Path) -> None:
97
106
  with pytest.raises(IsADirectoryError):
98
107
  assert is_elf(tmp_path) is False
99
108
 
100
109
 
101
- def test_is_elf_file_does_not_exist(tmp_path):
110
+ def test_is_elf_file_does_not_exist(tmp_path: pathlib.Path) -> None:
102
111
  lib_path = tmp_path / "test.so"
103
112
  with pytest.raises(FileNotFoundError):
104
113
  assert is_elf(lib_path) is False
105
114
 
106
115
 
107
- def test_parse_otool_l():
108
- # XXX
109
- pass
116
+ def test_parse_otool_l() -> None:
117
+ pytest.skip("Not implemented")
110
118
 
111
119
 
112
- def test_parse_macho():
113
- # XXX
114
- pass
120
+ def test_parse_macho() -> None:
121
+ pytest.skip("Not implemented")
115
122
 
116
123
 
117
- def test_handle_macho():
118
- # XXX
119
- pass
124
+ def test_handle_macho() -> None:
125
+ pytest.skip("Not implemented")
120
126
 
121
127
 
122
- def test_parse_readelf_d_no_rpath():
128
+ def test_parse_readelf_d_no_rpath() -> None:
123
129
  section = dedent(
124
130
  """
125
131
  Dynamic section at offset 0xbdd40 contains 28 entries:
@@ -134,7 +140,7 @@ def test_parse_readelf_d_no_rpath():
134
140
  assert parse_readelf_d(section) == []
135
141
 
136
142
 
137
- def test_parse_readelf_d_rpath():
143
+ def test_parse_readelf_d_rpath() -> None:
138
144
  section = dedent(
139
145
  """
140
146
  Dynamic section at offset 0x58000 contains 27 entries:
@@ -149,19 +155,19 @@ def test_parse_readelf_d_rpath():
149
155
  assert parse_readelf_d(section) == ["$ORIGIN/../.."]
150
156
 
151
157
 
152
- def test_is_in_dir(tmp_path):
158
+ def test_is_in_dir(tmp_path: pathlib.Path) -> None:
153
159
  parent = tmp_path / "foo"
154
160
  child = tmp_path / "foo" / "bar" / "bang"
155
161
  assert is_in_dir(child, parent) is True
156
162
 
157
163
 
158
- def test_is_in_dir_false(tmp_path):
164
+ def test_is_in_dir_false(tmp_path: pathlib.Path) -> None:
159
165
  parent = tmp_path / "foo"
160
166
  child = tmp_path / "bar" / "bang"
161
167
  assert is_in_dir(child, parent) is False
162
168
 
163
169
 
164
- def test_patch_rpath(tmp_path):
170
+ def test_patch_rpath(tmp_path: pathlib.Path) -> None:
165
171
  path = str(tmp_path / "test")
166
172
  new_rpath = str(pathlib.Path("$ORIGIN", "..", "..", "lib"))
167
173
  with patch("subprocess.run", return_value=MagicMock(returncode=0)):
@@ -172,7 +178,7 @@ def test_patch_rpath(tmp_path):
172
178
  assert patch_rpath(path, new_rpath) == new_rpath
173
179
 
174
180
 
175
- def test_patch_rpath_failed(tmp_path):
181
+ def test_patch_rpath_failed(tmp_path: pathlib.Path) -> None:
176
182
  path = str(tmp_path / "test")
177
183
  new_rpath = str(pathlib.Path("$ORIGIN", "..", "..", "lib"))
178
184
  with patch("subprocess.run", return_value=MagicMock(returncode=1)):
@@ -183,7 +189,7 @@ def test_patch_rpath_failed(tmp_path):
183
189
  assert patch_rpath(path, new_rpath) is False
184
190
 
185
191
 
186
- def test_patch_rpath_no_change(tmp_path):
192
+ def test_patch_rpath_no_change(tmp_path: pathlib.Path) -> None:
187
193
  path = str(tmp_path / "test")
188
194
  new_rpath = str(pathlib.Path("$ORIGIN", "..", "..", "lib"))
189
195
  with patch("subprocess.run", return_value=MagicMock(returncode=0)):
@@ -191,7 +197,7 @@ def test_patch_rpath_no_change(tmp_path):
191
197
  assert patch_rpath(path, new_rpath, only_relative=False) == new_rpath
192
198
 
193
199
 
194
- def test_patch_rpath_remove_non_relative(tmp_path):
200
+ def test_patch_rpath_remove_non_relative(tmp_path: pathlib.Path) -> None:
195
201
  path = str(tmp_path / "test")
196
202
  new_rpath = str(pathlib.Path("$ORIGIN", "..", "..", "lib"))
197
203
  with patch("subprocess.run", return_value=MagicMock(returncode=0)):
@@ -202,7 +208,7 @@ def test_patch_rpath_remove_non_relative(tmp_path):
202
208
  assert patch_rpath(path, new_rpath) == new_rpath
203
209
 
204
210
 
205
- def test_main_linux(tmp_path):
211
+ def test_main_linux(tmp_path: pathlib.Path) -> None:
206
212
  proj = LinuxProject(tmp_path)
207
213
  simple = proj.add_simple_elf("simple.so", "foo", "bar")
208
214
  simple2 = proj.add_simple_elf("simple2.so", "foo", "bar", "bop")
@@ -218,7 +224,7 @@ def test_main_linux(tmp_path):
218
224
  elf_mock.assert_has_calls(calls, any_order=True)
219
225
 
220
226
 
221
- def test_handle_elf(tmp_path):
227
+ def test_handle_elf(tmp_path: pathlib.Path) -> None:
222
228
  proj = LinuxProject(tmp_path / "proj")
223
229
  pybin = proj.add_simple_elf("python", "foo")
224
230
  libcrypt = tmp_path / "libcrypt.so.2"
@@ -245,7 +251,7 @@ def test_handle_elf(tmp_path):
245
251
  patch_rpath_mock.assert_called_with(str(pybin), "$ORIGIN/../lib")
246
252
 
247
253
 
248
- def test_handle_elf_rpath_only(tmp_path):
254
+ def test_handle_elf_rpath_only(tmp_path: pathlib.Path) -> None:
249
255
  proj = LinuxProject(tmp_path / "proj")
250
256
  pybin = proj.add_simple_elf("python", "foo")
251
257
  libcrypt = proj.libs_dir / "libcrypt.so.2"
@@ -0,0 +1,257 @@
1
+ # Copyright 2022-2025 Broadcom.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import pathlib
8
+ import shutil
9
+ import subprocess
10
+ from typing import Dict, List, Tuple
11
+
12
+ import pytest
13
+
14
+ from relenv import relocate
15
+
16
+
17
+ def test_is_elf_on_text_file(tmp_path: pathlib.Path) -> None:
18
+ sample = tmp_path / "sample.txt"
19
+ sample.write_text("not an ELF binary\n")
20
+ assert relocate.is_elf(sample) is False
21
+
22
+
23
+ def test_is_macho_on_text_file(tmp_path: pathlib.Path) -> None:
24
+ sample = tmp_path / "sample.txt"
25
+ sample.write_text("plain text\n")
26
+ assert relocate.is_macho(sample) is False
27
+
28
+
29
+ def test_parse_readelf_output() -> None:
30
+ output = """
31
+ 0x000000000000000f (NEEDED) Shared library: [libc.so.6]
32
+ 0x000000000000001d (RUNPATH) Library runpath: [/usr/lib:/opt/lib]
33
+ """
34
+ result = relocate.parse_readelf_d(output)
35
+ assert result == ["/usr/lib", "/opt/lib"]
36
+
37
+
38
+ def test_parse_otool_output_extracts_rpaths() -> None:
39
+ sample_output = """
40
+ Load command 0
41
+ cmd LC_LOAD_DYLIB
42
+ cmdsize 56
43
+ name /usr/lib/libSystem.B.dylib (offset 24)
44
+ Load command 1
45
+ cmd LC_RPATH
46
+ cmdsize 32
47
+ path @loader_path/../lib (offset 12)
48
+ """
49
+ parsed = relocate.parse_otool_l(sample_output)
50
+ assert parsed[relocate.LC_LOAD_DYLIB] == ["/usr/lib/libSystem.B.dylib"]
51
+ assert parsed[relocate.LC_RPATH] == ["@loader_path/../lib"]
52
+
53
+
54
+ def test_patch_rpath_adds_new_entry(
55
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
56
+ ) -> None:
57
+ binary = tmp_path / "prog"
58
+ binary.write_text("dummy")
59
+
60
+ monkeypatch.setattr(
61
+ relocate,
62
+ "parse_rpath",
63
+ lambda path: ["$ORIGIN/lib", "/abs/lib"],
64
+ )
65
+
66
+ recorded: Dict[str, List[str]] = {}
67
+
68
+ def fake_run(
69
+ cmd: List[str], **kwargs: object
70
+ ) -> subprocess.CompletedProcess[bytes]:
71
+ recorded.setdefault("cmd", []).extend(cmd)
72
+ return subprocess.CompletedProcess(cmd, 0, stdout=b"", stderr=b"")
73
+
74
+ monkeypatch.setattr(relocate.subprocess, "run", fake_run)
75
+
76
+ result = relocate.patch_rpath(binary, "$ORIGIN/../lib")
77
+ assert result == "$ORIGIN/../lib:$ORIGIN/lib"
78
+ assert pathlib.Path(recorded["cmd"][-1]) == binary
79
+
80
+
81
+ def test_patch_rpath_skips_when_present(
82
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
83
+ ) -> None:
84
+ binary = tmp_path / "prog"
85
+ binary.write_text("dummy")
86
+
87
+ monkeypatch.setattr(relocate, "parse_rpath", lambda path: ["$ORIGIN/lib"])
88
+
89
+ def fail_run(*_args: object, **_kwargs: object) -> None:
90
+ raise AssertionError("patchelf should not be invoked")
91
+
92
+ monkeypatch.setattr(relocate.subprocess, "run", fail_run)
93
+
94
+ result = relocate.patch_rpath(binary, "$ORIGIN/lib")
95
+ assert result == "$ORIGIN/lib"
96
+
97
+
98
+ def test_handle_elf_sets_rpath(
99
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
100
+ ) -> None:
101
+ bin_dir = tmp_path / "bin"
102
+ lib_dir = tmp_path / "lib"
103
+ bin_dir.mkdir()
104
+ lib_dir.mkdir()
105
+
106
+ binary = bin_dir / "prog"
107
+ binary.write_text("binary")
108
+ resident = lib_dir / "libfoo.so"
109
+ resident.write_text("library")
110
+
111
+ def fake_run(
112
+ cmd: List[str], **kwargs: object
113
+ ) -> subprocess.CompletedProcess[bytes]:
114
+ if cmd[0] == "ldd":
115
+ stdout = f"libfoo.so => {resident} (0x00007)\nlibc.so.6 => /lib/libc.so.6 (0x00007)\n"
116
+ return subprocess.CompletedProcess(
117
+ cmd, 0, stdout=stdout.encode(), stderr=b""
118
+ )
119
+ raise AssertionError(f"Unexpected command {cmd}")
120
+
121
+ monkeypatch.setattr(relocate.subprocess, "run", fake_run)
122
+
123
+ captured: Dict[str, str] = {}
124
+
125
+ def fake_patch_rpath(path: str, relpath: str) -> str:
126
+ captured["path"] = path
127
+ captured["relpath"] = relpath
128
+ return relpath
129
+
130
+ monkeypatch.setattr(relocate, "patch_rpath", fake_patch_rpath)
131
+
132
+ relocate.handle_elf(binary, lib_dir, rpath_only=False, root=lib_dir)
133
+
134
+ assert pathlib.Path(captured["path"]) == binary
135
+ expected_rel = os.path.relpath(lib_dir, bin_dir)
136
+ if expected_rel == ".":
137
+ expected_rpath = "$ORIGIN"
138
+ else:
139
+ expected_rpath = str(pathlib.Path("$ORIGIN") / expected_rel)
140
+ assert captured["relpath"] == expected_rpath
141
+
142
+
143
+ def test_patch_rpath_failure(
144
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
145
+ ) -> None:
146
+ binary = tmp_path / "prog"
147
+ binary.write_text("dummy")
148
+
149
+ monkeypatch.setattr(relocate, "parse_rpath", lambda path: [])
150
+
151
+ def fake_run(
152
+ cmd: List[str], **kwargs: object
153
+ ) -> subprocess.CompletedProcess[bytes]:
154
+ return subprocess.CompletedProcess(cmd, 1, stdout=b"", stderr=b"err")
155
+
156
+ monkeypatch.setattr(relocate.subprocess, "run", fake_run)
157
+
158
+ assert relocate.patch_rpath(binary, "$ORIGIN/lib") is False
159
+
160
+
161
+ def test_parse_macho_non_object(
162
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
163
+ ) -> None:
164
+ output = "foo: is not an object file\n"
165
+ monkeypatch.setattr(
166
+ relocate.subprocess,
167
+ "run",
168
+ lambda cmd, **kwargs: subprocess.CompletedProcess(
169
+ cmd, 0, stdout=output.encode(), stderr=b""
170
+ ),
171
+ )
172
+ assert relocate.parse_macho(tmp_path / "lib.dylib") is None
173
+
174
+
175
+ def test_handle_macho_copies_when_needed(
176
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
177
+ ) -> None:
178
+ binary = tmp_path / "bin" / "prog"
179
+ binary.parent.mkdir()
180
+ binary.write_text("exe")
181
+ source_lib = tmp_path / "src" / "libfoo.dylib"
182
+ source_lib.parent.mkdir()
183
+ source_lib.write_text("binary")
184
+ root_dir = tmp_path / "root"
185
+ root_dir.mkdir()
186
+
187
+ monkeypatch.setattr(
188
+ relocate,
189
+ "parse_macho",
190
+ lambda path: {relocate.LC_LOAD_DYLIB: [str(source_lib)]},
191
+ )
192
+
193
+ monkeypatch.setattr(os.path, "exists", lambda path: path == str(source_lib))
194
+
195
+ copied: Dict[str, Tuple[str, str]] = {}
196
+
197
+ monkeypatch.setattr(
198
+ shutil, "copy", lambda src, dst: copied.setdefault("copy", (src, dst))
199
+ )
200
+ monkeypatch.setattr(
201
+ shutil, "copymode", lambda src, dst: copied.setdefault("copymode", (src, dst))
202
+ )
203
+
204
+ recorded: Dict[str, List[str]] = {}
205
+
206
+ def fake_run(
207
+ cmd: List[str], **kwargs: object
208
+ ) -> subprocess.CompletedProcess[bytes]:
209
+ recorded.setdefault("cmd", []).extend(cmd)
210
+ return subprocess.CompletedProcess(cmd, 0, stdout=b"", stderr=b"")
211
+
212
+ monkeypatch.setattr(relocate.subprocess, "run", fake_run)
213
+
214
+ relocate.handle_macho(str(binary), str(root_dir), rpath_only=False)
215
+
216
+ assert copied["copy"][0] == str(source_lib)
217
+ assert pathlib.Path(copied["copy"][1]).name == source_lib.name
218
+ assert recorded["cmd"][0] == "install_name_tool"
219
+
220
+
221
+ def test_handle_macho_rpath_only(
222
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
223
+ ) -> None:
224
+ binary = tmp_path / "bin" / "prog"
225
+ binary.parent.mkdir()
226
+ binary.write_text("exe")
227
+ source_lib = tmp_path / "src" / "libfoo.dylib"
228
+ source_lib.parent.mkdir()
229
+ source_lib.write_text("binary")
230
+ root_dir = tmp_path / "root"
231
+ root_dir.mkdir()
232
+
233
+ monkeypatch.setattr(
234
+ relocate,
235
+ "parse_macho",
236
+ lambda path: {relocate.LC_LOAD_DYLIB: [str(source_lib)]},
237
+ )
238
+
239
+ monkeypatch.setattr(
240
+ os.path,
241
+ "exists",
242
+ lambda path: path == str(source_lib),
243
+ )
244
+
245
+ monkeypatch.setattr(shutil, "copy", lambda *_args, **_kw: (_args, _kw))
246
+ monkeypatch.setattr(shutil, "copymode", lambda *_args, **_kw: (_args, _kw))
247
+
248
+ def fake_run(
249
+ cmd: List[str], **kwargs: object
250
+ ) -> subprocess.CompletedProcess[bytes]:
251
+ if cmd[0] == "install_name_tool":
252
+ raise AssertionError("install_name_tool should not run in rpath_only mode")
253
+ return subprocess.CompletedProcess(cmd, 0, stdout=b"", stderr=b"")
254
+
255
+ monkeypatch.setattr(relocate.subprocess, "run", fake_run)
256
+
257
+ relocate.handle_macho(str(binary), str(root_dir), rpath_only=True)