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

Python 개선 제안 한국어 번역

PEP 817 – 휠 변형: 플랫폼 태그를 넘어서

Author:
Jonathan Dekhtiar <jonathan at dekhtiar.com>, Michał Górny <mgorny at quansight.com>, Konstantin Schütze <konstin at mailbox.org>, Ralf Gommers <ralf.gommers at gmail.com>, Andrey Talman <atalman at meta.com>, Charlie Marsh <charlie at astral.sh>, Michael Sarahan <msarahan at gmail.com>, Eli Uriegas <eliuriegas at meta.com>, Barry Warsaw <barry at python.org>, Donald Stufft <donald at stufft.io>, Andy R. Terrel <andy.terrel at gmail.com>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Topic:
Packaging
Created:
10-Dec-2025
Post-History:
24-Jan-2026

Table of Contents

번역·라이선스 안내

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

초록

Python의 기존 휠 패키징 형식은 주어진 휠이 지원하는 환경을 지정하기 위해 Platform compatibility tags를 사용합니다. 이 태그들은 GPU 가속 사용 가능 여부와 같은 최신 하드웨어 구성과 그 기능을 표현할 수 없습니다. 또한 이 태그들은 서로 다른 의존성 ABI를 대상으로 한 빌드와 같은 사용자 정의 패키지 변형을 제공하지 못합니다. 이러한 한계는 과학 컴퓨팅, 인공지능(AI), 머신러닝(ML), 고성능 컴퓨팅(HPC) 커뮤니티에 특히 큰 어려움이 됩니다.

이 PEP는 Binary distribution format의 확장인 “휠 변형(Wheel Variants)”을 제안합니다. 이 확장은 패키지 유지 관리자가 같은 패키지 버전에 대해 여러 빌드 변형을 선언할 수 있는 메커니즘을 도입하며, 설치 프로그램이 시스템의 하드웨어 및 소프트웨어 특성에 따라 가장 적합한 변형을 자동으로 선택할 수 있도록 합니다. 더 구체적으로는 다음을 제안합니다.

  • 하드웨어 또는 소프트웨어 속성으로 휠을 구별할 수 있게 하는, Wheel Variant라는 휠 형식의 발전된 형태.
  • 설치 프로그램이 플랫폼 속성을 동적으로 감지하고 가장 적합한 휠을 선택할 수 있도록 하는 변형 제공자 플러그인 인터페이스.

목표는 누구나 떠올릴 법한 설치 명령({tool} install <package>)이 가장 적합한 휠을 선택하여 최상의 사용자 경험을 제공하도록 하는 것입니다.

동기

2024 Python 개발자 설문조사에 따르면 Python 사용자 중 상당수가 과학 컴퓨팅 용도로 Python을 사용합니다. 여기에는 데이터 분석(응답자의 40%), 머신러닝(30%), 데이터 엔지니어링(30%)이 포함됩니다. 이러한 분야를 위해 개발된 소프트웨어 패키지 중 상당수는 다양한 하드웨어 기능에 의존하는데, 플랫폼 호환성 태그의 한계에서 강조한 것처럼 이러한 기능은 현재의 휠 형식으로는 적절히 표현할 수 없습니다.

예를 들어 PyTorch 같은 패키지는 특정 CUDA 또는 ROCm 버전용으로 빌드해야 하지만, 현재는 그 정보를 휠 태그에 포함할 수 없습니다. 서로 크게 다른 하드웨어 구성을 대상으로 여러 휠을 빌드해야 하므로 유지 관리자는 최적이 아닌 다양한 배포 전략을 택할 수밖에 없으며, 이는 사용자와 해당 패키지에 의존하려는 다른 소프트웨어 작성자에게 불편을 줍니다.

몇 가지 기존 접근 방식은 현재의 우회 방법과 그 단점에서 살펴봅니다. 여기에는 하드웨어 구성별로 별도의 패키지 색인을 유지하는 방법, 가능한 모든 변형을 상당히 큰 단일 휠에 묶는 방법, 또는 별도의 패키지 이름(mypackage-gpu, mypackage-cpu 등)을 사용하는 방법이 있습니다. 이 방법들은 각각 상당한 단점과 잠재적인 보안 문제를 안고 있습니다.

플랫폼 호환성 태그의 한계

현재의 휠 형식은 세 가지 플랫폼 태그를 통해 호환성을 인코딩합니다.

  1. Python 태그: 최소 Python 버전을 인코딩하며, 선택적으로 Python 배포판을 제한합니다(예: 모든 Python 3에는 py3, Python 3.13 이상에는 py313, CPython 3.13 이상에 한정할 때는 cp313).
  2. ABI 태그: 확장 모듈이 요구하는 Python ABI를 인코딩합니다(예: 요구 사항이 없을 때는 none, CPython 안정 ABI에는 abi3, CPython 3.13 ABI가 필요한 확장에는 cp313).
  3. 플랫폼 태그: 현재 운영 체제, 아키텍처, 핵심 시스템 라이브러리를 인코딩합니다(예: 모든 플랫폼에는 any, glibc 2.34 이상을 사용하는 x86-64 Linux 시스템에는 manylinux_2_34_x86_64, arm64 macOS 14.0 이상 시스템에는 macosx_14_0_arm64.

이 태그들은 Python 인터프리터, 운영 체제, 그리고 대략적인 CPU 아키텍처의 가장 기본적인 속성만 표현할 수 있습니다. CPU 이외의 하드웨어 요구 사항이나 라이브러리 ABI 제약 조건을 비롯하여 그보다 더 세부적인 내용은 표현할 수 없습니다.

이러한 유연성 부족으로 인해 많은 프로젝트가 PyTorch 팀이 제공하는 수동 설치 명령 선택기처럼 최적은 아니지만 불가피한 우회 방법을 찾아야 했습니다. 이러한 복잡성은 빌드 옵션의 조합적 복잡성을 처리할 만큼 확장성이 충분하지 않은 현재 태그 시스템의 근본적인 확장성 문제를 보여 줍니다.

현재의 우회 방법과 그 단점

런타임 CPU 디스패칭

NumPy 같은 프로젝트는 현재 기준(baseline) CPU 대상용 휠을 빌드하고, 성능이 중요한 루틴에는 런타임 디스패칭을 사용하는 방식을 택하고 있습니다. 이러한 방식은 패키지 유지 관리자의 추가 작업이 필요하며, 선택된 소수의 함수를 제외한 나머지 코드는 대개 컴파일러 최적화의 이점을 누리지 못합니다.

비교하자면, 더 높은 CPU 기준에 맞춰 GROMACS를 빌드했을 때 상당한 속도 향상을 얻을 수 있음이 입증되었습니다:

A bar graph comparing GROMACS performance (in ns/day) with various targets. The first two bars are labeled "yum (2018.8)" and "generic (SSE2)", reach about 1.0 ns/day and are both marked as "SSE2". The next bar is labeled "ivybridge" ("AVX") and reaches almost 1.5 ns/day. Two following bars are labeled "haswell" and "broadwell" (both "AVX2") and exceed 1.5 ns/day slightly. The last two bars are labeled "skylake_avx512" and "cascadelake" (both "AVX512") and reach almost 2.0 ns/day.

Performance of GROMACS 2020.1 built for different generations of CPUs. Vertical axis shows performance expressed in ns/day, a GROMACS-specific measure of simulation speed (higher is better).

Intel Cascade Lake 마이크로아키텍처가 지원하는 AVX-512 명령어를 활용할 수 있는 아키텍처용으로 GROMACS를 컴파일하면 AVX2 명령어를 사용할 때보다 성능이 18% 추가로 향상되며, 이는 SSE2만 사용하는 일반 GROMACS 설치와 비교하면 약 70%의 속도 향상입니다.

—archspec: A library for detecting, labeling, and reasoning about microarchitectures

변형으로서의 별도 패키지 색인

PyTorch와 RAPIDS 같은 프로젝트는 현재 사용자 지정 URL을 가진 별도의 패키지 색인을 통해 “변형”에 가까운 형태로 패키지를 배포하고 있습니다. 여기서는 PyTorch를 예로 들지만, 문제와 우회 방법, 사용자에게 미치는 영향은 다른 패키지에도 똑같이 적용됩니다.

A grid-based selector for PyTorch versions. Individual rows provide the choice of PyTorch Build (stable or nightly), operating system (Linux, Mac, Windows), package (Pip, LibTorch, Source), language (Python, C++ / Java), and Compute Platform (CUDA 12.6, CUDA 12.8, CUDA 13.0, ROCM 6.4, CPU). Below these rows, the pip install command for the selected variant is provided, utilizing the --index-url parameter.

The PyTorch install selector (https://pytorch.org/get-started/locally/, captured 22-Aug-2025)

PyTorch는 가속기 유형별 색인 URL과, 가속기 태그로 쓰이는 로컬 버전 세그먼트(+cu130, +rocm6.4, +cpu 등)를 조합하여 사용합니다. 사용자는 먼저 자신의 시스템에 맞는 올바른 색인 URL을 확인한 뒤, PyTorch 전용 색인을 추가해야 합니다.

pip install torch --index-url https://download.pytorch.org/whl/cu129

도구는 PyTorch가 로컬 버전 세그먼트를 사용하는 방식에 맞춰 특별한 처리를 구현해야 합니다. 이러한 요구 사항은 패키지를 설치하는 일반적인 패턴에서 벗어납니다. PyTorch 설치 문제는 사용자가 혼란을 겪는 매우 흔한 지점입니다. 이를 수치로 나타내면, 2025-12-05 기준 uv의 이슈 트래커에 올라온 이슈 8136건 중 552건(6.8%)에 “torch”라는 용어가 포함되어 있었습니다.

보안 위험: 안타깝게도 이 방식은 공급망 공격으로 이어졌습니다 - 자세한 내용은 PyTorch 블로그를 참고하십시오. 이는 해결하기 쉽지 않은 문제입니다. 이 문제 때문에 PyTorch 팀은 모든 의존성의 완전한 미러를 만들어야 했으며, 이는 PEP 766의 핵심 동기 중 하나이기도 합니다.

설정이 복잡하다 보니 프로젝트들이 임시방편적인 설치 안내를 제공하는 경우가 많고, 이러한 안내로는 패키지를 매끄럽게 업그레이드할 수 없습니다.

변형으로서의 패키지 이름

XGBoost 같은 패키지는 변형에 가까운 형태를 구현하기 위해 서로 다른 패키지 이름을 사용합니다:

pip install xgboost      # NVIDIA GPU variant
pip install xgboost-cpu  # CPU-only variant

다른 소프트웨어의 유지 관리자는 제공되는 변형 중 어느 것이든 하나가 선택되기만 하면 된다는 의존성을 표현할 수 없습니다. 이들은 특정 변형 하나에 의존하거나, extras를 사용하여 여러 대안 의존성 집합을 제공하거나, 심지어 업스트림 변형에 맞춰 자신의 소프트웨어를 여러 패키지 이름으로 게시해야 합니다.

일반적으로 이러한 패키지들은 서로 겹치는 파일을 설치합니다. Python 패키징은 두 패키지가 상호 배타적이라는 것을 표현하는 기능을 지원하지 않으므로, 설치 프로그램은 두 패키지를 모두 같은 환경에 설치할 수 있으며, 이 경우 두 번째로 설치된 패키지가 먼저 설치된 패키지의 파일을 덮어씁니다. 이로 인해 런타임 오류가 발생하며, 패키지 업그레이드 순서에 따라 의도치 않게 다른 변형으로 전환될 가능성도 생깁니다.

이 방식의 또 다른 한계는 현재 여러 패키지 이름에 걸쳐 새 릴리스를 동시에 게시할 수 없다는 점입니다. PEP 694는 단일 패키지 내의 여러 휠에 대해 이러한 메커니즘을 추가할 것을 제안하지만, 이를 여러 패키지로 확장하는 것은 목표가 아닙니다.

보안 위험: 접미사가 붙은 변형 패키지가 늘어나면 사용자는 다른 패키지에도 이러한 접미사가 있으리라 기대하게 되며, 그 결과 이름 선점(name squatting)이 훨씬 쉬워집니다. 예를 들어 누군가 악의적인 numpy-cuda 패키지를 만들면, 사용자는 이것이 NumPy의 CUDA 변형이라고 믿게 될 것입니다.

이 글을 작성하는 시점을 기준으로 CuPy는 이미 서로 다른 이름으로 총 55개의 cupy* 패키지를 등록했습니다. 그중 대부분은 실제로 한 번도 사용된 적이 없고(Simple API를 통해서만 확인할 수 있습니다), 나머지 중 상당수도 더 이상 업데이트되지 않습니다. 이는 문제의 규모가 얼마나 큰지, 그리고 이름 선점 위험에 대응하기 위해 얼마나 많은 노력이 들어가는지를 분명하게 보여 줍니다.

cupy
cupy-cuda70 cupy-cuda75 cupy-cuda80 cupy-cuda90 cupy-cuda91
cupy-cuda92 cupy-cuda100 cupy-cuda101 cupy-cuda102
cupy-cuda110 cupy-cuda111 cupy-cuda112 cupy-cuda113 cupy-cuda114
cupy-cuda115 cupy-cuda116 cupy-cuda117 cupy-cuda118 cupy-cuda119
cupy-cuda11x
cupy-cuda120 cupy-cuda121 cupy-cuda122 cupy-cuda123 cupy-cuda124
cupy-cuda125 cupy-cuda126 cupy-cuda127 cupy-cuda128 cupy-cuda129
cupy-cuda12x
cupy-cuda13x
cupy-rocm-4-0 cupy-rocm-4-1 cupy-rocm-4-2 cupy-rocm-4-3
cupy-rocm-4-4 cupy-rocm-4-5 cupy-rocm-5-0 cupy-rocm-5-1
cupy-rocm-5-2 cupy-rocm-5-3 cupy-rocm-5-4 cupy-rocm-5-5
cupy-rocm-5-6 cupy-rocm-5-7 cupy-rocm-5-8 cupy-rocm-5-9
cupy-rocm-6-0 cupy-rocm-6-1 cupy-rocm-6-2 cupy-rocm-6-3
cupy-rocm-7-0 cupy-rocm-7-1

변형으로서의 패키지 extras

JAX는 플러그인 기반 방식을 사용합니다. 중심이 되는 jax 패키지는 추가 플러그인을 설치하는 데 쓸 수 있는 여러 엑스트라를 제공합니다. 예를 들어 jax[cuda12], jax[tpu] 등이 있습니다. 하지만 이 방식은 이상적이라고 하기 어렵습니다. 엑스트라 없이 pip install jax를 실행하면 제대로 작동하지 않는 상태로 설치됩니다. 그 결과 Python 생태계에서 당연히 기대하는 기본 동작인 의존성 체인도 제대로 작동하지 않습니다.

JAX에는 모든 사용 사례를 다루는 엑스트라가 12개 있습니다. 그중 상당수는 서로 겹칩니다. 그래서 문서를 자세히 읽지 않는 사용자는 잘못 이해할 수 있습니다. 이들 대부분은 기술적으로 상호 배타적입니다. 하지만 현재로서는 패키지 메타데이터 안에서 이를 올바르게 표현할 수 없습니다.

Provides-Extra: minimum-jaxlib
Provides-Extra: cpu
Provides-Extra: ci
Provides-Extra: tpu
Provides-Extra: cuda
Provides-Extra: cuda12
Provides-Extra: cuda13
Provides-Extra: cuda12-local
Provides-Extra: cuda13-local
Provides-Extra: rocm
Provides-Extra: k8s
Provides-Extra: xprof

번들된 범용 패키지 - 모놀리식 빌드

가능한 모든 변형을 하나의 휠에 포함하는 방법도 있습니다. 하지만 이렇게 하면 결과물이 지나치게 커집니다. 그러면 특정 변형 하나만 필요한 사용자도 대역폭을 낭비하게 되고 설치 시간도 길어집니다. 경우에 따라서는 이런 결과물이 PyPI의 크기 제한을 넘어서 PyPI에 호스팅할 수 없습니다.

소스 배포판을 통한 휠 변형 선택

FlashAttention은 PyPI에 휠을 전혀 게시하지 않고, 대신 플랫폼 감지를 수행하고 업스트림 서버에서 적절한 휠을 내려받은 다음 이를 설치 프로그램에 제공하는 맞춤형 소스 배포판을 게시합니다. 이 방식은 최적의 변형을 자동으로 선택할 수 있습니다. 하지만 이 방식을 쓰면 바이너리 전용 설치가 작동하지 않습니다. 또한 소스 배포판을 거치는 빌드가 필요한데, 이 빌드는 느리고 오류가 나기 쉽습니다. 휠 파일 이름에 기반한 일반적인 캐싱 가정도 깨집니다. 또한 소프트웨어를 실행할 때 쓰일 버전과 같은 버전의 torch 패키지가 들어 있는 빌드 환경을 특별히 준비해야 합니다. 그래서 빌드 격리 없이 빌드해야 합니다. 프로젝트 쪽에서는 휠을 별도로 호스팅해야 합니다.

보안 위험: 일반적인 소스 빌드와 마찬가지로, 이 방식은 설치할 때 임의의 코드를 실행해야 합니다. 휠 다운로드는 전적으로 패키지 관리자의 통제 밖에서 이루어집니다. 그래서 공격 표면이 서로 다른 두 휠 다운로드 구현으로 넓어지고, 출처를 제대로 추적할 수 없습니다.

생태계 파편화

하드웨어와 ABI 요구 사항을 고려해 의존성을 해석하는 방식이 표준화되어 있지 않아서 생태계가 파편화되었습니다:

  • 일관성 없는 사용자 경험: 프로젝트마다 설치 방법이 달라 혼란스럽고, 필요한 패키지를 찾기도 어려워집니다.
  • 개발 도구의 복잡성: 설치 프로그램, IDE, CI/CD 시스템은 비표준 설치 요구 사항을 처리하는 데 어려움을 겪습니다.
  • 취약성: 흔히 쓰이는 우회 방법은 오류가 나기 쉬운 경우가 많습니다. 실제로 과거에 잘못된 결과물을 내려받는 등의 문제를 일으킨 적이 있습니다.

과학 컴퓨팅 및 AI/ML 워크플로에 미치는 영향

이러한 패키징의 한계는 성능 최적화가 중요한 과학 컴퓨팅과 AI/ML 애플리케이션에 특히 큰 영향을 미칩니다:

현재의 휠 형식은 하드웨어를 인식하지 못합니다. 그래서 하드웨어에 의존하는 패키지를 쓸 때 사용자 경험이 좋지 않습니다. 작고 범위가 명확한 패키지에는 플러그인이 도움이 됩니다. 하지만 현재는 사용자가 범용 기본값이나 호환되지 않는 조합을 피하려면 올바른 변형(예: jax[cuda13])을 직접 찾아야 합니다. 명시적으로 재정의하지 않는 한 pip install jax가 사용자의 하드웨어에 맞는 패키지를 자동으로 선택하는 시스템이 필요합니다.

이런 점에서 휠 변형은 분명히 올바른 방향으로 나아가는 한 걸음입니다.

—Michael Hudgins, JAX 개발자 인프라 책임자

이러한 한계는 패키지 작성자부터 학생, 과학자, 엔지니어를 비롯해 숙련도가 제각각인 최종 사용자까지 모든 사람에게 영향을 미칩니다:

모델을 실행하고 대규모 데이터셋을 처리하는 데 필요한 컴퓨팅 자원에 접근하는 일은 10년 넘게 과학 컴퓨팅의 고질적인 문제였습니다. 오늘날에도 연구자와 데이터 과학자는 작업을 시작하기 전에 PyTorch 같은 핵심 도구를 설치하는 데 몇 시간에서 며칠을 들입니다. 이러한 복잡성은 일상 업무에서 Python을 사용하려는 사용자에게 상당한 진입 장벽이 됩니다. WheelNext Wheel Variants 제안은 또 하나의 새롭고 별개인 해결책을 만들지 않고도, 더 넓은 패키징 생태계 안에서 계속되어 온 설치 문제와 컴퓨팅 자원 접근 문제를 해결할 길을 제시합니다. 사용자 경험 향상이라는 큰 그림에 집중합시다. 그것이 실질적인 변화를 가져올 것입니다.

—pyOpenSci 전무이사 겸 설립자 Leah Wasser

이기종 컴퓨팅 환경

연구 기관과 클라우드 제공업체는 서로 다른 아키텍처(CPU, 하드웨어 가속기, ASIC 등)로 구성된 이기종 컴퓨팅 클러스터를 관리합니다. 현재 시스템은 환경별 설치 절차를 요구하므로 재현 가능한 배포가 어렵습니다. 이러한 상황은 “과학 논문”의 재현을 어렵게 만드는 원인이기도 합니다. 이를 개선하는 데 주력하는 애플리케이션 개발자들도 패키징 장애물 때문에 어려움을 겪습니다:

저희는 과학자, 엔지니어, 데이터 분석가를 위한 Python IDE인 Spyder의 패키지 관리자를 세 가지 주요 목표를 두고 개발해 왔습니다. 첫째, 터미널에 난해한 명령을 입력하는 대신 GUI로 환경을 만들고 패키지를 설치할 수 있게 하여 사용자의 삶을 더 편하게 만드는 것입니다. 둘째, 연구 코드를 재현 가능하게 만들어 사용자가 코드와 그 의존성을 동료와 공유할 수 있게 하는 것입니다. 그리고 셋째, 사용자가 번거로움 없이 코드를 HPC 클러스터나 클라우드의 머신으로 옮겨 그곳의 방대한 컴퓨팅 자원을 활용할 수 있게 하는 것입니다. 이 PEP가 제안하는 개선 사항이 적용되면, 널리 쓰이는 과학 라이브러리(PyTorch, CuPy 등)를 알맞은 GPU와 명령어 집합에 맞게 설치하는 일이 uv/pip 기반 도구에서 간단하고 투명해지므로, 모든 PyPI 사용자에게 이를 현실로 만들어 줄 수 있을 것입니다.

—Spyder IDE 수석 개발자 Carlos Córdoba

인공지능, 머신러닝, 딥러닝

최신 AI 워크플로의 최근 발전은 점점 더 GPU 가속에 의존하고 있지만, 현재의 패키징 시스템은 배포를 복잡하게 만들고 전체 도구 스택(빌드 백엔드에서 설치 프로그램까지, 그리고 패키지 유지 관리자도 빼놓을 수 없습니다)의 오픈 소스 개발자에게 상당한 부담을 줍니다.

PyTorch의 광범위한 휠 지원은 언제나 최첨단이었으며, 패키지 선택기를 통해 처음부터 하드웨어 가속기를 지원해 왔습니다. 저희는 사용자가 별도 설정 없이 바로 사용할 수 있게 해 주는 것이 언제나 PyTorch의 강력한 무기였다고 생각합니다. 안타깝게도 이를 뒷받침하는 인프라는 매우 복잡하고, 유지 관리가 어려우며, (저희와 사용자, 그리고 패키지 저장소 모두에게) 비효율적입니다.

지원하는 하드웨어의 수가 다시 빠르게 늘어나고 있는 만큼, 저희는 PyTorch 설치 방법을 PyTorch가 처음 출시된 이래 사용자가 기대해 온 모습으로 만들어 줄 휠 변형 작업을 적극 지지합니다: pip install torch

—PyTorch 코어 유지 관리자

XGBoost의 리드 유지 관리자는 XGBoost가 겪고 있으며 휠 변형으로 해결될 것으로 기대하는 여러 문제를 다음과 같이 열거합니다:

  • 여러 SM[GPU 타깃]을 위한 “팻 바이너리(fat binary)”를 사용하기 때문에 다운로드 크기가 큽니다. 현재 XGBoost는 11가지 SM을 대상으로 빌드됩니다.
  • CPU 전용 패키지를 위한 별도의 패키징 이름이 필요합니다. 현재 xgboost-cpu라는 별도 패키지를 배포하고 있어, 사용자가 별도의 requirements.txt 파일을 유지 관리해야 합니다. 예시는 xgboost#11632를 참고하십시오.
  • 여러 CUDA 버전에 대응하는 복잡한 디스패치 로직. XGBoost의 일부 기능에는 새로운 CUDA 버전(12.5 또는 12.8)이 필요하지만, XGBoost 휠은 12.0을 대상으로 합니다. 그 결과, 저희는 런타임에 CUDA 및 드라이버 버전을 감지하기 위해 상당히 복잡한 디스패치 로직을 유지 관리하고 있습니다. 이러한 디스패치 로직은 NVIDIA 프로바이더 플러그인과 같은 전용 소프트웨어에 구현하는 것이 가장 바람직하며, 그래야 XGBoost 프로젝트가 핵심 사명에 집중할 수 있습니다.
  • 여러 OpenMP 런타임의 공존으로 인한 정의되지 않은 동작. XGBoost는 서로 다른 OpenMP 런타임을 갖춘(또는 OpenMP 런타임이 전혀 없는) 다양한 시스템에 설치됩니다. 지금까지 XGBoost는 OpenMP 런타임 사본을 벤더링해 왔지만, 이 방식은 점점 더 유지하기 어려워지고 있습니다. 시스템에 서로 호환되지 않는 여러 버전의 OpenMP 런타임이 존재하면 사용자는 크래시나 멈춤과 같은 정의되지 않은 동작을 겪게 됩니다. (이 문제는 MacOS에서 특히 심각하여, 결국 XGBoost의 MacOS 휠은 더 이상 OpenMP를 번들로 포함하지 않습니다.)

—XGBoost의 리드 유지 관리자 Philip Hyunsu Cho

패키징의 복잡성은 개발자들이 자신의 소프트웨어가 추구하는 실제 목표에 집중하지 못하게 만들고 있습니다:

저희는 이미지 시퀀스에서 생물학적 움직임을 분석하기 위해 딥러닝을 사용하는 과학 소프트웨어 도구를 유지 관리하고 있으며, 이 도구는 특화된 과학 데이터로 맞춤형 모델을 훈련하기 위한 프런트엔드로서의 사용 편의성 덕분에 큰 호응(3만 5천 명 이상의 사용자, 80개국 이상)을 얻었습니다. 저희 사용자층은 질병의 치료법을 찾기 위해 하루 종일 뇌 수술과 분자 유전학 연구를 수행하는 과학자들입니다. 그들이 단지 자신의 데이터를 분석하기 위해 하드웨어 가속기 드라이버 호환성 매트릭스와 환경 관리자에 대해 배우고, 끊임없이 변화하는 Python 패키징 생태계를 따라가야 한다고 기대하는 것은 전혀 합리적이지 않습니다.

이를 인식하여, 저희 팀은 이제 수백만 달러 규모의 연구의 재현성을 뒷받침하고 있는 저희 도구가 모든 플랫폼과 호환되도록 하기 위해 의존성과 패키징 편법을 유지 관리하는 데 엄청난 시간을 쏟아 왔습니다. 지난 몇 년 동안 저희는 이 문제에 대한 해결책을 엔지니어링하는 데 수백 시간과 25만 달러 이상의 납세자 지원 연구 자금을 사용한 것으로 추산합니다. WheelNext가 있었다면 이 문제를 완전히 해결하여, 저희가 신경퇴행성 질환을 이해하고 치료하는 데 노력을 집중할 수 있었을 것입니다.

—SLEAP의 저자이자 Salk Institute for Biological Studies의 책임 연구원인 Talmo Pereira, Ph.D.

개선 가능성은 다음과 같이 요약할 수 있습니다:

이 PEP는 점점 더 복잡하고 다양해지는 하드웨어 구성에 직면한 Python 생태계의 배포 문제를 개선하는 데 있어 중요한 진전입니다. 동일한 라이브러리에 대해 여러 배포 대상을 표준 방식으로 지원함으로써, 급속히 성장하는 AI/ML 및 과학 컴퓨팅 분야를 지원하기 위해 개발자들이 사용해 온 번거롭고 시간이 많이 드는 여러 우회 방법을 통합하고 단순화할 것입니다.

—NumPy와 SciPy의 저자이자 OpenTeams의 Chief AI Architect인 Travis Oliphant

범위 밖의 기능

이 PEP는 현대의 이기종 시스템 요구 사항을 충족하는 데 필요한 최소한의 범위를 제시합니다. 최소 범위를 벗어나는 측면은 도구나 향후 PEP를 통해 발전하도록 남겨 둡니다. 이러한 측면을 일부 나열하면 다음과 같습니다:

  • 변형을 결정론적으로 선택하거나 pylock.toml 파일에 변형을 포함하기 위한 정적 파일의 형식,
  • 설치 프로그램이 벤더링하거나 재구현하는 변형 제공자 목록,
  • 설치 프로그램이 벤더링되지 않은 변형 제공자를 실행할 수 있도록 하는 구체적인 옵트인 메커니즘과 UX,
  • 빌드 백엔드가 PEP 517 메커니즘을 통해 변형을 생성하도록 지시하는 방법.

선행 사례

이 문제는 Python 생태계에만 국한된 것이 아니며, 여러 그룹과 생태계가 바로 이 문제에 대해 다양한 해답을 내놓았습니다. 이 절에서는 여러 커뮤니티가 취한 다양한 접근 방식의 강점과 약점을 조명하는 데 중점을 둡니다.

Conda - conda-forge

Conda는 파일 이름을 파싱하지 않고 집계된 메타데이터 색인으로 의존성을 해석하는 바이너리 전용 패키지 생태계입니다. Simple repository API와 달리, conda의 의존성 해석은 전체 메타데이터를 담은 플랫폼별 repodata 색인에 의존합니다. 따라서 파일 이름은 파싱할 필요가 없는 순수한 식별자 역할만 합니다.

변형 시스템: 2016-2017년에 conda-build는 이름/버전은 같지만 의존성이 다른 패키지를 구분하기 위해 변형(variant)을 도입했습니다.

pytorch-2.8.0-cpu_mkl_py313_he1d8d61_100.conda      # CPU + MKL variant
pytorch-2.8.0-cuda128_mkl_py313_hf206996_300.conda  # CUDA 12.8 + MKL variant
pytorch-2.8.0-cuda129_mkl_py313_he100a2c_300.conda  # CUDA 12.9 + MKL variant

(변형 메타데이터로부터 계산된) 해시가 파일 이름 충돌을 방지하며, 실제 변형 선택은 솔버의 표준 의존성 제약을 통해 이루어집니다. 특별한 메타데이터 파싱은 필요하지 않으며, 설치 프로그램은 다음과 같이 의존성을 해결하기만 하면 됩니다:

conda install pytorch mkl

뮤텍스 메타패키지: Python 메타데이터와 conda 메타데이터에는 “이 패키지는 저 패키지와 충돌합니다”와 같은 개념을 표현할 좋은 방법이 없습니다. 이를 강제하는 주된 메커니즘은 공통 패키지 이름을 공유하는 것입니다. 주어진 이름을 가진 패키지는 한 번에 하나만 존재할 수 있습니다. 뮤텍스 메타패키지는 이름은 같지만 빌드 문자열이 서로 다른 패키지의 집합입니다. 패키지는 NumPy가 OpenBLAS를 사용하고 SciPy가 MKL을 사용하는 것처럼 관련 패키지가 서로 다른 의존성 라이브러리를 사용하여 생기는 문제를 피하기 위해 특정 뮤텍스 빌드(예: blas=*=openblas 대 blas=*=mkl)에 의존합니다.

소프트웨어 변형의 예: BLAS, MPI, OpenMP, noarch 대 native

가상 패키지: 2019년에 도입된 가상 패키지는 시스템 감지 결과(CUDA 버전, glibc, CPU 기능)를 솔버 제약 조건으로 주입합니다. 빌드된 패키지는 __cuda >=12.8 같은 의존성을 표현하며, 설치 프로그램은 설치 시점에 호환성을 검증합니다. 현재 가상 패키지에는 archspec (CPU 기능), OS/시스템 라이브러리, CUDA 드라이버 버전이 포함됩니다. 감지 로직은 도구마다 다릅니다(rattler, mamba).

Spack / Archspec

archspec은 CPU 마이크로아키텍처 변형을 감지하고, 레이블을 붙이고, 추론하기 위한 라이브러리로, Spack 패키지 관리자를 위해 개발되었습니다.

변형 모델: CPU 마이크로아키텍처(예: haswell, skylake, zen2, armv8.1a)는 바이너리 호환성을 인코딩하는 방향성 비순환 그래프(DAG)를 이루며, 이는 의존성 해석 시 packageB가 packageA에 의존한다는 것을 표현하는 데 도움이 됩니다. 이 순서는 부분 순서입니다. (1) 서로 다른 ISA 계열끼리는 비교할 수 없고, (2) 같은 시기의 설계라도 호환되지 않는 기능 집합을 가질 수 있기 때문입니다. cascadelake와 cannonlake는 둘 다 skylake에서 파생되었지만 각각 고유한 AVX-512 확장을 가지므로 서로 비교할 수 없습니다.

구현: 언어에 독립적인 JSON 데이터베이스가 마이크로아키텍처 메타데이터(기능, 호환성 관계, 컴파일러별 최적화 플래그)를 저장합니다. 언어 바인딩은 감지 기능(/proc/cpuinfo를 조회하여 호환되는 기능 부분 집합이 가장 큰 마이크로아키텍처와 대조)과 호환성 비교 연산자를 제공합니다.

패키지 관리자 통합: Spack은 대상 마이크로아키텍처를 패키지 출처 정보로 기록하고(spack install fftw target=broadwell), 컴파일러 플래그를 자동으로 선택하며, 마이크로아키텍처를 인식하는 바이너리 캐싱을 가능하게 합니다. European Environment for Scientific Software Installations (EESSI)는 최적화된 빌드를 마이크로아키텍처별 하위 디렉터리(예: x86_64, armv8.1a, haswell)에 나누어 배포합니다. 정확히 일치하는 빌드가 없으면 런타임 초기화 과정에서 archspec을 사용하여 호환되는 가장 적합한 빌드를 선택합니다.

Gentoo Linux

Gentoo Linux는 광범위한 패키지 사용자 정의를 지원하는 소스 우선 배포판입니다. 이는 주로 USE 플래그를 통해 이루어집니다. USE 플래그는 개별 패키지가 노출하는 불리언 플래그로, 활성화할 기능, 선택적 의존성, 일부 빌드 매개변수를 세밀하게 조정할 수 있게 합니다(예: JPEG XL 이미지 형식 지원을 위한 jpegxl, AVX2 명령어 집합 사용을 위한 cpu_flags_x86_avx2). 플래그는 개별적으로 켜고 끌 수 있으며, 플래그 조합마다 별도의 바이너리 패키지를 빌드할 수 있습니다. 패키지 관리자는 구성이 일치하는 바이너리 패키지를 선택하거나 소스에서 빌드할 수 있습니다.

API 및 ABI 일치는 주로 슬롯 지정을 통해 이루어집니다. 슬롯은 일반적으로 함께 설치할 수 있는 주어진 패키지의 여러 버전이나 변형(예: 서로 다른 GTK+ 또는 LLVM 메이저 버전, 또는 WebKitGTK의 GTK+3 빌드와 GTK4 빌드)을 제공하는 데 사용되며, 서브슬롯은 슬롯 내의 버전을 묶는 데 사용되고 보통 라이브러리 ABI 버전에 대응합니다. 그러면 패키지는 빌드 시점에 사용된 슬롯과 서브슬롯에 묶인 의존성을 선언할 수 있습니다. 마찬가지로, 서로 다른 의존성 슬롯을 대상으로 별도의 바이너리 패키지를 빌드할 수 있습니다. 다른 슬롯이나 서브슬롯에 속하는 의존성 버전을 설치할 때, 패키지 관리자는 해당 의존성이 필요한 패키지를 새 슬롯을 대상으로 빌드된 바이너리 패키지로 교체하거나 소스에서 다시 빌드할 수 있습니다.

일반적으로 슬롯을 사용할 때는 가능한 한 최신 버전으로 업그레이드하는 것이 바람직하다고 가정합니다. 더 세밀한 제어가 필요할 때는 슬롯을 USE 플래그와 함께 사용합니다. 예를 들어, llvm_slot_{major} 플래그는 빌드 대상이 될 LLVM 메이저 버전을 선택하는 데 사용됩니다.

개요 및 근거

휠 변형 용어집

변형 휠
배포 이름, 버전, 빌드 번호, 플랫폼 호환성 태그는 같지만, 임의의 변형 속성 집합으로 서로 구별되는 휠입니다.
변형 네임스페이스
단일 제공자가 제공하는 관련 기능들을 묶는 데 사용되는 식별자입니다(예: nvidia, x86_64, arm 등).
변형 기능
네임스페이스 내에서 하나 이상의 값을 가질 수 있는 특정 특성(키)입니다(예: version, avx512_bf16 등).
변형 속성
하나의 특정 기능과 그 값을 기술하는 3-튜플(namespace :: feature-name :: feature-value)입니다. 기능이 여러 값을 가지면 각 값은 별도의 속성으로 표현됩니다.
변형 레이블
변형을 고유하게 식별하기 위해 휠 파일 이름에 추가되는 문자열(최대 16자)입니다.
널 변형
변형 속성이 하나도 없고 예약된 레이블 null을 가지는 특수한 변형입니다. 항상 지원되는 것으로 간주되지만 휠 변형 중에서는 우선순위가 가장 낮으며, 비변형 휠보다는 우선하여 선택됩니다.
변형 제공자
특정 네임스페이스에 대해 지원되는 유효한 변형 속성을 제공하는 제공자로, 보통 시스템 감지를 구현하는 Python 패키지 형태입니다.
설치 시점 제공자
휠 설치 중에 질의할 수 있는 플러그인으로 구현된 제공자입니다.
사전 제공자
지원되는 속성의 정적 목록을 가지며, 이 목록이 휠 메타데이터에 포함되는 제공자입니다. 이러한 목록은 pyproject.toml에 포함되거나, 빌드 시점에 질의되는 플러그인이 제공할 수 있습니다.

개요

휠 변형은 기존 휠 태그가 제공하는 것 이상으로 빌드된 휠의 특성을 더 세밀하게 명세합니다. 개별 휠에는 Modified wheel filename에 설명된 대로 빌드 시점에 정의되며 사람이 읽을 수 있는 레이블이 붙고, 그 특성은 Variant property system으로 기술됩니다. 속성은 네임스페이스, 기능(feature), 기능 값(feature value)으로 이루어진 계층 구조로 구성됩니다. 설치할 휠을 평가할 때 설치 프로그램은 주어진 휠의 변형 속성이 시스템과 호환되는지 판단하고, 호환되는 변형 속성의 우선순위에 따라 Variant ordering을 수행합니다. 이 과정은 호환성 판단에 더해 추가로 수행됩니다. 변형 속성에 따른 정렬은 태그에 따른 정렬보다 우선합니다.

모든 변형 네임스페이스는 변형 제공자(variant provider)가 관리합니다. 변형 제공자에는 설치 시점 제공자와 사전(ahead-of-time, AoT) 제공자의 두 종류가 있습니다. 설치 시점 제공자에는 플러그인이 필요하며, 휠을 설치하는 동안 이 플러그인에 질의하여 지원되는 속성의 집합과 그 선호 순서를 결정합니다. AoT 제공자의 경우 이 데이터는 정적이며 휠에 내장됩니다. 이 데이터는 휠 유지 관리자가 직접 제공하거나 휠 빌드 시점에 AoT 플러그인에 질의하여 얻을 수 있습니다.

두 종류의 플러그인은 모두 일반적으로 Provider plugin API를 구현하는 Python 패키지로 구현되지만, Providers에서 설명하는 것처럼 사용자 경험을 개선하기 위해 설치 프로그램이 벤더링하거나 재구현할 수도 있습니다. 플러그인 패키지는 격리된 환경에 설치될 수도 있고 격리되지 않은 환경에 설치될 수도 있습니다. 특히 모든 플러그인은 PEP 517 백엔드의 get_requires_for_build_wheel() 훅이 반환할 수 있으며, 따라서 다른 빌드 의존성과 함께 설치될 수 있습니다. 이러한 이유로 플러그인 패키지는 의존성 버전을 좁게 고정하지 않는 것이 중요합니다. 좁게 고정하면 서로 다른 패키지를 같은 환경에 동시에 설치하지 못할 수 있기 때문입니다.

변형 지원을 관리하는 메타데이터는 pyproject.toml 파일에 정의되며, Metadata in source tree and wheels에서 살펴보는 것처럼 휠 안의 variant.json 파일로 복사됩니다. 또한 Variant environment markers를 사용하여 변형의 일부 집합에만 해당하는 의존성을 정의할 수 있습니다.

수정된 휠 파일 이름

이 설계의 핵심 요구 사항 중 하나는 이 PEP 이전의 설치 프로그램이 휠 변형 파일을 무시하도록 보장하는 것입니다. 이를 통해 변형 휠과 비변형 휠을 하나의 색인에 함께 게시할 수 있으며, 변형을 지원하지 않는 설치 프로그램은 전자를 안전하게 무시하고 후자로 대체합니다.

파일 이름에 변형 레이블 구성 요소를 추가하는 목적은 두 가지입니다. 하나는 파일 이름에서 변형 속성 집합으로의 고유한 매핑을 제공하는 것이고, 다른 하나는 사람이 읽을 수 있는 방식으로 변형을 식별하는 것입니다. 레이블은 서로 다른 파일 시스템에서 생길 수 있는 문제를 피하기 위해 짧은 소문자로 유지됩니다. 레이블은 기존 파일 이름 검증 알고리즘이 해당 파일 이름을 거부하도록 맨 끝에 -로 구분된 구성 요소로 추가됩니다:

  • 빌드 태그와 변형 레이블이 모두 있으면 파일 이름의 구성 요소가 너무 많아집니다. 예시:
    numpy-2.3.2-1-cp313-cp313t-musllinux_1_2_x86_64-x86_64_v3.whl
                                                   ^^^^^^^^^^
    
  • 변형 레이블만 있으면 세 번째 위치의 Python 태그가 빌드 번호로 잘못 해석됩니다. 빌드 번호는 숫자로 시작해야 하는데 현재 숫자로 시작하는 Python 태그는 없으므로, 해당 파일 이름은 유효하지 않은 것으로 간주됩니다. 예시:
    numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64-x86_64_v3.whl
                ^^^^^
    

이 동작은 다음 기존 도구들에서 확인되었습니다: auditwheel, packaging, pdm, pip, poetry, uv.

변형 속성 시스템

변형 속성은 변형의 특성을 표현하는 역할을 합니다. 플랫폼 호환성 태그와 달리 변형 속성은 변형 메타데이터에 저장되므로 휠 파일 이름의 길이에 영향을 주지 않습니다. 변형 속성은 계층적인 키-값 설계를 따르며, 키는 다시 네임스페이스와 기능 이름으로 나뉩니다. 네임스페이스는 단일 제공자가 정의한 기능들을 묶고, 여러 제공자가 같은 이름의 기능을 정의할 경우 충돌을 피하기 위해 사용됩니다. 이를 통해 각 네임스페이스의 독립적인 거버넌스와 발전이 가능합니다.

키에는 소문자, 숫자, 밑줄만 사용할 수 있습니다. 같은 이름이 서로 다르게 표기되는 것을 피하기 위해 대문자는 허용되지 않습니다. 값에 사용할 수 있는 문자 집합은 버전과 비슷한 값을 허용할 수 있도록 더 느슨합니다.

변형 속성은 PEP 301의 Trove 분류자에서 영감을 받은 구조화된 3-튜플 형식으로 직렬화됩니다:

{namespace} :: {feature_name} :: {feature_value}

속성은 변형 휠의 호환성을 판단하는 데에도, 설치할 최선의 변형을 선택하는 데에도 사용됩니다. 제공자 플러그인은 어떤 변형 속성이 시스템과 호환되는지 알려 주고, 이를 중요도 순으로 정렬합니다. 이 순서는 변형 휠 메타데이터에서 추가로 변경할 수 있습니다.

변형 기능은 하나의 변형 휠 안에 여러 값이 존재하는 것을 허용하도록 선언할 수 있습니다. 그런 경우 이 값들은 논리 OR로 매칭됩니다. 즉, 휠이 지원되는 것으로 간주되려면 값 하나만 시스템과 호환되면 됩니다. 반면 기능들은 논리 AND로 취급됩니다. 즉, 모든 기능이 호환되어야 합니다. 이를 통해 완전한 불리언 논리를 구현하지 않고도 변형 호환성을 지정하는 데 어느 정도 유연성을 확보할 수 있습니다.

일반적으로 변형 기능은 단일 값을 가지며, 최소 요구 사항이나 상호 배타적인 요구 사항을 나타냅니다. 시스템은 호환되는 값을 여러 개 알려 줄 수 있습니다. 예를 들어 기능이 최소 CUDA 런타임 버전을 선언한다면, 제공자는 현재 설치된 버전 또는 그보다 오래된 버전에 해당하는 최소 버전을 요구하는 휠과 호환된다고 알려 줍니다. 예컨대 CUDA 12.8의 경우 휠에서 사용되는 호환 최소 버전은 선호도가 높은 순서대로 다음과 같습니다:

nvidia :: cuda_version_lower_bound :: 12.8
nvidia :: cuda_version_lower_bound :: 12.7
nvidia :: cuda_version_lower_bound :: 12.6
...

마찬가지로 휠은 요구하는 최소 CPU 버전을 나타낼 수 있고, 제공자는 호환되는 모든 CPU 버전을 알려 줍니다.

다중 값 기능은 호환되지 않는 여러 대상을 단일 패키지가 지원하는 “fat” 패키지에 유용합니다. 대표적인 예는 GPU입니다. 이 경우 휠은 지원하는 여러 GPU를 선언하고, 제공자는 실제로 설치된 GPU(보통 하나)가 무엇인지 알려 줍니다. 두 목록에 겹치는 부분이 있으면 휠은 호환됩니다.

널 변형

널 변형은 속성이 없는 변형 휠이지만, null 변형 레이블과 변형 메타데이터를 가진다는 점에서 비변형 휠과 구별됩니다. 전환 기간 동안 null 변형은 제공된 변형을 하나도 지원하지 않는 시스템과 변형 휠을 아예 지원하는 시스템을 위해 별도의 대체를 제공할 수 있는 가능성을 제공합니다.

예를 들어 선택적으로 GPU를 지원하는 패키지는 세 종류의 휠을 게시할 수 있습니다:

  • 여러 개의 GPU 지원 휠입니다. 각 휠은 단일 CUDA 버전에 맞춰 그에 맞는 지원 GPU 집합과 함께 빌드되며, 제공자 플러그인이 시스템과 호환된다고 알려 줄 때에만 사용됩니다.
  • GPU 변형보다 훨씬 작은 CPU 전용 널 변형입니다. 제공자 플러그인이 호환되는 GPU가 설치되어 있지 않다고 알려 줄 때 설치됩니다.
  • 변형을 지원하는 설치 프로그램이 없는 시스템에 설치될 GPU+CPU 비변형 휠.

null 변형의 게시는 선택 사항이며, 별도의 대체가 사용자에게 이점을 제공하는 경우에만 의미가 있습니다. null 변형이 게시되어 있으면, 휠 변형을 지원하는 설치 프로그램은 비변형 휠보다 이를 우선합니다. 게시되어 있지 않으면, 대신 비변형 휠로 폴백합니다. 설치 프로그램 플래그로 변형 지원이 명시적으로 비활성화된 경우에도 비변형 휠이 사용됩니다.

null 변형은 일반 변형과 명확히 구별되도록 예약된 null 레이블을 사용합니다.

설치 시점 제공자와 사전(Ahead-of-Time) 제공자

변형 휠 메타데이터는 해당 속성에 어떤 제공자가 사용되는지를 명시합니다. 제공자는 두 가지 목적을 수행합니다.

  1. 설치 시점: 어떤 변형 휠이 사용자의 시스템과 호환되는지, 그리고 그중 어느 것이 최선의 선택인지 판단하는 것, 그리고
  2. 빌드 시점: 휠을 빌드할 때 어떤 변형 속성이 유효한지 판단하는 것.

이 명세는 설치 시점 제공자와 사전(Ahead-of-Time) 제공자라는 두 종류의 제공자를 제안합니다.

설치 시점 제공자는 질의하려면 설치하고 실행해야 하는 Python 패키지로 구현되거나, 도구에 벤더링되거나 재구현됩니다. 설치 시점 제공자는 휠 호환성을 판단하기 위해 사용자 시스템에 질의해야 할 때 사용됩니다. 예를 들어 GPU를 활용하는 변형이나 플랫폼 태그가 제공하는 범위를 넘어서는 CPU 명령어 세트를 요구하는 변형에 사용됩니다. 서드파티 패키지를 설치하는 것에는 Security implications 절에서 강조한 보안 위험이 따르며, 제안된 완화 조치는 설치 프로그램 구현에 비용을 발생시킵니다.

사전(Ahead-of-Time) 제공자는 휠에 내장된 정적 메타데이터로 구현됩니다. 사전 제공자는 특정 변형 속성이 사용자 시스템과 항상 호환되는 경우에 사용됩니다(해당 속성을 사용하는 휠이 성공적으로 빌드되었다는 전제하에). 다만 메타데이터는 어떤 속성이 선호되는지를 나타냅니다. 예를 들어 AoT 제공자는 서로 다른 BLAS / LAPACK 제공자를 대상으로 한 빌드 간에 선택지를 제공하거나, 패키지의 디버그 빌드를 제공하는 데 사용할 수 있습니다. AoT 제공자는 설치 프로그램 외부의 코드를 실행할 필요가 없으므로 설치 시점 제공자가 겪는 문제를 일으키지 않으며, 더 자유롭게 사용할 수 있습니다.

AoT 제공자는 플러그인 패키지를 갖출 수 있습니다. 그런 경우 이 패키지들은 휠을 빌드할 때만 사용되며, 그 출력은 설치 시점에 사용되는 정적 메타데이터를 채우는 데 사용됩니다. 이렇게 하면 여러 패키지에 걸쳐 일관된 속성 이름과 값을 사용하기가 더 쉬워집니다. 그렇지 않으면 패키지 유지 관리자가 지원되는 속성을 pyproject.toml 파일에 직접 포함해야 합니다.

Python 패키지로 구현될 때, 두 종류의 제공자 플러그인은 대체로 동일한 API를 노출합니다. 다만 AoT 제공자는 항상 모든 유효한 변형 속성을 지원되는 것으로 간주해야 하며, 사용자 시스템과 관계없이 항상 동일한 순서의 지원 속성 목록을 반환해야 합니다. 모든 AoT 제공자는 기술적으로 설치 시점 제공자로 사용할 수 있지만, 그 반대는 성립하지 않습니다.

플러그인 안정성과 버전 관리

이 명세로 인해 휠을 설치하기 위해 제공자 패키지를 설치하고 실행해야 할 가능성이 생기므로, 이러한 패키지는 아주 오래된 패키지 버전을 포함하여 과거에 게시된 변형 휠에 대해서도 계속 올바르게 동작하도록 유지할 것을 권장합니다. 이상적으로는 이전에 지원되던 속성은 절대 제거되어서는 안 됩니다.

호환성을 깨는 변경을 해야 한다면, 이를 위한 새 제공자 패키지를 도입하거나 기존 패키지에 새 플러그인 API 엔드포인트를 추가할 것을 권장합니다. 두 경우 모두 이전 휠을 계속 설치할 수 있도록 이전 엔드포인트를 최소 유지 보수 모드로 보존해야 할 수도 있습니다. 이전 엔드포인트는 패키지를 빌드할 때 사용하는 get_all_configs() 훅에서 폐지 경고를 발생시킬 수 있습니다.

대안적인 접근 방식은 의미 버전 관리(semantic versioning)를 사용하여 호환성을 깨뜨리는 변경을 차단하는 것입니다. 하지만 이 방법은 패키지 작성자가 의존성에 상한을 빠짐없이 지정해야 성립합니다. 그렇지 않으면 이전 휠이 호환되지 않는 플러그인 버전을 사용하게 되기 때문입니다. 이는 오늘날 사용되는 Python 빌드 백엔드에서 이미 문제가 되고 있습니다.

플러그인을 벤더링하거나 재구현할 때, 설치 프로그램은 해당 플러그인의 현재 동작을 따라야 합니다. 특히 설치 프로그램은 관련 제공자의 버전 번호를 인식해야 하며, 해당 패키지가 설치 프로그램의 구현과 호환되지 않을 때는 외부 플러그인을 설치하는 방식으로 대체할 수도 있습니다.

소스 트리와 휠의 메타데이터

변형은 소스 트리와 휠에 저장되는 몇 가지 새로운 메타데이터를 도입합니다. 소스 트리에서는 다른 프로젝트 속성과 함께 pyproject.toml 파일에 저장되며, TOML 형식의 가독성과 엄격성의 이점을 누립니다. 그 후 동등한 JSON 구조로 변환되어 .dist-info 디렉터리에 별도의 파일로 저장됩니다. 불필요한 비호환성을 피하고 불편한 핵심 메타데이터 형식으로 직렬화하지 않기 위해 기존 메타데이터 파일은 변경하지 않습니다.

pyproject.toml의 메타데이터에는 다음이 포함됩니다:

  • 휠에서 사용할 수 있는 변형 제공자에 대한 정보,
  • 선택적으로, 기본 속성 순서를 재정의하는 목록,
  • 플러그인을 사용하지 않는 사전(Ahead-of-Time) 제공자를 위한 정적 속성 목록.

휠 메타데이터에는 위 내용에 더해 플러그인에서 얻은 정적 속성 목록과 빌드된 휠의 변형 속성이 추가됩니다.

휠이 색인에 게시되면 모든 휠의 변형 메타데이터가 하나의 {name}-{version}-variants.json 파일로 합쳐집니다. 클라이언트는 이 파일을 사용하여 개별 휠에서 따로 내려받거나 패키지 색인 서버가 제공하는 API에서 명시적인 변형 메타데이터 지원을 구현하지 않고도 변형 메타데이터를 효율적으로 얻을 수 있습니다.

ABI 의존성 변형 제공자

일부 패키지는 넓은 버전 범위에 걸쳐 호환되지 않는 애플리케이션 바이너리 인터페이스(ABI)를 노출하는 확장 모듈을 제공합니다. 이 인터페이스를 사용하는 패키지는 자신의 휠을 빌드 시점에 사용된 버전으로 고정해야 합니다. ABI가 자주 변경되면 고정 범위가 매우 좁아지며, 같은 의존성의 서로 다른 버전으로 고정되어 있을 수 있는 두 패키지를 설치해야 할 때 사용자는 문제를 겪게 됩니다. 서로 다른 의존성 버전을 대상으로 빌드된 변형을 제공하면, 설치되는 모든 패키지와 호환되는 의존성 버전을 리졸버가 찾아낼 가능성을 높일 수 있습니다.

안타깝게도 이러한 변형 제공자는 명세에서 정의한 플러그인 API 안에서는 구현할 수 없습니다. 견고한 구현이라면 의존성 해석기와 인터페이스해야 하므로, 이 사용 사례를 다루도록 API를 확장하여 결과적으로 상당한 복잡성을 더하는 대신, 명세는 abi_dependency를 이 기능을 제공하고자 하는 설치 프로그램이 구현할 수 있는 특수한 변형 네임스페이스로 예약합니다.

문제의 복잡성을 고려하여 이 확장은 전적으로 선택 사항으로 합니다. 이는 이 확장을 사용하는 모든 패키지가 비변형 휠도 함께 제공해야 함을 의미합니다.

패키징 도구를 위한 구현 로직 제안

색인에서 패키지 설치하기

A diagram showing installing a package including variant wheel building. It is split into three columns: Developer, Installer and Install-time providers. The diagram starts with Develop initiating package install. The subsequent steps involve installer, in order: resolver selects package version; determine if release has variant wheels. If there are no variant wheels, jump to installing package and report success. If the version has variant wheels, check user's variant preferences. In parallel, download JSON from index, then extract variant provider configuration. If it uses AoT providers only, converge to determine optimal variant immediately. If it requires install-time providers, the further path depends on whether non-vendored providers are included. If they are not, query install-time providers immediately and converge to determine optimal variant. If non-vendored providers are included, they are installed if not present in env and then queried. Querying providers involves an exchange of data with different providers (in the diagram, "provider 1" and "provider 2" are given as examples), each filtering and ordering supported configurations for the current environment. All the variant paths converge on determining optimal variant, which is following by installing package and reporting success.

A conceptual diagram of installing a wheel.

색인에서 특정 버전의 패키지를 설치하라는 요청을 받았을 때, 제안하는 도구 동작은 다음과 같습니다:

  1. 원격 색인에서 원하는 패키지를 조회합니다.
  2. 평소와 같이 버전 제약 조건을 만족하는 패키지 버전의 초기 일치 항목을 선택합니다(이 단계에서는 변형 메타데이터를 고려할 필요가 없습니다).
  3. 플랫폼 호환성 태그를 기준으로 사용 가능한 휠을 필터링합니다.
  4. 남은 휠 중에 변형 휠이 있는지 확인합니다. 없다면 비변형 휠과 같은 방식으로 진행합니다.
  5. 변형 레이블이 있는 휠이 있으면 색인 수준 변형 메타데이터 파일 {name}-{version}-variants.json을 다운로드합니다. 이 파일이 없으면 모든 변형 휠이 호환되지 않는다고 간주하고 비변형 휠과 같은 방식으로 진행합니다.
  6. 색인 수준 변형 메타데이터 파일을 사용하여 변형 레이블을 변형 속성 집합에 매핑합니다. 휠 파일 이름에 있는 레이블 중 이 파일에 없는 것이 있으면 해당 휠은 호환되지 않는다고 간주합니다.
  7. 색인 수준 변형 메타데이터 파일에 지정된 제공자를 사용하여 지원되는 변형 속성의 순서 있는 목록을 얻습니다:
    • 활성화된 AoT 제공자의 경우, 색인 수준 변형 메타데이터 파일의 정적 속성 데이터에서 얻습니다.
    • 활성화된 설치 시점 제공자의 경우:
      • 사용자가 정적 호환성 정보를 제공했다면 그 정보를 사용합니다.
      • 그렇지 않고 제공자가 벤더링되었거나 재구현되었다면 구현별 방식으로 제공자에 질의합니다.
      • 그렇지 않고 Python 제공자 패키지가 (설치 프로그램에 의해 또는 사용자의 명시적인 옵트인을 통해) 안전한 것으로 간주되면, 이를 격리된 환경에 설치하고 플러그인 API를 통해 질의합니다.
      • 위의 어느 경우에도 해당하지 않으면 제공자를 실행하지 말고, 변형 속성을 호환되지 않는 것으로 간주하거나 설치를 실패 처리합니다.
    • 비활성화된 제공자(예: 사용자가 활성화하지 않은 옵트인 제공자, 환경 마커로 제외된 제공자)의 경우, 해당 네임스페이스의 모든 변형 속성이 호환되지 않는다고 간주합니다.
  8. 지원되는 속성 목록을 기준으로 변형을 필터링하고 정렬한 다음, 가장 선호되는 변형을 선택합니다. 일치하는 변형 휠이 없으면 비변형 휠을 해당 규칙에 따라 사용합니다.
  9. 특정 버전의 여러 휠이 같은 변형 레이블을 공유하면 플랫폼 호환성 태그와 빌드 번호를 기준으로 정렬하고 가장 적합한 휠을 선택합니다.

로컬 휠 설치하기

로컬 wheel 파일을 설치하라는 요청을 받았을 때, 이 도구가 제안하는 동작은 다음과 같습니다:

  1. 파일 이름에 변형 레이블이 없으면 비변형 휠과 같은 방식으로 진행합니다.
  2. 플랫폼 호환성 태그로 휠의 호환성을 검증합니다.
  3. wheel 파일 내부의 *.dist-info/variant.json에서 변형 메타데이터를 읽습니다.
  4. Installing a package from an index에서와 같이 지원되는 변형 속성의 순서 있는 목록을 얻습니다.
  5. 지원되는 속성으로 휠의 호환성을 검증합니다.

변형 휠 빌드하기

변형 휠을 빌드하려면 빌드 백엔드가 변형 속성 목록과 변형 레이블을 전달받아야 합니다. 이를 위한 권장 방법은 빌드 백엔드 훅에 전달되는 config_settings 딕셔너리에서 백엔드가 정의한 키를 사용하는 것입니다.

변형 휠을 빌드할 때 제안하는 빌드 백엔드의 동작은 다음과 같습니다:

  1. pyproject.toml에서 변형 제공자 메타데이터를 읽습니다.
  2. 사용자가 정의한 변형 속성에 지정된 모든 네임스페이스에 대해 메타데이터에 대응하는 제공자가 있는지 검증합니다.
  3. get_requires_for_build_wheel() 훅에서는 다른 빌드 의존성과 함께 변형 제공자 플러그인 패키지를 반환합니다.
  4. build_wheel() 훅에서는 제공자 플러그인의 get_all_configs() 함수를 조회하여 유효한 속성 키와 값을 모두 얻습니다. 이를 사용하여 지정된 속성이 올바른지 검증합니다.
  5. pyproject.toml의 변형 메타데이터를 JSON으로 변환하고 변형 레이블에서 변형 속성으로의 매핑을 덧붙인 뒤 그 결과를 휠의 *.dist-info/variant.json 파일에 기록합니다.
  6. *.dist-info/variant.json 파일을 포함하고 파일 이름에 변형 레이블을 넣는다는 점을 제외하면 평소와 같이 휠을 빌드합니다.

색인에 변형 휠 게시하기

변형 휠은 일반 휠과 똑같이 색인에 업로드됩니다. 패키지 버전마다 색인 수준의 {name}-{version}-variants.json 파일을 게시하는 방법은 두 가지입니다. 사용자가 이 파일을 준비하여 업로드할 수도 있고, 색인이 자동으로 생성할 수도 있습니다.

클라이언트가 이미 이 파일을 캐시했거나 기존 해시에 고정했을 수 있으므로, 파일을 한 번 게시한 후에는 변경해서는 안 됩니다. 이러한 이유로 색인이 파일 생성을 담당하는 경우에는 릴리스가 완전히 업로드될 때까지 파일 게시를 미루는 메커니즘을 사용해야 합니다(예: PEP 694).

{name}-{version}-variants.json 파일을 생성하는 방법은 다음과 같습니다.

  1. 주어진 패키지 버전의 첫 번째 변형 휠에 대해서는 해당 휠의 *.dist-info/variant.json 파일에서 데이터를 복사합니다.
  2. 이후의 휠에 대해서는 각 휠의 *.dist-info/variant.json 파일에 있는 데이터를 기존 데이터에 병합합니다.
    • providers, static-properties, variants 딕셔너리에서 서로 겹치지 않는 키는 함께 병합합니다
    • 이 딕셔너리들의 공통 키는 정확히 같은 값을 가져야 합니다
    • default-priorities.namespace 리스트는 새 값이 기존 값으로 시작하는 경우 교체할 수 있습니다
    • default-priorities.feature 및 default-priorities.value 키는 이전 default-priorities.namespace 값에 없던 경우 추가할 수 있습니다
    • 그 밖의 키는 정확히 같은 값을 가져야 합니다

사용 사례 예시

PyTorch CPU/GPU 변형

2025년 10월 기준으로 PyTorch는 릴리스마다 총 일곱 가지 변형을 게시합니다. CPU 전용 변형 하나, 최소 CUDA 런타임 버전과 지원 GPU가 서로 다른 CUDA 변형 셋, ROCm 변형 둘, Linux XPU 변형 하나입니다.

설치된 런타임 버전과 설치된 GPU/XPU를 조회하는 GPU/XPU 플러그인을 사용하면 이 구성을 개선할 수 있습니다. 이러한 플러그인은 런타임을 사용할 수 없거나, 런타임이 너무 오래되었거나, 사용자의 GPU가 지원되지 않는 휠을 걸러내고, 남은 변형을 런타임 버전 순으로 정렬합니다. CPU 전용 버전은 항상 지원되는 null 변형으로 게시됩니다.

GPU 런타임을 사용할 수 있고 지원되는 경우, 설치 프로그램은 지원되는 가장 최신 런타임용 휠을 자동으로 선택합니다. 그렇지 않으면 CPU 전용 변형으로 대체합니다. 여러 가속기를 사용할 수 있고 모두 지원되는 예외적인 경우에는 PyTorch 패키지 유지 관리자가 기본적으로 어느 가속기를 우선할지 지정합니다.

최적화된 CPU 변형

휠 변형을 사용하면 현재 플랫폼 태그가 제공하는 범위를 넘어 특정 CPU 확장이 필요한 변형을 제공할 수 있습니다. 휠 변형은 특히 런타임 디스패치가 비현실적인 경우, 패키지가 기준선을 넘는 명령어를 사용하는 사전 빌드된 구성 요소에 의존하는 경우, 명령어 집합의 가용성에 따라 라이브러리 ABI가 달라지는 경우, 또는 단순히 코드베이스 전반에 적용되는 자동 벡터화와 같은 컴파일러 최적화의 이점을 얻으려는 경우에 유용합니다.

예를 들어 x86-64 CPU 플러그인은 설치된 CPU의 기능을 감지하고, 이를 적절한 x86-64 아키텍처 수준과 확장 명령어 집합들에 매핑할 수 있습니다. 변형 휠은 어떤 수준 및/또는 명령어 집합이 필요한지 나타냅니다. 설치 프로그램은 요구 사항을 충족하지 않는 변형을 걸러 내고 가장 잘 최적화된 변형을 선택합니다. 지원되는 경우 비변형 휠을 사용하여 아키텍처 기준선을 나타낼 수 있습니다.

휠 변형을 사용해 구현하면 필요한 만큼 자주 업데이트할 수 있는 플러그인으로 요구되는 명령어 집합을 세밀하게 표시할 수 있습니다. 특히 처음부터 사용 가능한 모든 명령어 집합을 다룰 필요도 없고, 명령어 집합 지원 범위를 개선해야 할 때마다 설치 프로그램을 업데이트할 필요도 없습니다.

BLAS / LAPACK 변형

NumPy와 SciPy 같은 패키지는 서로 다른 BLAS / LAPACK 라이브러리를 사용하여 빌드할 수 있습니다. 사용자는 특정 하드웨어에서 성능을 높이거나 라이선스를 고려하여 특정 라이브러리를 선택하고 싶을 수 있습니다. 또한 라이브러리마다 서로 다른 OpenMP 구현을 사용할 수 있습니다. 반면 스택 전체에서 일관된 구현을 사용하면 스레드를 지나치게 많이 생성하여 성능이 저하되는 것을 피할 수 있습니다.

특정 플랫폼용으로 빌드된 모든 변형은 해당 플랫폼과 호환됩니다. 따라서 BLAS / LAPACK 변형은 설치 시점에 플러그인이 필요하지 않습니다. 따라서 미리 정의된 BLAS / LAPACK 라이브러리 이름 집합을 제공하며 install-time = false로 설정된 사전(ahead-of-time) 제공자를 사용할 수 있습니다. 패키지를 설치할 때는 일반적으로 기본 변형이 사용되지만, 사용자가 다른 변형을 명시적으로 선택할 수도 있습니다.

디버그 패키지 변형

패키지는 일반 릴리스 빌드 외에도 디버깅이나 CI 목적으로 디버그가 활성화된 특수 빌드를 제공하고 싶을 수 있습니다. 이를 위해 install-time = false와 optional = true를 지정한 선택적 사전 제공자를 사용하여 디버그 빌드용 사용자 정의 속성을 정의할 수 있습니다. 이 제공자는 기본적으로 비활성화되어 있으므로, 사용자는 일반적으로 릴리스 빌드를 제공하는 비변형 휠을 설치합니다. 그러나 선택적 제공자를 활성화하거나 변형을 명시적으로 선택하면 디버그 빌드를 쉽게 얻을 수 있습니다.

패키지 ABI 일치

vLLM 같은 패키지는 애플리케이션 바이너리 인터페이스(Application Binary Interface, ABI) 호환성을 유지하기 위해 빌드할 때 사용한 PyTorch 버전에 고정되어야 합니다. 이 때문에 패키지 버전이 불필요하게 엄격하게 고정되는 경우가 많습니다. 그 결과 서로 다른 버전의 PyTorch를 요구하는 여러 패키지가 포함된 환경에서 만족스러운 해결 결과를 찾을 수 없게 되거나 소스 빌드에 의존하게 됩니다. 변형 휠을 사용하면 서로 다른 PyTorch 버전을 기준으로 빌드된 vLLM 변형들을 게시할 수 있습니다. 따라서 업스트림이 여러 버전을 동시에 쉽게 지원할 수 있습니다.

선택적 abi_dependency 확장을 사용하면 서로 다른 PyTorch 버전에 고정된 여러 vllm 변형을 빌드할 수 있습니다. 예를 들면 다음과 같습니다.

  • abi_dependency :: torch :: 2.9를 가진 vllm-0.11.0-...-torch29.wheel
  • abi_dependency :: torch :: 2.8을 가진 vllm-0.11.0-...-torch28.wheel
  • abi_dependency :: torch :: 2.7을 가진 vllm-0.11.0-...-torch27.wheel

보안 영향

이 제안은 변형 휠의 호환 여부를 판단하기 위해 시스템 기능을 조회하는 플러그인 시스템을 도입합니다. 이 시스템에서는 플러그인을 제공하는 추가 Python 패키지를 패키지 색인 메타데이터에 지정할 수 있습니다. 특정 휠을 설치할 수 있는지 판단하거나 여러 변형 휠 가운데 가장 선호되는 변형을 선택해야 하는 설치 프로그램과 기타 도구는, 의존성을 해석하거나 휠을 처리하는 동안 이러한 패키지를 설치하고 그 안의 코드를 실행해야 할 수도 있습니다.

이 시스템은 악의적인 행위자가 임의의 코드 페이로드를 주입할 수 있는 새로운 지점을 두 가지 만듭니다. 따라서 공급망 공격 가능성이 높아집니다.

  1. 악성 코드가 포함된 변형 제공자 플러그인이나 그 의존성 중 하나의 버전을 게시하는 것
  2. 기존 패키지 메타데이터에 악성 변형 제공자 플러그인을 도입하는 경우.

이러한 공격은 패키지 의존성 수준에서도 이미 가능하지만, 일부 시나리오에서는 영향을 받는 도구가 상승된 권한으로 실행된다는 점을 강조할 필요가 있습니다. 예를 들어 다중 사용자 시스템에 패키지를 설치하는 경우, 도구는 상승된 권한으로 실행되지만 설치된 패키지는 이후 일반 사용자 권한으로만 사용됩니다. 따라서 변형 제공자 플러그인은 상승된 권한으로 원격 코드 실행(Remote Code Execution) 취약점을 초래할 수 있습니다.

패키징 생태계에는 패키지를 소스 배포판에서 설치할 때 이미 비슷한 문제가 존재하며, 이때 빌드 백엔드와 기타 빌드 의존성이 설치되고 실행됩니다. 그러나 휠만을 다루는 여러 도구와, 도구별 옵션으로 소스 배포판 사용을 비활성화하는 사용자는 의존성을 해석하거나 휠을 설치하거나 그 밖의 방식으로 휠을 처리하는 동안 시스템 외부의 코드가 실행되지 않는다는 가정에 의존해 왔습니다. 이 가정을 유지하기 위해, 이 제안은 신뢰할 수 없는 제공자 플러그인 패키지가 사용자의 명시적인 동의 없이 절대 설치되지 않을 것을 명시적으로 요구합니다.

명세의 Providers 섹션은 보안과 사용자 경험을 모두 개선하기 위한 추가 제안을 제공합니다. 특히 가장 널리 사용되는 제공자 플러그인은 별도 설정 없이 바로 사용할 수 있을 것으로 예상되며, 전담 유지 관리자 팀(초기에는 PEP 작성자 중 일부를 포함)이 이러한 플러그인의 보안 위험을 검사하고 사용하기에 안전한지 검증하는 책임을 맡게 됩니다. 설치 프로그램은 공개된 허용 목록을 사용하거나, 특정 제공자 플러그인 버전을 벤더링하거나, 이를 재구현하거나, Python 라이브러리로 사용하는 것 중에서 자유롭게 선택할 수 있습니다.

그 결과 대부분의 패키지는 경쟁하는 해결책을 구현하기보다 이러한 특정 플러그인에 집중하게 될 것입니다. 명시적인 옵트인이 필요한 플러그인은 드물어야 하며, 주로 전문 사용자에게만 영향을 주어야 합니다. 이는 변형 사용을 기본적으로 안전하게 만드는 데 중요합니다. 게다가 워크플로가 자주 중단되면 사용자는 모든 플러그인을 일괄 허용하려는 경향을 보이게 됩니다(보안 피로).

또한 이 명세는 정적 구성을 입력으로 사용하여 플러그인 실행을 완전히 건너뛰는 것을 허용합니다.

명세

이 PEP는 Binary distribution format 명세에 대한 일련의 확장을 제안합니다. 이 확장을 통해 휠의 추가 변형을 빌드할 수 있으며, 이러한 변형은 변형을 인식하는 도구로 설치할 수 있고 이 명세를 구현하지 않는 프로그램에서는 무시됩니다.

정의

이 문서의 핵심 단어 “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, “OPTIONAL”은 RFC 2119에 기술된 대로 해석해야 합니다.

확장된 휠 파일 이름

원래 PEP 427에서 정의한 휠 파일 이름 템플릿은 다음과 같이 변경됩니다:

{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}(-{variant label})?.whl
                                                                             +++++++++++++++++++

이 PEP에서 도입한 확장을 사용하는 휠은 반드시(MUST) 변형 레이블 구성 요소를 포함해야 합니다. 레이블은 반드시(MUST) 다음 규칙을 따라야 합니다:

  • 소문자만 사용(대소문자를 구분하는 파일 시스템과 구분하지 않는 파일 시스템 간의 문제를 방지하기 위함)
  • 1~16자
  • 0-9, a-z, . 또는 _ ASCII 문자만 사용

이는 다음 정규 표현식과 동일합니다: ^[0-9a-z._]{1,16}$.

모든 레이블은 반드시(MUST) 특정 변형 속성 집합 하나에 고유하게 대응해야 하며, 이 집합은 단일 패키지 버전 안에서 같은 레이블을 사용하는 모든 휠에 대해 반드시(MUST) 동일해야 합니다. 변형 레이블은 휠 빌드 시점에 사람이 읽을 수 있는 문자열로 지정해야 합니다(SHOULD). 레이블 null은 null 변형용으로 예약되어 있으며, 반드시(MUST) 빈 변형 속성 집합을 사용해야 합니다.

이 명세를 구현하지 않는 설치 프로그램은 색인에서 설치할 때 변형 레이블이 있는 휠을 반드시 무시해야 하며(MUST), 그러한 레이블이 없는 휠이 있으면 그 휠로 대체해야 합니다. 그러한 휠이 없으면 설치 프로그램은 적절한 진단 메시지를 출력해야 하며(SHOULD), 특히 그 결과 더 이전의 패키지 버전이 선택된다면 경고를, 설치할 수 있는 패키지 버전이 없다면 명확한 오류를 출력해야 합니다.

예시:

  • 비변형 휠: numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl
  • 변형 레이블이 있는 휠: numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64-x86_64_v3.whl

변형 속성

모든 변형 휠은 0개 이상의 변형 속성으로 기술되어야 합니다. 속성이 정확히 0개인 변형 휠은 널 변형을 나타냅니다. 속성은 변형 휠을 빌드할 때 프로젝트의 빌드 백엔드가 정의한 메커니즘을 사용하여 지정됩니다.

각 변형 속성은 3-튜플로 기술되며, 이 튜플은 다음 형식으로 직렬화됩니다:

{namespace} :: {feature_name} :: {feature_value}

네임스페이스는 0-9, a-z, _ ASCII 문자(^[a-z0-9_]+$)로만 구성되어야 합니다. 네임스페이스는 하나의 변형 제공자에 대응해야 합니다.

기능 이름은 0-9, a-z, _ ASCII 문자(^[a-z0-9_]+$)로만 구성되어야 합니다. 기능 이름은 해당 네임스페이스의 변형 제공자가 정의한 유효한 기능 이름에 대응해야 합니다.

기능 값은 0-9, a-z, _, . ASCII 문자(^[a-z0-9_.]+$)로만 구성되어야 합니다. 기능 값은 해당 기능에 대해 변형 제공자가 정의한 유효한 값에 대응해야 합니다.

제공자 플러그인이 어떤 기능을 “다중 값”으로 표시한 경우, 하나의 변형 휠은 동일한 네임스페이스와 기능 이름을 공유하는 여러 속성을 정의할 수 있습니다. 그렇지 않은 경우, 하나의 변형 휠 안에서 네임스페이스와 기능 이름의 한 쌍에 대응하는 값이 둘 이상 있어서는 안 됩니다.

변형 휠이 시스템과 호환되는 것으로 간주되려면, 그 안에 정의된 모든 기능이 호환되는 것으로 판정되어야 합니다. 기능이 호환되려면, 그 기능에 대응하는 값 중 적어도 하나가 호환되어야 합니다.

예시:

# all of the following must be supported
x86_64 :: level :: v3
x86_64 :: avx512_bf16 :: on
nvidia :: cuda_version_lower_bound :: 12.8
# additionally, at least one of the following must be supported
nvidia :: sm_arch :: 120_real
nvidia :: sm_arch :: 110_real

제공자

변형 휠을 설치하거나 해석할 때, 설치 프로그램은 주어진 휠의 속성이 시스템과 호환되는지 확인하고 Variant ordering을 통해 최적의 변형을 선택하기 위해 변형 제공자에게 질의해야 합니다(SHOULD). 다만 설치 도구는 이 검증을 생략하고 지정된 변형을 명시적으로 설치하는 옵션을 제공할 수 있습니다.

제공자는 설치 시점 제공자 또는 사전 제공자로 표시될 수 있습니다. 설치 시점 제공자의 경우, 설치 프로그램은 반드시 제공자에게 변형 속성 호환성을 질의하거나 사용자가 제공한 호환성 정보를 사용해야 합니다(MUST). 설치 프로그램은 특정 제공자를 벤더링하거나 재구현할 수 있습니다(MAY). 사용자가 제공하는 정보의 형식은 구현에서 정의하도록 남겨 둡니다.

사전 제공자의 경우, 설치 도구는 그 대신 휠에 내장된 정적 메타데이터를 사용해야 합니다.

제공자는 선택적 제공자로 표시될 수 있습니다. 제공자가 선택 사항으로 표시되어 있으면, 설치 프로그램은 기본적으로 해당 제공자에게 질의해서는 안 되며(MUST NOT), 대신 그 속성이 호환되지 않는다고 가정해야 합니다. 설치 도구는 선택적 제공자를 활성화하는 옵션을 제공하는 것이 좋습니다.

제공자는 Environment Markers에 따라 조건부로 만들 수도 있습니다. 그런 경우 설치 프로그램은 휠이 설치될 환경을 기준으로 마커를 반드시 검사해야 합니다(MUST). 설치 도구는 마커가 일치하지 않는 제공자를 사용해서는 안 되며, 대신 그 속성이 호환되지 않는다고 가정해야 합니다.

변형 제공자를 조회해야 하고 보안에 민감한 환경에서 실행되는 모든 도구는, 특정 제공자 패키지 버전을 신뢰할 수 있다고 판단할 수 있는 경우가 아니라면 제공자 패키지를 설치하거나 실행해서는 안 됩니다. 이를 위한 정확한 메커니즘은 구현마다 다릅니다. 그러나 설치 프로그램은 가장 흔히 사용되는 제공자를 사용자의 명시적인 옵트인 없이도 안전하게 사용할 수 있도록 보장해야 합니다(SHOULD).

제공자 패키지를 설치할 때 도구는 격리된 가상 환경을 사용해야 합니다.

설치 시점 제공자 패키지는 공급망 공격을 막기 위한 조치를 취해야 합니다. 예를 들어 모든 의존성을 벤더링하는 방법이 있습니다.

도구 간에 일관된 경험을 제공하기 위해 변형 휠은 기본적으로 지원되어야 합니다. 도구는 비변형 휠만 사용하도록 하는 옵션을 제공할 수 있습니다.

변형 메타데이터

이 절에서는 패키지와 그 휠의 제공자, 변형, 속성에 대한 메타데이터 형식을 설명합니다. 이 형식은 세 곳에서 약간씩 다르게 사용됩니다:

  1. 소스 트리에서는 pyproject.toml 파일 안에
  2. 빌드된 휠에서는 *.dist-info/variant.json 파일로
  3. 패키지 색인에서는 {name}-{version}-variants.json 파일로.

세 가지 변형 메타데이터 파일은 모두 JSON과 호환되는 공통 구조를 공유합니다:

(root)
|
+- providers
|  +- {namespace}
|     +- enable-if     : str | None = None
|     +- install-time  : bool       = True
|     +- optional      : bool       = False
|     +- plugin-api    : str | None = None
|     +- requires      : list[str]  = []
|
+- default-priorities
|  +- namespace        : list[str]
|  +- feature
|     +- {namespace}   : list[str]  = []
|  +- property
|     +- {namespace}
|        +- {feature}  : list[str]  = []
|
+- static-properties
|  +- {namespace}
|     +- {feature}     : list[str]  = []
|
+- variants
  +- {variant_label}
     +- {namespace}
        +- {feature}   : list[str]  = []

최상위 객체는 이를 담고 있는 파일의 특정 지점을 루트로 하는 딕셔너리입니다. 이 딕셔너리의 각 키는 하위 딕셔너리이며, 각 하위 딕셔너리와 그 존재 여부에 대한 요구 사항은 이어지는 절에서 설명합니다. 도구는 이 PEP의 향후 갱신에 대한 상위 호환성을 위해 딕셔너리의 알 수 없는 키를 반드시 무시해야 합니다. 그러나 사용자는 향후 발생할 수 있는 충돌을 피하기 위해 지원되지 않는 키를 사용해서는 안 됩니다.

메타데이터 형식의 컴프리헨션(이해)과 검증을 돕기 위해 이 PEP의 부록에 JSON 스키마가 포함되어 있습니다. 이 스키마는 변형 메타데이터 명세가 개정될 때마다 갱신됩니다. 스키마는 부록: 변형 메타데이터용 JSON 스키마에서 확인할 수 있습니다.

최종적으로 변형 메타데이터 JSON 스키마는 packaging.python.org에서 제공되어야 합니다.

제공자 정보

providers는 딕셔너리이며, 키는 네임스페이스이고 값은 제공자 정보를 담은 딕셔너리입니다. 이는 변형 제공자를 설치하고 사용하는 방법을 지정합니다. 패키지가 지원하는 모든 변형 네임스페이스에 대해 제공자 정보 딕셔너리를 pyproject.toml에 반드시 선언해야 합니다. 이 딕셔너리는 해당 휠에서 사용되지 않는 제공자의 데이터까지 포함하여 variant.json에 그대로 반드시 복사되어야 합니다.

제공자 정보의 사용 방법은 Providers 절과 Provider plugin API 절에서 설명합니다.

제공자 정보 딕셔너리에는 다음 키가 포함될 수 있습니다:

  • enable-if: str: 플러그인을 언제 사용해야 하는지 정의하는 환경 마커입니다.
  • install-time: bool: 설치 시점 제공자인지 여부입니다. 기본값은 true입니다. false는 대신 AoT 제공자임을 의미합니다.
  • optional: bool: 프로바이더가 선택 사항인지 여부입니다. 기본값은 false입니다. true이면 프로바이더는 선택 사항으로 간주됩니다.
  • plugin-api: str: 플러그인의 API 엔드포인트입니다. 지정된 경우, API endpoint 절에서 설명한 대로 객체 참조여야 합니다(MUST). 누락된 경우, requires의 첫 번째 의존성 지정자에 있는 패키지 이름을 정규화한 뒤 그 이름의 모든 - 문자를 _로 바꾸어 사용합니다.
  • requires: list[str]: 프로바이더 플러그인을 설치하는 데 사용되는 0개 이상의 패키지 의존성 지정자 리스트입니다. 의존성 지정자에 환경 마커가 포함되어 있으면, 플러그인이 설치되는 환경을 기준으로 마커를 평가하고 마커가 거짓으로 평가되는 요구 사항은 걸러냅니다. 이 경우, 가능한 모든 환경에서 적어도 하나의 의존성이 남아 있어야 합니다(MUST). 또한 plugin-api가 지정되지 않은 경우, 필터링 후 남은 첫 번째 의존성은 항상 같은 API 엔드포인트로 평가되어야 합니다(MUST).

모든 필드는 선택 사항(OPTIONAL)이며, 예외는 다음과 같습니다:

  1. install-time이 참이면, 딕셔너리는 설치 시점 프로바이더를 기술하며 requires 키가 존재하고 적어도 하나의 의존성을 지정해야 합니다(MUST).
  2. install-time이 거짓이면, 딕셔너리는 AoT 프로바이더를 기술하며 requires 키는 선택 사항(OPTIONAL)입니다. 이 경우:
    1. requires가 제공되고 비어 있지 않다면, 제공자 딕셔너리는 빌드 시 static-properties를 채우기 위해 질의될 AoT 제공자 플러그인을 반드시(MUST) 참조해야 합니다.
    2. 그렇지 않으면 static-properties를 pyproject.toml에 반드시(MUST) 지정해야 합니다.

기본 우선순위

default-priorities 딕셔너리는 변형(variant)의 순서를 제어합니다. 정확한 알고리즘은 Variant ordering 절에 설명되어 있습니다.

이 딕셔너리에는 필수(REQUIRED) 키가 하나 있습니다:

  • namespace: list[str]: 휠 변형이 사용하는 모든 네임스페이스이며, 우선순위가 높은 것부터 낮은 것 순으로 정렬됩니다. 이 리스트는 providers 딕셔너리의 키와 같은 멤버를 가져야 합니다(MUST).

다음과 같은 선택(OPTIONAL) 키를 가질 수 있습니다(MAY):

  • feature: dict[str, list[str]]: 네임스페이스를 키로, 해당 기능 이름의 순서 있는 리스트를 값으로 가지는 딕셔너리입니다. 각 리스트의 값은 프로바이더 출력의 기본 순서를 재정의합니다. 값은 가장 높은 우선순위부터 가장 낮은 우선순위 순으로 나열됩니다. 리스트에 없는 기능은 리스트에 있는 기능보다 우선순위가 낮은 것으로 간주되며, 그 기능들 사이의 상대적 우선순위는 플러그인이 정의합니다.
  • property: dict[str, dict[str, list[str]]]: 네임스페이스를 첫 번째 수준 키로, 기능 이름을 두 번째 수준 키로, 해당 속성 값의 순서 있는 목록을 두 번째 수준 값으로 갖는 중첩 딕셔너리입니다. 목록에 있는 값은 제공자 출력의 기본 순서를 재정의합니다. 값은 가장 높은 우선순위부터 가장 낮은 우선순위 순으로 나열됩니다. 목록에 없는 속성은 목록에 있는 속성보다 우선순위가 낮은 것으로 간주되며, 이들 사이의 상대적 우선순위는 플러그인 출력으로 정의됩니다.

정적 속성

static-properties 딕셔너리는 AoT 제공자가 지원하는 속성을 지정합니다. 이는 네임스페이스를 첫 번째 수준 키로, 기능 이름을 두 번째 수준 키로, 기능 값의 순서 있는 목록을 두 번째 수준 값으로 갖는 중첩 딕셔너리입니다.

pyproject.toml 파일에서 이 딕셔너리에 있는 네임스페이스는 플러그인이 없는 모든 AoT 제공자(즉, install-time이 false이고 requires가 없거나 비어 있는 제공자)에 대응해야 합니다. 휠을 빌드할 때 빌드 백엔드는 AoT 제공자 플러그인(즉, install-time이 false이고 requires가 비어 있지 않은 플러그인)에 질의하여 지원되는 속성을 얻고 이를 딕셔너리에 포함해야 합니다. 따라서 variant.json 및 *-variants.json의 딕셔너리는 모든 AoT 제공자(즉, install-time이 false인 모든 제공자)의 네임스페이스를 포함해야 합니다.

TOML과 JSON 딕셔너리는 정렬되지 않으므로 static-properties 딕셔너리의 기능도 정렬되지 않습니다. 하나의 네임스페이스에 둘 이상의 기능이 지정된 경우, 모든 기능의 순서를 default-priorities.feature.{namespace}에 지정해야 합니다. AoT 플러그인을 사용하여 static-properties를 채우는 경우, pyproject.toml의 목록에 아직 없는 기능을 그 목록에 덧붙여야 합니다.

값 목록은 get_supported_configs() 플러그인 API 호출(Plugin interface에 정의됨)이 반환하는 목록과 마찬가지로 가장 선호되는 값부터 가장 덜 선호되는 값 순으로 정렬됩니다. default-priorities.property 딕셔너리를 사용하여 속성 순서를 재정의할 수 있습니다.

변형

variants 딕셔너리는 variant.json에서는 휠이 빌드된 대상 변형을 나타내는 데 사용되고, *-variants.json에서는 사용 가능한 모든 휠 변형을 나타내는 데 사용됩니다. 이는 변형 레이블별로 모든 속성을 나열하는 3단계 딕셔너리입니다. 첫 번째 수준 키는 변형 레이블이고, 두 번째 수준 키는 네임스페이스이며, 세 번째 수준 키는 기능 이름이고, 세 번째 수준 값은 기능 값의 목록입니다.

pyproject.toml: 변형 프로젝트 수준 데이터 테이블

pyproject.toml 파일은 pyproject.toml specification에 정의된 표준 프로젝트 설정 파일입니다. 변형 메타데이터는 variant라는 이름의 최상위 테이블을 루트로 해야 합니다. 이 테이블은 variants 딕셔너리를 지정해서는 안 됩니다. 이 테이블은 빌드 백엔드가 변형 휠을 빌드하는 데 사용됩니다.

구조 예시:

[variant.default-priorities]
# prefer CPU features over BLAS/LAPACK variants
namespace = ["x86_64", "aarch64", "blas_lapack"]

# prefer aarch64 version and x86_64 level features over other features
# (specific CPU extensions like "sse4.1")
feature.aarch64 = ["version"]
feature.x86_64 = ["level"]

# prefer x86-64-v3 and then older (even if CPU is newer)
property.x86_64.level = ["v3", "v2", "v1"]

[variant.providers.aarch64]
# example using different package based on Python version
requires = [
   "provider-variant-aarch64 >=0.0.1; python_version >= '3.12'",
   "legacy-provider-variant-aarch64 >=0.0.1; python_version < '3.12'",
]
# use only on aarch64/arm machines
enable-if = "platform_machine == 'aarch64' or 'arm' in platform_machine"
plugin-api = "provider_variant_aarch64.plugin:AArch64Plugin"

[variant.providers.x86_64]
requires = ["provider-variant-x86-64 >=0.0.1"]
# use only on x86_64 machines
enable-if = "platform_machine == 'x86_64' or platform_machine == 'AMD64'"
plugin-api = "provider_variant_x86_64.plugin:X8664Plugin"

[variant.providers.blas_lapack]
# plugin-api inferred from requires
requires = ["blas-lapack-variant-provider"]
# plugin used only when building package, properties will be inlined
# into variant.json
install-time = false

*.dist-info/variant.json: 패키징된 변형 메타데이터 파일

variant.json 파일은 빌드된 변형 휠의 *.dist-info/ 디렉터리에 있어야 합니다. 이 파일은 변형 메타데이터 딕셔너리를 최상위 객체로 하여 JSON으로 직렬화됩니다. 이 파일은 pyproject.toml에 있는 모든 변형 메타데이터를 개별 키 절에 명시된 대로 복사하여 포함해야 합니다. 그 외에도 다음을 포함해야 합니다.

  • 이 PEP의 부록에 제공된 스키마 파일에 대응하는 URL을 값으로 갖는 $schema 키. 이 URL에는 형식의 버전이 포함되며, 향후 형식이 변경될 때마다 새 버전을 부록에 추가해야 합니다,
  • 정확히 하나의 변형, 즉 휠이 제공하는 변형을 나열하는 variants 객체.

x86-64-v3용 예제 pyproject.toml 파일로 빌드한 휠에 해당하는 variant.json 파일은 다음과 같습니다:

{
  // The schema URL will be replaced with the final URL on packaging.python.org
  "$schema": "https://variants-schema.wheelnext.dev/v0.0.3.json",
  "default-priorities": {
     "feature": {
        "aarch64": ["version"],
        "x86_64": ["level"]
     },
     "namespace": ["x86_64", "aarch64", "blas_lapack"],
     "property": {
        "x86_64": {
           "level": ["v3", "v2", "v1"]
        }
     }
  },
  "providers": {
     "aarch64": {
        "enable-if": "platform_machine == 'aarch64' or 'arm' in platform_machine",
        "plugin-api": "provider_variant_aarch64.plugin:AArch64Plugin",
        "requires": [
           "provider-variant-aarch64 >=0.0.1; python_version >= '3.12'",
           "legacy-provider-variant-aarch64 >=0.0.1; python_version < '3.12'"
        ]
     },
     "blas_lapack": {
        "install-time": false,
        "requires": ["blas-lapack-variant-provider"]
     },
     "x86_64": {
        "enable-if": "platform_machine == 'x86_64' or platform_machine == 'AMD64'",
        "plugin-api": "provider_variant_x86_64.plugin:X8664Plugin",
        "requires": ["provider-variant-x86-64 >=0.0.1"]
     }
  },
  "static-properties": {
     "blas_lapack": {
        "provider": ["accelerate", "openblas", "mkl"]
     },
  },
  "variants": {
     // always a single entry, expressing the variant properties of the wheel
     "x8664v3_openblas": {
        "blas_lapack": {
           "provider": ["openblas"]
        },
        "x86_64": {
           "level": ["v3"]
        }
     }
  }
}

{name}-{version}-variants.json: 색인 수준 변형 메타데이터 파일

변형 휠을 하나 이상 포함하는 모든 패키지 버전에는 패키지 색인이 호스팅하고 제공하는 해당 {name}-{version}-variants.json 파일이 반드시(MUST) 있어야 합니다. {name}과 {version} 자리 표시자는 바이너리 배포 형식 명세의 File name convention에 나와 있는 것처럼 wheel 파일과 같은 규칙에 따라 정규화된 패키지 이름과 버전에 해당합니다. 변형 휠이 링크된 모든 색인 페이지에는 이 파일의 링크가 반드시(MUST) 있어야 합니다. 이 링크는 색인의 소스 배포판 및 휠 링크와 동일한 단순 저장소 형식으로 제공되며, (선택 사항인(OPTIONAL)) 해시를 포함합니다.

이 파일은 앞에서 설명한 variant.json과 같은 구조를 사용합니다. 다만 variants 객체에는 해당 패키지 버전에 대해 패키지 색인에서 제공하는 모든 변형을 반드시(MUST) 나열해야 합니다. 도구는 파일에 나열된 모든 변형에 대해 default-priorities, providers, static-properties 섹션의 내용이 같도록 강제할 것을 권장합니다(RECOMMENDED). 다만 서로 충돌하는 정보가 생기지 않고 변형의 부분 집합 안에서 해석 결과가 바뀌지 않는다면 신중하게 병합할 수도 있습니다.

색인은 업로드된 휠 메타데이터로부터 색인 수준 변형 메타데이터 파일을 자동으로 생성할 수 있습니다(MAY). 그런 경우 파일은 내용이 확정되기 전에는 게시하지 않아야 하며(SHOULD NOT), 게시한 뒤에는 변경하지 않아야 합니다(SHOULD NOT). 클라이언트가 이 파일을 캐시할 수 있기(MAY) 때문입니다.

파일을 자동으로 생성하지 않는 경우, 색인은 패키지 유지 관리자가 이 파일을 업로드할 수 있도록 반드시(MUST) 허용해야 합니다. 변형 수준 메타데이터 파일이 업로드된 뒤에는 패키지 유지 관리자가 해당 버전의 새 변형을 업로드하지 않아야 합니다(SHOULD NOT).

휠 변형 두 개를 가진 패키지(그중 하나는 앞의 예제에 나온 변형입니다)에 해당하는 foo-1.2.3-variants.json 파일은 다음과 같습니다:

{
  // The schema URL will be replaced with the final URL on packaging.python.org
  "$schema": "https://variants-schema.wheelnext.dev/v0.0.3.json",
  "default-priorities": {
     // identical to above
  },
  "providers": {
     // identical to above
  },
  "static-properties": {
     // identical to above
  },
  "variants": {
     // all available wheel variants
     "x8664v3_openblas": {
        "blas_lapack": {
           "provider": ["openblas"]
        },
        "x86_64": {
           "level": ["v3"]
        }
     },
     "x8664v4_mkl": {
        "blas_lapack": {
           "provider": ["mkl"]
        },
        "x86_64": {
           "level": ["v4"]
        }
     }
 }
}

변형 순서 지정

호환되는 휠이 여러 개일 때 어느 변형 휠을 설치할지 결정하려면, 변형 휠을 변형 속성에 따라 반드시(MUST) 정렬해야 합니다.

정렬을 위해 변형 속성은 기능별로 묶이고, 기능은 다시 네임스페이스별로 묶입니다. 정렬 결과는 반드시(MUST) 다음 알고리즘과 동등해야 합니다:

  1. default-priorities.namespace 키의 값을 복사하여 네임스페이스의 순서 목록을 만듭니다.
  2. 모든 네임스페이스에 대해:
    1. 해당 default-priorities.feature.{namespace} 키의 값을 복사하여 기능 이름의 초기 순서 목록을 구성합니다.
    2. 제공자에게서 지원되는 기능 이름을 순서대로 가져옵니다. 만든 목록에 없는 기능 이름은 모두 목록 끝에 추가합니다.

    이 단계가 끝나면 모든 네임스페이스마다 순서가 정해진 기능 이름 목록이 만들어집니다.

  3. 모든 기능에 대해:
    1. 해당 default-priorities.property.{namespace}.{feature_name} 키의 값을 복사하여 값의 초기 순서 목록을 구성합니다.
    2. 제공자로부터 지원되는 값을 순서대로 가져옵니다. 구성된 목록에 없는 모든 값을 목록 끝에 추가합니다.

    이 단계를 마치면 모든 기능에 대해 순서가 정해진 속성 값 목록을 사용할 수 있습니다.

  4. 호환되는 변형 휠 중 적어도 하나에 존재하는 모든 변형 속성에 대해, 네임스페이스, 기능 이름, 기능 값 각각의 순서가 정해진 목록에서의 인덱스로 구성된 3-튜플을 정렬 키로 생성합니다.
  5. 호환되는 모든 변형 휠에 대해, 해당 휠의 속성을 정렬 키에 따라 오름차순으로 정렬합니다.
  6. 변형 휠의 순서를 정하려면 각 휠의 정렬된 속성을 비교합니다. 첫 번째 위치의 속성이 서로 다르면, 해당 속성의 3-튜플이 더 작은 변형이 앞에 정렬됩니다. 같으면 두 번째 위치의 속성을 비교하며, 우열이 가려지거나 한 휠의 속성 목록이 소진될 때까지 이와 같이 계속합니다. 후자의 경우에는 속성이 더 많은 변형이 앞에 정렬됩니다.

이 과정을 마치면 변형 휠은 가장 선호되는 것부터 가장 덜 선호되는 것 순으로 정렬됩니다. 널 변형은 자연스럽게 다른 모든 변형보다 뒤에 정렬되며, 비변형 휠은 반드시 널 변형보다 뒤에 정렬되어야 합니다. 그런 다음 동일한 변형 집합을 가진 여러 휠(및 여러 비변형 휠)은 반드시 플랫폼 호환성 태그에 따라 순서가 정해져야 합니다.

또는 변형 휠의 정렬 알고리즘을 다음 의사 코드로 설명할 수도 있습니다. 단순화를 위해 이 코드는 비변형 휠이나 태그를 고려하지 않습니다.

from typing import Self


def get_supported_feature_names(namespace: str) -> list[str]:
    """Get feature names from plugin's get_supported_configs()"""
    ...


def get_supported_feature_values(namespace: str, feature_name: str) -> list[str]:
    """Get feature values from plugin's get_supported_configs()"""
    ...


# default-priorities dict from variant metadata
default_priorities = {
    "namespace": [...],  # : list[str]
    "feature": {...},    # : dict[str, list[str]]
    "property": {...},   # : dict[str, dict[str, list[str]]]
}


# 1. Construct the ordered list of namespaces.
namespace_order = default_priorities["namespace"]
feature_order = {}
value_order = {}

for namespace in namespace_order:
    # 2. Construct the ordered lists of feature names.
    feature_order[namespace] = default_priorities["feature"].get(namespace, [])
    for feature_name in get_supported_feature_names(namespace):
        if feature_name not in feature_order[namespace]:
            feature_order[namespace].append(feature_name)

   value_order[namespace] = {}
   for feature_name in feature_order[namespace]:
        # 3. Construct the ordered lists of feature values.
        value_order[namespace][feature_name] = (
            default_priorities["property"].get(namespace, {}).get(feature_name, [])
        )
        for feature_value in get_supported_feature_values(namespace, feature_name):
            if feature_value not in value_order[namespace][feature_name]:
                value_order[namespace][feature_name].append(feature_value)


def property_key(prop: tuple[str, str, str]) -> tuple[int, int, int]:
    """Construct a sort key for variant property (akin to step 4.)"""
    namespace, feature_name, feature_value = prop
    return (
        namespace_order.index(namespace),
        feature_order[namespace].index(feature_name),
        value_order[namespace][feature_name].index(feature_value),
    )


class VariantWheel:
    """Example class exposing properties of a variant wheel"""
    properties: list[tuple[str, str, str]]

    def __lt__(self: Self, other: Self) -> bool:
        """Variant comparison function for sorting (akin to step 6.)"""
        for self_prop, other_prop in zip(self.properties, other.properties):
            if self_prop != other_prop:
                return property_key(self_prop) < property_key(other_prop)
        return len(self.properties) > len(other.properties)


# A list of variant wheels to sort.
wheels: list[VariantWheel] = [...]


for wheel in wheels:
    # 5. Order variant wheel properties by their sort keys.
    wheel.properties.sort(key=property_key)
# 6. Order variant wheels by comparing their sorted properties
# (see VariantWheel.__lt__())
wheels.sort()

pylock.toml과의 통합

다음 섹션이 pylock.toml Specification에 추가됩니다:

.. _pylock-packages-variants-json:

``[packages.variants-json]``
----------------------------

- **Type**: table
- **Required?**: no; requires that :ref:`pylock-packages-wheels` is used,
 mutually-exclusive with :ref:`pylock-packages-vcs`,
 :ref:`pylock-packages-directory`, and :ref:`pylock-packages-archive`.
- **Inspiration**: uv_
- The URL or path to the ``variants.json`` file.
- Only used if the project uses :ref:`wheel variants <wheel-variants>`.

.. _pylock-packages-variants-json-url:

``packages.variants-json.url``
''''''''''''''''''''''''''''''

See :ref:`pylock-packages-archive-url`.

.. _pylock-packages-variants-json-path:

``packages.variants-json.path``
'''''''''''''''''''''''''''''''

See :ref:`pylock-packages-archive-path`.

.. _pylock-packages-variants-json-hashes:

``packages.variants-json.hashes``
'''''''''''''''''''''''''''''''''

See :ref:`pylock-packages-archive-hashes`.

[packages.variants-json] 섹션이 있으면, 설치 프로그램은 변형을 해석하여 최적의 wheel 파일을 선택해야 합니다(SHOULD).

제공자 플러그인 API

상위 수준 설계

모든 제공자 플러그인은 반드시 단일 네임스페이스 안에서 동작해야 합니다. 이 네임스페이스는 플러그인과 관련된 모든 작업에서 고유 키로 사용됩니다. 플러그인이 정의하는 모든 속성은 해당 플러그인의 네임스페이스에 묶이며, 플러그인은 그 네임스페이스 안에서 유효한 모든 기능 이름과 값을 정의합니다.

제공자 플러그인 작성자는 자신이 대표하는 프로젝트와 명확하게 연관 지을 수 있는 네임스페이스를 선택해야 하며, 다른 프로젝트를 가리키거나 향후 이름 충돌로 이어질 수 있는 일반적인 용어로 된 네임스페이스는 피해야 합니다.

특정 패키지 버전에 대해 단일 색인에 게시된 모든 변형은 주어진 네임스페이스에 대해 반드시 동일한 제공자를 사용해야 합니다. 같은 릴리스 버전에서 같은 네임스페이스에 대해 둘 이상의 플러그인을 로드하려고 시도하면 반드시 치명적 오류가 발생해야 합니다. 같은 네임스페이스에 대한 여러 플러그인이 서로 다른 패키지나 릴리스 버전에 걸쳐 존재할 수는 있지만(예를 들어 유지 보수되지 않아 플러그인이 포크된 경우), 단일 릴리스 버전 안에서는 서로 배타적입니다.

플러그인을 더 쉽게 찾고 설치할 수 있도록, 플러그인은 해당 플러그인을 사용하는 패키지와 같은 색인에 게시되어야 합니다. 특히 PyPI에 게시되는 패키지는 다른 색인에서 설치해야 하는 플러그인에 의존해서는 안 됩니다.

이 PEP의 일부로 예약된 네임스페이스를 제외하고, 플러그인에 대해서는 설치 가능한 Python 패키지를 반드시 제공해야 합니다(MUST). 그러나 Providers 절에서 언급했듯이, 이러한 플러그인은 이를 필요로 하는 도구가 다시 구현할 수도 있습니다. 후자의 경우, 그 결과로 만들어진 재구현은 이 절에서 정의한 API를 따를 필요가 없습니다.

플러그인 패키지는 격리된 환경에서 실행될 수 있습니다. 플러그인 패키지는 설치된 패키지에 근거하여 결정을 내려서는 안 됩니다(MUST NOT).

Python 패키지로 구현된 플러그인은 지정된 API 엔드포인트에서 두 종류의 객체를 노출합니다:

  1. 다음을 통해 접근하면 특정 값을 반환하는 속성:
    {API endpoint}.{attribute name}
    
  2. 다음을 통해 호출되는 호출 가능 객체:
    {API endpoint}.{callable name}({arguments}...)
    

이들은 모듈로 구현하거나, 클래스 메서드 또는 정적 메서드를 가진 클래스로 구현할 수 있습니다. 구체적인 내용은 이어지는 절들에서 설명합니다.

API 엔드포인트

플러그인 코드의 위치를 “API 엔드포인트”라고 하며, 이는 Entry points specification에 따른 객체 참조 표기법을 사용하여 표현합니다:

{import_path}(:{object_path})?

API 엔드포인트 명세는 다음 Python 의사 코드와 동등합니다:

import {import_path}

if "{object_path}":
    plugin = {import_path}.{object_path}
else:
    plugin = {import_path}

API 엔드포인트는 두 가지 맥락에서 사용됩니다:

  1. variant 메타데이터의 plugin-api 키에 명시적으로 지정하거나 requires 키의 패키지 이름에서 추론하는 방식. 이는 휠을 빌드하고 설치할 때 플러그인을 사용하는 주된 방법입니다.
  2. variant_plugins 그룹에 설치된 진입점의 값으로 지정합니다. 해당 진입점의 이름은 중요하지 않습니다. 이는 선택 사항(OPTIONAL)이지만 권장됩니다(RECOMMENDED). 변형 관련 유틸리티가 사용자 환경에 설치된 변형 플러그인을 발견할 수 있게 해 주기 때문입니다.

변형 기능 설정 클래스

변형 기능 설정 클래스는 플러그인 API 함수의 반환값으로 사용됩니다. 이 클래스는 하나의 변형 기능을 가능한 값들의 목록과 함께 정의합니다. 맥락에 따라 값들의 순서가 중요할 수 있습니다(MAY). 이 클래스는 다음 프로토콜을 사용하여 정의됩니다:

from abc import abstractmethod
from typing import Protocol


class VariantFeatureConfigType(Protocol):
    @property
    @abstractmethod
    def name(self) -> str:
        """Feature name"""
        raise NotImplementedError

    @property
    @abstractmethod
    def multi_value(self) -> bool:
        """Does this property allow multiple values per variant?"""
        raise NotImplementedError

    @property
    @abstractmethod
    def values(self) -> list[str]:
        """List of values, possibly ordered from most preferred to least"""
        raise NotImplementedError

“변형 기능 설정”은 다음 프로퍼티 또는 속성을 반드시 제공해야 합니다(MUST):

  • name: str은 기능 이름을 지정합니다.
  • multi_value: bool은 하나의 변형 휠 안에서 이 기능이 여러 개의 대응 값을 가질 수 있는지를 지정합니다. 이 값이 False이면, 이 기능에 여러 값을 지정하는 것은 오류입니다.
  • values: list[str]은 기능 값들을 지정합니다. 순서가 중요한 맥락에서는 값들을 가장 선호하는 것부터 가장 덜 선호하는 것 순으로 반드시 정렬해야 합니다(MUST).

모든 기능은 플러그인의 네임스페이스 안에 있는 것으로 해석됩니다.

플러그인 인터페이스

플러그인 인터페이스는 반드시 다음 프로토콜을 따라야 합니다:

from abc import abstractmethod
from typing import Protocol


class PluginType(Protocol):
    # Note: properties are used here for docstring purposes, these
    # must be actually implemented as attributes.

    @property
    @abstractmethod
    def namespace(self) -> str:
        """The provider namespace"""
        raise NotImplementedError

    @property
    def is_aot_plugin(self) -> bool:
        """Is this plugin valid for `install-time = false`?"""
        return False

    @classmethod
    @abstractmethod
    def get_all_configs(cls) -> list[VariantFeatureConfigType]:
        """Get all valid configs for the plugin"""
        raise NotImplementedError

    @classmethod
    @abstractmethod
    def get_supported_configs(cls) -> list[VariantFeatureConfigType]:
        """Get supported configs for the current system"""
        raise NotImplementedError

플러그인 인터페이스는 다음 속성을 반드시 정의해야 합니다(MUST):

  • namespace: str는 플러그인의 네임스페이스를 지정합니다.
  • is_aot_plugin: bool은 플러그인이 유효한 AoT 플러그인인지 여부를 나타냅니다. 그런 경우 get_supported_configs()는 반드시 항상 get_all_configs()와 같은 값을 반환해야 하며(순서는 무관), 이 값은 반드시 플러그인이 실행되는 플랫폼과 무관한 고정된 목록이어야 합니다. 지정하지 않으면 기본값은 False입니다.

플러그인 인터페이스는 반드시 다음 함수를 제공해야 합니다:

  • get_all_config() -> list[VariantFeatureConfigType]은 플러그인의 네임스페이스 안에 있는 모든 유효한 변형 기능과 그 기능에 허용되는 모든 값을 기술하는 “변형 기능 구성”의 목록을 반환합니다. 여기서 목록의 순서는 중요하지 않습니다. 특정 플러그인 버전은 런타임 조건과 관계없이 반드시 항상 같은 값을 반환해야 합니다(순서는 무관).
  • get_supported_configs() -> list[VariantFeatureConfigType]은 플러그인의 네임스페이스 안에 있는 변형 기능 중 이 특정 시스템과 호환되는 기능과 그 기능에서 지원되는 값을 기술하는 “변형 기능 구성”의 목록을 반환합니다. 변형 기능 목록과 값 목록은 Variant ordering에 영향을 주므로, 반드시 가장 선호하는 것부터 가장 덜 선호하는 것 순으로 정렬되어야 합니다.

get_supported_configs()가 반환하는 값은 반드시 get_all_configs()가 반환하는 기능 이름과 값의 부분집합이어야 합니다(순서는 무관).

get_supported_configs()가 반환하는 값은 한 번의 설치 세션 동안 여러 패키지에 걸쳐 캐시될 수 있습니다.

구현 예

from dataclasses import dataclass


@dataclass
class VariantFeatureConfig:
    name: str
    values: list[str]
    multi_value: bool


# internal -- provided for illustrative purpose
_MAX_VERSION = 4
_ALL_GPUS = ["narf", "poit", "zort"]


def _get_current_version() -> int:
    """Returns currently installed runtime version"""
    ...  # implementation not provided


def _is_gpu_available(codename: str) -> bool:
    """Is specified GPU installed?"""
    ...  # implementation not provided


class MyPlugin:
    namespace = "example"

    # optional, defaults to False
    is_aot_plugin = False

    # all valid properties
    @staticmethod
    def get_all_configs() -> list[VariantFeatureConfig]:
        return [
            VariantFeatureConfig(
                # example :: gpu -- multi-valued, since the package
                # can target multiple GPUs
                name="gpu",
                # [narf, poit, zort]
                values=_ALL_GPUS,
                multi_value=True,
            ),
            VariantFeatureConfig(
                # example :: min_version -- single-valued, since
                # there is always one minimum
                name="min_version",
                # [1, 2, 3, 4] (order doesn't matter)
                values=[str(x) for x in range(1, _MAX_VERSION + 1)],
                multi_value=False,
            ),
        ]

    # properties compatible with the system
    @staticmethod
    def get_supported_configs() -> list[VariantFeatureConfig]:
        current_version = _get_current_version()
        if current_version is None:
            # no runtime found, system not supported at all
            return []

        return [
            VariantFeatureConfig(
                name="min_version",
                # [current, current - 1, ..., 1]
                values=[str(x) for x in range(current_version, 0, -1)],
                multi_value=False,
            ),
            VariantFeatureConfig(
                name="gpu",
                # this may be empty if no GPUs are supported --
                # 'example :: gpu feature' is not supported then;
                # but wheels with no GPU-specific code and only
                # 'example :: min_version' could still be installed
                values=[x for x in _ALL_GPUS if _is_gpu_available(x)],
                multi_value=True,
            ),
        ]

향후 확장

이 명세의 향후 버전과 서드파티 확장은 플러그인 인스턴스에 추가 프로퍼티와 메서드를 도입할 수 있습니다. 구현은 추가 속성을 무시해야 합니다(SHOULD).

최상의 호환성을 위해, 향후 확장과의 우발적인 충돌을 피할 수 있도록 모든 비공개 속성에는 밑줄(_) 문자 접두사를 붙여야 합니다(SHOULD).

빌드 백엔드

빌드 백엔드는 프런트엔드가 변형 휠을 지원하는지 여부를 판단할 수 없으므로, PEP 517 및 PEP 660 훅은 반드시 기본적으로 비변형 휠을 빌드해야 합니다. 빌드 백엔드는 변형 빌드를 요청하는 방법을 제공할 수 있습니다. 이 명세는 특정한 구성을 정의하지 않습니다.

변형 휠을 빌드할 때 빌드 백엔드는 반드시 변형 메타데이터가 올바른지 검증해야 하며, 규격에 맞지 않는 variant.json 파일이 포함된 휠을 절대 생성해서는 안 됩니다. 또한 빌드 백엔드는 사용자가 요청한 변형 속성이 유효한지 판단하기 위해 프로바이더에 질의해야 합니다(SHOULD). 다만 이 검증을 건너뛰도록 허용하여 알 수 없는 속성을 가질 수 있는 변형 휠을 내보낼 수도 있습니다(MAY).

변형 환경 마커

의존성 명세에 네 가지 새로운 환경 마커가 도입됩니다:

  1. variant_namespaces는 휠 변형이 빌드된 대상인 모든 변형 속성의 네임스페이스 집합에 해당합니다.
  2. variant_features는 휠 변형이 빌드된 대상인 모든 변형 속성의 namespace :: feature 쌍 집합에 해당합니다.
  3. variant_properties는 휠 변형이 빌드된 대상인 모든 변형 속성의 namespace :: feature :: value 튜플 집합에 해당합니다.
  4. 휠을 빌드할 때 사용한 정확한 변형 레이블에 해당하는 variant_label입니다. 비변형 휠의 경우 빈 문자열입니다.

문자열 집합으로 평가되는 마커는 반드시 in 또는 not in 연산자로 매칭해야 합니다. 예:

# satisfied by any "foo :: * :: *" property
dep1; "foo" in variant_namespaces
# satisfied by any "foo :: bar :: *" property
dep2; "foo :: bar" in variant_features
# satisfied only by "foo :: bar :: baz" property
dep3; "foo :: bar :: baz" in variant_properties

variant_label 마커는 일반 문자열입니다:

# satisfied by the variant "foobar"
dep4; variant_label == "foobar"
# satisfied by any wheel other other than the null variant
# (including the non-variant wheel)
dep5; variant_label != "null"
# satisfied by the non-variant wheel
dep6; variant_label == ""

구현은 기능과 속성을 매칭할 때 공백 차이를 반드시 무시해야 합니다.

변형 마커 표현식은 제공자 플러그인의 현재 출력이 아니라 설치 중인 휠에 저장된 변형 속성을 기준으로 반드시 평가해야 합니다. 비변형 휠이 선택되었거나 빌드된 경우, 모든 변형 마커는 False로 평가됩니다.

ABI 의존성 변형 네임스페이스(선택 사항)

이 절에서는 휠 변형 명세에 대한 선택적(OPTIONAL) 확장을 설명합니다. 이 기능을 구현하기로 한 도구는 반드시 이 명세를 따라야 합니다. 이 기능을 구현하지 않는 도구는 이 기능을 사용하는 변형을 반드시 호환되지 않는 것으로 취급해야 하며, 그러한 휠을 건너뛸 때 사용자에게 알리는 것이 좋습니다(SHOULD).

변형 네임스페이스 abi_dependency는 한 패키지의 같은 버전에 대한 서로 다른 빌드가 어떤 의존성의 서로 다른 버전 또는 버전 범위와 호환된다는 것을 표현하기 위해 예약되어 있습니다. 이 네임스페이스는 어떤 변형 제공자 플러그인도 사용해서는 안 되고(MUST NOT), providers 메타데이터에 나열되어서도 안 되며(MUST NOT), 빌드된 휠의 변형 속성에만 나타날 수 있습니다.

이 네임스페이스 안에서는 0개 이상의 속성을 사용하여 호환되는 의존성 버전을 표현할 수 있습니다. 각 속성에서 기능 이름은 반드시 의존성의 정규화된 이름이어야 하고, 값은 반드시 Version specifiers 명세에 정의된 공개 버전 식별자의 유효한 릴리스 세그먼트여야 합니다. 값은 반드시 최대 세 개의 버전 구성 요소를 포함해야 하며, 이 값은 =={value}.* 지정자와 같은 방식으로 설치된 버전과 매칭됩니다. 특히, 끝에 붙은 0은 구성 요소가 더 적은 버전과도 매칭됩니다(예: 2.0은 릴리스 2와는 매칭되지만 2.1과는 매칭되지 않습니다). 이는 또한 속성 값이 PEP 440 버전과 다른 의미를 가진다는 것을 뜻하며, 특히 2, 2.0, 2.0.0은 서로 다른 범위를 나타냅니다.

0이 아닌 에포크를 가진 버전은 지원하지 않습니다.

변형 속성 매칭 규칙
abi_dependency :: torch :: 2 torch==2.*
abi_dependency :: torch :: 2.9 torch==2.9.*
abi_dependency :: torch :: 2.8.0 torch==2.8.0.*

같은 기능 이름을 가진 여러 변형 속성을 사용하여, 제공 패키지의 여러 버전과 호환되는 휠을 나타낼 수 있습니다. 예:

abi_dependency :: torch :: 2.8.0
abi_dependency :: torch :: 2.9.0

이는 해당 휠이 PyTorch 2.8.0과 2.9.0 모두와 호환된다는 뜻입니다.

교육 방법

Python 패키지 사용자

Python 패키지 사용자를 위한 주된 정보원은 설치 프로그램 문서여야 하며, 명령줄 인터페이스가 제공하는 유용한 안내 메시지와 튜토리얼이 이를 보완해야 합니다. 특별한 요구 사항이 없는 사용자라면 변형(variant)에 대해 따로 알 필요가 없어야 합니다. 고급 사용자에게는 특히 다음 사항에 대한 문서가 필요합니다(해당 설치 프로그램이 이러한 기능을 구현한다는 전제하에):

  • 신뢰할 수 없는 제공자 플러그인을 활성화하는 방법과 그에 따른 보안상 영향
  • 제공자 사용을 제어하는 방법, 특히 선택적 제공자를 활성화하거나, 원치 않는 플러그인을 비활성화하거나, 변형 사용 자체를 비활성화하는 방법
  • 변형을 명시적으로 선택하는 방법과 변형 선택 과정을 제어하는 방법
  • 원격 배포 대상에 맞게 변형 선택을 구성하는 방법(예: 대상에서 생성한 정적 파일 사용)

설치 프로그램 문서는 Python 프로젝트별 문서, 특히 해당 프로젝트의 설치 안내로 보완될 수도 있습니다.

일부 패키지 관리자는 변형 휠을 지원하고 일부는 지원하지 않는 전환 기간에는, 특정 기능을 특정 도구에서만 사용할 수 있다는 점을 사용자가 알고 있어야 합니다.

Python 패키지 유지 관리자

Python 패키지 유지 관리자에게 가장 중요한 정보 출처는 빌드 백엔드 문서여야 하며, 튜토리얼이 이를 보완해야 합니다. 문서에서는 다음 내용을 안내해야 합니다:

  • pyproject.toml에서 변형 지원을 선언하는 방법
  • 변형 환경 마커를 사용하여 의존성을 지정하는 방법
  • 변형 휠을 빌드하는 방법
  • 로컬 색인에 변형 휠을 게시하고 *-variants.json 파일을 생성하는 방법

유지 관리자는 제공자 플러그인 문서도 꼼꼼히 읽어야 합니다. 또한 유지 관리자는 흔히 사용되는 설치 프로그램이 어떤 프로바이더 플러그인을 신뢰하는 것으로 간주하는지 알고 있어야 하며, 신뢰되지 않은 플러그인을 사용할 때의 영향도 알고 있어야 합니다. 이러한 자료는 변형 휠 게시 방법을 설명하는 일반 문서와 구체적인 사용 사례 예시로 보완될 수도 있습니다.

전환 기간에는 하위 호환성을 위해 비변형 휠도 계속 게시해야 한다는 점을 패키지 유지 관리자가 알고 있어야 합니다.

하위 호환성

기존 설치 프로그램은 변형 휠을 실수로 설치해서는 안 됩니다(MUST NOT). 변형 휠은 휠이 사용자의 시스템과 호환되는지 판단하기 위한 추가 로직을 필요로 하기 때문입니다. 이는 파일 이름 끝에 -{variant label} 구성 요소를 추가하여 휠 파일 이름을 확장함으로써 이루어지며, 그 결과 일반적인 설치 프로그램 구현은 사실상 변형 휠을 거부하게 됩니다. 하위 호환성을 위해 변형 휠과 함께 비변형 휠을 게시할 수 있습니다. 이 휠은 호환되지 않는 설치 프로그램이 지원하는 유일한 휠이 되며, 변형을 지원하는 설치 프로그램에서는 우선순위가 가장 낮은 휠이 됩니다.

이 명시적인 비호환성을 제외하면, 이 명세는 바이너리 패키지 형식을 최소한으로, 기존 동작을 해치지 않는 방식으로만 변경합니다. Variant metadata는 .dist-info 디렉터리 안의 별도 파일에 저장되며, 변형과 무관한 도구도 이 파일을 보존해야 하므로 필요한 변경은 파일 이름 검증 알고리즘(있는 경우)을 갱신하는 것으로 한정됩니다.

새로운 Variant environment markers를 휠 의존성에 사용하면 해당 휠은 기존 도구와 호환되지 않습니다. 이는 환경 마커 설계에서 비롯되는 일반적인 문제이며, 휠 변형에만 해당하는 문제는 아닙니다. 빌드 시점에 환경 마커를 부분적으로 평가하고, 변형 휠 전용 마커나 의존성을 비변형 휠에서 제거하면 이 문제를 우회할 수 있습니다.

Build backends는 기존 프런트엔드와의 하위 호환성을 유지하기 위해 비변형 휠을 생성합니다. 변형 휠은 사용자가 명시적으로 요청한 경우에만 출력할 수 있습니다.

공유 메타데이터를 담는 별도의 *-variants.json 파일을 사용하면 변형 메타데이터를 특별히 지원하지 않는 색인에서도 변형 휠을 사용할 수 있습니다. 다만 색인은 확장된 파일 이름 문법을 사용하는 휠과 JSON 파일의 배포를 허용해야 합니다(MUST).

참조 구현

variantlib 프로젝트에는 이 PEP에서 도입하는 모든 프로토콜과 알고리즘의 참조 구현이 들어 있습니다. 휠을 변환하고 *-variants.json 색인을 생성하며 플러그인을 조회하는 명령줄 도구도 함께 들어 있습니다.

변형 휠을 설치하는 클라이언트는 uv 브랜치에 구현되어 있습니다.

Wheel Variants 모노레포에는 제공자 플러그인의 예시 구현이 들어 있습니다. 변형 휠 빌드를 지원하도록 수정한 빌드 백엔드와 변형 휠의 사용 방식을 보여 주도록 수정한 일부 Python 패키지도 함께 들어 있습니다.

기각된 아이디어

변형을 전면적으로 또는 과도기적으로 옵트인으로 하는 방안

보안 문제를 논의하는 과정에서 변형 제공자의 사용을 전면적인 옵트인으로 하자는 제안이 나왔습니다. 영구적으로 그렇게 하자는 제안도 있었고, 추가 테스트를 쉽게 하도록 적어도 초기에는 그렇게 하자는 제안도 있었습니다. 이러한 접근 방식을 쓰면 제공자 코드를 검증할 책임을 누가 지는지가 달라질 수 있습니다. 그에 따라 유지 관리 노력이나 신뢰해야 하는 패키지 및 유지 관리자의 수도 달라질 수 있습니다. 하지만 장기적인 해결책으로는 적합하지 않습니다.

무엇보다도 옵트인 메커니즘을 쓰면 별도 설정이 없는 기본 상태에서 사용자 경험이 훨씬 나빠집니다. 변형을 사용하는 패키지는 기본적으로 최적이 아니거나 아예 작동하지 않는 변형이 설치되는 경험을 주게 됩니다. 변형을 사용하는 패키지는 직접 설치될 뿐만 아니라 다른 패키지의 의존성으로도 설치될 수 있다는 점에 유의해야 합니다. 따라서 최적의 사용자 경험을 위해서는 변형이 활성화된 모든 패키지, 또는 변형이 활성화된 의존성을 가진 모든 패키지가 해당 프로바이더 플러그인을 활성화하기 위한 설치 프로그램별 적절한 메커니즘을 문서화해야 할 것입니다.

이러한 경험이 널리 퍼지면 두 가지 중대한 결과가 생길 수 있습니다. 사용자가 별도 설정 없이 변형을 쓸 수 없으면 패키지 유지 관리자는 변형을 쓰지 않고 이전의 우회 방법을 계속 쓰게 될 수 있습니다. 더 나쁜 것은, 결국 사용자가 모든 변형 프로바이더를 무조건 활성화하도록 설치 프로그램을 안이하게 설정하게 될 수도 있다는 점입니다. 이렇게 되면 많은 사용자에게 프로바이더 사용이 사실상 옵트아웃 방식이 되어, Security implications 절에서 설명한 온갖 종류의 공급망 공격이 가능해집니다.

저자들은 사용자 경험을 심각하게 해치는 대가로 보안을 얻을 수는 없다는 점을 강조하고자 합니다. 그 대신 이 PEP는 중앙에서 유지 관리하고 검증하는 신뢰할 수 있는 제공자 풀을 도입하여 균형을 맞추려고 합니다.

제공자 플러그인이 없는 접근 방식

추가 변형 속성은 기술적으로 제공자 플러그인 없이도 지원할 수 있습니다. 현재 휠 태그를 구현하는 방식과 마찬가지로, 사용 가능한 속성과 그 탐지 방법을 명세의 일부로 정의하면 됩니다. 그러나 기존의 휠 태그 로직만으로도 지원되는 태그를 생성하는 로직을 유지 관리해야 하는 패키징 도구에 이미 상당한 복잡성이 생깁니다. 이 부담은 Python 인터프리터 자체가 제공하는 데이터로 일부 덜 수 있을 뿐입니다.

새로운 축이 추가될 때마다 패키지 관리자 유지 관리자에게 더 많은 부담이 가해지며, 이들은 속성 호환성을 판단하는 알고리즘을 유지 관리해야 합니다. 이 알고리즘은 상당히 복잡해질 수 있습니다. 다양한 플랫폼과 하드웨어 버전을 고려해야 할 수도 있고, 플랫폼 태그용 알고리즘보다 더 자주 업데이트해야 할 수도 있습니다. 이는 또한 새로운 축을 추가하는 데 대한 장벽을 크게 높이고, 그에 따라 서로 다른 설치 프로그램 간에 기능 동등성이 갖춰지지 않을 위험도 크게 높입니다. 새로운 축이 추가될 때마다 추가적인 유지 관리 비용이 발생하기 때문입니다.

이에 비해 플러그인 설계는 본질적으로 변형 속성을 민주화합니다. 제공자 플러그인은 필요한 지식과 하드웨어를 갖춘 사람들이 독립적으로 유지 관리할 수 있습니다. 제공자 플러그인은 패키지 관리자와 관계없이 필요한 만큼 자주 업데이트할 수 있습니다. 특정 프로바이더를 사용할지에 대한 결정은 전적으로 그 프로바이더를 필요로 하는 패키지의 유지 관리자에게 달려 있습니다. 다만 유지 관리자는 일반적인 설치 프로그램이 검증하지 않은 플러그인을 사용하면 사용자에게 불편을 끼치게 된다는 점을 고려해야 합니다.

변형을 별도의 패키지로 해석하는 방안

대안으로 나온 제안은 패키지의 변형들을 색인에 별도의 프로젝트로 게시하는 방안이었습니다. 이때 주 패키지는 메타데이터를 통해 다른 변형으로 안내하는 “리졸버” 역할을 합니다. 예를 들어 torch 패키지는 torch-cpu, torch-cu129 등의 하위 패키지를 사용할 조건을 지정할 수 있습니다.

이러한 접근 방식은 기존 도구와의 하위 호환성이 더 나을 수도 있습니다. 변경 사항은 설치 프로그램에 한정되며, 변형 도입 이전의 설치 프로그램에서도 사용자가 특정 변형의 설치를 명시적으로 요청할 수 있습니다. 그러나 이 방식은 여러 수준에서 문제를 일으킵니다.

변형마다 새 프로젝트를 만들어야 하므로 torch-cu123과 같은 오래된 프로젝트가 난립하게 됩니다. resolver 패키지를 사용하면 최신 변형만 사용되도록 보장할 수 있지만, 패키지를 수동으로 설치하는 사용자나 패키지 간 의존성이 실수로 오래된 변형 프로젝트에 고정될 수 있으며, 심지어 이름 선점(name squatting)의 피해를 입을 수도 있습니다. 이에 비해 변형 휠 제안은 변형의 범위를 각 프로젝트 버전으로 한정하며, 프로젝트 유지 관리자만 변형을 업로드할 수 있도록 보장합니다.

게다가 이 방식은 의존성 해석기와 패키지 메타데이터 형식에 상당한 변경을 요구합니다. 특히 의존성 해석기는 해석을 수행하기 전에 모든 “resolver” 패키지를 조회해야 합니다. 범용 해석(universal resolution)을 수행할 때 이러한 변형을 어떻게 고려해야 할지 불분명합니다. torch 의존성이 사실상 torch-cu129에 의해 충족될 수 있으므로, 의존성과 설치된 패키지 사이의 일대일 매핑이 사라지게 됩니다.

부록

참고 문헌

감사의 글

이 작업은 Python 패키징 커뮤니티의 많은 분들의 기여와 피드백 없이는 불가능했을 것입니다. 특히 이 PEP를 구체화하는 데 도움을 주신 다음 분들께 감사드립니다(알파벳순):

Alban Desmaison, Bradley Dice, Chris Gottbrath, Dmitry Rogozhkin, Emma Smith, Geoffrey Thomas, Henry Schreiner, Jeff Daily, Jeremy Tanner, Jithun Nair, Keith Kraus, Leo Fang, Mike McCarty, Nikita Shulga, Paul Ganssle, Philip Hyunsu Cho, Robert Maynard, Vyas Ramasubramani, 그리고 Zanie Blue.

변경 이력

  • 2026년 3월 18일
    • 패키징 도구 유형별로 권장하는 구현 로직의 상위 수준 개요와 설치 프로그램 동작에 대한 다이어그램을 추가했습니다.
    • 설치 프로그램에 프로바이더를 벤더링하는 방식의 비중을 낮췄습니다. 이 방식은 구현 선택지로서 여전히 허용되지만, 더 이상 보안 향상을 위한 권장 해결책으로 제시하지 않습니다.
    • 중앙에서 관리하는 허용 목록을 제공자를 기본으로 활성화하기 위한 주된 해결책으로 삼았습니다. 이러한 허용 목록은 전담 팀이 관리하며, 이 팀은 PEP 작성자 중 일부로 시작합니다.
    • 제공자 조회 대신 사용자가 제공한 호환성 정보를 사용할 수 있도록 명세를 명확히 했습니다.
    • 옵트인 메커니즘에 관한 불필요한 UX 제안을 제거했습니다.
    • 색인 수준 변형 메타데이터 파일은 색인 자체가 생성할 수도 있고, 색인이 이를 지원하지 않는 경우 패키지 유지 관리자가 업로드할 수도 있음을 명확히 했습니다.
    • 색인 수준 변형 메타데이터 파일이 게시된 후에는 새로운 변형을 도입하지 않도록 하는 권고를 추가했습니다.
    • 변형 제공자 패키지를 격리된 환경에서 실행하도록 하는 명시적 권고를 추가했습니다.
    • get_supported_configs()가 반환하는 값은 캐시될 수 있음을 명확히 했습니다.
    • 전면적인 옵트인 방식의 위험성을 강조했습니다.
    • GROMACS 도표가 다크 테마를 따르도록 수정했습니다.