Python Virtualenv

要不我们还是用回 virtualenv/venv 和 pip 吧 (链接备份)

如果你使用 Python 2,那就只能选择 virtualenv,你需要额外安装它。

venvvirtualenv 的子集,从 Python 3.3 起,被集成到 Python 标准库中。意味着 Python 3.2 以下的都用不来 venv。且 venv 有缺点,详见 virtualenv 文档。

  • python -m venv venv 会创建 venv 目录,默认是把 venv/bin/ 里面的文件都是软链接,指向全局环境的 python。
  • python -m venv --copies venv 会创建 venv 目录,--copies 把全局环境的 python 全都拷贝到 venv 目录里。
  • virtualenv -p ~/.pyenv/versions/3.8.2/bin/python --copies venv virtualenv 可以用 -p 指定拷贝哪个路径下的 python。

virtualenv 没有打包动态链接库

virtualenv 和 python 3 的 venv 都有这问题。

https://github.com/pypa/virtualenv/issues/1015 https://github.com/pypa/virtualenv/pull/1045

执行 ldd /usr/local/bin/python 看 python 依赖哪些动态链接库。

如果目标部署机器上没有对应的动态链接库,首先要把动态链接库拷贝到部署机任意位置(通常是 /usr/local/lib/)。然后在 LD_LIBRARY_PATH 加入目录路径即可。例如 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib

virtualenv 没有打包 /usr/local/lib/python3.5/

--system-site-packages --copies 即使加上这两个参数,也没有完全复制。

这里 python3.5 只是个例子,跟 python 具体版本没关系。

$ python
Could not find platform independent libraries <prefix>
Could not find platform dependent libraries <exec_prefix>
Consider setting $PYTHONHOME to <prefix>[:<exec_prefix>]
Fatal Python error: Py_Initialize: Unable to get the locale encoding
ImportError: No module named 'encodings'

Current thread 0x00007f4dffee0740 (most recent call first):
Aborted (core dumped)

encodings 这种 python 标准库,都在 /usr/local/lib/python3.5 路径下。

制作完整的 venv 离线包

两种方式,

  1. 从官方 docker 镜像中制作 venv,安装 python 依赖包。
  2. 自己编译 python,制作 venv,安装 python 依赖包。这种情况适用于 venv 拷贝到(跟镜像同一基线的)宿主机上。

方案 1

FROM python:3.8.2-slim

WORKDIR /app

SHELL ["/bin/bash", "-c"]

RUN apt-get update
RUN apt-get install -y build-essential
RUN python -m venv --copies venv
RUN mkdir -p venv/dll && \
    cp /usr/local/lib/{libpython3.8m.so,libpython3.8m.so.1.0,libpython3.so} venv/dll/ && \
    cp -rf /usr/local/lib/python3.8/* venv/lib/python3.8/

ADD requirements.txt .

RUN source ./venv/bin/activate && \
    pip install --upgrade pip && \
    pip install --no-cache-dir -r ./requirements.txt
RUN tar -czf ./venv.tgz -C ./venv .

CMD [ "bash" ]

方案 2

FROM centos:7

WORKDIR /app

SHELL ["/bin/bash", "-c"]

# See https://github.com/pyenv/pyenv/wiki/Common-build-problems
RUN yum install -y @development \
      zlib-devel bzip2 bzip2-devel readline-devel \
      sqlite sqlite-devel openssl-devel xz xz-devel libffi-devel findutils

RUN curl https://pyenv.run | bash
RUN export PATH="/root/.pyenv/bin:$PATH" &&\
    eval "$(pyenv init -)" &&\
    eval "$(pyenv virtualenv-init -)" &&\
    CFLAGS=-I/usr/include/openssl LDFLAGS=-L/usr/lib64 pyenv install -v 3.8.2

RUN export PATH="/root/.pyenv/bin:$PATH" &&\
    eval "$(pyenv init -)" &&\
    eval "$(pyenv virtualenv-init -)" &&\
    pyenv shell 3.8.2 &&\
    python -m venv --copies venv

RUN mkdir -p venv/dll && \
    cp /root/.pyenv/versions/3.8.2/lib/{libpython3.8m.so,libpython3.8m.so.1.0,libpython3.so} venv/dll/ && \
    cp -rf /root/.pyenv/versions/3.8.2/lib/python3.8/* venv/lib/python3.8/

RUN source ./venv/bin/activate && \
    pip install --upgrade pip

ADD requirements.txt .
RUN source ./venv/bin/activate && \
    pip install -r ./requirements.txt

RUN tar -czf ./venv.tgz -C ./venv .

CMD [ "bash" ]

docker cp 导出 venv 包

main() {
  local image=$1
  local venv_file=$2

  local cid
  cid=$(docker create "$image")
  docker cp "$cid":/app/venv.tgz "$venv_file"
  docker rm "$cid"

  # tar -xzf "$venv_file" -C "$output"
}

使用包需要重新路径

#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit

if (( $# < 1 )) || [[ ${1:-} == '-h' ]] || [[ ${1:-} == '--help' ]]; then
  cat <<EOF
USAGE: $0 <venv-path> [<replace>=<venv-path>] [<match>=/app/venv]

ARGS:
<venv-path>     Relative or absolute path. Example: './python/venv'.
<replace>       Absolute path. Example: '/opt/xxx/venv'.
<match>         Absolute path. Default to '/app/venv'.
EOF
  exit 0
fi

replace_file() {
  local file
  while read -r file; do
    echo "To modify file: $file"
    sed -i'' "s#$match#$replace#g" "$file"
  done
}

main() {
  local venv_dir
  venv_dir=$(realpath "$1")
  local replace=${2:-${venv_dir}}
  local match=${3:-/app/venv}

  echo "venv_dir: $venv_dir"
  echo "match: $match"
  echo "replace: $replace"

  grep -l -r "$match" "$venv_dir" | grep -v '__pycache__' | grep -v '.pyc' | replace_file
}

main "$@"

注意不要把 .pyc 文件修改了,会导致某些包失效。