Skip to content

Commit e34bb79

Browse files
lkollarmayeut
authored andcommitted
feature: add support for musllinux
Detect the running libc at startup & select the correct policy file based on this. For musllinux, we select only one musllinux policy depending on the running version of musl.
1 parent 9d47670 commit e34bb79

File tree

7 files changed

+273
-8
lines changed

7 files changed

+273
-8
lines changed

auditwheel/error.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class AuditwheelException(Exception):
2+
pass
3+
4+
5+
class InvalidLibc(AuditwheelException):
6+
pass

auditwheel/lddtree.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
import errno
1818
import logging
1919
import functools
20+
from pathlib import Path
2021
from typing import List, Dict, Optional, Any, Tuple
2122

2223
from elftools.elf.elffile import ELFFile
24+
from .libc import get_libc, Libc
25+
2326

2427
log = logging.getLogger(__name__)
2528
__all__ = ['lddtree']
@@ -195,11 +198,32 @@ def load_ld_paths(root: str = '/', prefix: str = '') -> Dict[str, List[str]]:
195198
# on a per-ELF basis so it can get turned into the right thing.
196199
ldpaths['env'] = parse_ld_paths(env_ldpath, path='')
197200

198-
# Load up /etc/ld.so.conf.
199-
ldpaths['conf'] = parse_ld_so_conf(root + prefix + '/etc/ld.so.conf',
200-
root=root)
201-
# the trusted directories are not necessarily in ld.so.conf
202-
ldpaths['conf'].extend(['/lib', '/lib64/', '/usr/lib', '/usr/lib64'])
201+
libc = get_libc()
202+
if libc == Libc.MUSL:
203+
# from https://git.musl-libc.org/cgit/musl/tree/ldso
204+
# /dynlink.c?id=3f701faace7addc75d16dea8a6cd769fa5b3f260#n1063
205+
root_prefix = Path(root) / prefix
206+
ld_musl = list((root_prefix / 'etc').glob("ld-musl-*.path"))
207+
assert len(ld_musl) <= 1
208+
if len(ld_musl) == 0:
209+
ldpaths['conf'] = [
210+
root + '/lib',
211+
root + '/usr/local/lib',
212+
root + '/usr/lib'
213+
]
214+
else:
215+
ldpaths['conf'] = []
216+
for ldpath in ld_musl[0].read_text().split(':'):
217+
ldpath_stripped = ldpath.strip()
218+
if ldpath_stripped == "":
219+
continue
220+
ldpaths['conf'].append(root + ldpath_stripped)
221+
else:
222+
# Load up /etc/ld.so.conf.
223+
ldpaths['conf'] = parse_ld_so_conf(root + prefix + '/etc/ld.so.conf',
224+
root=root)
225+
# the trusted directories are not necessarily in ld.so.conf
226+
ldpaths['conf'].extend(['/lib', '/lib64/', '/usr/lib', '/usr/lib64'])
203227
log.debug('linker ldpaths: %s', ldpaths)
204228
return ldpaths
205229

auditwheel/libc.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import logging
2+
from enum import IntEnum
3+
4+
from .error import InvalidLibc
5+
from .musllinux import find_musl_libc
6+
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class Libc(IntEnum):
12+
GLIBC = 1,
13+
MUSL = 2,
14+
15+
16+
def get_libc() -> Libc:
17+
try:
18+
find_musl_libc()
19+
logger.debug("Detected musl libc")
20+
return Libc.MUSL
21+
except InvalidLibc:
22+
logger.debug("Falling back to GNU libc")
23+
return Libc.GLIBC

auditwheel/musllinux.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import logging
2+
import pathlib
3+
import subprocess
4+
import re
5+
from typing import NamedTuple
6+
7+
from auditwheel.error import InvalidLibc
8+
9+
LOG = logging.getLogger(__name__)
10+
11+
12+
class MuslVersion(NamedTuple):
13+
major: int
14+
minor: int
15+
patch: int
16+
17+
18+
def find_musl_libc() -> pathlib.Path:
19+
try:
20+
ldd = subprocess.check_output(["ldd", "/bin/ls"], errors='strict')
21+
except (subprocess.CalledProcessError, FileNotFoundError):
22+
LOG.error("Failed to determine libc version", exc_info=True)
23+
raise InvalidLibc
24+
25+
match = re.search(
26+
r"libc\.musl-(?P<platform>\w+)\.so.1 " # TODO drop the platform
27+
r"=> (?P<path>[/\-\w.]+)",
28+
ldd)
29+
30+
if not match:
31+
raise InvalidLibc
32+
33+
return pathlib.Path(match.group("path"))
34+
35+
36+
def get_musl_version(ld_path: pathlib.Path) -> MuslVersion:
37+
try:
38+
ld = subprocess.run(
39+
[ld_path],
40+
check=False,
41+
errors='strict',
42+
stderr=subprocess.PIPE
43+
).stderr
44+
except FileNotFoundError:
45+
LOG.error("Failed to determine musl version", exc_info=True)
46+
raise InvalidLibc
47+
48+
match = re.search(
49+
r"Version "
50+
r"(?P<major>\d+)."
51+
r"(?P<minor>\d+)."
52+
r"(?P<patch>\d+)",
53+
ld)
54+
if not match:
55+
raise InvalidLibc
56+
57+
return MuslVersion(
58+
int(match.group("major")),
59+
int(match.group("minor")),
60+
int(match.group("patch")))

auditwheel/policy/__init__.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22
import json
33
import platform as _platform_module
44
from collections import defaultdict
5+
from pathlib import Path
56
from typing import Dict, List, Optional, Set
67
from os.path import join, dirname, abspath
78
import logging
89

10+
from ..libc import get_libc, Libc
11+
from ..musllinux import find_musl_libc, get_musl_version
12+
13+
14+
_HERE = Path(__file__).parent
915

1016
logger = logging.getLogger(__name__)
1117

@@ -22,6 +28,7 @@ def get_arch_name() -> str:
2228

2329

2430
_ARCH_NAME = get_arch_name()
31+
_LIBC = get_libc()
2532

2633

2734
def _validate_pep600_compliance(policies) -> None:
@@ -56,18 +63,64 @@ def _validate_pep600_compliance(policies) -> None:
5663
symbol_versions[arch] = symbol_versions_arch
5764

5865

59-
with open(join(dirname(abspath(__file__)), 'manylinux-policy.json')) as f:
66+
_POLICY_JSON_MAP = {
67+
Libc.GLIBC: _HERE / 'manylinux-policy.json',
68+
Libc.MUSL: _HERE / 'musllinux-policy.json',
69+
}
70+
71+
72+
def _get_musl_policy():
73+
if _LIBC != Libc.MUSL:
74+
return None
75+
musl_version = get_musl_version(find_musl_libc())
76+
return f'musllinux_{musl_version.major}_{musl_version.minor}'
77+
78+
79+
_MUSL_POLICY = _get_musl_policy()
80+
81+
82+
def _fixup_musl_libc_soname(whitelist):
83+
if _LIBC != Libc.MUSL:
84+
return whitelist
85+
soname_map = {
86+
"libc.so": {
87+
"x86_64": "libc.musl-x86_64.so.1",
88+
"i686": "libc.musl-x86.so.1",
89+
"aarch64": "libc.musl-aarch64.so.1",
90+
"s390x": "libc.musl-s390x.so.1",
91+
"ppc64le": "libc.musl-ppc64le.so.1",
92+
"armv7l": "libc.musl-armv7.so.1",
93+
}
94+
}
95+
new_whitelist = []
96+
for soname in whitelist:
97+
if soname in soname_map:
98+
new_soname = soname_map[soname][_ARCH_NAME]
99+
logger.debug(f"Replacing whitelisted '{soname}' by '{new_soname}'")
100+
new_whitelist.append(new_soname)
101+
else:
102+
new_whitelist.append(soname)
103+
return new_whitelist
104+
105+
106+
with _POLICY_JSON_MAP[_LIBC].open() as f:
60107
_POLICIES = []
61108
_policies_temp = json.load(f)
62109
_validate_pep600_compliance(_policies_temp)
63110
for _p in _policies_temp:
111+
if _MUSL_POLICY is not None and \
112+
_p['name'] not in {'linux', _MUSL_POLICY}:
113+
continue
64114
if _ARCH_NAME in _p['symbol_versions'].keys() or _p['name'] == 'linux':
65115
if _p['name'] != 'linux':
66116
_p['symbol_versions'] = _p['symbol_versions'][_ARCH_NAME]
67117
_p['name'] = _p['name'] + '_' + _ARCH_NAME
68118
_p['aliases'] = [alias + '_' + _ARCH_NAME
69119
for alias in _p['aliases']]
120+
_p['lib_whitelist'] = _fixup_musl_libc_soname(_p['lib_whitelist'])
70121
_POLICIES.append(_p)
122+
if _LIBC == Libc.MUSL:
123+
assert len(_POLICIES) == 2, _POLICIES
71124

72125
POLICY_PRIORITY_HIGHEST = max(p['priority'] for p in _POLICIES)
73126
POLICY_PRIORITY_LOWEST = min(p['priority'] for p in _POLICIES)
@@ -78,8 +131,8 @@ def load_policies():
78131

79132

80133
def _load_policy_schema():
81-
with open(join(dirname(abspath(__file__)), 'policy-schema.json')) as f:
82-
schema = json.load(f)
134+
with open(join(dirname(abspath(__file__)), 'policy-schema.json')) as f_:
135+
schema = json.load(f_)
83136
return schema
84137

85138

@@ -124,6 +177,8 @@ def get_replace_platforms(name: str) -> List[str]:
124177
return []
125178
if name.startswith('manylinux_'):
126179
return ['linux_' + '_'.join(name.split('_')[3:])]
180+
if name.startswith('musllinux_'):
181+
return ['linux_' + '_'.join(name.split('_')[3:])]
127182
return ['linux_' + '_'.join(name.split('_')[1:])]
128183

129184

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[
2+
{"name": "linux",
3+
"aliases": [],
4+
"priority": 0,
5+
"symbol_versions": {},
6+
"lib_whitelist": []
7+
},
8+
{"name": "musllinux_1_1",
9+
"aliases": [],
10+
"priority": 100,
11+
"symbol_versions": {
12+
"i686": {
13+
},
14+
"x86_64": {
15+
},
16+
"aarch64": {
17+
},
18+
"ppc64le": {
19+
},
20+
"s390x": {
21+
},
22+
"armv7l": {
23+
}
24+
},
25+
"lib_whitelist": [
26+
"libc.so"
27+
]},
28+
{"name": "musllinux_1_2",
29+
"aliases": [],
30+
"priority": 90,
31+
"symbol_versions": {
32+
"i686": {
33+
},
34+
"x86_64": {
35+
},
36+
"aarch64": {
37+
},
38+
"ppc64le": {
39+
},
40+
"s390x": {
41+
},
42+
"armv7l": {
43+
}
44+
},
45+
"lib_whitelist": [
46+
"libc.so"
47+
]}
48+
]

tests/unit/test_musllinux.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import subprocess
2+
from unittest.mock import patch
3+
4+
import pytest
5+
6+
from auditwheel.musllinux import find_musl_libc, get_musl_version
7+
from auditwheel.error import InvalidLibc
8+
9+
10+
@patch("auditwheel.musllinux.subprocess.check_output")
11+
def test_find_musllinux_no_ldd(check_output_mock):
12+
check_output_mock.side_effect = FileNotFoundError()
13+
with pytest.raises(InvalidLibc):
14+
find_musl_libc()
15+
16+
17+
@patch("auditwheel.musllinux.subprocess.check_output")
18+
def test_find_musllinux_ldd_error(check_output_mock):
19+
check_output_mock.side_effect = subprocess.CalledProcessError(1, "ldd")
20+
with pytest.raises(InvalidLibc):
21+
find_musl_libc()
22+
23+
24+
@patch("auditwheel.musllinux.subprocess.check_output")
25+
def test_find_musllinux_not_found(check_output_mock):
26+
check_output_mock.return_value = ""
27+
with pytest.raises(InvalidLibc):
28+
find_musl_libc()
29+
30+
31+
def test_get_musl_version_invalid_path():
32+
with pytest.raises(InvalidLibc):
33+
get_musl_version("/tmp/no/executable/here")
34+
35+
36+
@patch("auditwheel.musllinux.subprocess.run")
37+
def test_get_musl_version_invalid_version(run_mock):
38+
run_mock.return_value = subprocess.CompletedProcess([], 1, None, "Version 1.1")
39+
with pytest.raises(InvalidLibc):
40+
get_musl_version("anything")
41+
42+
43+
@patch("auditwheel.musllinux.subprocess.run")
44+
def test_get_musl_version_valid_version(run_mock):
45+
run_mock.return_value = subprocess.CompletedProcess([], 1, None, "Version 5.6.7")
46+
version = get_musl_version("anything")
47+
assert version.major == 5
48+
assert version.minor == 6
49+
assert version.patch == 7

0 commit comments

Comments
 (0)