Skip to content

Commit 78ba370

Browse files
committed
Update manylinux detection to be robust to incompatible ABIs
`armv7l` machine overlaps multiple ABI (`armhf`, `armel`). The same goes for `i686` when running on `x86_64` kernel (`i686`, `x32`). This commit checks that ABI is compatible with the ones defined in PEP 513/571/599
1 parent 2a87d1c commit 78ba370

15 files changed

+327
-5
lines changed

MANIFEST.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ include tox.ini
88

99
recursive-include docs *
1010
recursive-include tests *.py
11+
recursive-include tests hello-world-*
1112

1213
exclude .travis.yml
1314
exclude dev-requirements.txt
15+
exclude tests/build-hello-world.sh
16+
exclude tests/hello-world.c
1417

1518
prune docs/_build
1619
prune tasks

packaging/tags.py

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import os
1818
import platform
1919
import re
20+
import struct
2021
import sys
2122
import sysconfig
2223
import warnings
@@ -27,6 +28,7 @@
2728
from typing import (
2829
Dict,
2930
FrozenSet,
31+
IO,
3032
Iterable,
3133
Iterator,
3234
List,
@@ -505,16 +507,143 @@ def _have_compatible_glibc(required_major, minimum_minor):
505507
return _check_glibc_version(version_str, required_major, minimum_minor)
506508

507509

510+
# Python does not provide platform information at sufficient granularity to
511+
# identify the architecture of the running executable in some cases, so we
512+
# determine it dynamically by reading the information from the running
513+
# process. This only applies on Linux, which uses the ELF format.
514+
class _ELFFileHeader(object):
515+
# https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
516+
class _InvalidELFFileHeader(ValueError):
517+
"""
518+
An invalid ELF file header was found.
519+
"""
520+
521+
ELF_MAGIC_NUMBER = 0x7F454C46
522+
ELFCLASS32 = 1
523+
ELFCLASS64 = 2
524+
ELFDATA2LSB = 1
525+
ELFDATA2MSB = 2
526+
EM_386 = 3
527+
EM_S390 = 22
528+
EM_ARM = 40
529+
EM_X86_64 = 62
530+
EF_ARM_ABIMASK = 0xFF000000
531+
EF_ARM_ABI_VER5 = 0x05000000
532+
EF_ARM_ABI_FLOAT_HARD = 0x00000400
533+
534+
def __init__(self, file):
535+
# type: (IO[bytes]) -> None
536+
def unpack(fmt):
537+
# type: (str) -> int
538+
try:
539+
result, = struct.unpack(
540+
fmt, file.read(struct.calcsize(fmt))
541+
) # type: (int, )
542+
except struct.error:
543+
raise _ELFFileHeader._InvalidELFFileHeader()
544+
return result
545+
546+
self.e_ident_magic = unpack(">I")
547+
if self.e_ident_magic != self.ELF_MAGIC_NUMBER:
548+
raise _ELFFileHeader._InvalidELFFileHeader()
549+
self.e_ident_class = unpack("B")
550+
if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}:
551+
raise _ELFFileHeader._InvalidELFFileHeader()
552+
self.e_ident_data = unpack("B")
553+
if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}:
554+
raise _ELFFileHeader._InvalidELFFileHeader()
555+
self.e_ident_version = unpack("B")
556+
self.e_ident_osabi = unpack("B")
557+
self.e_ident_abiversion = unpack("B")
558+
self.e_ident_pad = file.read(7)
559+
format_h = "<H" if self.e_ident_data == self.ELFDATA2LSB else ">H"
560+
format_i = "<I" if self.e_ident_data == self.ELFDATA2LSB else ">I"
561+
format_q = "<Q" if self.e_ident_data == self.ELFDATA2LSB else ">Q"
562+
format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q
563+
self.e_type = unpack(format_h)
564+
self.e_machine = unpack(format_h)
565+
self.e_version = unpack(format_i)
566+
self.e_entry = unpack(format_p)
567+
self.e_phoff = unpack(format_p)
568+
self.e_shoff = unpack(format_p)
569+
self.e_flags = unpack(format_i)
570+
self.e_ehsize = unpack(format_h)
571+
self.e_phentsize = unpack(format_h)
572+
self.e_phnum = unpack(format_h)
573+
self.e_shentsize = unpack(format_h)
574+
self.e_shnum = unpack(format_h)
575+
self.e_shstrndx = unpack(format_h)
576+
577+
578+
def _get_elf_header():
579+
# type: () -> Optional[_ELFFileHeader]
580+
try:
581+
with open(sys.executable, "rb") as f:
582+
elf_header = _ELFFileHeader(f)
583+
except (IOError, OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader):
584+
return None
585+
return elf_header
586+
587+
588+
def _is_linux_armhf():
589+
# type: () -> bool
590+
# hard-float ABI can be detected from the ELF header of the running
591+
# process
592+
# https://static.docs.arm.com/ihi0044/g/aaelf32.pdf
593+
elf_header = _get_elf_header()
594+
if elf_header is None:
595+
return False
596+
result = elf_header.e_ident_class == elf_header.ELFCLASS32
597+
result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
598+
result &= elf_header.e_machine == elf_header.EM_ARM
599+
result &= (
600+
elf_header.e_flags & elf_header.EF_ARM_ABIMASK
601+
) == elf_header.EF_ARM_ABI_VER5
602+
result &= (
603+
elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD
604+
) == elf_header.EF_ARM_ABI_FLOAT_HARD
605+
return result
606+
607+
608+
def _is_linux_i686():
609+
# type: () -> bool
610+
elf_header = _get_elf_header()
611+
if elf_header is None:
612+
return False
613+
result = elf_header.e_ident_class == elf_header.ELFCLASS32
614+
result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
615+
result &= elf_header.e_machine == elf_header.EM_386
616+
return result
617+
618+
619+
def _have_compatible_manylinux_abi(arch):
620+
# type: (str) -> bool
621+
if arch == "armv7l":
622+
return _is_linux_armhf()
623+
if arch == "i686":
624+
return _is_linux_i686()
625+
return True
626+
627+
508628
def _linux_platforms(is_32bit=_32_BIT_INTERPRETER):
509629
# type: (bool) -> Iterator[str]
510630
linux = _normalize_string(distutils.util.get_platform())
511631
if linux == "linux_x86_64" and is_32bit:
512632
linux = "linux_i686"
513-
manylinux_support = (
514-
("manylinux2014", (2, 17)), # CentOS 7 w/ glibc 2.17 (PEP 599)
515-
("manylinux2010", (2, 12)), # CentOS 6 w/ glibc 2.12 (PEP 571)
516-
("manylinux1", (2, 5)), # CentOS 5 w/ glibc 2.5 (PEP 513)
517-
)
633+
manylinux_support = []
634+
_, arch = linux.split("_", 1)
635+
if _have_compatible_manylinux_abi(arch):
636+
if arch in {"x86_64", "i686", "aarch64", "armv7l", "ppc64", "ppc64le", "s390x"}:
637+
manylinux_support.append(
638+
("manylinux2014", (2, 17))
639+
) # CentOS 7 w/ glibc 2.17 (PEP 599)
640+
if arch in {"x86_64", "i686"}:
641+
manylinux_support.append(
642+
("manylinux2010", (2, 12))
643+
) # CentOS 6 w/ glibc 2.12 (PEP 571)
644+
manylinux_support.append(
645+
("manylinux1", (2, 5))
646+
) # CentOS 5 w/ glibc 2.5 (PEP 513)
518647
manylinux_support_iter = iter(manylinux_support)
519648
for name, glibc_version in manylinux_support_iter:
520649
if _is_manylinux_compatible(name, glibc_version):

tests/build-hello-world.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/bash
2+
3+
set -x
4+
set -e
5+
6+
if [ $# -eq 0 ]; then
7+
docker run --rm -v $(pwd):/home/hello-world arm32v5/debian /home/hello-world/build-hello-world.sh incontainer 52
8+
docker run --rm -v $(pwd):/home/hello-world arm32v7/debian /home/hello-world/build-hello-world.sh incontainer 52
9+
docker run --rm -v $(pwd):/home/hello-world i386/debian /home/hello-world/build-hello-world.sh incontainer 52
10+
docker run --rm -v $(pwd):/home/hello-world s390x/debian /home/hello-world/build-hello-world.sh incontainer 64
11+
docker run --rm -v $(pwd):/home/hello-world debian /home/hello-world/build-hello-world.sh incontainer 64
12+
docker run --rm -v $(pwd):/home/hello-world debian /home/hello-world/build-hello-world.sh x32 52
13+
cp -f hello-world-x86_64-i386 hello-world-invalid-magic
14+
printf "\x00" | dd of=hello-world-invalid-magic bs=1 seek=0x00 count=1 conv=notrunc
15+
cp -f hello-world-x86_64-i386 hello-world-invalid-class
16+
printf "\x00" | dd of=hello-world-invalid-class bs=1 seek=0x04 count=1 conv=notrunc
17+
cp -f hello-world-x86_64-i386 hello-world-invalid-data
18+
printf "\x00" | dd of=hello-world-invalid-data bs=1 seek=0x05 count=1 conv=notrunc
19+
head -c 40 hello-world-x86_64-i386 > hello-world-too-short
20+
exit 0
21+
fi
22+
23+
export DEBIAN_FRONTEND=noninteractive
24+
cd /home/hello-world/
25+
apt-get update
26+
apt-get install -y --no-install-recommends gcc libc6-dev
27+
if [ "$1" == "incontainer" ]; then
28+
ARCH=$(dpkg --print-architecture)
29+
CFLAGS=""
30+
else
31+
ARCH=$1
32+
dpkg --add-architecture ${ARCH}
33+
apt-get install -y --no-install-recommends gcc-multilib libc6-dev-${ARCH}
34+
CFLAGS="-mx32"
35+
fi
36+
NAME=hello-world-$(uname -m)-${ARCH}
37+
gcc -Os -s ${CFLAGS} -o ${NAME}-full hello-world.c
38+
head -c $2 ${NAME}-full > ${NAME}
39+
rm -f ${NAME}-full

tests/hello-world-armv7l-armel

52 Bytes
Binary file not shown.

tests/hello-world-armv7l-armhf

52 Bytes
Binary file not shown.

tests/hello-world-invalid-class

52 Bytes
Binary file not shown.

tests/hello-world-invalid-data

52 Bytes
Binary file not shown.

tests/hello-world-invalid-magic

52 Bytes
Binary file not shown.

tests/hello-world-s390x-s390x

64 Bytes
Binary file not shown.

tests/hello-world-too-short

40 Bytes
Binary file not shown.

tests/hello-world-x86_64-amd64

64 Bytes
Binary file not shown.

tests/hello-world-x86_64-i386

52 Bytes
Binary file not shown.

tests/hello-world-x86_64-x32

52 Bytes
Binary file not shown.

tests/hello-world.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include <stdio.h>
2+
3+
int main(int argc, char* argv[])
4+
{
5+
printf("Hello world");
6+
return 0;
7+
}

tests/test_tags.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,150 @@ def test_linux_platforms_manylinux2014(self, monkeypatch):
469469
]
470470
assert platforms == expected
471471

472+
def test_linux_platforms_manylinux2014_armhf_abi(self, monkeypatch):
473+
monkeypatch.setattr(
474+
tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014"
475+
)
476+
monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_armv7l")
477+
monkeypatch.setattr(
478+
sys,
479+
"executable",
480+
os.path.join(os.path.dirname(__file__), "hello-world-armv7l-armhf"),
481+
)
482+
platforms = list(tags._linux_platforms(is_32bit=True))
483+
expected = ["manylinux2014_armv7l", "linux_armv7l"]
484+
assert platforms == expected
485+
486+
def test_linux_platforms_manylinux2014_i386_abi(self, monkeypatch):
487+
monkeypatch.setattr(
488+
tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014"
489+
)
490+
monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64")
491+
monkeypatch.setattr(
492+
sys,
493+
"executable",
494+
os.path.join(os.path.dirname(__file__), "hello-world-x86_64-i386"),
495+
)
496+
platforms = list(tags._linux_platforms(is_32bit=True))
497+
expected = [
498+
"manylinux2014_i686",
499+
"manylinux2010_i686",
500+
"manylinux1_i686",
501+
"linux_i686",
502+
]
503+
assert platforms == expected
504+
505+
def test_linux_platforms_manylinux2014_armv6l(self, monkeypatch):
506+
monkeypatch.setattr(
507+
tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014"
508+
)
509+
monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_armv6l")
510+
platforms = list(tags._linux_platforms(is_32bit=True))
511+
expected = ["linux_armv6l"]
512+
assert platforms == expected
513+
514+
@pytest.mark.parametrize(
515+
"machine, abi, alt_machine",
516+
[("x86_64", "x32", "i686"), ("armv7l", "armel", "armv7l")],
517+
)
518+
def test_linux_platforms_not_manylinux_abi(
519+
self, monkeypatch, machine, abi, alt_machine
520+
):
521+
monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda name, _: True)
522+
monkeypatch.setattr(
523+
distutils.util, "get_platform", lambda: "linux_{}".format(machine)
524+
)
525+
monkeypatch.setattr(
526+
sys,
527+
"executable",
528+
os.path.join(
529+
os.path.dirname(__file__), "hello-world-{}-{}".format(machine, abi)
530+
),
531+
)
532+
platforms = list(tags._linux_platforms(is_32bit=True))
533+
expected = ["linux_{}".format(alt_machine)]
534+
assert platforms == expected
535+
536+
@pytest.mark.parametrize(
537+
"machine, abi, elf_class, elf_data, elf_machine",
538+
[
539+
(
540+
"x86_64",
541+
"x32",
542+
tags._ELFFileHeader.ELFCLASS32,
543+
tags._ELFFileHeader.ELFDATA2LSB,
544+
tags._ELFFileHeader.EM_X86_64,
545+
),
546+
(
547+
"x86_64",
548+
"i386",
549+
tags._ELFFileHeader.ELFCLASS32,
550+
tags._ELFFileHeader.ELFDATA2LSB,
551+
tags._ELFFileHeader.EM_386,
552+
),
553+
(
554+
"x86_64",
555+
"amd64",
556+
tags._ELFFileHeader.ELFCLASS64,
557+
tags._ELFFileHeader.ELFDATA2LSB,
558+
tags._ELFFileHeader.EM_X86_64,
559+
),
560+
(
561+
"armv7l",
562+
"armel",
563+
tags._ELFFileHeader.ELFCLASS32,
564+
tags._ELFFileHeader.ELFDATA2LSB,
565+
tags._ELFFileHeader.EM_ARM,
566+
),
567+
(
568+
"armv7l",
569+
"armhf",
570+
tags._ELFFileHeader.ELFCLASS32,
571+
tags._ELFFileHeader.ELFDATA2LSB,
572+
tags._ELFFileHeader.EM_ARM,
573+
),
574+
(
575+
"s390x",
576+
"s390x",
577+
tags._ELFFileHeader.ELFCLASS64,
578+
tags._ELFFileHeader.ELFDATA2MSB,
579+
tags._ELFFileHeader.EM_S390,
580+
),
581+
],
582+
)
583+
def test_get_elf_header(
584+
self, monkeypatch, machine, abi, elf_class, elf_data, elf_machine
585+
):
586+
path = os.path.join(
587+
os.path.dirname(__file__), "hello-world-{}-{}".format(machine, abi)
588+
)
589+
monkeypatch.setattr(sys, "executable", path)
590+
elf_header = tags._get_elf_header()
591+
assert elf_header.e_ident_class == elf_class
592+
assert elf_header.e_ident_data == elf_data
593+
assert elf_header.e_machine == elf_machine
594+
595+
@pytest.mark.parametrize(
596+
"content", [None, "invalid-magic", "invalid-class", "invalid-data", "too-short"]
597+
)
598+
def test_get_elf_header_bad_excutable(self, monkeypatch, content):
599+
if content:
600+
path = os.path.join(
601+
os.path.dirname(__file__), "hello-world-{}".format(content)
602+
)
603+
else:
604+
path = None
605+
monkeypatch.setattr(sys, "executable", path)
606+
assert tags._get_elf_header() is None
607+
608+
def test_is_linux_armhf_not_elf(self, monkeypatch):
609+
monkeypatch.setattr(tags, "_get_elf_header", lambda: None)
610+
assert not tags._is_linux_armhf()
611+
612+
def test_is_linux_i686_not_elf(self, monkeypatch):
613+
monkeypatch.setattr(tags, "_get_elf_header", lambda: None)
614+
assert not tags._is_linux_i686()
615+
472616

473617
@pytest.mark.parametrize(
474618
"platform_name,dispatch_func",

0 commit comments

Comments
 (0)