LoginSignup
8
5

More than 1 year has passed since last update.

環境に合わせたpip downloadコマンドを自動生成する

Posted at

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形式のモジュールをダウンロードするためのコマンドを生成するスクリプトは、こちら。

print_pip_download_wheels.py
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コマンド側から絞って問い合わせに行っている形になるので、通常の使い方とは異なっているのかな、と。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5