Youngkwang YangEnglish
목차

PEP 517 읽기: 빌드 시스템 독립 포맷

PEP 517은 “소스 트리를 빌드하는 방법”을 표준화한 규격이다.1 이 표준 덕분에 pip 같은 빌드 프런트엔드가 distutils나 setuptools에 묶이지 않고, 규격을 지키는 어떤 빌드 백엔드와도 동작한다. 아래는 그 관계의 뼈대다.

read requiresinstall intocall hooksreturn .whl / .tar.gzfrontendpip, buildpyproject.toml[build-system]isolated build envbackendhatchling, flit_core

무엇을 풀었나: distutils에 묶인 세계

PEP 517 이전에는 소스 트리를 빌드하는 표준 인터페이스가 없었다. 사실상 모두가 distutils와 setuptools에 의존했고, 여기서 세 가지 문제가 나왔다.

  1. 빌드타임 의존성을 선언할 방법이 없었다. 빌드에 필요한 도구를 미리 알릴 표준이 없어서, 빌드가 시작되고 나서야 없는 도구를 발견하곤 했다.
  2. 확장이 어려웠다. 조금만 특이한 요구가 생겨도 깨지기 쉬운 커스텀 코드를 붙여야 했다.
  3. 대체 빌드 시스템의 진입장벽이 높았다. 표준 인터페이스가 없으니 새 빌드 도구를 만들어도 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을 만든 뒤 메타데이터를 꺼낸다.

get_requires_for_build_wheel()optional: extra build depsfrontend installs themprepare_metadata_for_build_wheel()optional: .dist-info onlybuild_wheel()mandatory: writes the .whl

빌드 격리

프런트엔드는 기본적으로 빌드마다 격리된 환경을 만든다. 그 환경에는 표준 라이브러리와 명시적으로 선언된 빌드 의존성만 들어간다. 이렇게 하면 두 가지가 보장된다.

  1. 상반된 요구사항을 가진 패키지도 동시에 빌드할 수 있다. 각자 자기 환경을 쓰니 충돌하지 않는다.
  2. 의존성 선언을 정확하게 강제한다. 선언하지 않은 도구는 환경에 없으므로, 빠뜨리면 빌드가 실패한다. 즉 “내 머신에선 되던데”가 줄어든다.

훅은 새 서브프로세스에서 실행되고, 작업 디렉터리는 소스 트리의 루트다. 빌드 요구사항은 전부 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과 함께 오늘날의 파이썬 빌드 파이프라인을 이룬다. 도구 이름은 계속 바뀌어도, 그 아래 계약은 이 문서가 정한 그대로다.

Footnotes

  1. PEP 517, A build-system independent format for source trees

  2. PEP 518, Specifying Minimum Build System Requirements for Python Projects 2

  3. PEP 660, Editable installs for pyproject.toml based builds