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

Python 개선 제안 한국어 번역

PEP 725 – pyproject.toml에 외부 의존성 명시하기

Author:
Pradyun Gedam <pradyunsg at gmail.com>, Jaime Rodríguez-Guerra <jaime.rogue at gmail.com>, Ralf Gommers <ralf.gommers at gmail.com>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Topic:
Packaging
Created:
17-Aug-2023
Post-History:
18-Aug-2023, 22-Sep-2025

Table of Contents

번역·라이선스 안내

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

초록

이 PEP는 패키징 관련 도구가 사용할 수 있도록 프로젝트의 외부(즉, PyPI에 없는) 빌드 및 런타임 의존성을 pyproject.toml 파일에 작성하는 방법을 명세합니다.

이 PEP는 일곱 개의 키를 가진 [external] 테이블을 pyproject.toml에 추가할 것을 제안합니다. “build-requires”, “host-requires”, “dependencies”는 세 가지 유형의 필수 의존성을 명시하기 위한 것입니다:

  1. build-requires, 빌드 머신에서 실행할 빌드 도구
  2. host-requires, 호스트 머신에 필요하면서 빌드 시점에도 필요한 빌드 의존성
  3. dependencies, 호스트 머신에서 런타임에 필요하지만 빌드 시점에는 필요하지 않은 의존성

이 세 키에는 각각에 대응하는 선택적 external 키(optional-build-requires, optional-host-requires, optional-dependencies)도 있으며, 이들은 project.optional-dependencies가 project.dependencies에 대해 하는 것과 같은 역할을 합니다. 마지막으로, dependency-groups는 PEP 735와 같은 기능을 외부 의존성에 대해 제공합니다.

빌드 의존성과 호스트 의존성을 구분하여 크로스 컴파일을 고려합니다. 선택적 빌드 시점 의존성과 런타임 의존성도 [project] 테이블에서 지원하는 방식과 유사한 방식으로 지원합니다.

동기

Python 패키지는 빌드 도구, 라이브러리, 명령줄 도구 또는 PyPI에 없는 기타 소프트웨어에 대한 의존성을 가질 수 있습니다. 현재는 이러한 의존성을 표준화된 메타데이터로 표현할 방법이 없습니다 [1], [2]. 이 PEP의 주요 동기는 다음과 같습니다:

  • 도구가 외부 의존성을 다른 패키징 저장소의 패키지에 자동으로 매핑할 수 있도록 합니다.
  • Python 패키지 설치 프로그램과 빌드 프런트엔드가 내보내는 오류 메시지에 필요한 의존성을 포함할 수 있게 합니다.
  • 패키지 작성자가 이러한 의존성 정보를 기록할 표준 위치를 제공합니다.

Linux 배포판, conda, Homebrew, Spack, Nix와 같은 패키징 생태계는 Python 패키지의 전체 의존성 집합이 필요하며, 업스트림 Python 패키지의 메타데이터로부터 자체 패키지 관리자용 의존성 메타데이터를 자동으로 생성하려고 시도하는 pyp2spec (Fedora), Grayskull (conda), dh_python (Debian)과 같은 도구를 갖추고 있습니다. pyproject.toml이나 다른 어떤 표준 위치에도 이를 위한 메타데이터가 없기 때문에, 현재 외부 의존성은 수작업으로 처리됩니다. elfdeps (Fedora)처럼 Python 패키지 내부의 확장 모듈과 공유 라이브러리에서 의존성을 추출하는 방식에 의존하는 도구도 있습니다. 명시적으로 기재된 메타데이터만 사용하여 이러한 유형의 변환을 자동화할 수 있게 하는 것이 이 PEP의 핵심 이점이며, 이를 통해 배포판용 Python 패키지 패키징이 더 쉬워지고 더 안정적이 됩니다. 또한 저자들은 Repology, Dependabot, libraries.io 같은 의존성 분석 도구 등 다른 유형의 도구들도 이 정보를 활용할 것으로 예상합니다.

소프트웨어 자재 명세서(SBOM) 생성 도구도 이 정보를 활용할 수 있을 것입니다. 예를 들어 pyproject.toml에는 나열되어 있지만 휠 메타데이터에는 포함되지 않은 외부 의존성이 휠 안에 벤더링되어 있을 가능성이 높다고 표시하는 데 사용할 수 있습니다. 휠에 SBOM을 포함하는 방식을 표준화하는 PEP 770에는 해당 PEP가 이 PEP와 어떻게 다른지 설명하는 유익한 절이 있습니다.

외부 의존성이 있는 패키지는 일반적으로 소스에서 빌드하기 어렵고, 빌드 실패 시의 오류 메시지는 최종 사용자가 이해하기 어려운 경향이 있습니다. 최종 사용자의 시스템에 외부 의존성이 누락된 것이 빌드 실패의 가장 유력한 원인입니다. 설치 프로그램이 오류 메시지의 일부로 필요한 외부 의존성을 보여 줄 수 있다면, 사용자의 시간을 크게 절약할 수 있습니다.

현재 외부 의존성에 대한 정보는 개별 패키지의 설치 문서에만 기록되어 있습니다. 이러한 정보는 패키지 작성자가 유지 관리하기 어렵고 오래되어 최신 상태와 맞지 않게 되기 쉽습니다. 또한 사용자와 배포판 패키저가 찾기도 어렵습니다. 이러한 의존성 정보를 기록할 표준 위치가 생기면 이 상황이 개선될 것입니다.

이 PEP는 외부 의존성을 어떻게 사용해야 하는지 규정하려는 것이 아니며, PyPI에 게시된 Python 프로젝트에서 정식 이름으로 쓰이는 개별 패키지 이름을 다른 패키징 생태계의 이름으로 매핑하는 메커니즘을 규정하려는 것도 아닙니다. 정식 이름과 이름 매핑 메커니즘은 PEP 804에서 다룹니다.

근거

외부 의존성의 유형

외부 의존성은 여러 유형으로 구분할 수 있습니다:

  • 이름으로 식별할 수 있고, 다른 언어별 패키지 저장소에 정식 위치가 있는 구체적인 패키지입니다. 예를 들어 crates.io의 Rust 패키지, CRAN의 R 패키지, npm 레지스트리의 JavaScript 패키지가 있습니다.
  • 이름으로 식별할 수 있지만 명확한 정식 위치가 없는 구체적인 패키지입니다. 일반적으로 C, C++, Fortran, CUDA 및 기타 저수준 언어로 작성된 라이브러리와 도구가 여기에 해당합니다. 예를 들어 Boost, OpenSSL, Protobuf, Intel MKL, GCC가 있습니다.
  • 개념, 도구의 유형 또는 인터페이스를 가리키는 이름인 “가상” 패키지입니다. 이러한 패키지에는 일반적으로 여러 구현이 있으며, 그 구현들은 구체적인 패키지입니다. 예를 들어 C++ 컴파일러, BLAS, LAPACK, OpenMP, MPI가 있습니다.

구체적인 패키지는 이해하기 쉬우며, 모든 패키지 관리 시스템에 존재하는 개념입니다. 가상 패키지도 여러 패키징 시스템에 존재하는 개념이지만, 모든 시스템에 있는 것은 아니며 구현 세부 사항도 시스템마다 다릅니다.

크로스 컴파일

크로스 컴파일은 아직(2025년 9월 기준) 표준 라이브러리 모듈과 pyproject.toml 메타데이터에서 잘 지원되지 않습니다. 그러나 크로스 컴파일은 (pyp2spec과 같은 도구를 사용하여) 외부 의존성을 다른 패키징 시스템의 의존성으로 변환할 때 중요합니다. 향후 [external]을 확장하는 것보다 이 PEP에서 곧바로 크로스 컴파일 지원을 도입하는 것이 훨씬 쉬우므로, 저자들은 지금 이를 포함하기로 했습니다.

용어

이 PEP에서는 다음 용어를 사용합니다:

  • 빌드 머신: 패키지 빌드 과정이 실행되는 머신입니다.
  • 호스트 머신: 생성된 결과물이 설치되고 실행될 머신입니다.
  • 빌드 의존성: 빌드 과정에서만 필요한 패키지입니다. 빌드 시점에 사용할 수 있어야 하며, 빌드 머신의 OS와 아키텍처에 맞게 빌드됩니다. 대표적인 예로는 컴파일러, 코드 생성기, 빌드 도구가 있습니다.
  • 호스트 의존성: 빌드 중에 필요하며 런타임에도 필요한 경우가 많은 패키지입니다. 빌드 중에 사용할 수 있어야 하며, 호스트 머신의 OS와 아키텍처에 맞게 빌드됩니다. 보통 프로젝트가 링크하는 라이브러리가 여기에 해당합니다.
  • 런타임 의존성: 설치 후 패키지를 사용할 때만 필요한 패키지입니다. 빌드 시점에는 필요하지 않지만, 런타임에 호스트 머신에서 사용할 수 있어야 합니다.

이 용어는 빌드 도구와 패키징 도구마다 일관되지 않으므로, pyproject.toml의 빌드/호스트 의존성을 다른 패키지 관리자의 의존성과 비교할 때는 주의해야 합니다.

이 PEP에서는 “타깃 머신”이나 “타깃 의존성”이라는 용어를 사용하지 않는다는 점에 유의하십시오. 이러한 용어는 일반적으로 컴파일러를 크로스 컴파일하는 경우나 그 밖의 고급 시나리오에서만 의미가 있으며 [3], [4], 이는 이 PEP의 범위를 벗어납니다.

마지막으로, 빌드 시점에 필요한 패키지를 가리키는 용어로는 “dependency”가 가장 널리 쓰이지만, pyproject.toml에서 PyPI 빌드 시점 의존성을 나타내는 기존 키는 build-requires라는 점에 유의하십시오. 따라서 이 PEP는 일관성을 위해 [external] 아래에 build-requires 키와 host-requires 키를 사용합니다.

빌드 의존성과 호스트 의존성

빌드 플랫폼과 호스트 플랫폼이 항상 같다고 가정하는 대신, 빌드 플랫폼과 호스트 플랫폼의 정의와 관련된 메타데이터를 명확하게 분리하는 것이 중요합니다 [5].

빌드 의존성은 일반적으로 빌드 과정 중에 실행되며, 컴파일러, 코드 생성기 또는 그 밖의 도구일 수 있습니다. 빌드 의존성을 사용하는 것이 런타임 의존성을 수반하는 경우, 그 런타임 의존성을 명시적으로 선언할 필요는 없습니다. 예를 들어 gfortran으로 Fortran 코드를 컴파일하여 Python 확장 모듈을 만드는 경우, 그 패키지는 libgfortran 런타임 라이브러리에 대한 의존성을 갖게 될 가능성이 높습니다. 이러한 런타임 의존성을 명시적으로 나열하지 않는 근거는 두 가지입니다. (1) 해당 의존성이 존재하는지 여부는 컴파일러/링커 플래그나 빌드 환경의 세부 사항에 따라 달라질 수 있으며, (2) 이러한 런타임 의존성은 auditwheel 같은 도구로 자동으로 탐지하고 처리할 수 있습니다.

호스트 의존성은 일반적으로 빌드 과정 중에 실행되지 않으며, 링크 대상으로만 사용됩니다. 다만 이것이 규칙은 아닙니다. 호스트 의존성을 에뮬레이터에서 실행하거나 crossenv 같은 사용자 정의 도구를 통해 실행하는 것이 가능하거나 필요할 수도 있습니다. 호스트 의존성이 런타임 의존성을 수반하는 경우에도, 빌드 의존성과 마찬가지로 그 런타임 의존성을 선언할 필요는 없습니다.

호스트 의존성이 선언되어 있고 도구가 크로스 컴파일과 관련 없는 작업을 실행하는 경우, 그 도구는 host-requires 목록을 build-requires에 병합하기로 결정할 수 있습니다. 이것이 유용한지는 상황에 따라 다릅니다.

외부 의존성 지정하기

구체적인 패키지 명세

“Package URL” 또는 PURL은 패키지를 식별하는 데 널리 사용되는 URL 문자열로, 여러 패키징 생태계에 걸쳐 이식 가능하도록 설계되었습니다. 그 설계는 다음과 같습니다.:

scheme:type/namespace/name@version?qualifiers#subpath

scheme 구성 요소는 고정된 문자열인 pkg이며, 나머지 구성 요소 중에서는 type과 name만 필수입니다.

외부 의존성은 손으로 직접 입력될 가능성이 높으므로, 사용 편의성과 사용자 친화성을 위해 여러 가지 변경 사항(아래에서 자세히 설명합니다)을 도입한 PURL 파생 형식을 제안합니다.

  • 새로운 virtual 타입을 통해 가상 패키지를 지원합니다.
  • version 필드에 (리터럴뿐만 아니라) 버전 범위를 허용합니다.

이 파생 형식에서는 pkg 스킴을 dep으로 대체합니다. 따라서 이를 DepURL이라고 부르겠습니다.

예를 들어 PyPI의 requests 패키지에 대한 DepURL은 다음과 같습니다.:

dep:pypi/requests
# equivalent to pkg:pypi/requests

pyproject.toml에서 외부 의존성을 지정하는 데 PURL과 호환되는 문자열을 채택하면 여러 문제를 한 번에 해결할 수 있으며, 이미 Python을 비롯한 여러 언어로 된 명세 구현이 존재합니다. PURL은 이미 SPDX(External Repository Identifiers in the SPDX 2.3 spec 참고), Open Source Vulnerability format, Sonatype OSS Index 같은 의존성 관련 도구에서도 지원되고 있습니다. 이러한 도구에서 지원이 이루어지기까지 몇 년씩 기다릴 필요가 없다는 점은 가치가 있습니다. DepURL은 PURL로 매우 쉽게 변환할 수 있으며, 예외는 PURL에 대응하는 것이 없는 dep:virtual뿐입니다.

참조할 정규 패키지 관리자가 없는 구체적인 패키지의 경우, dep:generic/dep-name을 사용하거나 해당 패키지가 관리되는 VCS 시스템을 직접 참조할 수 있습니다(예: dep:github/user-or-org-name/dep-name). 이 중 어느 쪽이 더 적절한지는 상황에 따라 다릅니다. 이 PEP는 패키지 이름이 모호하지 않고 널리 알려진 경우에는 dep:generic을 사용하고(예: dep:generic/git 또는 dep:generic/openblas), 그렇지 않은 경우에는 VCS를 타입으로 사용할 것을 권장합니다. 특정 패키지에 대해 어떤 이름을 정규 이름으로 선택할지, 그리고 그러한 선택을 내리고 기록하는 절차는 PEP 804의 주제입니다.

가상 패키지 명세

PURL은 아직 가상 패키지나 가상 의존성 명세를 지원하지 않습니다. 개정판 1.1을 위해 가상 타입을 추가하자는 제안이 논의되고 있습니다.

그동안 저희는 dep: 파생 형식에 새로운 타입인 virtual 타입을 추가할 것을 제안합니다. 이 타입은 두 가지 네임스페이스를 가질 수 있습니다(PEP 804에 주어진 절차를 통해 확장할 수 있습니다):

  • interface: BLAS나 MPI 같은 구성 요소에 사용합니다.
  • compiler: C나 Rust 같은 컴파일 언어에 사용합니다.

이름은 해당 인터페이스나 언어를 가리키는 가장 일반적인 이름을 소문자로 쓴 것이어야 합니다. 몇 가지 예는 다음과 같습니다.:

dep:virtual/compiler/c
dep:virtual/compiler/cxx
dep:virtual/compiler/rust
dep:virtual/interface/blas
dep:virtual/interface/lapack

이러한 의존성의 수는 한정되어 있으므로, 이해하기 쉬우며 가상 패키지를 갖춘 Linux 배포판과 conda, Spack 같은 시스템에 잘 대응될 것으로 보입니다.

버전 관리

PURL은 URL의 @ 구성 요소를 통해 고정 버전을 지원합니다. 예를 들어 numpy===2.0은 pkg:pypi/numpy@2.0으로 표현할 수 있습니다.

PURL에서 고정 버전을 넘어서는 버전 표현식과 범위는 vers URI를 통해 지원됩니다(명세 참조):

vers:type/version-constraint|version-constraint|...

사용자는 pkg: URL과 vers: URL을 함께 사용해야 합니다. 예를 들어 numpy>=2.0을 표현하려면, PURL에서는 pkg:pypi/numpy에 vers:pypi/>=2.0을 더한 것이 이에 해당합니다. 이는 다음 방법으로 할 수 있습니다:

  • 두 항목으로 된 리스트: ["pkg:pypi/numpy", "vers:pypi/>=2.0"].
  • 퍼센트 인코딩된 URL 한정자: pkg:pypi/numpy?vers=vers:pypi%2F%3E%3D2.0.

이 방법들은 모두 그다지 사용하기 편하지 않으므로, 저희는 그 대신 DepURL이 PEP 440 의미론의 부분집합에 해당하는 의미론으로 버전 범위 지정자도 받아들이도록 했습니다. 허용되는 연산자는 여러 패키지 관리자에서 널리 제공되는 것들입니다(예를 들어 ==, >, >=는 흔하지만 ~=는 그렇지 않습니다).

몇 가지 예는 다음과 같습니다.

  • dep:pypi/numpy@2.0: 정확히 버전 2.0으로 고정된 numpy입니다.
  • dep:pypi/numpy@>=2.0: 버전이 2.0 이상인 numpy입니다.
  • dep:virtual/interface/lapack@>=3.7.1: 버전 3.7.1 이상의 LAPACK 인터페이스를 구현하는 모든 패키지입니다.

특정 가상 패키지의 버전 관리 체계는, 업스트림 프로젝트나 표준에 의해 명확하게 정의되지 않은 경우, 중앙 레지스트리에서 정의됩니다(PEP 804 참고).

환경 마커

일반적인 환경 마커(원래 PEP 508에서 정의된 것)는 DepURL 뒤에 사용할 수 있습니다. ? 뒤에 패키지 타입별 의존성 지정자 구성 요소를 붙이는 PURL 한정자는, 환경 마커로 충분한 용도에는 사용하지 않아야 합니다. 그 이유는 실용적인 것입니다. 환경 마커는 이미 pyproject.toml의 다른 메타데이터에 사용되고 있으므로, pyproject.toml과 함께 사용되는 도구라면 이를 파싱하는 견고한 구현을 이미 갖추고 있을 가능성이 높습니다. 또한 저희는 PURL 한정자가 제공하는 추가 기능(예를 들어 Conan이나 conda 채널, RubyGems 플랫폼을 지정하는 기능)이 필요하리라고 예상하지 않습니다.

기존 dependency specifiers와 유사하게, DepURL과 환경 마커의 조합을 “외부 의존성 지정자”라고 부릅니다.

의존성의 정규 이름과 -dev(el) 분할 패키지

배포판이 하나의 패키지를 두 개 이상의 패키지로 분할하는 경우는 꽤 흔하지만, 보편적인 것과는 거리가 멉니다. 특히 런타임 구성 요소는 개발 구성 요소(헤더, pkg-config 및 CMake 파일 등)와 별도로 설치할 수 있는 경우가 많습니다. 이 경우 후자는 일반적으로 프로젝트/라이브러리 이름 뒤에 -dev 또는 -devel이 붙은 이름을 가집니다. 또한 규모가 큰 패키지는 설치 크기를 관리 가능한 수준으로 유지하기 위해 여러 개의 별도 패키지로 분할되기도 합니다. 대개 이러한 패키지 분할은 패키지 유지 관리자가 정의하거나 인정한 것이 아니므로, 분할이 무엇을 의미하는지 모호합니다. 따라서 이러한 분할은 [external] 테이블에 반영되어서는 안 됩니다. 여러 배포판에 걸쳐 통용되는 합리적인 방식으로 이를 명시하는 것은 불가능하므로, [external]에는 정식 이름만 사용해야 합니다.

DepURL을 사용하는 것이 의도하는 의미는 “지정된 이름을 가진 패키지 전체”입니다. 즉, 해당 패키지에 속하는 설치 가능한 모든 아티팩트를 포함합니다. 패키지 분할이 관련이 있는지는 메타데이터가 사용되는 맥락에 따라 달라집니다. 예를 들어 libffi가 호스트 의존성이고 도구가 휠을 빌드하기 위한 환경을 준비하려는 경우, 배포판이 libffi의 헤더를 libffi-devel 패키지로 분리해 두었다면 도구는 libffi와 libffi-devel을 모두 설치해야 합니다.

정식 패키지 이름이 무엇인지, 그리고 도구가 설치 목적으로 [external]을 사용하려 할 때 패키지 분할을 실제로 어떻게 처리하는지에 대한 정의는 PEP 804를 참조하십시오.

Python 개발 헤더

Python 헤더와 기타 빌드 지원 파일도 분할될 수 있습니다. 이는 위 절과 같은 상황입니다(배포판에서 Python은 단순히 일반 패키지이기 때문입니다). 그러나, python-dev|devel 의존성은 특별한데, pyproject.toml에서 Python 자체는 명시적 의존성이 아니라 암묵적 의존성이기 때문입니다. 따라서 여기서는 python-dev를 암묵적으로 추가할지, 아니면 각 패키지 작성자가 [external] 아래에 이를 명시적으로 추가하도록 할지 선택해야 합니다. Python 의존성과 외부 의존성 간의 일관성을 위해, 이를 암묵적으로 추가하기로 합니다. [external] 테이블에 하나 이상의 컴파일러 패키지가 포함된 경우 Python 개발 헤더가 필요하다고 가정해야 합니다.

새로운 코어 메타데이터 필드

두 가지 새로운 코어 메타데이터 필드를 제안합니다.

  • Requires-External-Dep. 외부 요구 사항입니다. Requires에서 Requires-Dist로의 전환을 본뜬 것입니다. -Dep 접미사를 선택한 것은 그 값이 일반적인 Python 지정자(배포)가 아니라 DepURL을 포함하는 외부 의존성 지정자임을 강조하기 위해서입니다.
  • Provides-External-Extra. 외부 의존성(Requires-External-Dep에 있는 것)만을 담는 extra 그룹입니다.

코어 메타데이터 명세에는 pyproject.toml의 [build-system] 테이블에 있는 어떤 메타데이터에 대한 필드도 없으므로, build-requires와 host-requires 내용을 기존 코어 메타데이터 필드에 반영할 필요는 없습니다.

또한 이 PEP는 Requires-External 필드를 폐기할 것도 제안합니다. 그 이유는 다음과 같습니다:

  • 새로 제안된 필드와의 혼동을 방지합니다.
  • 기존 사용 사례와의 잠재적 비호환성을 방지합니다(사용이 제한적이더라도).
  • 생태계에서의 보급률이 낮습니다:
    • pyproject.toml 메타데이터에는 이 필드에 직접 대응하는 필드가 없습니다.
    • setuptools (pypa/setuptools#4220 참고), hatch (pypa/hatch#1712 참고), flit (pypa/flit#353 참고), poetry 같은 주류 빌드 백엔드는 이를 지정하는 방법을 제공하지 않거나 플러그인(예: poetry-external-dependencies)을 필요로 합니다. maturin은 0.7.0부터 이를 지원하는 것으로 보이지만(PyO3/maturin@5b0e4808 참고), 직접 문서화되어 있지는 않습니다. scikit-build-core나 meson-python 같은 다른 백엔드에서는 External-Requires에 대한 검색 결과가 없었습니다.
    • 이 필드는 PyPI JSON API responses에 포함되지 않습니다.

공유 라이브러리 벤더링이 휠 메타데이터에 미치는 영향

휠은 외부 의존성을 벤더링할 수 있습니다. 이는 특히 PyPI나 다른 Python 패키지 색인에 휠을 배포할 때 일어나며, auditwheel, delvewheel, delocate 같은 도구가 이 과정을 자동화합니다. 그 결과, sdist에 있던 Requires-External-Dep 항목이 cibuildwheel 같은 도구로 해당 sdist에서 빌드한 휠에서는 사라질 수 있습니다. 또한 Requires-External-Dep 항목이 변경되지 않은 채로, 또는 더 좁은 제약 조건으로 휠에 남아 있을 수도 있습니다. auditwheel은 기본적으로 OpenGL처럼 허용 목록에 있는 특정 의존성은 벤더링하지 않습니다. 게다가 auditwheel과 delvewheel은 사용자가 --exclude 또는 --no-dll 명령줄 플래그를 통해 의존성을 수동으로 제외할 수 있게 합니다. 이는 CUDA의 라이브러리처럼 큰 공유 라이브러리를 벤더링하지 않기 위해 사용됩니다.

따라서 pyproject.toml의 외부 의존성으로부터 생성된 Requires-External-Dep 항목은 빌드/배포 과정에 따라 sdist와 그에 대응하는 휠 사이에서 달라질 수 있습니다.

이것이 해당 필드를 Dynamic으로 표시해야 한다는 뜻은 아니라는 점에 유의하십시오. 이 구분은 빌드 백엔드가 sdist로부터 빌드한 휠에만 적용되기 때문입니다. 특히 다른 휠로부터 빌드된 휠은 이 제약 조건을 충족할 필요가 없습니다.

의존성 그룹

이 PEP는 PEP 735의 키인 dependency-groups를 [external] 테이블 아래에도 포함하기로 했습니다. 이 결정은 외부 메타데이터에도 비슷한 기능이 필요하다는 점에서 비롯되었습니다. 최상위 테이블은 PEP 508 문자열(그리고 그룹 포함을 위한 테이블)을 담을 것으로 기대되는 반면, 우리는 외부 의존성에 dep: URL을 사용하기로 했기 때문에 최상위 테이블을 외부 의존성에 사용할 수 없습니다. 둘을 섞으면 기존 사용 방식과 관련하여 심각한 하위 호환성 문제가 생길 것입니다.

엄밀히 말하면, dependency-groups 스키마로도 그룹별 하위 테이블에 외부 의존성을 정의할 수 있습니다:

[dependency-groups]
dev = [
  "pytest",
  { external = ["dep:cargo/ripgrep"] },
]

그러나 이 방식에도 같은 문제가 있습니다. 서로 다른 유형의 의존성 지정자를 같은 자료 구조에 섞게 된다는 것입니다. 우리는 관심사를 서로 다른 최상위 테이블로 분리하는 편이 더 깔끔하다고 생각하며, 그렇기 때문에 여전히 external.dependency-groups를 두는 쪽을 선호합니다.

선택적 의존성과 의존성 그룹 비교

external.dependency-groups를 두는 근거는 PEP 735에서 [dependency-groups]를 도입하며 제시한 근거와 동일합니다. 따라서 코어 메타데이터에 포함/제외되는 방식에 관한 의도된 사용법과 의미는 [dependency-groups]와 동일합니다.

external.optional-dependencies는 코어 메타데이터에 나타납니다. external.dependency-groups는 나타나지 않습니다.

명세

메타데이터가 잘못 지정된 경우, 도구는 반드시 오류를 발생시켜 사용자에게 실수를 알려야 합니다.

DepURL

DepURL은 여러 패키징 생태계에 걸쳐 이식 가능하도록 고안된 패키지 식별 체계를 구현합니다. 그 설계는:

dep:type/namespace/name@version?qualifiers#subpath

dep:는 고정 문자열이며 항상 존재합니다. type과 name은 필수이고, 그 밖의 구성 요소는 선택 사항입니다. 모든 구성 요소는 PURL type과 virtual type 모두에 적용되며, 다음 요구 사항을 따릅니다:

  • type (필수): 반드시 PURL type이거나 virtual이어야 합니다.
  • namespace (선택): 반드시 PURL namespace이거나 DepURL 중앙 레지스트리에 있는 네임스페이스여야 합니다(PEP 804 참고).
  • name (필수): 반드시 유효한 PURL name으로 파싱되는 이름이어야 합니다. 이름이 DepURL 중앙 레지스트리에 없으면 도구는 경고하거나 오류를 낼 수 있습니다(PEP 804 참고).
  • version (선택): 반드시 단일 버전이나 버전 범위를 나타내는 일반 version specifier(PEP 440 의미 체계)여야 하며, 다음 연산자만 사용할 수 있다는 제약이 있습니다: >=, >, <, <=, ==, ,.
  • qualifiers (선택): 반드시 유효한 PURL qualifier로 파싱되어야 합니다.
  • subpath (선택): 반드시 유효한 PURL subpath로 파싱되어야 합니다.

외부 의존성 지정자

외부 의존성 지정자는 반드시 DepURL을 포함해야 하며, 일반 dependency specifiers(원래 PEP 508에서 명세됨)에서 사용하는 것과 같은 문법의 환경 마커를 포함할 수 있습니다.

코어 메타데이터의 변경 사항

폐지 예정 사항

External-Requires 코어 메타데이터 필드는 폐기됨으로 표시되며 사용이 권장되지 않습니다.

추가 사항

코어 메타데이터에 두 개의 새 필드가 추가됩니다.

  • Requires-External-Dep. 외부 의존성 지정자 문자열로 표현한 외부 요구 사항입니다.
  • Provides-External-Extra. 외부 의존성(Requires-External-Dep에 있는 것)만 담는 extra 그룹입니다.

버전 상향

제안된 변경 사항은 순전히 추가적인 것이므로, 코어 메타데이터 버전은 2.6으로 올라갑니다.

이 변경은 PyPI와 외부 런타임 의존성을 지원하려는 도구에만 영향을 미치며, 그 밖에는 아무런 변경도 필요하지 않습니다.

pyproject.toml의 변경 사항

pyproject.toml 내용은 PEP 621과 같은 형식이라는 점에 유의하십시오.

테이블 이름

도구는 이 PEP에서 정의한 필드를 반드시 [external]이라는 이름의 테이블에 지정해야 합니다. 어떤 도구도 이 PEP나 이후 PEP에서 정의하지 않은 필드를 이 테이블에 추가해서는 안 됩니다. [external] 테이블이 없다는 것은 해당 패키지에 외부 의존성이 전혀 없거나, 있는 외부 의존성이 이미 시스템에 존재한다고 가정한다는 뜻입니다.

build-requires/optional-build-requires

  • 형식: 외부 의존성 지정자의 배열(build-requires) 및 외부 의존성 지정자 배열을 값으로 갖는 테이블(optional-build-requires)
  • Core metadata: 해당 없음

프로젝트를 빌드하는 데 필요한 (선택 사항인) 외부 빌드 요구 사항입니다.

build-requires의 경우, 값이 문자열 배열인 키입니다. 각 문자열은 프로젝트의 빌드 요구 사항 하나를 나타내며, 유효한 외부 의존성 지정자 형식이어야 합니다(MUST).

optional-build-requires의 경우, 각 키가 추가 빌드 요구 사항 집합을 지정하고 값이 문자열 배열인 테이블입니다. 배열의 문자열은 유효한 외부 의존성 지정자여야 합니다(MUST).

host-requires/optional-host-requires

  • 형식: 외부 의존성 지정자의 배열(host-requires) 및 외부 의존성 지정자 배열을 값으로 갖는 테이블(optional-host-requires) - Core metadata: 해당 없음

프로젝트를 빌드하는 데 필요한 (선택 사항인) 외부 호스트 요구 사항입니다.

host-requires의 경우, 값이 문자열 배열인 키입니다. 각 문자열은 프로젝트의 호스트 요구 사항 하나를 나타내며, 유효한 외부 의존성 지정자 형식이어야 합니다(MUST).

optional-host-requires의 경우, 각 키가 추가 호스트 요구 사항 집합을 지정하고 값이 문자열 배열인 테이블입니다. 배열의 문자열은 유효한 외부 의존성 지정자여야 합니다(MUST).

dependencies/optional-dependencies

  • 형식: 외부 의존성 지정자의 배열(dependencies) 및 외부 의존성 지정자 배열을 값으로 갖는 테이블(optional-dependencies)
  • Core metadata: Requires-External-Dep, Provides-External-Extra

프로젝트의 (선택 사항인) 런타임 의존성입니다.

dependencies의 경우, 값이 문자열 배열인 키입니다. 각 문자열은 프로젝트의 의존성 하나를 나타내며, 유효한 외부 의존성 지정자 형식이어야 합니다(MUST). 각 문자열은 Core Metadata에 Requires-External-Dep 필드로 추가되어야 합니다.

optional-dependencies의 경우, 각 키가 하나의 extra를 지정하고 값이 문자열 배열인 테이블입니다. 배열의 문자열은 유효한 외부 의존성 지정자여야 합니다(MUST). 각 optional-dependencies 그룹에 대해 다음을 따릅니다:

  • 그룹의 이름은 Core Metadata에 Provides-External-Extra 필드로 추가되어야 합니다(MUST).
  • 해당 그룹의 외부 의존성 지정자는 대응하는 ; extra == 'name' 환경 마커와 함께 Core Metadata에 Requires-External-Dep 필드로 추가되어야 합니다(MUST).

dependency-groups

  • 형식: 각 키가 그룹의 이름이고, 값은 외부 의존성 지정자, 테이블 또는 이 둘을 섞은 배열인 테이블입니다.
  • Core metadata: 해당 없음

PEP 735 스타일의 의존성 그룹이지만, PEP 508 문자열 대신 외부 의존성 지정자를 사용합니다. 그 밖의 모든 세부 사항(예: 그룹 포함, 이름 정규화)은 공식 dependency groups specification을 따릅니다.

예시

이 예제들은 여러 패키지에 대해 [external] 테이블의 내용과 그에 대응하는 PKG-INFO/METADATA 내용(있는 경우)이 어떠해야 하는지 보여 줍니다.

cryptography 39.0

pyproject.toml 내용:

[external]
build-requires = [
  "dep:virtual/compiler/c",
  "dep:virtual/compiler/rust",
  "dep:generic/pkg-config",
]
host-requires = [
  "dep:generic/openssl",
  "dep:generic/libffi",
]

PKG-INFO / METADATA 내용: 해당 없음.

SciPy 1.10

pyproject.toml 내용:

[external]
build-requires = [
  "dep:virtual/compiler/c",
  "dep:virtual/compiler/cpp",
  "dep:virtual/compiler/fortran",
  "dep:generic/ninja",
  "dep:generic/pkg-config",
]
host-requires = [
  "dep:virtual/interface/blas",
  "dep:virtual/interface/lapack@>=3.7.1",
]

PKG-INFO / METADATA 내용: 해당 없음.

Pillow 10.1.0

pyproject.toml 내용:

[external]
build-requires = [
  "dep:virtual/compiler/c",
]
host-requires = [
  "dep:generic/libjpeg",
  "dep:generic/zlib",
]

[external.optional-host-requires]
extra = [
  "dep:generic/lcms2",
  "dep:generic/freetype",
  "dep:generic/libimagequant",
  "dep:generic/libraqm",
  "dep:generic/libtiff",
  "dep:generic/libxcb",
  "dep:generic/libwebp",
  "dep:generic/openjpeg@>=2.0",
  "dep:generic/tk",
]

PKG-INFO / METADATA 내용: 해당 없음.

Spyder 6.0

pyproject.toml 내용:

[external]
dependencies = [
  "dep:cargo/ripgrep",
  "dep:cargo/tree-sitter-cli",
  "dep:golang/github.com/junegunn/fzf",
]

PKG-INFO / METADATA 내용:

Requires-External-Dep: dep:cargo/ripgrep
Requires-External-Dep: dep:cargo/tree-sitter-cli
Requires-External-Dep: dep:golang/github.com/junegunn/fzf

jupyterlab-git 0.41.0

pyproject.toml 내용:

[external]
dependencies = [
  "dep:generic/git",
]

[external.optional-build-requires]
dev = [
  "dep:generic/nodejs",
]

PKG-INFO / METADATA 내용:

Requires-External-Dep: dep:generic/git

PyEnchant 3.2.2

pyproject.toml 내용:

[external]
dependencies = [
  # libenchant is needed on all platforms but vendored into wheels
  # distributed on PyPI for Windows. Hence choose to encode that in
  # the metadata. Note: there is no completely unambiguous way to do
  # this; another choice is to leave out the environment marker in the
  # source distribution and either live with the unnecessary ``METADATA``
  # entry in the distributed Windows wheels, or to apply a patch to this
  # metadata when building those wheels.
  "dep:github/AbiWord/enchant; platform_system!='Windows'",
]

PKG-INFO / METADATA 내용:

Requires-External-Dep: dep:github/AbiWord/enchant; platform_system!="Windows"

의존성 그룹을 사용하는 경우

pyproject.toml 내용:

[external.dependency-groups]
dev = [
  "dep:generic/catch2",
  "dep:generic/valgrind",
]

PKG-INFO / METADATA 내용: 해당 없음.

하위 호환성

이 PEP는 선택적인 새 메타데이터를 추가할 뿐이므로 하위 호환성에 미치는 영향은 없습니다. 이러한 메타데이터가 없으면 패키지 작성자나 패키징 도구에는 아무것도 달라지지 않습니다.

이 PEP에서 도입하는 변경 중 기존 프로젝트에 영향을 주는 것은 External-Requires 코어 메타데이터 필드의 폐지(deprecation)뿐입니다. 이 필드는 생태계에서 거의 쓰이지 않으므로(근거 참조), 이번 폐지의 영향은 무시할 수 있는 수준이라고 추정합니다.

이 필드는 setuptools-ext 같은 기존 도구에서 계속 인식되지만, Requires처럼 더 이상 쓰이지 않는 필드(Requires-Dist로 대체되어 폐지됨)와 마찬가지로 Python Packaging User Guide에서는 사용을 권장하지 않을 것입니다.

보안 영향

이 PEP는 외부 의존성의 메타데이터를 정적으로 정의하는 방법을 다루므로 직접적인 보안 문제는 없습니다. 보안 문제가 생긴다면 그것은 도구가 메타데이터를 어떻게 사용하고 그에 따라 어떻게 동작하기로 하는지에서 비롯될 것입니다.

교육 방법

외부 의존성, 그리고 그 외부 의존성을 벤더링하는지 여부와 그 방식은 일반적으로 Python 패키지 작성자가 자세히 이해하고 있지 않은 주제입니다. 먼저 외부 의존성이 어떻게 정의되는지, 그리고 외부 의존성에 의존하는 여러 방식—ctypes나 subprocess 호출을 이용한 런타임 전용 의존부터 링크 대상이 되는 빌드 의존성까지—을 살펴본 다음, 메타데이터에 외부 의존성을 선언하는 방법으로 넘어가려고 합니다. 문서는 무엇이 패키지 작성자와 관련 있고 무엇이 배포판 패키저와 관련 있는지를 명확히 밝혀야 합니다.

이 주제에 관한 자료는 가장 관련성이 높은 패키징 튜토리얼, 주로 Python Packaging User Guide에 추가될 것입니다. 또한 외부 의존성 메타데이터 지원을 추가하는 빌드 백엔드라면 모두 자체 문서에 이에 관한 정보를 포함할 것으로 기대하며, auditwheel 같은 도구도 마찬가지일 것입니다.

참조 구현

이 PEP는 코드 기능이 아니라 메타데이터 명세를 담고 있으므로, 메타데이터 명세 전체를 구현하는 코드는 없을 것입니다. 그러나 참조 구현이 있는 부분도 있습니다:

  1. [external] 테이블은 유효한 TOML이어야 하므로 tomllib로 불러올 수 있습니다. 이 테이블은 아래에서 보여 드리는 것처럼 pyproject-external 패키지로 추가 처리할 수 있습니다.
  2. 이 명세의 핵심 부분인 PURL 명세에는 PURL을 구성하고 파싱하는 참조 구현을 담은 Python 패키지인 packageurl-python 이 있습니다. 이 패키지는 DepURL 전용 검증과 처리를 제공하기 위해 pyproject-external로 감싸여 있습니다.

이 메타데이터가 Python 패키지에 추가되면, 이 메타데이터를 소비할 수 있는 주체와 사용 사례는 여러 가지가 있습니다. 플랫폼별 휠을 게시한 PyPI 다운로드 상위 150개 패키지 전체에 대해 테스트된 메타데이터는 rgommers/external-deps-build에서 확인할 수 있습니다. 이 메타데이터는 깨끗한 Docker 컨테이너에서 해당 메타데이터로 패치한 sdist로부터 휠을 빌드하는 데 사용하는 방식으로 검증되었습니다.

예시

다음과 같은 [external] 테이블이 있는 pyproject.toml이 주어졌다고 합시다:

[external]
build-requires = [
  "dep:virtual/compiler/c",
  "dep:virtual/compiler/rust",
  "dep:generic/pkg-config",
]
host-requires = [
  "dep:generic/openssl",
  "dep:generic/libffi",
]

pyproject_external.External을 사용하여 이를 파싱하고 조작할 수 있습니다:

>>> from pyproject_external import External
>>> external = External.from_pyproject_path("./pyproject.toml")
>>> external.validate()
>>> external.to_dict()
{'external': {'build_requires': ['dep:virtual/compiler/c', 'dep:virtual/compiler/rust', 'dep:generic/pkg-config'], 'host_requires': ['dep:generic/openssl', 'dep:generic/libffi']}}
>>> external.build_requires
[DepURL(type='virtual', namespace='compiler', name='c', version=None, qualifiers={}, subpath=None), DepURL(type='virtual', namespace='compiler', name='rust', version=None, qualifiers={}, subpath=None), DepURL(type='generic', namespace=None, name='pkg-config', version=None, qualifiers={}, subpath=None)]
>>> external.build_requires[0]
DepURL(type='virtual', namespace='compiler', name='c', version=None, qualifiers={}, subpath=None)

제안된 [external] 테이블은 올바른 형식이었다는 점에 유의하십시오. 다음과 같이 유효하지 않은 내용이 있으면:

[external]
build-requires = [
  "dep:this-is-missing-the-type",
  "pkg:not-a-dep-url"
]

검증에 실패하게 됩니다:

>>> external = External.from_pyproject_data(
  {
    "external": {
      "build_requires": [
        "dep:this-is-missing-the-type",
        "pkg:not-a-dep-url"
      ]
    }
  }
)
ValueError: purl is missing the required type component: 'dep:this-is-missing-the-type'.

기각된 아이디어

PyPI에도 패키징되어 있는 외부 의존성을 위한 전용 문법

Ninja, patchelf, CMake처럼 PyPI에 패키징되어 있는 비 Python 패키지도 있습니다. 일반적으로 원하는 것은 이들의 시스템 버전을 사용하고, 시스템에 없으면 해당 PyPI 패키지를 설치하는 것입니다. 작성자들은 이 시나리오에 대한 전용 지원이 필요하지 않다고(또는 적어도 그러한 지원을 정당화하기에는 너무 복잡하다고) 봅니다. 외부 의존성을 위한 의존성 제공자는 PyPI를 패키지를 얻을 수 있는 여러 출처 중 하나로 취급할 수 있습니다. 이 사용 사례에 대한 매핑 예시가 PEP 804에 제안되어 있습니다.

라이브러리 및 헤더 이름을 외부 의존성으로 사용하기

이전 초안 PEP(“External dependencies” (2015))는 특정 라이브러리 및 헤더 이름을 외부 의존성으로 사용하자고 제안했습니다. 이 방식은 지나치게 세분화되어 있으면서도 불충분합니다(예를 들어, 헤더에는 버전이 없는 경우가 많고, 여러 패키지가 같은 헤더나 라이브러리를 제공할 수 있습니다). 패키지 이름을 사용하는 것은 여러 패키징 생태계에서 잘 확립된 패턴이므로 이 방식을 우선해야 합니다.

명시적인 -dev 또는 -devel 접미사로 호스트 의존성 분리하기

이 관례는 패키징 생태계마다 일관되지 않으며, 업스트림 패키지 작성자들에게 널리 받아들여지고 있지도 않습니다. 명시적인 제어가 필요한 경우(예: 패키지가 빌드 시점 의존성이 아니라 런타임 의존성으로 사용될 때 헤더를 설치하는 경우)는 상당히 드물고, 명확한 사용 사례가 충분하지 않은 상태에서 설계 복잡성을 더하고 싶지 않으므로, 저희는 build, host, run 범주 구분에만 의존하기로 했으며, 각 경우에 어떤 범주가 적용되는지는 도구가 맥락에 따라 결정하도록 했습니다.

이것이 불충분한 것으로 드러난다면, 향후 PEP에서 PURL 스키마에 있는 URL 한정자 기능(?key=value)을 사용하여 필요한 조정을 구현할 수 있습니다. 이는 하위 호환성을 유지하는 방식으로 할 수 있습니다.

식별자 간접 참조

일부 생태계에는 cmake("dependency") 또는 compiler("language") 같은 매개변수화된 함수를 기반으로 패키지를 선택하는 방법이 있으며, 이러한 함수는 추가적인 맥락이나 구성을 바탕으로 패키지 이름을 반환합니다. 이 기능은 그다지 흔하지 않으며, 있다 하더라도 거의 사용되지 않는다고 볼 수 있습니다. 또한 그 동적인 특성 때문에 시간이 지나면서 의미가 바뀌기 쉽고, 이름 해석을 특정 빌드 시스템에 의존하는 것은 일반적으로 좋은 생각이 아닙니다.

저자들은 잘 알려진 메타데이터를 통해 명시적으로 매핑할 수 있는 정적 식별자를 선호합니다(예: PEP 804에서 제안된 방식).

이러한 간접 참조를 구현한 생태계는 이를 활용하여 PEP 804에서 제안된 매핑을 생성하도록 설계된 인프라를 지원할 수 있습니다.

[build-system] 아래에 host-requires 키 추가하기

교차 컴파일을 지원하는 다른 패키징 시스템으로의 이름 매핑을 더 잘 지원하기 위해 PyPI에 있는 호스트 의존성에 대해 host-requires를 추가하는 것은, 이 PEP가 [external] 테이블 아래에 host-requires를 추가하는 것과 같은 이유로 원칙적으로는 유용해 보입니다. 하지만 이 PEP에 포함할 필요는 없으므로, 저자들은 이 PEP의 범위를 제한하는 편을 선호합니다. 이 문제는 교차 컴파일에 관한 향후 PEP에서 다룰 수 있을 것입니다. 이 PEP의 일부로 [build-system] 아래에 host-requires를 추가하는 데 찬성하는 인자와 반대하는 인자는 이 이슈에 더 많이 나와 있습니다.

코어 메타데이터의 Requires-External 필드 재사용

Core Metadata 명세에는 관련 필드가 하나 있는데, 바로 Requires-External입니다. 언뜻 보기에는 external.dependencies 테이블을 기록하기에 좋은 후보처럼 보이지만, 저자들은 외부 런타임 의존성 메타데이터를 전파하는 데 이 필드를 재사용하지 않기로 결정했습니다.

Requires-External 필드는 버전 2.4 기준으로 의미가 매우 느슨하게 정의되어 있습니다. 요컨대 name [(version)][; environment marker] 형식입니다(대괄호는 선택적 필드를 나타냅니다). name에 어떤 문자열이 유효한지는 정의되어 있지 않으며, 명세의 예시에서는 언어 이름으로 “C”를, 패키지 이름으로 “libpng”를 모두 사용합니다. 의미를 엄격하게 하면 하위 호환성이 깨지고, 그대로 두는 것도 만족스럽지 않아 보입니다. DepURL을 이 문법에 맞추려면 분해해야 합니다.

생태계별 버전 비교 의미 체계의 사용 허용하기

특히 사전 릴리스를 다룰 때, 버전 비교에 대한 PEP 440 의미 체계가 제대로 작동하지 않는 경우가 있습니다. 예를 들어 1.2.3a는 알파 버전이 아니라 1.2.3 이후의 릴리스를 나타낼 수도 있습니다. 이러한 경우를 올바르게 처리하려면 임의의 버전 관리 체계를 허용해야 합니다. 이 PEP의 저자들은 이를 허용함으로써 얻는 부가 가치가 추가되는 복잡성을 정당화하지 못한다고 봅니다. 원한다면 패키지 작성자는 코드 주석이나 DepURL의 qualifier 필드(근거 아래의 버전 관리 절 참고)를 사용하여 이 수준의 세부 사항을 기록할 수 있습니다.

미해결 문제

현재로서는 없습니다.

참고 문헌