Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for musllinux #315

Merged
merged 1 commit into from
Aug 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions auditwheel/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AuditwheelException(Exception):
pass


class InvalidLibc(AuditwheelException):
pass
34 changes: 29 additions & 5 deletions auditwheel/lddtree.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
import errno
import logging
import functools
from pathlib import Path
from typing import List, Dict, Optional, Any, Tuple

from elftools.elf.elffile import ELFFile
from .libc import get_libc, Libc


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

# Load up /etc/ld.so.conf.
ldpaths['conf'] = parse_ld_so_conf(root + prefix + '/etc/ld.so.conf',
root=root)
# the trusted directories are not necessarily in ld.so.conf
ldpaths['conf'].extend(['/lib', '/lib64/', '/usr/lib', '/usr/lib64'])
libc = get_libc()
if libc == Libc.MUSL:
# from https://git.musl-libc.org/cgit/musl/tree/ldso
# /dynlink.c?id=3f701faace7addc75d16dea8a6cd769fa5b3f260#n1063
root_prefix = Path(root) / prefix
ld_musl = list((root_prefix / 'etc').glob("ld-musl-*.path"))
assert len(ld_musl) <= 1
if len(ld_musl) == 0:
ldpaths['conf'] = [
root + '/lib',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the prefix should be taken into account here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. I guess it won't matter until cross-repairing is a thing.
PS: for glibc, some folders are appended without either root or prefix.

root + '/usr/local/lib',
root + '/usr/lib'
]
else:
ldpaths['conf'] = []
for ldpath in ld_musl[0].read_text().split(':'):
ldpath_stripped = ldpath.strip()
if ldpath_stripped == "":
continue
ldpaths['conf'].append(root + ldpath_stripped)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefix is not taken into account here either but I'm not actually sure how the prefix logic is supposed to work in this function so maybe it's fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be:

Suggested change
ldpaths['conf'].append(root + ldpath_stripped)
ldpaths['conf'].append(str(root_prefix / ldpath_stripped))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as earlier, it won't matter until cross-repairing is a thing.
It mimics parse_ld_so_conf where only root is taken into account.
The prefix seems to only be used for the configuration on glibc so I did the same on musl for now.

else:
# Load up /etc/ld.so.conf.
ldpaths['conf'] = parse_ld_so_conf(root + prefix + '/etc/ld.so.conf',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ldpaths['conf'] = parse_ld_so_conf(root + prefix + '/etc/ld.so.conf',
ldpaths['conf'] = parse_ld_so_conf(str(root_prefix / 'etc/ld.so.conf'),

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not touch the glibc code path for now. We can get this cleaned up later.

root=root)
# the trusted directories are not necessarily in ld.so.conf
ldpaths['conf'].extend(['/lib', '/lib64/', '/usr/lib', '/usr/lib64'])
log.debug('linker ldpaths: %s', ldpaths)
return ldpaths

Expand Down
23 changes: 23 additions & 0 deletions auditwheel/libc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import logging
from enum import IntEnum

from .error import InvalidLibc
from .musllinux import find_musl_libc


logger = logging.getLogger(__name__)


class Libc(IntEnum):
GLIBC = 1,
MUSL = 2,


def get_libc() -> Libc:
try:
find_musl_libc()
logger.debug("Detected musl libc")
return Libc.MUSL
except InvalidLibc:
logger.debug("Falling back to GNU libc")
return Libc.GLIBC
60 changes: 60 additions & 0 deletions auditwheel/musllinux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import logging
import pathlib
import subprocess
import re
from typing import NamedTuple

from auditwheel.error import InvalidLibc

LOG = logging.getLogger(__name__)


class MuslVersion(NamedTuple):
major: int
minor: int
patch: int


def find_musl_libc() -> pathlib.Path:
try:
ldd = subprocess.check_output(["ldd", "/bin/ls"], errors='strict')
except (subprocess.CalledProcessError, FileNotFoundError):
LOG.error("Failed to determine libc version", exc_info=True)
raise InvalidLibc

match = re.search(
r"libc\.musl-(?P<platform>\w+)\.so.1 " # TODO drop the platform
r"=> (?P<path>[/\-\w.]+)",
ldd)

if not match:
raise InvalidLibc

return pathlib.Path(match.group("path"))


def get_musl_version(ld_path: pathlib.Path) -> MuslVersion:
try:
ld = subprocess.run(
[ld_path],
check=False,
errors='strict',
stderr=subprocess.PIPE
).stderr
except FileNotFoundError:
LOG.error("Failed to determine musl version", exc_info=True)
raise InvalidLibc

match = re.search(
r"Version "
r"(?P<major>\d+)."
r"(?P<minor>\d+)."
r"(?P<patch>\d+)",
ld)
if not match:
raise InvalidLibc

return MuslVersion(
int(match.group("major")),
int(match.group("minor")),
int(match.group("patch")))
61 changes: 58 additions & 3 deletions auditwheel/policy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
import json
import platform as _platform_module
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Optional, Set
from os.path import join, dirname, abspath
import logging

from ..libc import get_libc, Libc
from ..musllinux import find_musl_libc, get_musl_version


_HERE = Path(__file__).parent

logger = logging.getLogger(__name__)

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


_ARCH_NAME = get_arch_name()
_LIBC = get_libc()


def _validate_pep600_compliance(policies) -> None:
Expand Down Expand Up @@ -56,18 +63,64 @@ def _validate_pep600_compliance(policies) -> None:
symbol_versions[arch] = symbol_versions_arch


with open(join(dirname(abspath(__file__)), 'manylinux-policy.json')) as f:
_POLICY_JSON_MAP = {
Libc.GLIBC: _HERE / 'manylinux-policy.json',
Libc.MUSL: _HERE / 'musllinux-policy.json',
}


def _get_musl_policy():
if _LIBC != Libc.MUSL:
return None
musl_version = get_musl_version(find_musl_libc())
return f'musllinux_{musl_version.major}_{musl_version.minor}'


_MUSL_POLICY = _get_musl_policy()


def _fixup_musl_libc_soname(whitelist):
if _LIBC != Libc.MUSL:
return whitelist
soname_map = {
"libc.so": {
"x86_64": "libc.musl-x86_64.so.1",
"i686": "libc.musl-x86.so.1",
"aarch64": "libc.musl-aarch64.so.1",
"s390x": "libc.musl-s390x.so.1",
"ppc64le": "libc.musl-ppc64le.so.1",
"armv7l": "libc.musl-armv7.so.1",
}
}
new_whitelist = []
for soname in whitelist:
if soname in soname_map:
new_soname = soname_map[soname][_ARCH_NAME]
logger.debug(f"Replacing whitelisted '{soname}' by '{new_soname}'")
new_whitelist.append(new_soname)
else:
new_whitelist.append(soname)
return new_whitelist


with _POLICY_JSON_MAP[_LIBC].open() as f:
_POLICIES = []
_policies_temp = json.load(f)
_validate_pep600_compliance(_policies_temp)
for _p in _policies_temp:
if _MUSL_POLICY is not None and \
_p['name'] not in {'linux', _MUSL_POLICY}:
continue
if _ARCH_NAME in _p['symbol_versions'].keys() or _p['name'] == 'linux':
if _p['name'] != 'linux':
_p['symbol_versions'] = _p['symbol_versions'][_ARCH_NAME]
_p['name'] = _p['name'] + '_' + _ARCH_NAME
_p['aliases'] = [alias + '_' + _ARCH_NAME
for alias in _p['aliases']]
_p['lib_whitelist'] = _fixup_musl_libc_soname(_p['lib_whitelist'])
_POLICIES.append(_p)
if _LIBC == Libc.MUSL:
assert len(_POLICIES) == 2, _POLICIES

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


def _load_policy_schema():
with open(join(dirname(abspath(__file__)), 'policy-schema.json')) as f:
schema = json.load(f)
with open(join(dirname(abspath(__file__)), 'policy-schema.json')) as f_:
schema = json.load(f_)
return schema


Expand Down Expand Up @@ -124,6 +177,8 @@ def get_replace_platforms(name: str) -> List[str]:
return []
if name.startswith('manylinux_'):
return ['linux_' + '_'.join(name.split('_')[3:])]
if name.startswith('musllinux_'):
return ['linux_' + '_'.join(name.split('_')[3:])]
return ['linux_' + '_'.join(name.split('_')[1:])]


Expand Down
48 changes: 48 additions & 0 deletions auditwheel/policy/musllinux-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[
{"name": "linux",
"aliases": [],
"priority": 0,
"symbol_versions": {},
"lib_whitelist": []
},
{"name": "musllinux_1_1",
"aliases": [],
"priority": 100,
"symbol_versions": {
"i686": {
},
"x86_64": {
},
"aarch64": {
},
"ppc64le": {
},
"s390x": {
},
"armv7l": {
}
mayeut marked this conversation as resolved.
Show resolved Hide resolved
},
"lib_whitelist": [
"libc.so"
]},
{"name": "musllinux_1_2",
"aliases": [],
"priority": 90,
"symbol_versions": {
"i686": {
},
"x86_64": {
},
"aarch64": {
},
"ppc64le": {
},
"s390x": {
},
"armv7l": {
}
mayeut marked this conversation as resolved.
Show resolved Hide resolved
},
"lib_whitelist": [
"libc.so"
]}
]
49 changes: 49 additions & 0 deletions tests/unit/test_musllinux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import subprocess
from unittest.mock import patch

import pytest

from auditwheel.musllinux import find_musl_libc, get_musl_version
from auditwheel.error import InvalidLibc


@patch("auditwheel.musllinux.subprocess.check_output")
def test_find_musllinux_no_ldd(check_output_mock):
check_output_mock.side_effect = FileNotFoundError()
with pytest.raises(InvalidLibc):
find_musl_libc()


@patch("auditwheel.musllinux.subprocess.check_output")
def test_find_musllinux_ldd_error(check_output_mock):
check_output_mock.side_effect = subprocess.CalledProcessError(1, "ldd")
with pytest.raises(InvalidLibc):
find_musl_libc()


@patch("auditwheel.musllinux.subprocess.check_output")
def test_find_musllinux_not_found(check_output_mock):
check_output_mock.return_value = ""
with pytest.raises(InvalidLibc):
find_musl_libc()


def test_get_musl_version_invalid_path():
with pytest.raises(InvalidLibc):
get_musl_version("/tmp/no/executable/here")


@patch("auditwheel.musllinux.subprocess.run")
def test_get_musl_version_invalid_version(run_mock):
run_mock.return_value = subprocess.CompletedProcess([], 1, None, "Version 1.1")
with pytest.raises(InvalidLibc):
get_musl_version("anything")


@patch("auditwheel.musllinux.subprocess.run")
def test_get_musl_version_valid_version(run_mock):
run_mock.return_value = subprocess.CompletedProcess([], 1, None, "Version 5.6.7")
version = get_musl_version("anything")
assert version.major == 5
assert version.minor == 6
assert version.patch == 7