PEP 552 – 결정론적 pyc
- Author:
- Benjamin Peterson <benjamin at python.org>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 04-Sep-2017
- Python-Version:
- 3.7
- Post-History:
- 07-Sep-2017
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 pyc 형식을 더 결정론적으로 만들기 위한 확장을 제안합니다.
근거
재현 가능한 빌드(reproducible build)란 같은 소스를 빌드할 때마다 바이트 단위까지 똑같은 출력이 생성되는 빌드를 말하며, 이는 서로 다른 기계에서도 마찬가지입니다(물론 꽤 비슷한 환경이 구성되어 있어야 한다는 조건이 따릅니다). 재현 가능성은 보안에 중요합니다. 재현 가능성은 Bazel 같은 내용 기반 빌드 시스템의 핵심 개념이기도 합니다. 이러한 시스템은 출력 파일의 내용이 입력 파일 내용의 결정론적 함수일 때 가장 효과적입니다.
현재 Python pyc 형식은 모듈의 코드 객체를 마샬링한 것 앞에 magic number, 소스 타임스탬프, 소스 파일 크기를 붙인 것입니다. 소스 타임스탬프가 있다는 것은 pyc가 입력 파일 내용의 결정론적 함수가 아니라는 뜻입니다. 즉, pyc는 소스의 mtime이라는 변하기 쉬운 메타데이터에도 의존합니다. 따라서 pyc는 제대로 된 재현 가능성을 가로막는 장애물입니다.
현재 Python 코드 배포자에게 주어진 선택지는 다음과 같습니다.
- pyc를 배포하지 않고 캐싱의 이점을 포기하는 것
- pyc를 배포하고 재현 가능성을 포기하는 것
- 모든 파이썬 소스 파일에 결정론적 타임스탬프를 신중하게 부여하는 것(예를 들어 https://github.com/python/cpython/pull/296 을 참고하십시오)
- 설치 시점에 pyc를 생성하는 것처럼 1.과 2.를 복잡하게 섞는 것
이 선택지 중 어느 것도 그다지 매력적이지 않습니다. 이 PEP는 타임스탬프를 결정론적 해시로 대체할 수 있도록 허용할 것을 제안합니다. 다만 현재의 타임스탬프 무효화 방식은 계속 기본값으로 유지됩니다. 비결정성에도 불구하고, 타임스탬프 무효화는 많은 워크플로와 사용 사례에서 잘 작동합니다. 해시 기반 pyc 형식은 모든 소스 파일을 읽고 해시하는 비용을 발생시킬 수 있으며, 이는 단순히 타임스탬프를 확인하는 것보다 비용이 많이 듭니다. 따라서 당분간은 주로 배포자와 고급 사용 사례에서 이 형식을 사용할 것으로 예상합니다.
(pyc를 비결정론적으로 만들 수 있는 다른 문제 [1] [2]도 있지만, 여기서는 다루지 않는다는 점에 유의하십시오.)
명세
pyc 헤더는 현재 32비트 워드 3개로 구성됩니다. 이를 4개로 늘립니다. 첫 번째 워드는 계속해서 매직 넘버로 남으며, 바이트코드와 pyc 형식의 버전 관리를 담당합니다. 개념상 새로 추가되는 워드인 두 번째 워드는 비트 필드가 됩니다. 헤더의 나머지 부분을 해석하는 방법과 pyc의 무효화 동작은 비트 필드의 내용에 따라 달라집니다.
비트 필드가 0이면 해당 pyc는 전통적인 타임스탬프 기반 pyc입니다. 즉, 세 번째 워드와 네 번째 워드는 각각 타임스탬프와 파일 크기이며, 무효화는 소스 파일의 메타데이터를 헤더에 있는 메타데이터와 비교하여 수행됩니다.
비트 필드의 최하위 비트가 설정되어 있으면 해당 pyc는 해시 기반 pyc입니다. 두 번째로 낮은 비트를 check_source 플래그라고 부릅니다. 비트 필드 다음에는 소스 파일의 64비트 해시가 옵니다. 소스 파일의 내용에 하드코딩된 키를 사용하는 SipHash를 적용할 것입니다. MD5나 BLAKE2 같은 다른 빠른 해시도 사용할 수 있습니다. SipHash를 선택하는 이유는 Python에 이미 PEP 456에서 도입된 내장 구현이 있기 때문입니다. 다만 SipHash 키를 선택할 수 있는 인터페이스를 Python에 노출해야 합니다. 해시의 보안성은 고려 대상이 아니지만, 통제된 환경에서 Python을 쉽게 감사할 수 있도록 MD5처럼 완전히 깨진 해시는 사용하지 않습니다.
Python이 해시 기반 pyc를 만나면, 그 동작은 check_source 플래그의 설정에 따라 달라집니다. check_source 플래그가 설정되어 있으면, Python은 소스 파일을 해시한 뒤 그 해시를 pyc에 저장된 예상 해시와 비교하여 pyc의 유효성을 판단합니다. pyc를 다시 생성해야 하는 경우, check_source 플래그가 설정된 해시 기반 pyc로 다시 생성됩니다.
check_source가 설정되지 않은 해시 기반 pyc의 경우, Python은 소스 파일의 해시를 검사하지 않고 pyc를 그대로 로드합니다. 이 경우에는 어떤 외부 시스템(예: 로컬 Linux 배포판의 패키지 관리자)이 pyc를 최신 상태로 유지할 책임을 진다고 가정하므로, Python이 직접 검사할 필요가 없습니다. 검증이 비활성화된 경우에도 해시 필드는 올바르게 설정되어야 하며, 그래야 대역 외(out-of-band) 일관성 검사기가 pyc가 최신 상태인지 확인할 수 있습니다. 또한 대응하는 소스 파일이 없는 pyc는 로드하지 않는다는 PEP 3147의 규정은 해시 기반 pyc에도 계속 적용된다는 점에 유의하십시오.
py_compile과 compileall의 프로그래밍 API는 해시 기반 pyc 생성을 지원할 것입니다. 주로, py_compile은 사용 가능한 모든 pyc 무효화 모듈에 대응하는 새 열거형을 정의할 것입니다:
class PycInvalidationMode(Enum):
TIMESTAMP
CHECKED_HASH
UNCHECKED_HASH
py_compile.compile, compileall.compile_dir, compileall.compile_file에는 모두 invalidation_mode 매개변수가 추가되며, 이 매개변수는 PycInvalidationMode 열거형의 값을 받습니다.
compileall 도구에는 check_source 비트가 설정된 해시 기반 pyc와 설정되지 않은 해시 기반 pyc를 생성하기 위한 새 명령줄 옵션 --invalidation-mode가 추가됩니다. --invalidation-mode는 PycInvalidationMode의 값에 대응하는 timestamp (기본값), checked-hash, unchecked-hash 값을 받는 삼중 상태 옵션이 될 것입니다.
importlib.util에는 바이트열 source에 대해 pyc 작성 코드가 사용하는 해시를 계산하는 source_hash(source) 함수가 추가될 것입니다.
해시 기반 pyc 무효화의 런타임 구성은 새로운 --check-hash-based-pycs 인터프리터 옵션을 통해 가능해집니다. 이 옵션은 default, always, never의 3가지 값을 가질 수 있는 삼중 상태 옵션입니다. 기본값인 default는 앞에서 설명한 대로 해시 기반 pyc의 check_source 플래그가 무효화 여부를 결정한다는 뜻입니다. always는 check_source 비트의 값과 관계없이 인터프리터가 무효화 판단을 위해 소스 파일을 해시하도록 합니다. never는 인터프리터가 해시 기반 pyc를 항상 유효하다고 가정하도록 합니다. --check-hash-based-pycs=never가 적용된 상태에서는, 검사하지 않는 해시 기반 pyc가 검사하지 않는 해시 기반 pyc로 다시 생성됩니다. 타임스탬프 기반 pyc는 --check-hash-based-pycs의 영향을 받지 않습니다.
참고 문헌
감사의 말
저자는 이 PEP의 주제에 관해 유익한 대화를 나누어 준 Gregory P. Smith, Christian Heimes, Steve Dower에게 감사드립니다.
Copyright
This document has been placed in the public domain.