Skip to content

Commit a9b4794

Browse files
authored
Create ELF patcher interface (#240)
* Create ELF patcher interface Add a new `ElfPatcher` interface to create an abstraction for patching ELF files. The only implementation at the moment is `Patchelf` based on the existing logic around invoking `patchelf`. * Add tests for Patchelf class * Fix lint issue in condatools The single letter variable name fails linting.
1 parent 5f03cee commit a9b4794

File tree

6 files changed

+160
-56
lines changed

6 files changed

+160
-56
lines changed

auditwheel/condatools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ def __enter__(self):
3131
def iter_files(self):
3232
files = os.path.join(self.path, 'info', 'files')
3333
with open(files) as f:
34-
return [native(l.strip()) for l in f.readlines()]
34+
return [native(line.strip()) for line in f.readlines()]

auditwheel/main_repair.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from os.path import isfile, exists, abspath, basename
2+
3+
from auditwheel.patcher import Patchelf
24
from .policy import (load_policies, get_policy_name, get_priority_by_name,
35
POLICY_PRIORITY_HIGHEST)
46
from .tools import EnvironmentDefault
@@ -45,14 +47,11 @@ def configure_parser(sub_parsers):
4547

4648
def execute(args, p):
4749
import os
48-
from distutils.spawn import find_executable
4950
from .repair import repair_wheel
5051
from .wheel_abi import analyze_wheel_abi, NonPlatformWheel
5152

5253
if not isfile(args.WHEEL_FILE):
5354
p.error('cannot access %s. No such file' % args.WHEEL_FILE)
54-
if find_executable('patchelf') is None:
55-
p.error('cannot find the \'patchelf\' tool, which is required')
5655

5756
logger.info('Repairing %s', basename(args.WHEEL_FILE))
5857

@@ -81,11 +80,13 @@ def execute(args, p):
8180
(args.WHEEL_FILE, args.PLAT))
8281
p.error(msg)
8382

83+
patcher = Patchelf()
8484
out_wheel = repair_wheel(args.WHEEL_FILE,
8585
abi=args.PLAT,
8686
lib_sdir=args.LIB_SDIR,
8787
out_dir=args.WHEEL_DIR,
88-
update_tags=args.UPDATE_TAGS)
88+
update_tags=args.UPDATE_TAGS,
89+
patcher=patcher)
8990

9091
if out_wheel is not None:
9192
analyzed_tag = analyze_wheel_abi(out_wheel).overall_tag
@@ -98,6 +99,7 @@ def execute(args, p):
9899
abi=analyzed_tag,
99100
lib_sdir=args.LIB_SDIR,
100101
out_dir=args.WHEEL_DIR,
101-
update_tags=args.UPDATE_TAGS)
102+
update_tags=args.UPDATE_TAGS,
103+
patcher=patcher)
102104

103105
logger.info('\nFixed-up wheel written to %s', out_wheel)

auditwheel/patcher.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import re
2+
from distutils.spawn import find_executable
3+
from subprocess import check_call, check_output, CalledProcessError
4+
5+
6+
class ElfPatcher:
7+
def replace_needed(self,
8+
file_name: str,
9+
so_name: str,
10+
new_so_name: str) -> None:
11+
raise NotImplementedError
12+
13+
def set_soname(self,
14+
file_name: str,
15+
new_so_name: str) -> None:
16+
raise NotImplementedError
17+
18+
def set_rpath(self,
19+
file_name: str,
20+
rpath: str) -> None:
21+
raise NotImplementedError
22+
23+
24+
def _verify_patchelf() -> None:
25+
"""This function looks for the ``patchelf`` external binary in the PATH,
26+
checks for the required version, and throws an exception if a proper
27+
version can't be found. Otherwise, silcence is golden
28+
"""
29+
if not find_executable('patchelf'):
30+
raise ValueError('Cannot find required utility `patchelf` in PATH')
31+
try:
32+
version = check_output(['patchelf', '--version']).decode("utf-8")
33+
except CalledProcessError:
34+
raise ValueError('Could not call `patchelf` binary')
35+
36+
m = re.match(r'patchelf\s+(\d+(.\d+)?)', version)
37+
if m and tuple(int(x) for x in m.group(1).split('.')) >= (0, 9):
38+
return
39+
raise ValueError(('patchelf %s found. auditwheel repair requires '
40+
'patchelf >= 0.9.') %
41+
version)
42+
43+
44+
class Patchelf(ElfPatcher):
45+
def __init__(self):
46+
_verify_patchelf()
47+
48+
def replace_needed(self,
49+
file_name: str,
50+
so_name: str,
51+
new_so_name: str) -> None:
52+
check_call(['patchelf', '--replace-needed', so_name, new_so_name,
53+
file_name])
54+
55+
def set_soname(self,
56+
file_name: str,
57+
new_so_name: str) -> None:
58+
check_call(['patchelf', '--set-soname', new_so_name, file_name])
59+
60+
def set_rpath(self,
61+
file_name: str,
62+
rpath: str) -> None:
63+
64+
check_call(['patchelf', '--remove-rpath', file_name])
65+
check_call(['patchelf', '--force-rpath', '--set-rpath',
66+
rpath, file_name])

auditwheel/repair.py

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1+
import itertools
2+
import logging
13
import os
24
import re
35
import stat
46
import shutil
5-
import itertools
6-
import functools
7-
from os.path import exists, relpath, dirname, basename, abspath, isabs
7+
from os.path import exists, basename, abspath, isabs
88
from os.path import join as pjoin
9-
from subprocess import check_call, check_output, CalledProcessError
10-
from distutils.spawn import find_executable
119
from typing import Dict, Optional
12-
import logging
1310

14-
from .policy import get_replace_platforms
15-
from .wheeltools import InWheelCtx, add_platforms
16-
from .wheel_abi import get_wheel_elfdata
11+
from auditwheel.patcher import ElfPatcher
1712
from .elfutils import elf_read_rpaths, elf_read_dt_needed
1813
from .hashfile import hashfile
19-
14+
from .policy import get_replace_platforms
15+
from .wheel_abi import get_wheel_elfdata
16+
from .wheeltools import InWheelCtx, add_platforms
2017

2118
logger = logging.getLogger(__name__)
2219

@@ -28,29 +25,8 @@
2825
re.VERBOSE).match
2926

3027

31-
@functools.lru_cache()
32-
def verify_patchelf():
33-
"""This function looks for the ``patchelf`` external binary in the PATH,
34-
checks for the required version, and throws an exception if a proper
35-
version can't be found. Otherwise, silcence is golden
36-
"""
37-
if not find_executable('patchelf'):
38-
raise ValueError('Cannot find required utility `patchelf` in PATH')
39-
try:
40-
version = check_output(['patchelf', '--version']).decode('utf-8')
41-
except CalledProcessError:
42-
raise ValueError('Could not call `patchelf` binary')
43-
44-
m = re.match(r'patchelf\s+(\d+(.\d+)?)', version)
45-
if m and tuple(int(x) for x in m.group(1).split('.')) >= (0, 9):
46-
return
47-
raise ValueError(('patchelf %s found. auditwheel repair requires '
48-
'patchelf >= 0.9.') %
49-
version)
50-
51-
5228
def repair_wheel(wheel_path: str, abi: str, lib_sdir: str, out_dir: str,
53-
update_tags: bool) -> Optional[str]:
29+
update_tags: bool, patcher: ElfPatcher) -> Optional[str]:
5430

5531
external_refs_by_fn = get_wheel_elfdata(wheel_path)[1]
5632

@@ -83,13 +59,14 @@ def repair_wheel(wheel_path: str, abi: str, lib_sdir: str, out_dir: str,
8359
'library "%s" could not be located') %
8460
soname)
8561

86-
new_soname, new_path = copylib(src_path, dest_dir)
62+
new_soname, new_path = copylib(src_path, dest_dir, patcher)
8763
soname_map[soname] = (new_soname, new_path)
88-
check_call(['patchelf', '--replace-needed', soname,
89-
new_soname, fn])
64+
patcher.replace_needed(fn, soname, new_soname)
9065

9166
if len(ext_libs) > 0:
92-
patchelf_set_rpath(fn, dest_dir)
67+
new_rpath = os.path.relpath(dest_dir, os.path.dirname(fn))
68+
new_rpath = os.path.join('$ORIGIN', new_rpath)
69+
patcher.set_rpath(fn, new_rpath)
9370

9471
# we grafted in a bunch of libraries and modified their sonames, but
9572
# they may have internal dependencies (DT_NEEDED) on one another, so
@@ -99,16 +76,15 @@ def repair_wheel(wheel_path: str, abi: str, lib_sdir: str, out_dir: str,
9976
needed = elf_read_dt_needed(path)
10077
for n in needed:
10178
if n in soname_map:
102-
check_call(['patchelf', '--replace-needed', n,
103-
soname_map[n][0], path])
79+
patcher.replace_needed(path, n, soname_map[n][0])
10480

10581
if update_tags:
10682
ctx.out_wheel = add_platforms(ctx, [abi],
10783
get_replace_platforms(abi))
10884
return ctx.out_wheel
10985

11086

111-
def copylib(src_path, dest_dir):
87+
def copylib(src_path, dest_dir, patcher):
11288
"""Graft a shared library from the system into the wheel and update the
11389
relevant links.
11490
@@ -142,17 +118,9 @@ def copylib(src_path, dest_dir):
142118
if not statinfo.st_mode & stat.S_IWRITE:
143119
os.chmod(dest_path, statinfo.st_mode | stat.S_IWRITE)
144120

145-
verify_patchelf()
146-
check_call(['patchelf', '--set-soname', new_soname, dest_path])
121+
patcher.set_soname(dest_path, new_soname)
147122

148123
if any(itertools.chain(rpaths['rpaths'], rpaths['runpaths'])):
149-
patchelf_set_rpath(dest_path, dest_dir)
124+
patcher.set_rpath(dest_path, dest_dir)
150125

151126
return new_soname, dest_path
152-
153-
154-
def patchelf_set_rpath(fn, libdir):
155-
rpath = pjoin('$ORIGIN', relpath(libdir, dirname(fn)))
156-
logger.debug('Setting RPATH: %s to "%s"', fn, rpath)
157-
check_call(['patchelf', '--remove-rpath', fn])
158-
check_call(['patchelf', '--force-rpath', '--set-rpath', rpath, fn])

tests/integration/test_manylinux.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@ def test_build_repair_numpy(any_manylinux_container, docker_python, io_folder):
211211

212212
# Repair the wheel using the manylinux container
213213
repair_command = (
214-
'auditwheel repair --plat {policy} -w /io /io/{orig_wheel}'
214+
'auditwheel repair '
215+
'--plat {policy} -w /io /io/{orig_wheel}'
215216
).format(policy=policy, orig_wheel=orig_wheel)
216217
docker_exec(manylinux_ctr, repair_command)
217218
filenames = os.listdir(io_folder)

tests/unit/test_elfpatcher.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from subprocess import CalledProcessError
2+
from unittest.mock import patch, call
3+
4+
import pytest
5+
6+
from auditwheel.patcher import Patchelf
7+
8+
9+
@patch("auditwheel.patcher.find_executable")
10+
def test_patchelf_unavailable(find_executable):
11+
find_executable.return_value = False
12+
with pytest.raises(ValueError):
13+
Patchelf()
14+
15+
16+
@patch("auditwheel.patcher.check_output")
17+
def test_patchelf_check_output_fail(check_output):
18+
check_output.side_effect = CalledProcessError(1, "patchelf --version")
19+
with pytest.raises(ValueError, match="Could not call"):
20+
Patchelf()
21+
22+
23+
@patch("auditwheel.patcher.check_output")
24+
@pytest.mark.parametrize("version", ["0.9", "0.9.1", "0.10"])
25+
def test_patchelf_version_check(check_output, version):
26+
check_output.return_value.decode.return_value = "patchelf {}".format(version)
27+
Patchelf()
28+
29+
30+
@patch("auditwheel.patcher.check_output")
31+
@pytest.mark.parametrize("version", ["0.8", "0.8.1", "0.1"])
32+
def test_patchelf_version_check_fail(check_output, version):
33+
check_output.return_value.decode.return_value = "patchelf {}".format(version)
34+
with pytest.raises(ValueError, match="patchelf {} found".format(version)):
35+
Patchelf()
36+
37+
38+
@patch("auditwheel.patcher._verify_patchelf")
39+
@patch("auditwheel.patcher.check_call")
40+
class TestPatchElf:
41+
""""Validate that patchelf is invoked with the correct arguments."""
42+
43+
def test_replace_needed(self, check_call, _):
44+
patcher = Patchelf()
45+
filename = "test.so"
46+
soname_old = "TEST_OLD"
47+
soname_new = "TEST_NEW"
48+
patcher.replace_needed(filename, soname_old, soname_new)
49+
check_call.assert_called_once_with(['patchelf', '--replace-needed',
50+
soname_old, soname_new, filename])
51+
52+
def test_set_soname(self, check_call, _):
53+
patcher = Patchelf()
54+
filename = "test.so"
55+
soname_new = "TEST_NEW"
56+
patcher.set_soname(filename, soname_new)
57+
check_call.assert_called_once_with(['patchelf', '--set-soname',
58+
soname_new, filename])
59+
60+
def test_set_rpath(self, check_call, _):
61+
patcher = Patchelf()
62+
patcher.set_rpath("test.so", "$ORIGIN/.lib")
63+
expected_args = [call(['patchelf', '--remove-rpath', 'test.so']),
64+
call(['patchelf', '--force-rpath', '--set-rpath',
65+
'$ORIGIN/.lib', 'test.so'])]
66+
67+
assert check_call.call_args_list == expected_args

0 commit comments

Comments
 (0)