PEP 518 – Python 프로젝트의 최소 빌드 시스템 요구 사항 지정
- Author:
- Brett Cannon <brett at python.org>, Nathaniel J. Smith <njs at pobox.com>, Donald Stufft <donald at stufft.io>
- BDFL-Delegate:
- Alyssa Coghlan
- Discussions-To:
- Distutils-SIG list
- Status:
- Final
- Type:
- Standards Track
- Topic:
- Packaging
- Created:
- 10-May-2016
- Post-History:
- 10-May-2016, 11-May-2016, 13-May-2016
- Resolution:
- Distutils-SIG message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 Python 소프트웨어 패키지가 자신이 선택한 빌드 시스템을 실행하는 데 필요한 빌드 의존성을 지정하는 방법을 규정합니다. 이 명세의 일부로, 소프트웨어 패키지가 빌드 의존성을 지정하는 데 사용할 새로운 설정 파일을 도입합니다(향후 다른 설정 세부 사항에도 같은 설정 파일을 사용할 것으로 예상합니다).
근거
Python이 처음 프로젝트용 소프트웨어 배포판을 빌드하는 도구를 개발했을 때는 distutils [1]가 선택된 해결책이었습니다. 시간이 지나면서 distutils 위에 몇 가지 기능을 추가한 setuptools [2]가 인기를 얻었습니다. 두 도구 모두 setup.py 파일이라는 개념을 사용했는데, 프로젝트 유지 관리자는 이 파일을 실행하여 소프트웨어의 배포를 빌드했습니다(사용자도 이 파일을 실행하여 해당 배포를 설치했습니다).
distutils는 Python 표준 라이브러리의 일부이므로, distutils에서 빌드 요구 사항을 지정하기 위해 실행 파일을 사용하는 것은 문제가 되지 않습니다. 빌드 도구가 Python의 일부라는 것은, 프로젝트 유지 관리자가 자기 프로젝트의 배포를 빌드할 때 setup.py에 관해 신경 써야 할 외부 의존성이 없다는 뜻입니다. 유일한 의존성이 Python이었으므로 의존성 정보를 지정할 필요가 전혀 없었습니다.
하지만 프로젝트가 setuptools를 사용하기로 하면 setup.py 같은 실행 파일을 사용하는 것이 문제가 됩니다. setup.py 파일은 그 의존성을 알지 못하면 실행할 수 없습니다. 하지만 현재로서는 해당 정보가 저장된 setup.py 파일을 실행하지 않고 그 의존성이 무엇인지 자동으로 알아낼 표준 방법이 없습니다. 파일은 자기 내용을 알지 못하면 실행할 수 없는데, 그 내용은 파일을 실행하지 않고는 프로그램으로 알아낼 수 없으므로 진퇴양난(catch-22)입니다.
Setuptools는 setup() 함수의 setup_requires 인자로 이 문제를 해결하려 했습니다 [3]. 이 해결책에는 다음과 같은 여러 문제가 있습니다.
- (setuptools 자신을 제외하면) 어떤 도구도
setup.py를 실행하지 않고는 이 정보에 접근할 수 없지만,setup.py는 이 항목들이 설치되어 있지 않으면 실행할 수 없습니다. - setuptools 자신은 여기에 나열된 것을 모두 설치하지만,
setup()함수가 실행되는 도중에야 설치됩니다. 따라서 여기에 추가한 것을 실제로 사용하는 유일한 방법은 이 모듈들의 임포트와 사용을setup()함수 실행 후반부까지 미루는, 점점 더 복잡한 술책뿐입니다. - 여기에는
setuptools자신도,setuptools의 대체품도 포함할 수 없습니다. 그래서numpy.distutils같은 프로젝트는 이 기능을 거의 활용할 수 없고, 프로젝트는 사용자가 setuptools를 자연스럽게 새 버전으로 업그레이드할 때까지 setuptools의 새 기능을 활용할 수 없습니다. setup_requires에 나열된 항목은setup.py를 실행할 때마다 암묵적으로 설치됩니다. 하지만setup.py는 흔히 이미 의존성을 관리하고 있는pip같은 다른 도구를 통해 실행됩니다. 그래서pip install spam같은 명령 하나로 pip와 setuptools가 모두 패키지를 내려받아 설치하게 될 수 있습니다. 이 경우 최종 사용자는 어느 저장소에서 설치할지 같은 설정을 바꾸려면 두 도구를 모두 설정해야 합니다(setuptools는 그 호출을 직접 제어할 수도 없는 상태에서 설정해야 합니다). 또한 한 도구가 다른 패키지 형식을 지원하거나 최신 버전을 다르게 판단할 수 있으므로, 사용자는 두 도구의 탐색 규칙을 모두 알고 있어야 합니다.
그 결과 setup_requires는 거의 쓰이지 않게 되었습니다. 프로젝트들은 setup.py 파일 사이에서 코드 조각을 그냥 복사해 붙여 넣거나, 아예 이 기능을 쓰지 않고 프로젝트를 빌드하거나 설치하기 전에 사용자가 직접 설치해 두어야 할 것을 다른 곳에 문서로 적어 두는 쪽을 택하는 경향이 있습니다.
이 모든 일 때문에 pip [4]는 setup.py 파일을 실행할 때 setuptools가 필요하다고 그냥 가정하게 되었습니다. 하지만 이 방식에는 다른 프로젝트가 setuptools처럼 커뮤니티에서 주목받기 시작하면 확장되지 않는다는 문제가 있습니다. 또한 setuptools가 아닌 다른 것이 필요하다는 사실을 pip가 추론할 수 없으면 그것을 프로젝트에서 사용하기가 번거로워지므로, 다른 프로젝트가 주목받기도 어려워집니다.
이 PEP는 프로젝트 빌드 시스템의 최소 의존성을 특정 파일에 선언적으로 나열하는 방법을 규정하여 이 상황을 바로잡고자 합니다. 이를 통해 프로젝트는 예를 들어 소스 체크아웃에서 휠까지 가는 데 필요한 빌드 의존성을 나열할 수 있으면서도, 프로젝트가 스스로를 빌드하는 데 무엇이 필요한지 도구가 추론할 수 없는 setup.py의 진퇴양난에 빠지지 않습니다. 이 PEP를 구현하면 프로젝트는 자신이 의존하는 빌드 시스템을 미리 지정할 수 있습니다. 그러면 pip 같은 도구는 프로젝트를 빌드하기 위해 빌드 시스템을 실행할 수 있도록 그 빌드 시스템이 설치되어 있는지 확인할 수 있습니다.
이 PEP의 맥락과 동기를 더 설명하기 위해, 프로젝트의 빌드 산출물을 만드는 데 필요한 (대략적인) 단계를 생각해 봅시다.
- 프로젝트의 소스 체크아웃.
- 빌드 시스템 설치.
- 빌드 시스템을 실행합니다.
이 PEP는 2단계를 다룹니다. PEP 517은 3단계를 다루며, 여기에는 빌드 시스템이 작업을 수행하는 데 필요한 추가 의존성을 빌드 시스템이 동적으로 지정하는 방법도 포함됩니다. 그러나 이 PEP의 목적은 빌드 시스템이 실행을 시작하는 데 필요한 최소한의 요구 사항을 지정하는 것입니다.
명세
파일 형식
빌드 시스템 의존성은 TOML 형식 [5]으로 작성된 pyproject.toml이라는 이름의 파일에 저장됩니다.
이 형식을 선택한 이유는 사람이 다루기 쉽고(JSON [6]과 달리), 충분히 유연하며(configparser [8]와 달리), 표준에서 비롯되었고(이 역시 configparser [8]와 달리), 지나치게 복잡하지 않기(YAML [7]과 달리) 때문입니다. TOML 형식은 이미 Rust 커뮤니티에서 Cargo 패키지 관리자 [12]의 일부로 사용되고 있으며, Rust 커뮤니티는 비공개 이메일에서 TOML을 선택한 것에 상당히 만족하고 있다고 밝혔습니다. 여러 대안을 선택하지 않은 이유에 대한 더 자세한 논의는 Other file formats 절에서 읽을 수 있습니다. 그러나 저자들은 설정 파일 형식의 선택이 궁극적으로 주관적이라는 점과 그럼에도 무언가는 선택해야 했다는 점을 알고 있으며, 이 상황에서는 TOML을 선호합니다.
아래에 도구가 인식하고 준수해야 하는 테이블을 나열합니다. 이 PEP에서 지정하지 않은 테이블은 향후 다른 PEP에서 사용하도록 예약되어 있습니다.
build-system 테이블
[build-system] 테이블은 빌드 관련 데이터를 저장하는 데 사용됩니다. 처음에는 이 테이블에서 단 하나의 키만 유효하며, 이 키는 테이블에 반드시 있어야 합니다. 그 키는 requires입니다. 이 키의 값은 빌드 시스템을 실행하는 데 필요한 PEP 508 의존성을 나타내는 문자열의 리스트여야 합니다(현재로서는 setup.py 파일을 실행하는 데 어떤 의존성이 필요한지를 의미합니다).
setuptools에 의존하는 대다수의 Python 프로젝트에서 pyproject.toml 파일은 다음과 같습니다:
[build-system]
# Minimum requirements for the build system to execute.
requires = ["setuptools"] # PEP 508 specifications.
현재 커뮤니티에서 setuptools가 매우 광범위하게 사용되고 있으므로, 빌드 도구는 pyproject.toml 파일이 없을 때 위의 예시 설정 파일을 기본 의미로 사용해야 합니다.
도구는 [build-system] 테이블의 존재를 요구해서는 안 됩니다. pyproject.toml 파일은 빌드 관련 데이터 이외의 설정 세부 사항을 저장하는 데 사용될 수 있으므로, 정당하게 [build-system] 테이블이 없을 수 있습니다. 파일은 존재하지만 [build-system] 테이블이 없다면 위에서 지정한 기본값을 사용해야 합니다. 테이블이 지정되었지만 필수 필드가 누락되었다면 도구는 이를 오류로 간주해야 합니다.
tool 테이블
[tool] 테이블은 빌드 도구뿐만 아니라 Python 프로젝트와 관련된 모든 도구가 [tool] 안의 하위 테이블을 사용하는 한 사용자에게 설정 데이터를 지정하게 할 수 있는 곳입니다. 예를 들어 flit 도구는 자신의 설정을 [tool.flit]에 저장합니다.
서로 다른 프로젝트가 같은 하위 테이블을 사용하려다 충돌하지 않도록, tool.* 네임스페이스 안에서 이름을 할당하는 메커니즘이 필요합니다. 우리의 규칙은, 프로젝트가 Cheeseshop/PyPI에서 $NAME에 대한 항목을 소유한 경우에만, 그리고 그 경우에 한해 하위 테이블 tool.$NAME을 사용할 수 있다는 것입니다.
JSON 스키마
오로지 설명을 위한 목적으로 TOML 파일에서 얻은 데이터를 타입별로 표현하자면, 다음 JSON 스키마 [13]가 이 데이터 형식에 부합합니다:
{
"$schema": "http://json-schema.org/schema#",
"type": "object",
"additionalProperties": false,
"properties": {
"build-system": {
"type": "object",
"additionalProperties": false,
"properties": {
"requires": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["requires"]
},
"tool": {
"type": "object"
}
}
}
기각된 아이디어
의미 버전 키
설정 파일 구조의 미래 대비를 위해 처음에는 semantics-version 키가 제안되었습니다. 이 아이디어는 기본값을 1로 하고, 이전에 정의된 키나 테이블의 의미에 하위 호환성이 없는 변경이 발생하면 semantics-version을 새로운 숫자로 증가시킨다는 것이었습니다.
하지만 결국 이는 섣부른 최적화라고 결정되었습니다. 설정 파일에서 의미적으로 미리 정의되는 내용의 변경은 상당히 보수적으로 이루어질 것으로 예상됩니다. 그리고 하위 호환성을 깨는 변경이 발생했을 경우에는, 기존 도구가 깨지지 않도록 새로운 의미에 다른 이름을 사용할 수 있습니다.
더 깊이 중첩된 네임스페이스
이 PEP의 초기 초안에는 최상위 [package] 테이블이 있었습니다. 이는 의미 버전 관리 체계에 일정한 범위를 부여하려는 의도였습니다(이 아이디어가 거부된 이유는 A semantic version key를 참조하십시오). 범위 지정의 필요성이 사라지면서 최상위 테이블을 둘 이유도 없어졌습니다.
다른 테이블 이름
[build-system] 테이블의 또 다른 이름으로 [build]가 제안되었습니다. 이 대안 이름은 더 짧지만, 테이블에 어떤 정보가 저장되는지에 대한 의도를 그만큼 잘 전달하지 못합니다. distutils-sig 메일링 리스트에서 투표한 결과 현재 이름이 채택되었습니다.
다른 파일 형식
여러 다른 파일 형식이 검토 대상으로 제시되었지만, 여러 가지 이유로 모두 거부되었습니다. 핵심 요구 사항은 사람이 편집할 수 있는 형식이어야 하며, 프로젝트가 쉽게 벤더링할 수 있는 구현이 있어야 한다는 것이었습니다. 이로 인해 XML처럼 사람에게 친화적이지 않은 특정 형식은 처음부터 제외되었으며, 이러한 형식은 진지하게 논의된 적도 없습니다.
검토한 파일 형식 개요
다른 대안들을 기각한 주요 이유는 다음 절들에 요약되어 있으며, 전체 검토 내용(TOML을 지지하는 긍정적인 인자 포함)은 [14]에서 확인할 수 있습니다.
TOML은 우리가 원했던 모든 기능을 제공하면서도 다른 대안들이 가진 단점을 피할 수 있었기 때문에 최종적으로 선택되었습니다.
| 기능 | TOML | YAML | JSON | CFG/INI |
|---|---|---|---|---|
| 명확한 정의 | 예 | 예 | 예 | |
| 실제 데이터 타입 | 예 | 예 | 예 | |
| 신뢰할 수 있는 유니코드 | 예 | 예 | 예 | |
| 신뢰할 수 있는 주석 | 예 | 예 | ||
| 사람이 편집하기 쉬움 | 예 | ?? | ?? | |
| 도구로 편집하기 쉬움 | 예 | ?? | 예 | ?? |
| 표준 라이브러리에 포함 | 예 | 예 | ||
| pip이 벤더링하기 쉬움 | 예 | 해당 없음 | 해당 없음 |
(표의 “??”는 대부분의 사람이 “예”라고 답하고 싶어 할 항목이지만, 명확한 명세가 없거나 기반이 되는 파일 형식 명세가 의외로 복잡해서 실제로는 특이한 점과 예외 상황이 많이 발생하는 항목을 나타냅니다)
pytoml TOML 파서는 순수 Python 코드 약 300줄이므로, 표준 라이브러리 밖에 있다는 점이 크게 불리하게 작용하지 않았습니다.
Python 리터럴도 후보 형식으로 논의되었지만, 파일 형식 검토에서는 고려되지 않았습니다(널리 쓰이는 기존 파일 형식이 아니기 때문입니다).
JSON
JSON 형식 [6]은 처음에 고려되었지만 곧 기각되었습니다. 사람이 읽을 수 있는 문자열 기반 데이터 교환 형식으로서는 훌륭하지만, 그 문법은 사람이 쉽게 편집하기에 적합하지 않습니다(예를 들어 문법이 필요 이상으로 장황하면서도 주석을 허용하지 않습니다).
제안된 데이터에 대한 JSON 파일의 예는 다음과 같습니다:
{
"build": {
"requires": [
"setuptools",
"wheel>=0.27"
]
}
}
YAML
YAML 형식 [7]은 JSON [6]의 상위 집합이면서 사람이 직접 다루기 더 쉽도록 설계되었습니다. YAML에는 세 가지 주요 문제가 있습니다.
첫째는 명세가 방대하다는 점입니다. 레터 크기 용지에 인쇄하면 86페이지에 달합니다. 이 때문에 누군가 한 파서에서는 동작하지만 다른 파서에서는 동작하지 않는 YAML 기능을 사용할 가능성이 생깁니다. 부분 집합으로 표준화하자는 제안도 있었지만, 이는 사실상 이 파일 전용의 새로운 표준을 만드는 것을 의미하며 장기적으로 감당할 수 없습니다.
두 번째 문제는 YAML 자체가 기본적으로 안전하지 않다는 점입니다. YAML 명세는 임의의 코드 실행을 허용하는데, 설정 데이터를 다룰 때는 이를 피하는 것이 가장 좋습니다. 물론 이러한 동작을 피할 수는 있습니다. 예를 들어 PyYAML은 safe_load 연산을 제공합니다. 하지만 어떤 도구가 부주의하게 load를 대신 사용하면, 그 도구는 임의의 코드 실행에 노출됩니다. 이 PEP는 본질적으로 코드 실행을 수반하는 프로젝트 빌드에 초점을 맞추고 있지만, 언젠가는 프로젝트 이름이나 버전 번호 같은 다른 설정 데이터도 같은 파일에 들어갈 수 있으며, 그러한 데이터에는 임의의 코드 실행이 바람직하지 않습니다.
마지막으로, 가장 널리 쓰이는 YAML의 Python 구현은 PyYAML [9]이며, 이는 수천 줄의 코드와 선택적인 C 확장 모듈로 이루어진 큰 프로젝트입니다. 그 자체로 반드시 문제가 되는 것은 아니지만, pip 같은 프로젝트에서는 더 큰 문제가 됩니다. 이러한 프로젝트는 완전히 자체적으로 완결되려면 PyYAML을 의존성으로 벤더링해야 할 가능성이 매우 높기 때문입니다(그렇지 않으면 설치 도구가 동작하는 데 또 다른 설치 도구가 필요해집니다). 더 단순한 버전의 라이브러리를 벤더링하는 것이 얼마나 쉬울지 알아보기 위해 PyYAML을 다시 작성하는 개념 증명이 진행되었으며, 그 결과 이것이 가능하다는 것을 확인했습니다.
YAML 파일의 예는 다음과 같습니다:
build:
requires:
- setuptools
- wheel>=0.27
configparser
configparser [8]가 받아들이는 형식을 기반으로 한 INI 스타일 설정 파일도 검토되었습니다. 안타깝게도 configparser가 무엇을 받아들이는지에 대한 명세가 없어서, 버전마다 지원 범위가 달라집니다. 예를 들어 Python 2.7의 ConfigParser가 받아들이는 것은 Python 3의 configparser가 받아들이는 것과 같지 않습니다. Python 3가 받아들이는 것을 표준으로 삼고 configparser 모듈의 백포트를 벤더링하면 된다고 생각할 수도 있지만, 그렇게 하면 이 PEP가 명시하는 메타데이터를 사용하려는 모든 프로젝트가 configparser의 백포트를 사용해야 한다는 내용을 이 PEP에 명문화해야 합니다. 이는 지나치게 제한적이며, 특정 버전의 configparser가 요구된다는 사실을 모르는 사람에게 혼란을 줄 수 있습니다.
INI 파일의 예는 다음과 같습니다:
[build]
requires =
setuptools
wheel>=0.27
Python 리터럴
Python 리터럴을 설정 형식으로 사용하자는 제안도 있었습니다. 파일의 최상위 수준에는 dict 하나가 있고, 모든 데이터는 그 dict 안에 들어가며, 섹션은 키로 정의됩니다. 모든 Python 프로그래머가 이 형식에 익숙할 것이고, 설정 데이터를 읽는 데 서드파티 의존성이 자연히 필요 없으며, ast.literal_eval() [11]로 파싱하면 안전할 수 있습니다. Python 리터럴은 JSON과 똑같이 쓸 수 있으면서도, 후행 쉼표와 주석을 지원한다는 이점이 더해집니다. 또한 Python의 더 풍부한 데이터 모델은 앞으로 생길 일부 설정 요구 사항(예: 문자열이 아닌 dict 키, 부동 소수점 값과 정수 값의 구분)에 유용할 수 있습니다.
반면에 Python 리터럴은 Python 전용 형식이며, 이 데이터는 Python으로 작성되지 않은 패키징 도구 등에서도 읽어야 할 수 있을 것으로 예상됩니다.
제안된 데이터를 Python 리터럴 파일로 나타낸 예는 다음과 같습니다:
# The build configuration
{"build": {"requires": ["setuptools",
"wheel>=0.27", # note the trailing comma
# "numpy>=1.10" # a commented out data line
]
# and here is an arbitrary comment.
}
}
setup.cfg를 계속 사용하기
setuptools가 사용하는 setup.cfg를 범용 형식으로 쓰는 데에는 두 가지 문제가 있습니다. 하나는 이 파일이 .ini 파일이어서 위의 configparser 논의에서 언급한 문제를 그대로 안고 있다는 점입니다. 다른 하나는 이 파일의 스키마가 한 번도 엄밀하게 정의된 적이 없어서, 앞으로 기존 setuptools 설치에 혼란을 줄 위험 없이 안전하게 사용할 수 있는 형식이 무엇인지 알 수 없다는 점입니다.
다른 파일 이름
다른 파일 이름도 여러 개 검토되었으나 채택되지 않았습니다(다만 이는 전형적인 자전거 보관소 논쟁(bikeshedding) 주제여서, 결정은 대부분 취향의 문제로 귀결됩니다).
- pysettings.toml
- 가장 합리적인 대안입니다.
- pypa.toml
- PyPA [10]를 참조하는 것은 타당하지만, 다소 틈새 용어입니다. 도메인 특화 지식이 없어도 파일 이름만으로 의미를 알 수 있는 편이 더 낫습니다.
- pybuild.toml
- 이 PEP의 제한적인 관점에서는 이 파일 이름이 타당하지만, 빌드와 관련 없는 메타데이터가 파일에 추가되는 순간 이름이 더 이상 타당하지 않게 됩니다.
- pip.toml
- 특정 도구에 지나치게 한정됩니다.
- meta.toml
- 너무 일반적입니다. 프로젝트가 자체 메타데이터 파일을 두고 싶어 할 수 있습니다.
- setup.toml
setup.py덕분에 전통을 따르기는 하지만, 앞으로 파일에 담길 수 있는 내용과 반드시 들어맞지는 않습니다(예: 프로젝트의 이름을 아는 것이 본질적으로 설정의 일부입니까?).- pymeta.toml
- 프로그래밍이나 Python을 처음 접하는 사람에게는 의미가 명확하지 않습니다.
- pypackage.toml & pypackaging.toml
- 이름이 “패키지”가 무엇인지(프로젝트인지 네임스페이스인지)를 혼동하게 만듭니다.
- pydevelop.toml
- 파일에 개발에 국한되지 않는 세부 사항이 담길 수 있습니다.
- pysource.toml
- 소스 코드와 직접적인 관련이 없습니다.
- pytools.toml
- 파일이 (현재) 프로젝트 관리를 목적으로 하므로 오해의 소지가 있습니다.
- dstufft.toml
- 특정 인물에 지나치게 한정됩니다. ;)
참고 문헌
Copyright
This document has been placed in the public domain.