Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 513 – 이식 가능한 Linux 빌드 배포판을 위한 플랫폼 태그

Author:
Robert T. McGibbon <rmcgibbo at gmail.com>, Nathaniel J. Smith <njs at pobox.com>
BDFL-Delegate:
Alyssa Coghlan <ncoghlan at gmail.com>
Discussions-To:
Distutils-SIG list
Status:
Superseded
Type:
Informational
Topic:
Packaging
Created:
19-Jan-2016
Post-History:
19-Jan-2016, 25-Jan-2016, 29-Jan-2016
Superseded-By:
600
Resolution:
Distutils-SIG message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 wheel과 같은 Python 패키지 빌드 배포판을 위해 manylinux1_{x86_64,i686}이라는 새로운 플랫폼 태그를 만들 것을 제안합니다. 이 태그의 외부 의존성은 Linux 커널과 코어 사용자 공간 ABI 중 표준화되고 제한된 일부로 한정됩니다. 또한 PyPI가 이 플랫폼 태그가 붙은 휠의 업로드와 배포를 지원하고, pip이 호환되는 플랫폼에서 이러한 패키지의 다운로드와 설치를 지원할 것을 제안합니다.

근거

현재 Windows와 OS X용 바이너리 Python 확장을 배포하는 일은 간단합니다. 개발자와 패키저는 휠(PEP 427, PEP 491)을 빌드하며, 이 휠에는 win32나 macosx_10_6_intel 같은 플랫폼 태그가 부여됩니다. 그런 다음 이 휠을 PyPI에 업로드합니다. 사용자는 pip 같은 도구를 사용하여 이 휠을 다운로드하고 설치할 수 있습니다.

Linux의 경우에는 상황이 훨씬 더 까다롭습니다. 일반적으로 한 Linux 배포판에서 빌드한 컴파일된 Python 확장 모듈은 다른 Linux 배포판에서 동작하지 않으며, 같은 Linux 배포판을 실행하더라도 다른 시스템 라이브러리가 설치된 다른 머신에서는 동작하지 않기도 합니다.

PEP 425 플랫폼 태그를 사용하는 빌드 도구는 특정 Linux 배포판이나 설치된 시스템 라이브러리에 관한 정보를 추적하지 않으며, 그 대신 모든 wheel에 지나치게 모호한 linux_i686 또는 linux_x86_64 태그를 붙입니다. 이러한 모호함 때문에 한 기기에서 컴파일된 linux 태그가 붙은 빌드 배포판이 다른 기기에서 제대로 동작하리라고 기대할 수 없으며, 이 때문에 PyPI는 리눅스용 휠의 업로드를 허용하지 않았습니다.

어떤 linux 시스템에서도 동작하는 wheel 패키지를 컴파일할 수 있다면 이상적일 것입니다. 그러나 PC에서 Android, 사용자 정의 libc를 사용하는 임베디드 시스템에 이르기까지 Linux 시스템은 엄청나게 다양하기 때문에, 일반적으로 이를 보장할 수는 없습니다.

그 대신 우리는 커널+코어 사용자 공간 ABI의 표준 부분집합을 정의합니다. 이 부분집합은 실제로 충분한 호환성을 갖추고 있어서, 이 표준을 따르는 패키지는 흔히 사용되는 데스크톱 및 서버 배포판을 사실상 모두 포함하여 많은 linux 시스템에서 동작합니다. 우리가 이를 알고 있는 것은 이처럼 폭넓게 이식 가능한, 미리 컴파일된 Linux용 Python 확장 모듈을 배포해 온 회사들이 있기 때문입니다. 예를 들어 Canopy [4]를 배포하는 Enthought와 Anaconda [5]를 배포하는 Continuum Analytics가 있습니다.

따라서 우리는 이들 회사에서 얻은 호환성에 관한 교훈을 바탕으로 바이너리 Python wheel이 사용할 기준 manylinux1 플랫폼 태그를 정의하고, 이러한 manylinux1 wheel을 만드는 데 도움이 되는 예비 도구의 구현을 소개합니다.

Linux 간 바이너리 비호환성의 주요 원인

이 명세를 충족하는 wheel 패키지가 많은 linux 플랫폼에서 동작함을 보장하는 표준을 올바르게 정의하려면, Linux에서 미리 컴파일된 바이너리의 이식성을 흔히 가로막는 근본 원인을 이해해야 합니다. 두 가지 주요 원인은 사용자의 시스템에 없는 공유 라이브러리에 대한 의존성과, glibc 같은 특정 핵심 라이브러리의 특정 버전에 대한 의존성입니다.

외부 공유 라이브러리

대부분의 데스크톱 및 서버 linux 배포판에는 시스템 패키지 관리자(예를 들어 Debian 기반 시스템의 APT, RPM 기반 시스템의 yum, Arch linux의 pacman)가 함께 제공되며, 이 패키지 관리자는 여러 역할 가운데 특히 /usr/lib와 같은 시스템 디렉터리에 설치되는 공유 라이브러리의 설치를 관리합니다. 대부분의 단순하지 않은 Python 확장은 이러한 공유 라이브러리 중 하나 이상에 의존하므로, 패키지 관리자를 사용해 설치했거나 LD_LIBRARY_PATH 같은 특정 환경 변수를 설정하여 의존 대상 공유 라이브러리의 위치를 런타임 링커에 알리는 방식으로 수동 설치했거나 간에, 사용자가 적절한 라이브러리(및 그 적절한 버전)를 갖춘 시스템에서만 확장의 함수가 제대로 동작합니다.

핵심 공유 라이브러리의 버전 관리

Python 확장 모듈의 개발자가 외부 공유 라이브러리를 전혀 사용하지 않으려 하더라도, 그 모듈은 일반적으로 GNU C 라이브러리인 glibc에 대한 동적 런타임 의존성을 갖게 됩니다. glibc를 정적으로 링크하는 것이 가능하기는 하지만, glibc를 정적으로 링크한 코드에서는 dlopen() 같은 일부 중요한 C 함수를 호출할 수 없기 때문에 보통 좋지 않은 방법입니다. 실제로는 시스템이 제공하는 glibc에 대한 런타임 공유 라이브러리 의존성을 피할 수 없습니다.

GNU C 라이브러리의 유지 관리자는 하위 호환성을 위해 엄격한 심볼 버전 관리 체계를 따릅니다. 이 방식 덕분에 이전 버전의 glibc에 맞춰 컴파일된 바이너리는 더 새로운 glibc가 있는 시스템에서 실행될 수 있습니다. 그 반대는 일반적으로 성립하지 않습니다. 최신 Linux 배포판에서 컴파일된 바이너리는 오래된 시스템에서는 사용할 수 없는 glibc의 버전이 지정된 함수에 의존하는 경향이 있습니다.

이 때문에 일반적으로 최신 Linux 배포판에서 컴파일된 wheel은 이식성을 갖지 못합니다.

manylinux1 정책

이러한 이유로, 폭넓은 이식성을 확보하려면 Python 휠은

  • 극히 제한된 외부 공유 라이브러리 집합에만 의존해야 하고,
  • 그 외부 공유 라이브러리의 “오래된” 심볼 버전에만 의존해야 하며,
  • 폭넓게 호환되는 커널 ABI에만 의존해야 합니다.

따라서 manylinux1 플랫폼 태그를 받으려면 Python 휠은 (a) 다음 목록에 포함된 SONAME을 가진 라이브러리에만 링크하는 바이너리 실행 파일과 컴파일된 코드를 포함해야 하고,

libpanelw.so.5
libncursesw.so.5
libgcc_s.so.1
libstdc++.so.6
libm.so.6
libdl.so.2
librt.so.1
libc.so.6
libnsl.so.1
libutil.so.1
libpthread.so.0
libresolv.so.2
libX11.so.6
libXext.so.6
libXrender.so.1
libICE.so.6
libSM.so.6
libGL.so.1
libgobject-2.0.so.0
libgthread-2.0.so.0
libglib-2.0.so.0

(b) 이 라이브러리들의 시스템 패키지 관리자 제공 버전이 설치된 기본 CentOS 5.11 [6] 시스템에서 동작해야 합니다.

Fedora 30이 libcrypt.so.1 대신 libcrypt.so.2를 포함하여 출시된 뒤, libcrypt.so.1은 소급하여 허용 목록에서 제거되었습니다.

CentOS 5는 x86_64와 i686 아키텍처에서만 사용할 수 있으므로, 현재 manylinux1 정책이 지원하는 아키텍처도 이 두 가지뿐입니다.

Debian 기반 시스템에서 이 라이브러리들은 다음 패키지로 제공됩니다.

libncurses5 libgcc1 libstdc++6 libc6 libx11-6 libxext6
libxrender1 libice6 libsm6 libgl1-mesa-glx libglib2.0-0

RPM 기반 시스템에서 이 라이브러리들은 다음 패키지로 제공됩니다.

ncurses libgcc libstdc++ glibc libXext libXrender
libICE libSM mesa-libGL glib2

이 목록은 Canopy [4] 및 Anaconda [5] 배포의 외부 공유 라이브러리 의존성을 확인하여 작성했습니다. 두 배포 모두 가장 인기 있는 Python 모듈의 폭넓은 배열을 포함하고 있으며, 실제 사용 환경의 광범위한 Linux 시스템에서 동작함이 실무적으로 확인되었습니다.

위에 나열된 허용 시스템 라이브러리 중 다수는 하위 호환성을 위해 심볼 버전 관리 방식을 사용합니다. 이 라이브러리들의 CentOS 5.11 버전이 제공하는 최신 심볼 버전은 다음과 같습니다.

GLIBC_2.5
CXXABI_3.4.8
GLIBCXX_3.4.9
GCC_4.2.0

따라서 요구 사항 (b)에 따라, 위 공유 라이브러리의 버전이 지정된 심볼에 의존하는 휠은 다음 버전의 심볼에만 의존할 수 있습니다.

GLIBC <= 2.5
CXXABI <= 3.4.8
GLIBCXX <= 3.4.9
GCC <= 4.2.0

이 권고 사항은 2016년 1월에 있었던 관련 논의 [7], [8]의 결과입니다.

아래 권고 사항에서 pip나 PyPI가 이 정책의 세부 사항을 검사하고 강제해야 한다고 제안하지는 않는다는 점에 유의하십시오(win32 같은 기존 플랫폼 태그의 세부 사항을 검사하거나 강제하지 않는 것과 마찬가지입니다). 위 내용은 (a) 패키지 빌더를 위한 조언으로서, 그리고 (b) 어떤 휠이 특정 시스템에서 동작하지 않을 때 책임 소재를 가리는 방법으로서 제공됩니다. 휠이 위 정책을 만족한다면 이는 명세나 설치 도구의 버그이고, 위 정책을 만족하지 않는다면 휠의 버그입니다. 이 접근 방식의 유용한 결과 중 하나는 경험이 쌓임에 따라 추가 업데이트와 조정의 가능성을 열어 둔다는 점입니다. 예를 들어, 동일한 시스템을 대상으로 하고 동일한 manylinux1 플랫폼 태그를 사용하되(따라서 pip나 PyPI를 더 변경할 필요가 없음), 문제가 있는 것으로 밝혀진 라이브러리를 제거하거나 안전한 것으로 밝혀진 라이브러리를 추가하도록 위 목록을 조정한 “manylinux 1.1” 정책을 둘 수도 있습니다.

libpythonX.Y.so.1

libpythonX.Y.so.1은 manylinux1 확장이 링크할 수 있는 라이브러리 목록에 포함되어 있지 않다는 점에 유의하십시오. 거의 모든 경우에 libpythonX.Y.so.1에 명시적으로 링크할 필요는 없습니다. ELF 링크 방식에 따라, 인터프리터에 로드되는 확장 모듈은 확장 자체가 libpython에 명시적으로 링크되었는지 여부와 관계없이 인터프리터의 모든 심볼에 자동으로 접근할 수 있기 때문입니다. 게다가 libpython에 명시적으로 링크하면, Python이 --enable-shared로 빌드되지 않은 흔한 구성에서 문제가 생깁니다. 특히 Debian과 Ubuntu 시스템에서는 apt install pythonX.Y가 libpythonX.Y.so.1을 설치조차 하지 않으므로, libpythonX.Y.so.1에 실제로 의존하는 휠은 임포트에 실패할 수 있습니다.

이런 방식으로 링크된 확장이 동작하지 않을 수 있는 상황이 하나 있습니다. 호스트 프로그램(예: apache2)이 dlopen()을 사용하여 CPython 인터프리터를 내장한 모듈(예: mod_wsgi)을 로드하면서 RTLD_GLOBAL 플래그를 dlopen()에 전달하지 않는 경우, 내장된 CPython은 스스로 libpythonX.Y.so.1에 명시적으로 링크하지 않은 확장 모듈을 로드할 수 없습니다. 다행히 apache2는 RTLD_GLOBAL 플래그를 실제로 설정하며, dlopen으로 로드되는 플러그인을 통해 CPython을 내장하는 프로그램 중 저희가 찾을 수 있었던 다른 모든 프로그램도 마찬가지이므로, 실제로는 심각한 문제가 아닌 것으로 보입니다. 상당히 드문 예외적 상황에서의 이론적 비호환성보다는 Debian/Ubuntu와의 비호환성이 더 큰 문제입니다.

이는 manylinux1의 범위를 넘어서는 상당히 복잡하고 미묘한 문제입니다. 자세한 논의는 [9], [10], [11]을 참고하십시오.

UCS-2 대 UCS-4 빌드

CPython 2.x의 모든 버전과 CPython 3.0부터 3.2까지(3.2 포함)는 ABI가 서로 호환되지 않는 두 가지 모드로 빌드할 수 있습니다. --enable-unicode=ucs2 configure 플래그를 사용한 빌드는 유니코드 데이터를 UCS-2(실제로는 UTF-16) 형식으로 저장하고, --enable-unicode=ucs4 configure 플래그를 사용한 빌드는 유니코드 데이터를 UCS-4로 저장합니다. (CPython 3.3 이상은 항상 UCS-4를 지원하는 다른 저장 방식을 사용합니다.) ucs2 휠이 ucs4 CPython에 설치되지 않도록, 또 그 반대의 경우도 일어나지 않도록 하려면 무언가 조치를 취해야 합니다.

이 PEP의 이전 버전에는 이러한 구버전 CPython을 대상으로 하는 manylinux1 휠이 항상 ucs4 ABI를 사용해야 한다는 요구 사항이 포함되어 있었습니다. 그러나 PEP가 처음 승인된 뒤 구현되기 전 사이에, pip과 wheel이 해당 CPython 버전에 대해 ABI 호환성의 이러한 측면을 추적하고 검사하는 기능을 정식으로 지원하게 되었으며, 이것이 더 나은 해결책입니다. 따라서 이제는 manylinux1 플랫폼 태그를 어떤 ABI 태그와도 함께 사용할 수 있도록 허용합니다. 하지만 호환성을 유지하려면 모든 manylinux1 휠이 자명하지 않은 abi 태그를 포함하도록 보장하는 것이 매우 중요합니다. 예를 들어, ucs4 CPython을 대상으로 빌드된 휠은 다음과 같은 이름을 가질 수 있습니다:

PKG-VERSION-cp27-cp27mu-manylinux1_x86_64.whl
                 ^^^^^^ Good!

반면 ucs2 ABI를 대상으로 빌드된 휠은 다음과 같은 이름을 가질 수 있습니다:

PKG-VERSION-cp27-cp27m-manylinux1_x86_64.whl
                 ^^^^^ Okay!

하지만 다음과 같은 이름의 휠은 절대 있어서는 안 됩니다:

PKG-VERSION-cp27-none-manylinux1_x86_64.whl
                 ^^^^ BAD! Don't do this!

이 휠은 ucs2 빌드와 ucs4 빌드 모두와 동시에 호환된다고 주장하는데, 이는 잘못된 것입니다.

참고로, Linux CPython 배포자들 사이에서는 ucs4 ABI가 훨씬 더 널리 쓰이는 것으로 보인다는 점을 밝혀 둡니다.

fpectl 빌드 대 fpectl 없는 빌드

현존하는 모든 CPython 버전은 configure에 --with-fpectl 플래그를 주거나 주지 않고 빌드할 수 있습니다. 그런데 이것이 CPython ABI를 바꾼다는 사실이 밝혀졌습니다. fpectl이 없는 CPython을 대상으로 빌드된 확장은 fpectl이 있는 CPython과 항상 호환되지만, 그 반대는 반드시 성립하지는 않습니다. (증상: 임포트 시점에 undefined symbol: PyFPE_jbuf에 대해 불평하는 오류가 발생합니다.) 참고: [16].

따라서 최대한의 호환성을 위해, manylinux1 휠을 빌드하는 데 사용하는 CPython은 반드시 --with-fpectl 플래그 없이 컴파일해야 하며, manylinux1 확장은 PyFPE_jbuf 심볼을 참조해서는 안 됩니다.

규격을 준수하는 휠의 컴파일

glibc, libgcc, libstdc++가 심볼 버전 관리를 처리하는 방식 때문에, 실제로 대부분의 개발자가 일상적인 작업에 사용하는 컴파일러 툴체인으로는 manylinux1을 준수하는 wheel을 빌드할 수 없습니다. 그러므로 pip wheel / bdist_wheel의 기본 동작을 바꾸려고 하지 않습니다. 이들은 계속해서 일반적인 linux_* 플랫폼 태그를 생성할 것이며, 이들을 사용해 manylinux1 태그가 붙은 휠을 생성하려는 개발자는 두 번째 후처리 단계로 태그를 변경해야 합니다.

manylinux1 표준을 충족하는 휠의 컴파일을 지원하기 위해, 두 가지 도구의 초기 초안을 제공합니다.

Docker 이미지

첫 번째 도구는 CentOS 5.11 기반의 Docker 이미지로, manylinux1 휠을 컴파일하기 위한 사용하기 쉬운 독립형 빌드 환경으로 권장됩니다 [12]. 더 최근에 출시된 Linux 배포판에서 컴파일하면 일반적으로 너무 새로운 버전의 심볼에 대한 의존성이 생깁니다. 이 이미지에는 전체 컴파일러 모음(gcc, g++, gfortran 4.8.2)과 함께 Python과 pip의 최신 릴리스가 설치되어 있습니다.

Auditwheel

두 번째 도구는 auditwheel [13]이라는 명령줄 실행 파일로, 패키지 유지 관리자가 서드파티 외부 의존성을 다루는 데 도움이 될 수 있습니다.

서드파티 외부 라이브러리를 사용하는 휠을 위 정책에 맞게 빌드하는 방법은 적어도 세 가지가 있습니다.

  1. 서드파티 라이브러리를 정적으로 링크할 수 있습니다.
  2. 서드파티 공유 라이브러리를 휠이 의존하는 별도의 패키지로 PyPI에 배포할 수 있습니다.
  3. 서드파티 공유 라이브러리를 휠 라이브러리 안에 번들로 포함하고 상대 경로로 링크할 수 있습니다.

이들은 모두 유효한 선택지이며, 서로 다른 패키지와 커뮤니티에서 효과적으로 사용할 수 있습니다. 정적 링크는 일반적으로 빌드 시스템을 패키지별로 수정해야 하며, 서드파티 의존성을 PyPI에 배포하려면 해당 패키지 사용자 커뮤니티 간에 어느 정도 조율이 필요할 수 있습니다.

이러한 방법들의 대안으로, 대부분 자동으로 처리할 수 있는 auditwheel을 소개합니다. 이 도구는 휠 내부의 모든 ELF 파일을 검사해 버전이 지정된 심볼이나 외부 공유 라이브러리에 대한 의존성이 있는지 확인하고, manylinux1 정책을 준수하는지 검증합니다. 여기에는 정책을 준수하는 휠에 새 플랫폼 태그를 추가하는 기능도 포함됩니다. 더 중요한 점은 auditwheel이 외부 공유 라이브러리에 의존하는 휠을 자동으로 수정할 수 있다는 것입니다. 이 도구는 해당 공유 라이브러리를 시스템에서 휠 내부로 복사하고, 런타임에 그 라이브러리를 찾을 수 있도록 적절한 RPATH 항목을 수정합니다. 이렇게 하면 빌드 시스템을 변경하지 않고도 라이브러리를 정적으로 링크한 것과 비슷한 결과를 얻을 수 있습니다. 번들링도 정적 링크와 마찬가지로 저작권 문제를 일으킬 수 있다는 점에 패키지 작성자는 유의하십시오.

Linux에서의 번들 휠

manylinux1 휠에서 서드파티 라이브러리 의존성을 처리하는 방법이 여러 가지라는 점은 알고 있습니다. 그러나 manylinux1 정책은 외부 의존성의 번들링을 권장하며, 이는 많은 Linux 배포판 시스템 패키지 관리자의 패키지 관리 정책에 어긋나는 관행이라는 점을 인정합니다 [14], [15]. 이렇게 하는 주된 목적은 배포판 간 호환성입니다. 또한 PyPI의 manylinux1 휠은 시스템 패키지 관리자를 통해 제공되는 Python 패키지와는 다른 역할을 맡습니다.

이 PEP에서 일반적인 Linux 배포판의 언번들링 정책을 따르지 않도록 권장하기로 한 결정은 다음과 같은 고려 사항에 근거합니다:

  1. 지속적 통합 및 배포 파이프라인이 자동화된 오늘날에는 새 버전을 게시하고 의존성을 업데이트하는 일이 그 정책들이 정해졌던 당시보다 쉬워졌습니다.
  2. pip 사용자는 미리 빌드된 wheel 파일을 사용하는 대신 로컬 빌드를 강제하고 싶다면 여전히 "--no-binary" 옵션을 자유롭게 사용할 수 있습니다.
  3. 최근 널리 쓰이는 컨테이너 기반 배포와 “불변 인프라” 모델은 어차피 애플리케이션 계층에서 상당한 번들링을 수반합니다.
  4. 현재 Windows와 OS X에서는 PyPI를 통해 번들 휠을 배포하는 것이 일반적입니다.
  5. 이 PEP는 앞으로 특정 Linux 배포판에 더 맞춘 바이너리를 제공할 가능성을 배제하지 않습니다.

이 PEP에서 설명하는 모델은 크로스 플랫폼 Python 패키지에 가장 적합합니다. 이러한 패키지는 정적 Windows 및 OS X 휠을 만들기 위해 이미 하고 있는 작업의 상당 부분을 재사용할 수 있기 때문입니다. Linux 고유의 패키지 관리 기능과 더 긴밀하게 상호 작용하기를 원하고 소수의 특정 배포판만 대상으로 삼는 Linux 전용 패키지에는 이 모델이 덜 적합하다는 점을 인정합니다.

보안 영향

Linux에서 중앙 집중식 라이브러리에 대한 의존성의 장점 중 하나는 버그 수정과 보안 업데이트를 시스템 전체에 배포할 수 있다는 것이며, 이러한 라이브러리에 의존하는 애플리케이션은 기반 라이브러리가 업데이트될 때 이러한 패치의 효과를 자동으로 누리게 됩니다. 이는 네트워크 통신이나 암호화를 다루는 패키지의 보안 업데이트에서 특히 중요할 수 있습니다.

따라서 PyPI를 통해 배포되며 OpenSSL처럼 보안에 중요한 라이브러리를 번들링하는 manylinux1 휠은 취약점과 패치가 공개되면 이에 대응해 신속하게 업데이트할 책임을 집니다. 이는 Windows의 바이너리 wheel 배포가 갖는 보안 영향과 매우 유사합니다. Windows 플랫폼에는 시스템 패키지 관리자가 없으므로 바이너리 wheel은 일반적으로 의존성을 함께 번들로 포함합니다. 특히 OpenSSL은 안정 ABI가 없기 때문에 manylinux1 프로파일에 포함될 수 없습니다.

설치 프로그램을 위한 플랫폼 감지

앞에서는 휠이 manylinux1 호환이라는 것이 무엇을 뜻하는지 정의했습니다. 여기서는 Python 설치 환경이 manylinux1 호환이라는 것이 무엇을 뜻하는지 논의합니다. 이는 특히 pip 같은 도구가 manylinux1 태그가 붙은 휠을 설치 대상으로 고려할지 결정할 때 알아야 하는 중요한 사항입니다.

manylinux1 프로필은 널리 쓰이는 상용 Python 배포판의 수많은 사용자 환경에서 이미 동작한다고 알려져 있습니다. 따라서 설치 도구는 달리 판단할 구체적인 이유가 없는 한 시스템이 호환된다고 가정하는 쪽을 택할 것을 제안합니다.

실제로 발생할 가능성이 높은 잠재적 비호환성의 주요 원인은 다음 네 가지입니다:

  • 결국 언젠가는 이 프로필과의 호환성을 깨뜨리는 배포판이 나올 수 있습니다(예: 프로필에 포함된 라이브러리 중 하나가 하위 호환되지 않는 방식으로 ABI를 변경하는 경우)
  • 너무 오래된 Linux 배포판(예: RHEL 4)
  • glibc를 사용하지 않는 linux 배포판(예: musl libc에 기반한 Alpine Linux, 또는 Android)

이러한 경우들에 대처하기 위해 두 갈래의 접근 방식을 제안합니다. 앞으로 생길 수 있는 비호환성에 대처하기 위해, 특정 Python 설치본이 manylinux1과 확실히 호환되는지 또는 호환되지 않는지를 Python 배포자가 알릴 수 있는 메커니즘을 표준화합니다. 이는 _manylinux라는 이름의 모듈을 설치하고 그 모듈의 manylinux1_compatible 속성을 설정하는 방식으로 이루어집니다. 이러한 모듈을 표준 라이브러리에 추가하자고 제안하는 것은 아닙니다. 이것은 단지 배포자와 설치 도구가 서로 만날 수 있도록 정해 둔, 널리 알려진 이름일 뿐입니다. 그러나 배포자가 이 모듈을 추가한다면, site-packages/ 디렉터리가 아니라 표준 라이브러리에 추가해야 합니다. 표준 라이브러리는 virtualenv에 상속되지만(이것이 바라는 동작입니다), site-packages/는 일반적으로 상속되지 않기 때문입니다.

다음으로, 기존 Python 배포판에서 마지막 두 경우에 대처하기 위해, glibc의 존재 여부와 버전을 확인하는 간단하고 신뢰할 수 있는 방법을 제안합니다(기본적으로 이를 배포판 전체의 연식을 가늠하는 “시계”로 사용하는 것입니다).

구체적으로, 제안하는 알고리즘은 다음과 같습니다:

def is_manylinux1_compatible():
    # Only Linux, and only x86-64 / i686
    from distutils.util import get_platform
    if get_platform() not in ["linux-x86_64", "linux-i686"]:
        return False

    # Check for presence of _manylinux module
    try:
        import _manylinux
        return bool(_manylinux.manylinux1_compatible)
    except (ImportError, AttributeError):
        # Fall through to heuristic check below
        pass

    # Check glibc version. CentOS 5 uses glibc 2.5.
    return have_compatible_glibc(2, 5)

def have_compatible_glibc(major, minimum_minor):
    import ctypes

    process_namespace = ctypes.CDLL(None)
    try:
        gnu_get_libc_version = process_namespace.gnu_get_libc_version
    except AttributeError:
        # Symbol doesn't exist -> therefore, we are not linked to
        # glibc.
        return False

    # Call gnu_get_libc_version, which returns a string like "2.5".
    gnu_get_libc_version.restype = ctypes.c_char_p
    version_str = gnu_get_libc_version()
    # py2 / py3 compatibility:
    if not isinstance(version_str, str):
        version_str = version_str.decode("ascii")

    # Parse string and check against requested version.
    version = [int(piece) for piece in version_str.split(".")]
    assert len(version) == 2
    if major != version[0]:
        return False
    if minimum_minor > version[1]:
        return False
    return True

기각된 대안: 설정 파일(예: /etc/python/compatibility.cfg)을 사용하는 방안도 검토했습니다. 이 방안의 문제는 하나의 파일 시스템에 저마다 고유한 ABI 프로파일을 가진 여러 인터프리터 환경이 있을 수 있다는 점입니다. 시스템에 설치된 x86_64 CPython의 manylinux1 호환성은 사용자가 설치한 i686 PyPy의 manylinux1 호환성에 대해 별다른 정보를 주지 못할 수 있습니다. 이 설정 정보를 Python 환경 자체 안에 두면 설정 정보가 올바른 바이너리에 연결된 상태로 유지되고, 조회 코드도 크게 단순해집니다.

호환되는 것으로 간주해야 하는 모든 플랫폼 태그의 목록과 그 선호 순서를 함께 두는 것처럼 더 정교한 구조를 사용하는 방안도 검토했습니다. 예를 들면 다음과 같습니다: _binary_compat.compatible = ["manylinux1_x86_64", "centos5_x86_64", "linux_x86_64"]. 그러나 이 방안에는 몇 가지 복잡한 문제가 따릅니다. 예를 들어, “manylinux1을 지원하지 않음”(또는 나중에는 manylinux2 등) 상태와 “manylinux1을 지원하는지 여부를 어느 쪽으로도 명시하지 않음” 상태를 구별할 수 있어야 하는데, 위의 표현에서는 이것이 그다지 명확하지 않습니다. 또한 현재 가능한 플랫폼 태그가 manylinux1과 linux뿐이라는 점을 고려하면, 선호 순서와 관련하여 실제로 어떤 기능이 필요한지도 전혀 분명하지 않습니다. 따라서 더 완전한 해결책은 Linux에 더 많은 플랫폼 태그가 생기는 시점에, 혹은 생긴다면, 별도의 PEP로 미룹니다.

라이브러리 호환성 검사에 대해서도 훨씬 더 정교한 검사(예: 커널 버전 확인, manylinux1 프로파일에 나열된 개별 라이브러리를 모두 찾아 각각의 버전 확인 등)를 검토했지만, 결국 이러한 검사는 사용자에게 실제로 도움이 되기보다는 혼란스러운 버그를 일으킬 가능성이 더 크다고 판단했습니다. (예를 들어, 배포판마다 이러한 라이브러리를 실제로 두는 위치가 다르며, 검사 코드가 올바른 경로 탐색을 사용하지 못하면 잘못된 답을 반환하기 쉽습니다.)

PyPI 지원

PyPI는 manylinux1 플랫폼 태그가 포함된 휠의 업로드를 허용해야 합니다. PyPI는 manylinux1 플랫폼 태그가 포함된 휠이 이 문서에 설명된 manylinux1 정책을 준수하는지 공식적으로 검증하려고 해서는 안 됩니다. 이러한 검증 작업은 auditwheel과 같이 별도로 개발되는 다른 도구에 맡겨야 합니다.

기각된 대안

한 가지 대안은 각 Linux 배포판(및 그 각 버전)마다 별도의 플랫폼 태그를 제공하는 것입니다. 예를 들면 RHEL6, ubuntu14_10, debian_jessie 등입니다. 이 제안의 어떤 내용도 앞으로 이러한 플랫폼 태그를 추가할 가능성이나, 휠이 시스템에 설치된 외부 패키지에 대한 의존성을 선언할 수 있도록 휠 메타데이터를 더 확장할 가능성을 배제하지 않습니다. 그러나 이러한 확장은 이 제안보다 훨씬 많은 작업이 필요하며, 흔히 쓰이는 모든 Linux 배포판을 지원하기 위해 여러 빌드 환경을 유지하고 여러 휠을 빌드하는 일을 원치 않는 패키지 개발자들에게는 여전히 환영받지 못할 수 있습니다. 따라서 이러한 제안은 이 PEP의 범위를 벗어나는 것으로 간주합니다.

향후 업데이트

앞으로 언젠가는 더 현대적인 기준 환경(아마도 CentOS 6 기반)을 규정하는 manylinux2가 나오고, 언젠가는 manylinux3 등도 나올 것으로 예상하지만, 초기 manylinux1 제안에 대한 경험이 더 쌓일 때까지 이들의 규정은 미룹니다.

참고 문헌