What's?
pipを使ってモジュールインストールを行う際に、その環境がオフラインだったりするとちょっと困ります。
そうなると、pip download
でモジュールを事前にダウンロードしておいて、実際の環境ではローカルインストールを行うことになるわけですが。
Installing from local packages
このダウンロードを行うのがなかなか面倒だったので、pipコマンドの結果から自動生成するようにしてみました、という話です。
pip download
pip download
を使うと、Pythonのモジュールをローカルにダウンロードできます。
できるのですが、特になにも指定せず実行すると「pipを実行した環境で選ぶべきモジュール」を選定します。
この場合、wheelのようにパッケージにプラットフォームの情報が含まれるようなものは、pip download
を実行する環境とpip install
を実行する環境が異なる場合は困ったことになります。
この情報は、pip debug
で確認することができます。
$ pip3.8 debug
WARNING: This command is only meant for debugging. Do not use this with automation for parsing and getting these details, since the output and options of this command may change without notice.
pip version: pip 19.3.1 from /usr/lib/python3.8/site-packages/pip (python 3.8)
sys.version: 3.8.3 (default, Aug 31 2020, 16:03:14)
[GCC 8.3.1 20191121 (Red Hat 8.3.1-5)]
sys.executable: /usr/bin/python3.8
sys.getdefaultencoding: utf-8
sys.getfilesystemencoding: utf-8
locale.getpreferredencoding: UTF-8
sys.platform: linux
sys.implementation:
name: cpython
Compatible tags: 52
cp38-cp38-manylinux2014_x86_64
cp38-cp38-manylinux2010_x86_64
cp38-cp38-manylinux1_x86_64
cp38-cp38-linux_x86_64
cp38-abi3-manylinux2014_x86_64
cp38-abi3-manylinux2010_x86_64
cp38-abi3-manylinux1_x86_64
cp38-abi3-linux_x86_64
cp38-none-manylinux2014_x86_64
cp38-none-manylinux2010_x86_64
...
[First 10 tags shown. Pass --verbose to show all.]
ポイントは、Compatible tags
の部分ですね。
Compatible tags: 52
cp38-cp38-manylinux2014_x86_64
cp38-cp38-manylinux2010_x86_64
cp38-cp38-manylinux1_x86_64
cp38-cp38-linux_x86_64
cp38-abi3-manylinux2014_x86_64
cp38-abi3-manylinux2010_x86_64
cp38-abi3-manylinux1_x86_64
cp38-abi3-linux_x86_64
cp38-none-manylinux2014_x86_64
cp38-none-manylinux2010_x86_64
...
[First 10 tags shown. Pass --verbose to show all.]
省略されている分は、-v
オプションを付与することで出力できます。
※大量に出力されるので、載せませんが
$ pip3.8 debug -v
今回は、この結果を使ってpip download
でプラットフォームやPythonのバージョンを指定するようなコマンドを自動生成してみたいと思います。
注意点としては、pip debug
の出力にあるように、この結果を使うこと自体はオススメされていないということですね。
WARNING: This command is only meant for debugging. Do not use this with automation for parsing and getting these details, since the output and options of this command may change without notice.
では、進めていきましょう。
環境
今回の環境は、こちら。CentOS 8.3を使います。
$ cat /etc/redhat-release
CentOS Linux release 8.3.2011
$ uname -srvmpio
Linux 4.18.0-240.22.1.el8_3.x86_64 #1 SMP Thu Apr 8 19:01:30 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
Pythonは、3.8をインストールします。
$ sudo dnf install python3.8
バージョン。
$ python3.8 -V
Python 3.8.3
$ pip3.8 -V
pip 19.3.1 from /usr/lib/python3.8/site-packages/pip (python 3.8)
このインストール方法だと、python
コマンドはpytyhon3.8
に、pip
コマンドはpip3.8
となります。
考え方
Pythonのモジュールをダウンロードする際には、現在はwheelとソース配布物を主に見れば良さそうです。
pip download
で--platform
とか--python-version
を指定する場合は、この両者を分けて考える必要がありそうです。
ソース配布物の場合
ソース配布物をダウンロードする場合は、以下のコマンド固定で良いでしょう。
$ pip3.8 download -d lib \
--no-binary=:all: \
--no-deps \
-r requirements.txt
ここで、requirements.txt
にはソース配布物のみのモジュール名が含まれているものとします。
pip debug
の結果を使うのは、wheelの方です。
wheel
pipでのモジュールインストール先に合った、wheel形式のモジュールをダウンロードするためのコマンドを生成するスクリプトは、こちら。
import re
import subprocess
download_dir = 'lib'
pip_command = 'pip3.8'
pip_debug = subprocess.getoutput(f'{pip_command} debug -v')
tags = re.split(r'Compatible tags: .+\r?\n', pip_debug)[1]
tags = re.sub(r' +', '', tags)
python_versions = set()
implementations = set()
abis = set()
platforms = set()
for tag in re.split(r'\r?\n', tags):
python_version_implementation, abi, platform = re.split(r'-', tag)
m = re.search(r'(?P<implementation>[a-z]+)(?P<python_version>\d+)', python_version_implementation)
python_versions.add(m.group('python_version'))
implementations.add(m.group('implementation'))
abis.add(abi)
platforms.add(platform)
print(f'''{pip_command} download -d {download_dir} \\
--only-binary=:all: \\
--no-deps \\'''
)
## platform
platform_list = sorted(list(platforms))
if 'any' in platform_list:
print(f' --platform any \\')
platform_list.remove('any')
for platform in platform_list:
print(f' --platform {platform} \\')
## python-version
for python_version in sorted(list(python_versions)):
print(f' --python-version {python_version} \\')
## implementation
implementation_list = sorted(list(implementations))
if 'py' in implementation_list:
print(f' --implementation py \\')
implementation_list.remove('py')
for implementation in implementation_list:
print(f' --implementation {implementation} \\')
## abi
abi_list = sorted(list(abis))
if 'none' in abi_list:
print(f' --abi none \\')
abi_list.remove('none')
for abi in abi_list:
print(f' --abi {abi} \\')
print(' -r requirements.txt')
pip
コマンドは、pip3.8
に固定していますが、環境に合わせて変更を…。
このスクリプトを、pip install
を実行する環境と同じ条件の環境で実行すると、以下のような結果が得られます。
$ python3.8 print_pip_download_wheels.py
pip3.8 download -d lib \
--only-binary=:all: \
--no-deps \
--platform any \
--platform linux_x86_64 \
--platform manylinux1_x86_64 \
--platform manylinux2010_x86_64 \
--platform manylinux2014_x86_64 \
--python-version 3 \
--python-version 30 \
--python-version 31 \
--python-version 32 \
--python-version 33 \
--python-version 34 \
--python-version 35 \
--python-version 36 \
--python-version 37 \
--python-version 38 \
--implementation py \
--implementation cp \
--abi none \
--abi abi3 \
--abi cp38 \
-r requirements.txt
ここでのrequirements.txt
はwheel形式のもののみが含まれている前提にしています。
あとは、これをシェルスクリプトにでも保存して実行すればよいでしょう。
pip debug
の結果は、実行する環境によって変わるので、実際にpip install
を使う環境と同じものを使って確認するべきですね。
ポイント
どうも、--implementation
ではpy
を、--api
ではnone
を先に持ってきた方が良さそうです。
あと、ソース配布物、wheelともに--no-deps
を入れているのは、モジュールの依存先に逆パターン(ソース配布物がwheelに依存する、wheelがソース配布物に依存する)があるとpip download
コマンドの設定と矛盾してダウンロードに失敗するからです。
よって、ダウンロード結果からpip install
する時も、やはり--no-deps
を付けた方が良いですね。
# wheel
$ pip3.8 install --no-index --find-links=lib --no-deps -r requirements-wheels.txt
# ソース配布物
$ pip3.8 install --no-index --find-links=lib --no-deps -r requirements-sdists.txt
また、wheelを先にpip install
しておかないと、ソース配布物にwheelへの依存が含まれていた場合に依存するwheelを取得しようとしてやっぱり困ったことになります。
というわけで、このやり方を使う場合は、requirements.txt
はwheelとソース配布物で分けた方がいいでしょうね…。
ふだんのpip install
でこういう悩みに合わないのは、pip install -v
などで挙動を見ていると、リポジトリが持っているモジュールのリストから自分がどれを選ぶべきかをpip側で確認しているから、な気がしますね。
今回はpipコマンド側から絞って問い合わせに行っている形になるので、通常の使い方とは異なっているのかな、と。