The Architecture of Open Source Application의 Python Packaging 챕터를 번역한 것입니다.
일부 오역이 있을 수 있습니다.
Python Packaging
Tarek Ziade
14.1. 소개
제품을 인스톨 하는데에는 두 가지 방식이 있다. 첫번째는 Windows나 Mac OS X에서 흔한 방법으로, 필요한 라이브러리를 어플리케이션이 스스로 모두 포함하고 인스톨하는데 다른 무엇에도 의존하지 않는 방식이다. 이러한 체계는 어플리케이션의 관리를 쉽게한다. 각각의 어플리케이션은 각자 독립적으로 존재한다. 설치하거나 제거하는 행위는 OS의 다른 부분을 건들지 않는다. 만약 해당 어플리케이션이 흔히 쓰이지 않는 라이브러리를 필요로 한다면, 그 라이브러리는 해당 어플리케이션의 배포판에 포함되어야 한다.
두 번째는 리눅스 기반의 표준이다. 소프트웨어를 패키지라 불리는 작은 유닛들의 집합으로 다룬다. 라이브러리들은 패키지들의 묶음이다. 어떤 패키지든 다른 패키지에 의존성을 가질 수 있다. 어플리케이션을 인스톨 할 때는 연관된 상당수의 다른 라이브러리들을 찾아 인스톨해야 한다. 이런 의존성은 보통 다수의 패키지들을 가지고 있는 중앙 리파지토리에서 가져온다. 이러한 철학 때문에 리눅스 배포판은 dpkg나 RPM과 같은 복잡한 패키지 관리 시스템을 이용해 의존성을 추적하고, 서로 다른 두 어플리케이션이 호환되지 않는 다른 버전의 동일한 라이브러리를 쓴다면 설치할 수 없는 것이다.
각각의 접근방식에는 장단점이 있다. 모든 부분들이 업데이트되고 교체될 수 있는 높은 모듈화 시스템은 관리가 더 쉽다. 왜냐하면 각 라이브러리는 항상 한 장소에 존재하고 그것이 업데이트 되었을때 그 것을 모든 어플리 케이션이 공유 할 수 있다는 장점이 있다. 예를 들어 특정 라이브러리의 보안 이슈가 수정되면 모든 어플리케이션에 한번에 해당 사항이 적용된다. 반면에 어플리케이션이 스스로 라이브러리를 포함하고 있다면 해당 보안 이슈의 배포는 훨씬 복잡할 것이다. 특히 각각의 다른 어플리케이션이 다른 버전의 동일한 라이브러리를 쓰고 있다면 그렇다.
하지만 모듈화 방식은 흔히 개발자들에게 약점이 알려져 있다. 그것은 개발자가 자신의 제품과 종속성을 직접 컨트롤 할 수 없다는 것이다. 반면 스탠드얼론 소프트워에에서는 제품의 실행 환경이 더 안정적임을 보장할 수 있고 시스템을 업그레이드 할 때 의존성 지옥에 시달릴 필요가 없다.
스탠드얼론 방식은 또한 여러 플랫폼을 지원할때 개발자가 편하다. 포터블 어플리케이션과 같은 일부 프로그램은 자신이 담긴 디렉토리외에 호스트 시스템과의 모든 종속성을 없앤다. 심지어 로그파일마저 독립적이다.
파이썬의 패키징 시스템은 두번째 방식을 이용한다. (각 인스톨 마다 다수의 의존성을 가지는 방식) 개발자, 관리자, 패키지 작업자, 그리고 유저 모두에게 가능한 친숙하게 다가간다. 불행하게도 이것은 다양한 문제점을 허용하고 일으킨다. 비직관적인 버전 스키마, 잘못 다뤄지는 데이터 파일, 리패키지의 어려움 등. 3년전에 나와 다른 파이썬 사용자들은 이 문제를 고치기 위해 이것을 다시 개발하기로 결정했다. 우리는 우리들을 패키징 연대(Fellowship of the Packaging) 이라고 부른다. 그리고 이 챕터에서는 우리가 이 문제들을 고칠때 겪었던 점들과 해결 방법이 어떠한 것인지 설명한다.
[용어] 파이썬에서 패키지는 파이썬 파일을 가지고 있는 디렉토리이다. 그리고 파이썬 파일들은 모듈이라고 불린다. 이 정의는 “패키지”라는 단어를 약간 모호하게한다. 왜냐하면 많은 시스템들에서는 릴리즈된 프로젝트들을 패키지라고 부르기 때문이다. 파이썬 개발자들 스스로도 종종 이에 관해 혼란스러워한다. 이 모호함을 해결하는 단 한가지 방법은 파이썬 모듈을 담고있는 디렉토리를 “파이썬 패키지”라고 부르는 것이다. ‘릴리즈’라는 용어는 어느 한 버전의 프로젝트를 지칭하도록 하고, 배포판의 정의는 tar나 zip등으로 릴리즈된 소스 또는 바이너리들을 지칭한다. |
14.2. 파이썬 개발자의 부담
많은 파이썬 프로그래머들은 제작한 프로그램이 어떤 환경에서든 쓸 수 있길 바란다. 그리고 보통 파이썬 표준 라이브러리들과 시스템 종속적인 라이브러리를 혼용하여 쓰고 싶어한다 . 하지만 어플리케이션을 모든 패키징 시스템별로 각각 패키지 할게 아니라면, 파이썬 전용 릴리즈(Python-specific releases)들을 만들다 지칠것이다. (파이썬 전용 릴리즈는 OS가 뭐든 상관없이 파이썬에 설치될 수 있는 릴리즈이다.)
또한 다음을 만족해야 한다.
- 어떠한 타겟 플랫폼에서든 당신의 프로젝트를 다시 패키징 할 수 있어야 한다.
- 그 프로젝트가 가진 의존성들은 모든 타겟 플랫폼에서 다시 패키징 된다.
- 시스템 의존성은 분명하게 기술 되어야 한다.
때로는 이것이 간단하지 않다. 가령 Plone (파이썬으로 만든 CMS)은 수백가지의 작은 순수 파이썬 라이브러리를 사용한다. 그런데 모든 라이브러리들이 파이썬 패키징 시스템에서 사용 가능하지는 않다. 이말은 Plone은 포터블 어플리케이션을 만드려면 필요로 하는 모든 것들을 제공해야 한다는 뜻이다. 이를 위해 zc.buildout을 사용하는데, 이것은 대상의 모든 의존성들을 수집하고 어떤 시스템에서도 디렉토리 하나로 돌아갈 수 있는 포터블 어플리케이션을 만들어준다. 이건 사실상 바이너리 릴리즈다. C코드 조차도 컴파일되어 포함될 것이기 때문이다.
이것은 개발자에게 큰 성과다. 제품을 릴리즈 하기 위해서는 그저 아래 기술된 파이썬 표준들과 zc.buildout을 이용해 그것들의 의존성을 기술하면 된다. 하지만 앞에 언급했듯이 이런 종류의 릴리즈는 대부분의 리눅스 관리자들이 싫어 할만한 복잡한 것들을 시스템에 설치한다. 물론 윈도우 관리자들은 개의치 않을 거다. 하지만 CentOS나 Debian 관리자들 역시 싫어 할 것이다. 왜냐하면 이런 시스템들은 시스템의 모든 파일들이 등록되어 있고, 분류되어있고, 관리툴에 알려져있다고 가정된 관리 시스템에 기반하기 때문이다.
그런 관리자들은 자신들의 룰에 따라 당신의 어플리케이션을 다시 패키징 할 수 있기를 바란다. 여기서 우리가 답변해야 하는 질문은 “파이썬은 다른 패키징 시스템으로 자동으로 변환하는 기능을 갖춘 패키징 시스템을 가질 수 있는가?”이다. 만약 그렇다면 하나의 어플리케이션이나 라이브러리가 추가적인 패키징 작업 없이 어떤 시스템에라도 인스톨 될 수 있는가? 여기 ‘자동’ 이라는 말은 그 일이 스크립트를 이용해 완전히 자동으로 완료됨을 의미하지 않는다. (PRM 이나 dpkg 같은 패키징 시스템으로는 불가능하다) 우리는 항상 다시 패키징 할때 우리 프로젝트의 추가적인 세부사항들을 일부 추가해야 할 필요성이 있다. 또한 일부 코드들은 다시 패키지 하는데 어려움이 있을 것이다. 왜냐하면 개발자들은 패키징 룰을 잘 모르기 때문이다.
여기 파이썬 패키징 시스템을 잘못 이용한 예제가 한 가지 있다. Fumanchu라는 버전이름으로 릴리즈된 MathUntils라는 라이브러리가 있다. 이 라이브러리를 만든 매우 재능이 뛰어난 수학자는 자신의 고양이들 이름으로 버전이름을 짓는게 매우 재밌을 거라 생각했다. 하지만 패키징 하는 입장에서 어떻게 Fumanchu가 그의 두 번째 고양이이름이고 첫 번째는 Phill인걸 알 수 있겠는가? 그리고 Fumanchu가 Phill다음에 온다는 것을 어떻게 알겠는가?
매우 극단적인 예제처럼 들린다. 하지만 오늘날의 툴과 표준에서 이것은 충분히 가능하다. 가장 최악은 easy_install이나 pip를 이용해 인스톨된 파일들을 추적하기 위하여 비표준의 자신만의 레지스트리를 이용하고 Fumanchu와 Phill를 글자, 숫자순으로 정렬하는 것이다.
다른 문제는 데이터 파일을 어떻게 다룰 것인가이다. 예를 들어 당신의 어플리케이션이 SQLite 데이터베이스를 쓴다면? 그것을 당신의 패키지 디렉토리 안에 둔다면, 당신의 어플리케이션은 시스템의 쓰기 금지된 디렉토리로 인해 실패할 수도 있다. 또한 리눅스에서는 이러한 것들을 어플리케이션 데이터를 위한 디렉토리(/var)에 저장해야 한다는 관례와도 상충한다.
시스템 관리자들은 당신의 파일들을 당신의 어플리케이션이 동작하는데 아무런 문제가 없게끔 원하는 장소에 위치시켜야 할 필요가 있다. 그리고 당신은 그 파일들이 무엇인지 설명해야 할 것이다. 자 다시 질문을 정리해 보면, “어떠한 서드파티 패키지 시스템으로 작성 되었더라도 코드를 읽지 않고, 파이썬에서 어플리케이션을 다시 패키지하는데 필요한 모든 정보를 제공하는 패키지 시스템을 가지는 것이 가능한가? 모두가 행복할 수 있는 그런 패키지 시스템을 말이다.
14.3. 현재 파이썬 패키징 구조
파이썬 표준 라이브러리에 있는 Distutils 패키지는 위에서 언급한 난제들을 가지고 있다. 이것이 표준이 된 이후로, 사람들은 이것의 결함을 감수하고 사용하거나 여기에 몇 가지 기능이 추가된 Setuptools같은 좀 더 고급 버전을, 또는 Setupools에서 갈라져 나온 Distribute를 사용한다. 추가로 Setuptools에 의존하는 더 고급스런 인스톨러인 Pip도 존재한다.
그런데 이런 새로운 툴들은 모두 Distutils에 기반하고 그것의 문제점들을 그대로 물려받았다. Distutils를 적절히 수정하는 노력들은 있었지만 Distutils의 코드를 변경하면 영향받는 다른 툴들이 많았고, 심지어 그 코드는 언급한 다른 툴들에서 내부까지 깊숙히 사용되고 있었다. 이러한 것들은 전체 파이썬 생태계를 잠재적으로 퇴보시킬수 있는 것들이었다.
그러므로 우리는 Distuils를 그대로 두고, 같은 코드기반에서 하위호환성에 대한 큰 고려 없이 Distutils2를 만들기 시작했다. 무엇이, 왜 바뀌었는지 이해하기 위해 Distutils부터 자세히 보자.
14.3.1. Distutils의 기본과 설계 오류.
Distutils는 여러 커맨드를 가지고 있는데, 이것들은 각각 클래스들이고, 이 클래스는 일부 옵션을 가진 run메소드를 가지고 있다. Distutils는 또한 Distribution 클래스를 제공하는데, 이 클래스는 모든 커맨드들이 접근 가능한 전역변수들을 가지고 있다.
Distutils를 사용하기 위해 개발자는 별 특색 없는 이름인 setup.py라는 파이썬 모듈 하나를 프로젝트에 추가해야 한다. 이 모듈은 Distutils의 메인 엔트리 포인트를 호출하는 함수인 setup함수를 가지고 있다. 이 함수는 많은 옵션들을 받을 수 있다. 이 옵션들은 Distribution 인스턴스가 들고 있고 커맨드들이 사용하는 옵션들이다. 여기에 일부 표준 옵션들을 정의하는 예제가 있다. 이 옵션들은 프로젝트의 이름과 버전, 그리고 포함하는 모듈의 리스트들이다.
from distutils.core import setup setup(name=’MyProject’, version=’1.0′, py_modules=[‘mycode.py’]) |
이 setup.py 모듈은 Distutils의 커맨드중 하나인 sdist를 인자로 실행 될 수 있다. 이것은 배포원본을 압축파일로 만들고 dist 폴더에 저장한다.
$ python setup.py sdist |
같은 스크립트에 install 커맨드를 이용하여 프로젝트를 인스톨 할 수 있다.
$ python setup.py install |
Disutils는 다음과 같은 커맨드들을 제공한다.
- upload : 온라인 리파지토리에 배포판을 업로드한다.
- register : 프로젝트의 메타데이터를 온라인 리파지토리에 등록한다. 배포판 업로드는 하지 않는다.
- bdist : 바이너리 배포판을 생성한다.
- bdist_msi : 윈도우용 .msi 파일을 생성한다.
추가적으로 다른 커맨드라인 옵션들을 살펴보면 더 많은 정보를 얻을 수 있다.
이렇게 프로젝트를 인스톨하거나 프로젝트에 관한 정보를 얻는 것은 Distutils를 사용한 이 파일을 이용함으로써 이뤄진다. 예를 들어 프로젝트 이름을 얻는 방법은 다음과 같다.
$ python setup.py –name MyProject |
그러므로 setup.py는 모든 사람들이 프로젝트를 빌드하거나, 패키징하거나, 출시하거나, 인스톨하는 등의 상호작용을 하는 방법이다.
개발자는 함수에 옵션들을 넘김으로서 프로젝트의 내용을 기술한다. 그리고 모든 패키징 작업에 이 파일을 이용한다. 이 파일은 또한 프로젝트를 대상 시스템에 인스톨할때 인스톨러에 의해 사용된다.
프로젝트를 패키지하고 릴리즈하고 인스톨하기 위해 하나의 파이썬 모듈만을 쓰는 것은 Distutils의 큰 약점이다. 예를 들어, 만약 lxml 프로젝트의 이름을 알고 싶을때, setup.py파일은 간단한 문자열 하나를 얻기 위해 많은 작업을 수행할 것이다.
$ python setup.py –name Building lxml version 2.2. NOTE: Trying to build without Cython, pre-generated ‘src/lxml/lxml.etree.c’ needs to be available. Using build configuration of libxslt 1.1.26 Building against libxml2/libxslt in the following directory: /usr/lib/lxml |
심지어 일부 프로젝트에서는 이것이 실패할 수도 있다. 왜냐하면 개발자들은 setup.py가 오직 인스톨 용도이거나, 또는 개발하는 동안만 쓸 목적으로 만들었을 수도 있기 때문이다. setup.py 스크립트의 여러 역할들은 쉽게 혼란을 줄 수 있다.
14.3.2. 메타데이터와 PyPI
Distutils는 배포판을 빌드할때, PEP10) 314 표준을 따르는 메타파일을 만들어낸다. 이것은 프로젝트 이름이나 릴리즈 버전 같은 모든 일반적인 메타데이터의 정적인 버전을 가지고 있다. 중요한 메타데이터 필드는 다음과 같다.
- Name : 프로젝트 이름
- Version : 릴리즈 버전
- Summary : 한 줄짜리 설명
- Description : 세부 설명
- Home-Page : 프로젝트 URL
- Author : 제작자 이름
- Classifiers : 프로젝트 구분자. 파이썬은 개발 단계(알파, 베타, 최종)와 같이 라이선스를 위한 여러 구분자 리스트를 제공한다. 1)
- Requires, Provides, Obsoletes : 모듈들과의 의존성을 정의한다.
이러한 필드들은 다른 패키징 시스템들에 존재하는 것들 과도 쉽게 대응하는 부분이다.
PyPI(The Python Package Index)는 CPAN2)과 같은 패키지들의 중앙 리파지토리이다. 이것은 Distutils의 register와 upload 커맨드를 통해 프로젝트를 등록하고 출시하고 릴리즈한다. register는 메타데이터 파일을 빌드하고 PyPI로 보내서 사람들이나 툴(가령 인스톨러)이 웹페이지나 웹서비스를 통해 찾아 볼 수 있게 한다.
Figure 14.2 PyPI 리파지토리
프로젝트 식별자(Classifiers)를 이용해 프로젝트들을 찾고 제작자의 이름과 프로젝트 URL 정보를 얻을 수 있다. 한편, Requires는 다른 파이썬 모듈들과의 의존성을 정의하기 위해 사용된다. Requires 옵션은 프로젝트 메타데이터의 Requires항목을 추가하기 위해 사용된다.
from distutils.core import setup setup(name=’foo’, version=’1.0′, requires=[‘ldap’]) |
ldap모듈에 대한 의존성을 정의 하는 방법은 선언이 전부다. 해당 모듈이 존재한다고 보장하는 어떤 툴이나 인스톨러도 없다. 파이썬이 요구사항들을 requires키워드를 통해 모듈 수준에서 정의한다면 이것으로 충분하다. Perl이 그러하듯이 말이다. 하지만 이렇게 하고 나면 인스톨러가 PyPI에서 의존성을 찾아 인스톨할 때 문제가 된다. (CPAN이 바로 이와 같은 일을 한다.) 하지만 파이썬에서는 ldap이라는 모듈명이 어떤 프로젝트에도 존재 할 수 있기 때문에 이것이 불가능하다. Distutils에서 프로젝트를 릴리즈 할 때, 프로젝트가 여러 패키지와 모듈을 가질 수 있도록 허용한 이상 이것은 전혀 쓸모 없어졌다.
메타데이터 파일의 또 다른 문제점은 이것이 파이썬 스크립트에 의해 생성된다는 것이다. 그래서 파이썬 스크립트가 실행되어진 플랫폼에 특화된다. 예를 들어, 프로젝트가 윈도우에 특화된 기능들을 제공할 때 해당 프로젝트의 setup.py에 다음과 같이 정의 할 수 있다.
from distutils.core import setup setup(name=’foo’, version=’1.0′, requires=[‘win32com’]) |
하지만 이것은 해당 프로젝트가 오직 윈도우에서만 돌아 갈 것이라고 가정한다. 설령 프로젝트가 포터블 기능을 제공해도 말이다. 이 문제를 해결하는 한 가지 방법은 requires 옵션을 윈도우로 한정하는 것이다.
from distutils.core import setup import sys if sys.platform == ‘win32′: setup(name=’foo’, version=’1.0′, requires=[‘win32com’]) else: setup(name=’foo’, version=’1.0′) |
사실 이건 문제를 더 심각하게 만든다. 명심하건데, 이 스크립트는 PyPI를 통해 전 세계로 릴리즈될 소스 아카이브를 빌드하는데 쓰인다. 이 말은 PyPI에 올라간 정적인 메타데이터 파일이 그것을 컴파일 할 플랫폼에 의존적이라는 것이다. 다른 말로 하면, 이것이 특정 플랫폼에 특화 되어 있다고 명시할 방법이 정적으로는 없다는 것이다.
14.3.3 PyPI의 구조
Fiaure 14.3: PyPI Workflow
앞에서 언급 했듯이, PyPI는 파이썬 프로젝트들의 중앙 인덱스 서버이다. 사람들은 여기에서 카테고리로 프로젝트를 찾아 보거나 자신들의 작업을 등록 할 수 있다. 소스나 바이너리 배포판들은 업로드 되거나 기존 프로젝트에 추가 될 수 있고, 설치나 공부할 목적으로 다운로드 할 수도 있다.
프로젝트 등록과 배포판 업로드
프로젝트를 PyPI에 등록하는 것은 Distutils
register 명령으로 수행된다. 이것은 프로젝트의 메타데이터를 담고 있는 POST 요청을 생성한다. 이때 프로젝트의 버전은 관계가 없다. 이 요청은 인증 헤더를 요구하는데, 이것은 PyPI가 모든 등록된 프로젝트들에 대해 처음 프로젝트를 PyPI에 등록했던 사용자인지 인증을 수행하는데 필요하다. 인증서는 로컬의 Distutils 설정에 보관해 둘 수도 있고, register 커맨드를 실행할 때 마다 매번 입력할 수도 있다. 다음은 사용 예제이다.
$ python setup.py register running register Registering MPTools to http://pypi.python.org/pypi Server response (200): OK |
각각의 등록된 프로젝트는 HTML로 작성된 메타데이터 페이지가 생성되고, 배포판을 upload 커맨드를 사용해 PyPI에 업로드 할 수 있다.
$ python setup.py sdist upload running sdist … running upload Submitting dist/mopytools-0.1.tar.gz to http://pypi.python.org/pypi Server response (200): OK |
또한 Download-URL 메타데이터 필드를 이용하여 PyPI로 직접 업로드 하지 않고 다른 경로에서 받아가게 할 수도 있다.
PyPI 조회
PyPI는 웹 사용자를 위해 앞서 언급한 HTML페이지를 제공할 뿐만 아니라, 심플 인덱스 프로토콜(The Simple Index protocol)과 XML-RPC API의 두 가지 방식으로 내용을 조회 할 수 있는 서비스를 제공한다.
심플 인덱스 프로토콜은 http://pypi.python.org/simple/ 에서 접근할 수 있다. 이 페이지는 간단한 HTML페이지를 제공하는데, 모든 등록된 프로젝트들의 링크를 가지고 있다.
<html><head><title>Simple Index</title></head><body> <a href=’MontyLingua/’>MontyLingua</a><br/> <a href=’mootiro_web/’>mootiro_web</a><br/> <a href=’Mopidy/’>Mopidy</a><br/> <a href=’mopowg/’>mopowg</a><br/> <a href=’MOPPY/’>MOPPY</a><br/> <a href=’MPTools/’>MPTools</a><br/> <a href=’morbid/’>morbid</a><br/> <a href=’Morelia/’>Morelia</a><br/> <a href=’morse/’>morse</a><br/> </body></html> |
예를 들어, MPTools프로젝트는 MPTools/ 라는 링크를 가지고 있다. 이 말은 해당 프로젝트는 이 인덱스에 존재한다는 것이다. 이 페이지에는 해당 프로젝트와 관련된 모든 링크의 목록이 있다.
- PyPI에 저장된 모든 배포판들 링크들
- Metadata에 정의된 모든 Home URL 링크, 등록된 프로젝트 버전별로 존재
- Metadata에 정의된 모든 Download URL 링크, 등록된 프로젝트 버전별로 존재
MPTools 페이지의 내용은 다음과 같다.
<html><head><title>Links for MPTools</title></head> <body><h1>Links for MPTools</h1> <a href=”../../packages/source/M/MPTools/MPTools-0.1.tar.gz”>MPTools-0.1.tar.gz</a><br/> <a href=”http://bitbucket.org/tarek/mopytools” rel=”homepage”>0.1 home_page</a><br/> </body></html> |
인스톨러와 같은 어떤 툴이 특정 프로젝트의 배포판을 찾는다면, 인덱스페이지에 있는지 찾아보거나 단순히 “http://pypi.python.org/simple/프로젝트 이름/” 페이지를 확인하면 된다.
이 프로토콜은 두 가지의 중요한 한계를 갖는다. 첫 째로, PyPI는 현재 서버가 한 대이다. 그리고 보통은 사람들이 해당 내용의 로컬 사본을 가지고 일하는데 반하여, 일부 개발자들은 매번 빌드할 때마다 인스톨러를 통해 PyPI에서 프로젝트의 모든 의존성을 조회하여 서버를 마비시켰다. 덕분에 과거 2년간 우리는 수 차례의 서버 장애를 겪었다. 일례로, Plone 어플리케이션을 빌드하는 작업은 필요한 모든 정보를 얻기 위해 수 백개의 쿼리를 PyPI에 발생시킬 것이다. 결국 PyPI는 이것 하나 때문에 시스템 전체가 중단될 것이다.3
두 번째는, 배포판이 PyPI에 등록되지 않은채 심플인덱스 페이지에 다운로드 링크만 등록되었다면, 인스톨러는 여전히 해당 링크에 릴리즈가 있는 줄 알 것이다. 이런 간접적인 참조는 심플 인덱스를 기반으로 하는 작업의 약점이다.
심플 인덱스 프로토콜의 목적은 인스톨러가 프로젝트를 인스톨할 때 이용할 수 있는 링크 목록을 주는 것이다. 따라서 프로젝트 메타데이터는 공개되어있지 않다. 대신에 XML-RPC 방식이 등록된 프로젝트의 추가 정보를 제공한다.
>>> import xmlrpclib >>> import pprint >>> client = xmlrpclib.ServerProxy(‘http://pypi.python.org/pypi’) >>> client.package_releases(‘MPTools’) [‘0.1’] >>> pprint.pprint(client.release_urls(‘MPTools’, ‘0.1’)) [{‘comment_text’: &rquot;, ‘downloads’: 28, ‘filename’: ‘MPTools-0.1.tar.gz’, ‘has_sig’: False, ‘md5_digest’: ‘6b06752d62c4bffe1fb65cd5c9b7111a’, ‘packagetype’: ‘sdist’, ‘python_version’: ‘source’, ‘size’: 3684, ‘upload_time’: <DateTime ‘20110204T09:37:12’ at f4da28>, ‘url’: ‘http://pypi.python.org/packages/source/M/MPTools/MPTools-0.1.tar.gz‘}] >>> pprint.pprint(client.release_data(‘MPTools’, ‘0.1’)) {‘author’: ‘Tarek Ziade’, ‘author_email’: ‘tarek@mozilla.com’, ‘classifiers’: [], ‘description’: ‘UNKNOWN’, ‘download_url’: ‘UNKNOWN’, ‘home_page’: ‘http://bitbucket.org/tarek/mopytools‘, ‘keywords’: None, ‘license’: ‘UNKNOWN’, ‘maintainer’: None, ‘maintainer_email’: None, ‘name’: ‘MPTools’, ‘package_url’: ‘http://pypi.python.org/pypi/MPTools‘, ‘platform’: ‘UNKNOWN’, ‘release_url’: ‘http://pypi.python.org/pypi/MPTools/0.1‘, ‘requires_python’: None, ‘stable_version’: None, ‘summary’: ‘Set of tools to build Mozilla Services apps’, ‘version’: ‘0.1’} |
이 접근 방식에 있어서의 문제점은 클라이언트 툴의 작업을 쉽게하려는 목적으로, XML-RPC API가 게시한 일부 데이터를 정적인 파일로 저장해 놓고 심플 인덱스 페이지에도 게시 할 수 있다는 것이다. 이렇게 하는 이유는 PyPI가 이런 정보를 제공하기 위해 해야 할 추가적인 작업을 피할 수 있기 때문이다. 물론 이것이 특별한 웹 서비스에 게시된 배포판의 다운로드 수와 같은 동적인 데이터라면 괜찮다, 하지만 프로젝트의 모든 정적인 데이터를 얻기 위해 두 개의 다른 서비스를 이용해야 한다는 것은 쉽게 용납되지 않는다.
14.3.4. 파이썬의 인스톨 구조
만약 python setup.py install을 이용하여 파이썬 프로젝트를 인스톨 한다면, Distutils(이것은 표준 라이브러리에 있다.)는 파일들을 시스템에 복사할 것이다.
- 파이썬 패키지와 모듈들은 파이썬 디렉토리에 저장될 것이다. 이것들은 인터프리터가 시작될 때 로드된다. 우분투 최신버전에서는 /usr/local/lib/python2.6/dist-packages/에, 페도라에서는 /usr/local/lib/python2.6/sites-packages/에 저장된다.
- 프로젝트에 정의된 데이터 파일들은 시스템 어디든 저장될 수 있다.
- 실행가능한 스크립트들은 시스템의 bin디렉토리에 저장된다. 플랫폼에 따라서 /usr/local/bin 또는 파이썬이 설치된 경로의 bin 폴더에 저장될 수 있다.
Python 2.5이후로, 메타데이터 파일은 project-version.egg-info라는 이름으로 모듈, 그리고 패키지와 함께 저장된다. 예를 들어 virtualenv프로젝트는 virtualenv-1.4.9.egg-info파일을 가지고 있을 것이다. 이 메타파일은 시스템에 인스톨된 프로젝트의 정보를 얻는데 사용된다. 이 파일들을 뒤져보면 프로젝트 목록을 버전별로 구할 수 있기 때문이다. 그런데 Distutils 인스톨러는 자신이 인스톨한 파일들 목록을 시스템에 기록해 두지 않는다. 다시 말해, 시스템에 복사된 모든 파일들을 지울 수 있는 방법이 없다는 것이다. 이로인해 install 커맨드는 모든 설치된 파일들을 기록하는 –record옵션을 가지게 되었다. 하지만 이 옵션은 기본으로 적용되지 않는다. 그리고 Distutils문서는 이에 대해 거의 언급하지 않는다.
14.3.5 Setuptools, Pip 그리고 그 외의 것들
앞에서 언급했듯이, 일부 프로젝트들은 Distutils의 문제점을 해결하려 했지만 성공여부는 조금씩 달랐다.
의존성 문제
PyPI에 게시되는 프로젝트는 파이썬 패키지로 구조화된 여러 모듈들을 포함할 수 있다. 하지만 이를 위해, 프로젝트는 Require문을 이용하여 모듈 수준의 의존성을 정의해야 한다. 각각의 아이디어들은 나름 합당하다. 하지만 두 가지가 함께 라면 문제가 달라진다.
올바른 방법은 프로젝트 수준의 의존성을 갖는 것이다. 이것이 바로 Setuptools가 Distutils에 추가한 기능이다. 또한 PyPI를 참조하여 자동으로 의존성을 설정하는 easy_install이라는 스크립트를 제공한다. 실제로 모듈 수준의 의존성은 결코 사용되지 않는다. 대신 사람들은 Setuptools의 확장기능을 이용한다. 하지만 이런 기능들이 Setuptools에 추가되고 Distutils나 PyPI는 이를 무시하는 동안, Setuptools는 효과적으로 자신만의 표준을 확립하였고 이것은 가장 나쁜 디자인의 핵심이 되었다.
easy_install은 따라서 프로젝트 아카이브를 다운로드하고 필요한 메타데이터를 얻기 위해 그것의 setup.py 스크립트를 다시 실행해야 했다. 그리고 이것을 모든 의존성마다 반복했다. 의존성 그래프는 각각의 다운로드 이후에 하나씩 만들어 졌다.
심지어 새로운 메타데이터가 PyPI에 등록되고 조회할 수 있게 되었을때, easy_install은 여전히 모든 아카이브를 다운로드 해야했다. 왜냐하면 앞서 말했듯이 PyPI에 게시된 메타데이터는 그것을 업로드한 플랫폼 특화되어있는데, 사용되는 플랫폼은 다른 플랫폼일 수도 있기 때문이다. 하지만 프로젝트와 그 의존성들을 설치하는 기능은 대부분의 상황에서 충분히 좋았고, 쓸만한 기능이었다. 따라서 Setuptools는 광범위하게 사용되었다. 비록 몇 가지 다른 문제들에 여전히 시달리긴 했지만 말이다.
- 만약 의존성을 설치하는데 실패하면 롤백되지 않았고, 시스템은 깨진 상태가 되었다.
- 의존성 그래프는 인스톨 하면서 그때 그때 생성되었기 때문에 의존성 충돌이 일어나면 마찬가지로 깨진 상태가 될 수 있었다.
언인스톨 문제
Setuptools는 언인스톨러를 제공하지 않았다. 심지어 확장된 메타파일이 인스톨된 파일 목록을 들고 있어도 말이다. 반면 Pip는, 인스톨된 파일들을 기록하기 위해 Setuptools의 메타데이터를 확장하였다. 따라서 언인스톨을 할 수 있었다. 그러나 여전히 다른 메타데이터 커스텀 셋도 존재했다. 그 말은 파이썬 인스톨은 각각의 설치된 프로젝트에 대해 최대 4가지 종류의 메타파일을 가질 수 있다는 것이었다.
- Distutils의 egg-info. 이것은 파일 하나짜리 메타데이터이다.
- Setuptools의 egg-info. 이것은 메타데이터와 Setuptools의 추가적인 특수 옵션들을 담고 있는 디렉토리이다.
- Pip의 egg-info. 이것은 이전것의 확장된 버전이다.
- 다른 패키징시스템이 생성한 것들.
14.3.6 데이터파일은 어떠할까?
Distutils에서, 데이터 파일은 시스템의 어느 곳에든 설치될 수 있다. 만약 setup.py 스크립트에 패키지 데이터파일을 정의한다면 다음과 같은 형식이 된다.
setup(…, packages=[‘mypkg’], package_dir={‘mypkg’: ‘src/mypkg’}, package_data={‘mypkg’: [‘data/*.dat’]}, ) |
이렇게 하고 나면 mypkg프로젝트의 .dat확장자를 갖는 모든 파일들은 배포판에 포함되고 다른 모듈들과 함께 파이썬 폴더에 설치될 것이다.
배포판과는 다른 위치에 설치하려면 다른 옵션을 사용하면 된다. 하지만 정의된 위치에 놓아야 한다.
setup(…, data_files=[(‘bitmaps’, [‘bm/b1.gif’, ‘bm/b2.gif’]), (‘config’, [‘cfg/data.cfg’]), (‘/etc/init.d’, [‘init-script’])] ) |
이것은 다음과 같은 이유로 패키징하는데 있어 나쁜 소식이다.
- 데이터파일은 메타파일의 일부가 아니다. 따라서 패키지하는 사람은 setup.py를 읽어야 하고 때로는 프로젝트 코드까지 분석해야 한다.
- 개발자가 직접 데이터파일을 대상 시스템의 어디에 저장 할지 결정해서는 안된다.
- 데이터파일들의 분류가 없다. 이미지, man 페이지, 기타 모든 것들이 똑같은 방식으로 취급된다.
이러한 파일들이 포함된 프로젝트를 다시 패키지해야 하는 사람은, 자신의 플랫폼에서 올바로 작동하게 하기 위해 setup.py를 이용하는 것 외에는 선택의 여지가 없다. 그러기 위해선 그 코드를 살펴보고 해당 파일들을 사용하는 모든 라인들을 고쳐야 한다. 왜냐하면 그것을 개발한 사람은 자신의 환경에 맞는 경로에 기반하여 만들었기 때문이다. Setuptools와 Pip는 이것을 개선하지 않았다.
14.4. 향상된 표준
그래서 결국 우리는 모든 것이 파이썬 모듈 한 개에 집중 되있고, 불충분한 메타파일과 프로젝트가 가진 전부를 기록 할 수 없는, 뒤죽박죽이고 혼란스러운 패키징 환경을 갖게 되었다. 이를 개선하기 위해 우리가 할 것은 다음과 같다.
14.4.1. 메타데이터
첫 째로, 메타데이터 표준을 고치는 것이다. PEP 345는 새 버전을 정의 하고 있는데 다음과 같은 것들을 포함한다.
- 제대로된 방식의 버전 정의
- 프로젝트 수준의 의존성
- 정적인 방식의 플랫폼 특화 값 정의
버전
메타데이터 표준의 목표 중 하나는 파이썬 프로젝트에 대해 작업하는 모든 툴이 프로젝트를 같은 방식으로 식별할 수 있는 것이다.
버전의 예를 들면, 모든 툴은 “1.1” 버전은 “1.0”버전 다음에 온다는 것을 알 수 있어야 한다는 뜻이다. 하지만 만약 프로젝트가 커스텀한 버전 구조를 가지고 있다면 이것은 훨씬 어려워진다.
일관된 버전관리를 위한 단 한가지 방법은 프로젝트가 따라야 하는 표준을 공표하는 것이다. 우리가 선택한 구조는 전통적인 시퀀스 기반 구조이다. PEP 386에 정의된 바와 같이 구성은 다음과 같다.
N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN] |
- N은 정수이다. 원하는 만큼 여러개의 N들을 이용할 수 있고, 적어도 두 개이상 N이 존재한다면 ‘.’으로 그것들을 분리해야 한다.(메이저.마이너)
- a, b, c 와 rc는 알파, 베타, 릴리즈 후보(release candidate) 표시이다. 이것들은 정수 다음에 와야 한다. 릴리즈 후보는 두개의 표시가 있다. 그것은 버전 구조가 파이썬과 호환되는 rc와, 그 보다 단순한 c이다.
- dev는 dev다음에 숫자가 따라온다.
- post-release는 post다음에 숫자가 따라온다.
프로젝트 출시 프로세스에 따라, dev 또는 post 는 두개의 최종 릴리즈 사이의 모든 중간 버전으로 사용될 수 있다. 대부분의 프로세스는 dev를 사용한다.
이 구조에 따라 PEP 386은 엄격한 순서를 정의한다.
- 알파 < 베타 < 릴리즈 후보 < 최종
- dev < non-dev < poat, non-dev는 알파, 베타, 릴리즈 후보, 최종이 될수 있다.
다음은 순서 예제이다.
1.0a1 < 1.0a2.dev456 < 1.0a2 < 1.0a2.1.dev456 < 1.0a2.1 < 1.0b1.dev456 < 1.0b2 < 1.0b2.post345 < 1.0c1.dev456 < 1.0c1 < 1.0.dev456 < 1.0 < 1.0.post456.dev34 < 1.0.post456 |
이 구조의 목적은 다른 패키징 시스템들이 파이썬 프로젝트의 버전을 자신의 구조대로 변환하기 쉽게 하기 위함이다. PyPI는 PEP 345 메타데이터를 업로드 하는 프로젝트가 PEP 386을 따르지 않는 버전 넘버를 가지면 업로드를 거부한다.
의존성
PEP 345는 PEP 314의 Requires, Provides 와 Obsoletes를 대체하는 세 가지 새로운 필드를 정의한다. 이 필드는 Requires-Dist, Provides-Dist 그리고 Obsoletes-Dist 이다. 이것들은 메타파일에서 여러번 반복되어 쓰일 수 있다.
Requires-Dist의 각각의 항목은 배포가 필요한 다른 Distutils 프로젝트의 이름을 문자열로 갖는다. 이 문자열 포맷은 Distutils프로젝트 명(Name 필드에 적힌)과 같다. 그리고 옵션으로 이름 뒤 괄호 안에 버전을 정의 할 수 있다. 이 Distutils 프로젝트 이름은 PyPI에 등록된 이름과 같아야 한다. 그리고 버전이 정의되었다면 PEP 386 규칙을 따라야 한다. 다음은 그 예제이다.
Requires-Dist: pkginfo Requires-Dist: PasteDeploy Requires-Dist: zope.interface (>3.5.0) |
Provides-Dist는 프로젝트의 추가적인 이름을 정의하는데 쓰인다. 이것은 프로젝트를 다른 프로젝트와 병합할때 유용하다. 예를들어 ZODB 프로젝트는 transaction 프로젝트를 가질 수 있다.
Provides-Dist: transaction |
Obsoletes-Dist는 더 이상 사용하지 않는 다른 프로젝트를 지정하는데 쓰인다.
Obsoletes-Dist: OldName |
환경 표시
환경 표시는 필드에 실행 환경 정보를 추가하는 것으로서 각 필드 마지막에 세미콜론 뒤에 추가 할 수 있다.
Requires-Dist: pywin32 (>1.0); sys.platform == ‘win32’ Obsoletes-Dist: pywin31; sys.platform == ‘win32’ Requires-Dist: foo (1,!=1.3); platform.machine == ‘i386’ Requires-Dist: bar; python_version == ‘2.4’ or python_version == ‘2.5’ Requires-External: libxslt; ‘linux’ in sys.platform |
환경 표시를 위한 이 간단한 언어는 파이썬 프로그래머가 아니라도 이해할 수 있도록 충분히 단순하게 작성된다. 이것은 문자열들을 ==와 in 연산자(혹은 그 반대 연산자)로 비교하고, 불린(Boolean) 조합을 이용할 수 있다. PEP 345에 의하면 이러한 마커들을 사용할 수 있는 필드들은 다음과 같다.
- Requires-Python
- Requires-External
- Requires-Dist
- Provides-Dist
- Obsoletes-Dist
- Classifier
14.4.2. 무엇이 설치되는가?
모든 파이썬 툴들이 하나의 인스톨 포맷을 공유하는건 상호 호환성을 위한 의무 사항이다. 만약 인스톨러A가 기존에 Foo라는 프로젝트를 설치했고, 그 프로젝트를 찾는 B라는 인스톨러를 만든다고 하면, 이 두 인스톨러는 Foo 프로젝트의 동일한 설치정보를 공유하고 갱신해야 한다.
물론 사용자가 시스템에 하나의 인스톨러만을 이용한다면 이상적이겠지만, 무언가 특별한 기능이 추가된 새로운 인스톨러로 교체하길 원할 수도 있다. 예를들어, Mac OS X는 Setuptools를 제공하므로 사용자들은 자동적으로 easy_install 스크립트를 갖게 된다. 만약 사용자들이 다른 새로운 툴로 교체하고 싶다면, 이것은 이전버전과의 하위 호환성이 필요할 것이다.
또 다른 문제는 RPM과 같은 패키징 시스템을 가지고 있는 플랫폼에서 파이썬 인스톨러를 사용할 때, 시스템에 현재 프로젝트가 설치되고 있다고 알려줄 수 있는 방법이 없다는 것이다. 더 심각한 것은, 파이썬 인스톨러가 어떻게든 중앙 패키지 시스템에 설치되었음을 통보했을 때, 파이썬 메타파일과 시스템 메타파일을 매핑시켜야 한다는 것이다. 가령 프로젝트 이름을 예로 들 수 있는데, 이 두 이름들이 서로 다를 수 있다. 이것은 여러 문제들의 원인이 된다. 가장 흔한 문제는 이름 충돌이다. 파이썬 디렉토리와 다른 곳에 이미 존재하는 프로젝트가 RPM에 같은 이름으로 등록되어 있을 수 있다. 다른 문제는 플랫폼의 이름 규칙을 깨는 python 접두사를 가지는 이름인 경우이다. 가령, 프로젝트 이름을 foo-python이라고 지었다면, 페도라 RPM에서 python-foo라고 부를 확률이 높다.
이러한 문제들을 피하는 한가지 방법은 전역으로 설치된 파이썬 단독으로 사용하지 말고, 중앙 패키징 시스템에의해 관리되는 독립된 환경에서 작업하는 것이다. Virtualenv 와 같은 툴이 이를 수행한다.
어떤 경우이든, 우리는 파이썬에 하나의 인스톨 포맷이 필요하다. 파이썬 프로젝트를 설치할 때, 여러 툴과의 상호 호환성은 다른 패키징 시스템에게도 해결해야할 문제이기 때문이다. 특정 써드파티 패키징시스템에서 새로운 프로젝트를 자체적으로 관리하는 인스톨 정보를 이용하여 설치하고 나면, 해당 패키징 시스템에서는 파이썬 인스톨을 위한 올바른 메타데이터를 생성해야 한다. 그래서 설치된 프로젝트가 다른 인스톨러나 프로젝트를 조회하는 어떤 API에서도 설치되었음을 확인할 수 있게 말이다.
메타데이터를 매핑하는 이슈는 다음과 같은 방법이 고려될 수 있다. 파이썬 프로젝트들이 설치 정보를 내부적으로 관리한다는 사실을 RPM에서 알게 되면서, RPM은 적당한 파이썬 수준의 메타데이터를 생성하게 되었다. 예를 들어, PyPI에서 python26-webob는 WebOb로 불린다는 것을 알고 있다.
다시 표준에 관한 얘기로 돌아가서, PEP 3764)는 설치된 패키지에 대한 표준을 정의한다. 이 규격은 Setuptools와 Pip에서 사용하는 것과 매우 유사하다. 이 구조는 확장기능인 dist-info가 담고 있는 디렉토리이다. 다음과 같은 것들을 담고 있다.
- METADATA : PEP 345, PEP 314, PEP 241 에서 기술한 메타데이터
- RECORD : 설치한 파일 목록, csv 형태로 되어 있다.
- INSTALLER : 해당 프로젝트를 설치한 툴 이름
- REQUESTED : 직접적인 설치 요청에 의해 설치된 파일인지를 나타낸다. (다시말하면, 의존성에 의해 설치된 것이 아니란 뜻이다)
모든 툴들이 이 포맷을 인식하게 되면, 우리는 더 이상 특정한 인스톨러나 기능에 의존할 필요 없이 파이썬에서 프로젝트를 관리할 수 있다. 또한, PEP 376에서 메타데이터를 디렉토리로 정의했기 때문에, 기능을 확장하기 위해 새로운 파일을 추가하는 것이 쉬울 것이다. 사실은, 새로운 메타데이터파일은 RESOURES로 불린다. 다음 섹션에서 소개할텐데, 이것은 머지않아 추가될 것이다. 하지만 PEP 376은 바로 수정되지 않고, 추후 이 새로운 파일이 모든 툴에 유용해지고 나면 PEP에 추가될 것이다.
14.4.3. 데이터 파일 구조
앞에서 설명했듯이, 우리는 인스톨을 수행할 때 데이터파일을 저장할 장소를 개발자 소스를 건드리지 않고 결정할 수 있게 해야한다. 동시에, 개발자는 데이터 파일의 위치를 신경쓰지 않고 작업을 할 수 있어야 한다. 우리의 솔루션은 일반적인 방법인 간접 참조(indirection) 방식이다.
데이터 파일 사용
MPTools라는 어플리케이션이 환경설정 파일을 이용하여 작업 한다고 가정해보자. 개발자는 해당 파일을 파이썬 패키지에 넣어두고 __file__을 이용하여 접근할 것이다.
import os here = os.path.dirname(__file__) cfg = open(os.path.join(here, ‘config’, ‘mopy.cfg’)) |
이것은 환경설정 파일이 코드처럼 포함되어 있다는 것을 암시한다. 그리고 개발자는 그 파일을 코드에 명시한대로 위치시켜야 한다. 이 예제의 경우에는 config라는 서브 폴더 아래에 해당한다.
우리가 디자인한 새로운 데이터파일 구조는 프로젝트의 디렉토리 구조를 모든 파일들의 루트 디렉토리로 이용한다. 그리고 그 디렉토리 상의 모든 파일에 대해 접근을 허용한다. 그 파일이 파이썬 패키지에 들어 있든 그냥 어떤 디렉토리에 들어 있든 상관없다. 이것은 개발자로 하여금 데이터파일을 위한 전용 디렉토리를 생성하고 pkgutil.open을 이용하여 접근할 수 있게 한다.
import os import pkgutil # Open the file located in config/mopy.cfg in the MPTools project cfg = pkgutil.open(‘MPTools’, ‘config/mopy.cfg’) |
pkgutil.open은 프로젝트 메타파일을 찾아서 RESOURCES파일을 가지고 있는지 확인한다. 이것은 시스템에 있는 파일 위치에 대한 단순한 매핑이다.
config/mopy.cfg {confdir}/{distribution.name} |
여기 {confdir} 변수는 시스템의 환경설정 디렉토리를 가리키고 {distribution.name} 은 메타데이터에 있는 파이썬 프로젝트 이름이다.
14.4. 파일 찾기
이 RESOURCES 메타데이터 파일이 인스톨 할 때 만들어진다면, API는 개발자가 요구한 mopy.cfg의 위치를 찾을 것이다. 그리고 config/mopy.cfg는 프로젝트 디렉토리에 따라 달라지기 때문에, 우리는 프로젝트의 메타데이터를 적당한 위치에 생성한 다음 pkgutil이 검색하는 디렉토리에 추가함으로써 개발 모드를 제공할 수도 있다.
데이터 파일 선언
사실, 프로젝트는 데이터파일을 어디에 설치할지 setup.cfg에 연결자(mapper)를 둠으로서 정의할 수 있다. 연결자는 (glob-style pattern, target) 튜플의 리스트이다. glob-style pattern(역자주: 와일드카드가 포함된 패턴)은 프로젝트 디렉토리의 포함할 일부 파일들을 가리킨다. 반면 target은 인스톨 경로이다. 만약 경로가 변수를 갖는다면 괄호 안에 쓴다. 예를 들어, MPTools의 setup.cfg는 다음과 같은 모습이 될 수 있다.
[files] resources = config/mopy.cfg {confdir}/{application.name}/ images/*.jpg {datadir}/{application.name}/ |
sysconfig 모듈은 사용할 수 있는 변수의 목록을 제공하고 기록하며, 각 플랫폼의 기본값을 나타낸다. 예를 들어, 리눅스에서 {confdir} 의 값은 /etc 이다. 따라서 인스톨러는 인스톨할때 파일을 어디에 복사해야할 지 판단하기 위해 연결자를 sysconfig와 함께 사용할 수 있다. 그리고 앞서 언급한 RESOURCES 파일을 설치한 메타데이터에 생성하여 pkgutil이 그 파일들을 찾을 수 있도록 할 것이다.
14.5 인스톨러
14.4.4. PyPI의 개선
앞서 이야기 했듯이 PyPI는 사실상 한 곳이 중단되면 전체가 중단되는 시스템이다. PEP 380은 이 문제를 해결하기 위해 미러링 프로토콜(mirroring protocol)을 이용하여 PyPI서버가 다운되면 다른 대체 서버로 돌아가도록 하였다. 이것의 목표는 커뮤니티 멤버들이 세계 곳곳의 미러 서버들로 돌아갈 수 있게 하는것이다.
14.6 미러링(Mirroring)
미러 서버 목록은 X.pypi.python.org의 호스트 이름의 목록으로 제공된다. X는 a, b, c, …, aa, ab, … 와 같은 시퀀스 중 하나이다. a.pypi.python.org는 마스터서버이고 b와 미러링을 처음 시작한다. CNAME 레코드5) last.pypi.python.org 는 마지막 호스트 이름을 가리킨다. 따라서 PyPI를 사용하는 클라이언트들이 CNAME을 이용해 미러 서버들의 리스트를 얻을 수 있다.
예를 들어, 이 호출은 마지막 미러 서버가 h.pypi.python.org임을 나타낸다. 이 말은 PyPI는 현재 6개의 미러들을 가지고 있다는 뜻이다. (b에서 h까지)
>>> import socket >>> socket.gethostbyname_ex(‘last.pypi.python.org’)[0] ‘h.pypi.python.org’ |
잠재적으로, 이 프로토콜은 미러 서버의 IP를 로컬라이징(localizing, 역자 주: 지역적으로 가까운 서버에 앞쪽의 알파벳을 할당 함) 함으로서 클라이언트들이 요청을 가까운 미러 서버로 리다이렉트 할 수 있게 한다. 또한 어느 미러 서버나 마스터 서버가 죽었을때 다른 서버로 돌아가게 한다. 이 미러링 프로토콜은 단순한 rsync에 비해 더욱 복잡하다. 왜냐하면 우리는 다운로드에 대한 정확한 통계를 유지하고 최소한의 보안을 제공할 것이기 때문이다.
동기화
미러 서버들은 중앙 서버와 교환하는 데이터의 양을 줄여야만 한다. 이를 위해 changelog PyPI XML-RPC 호출을 이용해야 한다. 그리고 오직 지난 번 이후로 바뀐 패키지들만 다시 가져와야 한다. 각 패키지를 P라고 하면, P는 /simple/P/ 와 /serversig/P에 기록을 복사해야 한다.
만약 패키지가 중앙 서버에서 지워졌다면, 이들 미러들도 그 패키지, 그리고 그와 연관된 모든 파일들을 지워야 한다. 패키지 파일이 수정되었음을 알기 위해서, 파일의 ETag6)를 캐시해둘 수 있다. 그리고 If-None-Match 헤더를 이용해 스킵을 요청할 수 있을 것이다. 동기화가 끝나면, 미러 서버는 /last-modified를 현재 날짜로 수정한다.
통계 전파
어느 미러 서버에서 특정 릴리즈를 다운로드 받았을 때, 프로토콜은 다운로드 수가 마스터 PyPI 서버로, 그리고 다른 미러 서버로 보내졌음을 보증한다. 이것은 사람이나 툴들이 PyPI를 조회 했을때 미러를 포함한 모든 서버들을 통해서 얼마나 릴리즈가 많이 다운로드 되었는지 정확히 알 수 있게 한다.
통계는 일간과 주간으로 묶여져서 중앙 PyPI서버의 stats 디렉토리에 CSV파일로 보관된다. 각 미러들은 각자의 통계를 저장한 local-stats 디렉토리를 제공해야 한다. 각 파일들은 사용한 각 아카이브당 다운로드 횟수를 접근한 브라우저 별로 제공한다. 중앙 서버는 이러한 통계들을 수집하기 위해 미러서버들을 매일 방문하고 이 정보들을 글로벌 stats 디렉토리에 병합한다. 따라서 각 미러 서버는 /local-stats 디렉토리를 적어도 하루에 한번은 업데이트하고 있어야 한다.
미러 인증
어떤 분산 미러링 시스템이든, 클라이언트는 미러링된 데이터가 정확한지 검증하고 싶어 할 것이다. 가능한 위협은 다음과 같다.
- 중앙 인덱스 서버가 탈취되었을 가능성
- 미러가 조작되었을 가능성
- 중앙 인덱스 서버와 사용자, 또는 미러와 사용자 간의 중간자 공격(Man-in-the-Middle Attack)의 가능성
첫번째 공격을 감지 하기 위해서는, 패키지 작성자는 패키지를 PGP7) 키를 이용해 서명하여, 사용자가 검증할 수 있게 해야 한다. 중간자 공격을 감지하기 위한 일부 시도는 있지만, 미러링 프로토콜 자체는 오직 두번째 위협에 대한 고려만이 되어 있다.
중앙 인덱스 서버는 /serverkey URL에서 DSA키를 제공한다. 이것은 openssl dsa -pubout 에서 생성한 PEM 규격이다. 이 URL은 미러링 되어서는 안된다. 그리고 클라이언트들은 serverkey 를 PyPI에서 직접 가져오거나 PyPI 클라이언트 소프트웨어에서 복사한 것을 써야 한다. 미러 서버들은 키가 변경 되는 것을 감지하기 위해 키를 계속 다운로드해야 한다.
각 패키지에 대해서 미러링 된 서명은 /serversig/package 에서 제공 된다. 이것은 /simple/package 병렬 URL8)의 DSA 서명이고 DSA4와 SHA-1를 사용한 DER형식이다.
미러를 사용하는 클라리언트들은 패키지를 검증하기 위해 다음 단계를 수행해야 한다.
- /simple 페이지를 다운로드하여 SHA-1 해시를 계산한다.
- 해당 해시의 DSA 시그니처를 계산한다.
- /serversig 와 일치하는 것을 다운로드하여 2단계에서 계산된 것과 바이트 단위로 비교한다.
- 미러서버에서 다운로드한 모든 파일에 대한 MD5 해시를 계산하고 검증한다.
중앙 인덱스 서버에서 다운로드할 때는 검증이 필요하지 않다. 이는 계산하는 오버헤드를 줄이기 위함이다.
일년에 한 번 쯤, 키는 새것으로 교체된다. 미러 서버들은 모든 /serversig 페이지를 다시 가져 와야 한다. 미러 서버를 이용하는 클라이언트는 신뢰할수 있는 새 키의 복사본을 확보해야 할 필요성이 있다. 이를 위한 방법 한가지는 https://pypi.python.org/serverkey 에서 다운로드 하는 것이다. 중간자 공격을 감지하기 위해서, 클라이언트는 해당 SSL 서버 인증서가 검증된 인증기관에 의해 서명되었는지를 확인해야 한다.
14.5. 구현 상세사항
이전 섹션에서 기술한 개선 사항의 대부분은 Distutils2 에 구현되어 있다. setup.py 파일은 더 이상 사용되지 않고, 프로젝트는 완벽히setup.cfg에 .ini같은 파일로 기술되어진다. 이렇게 함으로서, 우리는 패키지하는 사람이 파이썬 코드를 신경쓸 필요 없이 프로젝트 인스톨의 동작을 변경하기 쉽게 만들었다. 다음은 이러한 파일의 예제이다.
[metadata] name = MPTools version = 0.1 author = Tarek Ziade author-email = tarek@mozilla.com summary = Set of tools to build Mozilla Services apps description-file = README home-page = http://bitbucket.org/tarek/pypi2rpm project-url: Repository, http://hg.mozilla.org/services/server-devtools classifier = Development Status :: 3 – Alpha License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1) |
[files] packages = mopytools mopytools.tests extra_files = setup.py README build.py _build.py resources = |
Distutils2 는 이 환경 설정 파일을 다음과 같은 곳에 사용한다.
- META-1.2 메타데이터 파일 생성. 이것은 PyPI에 등록하는 것과 같은 다양한 동작에 사용된다.
- sdist 같은 어떤 패키지 관리 명령어 든지 실행한다.
- Distutils2 기반의 프로젝트를 설치한다.
Distutils2 는 또한 자체적인 version 모듈을 이용하여 VERSION 을 구한현다.
INSTALL-DB 구현은 Python 3.3 표준 라이브러리에 구현하는 중이고 pkgutil 모듈에 위치하게 될 것이다. 그 전까지는 Distutils2 에 임시적으로 위치할 것이다. 제공되는 API들은 설치된 것들을 조회할 수 있게 하고 정확히 무엇이 설치되었는지 확인할 수 있게 해준다.
이러한 API들은 깔끔한 Distutils2 의 기능에 기초한다.
- 인스톨러 / 언인스톨러
- 설치된 프로젝트에 대한 의존성 그래프 뷰
14.6. 교훈
14.6.1 PEP에 관해
파이썬 패키징이 필요로 하는만큼 가능한 넓고 복잡하게 아키텍쳐를 바꾸는 것은 PEP 프로세스를 통해 표준을 바꿈으로서 조심스럽게 행해져야 한다. 그리고 새로운 PEP를 추가하거나 수정하는 것은 내 경험상 1년 정도 걸린다.
그 과정에서 일어난 커뮤니티의 한 가지 실수는 PEP에 큰 수정 없이 메타데이터를 확장하고 일부 파이썬 어플리케이션이 인스톨 방식을 수정한 툴들을 제공한 것이다.
다른 말로 하면, 당신이 이용하는 툴들, 가령 표준라이브러리 Distutils 또는 Setuptools 와 같이 어플리케이션을 각자 다른 방식으로 인스톨하는 툴들이 문제였다. 이 문제는 커뮤니티의 어느 한 부분에서 발생한 문제를 해결하기 위한 것이었다. 하지만 더 많은 문제들을 나머지 다른 세상에 일으켰다. 가령 OS 패키징을 하는 사람은, 다양한 파이썬 표준들과 마주해야 했다. 공식적으로 문서화된 표준과 공식 표준은 아니지만 Setuptools가 주도한 널리쓰이는 표준(de-facto standard)이었다.
하지만 동시에, Setuptools 는 실제 규모(전체 커뮤니티)에서 실험을 해볼 수 있는 기회를 가졌다. 일부 혁신들은 매우 발전 속도가 빨랐고 그 피드백들은 상당히 가치 있었다. 이로 인해 우리는 무엇이 효과적이었고 아니었는지 좀 더 확신을 가지고 새로운 PEP를 작성할 수 있었는데, 어쩌면 이토록 다르게 하기는 불가능했을거다. 따라서 이건 모두 문제를 해결하는데 혁신을 기여한 서드파티 툴들 덕분이다. 이것이 PEP를 변경하는데 불을 붙였다.
14.6.2. 표준 라이브러리에 들어간 패키지는 오래 못간다.
귀도 반 로썸의 말을 제목에 인용했다. 하지만 이건 우리의 노력에 영향을 주었던 배터리 포함9) 철학의 한 가지 관점이다.
Distutils는 표준라이브러리의 일부이다. Distutils2 역시 곧 그렇게 될 것이다. 표준라이브러리의 패키지는 발전시키기가 매우 어렵다. 물론 2개의 마이너 파이썬 버전 후에 API를 수정하거나 제거할 수 있는 프로세스도 있다. 하지만 한 번 API가 공개되고 나면 이것은 수년간 유지된다.
그래서 표준라이브러리의 패키지에 어떤 수정을 가한다면 이것은 버그 수정이 아니라 파이썬 생태계에 있어 잠재적인 혼란이다. 따라서 중요한 수정을 가할 때는, 새로운 패키지를 만들어야 한다.
나는 Distutils 를 통해서 이것을 힘들게 배웠다. 나는 1년이상 Distutils에 적용했던 수정들을 결국 되돌리고 Distutils2를 새로 만들어야 했다. 앞으로 만약 우리 라이브러리가 다시 크게 변한다면, 표준 라이브러리가 특정 위치에서 새로 릴리즈되지 않는 한, 독립된 Distutils3부터 만들게 될 확률이 높다.
14.6.3. 하위 호환성
파이썬에서 패키징 작업 방식을 변경하는 것은 매우 긴 과정이다. 파이썬 생태계는 과거 패키징 툴에 기반한 많은 프로젝트를 가지고 있다. 이러한 점은 변경에 있어 큰 어려움이다. (이 글에서 소개된 합의가 된 주제들은 수 년이 걸렸다. 나는 몇 개월로 예상했었다.) 파이썬3에서 모든 프로젝트가 새로운 표준으로 바뀌는 데는 수 년이 걸릴 것이다.
이것이 우리가 한 모든 것들이 이전의 모든 툴들, 인스톨, 그리고 표준과 하위 호환성을 유지한 이유이다. 이러한 점들이 Distutils2 에 나쁜 문제를 만들었다.
예를 들어, 새로운 표준을 이용하는 프로젝트가 아직 예전 표준을 사용하는 프로젝트에 의존성이 있다면, 우리는 이 의존성이 알 수 없는 포맷이어도 설치 과정을 멈출수 없다.
예를 들어, INSTALL-DB 구현은 Distutils, Pip, Distribute, 또는 Setuptools 에 의해 설치된 프로젝트들을 검색할 수 있는 호환 코드를 가지고 있다. Distutils2 역시 구 버전 Distutils 가 만든 프로젝트를 그때 그때 해당 메타데이터를 변환함으로서 설치할 수 있다.
14.7. 참조 자료 및 기여자들
문서의 일부 섹션은 우리가 패키징에 관해 썼던 여러 PEP 문서에서 직접 가져다 썼다. 원본 문서는 http://python.org 에서 볼 수 있다.
- PEP 241: Metadata for Python Software Packages 1.0: http://python.org/peps/pep-0214.html
- PEP 314: Metadata for Python Software Packages 1.1: http://python.org/peps/pep-0314.html
- PEP 345: Metadata for Python Software Packages 1.2: http://python.org/peps/pep-0345.html
- PEP 376: Database of Installed Python Distributions: http://python.org/peps/pep-0376.html
- PEP 381: Mirroring infrastructure for PyPI: http://python.org/peps/pep-0381.html
- PEP 386: Changing the version comparison module in Distutils: http://python.org/peps/pep-0386.html
패키징에 관해 작업을 한 모든 이들에게 감사하고 싶다. 그 이름들은 내가 언급한 모든 PEP 문서에 있다. 특별히 패키징 연대의 모든 이들에게 감사하고 싶다. 또한 이 챕터에 피드백을 준 Alexis Metaireau, Toshio Kuratomi, Holger Krekel 그리고 Stefane Fermigier에게 감사한다.
이 챕터에서 이야기한 프로젝트는 여기에 있다.
- Distutils: http://docs.python.org/distutils
- Distutils2: http://packages.python.org/Distutils2
- Distribute: http://packages.python.org/distribute
- Setuptools: http://pypi.python.org/pypi/setuptools
- Pip: http://pypi.python.org/pypi/pip
- Virtualenv: http://pypi.python.org/pypi/virtualenv
- http://pypi.python.org/pypi?:action=list_classifiers
- http://www.cpan.org/, 펄에서 사용하는 라이브리리 중앙 저장소
- Single point of failure : 한 곳이 중단되면 시스템 전체가 중단되는 지점. http://en.wiktionary.org/wiki/single_point_of_failure
- http://www.python.org/dev/peps/pep-0376/
- CNAME 레코드 : 특정 도메인 이름의 별칭 http://en.wikipedia.org/wiki/CNAME_record
- http://en.wikipedia.org/wiki/Etag
- 공개키 암호화 방식의 하나. http://terms.naver.com/100.nhn?docid=769505
- 여러 미러링된 서버에 동시에 접근되는 URL
- 파이썬은 거의 모든걸 다 제공한다는 걸 뜻함. http://www.python.org/about/
- PEP : Python Enhancement Proposals <http://www.python.org/dev/peps/>
파이썬의 표준을 정의하거나, 가이드라인을 제시