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, 호스트 머신에서 런타임에 필요하지만 빌드 시점에는 필요하지 않은 의존성입니다.

이 세 키에는 optional external 대응 항목(optional-build-requires, optional-host-requires, optional-dependencies)도 있으며, 이는 project.optional-dependenciesproject.dependencies에 대해 수행하는 것과 동일한 역할을 합니다. 마지막으로 dependency-groups는 외부 의존성에 대해 PEP 735와 동일한 기능을 제공합니다.

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

동기

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

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

Linux 배포판, conda, Homebrew, Spack 및 Nix와 같은 패키징 생태계에는 Python 패키지의 전체 의존성 집합이 필요하며, pyp2spec (Fedora), Grayskull (conda) 및 dh_python (Debian)과 같은 도구를 사용하여 업스트림 Python 패키지의 메타데이터에서 자체 패키지 관리자를 위한 의존성 메타데이터를 자동으로 생성하려고 합니다. 현재는 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가 있습니다.

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

크로스 컴파일

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

용어

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

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

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

이 PEP에서는 “target machine” 또는 “target dependency”라는 용어를 사용하지 않는다는 점에 유의하십시오. 이는 일반적으로 컴파일러를 크로스 컴파일하거나 그 밖의 고급 시나리오 [3], [4]에서만 관련되며, 이 PEP의 범위에 포함되지 않습니다.

마지막으로, 빌드 시점에 필요한 패키지에는 “dependency”라는 용어가 가장 널리 사용되지만, PyPI 빌드 시점 의존성에 대해 pyproject.toml에 이미 존재하는 키는 build-requires입니다. 따라서 이 PEP에서는 일관성을 위해 [external]아래에서 build-requireshost-requires 키를 사용합니다.

빌드 및 호스트 의존성

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

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

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

호스트 의존성이 선언되어 있고 크로스 컴파일과 무관한 동작을 실행하는 도구라면 host-requires목록을 build-requires에 병합할 수 있으며, 이것이 유용한지는 컨텍스트에 따라 달라집니다.

외부 의존성 지정

구체적인 패키지 지정

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

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

scheme 구성 요소는 고정 문자열인 pkg이며, 나머지 구성 요소 중 typename만 필수입니다.

외부 의존성은 직접 입력될 가능성이 높으므로, 편의성과 사용자 친화성을 위해 여러 변경 사항을 도입한 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 등에서도 이미 지원됩니다. 이러한 도구에서 지원이 제공될 때까지 수년을 기다릴 필요가 없다는 점은 중요합니다. dep:virtualPURL에 대응하는 등가물이 없다는 점을 제외하면 DepURL은 PURL로 매우 쉽게 변환할 수 있습니다.

참조할 표준 패키지 관리자가 없는 구체적인 패키지의 경우 dep:generic/dep-name을 사용할 수도 있고, 해당 패키지가 관리되는 VCS 시스템을 직접 참조할 수도 있습니다(예: dep:github/user-or-org-name/dep-name). 어느 쪽이 더 적절한지는 상황에 따라 달라집니다. 이 PEP에서는 패키지 이름이 모호하지 않고 잘 알려진 경우(예: dep:generic/git 또는 dep:generic/openblas) dep:generic을 사용하고, 그렇지 않은 경우에는 VCS를 유형으로 사용할 것을 권장합니다. 주어진 패키지에 대해 어떤 이름을 표준 이름으로 선택할지와 그러한 선택을 만들고 기록하는 프로세스는 PEP 804의 주제입니다.

가상 패키지 사양

PURL은 아직 가상 또는 가상 의존성 사양을 지원하지 않습니다. 개정판 1.1을 위해 proposal to add a virtual type이 논의되고 있습니다.

그동안 저희는 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.0pkg:pypi/numpy@2.0로 표현할 수 있습니다.

고정 버전을 넘어서는 버전 표현식과 범위에 대한 PURL의 지원은 vers URI를 통해 제공됩니다 (see specification).:

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

사용자는 pkg:URL을 vers: URL과 결합해야 합니다. 예를 들어 numpy>=2.0을 표현하려면 PURL에 해당하는 표현은 pkg:pypi/numpyvers:pypi/>=2.0를 더한 것입니다. 다음과 같이 할 수 있습니다.

  • 두 항목으로 구성된 리스트: ["pkg:pypi/numpy", "vers:pypi/>=2.0"].
  • percent-encoded URL 한정자: pkg:pypi/numpy?vers=vers:pypi%2F%3E%3D2.0.

이러한 옵션 중 어느 것도 사용하기 편리하지 않으므로, 저희는 대신 DepURL이 PEP 440 의미론의 부분집합에 해당하는 의미 체계로 버전 범위 지정자도 허용하도록 선택했습니다. 허용되는 연산자는 패키지 관리자 전반에서 널리 사용되는 연산자입니다(예를 들어 ==, >>=는 일반적이지만 ~=는 그렇지 않습니다).

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

  • dep:pypi/numpy@2.0: numpy를 정확히 버전 2.0으로 고정합니다.
  • 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 플랫폼 지정)이 필요할 것으로 예상하지 않습니다.

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

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

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

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

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

Python 개발 헤더

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

새로운 Core Metadata 필드

두 개의 새로운 Core Metadata 필드가 제안됩니다.

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

Core Metadata 사양에는 pyproject.toml[build-system] 테이블에 있는 메타데이터를 위한 필드가 없으므로, build-requireshost-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 항목이 해당 sdist에서 cibuildwheel 같은 도구로 빌드한 휠에서 사라질 수 있습니다. 또한 Requires-External-Dep 항목이 변경되지 않거나 더 좁은 제약 조건으로 휠에 남아 있을 수도 있습니다. auditwheel은 기본적으로 OpenGL과 같이 허용 목록에 포함된 특정 종속성을 벤더링하지 않습니다. 또한 auditwheeldelvewheel을 사용하면 사용자가 --exclude 또는 --no-dll 명령줄 플래그를 통해 종속성을 수동으로 제외할 수 있습니다. 이는 예를 들어 CUDA의 대형 공유 라이브러리를 벤더링하지 않기 위해 사용됩니다.

따라서 pyproject.toml의 외부 종속성에서 생성된 Requires-External-Dep 항목은 빌드/배포 프로세스에 따라 sdist와 이에 대응하는 휠 사이에서 달라질 수 있습니다.

이는 빌드 백엔드가 sdist에서 빌드한 휠에만 이 구분이 적용되므로 해당 필드를 Dynamic으로 표시해야 한다는 의미는 아닙니다. 특히 다른 휠에서 빌드한 휠은 이 제약 조건을 충족할 필요가 없습니다.

종속성 그룹

이 PEP에서는 [external] 테이블 아래에 PEP 735 키인 dependency-groups도 포함하기로 했습니다. 이러한 결정은 외부 메타데이터에 대해서도 유사한 기능이 필요하다는 점에 근거합니다. 최상위 테이블은 외부 종속성에 사용할 수 없습니다. PEP 508 문자열(및 그룹 포함을 위한 테이블)이 포함될 것으로 예상되지만, 외부 종속성에는 dep: URL을 사용하기로 했기 때문입니다. 두 가지를 혼합하면 기존 사용 방식과의 하위 호환성에 중대한 문제가 발생합니다.

엄밀히 말하면 dependency-groups 스키마에서는 그룹별 하위 테이블에 외부 종속성을 정의할 수 있습니다.:

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

그러나 이 방식에도 동일한 문제가 있습니다. 같은 데이터 구조에서 서로 다른 유형의 종속성 지정자를 혼합하게 됩니다. 서로 다른 최상위 테이블에서 각 관심사를 분리하는 편이 더 명확하다고 판단하므로, 여전히 external.dependency-groups를 선호합니다.

선택적 종속성과 종속성 그룹

external.dependency-groups를 두는 근거는 [dependency-groups]를 도입한 근거로 PEP 735에서 제시한 내용과 동일합니다. 따라서 Core Metadata에 포함하거나 제외하는 의도된 사용 방식과 의미 체계는 [dependency-groups]와 동일합니다.

external.optional-dependencies는 Core Metadata에 표시됩니다. external.dependency-groups는 표시되지 않습니다.

사양

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

DepURL

DepURL은 패키징 생태계 전반에서 이식 가능하도록 설계된 패키지 식별 체계를 구현합니다. 그 설계는 다음과 같습니다.:

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

dep:는 고정 문자열이며 항상 존재합니다. typename은 필수이고, 다른 구성 요소는 선택 사항입니다. 모든 구성 요소는 PURL 유형과 가상 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에서 처음 지정됨).

Core Metadata의 변경 사항

사용 중단

External-Requires Core Metadata 필드는 obsolete로 표시되며, 사용이 권장되지 않습니다.

추가 사항

Core Metadata에 두 개의 새 필드가 추가됩니다.

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

버전 상향

제안된 변경 사항은 순전히 추가적이므로 Core Metadata 버전은 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는 값이 문자열 배열인 키입니다. 각 문자열은 프로젝트의 빌드 요구 사항을 나타내며 유효한 외부 의존성 지정자로 형식이 지정되어야 합니다.

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

host-requires/optional-host-requires

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

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

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

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

dependencies/optional-dependencies

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

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

dependencies는 값이 문자열 배열인 키입니다. 각 문자열은 프로젝트의 의존성을 나타내며 유효한 외부 의존성 지정자로 형식이 지정되어야 합니다. 각 문자열은 Core MetadataRequires-External-Dep 필드로 추가되어야 합니다.

optional-dependencies는 각 키가 하나의 extra를 지정하고 값이 문자열 배열인 테이블입니다. 배열의 문자열은 유효한 외부 의존성 지정자여야 합니다. 각 optional-dependencies그룹에 대해 다음을 적용합니다.

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

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 내용: N/A.

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 내용: N/A.

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 내용: N/A.

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 내용: N/A.

하위 호환성

이 PEP는 새 선택적 메타데이터만 추가하므로 하위 호환성에 미치는 영향이 없습니다. 이러한 메타데이터가 없으면 패키지 작성자나 패키징 도구에는 아무런 변화가 없습니다.

이 PEP에서 도입된 변경 사항 중 기존 프로젝트에 영향을 미치는 유일한 것은 External-Requires핵심 메타데이터 필드의 사용 중단입니다. 이 사용 중단이 생태계에 미치는 영향은 보급률이 낮으므로 무시할 수 있을 정도라고 추정합니다(근거 참조).

이 필드는 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에서 확인할 수 있습니다. 이 메타데이터는 해당 메타데이터를 적용한 sdist로부터 깨끗한 Docker 컨테이너에서 휠을 빌드하는 방식으로 검증되었습니다.

예제

다음과 같은 [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, hostrun 범주 분할에만 전적으로 의존하고, 각 상황에서 어떤 범주를 적용할지는 도구가 결정하도록 하기로 했습니다.

이것이 불충분한 것으로 판명되면, 향후 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를 추가하는 것에 찬성하는 주장과 반대하는 주장이 더 많이 포함되어 있습니다.

Core Metadata에서 Requires-External 필드 재사용

Core Metadata 사양에는 관련 필드가 하나 포함되어 있으며, 바로 Requires-External입니다. 처음 보기에는 external.dependencies 테이블을 기록하기에 좋은 후보처럼 보이지만, 저자들은 외부 런타임 의존성 메타데이터를 전달하기 위해 이 필드를 재사용하지 않기로 했습니다.

버전 2.4 현재 Requires-External 필드의 의미론은 매우 느슨하게 정의되어 있습니다. 본질적으로 name [(version)][; environment marker] 형식이며(대괄호는 선택적 필드를 나타냅니다), name에 유효한 문자열이 무엇인지는 정의되어 있지 않습니다. 사양의 예에서는 언어 이름인 “C”와 패키지 이름인 “libpng”를 모두 사용합니다. 의미론을 엄격하게 정의하면 하위 호환성을 깨뜨리며, 현재 상태로 두는 것도 만족스럽지 않아 보입니다. DepURL은 이 구문에 맞도록 분해해야 합니다.

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

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

미해결 문제

현재는 없습니다.

참고 문헌