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

Python 개선 제안 한국어 번역

PEP 517 – 소스 트리를 위한 빌드 시스템 독립적 형식

Author:
Nathaniel J. Smith <njs at pobox.com>, Thomas Kluyver <thomas at kluyver.me.uk>
BDFL-Delegate:
Alyssa Coghlan <ncoghlan at gmail.com>
Discussions-To:
Distutils-SIG list
Status:
Final
Type:
Standards Track
Topic:
Packaging
Created:
30-Sep-2015
Post-History:
01-Oct-2015, 25-Oct-2015, 19-May-2017, 11-Sep-2017
Resolution:
Distutils-SIG message

Table of Contents

번역·라이선스 안내

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

초록

distutils / setuptools 덕분에 많은 발전을 이루었지만, 여기에는 세 가지 심각한 문제가 있습니다. (a) 쓸 만한 빌드 시점 의존성 선언, 자동 구성 같은 중요한 기능이 없고, DRY 원칙을 준수하는 버전 번호 관리와 같은 기본적인 사용 편의 기능조차 없습니다. (b) 이를 확장하기가 어려워서, 위 문제들에 대한 다양한 해결책이 존재하기는 하지만 대개 별나고 취약하며 유지 보수 비용이 많이 듭니다. 그런데도 (c) 다른 것을 사용하기는 매우 어렵습니다. 사용자와 pip 같은 설치 도구가 모두 기대하는 패키지 설치용 표준 인터페이스를 distutils/setuptools가 제공하기 때문입니다.

이전의 시도들(예: distutils2나 setuptools 자체)은 문제 (a) 및/또는 (b)를 해결하려고 했습니다. 이 제안은 문제 (c)를 해결하는 것을 목표로 합니다.

이 PEP의 목표는 distutils-sig가 Python 빌드 시스템의 문지기 역할에서 벗어나게 하는 것입니다. distutils를 사용하고 싶다면 그것도 좋습니다. 다른 것을 사용하고 싶다면 표준화된 방법으로 쉽게 그렇게 할 수 있어야 합니다. distutils와 연동하기 어렵기 때문에 현재는 그러한 시스템이 많지 않지만, 우리가 염두에 두고 있는 것이 무엇인지 감을 잡으려면 flit이나 bento를 참고하십시오. 다행히도 이제 휠이 이 분야의 어려운 문제를 상당수 해결했으므로(예: 빌드 시스템이 가능한 모든 설치 구성까지 알아야 할 필요가 더 이상 없습니다), 빌드 시스템에 실제로 필요한 것은 사실상 표준을 준수하는 휠과 sdist를 만들어 낼 방법이 있다는 것뿐입니다.

따라서 우리는 pip 같은 설치 도구가 패키지 소스 트리 및 소스 배포판과 상호 작용할 수 있도록 새롭고 비교적 최소한의 인터페이스를 제안합니다.

용어와 목표

소스 트리는 VCS 체크아웃과 같은 것입니다. pip install some-directory/ 같은 사용법을 지원하려면 이 형식에서 설치하기 위한 표준 인터페이스가 필요합니다.

소스 배포판은 lxml-3.4.4.tar.gz처럼 어떤 소스 코드의 특정 릴리스를 나타내는 정적 스냅숏입니다. 소스 배포판은 여러 목적에 쓰입니다. 소스 배포판은 릴리스의 보관 기록이 되고, 여러 언어로 작성되었을 수 있는 대량의 코드 코퍼스를 수집하고 처리하려는 도구(예: 코드 검색)를 위해 아주 단순한 사실상의 표준을 제공하며, Debian/Fedora/Conda/… 같은 다운스트림 패키징 시스템의 입력으로 쓰이는 등의 역할을 합니다. Python 생태계에서 소스 배포판은 추가로 특히 중요한 역할을 맡습니다. pip 같은 패키징 도구가 소스 배포판을 사용해 바이너리 의존성을 충족할 수 있기 때문입니다. 예를 들어 bar에 대한 의존성을 선언하는 배포 foo.whl이 있다면, pip install bar 또는 pip install foo가 bar의 sdist를 자동으로 찾아 내려받고, 빌드하고, 그 결과로 만들어진 패키지를 설치하는 경우를 지원해야 합니다.

소스 배포판은 줄여서 sdist라고도 합니다.

빌드 프런트엔드는 사용자가 실행할 수 있는 도구로, 임의의 소스 트리나 소스 배포판을 받아 그로부터 wheel을 빌드합니다. 실제 빌드는 각 소스 트리의 빌드 백엔드가 수행합니다. pip wheel some-directory/ 같은 명령에서 pip는 빌드 프런트엔드 역할을 합니다.

통합 프런트엔드는 사용자가 실행할 수 있는 도구로, 패키지 요구 사항 집합(예: requirements.txt 파일)을 받아 해당 요구 사항을 충족하도록 작업 환경을 갱신하려고 시도합니다. 이를 위해 휠과 sdist를 조합하여 찾고, 빌드하고, 설치해야 할 수 있습니다. pip install lxml==2.4.0 같은 명령에서 pip는 통합 프런트엔드 역할을 합니다.

소스 트리

setup.py를 사용하는 기존의 레거시 소스 트리 형식이 있습니다. 이 형식을 더 명세하려고 하지는 않습니다. 그 사실상의 명세는 distutils, setuptools, pip 및 기타 도구의 소스 코드와 문서에 담겨 있습니다. 이를 setup.py 스타일이라고 부르겠습니다.

여기서는 PEP 518에서 정의한 pyproject.toml 파일을 중심으로 하는 새로운 스타일의 소스 트리를 정의하며, 해당 파일의 [build-system] 테이블에 build-backend라는 키 하나를 추가하여 확장합니다. 다음은 그 모습을 보여 주는 예입니다:

[build-system]
# Defined by PEP 518:
requires = ["flit"]
# Defined by this PEP:
build-backend = "flit.api:main"

build-backend는 빌드를 수행하는 데 사용할 Python 객체의 이름을 나타내는 문자열입니다(자세한 내용은 아래를 참조하십시오). 이는 setuptools 진입점과 같은 module:object 문법을 따라 형식화됩니다. 예를 들어 위 예에서처럼 문자열이 "flit.api:main"이라면, 이 객체는 다음과 동등한 코드를 실행하여 조회됩니다:

import flit.api
backend = flit.api.main

:object 부분을 생략하는 것도 허용됩니다. 예를 들면 다음과 같습니다.

build-backend = "flit.api"

이는 다음과 같이 동작합니다.:

import flit.api
backend = flit.api

형식적으로, 이 문자열은 다음 문법을 만족해야 합니다.:

identifier = (letter | '_') (letter | '_' | digit)*
module_path = identifier ('.' identifier)*
object_path = identifier ('.' identifier)*
entry_point = module_path (':' object_path)?

그리고 module_path를 임포트한 다음 module_path.object_path를 조회합니다(object_path가 없으면 module_path만 조회합니다).

모듈 경로를 임포트할 때 소스 트리를 포함하는 디렉터리에서는 찾지 않습니다. 단, 그 디렉터리가 어차피 sys.path에 들어 있는 경우(예: PYTHONPATH에 지정되어 있는 경우)는 예외입니다. Python은 일부 상황에서 작업 디렉터리를 sys.path에 자동으로 추가하지만, 백엔드를 찾아내는 코드는 이 동작의 영향을 받아서는 안 됩니다.

pyproject.toml 파일이 없거나 build-backend 키가 없으면 해당 소스 트리는 이 명세를 사용하지 않는 것이며, 도구는 setup.py를 실행하는 기존 동작으로 되돌아가야 합니다(직접 실행하거나 setuptools.build_meta:__legacy__ 백엔드를 암묵적으로 호출하는 방식으로).

build-backend 키가 있으면 이 키가 우선하며, 소스 트리는 지정된 백엔드의 형식과 관례를 따릅니다(따라서 백엔드가 요구하지 않는 한 setup.py는 필요하지 않습니다). 그래도 프로젝트는 이 명세를 사용하지 않는 도구와의 호환성을 위해 setup.py를 포함하려 할 수 있습니다.

이 PEP는 pyproject.toml에서 사용할 backend-path 키도 정의합니다. 아래의 “트리 내 빌드 백엔드” 절을 참고하십시오. 이 키는 다음과 같이 사용됩니다.:

[build-system]
# Defined by PEP 518:
requires = ["flit"]
# Defined by this PEP:
build-backend = "local_backend"
backend-path = ["backend"]

빌드 요구 사항

이 PEP는 pyproject.toml의 “빌드 요구 사항” 섹션에 여러 가지 추가 요구 사항을 둡니다. 이는 프로젝트가 빌드 요구 사항으로 인해 충족할 수 없는 조건을 만들지 않도록 보장하기 위한 것입니다.

  • 프로젝트 빌드 요구 사항은 요구 사항의 방향 그래프를 정의합니다(프로젝트 A는 빌드하는 데 B가 필요하고, B는 C와 D가 필요한 식입니다). 이 그래프는 순환을 포함해서는 안 됩니다(MUST NOT). (예를 들어 프로젝트 간 조율이 부족하여) 순환이 존재한다면, 프런트엔드는 해당 프로젝트의 빌드를 거부할 수 있습니다(MAY).
  • 빌드 요구 사항을 휠로 사용할 수 있는 경우, 프런트엔드는 깊게 중첩된 빌드를 피하기 위해 가능한 한 휠을 사용해야 합니다(SHOULD). 그러나 프런트엔드는 빌드 요구 사항을 찾을 때 휠을 고려하지 않는 모드를 가질 수 있으므로(MAY), 프로젝트는 휠을 배포하는 것만으로 요구 사항 순환을 끊기에 충분하다고 가정해서는 안 됩니다(MUST NOT).
  • 프런트엔드는 요구 사항 순환을 명시적으로 검사하고, 순환이 발견되면 유익한 메시지와 함께 빌드를 종료해야 합니다(SHOULD).

특히 요구 사항 순환이 없어야 한다는 요구 사항은, 셀프 호스팅(즉, 백엔드의 휠을 빌드할 때 그 백엔드 자신을 빌드에 사용하는 것)을 원하는 백엔드가 순환을 일으키지 않도록 특별한 대비를 해야 함을 의미한다는 점에 유의하십시오. 일반적으로 이는 자기 자신을 트리 내 백엔드로 지정하고, 외부 빌드 의존성을 피하는 것(보통 벤더링을 통해)을 포함합니다.

빌드 백엔드 인터페이스

빌드 백엔드 객체는 다음 훅의 일부 또는 전부를 제공하는 속성을 가지고 있어야 합니다. 공통 config_settings 인자는 개별 훅을 설명한 다음에 설명합니다.

필수 훅

build_wheel

def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
    ...

.whl 파일을 빌드하여 지정된 wheel_directory에 두어야 합니다. 생성한 .whl 파일의 기본 이름(전체 경로가 아님)을 유니코드 문자열로 반환해야 합니다.

빌드 프런트엔드가 이전에 prepare_metadata_for_build_wheel을 호출했고, 이번 호출로 생성되는 휠의 메타데이터가 그 이전 호출의 메타데이터와 일치하는 것에 의존한다면, 생성된 .dist-info 디렉터리의 경로를 metadata_directory 인자로 제공해야 합니다. 이 인자가 제공되면 build_wheel은 동일한 메타데이터를 가진 휠을 생성해야 합니다(MUST). 빌드 프런트엔드가 전달하는 디렉터리는 prepare_metadata_for_build_wheel이 생성한 디렉터리와 동일해야 하며(MUST), 이 훅이 생성한 인식되지 않는 파일도 모두 포함해야 합니다.

prepare_metadata_for_build_wheel 훅을 제공하지 않는 백엔드는 build_wheel의 metadata_directory 매개변수를 조용히 무시하거나, 이 매개변수가 None 이외의 값으로 설정되면 예외를 발생시킬 수 있습니다.

서로 다른 소스에서 나온 휠이 같은 방식으로 빌드되도록 보장하기 위해, 프런트엔드는 먼저 build_sdist를 호출한 다음 압축을 푼 sdist 안에서 build_wheel을 호출할 수 있습니다. 그러나 백엔드가 sdist를 만드는 데 필요한 일부 요구 사항이 없다고 알리면(아래 참조), 프런트엔드는 대신 소스 디렉터리에서 build_wheel을 호출합니다.

소스 디렉터리는 읽기 전용일 수 있습니다. 따라서 백엔드는 소스 디렉터리에 파일을 생성하거나 수정하지 않고도 빌드할 수 있도록 대비해야 합니다. 다만 백엔드는 이 경우를 처리하지 않기로 할 수도 있으며, 그러면 실패가 사용자에게 드러납니다. 프런트엔드는 읽기 전용 소스 디렉터리를 특별히 처리할 책임이 없습니다.

백엔드는 중간 산출물을 캐시 위치나 임시 디렉터리에 저장할 수 있습니다. 캐시가 있든 없든 빌드의 최종 결과에 실질적인 차이가 생겨서는 안 됩니다.

build_sdist

def build_sdist(sdist_directory, config_settings=None):
    ...

.tar.gz 소스 배포판을 빌드하여 지정된 sdist_directory에 넣어야 합니다. 생성한 .tar.gz 파일의 기본 이름(전체 경로가 아님)을 유니코드 문자열로 반환해야 합니다.

.tar.gz 소스 배포판(sdist)에는 {name}-{version}(예: foo-1.0)이라는 단일 최상위 디렉터리가 있으며, 이 디렉터리에 패키지의 소스 파일이 들어 있습니다. 이 디렉터리에는 빌드 디렉터리의 pyproject.toml과 PEP 345에 설명된 형식의 메타데이터를 담은 PKG-INFO 파일도 들어 있어야 합니다. 과거에는 zip 파일도 sdist로 사용되었지만, 이 훅은 gzip으로 압축된 tarball을 생성해야 합니다. 이 형식은 이미 sdist에서 더 흔히 쓰이는 형식이며, 형식이 일관되면 도구를 더 단순하게 만들 수 있습니다.

생성된 tarball은 UTF-8 기반 파일 이름을 규정하는 최신 POSIX.1-2001 pax tar 형식을 사용해야 합니다. 이 형식은 Python 3.6과 함께 제공되는 tarfile 모듈의 기본값이 아직 아니므로, tarfile 모듈을 사용하는 백엔드는 format=tarfile.PAX_FORMAT을 명시적으로 전달해야 합니다.

일부 백엔드는 sdist를 만드는 데 버전 관리 도구와 같은 추가 요구 사항이 있을 수 있습니다. 그러나 일부 프런트엔드는 일관성을 보장하기 위해 휠을 만들 때 중간 sdist를 만드는 방식을 선호할 수 있습니다. 의존성이 없거나 그 밖에 잘 알려진 다른 이유로 sdist를 생성할 수 없다면, 백엔드는 특정 타입의 예외를 발생시켜야 하며, 이 예외 타입은 백엔드 객체에 UnsupportedOperation으로 제공합니다. 프런트엔드가 휠을 만들기 위한 중간 단계로 sdist를 빌드하는 도중 이 예외를 받으면, 대신 휠을 직접 빌드해야 합니다. 백엔드가 이 예외를 절대 발생시키지 않는다면 이 예외 타입을 정의할 필요가 없습니다.

선택적 훅

get_requires_for_build_wheel

def get_requires_for_build_wheel(config_settings=None):
    ...

이 훅은 build_wheel 또는 prepare_metadata_for_build_wheel 훅을 호출할 때 설치할 PEP 508 의존성 명세를 담은 문자열의 추가 리스트를 반드시 반환해야 합니다. 이 리스트는 pyproject.toml 파일에 지정된 의존성에 더해 추가로 설치할 항목입니다.

예시:

def get_requires_for_build_wheel(config_settings):
    return ["wheel >= 0.25", "setuptools"]

정의되지 않은 경우 기본 구현은 return []과 동등합니다.

prepare_metadata_for_build_wheel

def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
    ...

지정된 metadata_directory 안에 휠 메타데이터를 담은 .dist-info 디렉터리를 생성해야 합니다(즉, {metadata_directory}/{package}-{version}.dist-info/와 같은 디렉터리를 생성합니다). 이 디렉터리는 반드시 휠 명세에 정의된 유효한 .dist-info 디렉터리여야 합니다. 단, RECORD나 서명은 포함하지 않아도 됩니다. 훅은 이 디렉터리 안에 다른 파일을 생성할 수도 있으며, 빌드 프런트엔드는 그러한 파일을 반드시 보존해야 하지만 그 외에는 무시해야 합니다. 이는 메타데이터가 빌드 시점의 결정에 따라 달라지는 경우, 빌드 백엔드가 실제 휠 빌드 단계에서 재사용할 수 있도록 이러한 결정을 편리한 형식으로 기록해야 할 수도 있다는 점을 고려한 것입니다.

생성한 .dist-info 디렉터리의 기본 이름(전체 경로가 아님)을 유니코드 문자열로 반환해야 합니다.

빌드 프런트엔드가 이 정보를 필요로 하는데 이 메서드가 정의되어 있지 않다면, build_wheel을 호출하고 그 결과로 생성된 메타데이터를 직접 확인해야 합니다.

get_requires_for_build_sdist

def get_requires_for_build_sdist(config_settings=None):
    ...

이 훅은 pyproject.toml 파일에 지정된 것 외에 추가로 필요한 PEP 508 의존성 명세를 담은 문자열 리스트를 반드시 반환해야 합니다(MUST). 이 의존성들은 build_sdist 훅을 호출할 때 설치됩니다.

정의되지 않은 경우, 기본 구현은 return []과 동일합니다.

Note

편집 가능한 설치

이 PEP는 원래 (pip install -e처럼) 편집 가능한 설치를 수행하는 또 다른 훅인 install_editable을 명시했습니다. 이 주제가 복잡하여 제거되었지만, 이후의 PEP에서 명시될 수 있습니다.

간단히 말해, 답해야 할 질문에는 다음이 포함됩니다: ‘편집 가능한 설치’를 구현하는 합리적인 방법으로는 어떤 것들이 있습니까? 편집 가능한 설치를 어떻게 만들지는 백엔드와 프론트엔드 중 어느 쪽이 선택해야 합니까? 그리고 프론트엔드가 선택한다면, 그렇게 하기 위해 백엔드로부터 무엇이 필요합니까?

설정 값

config_settings

모든 훅에 전달되는 이 인자는 사용자가 개별 패키지 빌드에 임시 설정을 전달할 수 있도록 “비상구” 역할로 제공되는 임의의 딕셔너리입니다. 빌드 백엔드는 이 딕셔너리에 원하는 어떤 의미든 부여할 수 있습니다(MAY). 빌드 프론트엔드는 사용자가 이 딕셔너리에 넣을 임의의 문자열 키/문자열 값 쌍을 지정할 수 있는 메커니즘을 제공해야 합니다(SHOULD). 예를 들어, --package-config CC=gcc와 같은 문법을 지원할 수 있습니다. 사용자가 중복된 문자열 키를 제공하는 경우, 빌드 프론트엔드는 해당 문자열 값들을 하나의 문자열 리스트로 합쳐야 합니다(SHOULD). 빌드 프론트엔드는 사용자가 이 딕셔너리에 항목을 넣을 수 있는 임의의 다른 메커니즘을 제공할 수도 있습니다(MAY). 예를 들어, pip은 다음과 같이 최신 명령줄 인자와 레거시 명령줄 인자가 섞인 것을:

pip install                                           \
  --package-config CC=gcc                             \
  --global-option="--some-global-option"              \
  --build-option="--build-option1"                    \
  --build-option="--build-option2"

다음과 같은 config_settings 딕셔너리로 매핑하도록 선택할 수 있습니다.:

{
 "CC": "gcc",
 "--global-option": ["--some-global-option"],
 "--build-option": ["--build-option1", "--build-option2"],
}

물론, 빌드하려는 특정 빌드 백엔드와 패키지에 맞는 옵션을 전달하는지 확인하는 것은 사용자의 몫입니다.

훅은 위치 인자나 키워드 인자로 호출될 수 있으므로, 훅을 구현하는 백엔드는 시그니처가 위에 나온 인자들의 순서와 이름 모두와 일치하도록 주의해야 합니다.

모든 훅은 작업 디렉터리를 소스 트리의 루트로 설정한 상태에서 실행되며, stdout과 stderr에 임의의 정보성 텍스트를 출력할 수 있습니다(MAY). 훅은 stdin에서 읽어서는 절대 안 되며(MUST NOT), 빌드 프론트엔드는 훅을 호출하기 전에 stdin을 닫을 수 있습니다(MAY).

빌드 프론트엔드는 백엔드의 stdout과 stderr 중 하나 또는 둘 다를 캡처할 수 있습니다. 백엔드가 출력 스트림이 터미널/콘솔이 아님을 감지하면(예: not sys.stdout.isatty()), 해당 스트림에 쓰는 모든 출력이 UTF-8로 인코딩되도록 해야 합니다(SHOULD). 빌드 프런트엔드는 캡처한 출력이 유효한 UTF-8이 아니더라도 실패해서는 안 되지만(MUST NOT), 그 경우 모든 정보를 보존하지 않을 수도 있습니다(MAY)(예: Python의 replace 오류 핸들러를 사용해 디코딩할 수 있습니다). 출력 스트림이 터미널인 경우, 터미널에서 실행되는 여느 프로그램과 마찬가지로 빌드 백엔드가 출력을 정확하게 표시할 책임을 집니다.

훅이 예외를 발생시키거나 프로세스를 종료시키면, 이는 오류를 나타냅니다.

빌드 환경

빌드 프론트엔드의 책임 중 하나는 빌드 백엔드가 실행될 Python 환경을 구성하는 것입니다.

특정 “가상 환경” 메커니즘의 사용을 요구하지는 않습니다. 빌드 프론트엔드는 virtualenv나 venv를 사용할 수도 있고, 특별한 메커니즘을 전혀 사용하지 않을 수도 있습니다. 그러나 어떤 메커니즘을 사용하든 다음 기준을 반드시 충족해야 합니다(MUST):

  • 프로젝트의 빌드 요구 사항에 지정된 모든 요구 사항은 Python에서 임포트할 수 있어야 합니다. 구체적으로는 다음과 같습니다.
    • get_requires_for_build_wheel 및 get_requires_for_build_sdist 훅은 pyproject.toml 파일에 지정된 부트스트랩 요구 사항이 포함된 환경에서 실행됩니다.
    • prepare_metadata_for_build_wheel 및 build_wheel 훅은 pyproject.toml의 부트스트랩 요구 사항과 get_requires_for_build_wheel 훅이 지정한 요구 사항이 포함된 환경에서 실행됩니다.
    • build_sdist 훅은 pyproject.toml의 부트스트랩 요구 사항과 get_requires_for_build_sdist 훅이 지정한 요구 사항이 포함된 환경에서 실행됩니다.
  • 이 조건은 빌드 환경이 생성하는 새 Python 서브프로세스에서도 마찬가지로 성립해야 합니다. 예를 들어 다음과 같은 코드는:
    import sys, subprocess
    subprocess.check_call([sys.executable, ...])
    

    프로젝트의 모든 빌드 요구 사항에 접근할 수 있는 Python 프로세스를 생성해야 합니다. 이는 예를 들어 레거시 setup.py 스크립트를 서브프로세스에서 실행하려는 빌드 백엔드에 필요합니다.

  • 빌드에 필요한 패키지가 제공하는 모든 명령줄 스크립트는 빌드 환경의 PATH에 있어야 합니다. 예를 들어 어떤 프로젝트가 flit에 대한 빌드 요구 사항을 선언했다면, flit 명령줄 도구를 실행하는 방법으로 다음이 동작해야 합니다:
    import subprocess
    import shutil
    subprocess.check_call([shutil.which("flit"), ...])
    

빌드 백엔드는 위 기준을 충족하는 어떤 환경에서도 함수로서 제대로 기능할 수 있도록 준비되어 있어야 합니다(MUST). 특히 빌드 백엔드는 표준 라이브러리에 있는 패키지나 빌드 요구 사항으로 명시적으로 선언된 패키지 외의 다른 패키지에 접근할 수 있다고 절대 가정해서는 안 됩니다.

프런트엔드는 각 훅을 새 서브프로세스에서 호출해야 합니다. 그래야 백엔드가 (환경 변수나 작업 디렉터리 같은) 프로세스 전역 상태를 자유롭게 변경할 수 있습니다. 프런트엔드가 이런 방식으로 훅을 쉽게 호출하는 데 사용할 수 있는 Python 라이브러리가 제공될 예정입니다.

빌드 프런트엔드를 위한 권장 사항 (비규범적)

빌드 프런트엔드는 위 기준을 충족하는 빌드 환경을 설정하기 위해 어떤 메커니즘이든 사용할 수 있습니다. 예를 들어 모든 빌드 요구 사항을 전역 환경에 설치하기만 해도 규격을 준수하는 어떤 패키지든 빌드하기에는 충분합니다. 하지만 이 방법은 여러 가지 이유로 최선이 아닙니다. 이 절에는 프런트엔드 구현자를 위한 비규범적 조언이 담겨 있습니다.

빌드 프런트엔드는 기본적으로 각 빌드마다 표준 라이브러리와 명시적으로 요청된 빌드 의존성만 포함하는 격리된 환경을 생성해야 합니다. 이렇게 하면 두 가지 이점이 있습니다:

  • 한 번의 설치 실행으로 빌드 요구 사항이 서로 충돌하는 여러 패키지를 빌드할 수 있습니다. 예를 들어 package1은 빌드에 pbr==1.8.1이 필요하고 package2는 빌드에 pbr==1.7.2가 필요하다면, 이 둘을 전역 환경에 동시에 설치할 수 없습니다. 이는 사용자가 pip install package1 package2를 요청할 때 문제가 됩니다. 또는 사용자가 이미 전역 환경에 pbr==1.8.1을 설치해 두었는데 어떤 패키지가 빌드에 pbr==1.7.2를 요구한다면, 사용자의 버전을 다운그레이드하는 것은 상당히 무례한 일입니다.
  • 이는 실제로 정확한 빌드 의존성을 선언하는 패키지의 수를 최대화하는 일종의 공중 보건 조치 역할을 합니다. 패키지 작성자에게 강한 어조의 경고문을 얼마든지 써 보낼 수는 있지만, 빌드 프런트엔드가 기본적으로 격리를 강제하지 않으면 원래 작성자의 컴퓨터에서만 잘 빌드되고 다른 곳에서는 빌드되지 않는 패키지가 PyPI에 잔뜩 쌓이게 될 수밖에 없습니다. 이는 누구도 원하지 않는 골칫거리입니다.

하지만 빌드 요구 사항이 여러 방식으로 문제를 일으키는 상황도 있을 것입니다. 예를 들어 우리가 최선을 다했는데도 패키지 작성자가 실수로 중요한 요구 사항을 빠뜨릴 수 있습니다. 또는 어떤 패키지가 foo >= 1.0에 대한 빌드 요구 사항을 선언했는데, 1.0이 최신 버전이었을 때는 아주 잘 동작했지만 이제 1.1이 나왔고 거기에 치명적인 버그가 있을 수 있습니다. 또는 사용자가 빌드 결과물이 이전 버전의 numpy와 C ABI 수준에서 호환되도록 보장하기 위해, 패키지가 선호하는 numpy==1.8 대신 numpy==1.7을 기준으로 패키지를 빌드하기로 결정할 수도 있습니다(이로 인해 빌드 결과물이 업스트림에서 지원되지 않게 되더라도 말입니다). 따라서 빌드 프런트엔드는 사용자가 위의 기본값을 재정의할 수 있는 메커니즘을 제공해야 합니다. 예를 들어 빌드 프런트엔드는 빌드 환경을 생성할 때 virtualenv 또는 그에 상응하는 도구에 --system-site-packages 옵션이 전달되도록 하는 --build-with-system-site-packages 옵션이나, 프로젝트의 일반 빌드 요구 사항을 재정의하는 --build-requirements-override=my-requirements.txt 옵션을 둘 수 있습니다.

여기서 일반 원칙은 패키지 작성자에게는 위생 관리를 강제하면서도, 최종 사용자는 필요할 때 보닛을 열고 덕트 테이프를 붙일 수 있도록 허용하는 것입니다.

트리 내 빌드 백엔드

특정 상황에서는 프로젝트가 requires 키로 백엔드를 참조하는 대신 빌드 백엔드의 소스 코드를 소스 트리에 직접 포함하려고 할 수 있습니다. 이런 경우가 예상되는 구체적인 상황은 다음 두 가지입니다.

  • 자기 자신을 빌드할 때 자신의 기능을 사용하려는 백엔드 자체(“셀프 호스팅 백엔드”)
  • 프로젝트 전용 백엔드. 대개 표준 백엔드를 감싸는 사용자 정의 래퍼로 이루어집니다. 이 래퍼는 프로젝트에 지나치게 특화되어 있어 따로 배포할 가치가 없습니다(“트리 내 백엔드”).

프로젝트는 pyproject.toml에 backend-path 키를 넣어 백엔드 코드가 트리 안에 있음을 지정할 수 있습니다. 이 키에는 디렉터리 목록이 들어갑니다. 프런트엔드는 백엔드를 로드하고 백엔드 훅을 실행할 때 이 디렉터리들을 sys.path의 맨 앞에 추가합니다.

backend-path 키의 내용에는 두 가지 제약이 있습니다.

  • backend-path에 있는 디렉터리는 프로젝트 루트를 기준으로 한 상대 경로로 해석됩니다. 또한 상대 경로와 심볼릭 링크를 해석한 뒤에 반드시 소스 트리 내부의 위치를 가리켜야 합니다.
  • 백엔드 코드는 반드시 backend-path에 지정된 디렉터리 중 하나에서 로드해야 합니다. 즉, backend-path를 지정하면서 트리 내 백엔드 코드를 두지 않는 것은 허용되지 않습니다.

첫 번째 제약은 소스 트리가 자기 완결적인 상태를 유지하고 소스 트리 밖의 위치를 참조할 수 없도록 하기 위한 것입니다. 프런트엔드는 이 조건을 검사해야 합니다(SHOULD). 대개 위치를 절대 경로로 바꾸고 심볼릭 링크를 해석한 다음 프로젝트 루트와 비교하는 방식으로 검사합니다. 조건을 위반하면 오류 메시지를 내고 실패해야 합니다.

backend-path 기능은 트리 내 백엔드의 구현을 지원하기 위한 것입니다. 기존 백엔드를 설정할 수 있게 하려는 기능이 아닙니다. 위의 두 번째 제약은 바로 이 기능이 그런 방식으로만 사용되도록 하기 위한 것입니다. 프런트엔드는 이 검사를 강제할 수 있지만, 반드시 그래야 하는 것은 아닙니다. 이 검사는 대개 백엔드의 __file__ 속성을 backend-path의 위치들과 비교하는 방식으로 합니다.

소스 배포판

기존의 레거시 sdist 형식을 계속 사용하면서 몇 가지 새로운 제약을 추가합니다. 이 형식은 대부분 정의되어 있지 않습니다. 기본적으로는 {NAME}-{VERSION}.{EXT}라는 이름의 파일이며, 압축을 풀면 이름이 {NAME}-{VERSION}/인 빌드 가능한 소스 트리가 나옵니다. 전통적으로 이 파일에는 항상 setup.py-스타일 소스 트리가 들어 있었습니다. 이제는 pyproject.toml-스타일 소스 트리도 담을 수 있도록 허용합니다.

통합 프런트엔드는 {NAME}-{VERSION}.{EXT}라는 이름의 sdist가 이름이 {NAME}-{VERSION}-{COMPAT-INFO}.whl인 휠을 생성할 것을 요구합니다.

PEP 517 백엔드가 빌드한 sdist에 적용되는 새로운 제약은 다음과 같습니다.

  • sdist는 .tar.gz 확장자를 가진, gzip으로 압축한 tar 아카이브여야 합니다. Zip 아카이브나 tarball의 다른 압축 형식은 현재 허용되지 않습니다.
  • Tar 아카이브는 파일 이름에 UTF-8을 쓰는 최신 POSIX.1-2001 pax tar 형식으로 만들어야 합니다.
  • sdist에 담긴 소스 트리에는 pyproject.toml 파일이 있어야 합니다.

발전 방향에 대한 메모

여기서의 목표 중 하나는 구식 sdist를 신식 sdist로 최대한 간단하게 변환할 수 있게 하는 것입니다. (예를 들어, 동적 빌드 요구 사항을 지원하는 이유 중 하나가 이것입니다.) 가장 이상적인 모습은 단일한 정적 pyproject.toml이 있고, 이를 어떤 “버전 0” VCS 체크아웃에든 넣기만 하면 새롭고 멋진 형식으로 바뀌는 것입니다. 아마 100% 가능하지는 않겠지만 그에 가깝게 만들 수는 있습니다. 그리고 목표에 얼마나 가까워졌는지 계속 파악하는 것이 중요합니다… 이 절이 있는 이유가 바로 그것입니다.

대략적인 계획은 다음과 같습니다. 우리가 정하게 될 훅 언어를 이해하고, 이를 setup.py 호출로 변환할 수 있는 빌드 시스템 패키지(setuptools_pypackage 또는 그 비슷한 것)를 만드는 것입니다. 그러려면 setuptools에 일종의 훅이나 몽키패칭이 필요할 것입니다. 필요할 때 setup_requires= 인자를 추출하는 방법을 제공하고, 신식 형식을 생성하는 새로운 버전의 sdist 명령을 제공하기 위해서입니다. 이 모든 작업은 실현 가능해 보이며 상당수 패키지에는 이것으로 충분해 보입니다(물론 여기서 무엇이든 확정하기 전에 그러한 시스템의 프로토타입을 먼저 만들어 보고 싶을 것입니다). (별도의 패키지를 만드는 대신 setuptools 자체에 이 변경 사항을 적용할 수도 있습니다.)

그러나 아직 두 가지 장애물이 남아 있어서, 패키지를 새로운 형식으로 자동 업그레이드하기는 아마 어려울 것입니다.

  1. 현재 setup.py가 실행되기 전에 특정 패키지가 환경에 준비되어 있어야 한다고 요구하는 패키지가 있습니다. 따라서 격리된 virtualenv 같은 환경에서 빌드 스크립트를 실행하기로 결정한다면, 프로젝트는 자신이 이렇게 하고 있는지 확인해야 합니다. 그렇다면 새 시스템으로 업그레이드할 때 이러한 의존성을 (setup_requires=를 통하거나 pyproject.toml의 정적 선언을 통해) 명시적으로 선언하기 시작해야 합니다.
  2. 현재 일관된 메타데이터를 선언하지 않는 패키지도 있습니다(예: egg_info와 bdist_wheel이 서로 다른 install_requires=를 얻을 수 있습니다). 새 시스템으로 업그레이드할 때 프로젝트는 이것이 자신에게 해당하는지 평가해야 하며, 해당한다면 더 이상 그렇게 하지 않아야 합니다.

기각된 선택지

  • wheel 및 sdist 훅이 각각의 아카이브와 같은 내용을 담은 압축 해제된 디렉터리를 빌드하도록 하는 방안을 논의했습니다. 경우에 따라서는 이렇게 하면 아카이브를 묶고 풀 필요가 없어지겠지만, 이는 섣부른 최적화로 보입니다. 도구가 아카이브를 표준 교환 형식으로 다루는 편이 유리합니다(특히 아카이브 형식이 이미 표준화된 wheel의 경우 그렇습니다). 재현 가능한 빌드를 위해서는 아카이브 생성을 세밀하게 제어하는 것이 중요합니다. 또한 압축 해제된 배포본이 필요한 작업이 아카이브가 필요한 작업보다 더 흔할지도 분명하지 않습니다.
  • build_wheel을 호출하기 전에 빌드 디렉터리로 파일을 복사하는 추가 훅을 검토했습니다. 기존 빌드 시스템을 살펴본 결과, 많은 도구에서는 빌드 디렉터리로 파일을 미리 복사하는 것보다 build_wheel에 빌드 디렉터리를 전달하는 편이 더 합리적이라는 것을 알게 되었습니다.
  • 이후 build_wheel에 빌드 디렉터리를 전달한다는 아이디어 역시 불필요하게 복잡하다고 판단되었습니다. 빌드 도구는 빌드하는 동안 중간 파일을 저장하기 위해 임시 디렉터리나 캐시 디렉터리를 사용할 수 있습니다. 필요하다면 향후 프런트엔드가 제어하는 캐시 디렉터리를 추가할 수 있습니다.
  • build_sdist가 예상된 이유로 인한 실패를 알리는 방법으로, NotImplementedError를 발생시키는 것과 NotImplemented 또는 None을 반환하는 것을 포함한 다양한 선택지를 매우 오랫동안 논의했습니다. 이 논의에 몹시 지쳤으므로, 정말로 타당한 이유가 없다면 이 논의를 다시 시작하려 하지 마십시오.
  • 소스 트리의 파일에서 백엔드를 임포트할 수 있게 하면 Python 임포트가 흔히 동작하는 방식과 더 일관될 것입니다. 그러나 이를 허용하지 않으면 모듈 이름 충돌로 인한 혼란스러운 오류를 방지할 수 있습니다. 이 PEP의 초기 버전은 소스 트리 안의 파일에서 백엔드를 임포트할 수 있게 하는 수단을 제공하지 않았지만, 다음 개정에서 필요한 경우 프로젝트가 이 동작을 선택할 수 있도록 backend-path 키가 추가되었습니다.

PEP 517 변경 사항 요약

초기 참조 구현이 pip 19.0에서 릴리스된 후 이 PEP에 다음과 같은 변경 사항이 적용되었습니다.

  • 빌드 요구 사항의 순환이 명시적으로 금지되었습니다.
  • [build-system] 테이블에 backend-path 키를 도입하여 트리 내 백엔드와 백엔드의 자체 호스팅 지원이 추가되었습니다.
  • build-backend를 명시적으로 지정하지 않은 소스 트리의 경우, setuptools.build_meta:__legacy__ PEP 517 백엔드가 setup.py를 직접 호출하는 것을 대신할 수 있는 허용 가능한 대안임을 명확히 했습니다.

부록 A: PEP 516과의 비교

PEP 516은 빌드 시스템 인터페이스를 명세하기 위한 경쟁 제안이었으며, 현재는 이 PEP가 채택되면서 기각되었습니다. 주된 차이점은 우리의 빌드 백엔드가 명령줄 기반 인터페이스가 아니라 Python 훅 기반 인터페이스를 통해 정의된다는 점입니다.

이 부록은 PEP 516 대신 이 PEP를 채택해야 한다는 근거로 제시된 인자(arguments), 즉 논거를 기록합니다.

명령줄 인터페이스 대신 Python 훅을 지정하는 것만으로 백엔드 호출의 복잡성이 줄어들 것이라고는 기대하지 않습니다. 빌드 프런트엔드는 어떤 경우든 자식 프로세스 안에서 훅을 실행하려 할 것이기 때문입니다 – 이는 빌드 프런트엔드 자체를 백엔드 코드로부터 격리하고 빌드 백엔드의 실행 환경을 더 잘 제어하기 위해 중요합니다. 따라서 두 제안 모두에서, pip에는 하위 프로세스를 생성하고 일종의 명령줄/IPC 인터페이스와 통신하는 코드가 필요하며, 하위 프로세스에는 이러한 명령줄 인자를 파싱하고 실제 빌드 백엔드 구현을 호출하는 방법을 아는 코드가 필요합니다. 따라서 이 다이어그램은 모든 제안에 동일하게 적용됩니다:

+-----------+          +---------------+           +----------------+
| frontend  | -spawn-> | child cmdline | -Python-> |    backend     |
|   (pip)   |          |   interface   |           | implementation |
+-----------+          +---------------+           +----------------+

두 접근 방식의 핵심 차이는 이러한 인터페이스 경계가 프로젝트 구조에 어떻게 대응되는가에 있습니다:

.-= This PEP =-.

+-----------+          +---------------+    |      +----------------+
| frontend  | -spawn-> | child cmdline | -Python-> |    backend     |
|   (pip)   |          |   interface   |    |      | implementation |
+-----------+          +---------------+    |      +----------------+
                                            |
|______________________________________|    |
   Owned by pip, updated in lockstep        |
                                            |
                                            |
                                 PEP-defined interface boundary
                               Changes here require distutils-sig


.-= Alternative =-.

+-----------+    |     +---------------+           +----------------+
| frontend  | -spawn-> | child cmdline | -Python-> |    backend     |
|   (pip)   |    |     |   interface   |           | implementation |
+-----------+    |     +---------------+           +----------------+
                 |
                 |     |____________________________________________|
                 |      Owned by build backend, updated in lockstep
                 |
    PEP-defined interface boundary
  Changes here require distutils-sig

PEP가 정의하는 인터페이스 경계를 Python 코드로 옮김으로써 우리는 세 가지 핵심 이점을 얻습니다.

첫째, 빌드 프런트엔드는 소수(pip, 그리고… 아마 몇 개 더?)에 불과할 가능성이 높은 반면, 커스텀 빌드 백엔드는 롱테일을 이룰 가능성이 높으므로(각 패키지가 자신의 특정한 빌드 요구 사항에 맞춰 개별적으로 선택하기 때문입니다), 실제 다이어그램은 아마 다음과 더 비슷할 것입니다:

.-= This PEP =-.

+-----------+          +---------------+           +----------------+
| frontend  | -spawn-> | child cmdline | -Python+> |    backend     |
|   (pip)   |          |   interface   |        |  | implementation |
+-----------+          +---------------+        |  +----------------+
                                                |
                                                |  +----------------+
                                                +> |    backend     |
                                                |  | implementation |
                                                |  +----------------+
                                                :
                                                :

.-= Alternative =-.

+-----------+          +---------------+           +----------------+
| frontend  | -spawn+> | child cmdline | -Python-> |    backend     |
|   (pip)   |       |  |   interface   |           | implementation |
+-----------+       |  +---------------+           +----------------+
                    |
                    |  +---------------+           +----------------+
                    +> | child cmdline | -Python-> |    backend     |
                    |  |   interface   |           | implementation |
                    |  +---------------+           +----------------+
                    :
                    :

즉, 이 PEP을 따르면 생태계 전체의 코드 총량이 줄어듭니다. 특히 새로운 빌드 시스템을 만드는 진입 장벽을 낮춥니다. 예를 들어, 다음은 완전하고 실제로 동작하는 빌드 백엔드입니다:

# mypackage_custom_build_backend.py
import os.path
import pathlib
import shutil
import tarfile

SDIST_NAME = "mypackage-0.1"
SDIST_FILENAME = SDIST_NAME + ".tar.gz"
WHEEL_FILENAME = "mypackage-0.1-py2.py3-none-any.whl"

#################
# sdist creation
#################

def _exclude_hidden_and_special_files(archive_entry):
    """Tarfile filter to exclude hidden and special files from the archive"""
    if archive_entry.isfile() or archive_entry.isdir():
        if not os.path.basename(archive_entry.name).startswith("."):
            return archive_entry

def _make_sdist(sdist_dir):
    """Make an sdist and return both the Python object and its filename"""
    sdist_path = pathlib.Path(sdist_dir) / SDIST_FILENAME
    sdist = tarfile.open(sdist_path, "w:gz", format=tarfile.PAX_FORMAT)
    # Tar up the whole directory, minus hidden and special files
    sdist.add(os.getcwd(), arcname=SDIST_NAME,
              filter=_exclude_hidden_and_special_files)
    return sdist, SDIST_FILENAME

def build_sdist(sdist_dir, config_settings):
    """PEP 517 sdist creation hook"""
    sdist, sdist_filename = _make_sdist(sdist_dir)
    return sdist_filename

#################
# wheel creation
#################

def get_requires_for_build_wheel(config_settings):
    """PEP 517 wheel building dependency definition hook"""
    # As a simple static requirement, this could also just be
    # listed in the project's build system dependencies instead
    return ["wheel"]

def build_wheel(wheel_directory,
                metadata_directory=None, config_settings=None):
    """PEP 517 wheel creation hook"""
    from wheel.archive import archive_wheelfile
    path = os.path.join(wheel_directory, WHEEL_FILENAME)
    archive_wheelfile(path, "src/")
    return WHEEL_FILENAME

물론 이것은 끔찍한 빌드 백엔드입니다. 사용자가 src/mypackage-0.1.dist-info/에 휠 메타데이터를 직접 설정해 두어야 하고, 버전 번호가 바뀌면 여러 곳을 직접 수정해야 합니다… 하지만 동작은 하며, 더 많은 기능을 점진적으로 추가할 수 있습니다. 많은 경험에 비추어 보면, 크게 성공한 프로젝트는 흔히 급하게 만든 해킹에서 시작합니다(예: Linux – “그저 취미일 뿐, 크고 전문적인 것이 되지는 않을 것”; IPython/Jupyter – 한 대학원생의 $PYTHONSTARTUP 파일). 따라서 좋은 빌드 도구로 이루어진 활발한 생태계의 성장을 장려하는 것이 목표라면 진입 장벽을 최소화하는 것이 중요합니다.

둘째, Python은 인터페이스를 기술하는 데 더 단순하면서도 더 풍부한 구조를 제공하므로, 명세에서 불필요한 복잡성을 제거할 수 있습니다 – 그리고 명세는 복잡성이 들어가기에 최악의 장소입니다. 명세를 변경하려면 많은 이해관계자 사이에서 고통스러운 합의 형성 과정을 거쳐야 하기 때문입니다. 명령줄 인터페이스 방식에서는 여러 종류의 입력을 하나의 선형 명령줄에 매핑하는 임시방편을 고안해야 합니다(예: 사용자가 지정한 설정 인자와 PEP가 정의한 인자 사이의 충돌을 어떻게 피합니까? 선택적 인자는 어떻게 지정합니까? Python 인터페이스로 작업할 때는 이런 질문에 단순하고 명백한 답이 있습니다). 서브프로세스를 생성하고 관리할 때는 제대로 처리해야 하는 까다로운 세부 사항이 많고, 플랫폼 간 미묘한 차이도 있으며, 가장 뻔한 접근 방식 중 일부는 – 예를 들어 build_requires 연산의 데이터를 반환하는 데 stdout을 사용하는 것 – 예상치 못한 함정을 만들 수 있습니다(예: 빌드 요구 사항을 계산하는 데 자식 프로세스를 몇 개 생성해야 하고, 이 자식 프로세스들이 가끔 stdout에 오류 메시지를 출력하면 어떻게 됩니까? 물론 신중한 빌드 백엔드 작성자라면 이 문제를 피할 수 있지만, Python 인터페이스를 정의하는 가장 뻔한 방식에서는 훅 반환값이 명확하게 구분되므로 이런 가능성이 완전히 사라집니다).

일반적으로 빌드 백엔드를 별도의 프로세스로 격리해야 하므로 IPC의 복잡성을 완전히 제거할 수는 없습니다 – 하지만 IPC 채널의 양쪽을 모두 하나의 프로젝트가 통제하도록 하면, 버그를 고치는 데 생태계 전반의 조율된 합의와 조율된 변경이 필요한 경우보다 IPC 인터페이스의 버그를 훨씬 적은 비용으로 고칠 수 있습니다.

셋째, 그리고 가장 결정적으로, Python 훅 방식은 앞으로 이 명세를 발전시킬 수 있는 훨씬 더 강력한 선택지를 제공합니다.

구체적인 예로, 내년에 새로운 build_sdist_from_vcs 훅을 추가한다고 가정해 봅시다. 이 훅은 현재의 build_sdist 훅에 대한 대안으로, 개별 백엔드가 버전 관리 추적 메타데이터를 직접 조회하는 대신 프런트엔드가 그 정보를 백엔드에 전달할 책임을 집니다(디스크상의 모든 파일이 추적되고 있는지 알리는 것도 포함합니다). 이 전환을 관리하려면 빌드 프런트엔드가 build_sdist_from_vcs를 사용할 수 있을 때는 투명하게 사용하고 그렇지 않으면 build_sdist로 대체할 수 있어야 하며, 빌드 백엔드는 이전 빌드 프런트엔드와 새 빌드 프런트엔드 모두와 호환되도록 두 메서드를 모두 정의할 수 있어야 합니다.

나아가 우리의 메커니즘은 두 가지 목표를 더 충족해야 합니다. (a) 예를 들어 pip과 flit의 새 버전이 모두 새 인터페이스를 지원하도록 업데이트되었다면, 그것만으로 새 인터페이스가 사용되기에 충분해야 합니다. 특히 flit을 사용하는 모든 프로젝트가 각자의 pyproject.toml 파일을 업데이트할 필요는 없어야 합니다. (b) 이 협상을 수행하는 것만을 위해 추가 프로세스를 생성하고 싶지는 않습니다. 일부 플랫폼(Windows)에서 여러 패키지로 이루어진 대규모 스택을 배포할 때 프로세스 생성은 쉽게 병목이 될 수 있기 때문입니다.

여기서 설명하는 인터페이스에서는 이 모든 목표를 쉽게 달성할 수 있습니다. pip이 자식 프로세스 안에서 실행되는 코드를 통제하므로, 다음과 같은 일을 하도록 코드를 쉽게 작성할 수 있습니다:

command, backend, args = parse_command_line_args(...)
if command == "build_sdist":
   if hasattr(backend, "build_sdist_from_vcs"):
       backend.build_sdist_from_vcs(...)
   elif hasattr(backend, "build_sdist"):
       backend.build_sdist(...)
   else:
       # error handling

공개 인터페이스 경계를 서브프로세스 호출 지점에 두는 대안에서는 이것이 불가능합니다 – 지원되는 인터페이스가 무엇인지 조회하는 것만을 위해 추가 프로세스를 생성하거나(이 PEP의 대안인 PEP 516의 초기 초안에 포함되었던 방식입니다), 아니면 자동 협상을 완전히 포기해야 합니다(그 PEP의 현재 버전처럼). 이는 인터페이스에 어떤 변경이 생기든 그 변경이 적용되기 전에 N개의 개별 패키지가 각자의 pyproject.toml 파일을 업데이트해야 하며, 모든 변경이 필연적으로 새 릴리스로 제한된다는 뜻입니다.

이로 인한 구체적인 결과 중 하나는 이 PEP에서 prepare_metadata_for_build_wheel 명령을 선택 사항으로 만들 수 있다는 것입니다. 우리의 설계에서는 빌드 프런트엔드가 이를 손쉽게 처리할 수 있습니다. 프런트엔드는 서브프로세스 실행기에 다음과 같은 코드를 넣을 수 있습니다:

def dump_wheel_metadata(backend, working_dir):
    """Dumps wheel metadata to working directory.

       Returns absolute path to resulting metadata directory
    """
    if hasattr(backend, "prepare_metadata_for_build_wheel"):
        subdir = backend.prepare_metadata_for_build_wheel(working_dir)
    else:
        wheel_fname = backend.build_wheel(working_dir)
        already_built = os.path.join(working_dir, "ALREADY_BUILT_WHEEL")
        with open(already_built, "w") as f:
            f.write(wheel_fname)
        subdir = unzip_metadata(os.path.join(working_dir, wheel_fname))
    return os.path.join(working_dir, subdir)

def ensure_wheel_is_built(backend, output_dir, working_dir, metadata_dir):
    """Ensures built wheel is available in output directory

       Returns absolute path to resulting wheel file
    """
    already_built = os.path.join(working_dir, "ALREADY_BUILT_WHEEL")
    if os.path.exists(already_built):
        with open(already_built, "r") as f:
            wheel_fname = f.read().strip()
        working_path = os.path.join(working_dir, wheel_fname)
        final_path = os.path.join(output_dir, wheel_fname)
        os.rename(working_path, final_path)
        os.remove(already_built)
    else:
        wheel_fname = backend.build_wheel(output_dir, metadata_dir=metadata_dir)
    return os.path.join(output_dir, wheel_fname)

그렇게 하여 추가 서브프로세스 호출이나 중복 빌드 등 없이 프런트엔드의 나머지 부분에 완전히 균일한 인터페이스를 노출할 수 있습니다. 하지만 이는 분명히 프로젝트 내부의 비공개 인터페이스의 일부로서만 작성하고 싶은 종류의 코드입니다(예: 주어진 예제에서는 작업 디렉터리가 두 호출 사이에서는 공유되지만 다른 wheel 빌드와는 공유되지 않아야 하고, 메타데이터 헬퍼 함수의 반환값이 wheel 빌드 함수에 다시 전달되어야 합니다).

(그리고 물론 metadata 명령을 선택 사항으로 만드는 것은 앞서 논의한 것처럼 새 백엔드 개발의 진입 장벽을 낮추는 방법 중 하나입니다.)

그 밖의 차이점

앞서 설명한 명령줄 대 Python 훅이라는 핵심 차이 외에도, 이 제안에는 몇 가지 다른 차이점이 있습니다.

  • 메타데이터 명령은 선택 사항입니다(위에서 설명한 대로).
  • 메타데이터는 단일 메타데이터 파일(METADATA)이 아니라 디렉터리로 반환합니다. 이는 실제로 wheel 메타데이터가 여러 파일(예: 진입점)에 나뉘어 있는 방식과 더 잘 맞고, 앞으로 더 많은 선택지를 줍니다. (예를 들어 METADATA 형식을 JSON으로 바꾸자는 PEP 426 제안을 따르는 대신, 하위 호환성을 위해 기존 METADATA는 그대로 유지하면서 같은 디렉터리 안에 새 확장을 JSON “사이드카” 파일로 추가하기로 결정할 수도 있습니다. 아니면 그렇게 하지 않을 수도 있습니다. 요점은 선택지를 더 열어 둔다는 것입니다.)
  • 메타데이터 단계와 휠 빌드 단계 사이에 정보를 전달하는 메커니즘을 제공합니다. 아마 모두가 이것이 좋은 생각이라는 데 동의하지 않겠습니까?
  • 빌드 환경에 대해 더 자세한 권장 사항을 제공하지만, 어차피 이는 규범적인 내용이 아닙니다.