인터넷이 되는 곳에서 패키지와 그 의존성을 한꺼번에 내려받아 폐쇄망으로 옮겨 설치하는 절차다. 옮길 대상 환경의 Python 버전과 플랫폼을 정확히 맞추지 않으면, 받을 때는 멀쩡하고 설치할 때 실패한다.
pip download 는 설치하지 않고 내려받기만 하며, 의존성은 기본으로 따라온다.
python3.10 -m pip download numpy pandas \
--only-binary=:all: \
--dest /tmp/wheelhouse
--only-binary=:all: 을 주면 소스 배포판(.tar.gz)을 받지 않는다. 소스가 섞이면 폐쇄망 쪽에서 빌드를 시도하다가 컴파일러와 헤더가 없어 실패한다.
받는 서버와 설치할 서버가 다르면 반드시 대상 환경을 지정한다. 지정하지 않으면 받는 서버 기준으로 고른다.
python3 -m pip download -r requirements.txt \
--python-version 310 \
--implementation cp \
--abi cp310 \
--platform manylinux2014_x86_64 \
--only-binary=:all: \
--dest /tmp/wheelhouse
| 옵션 | 뜻 |
|---|---|
--python-version |
대상 Python 버전 |
--implementation cp |
CPython |
--abi cp310 |
대상 ABI 태그 |
--platform |
대상 플랫폼 태그 |
플랫폼 태그는 glibc 버전과 묶여 있다. RHEL 8 계열은 glibc 2.28 이라 manylinux2014_x86_64(glibc 2.17 기준) 휠이 안전하게 돈다. RHEL 9 라면 manylinux_2_28_x86_64 휠도 쓸 수 있다. 대상 서버의 값을 직접 확인해 맞춘다.
ldd --version | head -1
python3 -c "import sysconfig; print(sysconfig.get_platform())"
python3 -m pip debug --verbose | sed -n '/Compatible tags/,+15p'
이 옵션들을 쓰면 pip 은 소스 배포판을 쓸 수 없으므로 --only-binary=:all: 이 사실상 강제된다. 순수 파이썬 패키지도 휠이 있으면 문제없다.
한 패키지에서 오류가 나면 전체가 중단된다. 흔한 사유는 요구 Python 버전이 맞지 않는 것이다.
ERROR: Ignored the following versions that require a different python version:
0.5.0 Requires-Python >=3.6,<3.8
ERROR: Could not find a version that satisfies the requirement <pkg>
되는 것만 받고 실패 목록을 남기려면 줄 단위로 돌린다. pip 옵션 하나로는 되지 않는다.
#!/bin/bash
PYTHON=/opt/python/3.10.14/bin/python3.10
REQ=requirements.txt
DEST=/tmp/wheelhouse
FAILED=failed.txt
mkdir -p "$DEST"
: > "$FAILED"
grep -vE '^\s*(#|$)' "$REQ" | while IFS= read -r pkg; do
if ! "$PYTHON" -m pip download "$pkg" \
--only-binary=:all: --dest "$DEST" >>download.log 2>&1; then
echo "$pkg" >> "$FAILED"
fi
done
echo "실패 $(wc -l < "$FAILED") 건"
cat "$FAILED"
실패 목록이 나오면 그 패키지들만 따로 본다. 요구 버전이 맞지 않는 것은 대상 Python 에서 쓸 수 있는 버전으로 고정하고, 휠이 아예 없는 패키지는 소스 빌드를 각오하거나 대체를 찾는다.
디렉터리를 통째로 옮긴 뒤 그 디렉터리만 보고 설치한다.
tar czf wheelhouse.tar.gz -C /tmp wheelhouse
# 옮긴 뒤
python3.10 -m pip install --no-index --find-links=/tmp/wheelhouse -r requirements.txt
--no-index 가 핵심이다. 이것을 빼면 pip 이 PyPI 에 접속하려다 시간만 끌고 실패한다.
설치가 "의존성을 찾을 수 없다" 로 실패하면 대개 wheelhouse 에 그 의존성의 휠이 빠져 있는 것이다. 받을 때 이미 설치돼 있던 패키지는 pip 이 다시 받지 않기 때문에, 깨끗한 가상환경에서 받는 편이 누락이 적다.
python3.10 -m venv /tmp/collect && /tmp/collect/bin/python -m pip install -U pip
/tmp/collect/bin/python -m pip download -r requirements.txt --only-binary=:all: --dest /tmp/wheelhouse
폐쇄망 환경에서 from pydantic import BaseModel 같은 코드가 깨진다면, 패키지가 없어서가 아니라 버전이 맞지 않아서인 경우가 많다. 사내 저장소에 특정 메이저 버전만 올라와 있는 상황이 대표적이다.
먼저 무엇이 설치돼 있고 pip 이 어디를 보는지 확인한다.
python3 -m pip --version
python3 -m pip config list
python3 -m pip config debug
env | grep -Ei 'PIP|INDEX|PROXY'
python3 -m pip show pydantic
python3 -m pip index versions pydantic
pip config debug 는 어떤 설정 파일이 어떤 순서로 읽혔는지까지 보여 준다. 사용자 설정과 전역 설정이 서로 다른 인덱스를 가리켜 "왜 그 버전이 설치되는지" 를 설명하지 못하는 상황이 여기서 드러난다.
사내 저장소에 필요한 버전이 없다면 애플리케이션 쪽을 있는 버전에 맞추거나, 저장소에 해당 버전을 올려야 한다. 메이저가 다른 버전은 API 가 달라 그대로 돌아가지 않는다 — 예를 들어 Pydantic 1 과 2 는 검증 API 와 설정 방식이 다르다.
conda 패키지는 PyPI 가 아니라 conda 채널에서 오고, 의존성 해석 방식도 다르다. 그래서 폐쇄망에서 pip 으로 conda 를 설치하려는 시도는 다음처럼 끝난다.
ERROR: Could not find a version that satisfies the requirement pycosat (from versions: none)
conda 를 써야 한다면 설치 프로그램 자체를 옮기고, 필요한 채널을 미러하거나 conda-pack 으로 만든 환경을 통째로 옮긴다. pip 휠 수집과 섞어서 해결하려 하지 않는다.
수백 개 규모의 요구사항 목록을 한 번에 받으려 하면 중간에 하나만 실패해도 처음부터 다시 하게 된다. 반드시 줄 단위 루프로 돌리고 로그를 남긴다.
requirements.txt 에 버전이 고정돼 있지 않으면 받은 시점과 설치 시점의 조합이 달라질 수 있다. 폐쇄망 반입 전에 pip freeze 로 고정본을 만든다.