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 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

distutilssetuptools가 우리에게 큰 진전을 가져왔지만, 세 가지 심각한 문제가 있습니다. 즉, (a) 사용 가능한 빌드 시 의존성 선언, 자동 구성, 심지어 DRY를 준수하는 버전 번호 관리와 같은 기본적인 사용 편의 기능도 포함한 중요한 기능이 빠져 있으며, (b) 이를 확장하기가 어려워서 위 문제에 대한 다양한 해결책이 존재하기는 하지만 대개 특이하고 불안정하며 유지 관리 비용이 많이 들고, 그럼에도 (c) 다른 것을 사용하기가 매우 어렵습니다. distutils/setuptools가 사용자와 pip같은 설치 도구 모두가 기대하는 패키지 설치용 표준 인터페이스를 제공하기 때문입니다.

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

이 PEP의 목표는 distutils-sig가 Python 빌드 시스템의 문지기 역할에서 벗어나도록 하는 것입니다. distutils를 사용하고 싶다면 그렇게 하십시오. 다른 것을 사용하고 싶다면 표준화된 방법으로 쉽게 사용할 수 있어야 합니다. distutils와의 인터페이스를 구성하기가 어렵기 때문에 현재 그러한 시스템은 많지 않지만, 우리가 무엇을 생각하고 있는지 감을 잡으려면 flit 또는 bento 를 살펴보십시오. 다행히 이제 휠이 여기서 발생하는 많은 어려운 문제를 해결했습니다. 예를 들어 빌드 시스템이 가능한 모든 설치 구성을 알고 있어야 할 필요가 더는 없습니다. 따라서 빌드 시스템에 실제로 필요한 것은 표준을 준수하는 휠과 sdist를 출력할 방법을 갖추는 것뿐입니다.

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

용어 및 목표

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

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

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

build frontend는 사용자가 실행할 수 있는 도구로, 임의의 소스 트리 또는 소스 배포판을 가져와 휠을 빌드합니다. 실제 빌드는 각 소스 트리의 build backend가 수행합니다. pip wheel some-directory/와 같은 명령에서 pip는 빌드 프런트엔드로 동작합니다.

integration frontend는 사용자가 실행할 수 있는 도구로, 패키지 요구 사항 집합(예: 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 키도 정의합니다. 아래의 “In-Tree Build Backends” 절을 참조하십시오. 이 키는 다음과 같이 사용합니다.:

[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가 필요한 식입니다). 이 그래프에는 순환이 있어서는 안 됩니다. (예를 들어 프로젝트 간 조정 부족으로 인해) 순환이 존재하면 프런트엔드는 프로젝트 빌드를 거부할 수 있습니다.
  • 빌드 요구 사항을 휠로 사용할 수 있는 경우, 프런트엔드는 깊게 중첩된 빌드를 피하기 위해 가능한 경우 이를 사용해야 합니다. 그러나 프런트엔드에는 빌드 요구 사항을 찾을 때 휠을 고려하지 않는 모드가 있을 수 있으므로, 프로젝트는 휠을 게시하는 것만으로 요구 사항 순환을 끊기에 충분하다고 가정해서는 안 됩니다.
  • 프런트엔드는 요구 사항 순환을 명시적으로 확인하고, 순환이 발견되면 유용한 메시지와 함께 빌드를 종료해야 합니다.

특히 요구 사항 순환이 없어야 한다는 요구 사항은 자체 호스팅을 원하며(즉, 백엔드용 휠을 빌드할 때 해당 백엔드를 빌드에 사용하는) 백엔드가 순환을 일으키지 않도록 특별한 조치를 취해야 한다는 것을 의미합니다. 일반적으로 여기에는 자체를 인트리 백엔드로 지정하고 외부 빌드 종속성을 피하는 작업(대개 이를 벤더링하는 방식)이 포함됩니다.

빌드 백엔드 인터페이스

빌드 백엔드 객체에는 다음 훅의 일부 또는 전부를 제공하는 속성이 있어야 합니다. 공통 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은 동일한 메타데이터를 가진 휠을 반드시 생성해야 합니다. 빌드 프런트엔드가 전달한 디렉터리는 prepare_metadata_for_build_wheel이 생성한 디렉터리와 반드시 동일해야 하며, 여기에는 해당 디렉터리가 생성한 인식되지 않는 파일도 포함됩니다.

prepare_metadata_for_build_wheel훅을 제공하지 않는 백엔드는 metadata_directorybuild_wheel의 매개변수로 조용히 무시하거나, 해당 값이 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 파일의 basename(전체 경로가 아님)을 유니코드 문자열로 반환해야 합니다.

패키지의 소스 파일을 포함하는 {name}-{version}(예: foo-1.0)라는 단일 최상위 디렉터리가 .tar.gz 소스 배포판(sdist)에 포함됩니다. 이 디렉터리에는 빌드 디렉터리의 pyproject.tomlPEP 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):
    ...

이 훅은 pyproject.toml 파일에 지정된 항목에 더하여, build_wheel또는 prepare_metadata_for_build_wheel훅을 호출할 때 설치할 PEP 508 종속성 사양을 포함하는 문자열의 추가 목록을 반환해야 합니다.

예:

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 디렉터리의 basename(전체 경로가 아님)을 유니코드 문자열로 반환해야 합니다.

빌드 프런트엔드에 이 정보가 필요하지만 해당 메서드가 정의되어 있지 않은 경우, build_wheel을 호출하고 결과 메타데이터를 직접 확인해야 합니다.

get_requires_for_build_sdist

def get_requires_for_build_sdist(config_settings=None):
    ...

이 훅은 pyproject.toml 파일에 지정된 것에 더해 PEP 508의 의존성 사양을 포함하는 문자열의 추가 목록을 반드시 반환해야 합니다. 이러한 의존성은 build_sdist 훅을 호출할 때 설치됩니다.

정의되어 있지 않으면, 기본 구현은 return []와 동등합니다.

Note

편집 가능 설치

이 PEP는 원래 편집 가능 설치(pip install -e와 같은 방식)를 수행하기 위한 또 다른 훅인 install_editable을 명시했습니다. 이 주제의 복잡성 때문에 제거되었지만, 이후 PEP에서 지정될 수 있습니다.

간단히 말해, 답해야 할 질문에는 기존에 ‘편집 가능한 설치’를 구현하는 합리적인 방법은 무엇인가가 포함됩니다. 편집 가능한 설치를 만드는 방법은 백엔드가 선택해야 합니까, 아니면 프론트엔드가 선택해야 합니까? 그리고 프론트엔드가 선택한다면, 그렇게 하기 위해 백엔드에서 무엇이 필요합니까?

구성 설정

config_settings

모든 훅에 전달되는 이 인자는 사용자가 개별 패키지 빌드에 임의의 구성을 전달할 수 있도록 제공되는 임의의 딕셔너리입니다. 빌드 백엔드는 이 딕셔너리에 원하는 의미를 할당할 수 있습니다. 빌드 프론트엔드는 사용자가 이 딕셔너리에 배치할 임의의 문자열 키/문자열 값 쌍을 지정할 수 있는 메커니즘을 제공해야 합니다. 예를 들어, --package-config CC=gcc와 같은 구문을 지원할 수 있습니다. 사용자가 중복된 문자열 키를 제공하는 경우, 빌드 프론트엔드는 해당 문자열 값들을 문자열 목록으로 결합해야 합니다. 빌드 프론트엔드는 사용자가 이 딕셔너리에 항목을 배치할 수 있도록 임의의 다른 메커니즘도 제공할 수 있습니다. 예를 들어, 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에 임의의 정보 텍스트를 출력할 수 있습니다. 훅은 stdin에서 읽어서는 안 되며, 빌드 프론트엔드는 훅을 호출하기 전에 stdin을 닫을 수 있습니다.

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

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

빌드 환경

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

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

  • 프로젝트의 빌드 요구 사항에 지정된 모든 요구 사항은 Python에서 임포트할 수 있어야 합니다. 특히 다음과 같습니다.
    • get_requires_for_build_wheelget_requires_for_build_sdist 후크는 pyproject.toml파일에 지정된 부트스트랩 요구 사항이 포함된 환경에서 실행됩니다.
    • prepare_metadata_for_build_wheelbuild_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"), ...])
    

빌드 백엔드는 위 기준을 충족하는 모든 환경에서 작동할 준비가 반드시 되어 있어야 합니다. 특히 표준 라이브러리에 존재하거나 빌드 요구 사항으로 명시적으로 선언된 패키지를 제외한 어떤 패키지에도 액세스할 수 있다고 가정해서는 안 됩니다.

프런트엔드는 각 후크를 새로운 서브프로세스에서 호출해야 합니다. 그러면 백엔드는 환경 변수나 작업 디렉터리와 같은 프로세스 전역 상태를 자유롭게 변경할 수 있습니다. 프런트엔드가 이러한 방식으로 후크를 쉽게 호출할 수 있도록 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==1.8을 무시하고 numpy==1.7에 맞춰 패키지를 빌드하여, 그 결과물이 이전 버전의 numpy와 C ABI 수준에서 호환되도록 보장하려 할 수도 있습니다. 이 경우 그 결과물은 업스트림에서 지원되지 않을 수 있습니다. 따라서 빌드 프런트엔드는 사용자가 위의 기본값을 재정의할 수 있는 메커니즘을 제공해야 합니다. 예를 들어 빌드 프런트엔드에는 빌드 환경을 생성할 때 --system-site-packages 옵션이 virtualenv 또는 이에 상응하는 도구에 전달되도록 하는 --build-with-system-site-packages 옵션이나, 프로젝트의 일반적인 빌드 요구 사항을 재정의하는 --build-requirements-override=my-requirements.txt 옵션이 있을 수 있습니다.

여기서의 일반 원칙은 패키지 작성자에게는 위생적인 관행을 강제하면서도, 필요한 경우 최종 사용자가 내부를 열어 보고 임시방편을 적용할 수 있도록 하자는 것입니다.

트리 내 빌드 백엔드

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

  • 백엔드 자체가 자체 빌드에 자신의 기능을 사용하고자 하는 경우(“자체 호스팅 백엔드”)
  • 일반적으로 표준 백엔드 주위의 사용자 지정 래퍼로 구성되며, 해당 래퍼가 너무 프로젝트에 특화되어 독립적으로 배포할 가치가 없는 경우(“인트리 백엔드”)

프로젝트는 pyproject.tomlbackend-path 키를 포함하여 백엔드 코드가 인트리에 호스팅된다고 지정할 수 있습니다. 이 키에는 디렉터리 목록이 포함되며, 프런트엔드는 백엔드를 로드하고 백엔드 훅을 실행할 때 해당 디렉터리를 sys.path의 맨 앞에 추가합니다.

backend-path 키의 내용에는 다음 두 가지 제한이 있습니다:

  • backend-path의 디렉터리는 프로젝트 루트를 기준으로 상대 경로로 해석되며, 소스 트리 내부의 위치를 가리켜야 합니다(상대 경로와 기호 링크가 해석된 후를 기준으로 합니다).
  • 백엔드 코드는 backend-path에 지정된 디렉터리 중 하나에서 로드되어야 합니다(즉, backend-path를 지정하면서 인트리 백엔드 코드를 갖지 않는 것은 허용되지 않습니다).

첫 번째 제한은 소스 트리가 자체적으로 완결된 상태를 유지하고 소스 트리 외부의 위치를 참조할 수 없도록 하기 위한 것입니다. 프런트엔드는 이 조건을 확인해야 하며(일반적으로 위치를 절대 경로로 해석하고 기호 링크를 해석한 다음 프로젝트 루트와 비교합니다), 조건을 위반하면 오류 메시지와 함께 실패해야 합니다.

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에 대한 새로운 제한 사항입니다:

  • 해당 파일은 .tar.gz 확장자를 사용하는 gzip 압축 tar 아카이브여야 합니다. Zip 아카이브나 tarball에 대한 기타 압축 형식은 현재 허용되지 않습니다.
  • Tar 아카이브는 파일 이름에 UTF-8을 사용하는 최신 POSIX.1-2001 pax tar 형식으로 생성해야 합니다.
  • sdist에 포함된 소스 트리에는 pyproject.toml 파일이 포함되어야 합니다.

발전에 관한 참고 사항

여기서의 목표는 구식 sdist를 신식 sdist로 최대한 간단하게 변환하는 것입니다. (예를 들어, 동적 빌드 요구 사항을 지원하는 것도 이러한 동기 중 하나입니다.) 이상적인 방법은 모든 “버전 0” VCS 체크아웃에 넣어 새로운 형식으로 변환할 수 있는 단일 정적 pyproject.toml을 만드는 것입니다. 이것이 100% 가능한 일은 아니겠지만, 이에 가깝게 만들 수 있으며 우리가 얼마나 근접했는지를 계속 추적하는 것이 중요합니다… 따라서 이 절이 존재합니다.

대략적인 계획은 어떤 훅 언어를 고안하더라도 이를 이해하고 setup.py 호출로 변환할 수 있는 빌드 시스템 패키지(setuptools_pypackage 또는 그와 유사한 것)를 만드는 것입니다. 이를 위해서는 필요할 때 setup_requires= 인자를 추출할 방법을 제공하고 새로운 형식으로 생성하는 sdist 명령의 새 버전을 제공하도록 setuptools에 일종의 후킹이나 몽키 패칭을 적용해야 할 것입니다. 이 모든 작업은 상당수 패키지에 대해 실현 가능하고 충분해 보입니다(물론 이 내용을 확정하기 전에 이러한 시스템의 프로토타입을 만들고 싶을 것입니다). (또는 이러한 변경 사항을 별도 패키지로 분리하지 않고 setuptools 자체에 적용할 수도 있습니다.)

그러나 패키지를 새로운 형식으로 자동 업그레이드하지 못하게 할 가능성이 높은 장애물이 두 가지 남아 있습니다:

  1. setup.py가 실행되기 전에 특정 패키지를 해당 환경에서 사용할 수 있어야 한다고 고집하는 패키지가 현재 존재합니다. 이는 격리된 virtualenv와 유사한 환경에서 빌드 스크립트를 실행하기로 결정할 경우, 프로젝트가 이러한 작업을 수행하는지 확인해야 하며, 수행한다면 새 시스템으로 업그레이드할 때 이러한 의존성을 명시적으로 선언하기 시작해야 한다는 의미입니다(setup_requires=를 통해 선언하거나 pyproject.toml에 정적으로 선언해야 합니다).
  2. 현재 일관된 메타데이터를 선언하지 않는 패키지도 존재합니다(예를 들어 egg_infobdist_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를 지지하는 주장을 기록합니다.

빌드 프런트엔드는 어떤 경우든 자식 프로세스 내부에서 훅을 실행하려 하기 때문에, 명령줄 인터페이스 대신 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에서 정의한 인터페이스 경계를 파이썬 코드로 옮김으로써, 우리는 세 가지 핵심 이점을 얻습니다.

첫째, 빌드 프론트엔드(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/에 wheel 메타데이터를 수동으로 설정해 두어야 하며, 버전 번호가 바뀌면 여러 곳에서 수동으로 갱신해야 합니다… 하지만 동작은 하며, 더 많은 기능을 점진적으로 추가할 수 있습니다. 많은 경험에 비추어 보면, 크게 성공한 프로젝트는 종종 간단한 해킹에서 출발하는 경우가 많습니다(예: 리눅스 – “그냥 취미일 뿐, 크고 전문적인 것이 되지는 않을 것”; IPython/Jupyter대학원생의 $PYTHONSTARTUP 파일), 따라서 우리의 목표가 훌륭한 빌드 도구들로 이루어진 활기찬 생태계의 성장을 장려하는 것이라면, 진입 장벽을 최소화하는 것이 중요합니다.

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

일반적으로 빌드 백엔드를 자신만의 프로세스로 격리해야 할 필요성 때문에 IPC의 복잡성을 완전히 없앨 수는 없습니다 – 하지만 IPC 채널의 양쪽을 모두 단일 프로젝트의 통제 아래 둠으로써, 생태계 전반에 걸친 조율된 합의와 조율된 변경이 필요한 경우에 비해 IPC 인터페이스의 버그를 수정하는 비용을 훨씬 낮출 수 있습니다.

셋째, 그리고 가장 결정적으로, 파이썬 훅 방식은 향후 이 명세를 진화시킬 훨씬 더 강력한 옵션을 제공합니다.

구체적인 예를 들자면, 내년에 새로운 build_sdist_from_vcs 훅을 추가한다고 상상해 봅시다. 이 훅은 기존의 build_sdist 훅에 대한 대안으로, 각 백엔드가 직접 버전 관리 추적 메타데이터를 조회하는 대신 프론트엔드가 그 정보(디스크상의 모든 파일이 추적되고 있는지 여부를 포함하여)를 백엔드에 전달하는 책임을 지도록 합니다. 전환 과정을 관리하기 위해서는, 빌드 프론트엔드가 build_sdist_from_vcs를 사용할 수 있을 때는 투명하게 사용하고 그렇지 않으면 build_sdist로 폴백할 수 있기를 원할 것이며, 빌드 백엔드는 옛 프론트엔드와 새 프론트엔드 모두와의 호환성을 위해 두 메서드를 모두 정의할 수 있기를 원할 것입니다.

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

여기서 설명한 인터페이스에서는, 이러한 목표를 모두 쉽게 달성할 수 있습니다. 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 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 명령을 선택적으로 만드는 것은 위에서 논의한 것처럼 새 백엔드 개발의 진입 장벽을 낮추는 방안 중 하나입니다.)

그 밖의 차이점

위에서 설명한 명령줄 방식과 파이썬 훅 방식 간의 핵심적인 차이 외에도, 이 제안에는 몇 가지 다른 차이점이 있습니다:

  • 메타데이터 명령은 (위에서 설명한 대로) 선택 사항입니다.
  • 우리는 메타데이터를 단일 METADATA 파일이 아니라 디렉터리 형태로 반환합니다. 이는 실제로 wheel 메타데이터가 여러 파일(예: 엔트리 포인트)에 걸쳐 분산되어 있는 방식과 더 잘 부합하며, 앞으로 더 많은 선택지를 제공합니다. (예를 들어, METADATA의 형식을 JSON으로 전환하자는 PEP 426의 제안을 따르는 대신, 하위 호환성을 위해 기존 METADATA를 그대로 유지하면서 같은 디렉터리 안에 JSON “사이드카” 파일 형태로 새로운 확장을 추가하는 방향으로 결정할 수도 있습니다. 아니면 그렇게 하지 않을 수도 있습니다. 핵심은 우리의 선택지를 더 열어 둔다는 점입니다.)
  • 우리는 메타데이터 단계와 wheel 빌드 단계 사이에 정보를 전달하는 메커니즘을 제공합니다. 아마 모두가 이것이 좋은 아이디어라는 데 동의할 것이라고 생각합니다.
  • 우리는 빌드 환경에 대해 더 상세한 권장 사항을 제공하지만, 이는 어쨌든 규범적인 것은 아닙니다.