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.
- relenv/__init__.py +14 -2
- relenv/__main__.py +12 -6
- relenv/_resources/xz/config.h +148 -0
- relenv/_resources/xz/readme.md +4 -0
- relenv/build/__init__.py +28 -30
- relenv/build/common/__init__.py +50 -0
- relenv/build/common/_sysconfigdata_template.py +72 -0
- relenv/build/common/builder.py +907 -0
- relenv/build/common/builders.py +163 -0
- relenv/build/common/download.py +324 -0
- relenv/build/common/install.py +609 -0
- relenv/build/common/ui.py +432 -0
- relenv/build/darwin.py +128 -14
- relenv/build/linux.py +292 -74
- relenv/build/windows.py +123 -169
- relenv/buildenv.py +48 -17
- relenv/check.py +10 -5
- relenv/common.py +492 -165
- relenv/create.py +147 -7
- relenv/fetch.py +16 -4
- relenv/manifest.py +15 -7
- relenv/python-versions.json +350 -0
- relenv/pyversions.py +817 -30
- relenv/relocate.py +101 -55
- relenv/runtime.py +457 -282
- relenv/toolchain.py +9 -3
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/METADATA +1 -1
- relenv-0.22.1.dist-info/RECORD +48 -0
- tests/__init__.py +2 -0
- tests/_pytest_typing.py +45 -0
- tests/conftest.py +42 -36
- tests/test_build.py +426 -9
- tests/test_common.py +373 -48
- tests/test_create.py +149 -6
- tests/test_downloads.py +19 -15
- tests/test_fips_photon.py +6 -3
- tests/test_module_imports.py +44 -0
- tests/test_pyversions_runtime.py +177 -0
- tests/test_relocate.py +45 -39
- tests/test_relocate_module.py +257 -0
- tests/test_runtime.py +1968 -6
- tests/test_verify_build.py +477 -34
- relenv/build/common.py +0 -1707
- relenv-0.21.2.dist-info/RECORD +0 -35
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/WHEEL +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/entry_points.txt +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/licenses/LICENSE.md +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/licenses/NOTICE +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.1.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
|
|
37
|
+
if self.root_dir.exists():
|
|
37
38
|
shutil.rmtree(self.root_dir, ignore_errors=True)
|
|
38
39
|
|
|
39
|
-
def add_file(
|
|
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
|
-
|
|
50
|
+
data = contents if isinstance(contents, bytes) else contents.encode()
|
|
51
|
+
file_path.write_bytes(data)
|
|
44
52
|
else:
|
|
45
|
-
|
|
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,
|
|
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
|
-
|
|
109
|
-
pass
|
|
116
|
+
def test_parse_otool_l() -> None:
|
|
117
|
+
pytest.skip("Not implemented")
|
|
110
118
|
|
|
111
119
|
|
|
112
|
-
def test_parse_macho():
|
|
113
|
-
|
|
114
|
-
pass
|
|
120
|
+
def test_parse_macho() -> None:
|
|
121
|
+
pytest.skip("Not implemented")
|
|
115
122
|
|
|
116
123
|
|
|
117
|
-
def test_handle_macho():
|
|
118
|
-
|
|
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)
|