PEP 517 읽기: 빌드 시스템 독립 포맷
PEP 517은 “소스 트리를 빌드하는 방법”을 표준화한 규격이다.1 이 표준 덕분에 pip 같은 빌드 프런트엔드가 distutils나 setuptools에 묶이지 않고, 규격을 지키는 어떤 빌드 백엔드와도 동작한다. 아래는 그 관계의 뼈대다.
무엇을 풀었나: distutils에 묶인 세계
PEP 517 이전에는 소스 트리를 빌드하는 표준 인터페이스가 없었다. 사실상 모두가 distutils와 setuptools에 의존했고, 여기서 세 가지 문제가 나왔다.
- 빌드타임 의존성을 선언할 방법이 없었다. 빌드에 필요한 도구를 미리 알릴 표준이 없어서, 빌드가 시작되고 나서야 없는 도구를 발견하곤 했다.
- 확장이 어려웠다. 조금만 특이한 요구가 생겨도 깨지기 쉬운 커스텀 코드를 붙여야 했다.
- 대체 빌드 시스템의 진입장벽이 높았다. 표준 인터페이스가 없으니 새 빌드 도구를 만들어도 pip 같은 도구가 그걸 부를 방법이 없었다.
PEP 517의 목표는 최소한의 표준 인터페이스 하나를 정하는 것이다. 그 인터페이스만 지키면 어떤 빌드 도구든 프런트엔드와 맞물린다.
프런트엔드와 백엔드로 나누다
핵심은 빌드를 두 역할로 쪼갠 것이다.
- 프런트엔드(pip,
build같은 도구): 격리된 빌드 환경을 만들고, 빌드 요구사항을 설치하고, 백엔드의 훅을 호출한다. 서브프로세스 실행과 프로세스 간 통신도 프런트엔드 몫이다. - 백엔드(hatchling, flit_core, setuptools 같은 라이브러리): 훅을 구현해서 실제로 wheel과 sdist를 만든다. 백엔드는 표준 라이브러리와 명시적으로 선언된 의존성만 있다고 가정하고 동작해야 한다.
프로젝트는 이 관계를 pyproject.toml의 [build-system] 테이블에 적는다. PEP
518이 requires(빌드에 필요한 패키지)를 도입했고,2 PEP 517이
build-backend(어떤 백엔드를 부를지)를 더했다.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
build-backend는 모듈:객체 문법으로 백엔드를 가리킨다(객체 부분은 생략 가능).
이 키나 pyproject.toml 자체가 없으면 도구는 예전 setup.py 방식으로 되돌아간다.
백엔드가 구현하는 훅
백엔드는 정해진 함수(훅)를 노출한다. 필수 훅은 둘, 나머지는 선택이다.
# 필수 (mandatory)
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): ...
def build_sdist(sdist_directory, config_settings=None): ...
# 선택 (optional)
def get_requires_for_build_wheel(config_settings=None): ...
def get_requires_for_build_sdist(config_settings=None): ...
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): ...
build_wheel은 지정한 디렉터리에 .whl 파일을 만들고 그 파일명을 돌려준다.
build_sdist는 .tar.gz 소스 배포본을 만든다. 이 둘만 있으면 빌드가 성립한다.
선택 훅은 최적화나 부가 정보를 위한 것이다. get_requires_for_build_wheel은
빌드에 추가로 필요한 의존성 목록을 미리 알려주고, prepare_metadata_for_build_wheel은
.dist-info 메타데이터만 먼저 만들어 준다(전체 wheel을 굽지 않고). 선택 훅이
없으면 프런트엔드는 정의된 기본 동작으로 대체한다. 예를 들어 메타데이터 훅이
없으면 그냥 build_wheel을 불러 wheel을 만든 뒤 메타데이터를 꺼낸다.
빌드 격리
프런트엔드는 기본적으로 빌드마다 격리된 환경을 만든다. 그 환경에는 표준 라이브러리와 명시적으로 선언된 빌드 의존성만 들어간다. 이렇게 하면 두 가지가 보장된다.
- 상반된 요구사항을 가진 패키지도 동시에 빌드할 수 있다. 각자 자기 환경을 쓰니 충돌하지 않는다.
- 의존성 선언을 정확하게 강제한다. 선언하지 않은 도구는 환경에 없으므로, 빠뜨리면 빌드가 실패한다. 즉 “내 머신에선 되던데”가 줄어든다.
훅은 새 서브프로세스에서 실행되고, 작업 디렉터리는 소스 트리의 루트다. 빌드
요구사항은 전부 import 가능해야 하고, 빌드 의존 패키지가 제공하는 명령줄
스크립트는 PATH에 있어야 한다.
config_settings와 in-tree 백엔드
두 가지 세부 장치를 알아두면 실전에서 쓸모가 있다.
config_settings: 모든 훅에 전달되는 임의의 딕셔너리다. 사용자가 그때그때 설정(예: 컴파일러 플래그)을 백엔드에 넘기는 통로다. 프런트엔드는 문자열 키와 값을 지정할 수단을 제공해야 하고, 같은 키가 여러 번 오면 리스트로 모인다.backend-path: 소스 트리 안에 든 백엔드 코드를 백엔드 로딩 시점에sys.path에 추가한다. 백엔드가 자기 자신을 빌드하는 self-hosting이나, 프로젝트 전용 백엔드 래퍼를 만들 때 쓴다. 경로는 프로젝트 루트 기준이며 소스 트리 밖으로 벗어나면 안 된다.
정리
PEP 517의 핵심은 프런트엔드와 백엔드 사이의 계약을 표준화한 것이다. 그 덕분에 hatchling, flit, pdm, setuptools, uv 같은 서로 다른 백엔드가 pip와 그대로 맞물린다. 소비자(프런트엔드)는 백엔드가 무엇이든 같은 훅만 부르면 되고, 생산자(백엔드)는 같은 훅만 구현하면 된다.
이 표준은 혼자 있지 않다. requires를 도입한 PEP 518,2 그리고 나중에
editable 설치용 훅(build_editable 등)을 더한 PEP 6603과 함께 오늘날의
파이썬 빌드 파이프라인을 이룬다. 도구 이름은 계속 바뀌어도, 그 아래 계약은 이
문서가 정한 그대로다.