pex を試してみる

ちょっと前に pex というものを知った。
tl;dr — pex 0.7.0-rc0 documentation


pex って何かというと、公式サイトのドキュメントには以下のようにある。

PEX files are self-contained executable Python virtual environments. More specifically, they are carefully constructed zip files with a #!/usr/bin/env python and special __main__.py that allows you to interact with the PEX runtime.

https://pex.readthedocs.org/en/stable/whatispex.html#tl-dr

簡単に説明すると、.pex というファイルに Python の仮想環境を作って、実行できるもの。
GolangJava の FatJar みたいなものという印象を受けた*1
つまり Python でもライブラリ、アプリケーションを一つにまとめて、実行ファイル形式にして、配布できて、実行できる。


Golang をやるようになって、1 つのバイナリで配布できるメリットは凄くあったので、Python でもやりたいなと思っていた。
これを叶えてくれるのが pex みたい。


という訳で色々試してみる。
まずは pex の環境を作る。

$ mkvirtualenv pex-py34 --python=/opt/local/python-3.4/bin/python3
$ pip install pex
$ pex
Python 3.4.3 (default, May 25 2015, 18:46:46) 
[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

無事に入った。
サンプルとしてドキュメントにもある Sphinx を 1 つのバイナリにしてみる。

$ pex -v sphinx sphinx_rtd_theme -e sphinx:main -o sphinx.pex
  pytz 2015.4 pex :: Resolving distributions :: Packaging MarkupSafe     
  docutils 0.12
  Babel 2.0
  Jinja2 2.8
  sphinx-rtd-theme 0.1.8
  snowballstemmer 1.2.0
  six 1.9.0
  MarkupSafe 0.23
  Pygments 2.0.2
  alabaster 0.7.6
  Sphinx 1.3.1
pex: Building pex: 9286.8ms                                         
pex:   Resolving distributions: 7982.2ms
pex:       Packaging alabaster: 514.7ms
pex:       Packaging snowballstemmer: 445.4ms
pex:       Packaging Babel: 3715.0ms
pex:       Packaging MarkupSafe: 889.9ms
Saving PEX file to sphinx.pex

以下引数の説明。
-v は verbose で標準出力のログをだす。
sphinx sphinx_rtd_theme は Sphinx のパッケージを指定する。ここで指定すると自動的にダウンロードしてパッケージ化してくれる模様。
-e ではアプリケーションのエントリーポイントを指定する。ここでは sphinx:main がそれに当たる。
-o は出力するバイナリファイルを指定する。
つまり -o で出力されたバイナリのエントリーポイントに -e で指定したものが実行されるようになる。


出力結果を見てみる。

$ ls -la
-rwxr-xr-x  1 heavenshell  admin  7783967  9  8 01:21 sphinx.pex 

ここで一旦 virtualenv を抜けて(環境が無い場所で実行してみるため)、実行をしてみる。

$ deactivate
$ ./sphinx.pex --version
Sphinx (sphinx-build) 1.3.1

無事に Sphinx が 1 バイナリになった。
pex は依存するパッケージを指定して、そのエントリーポイントを指定することで実行ファイルになってくれる。


次に PyPi などにあがっていない自分のアプリケーションを pex 化してみる。
ここでは Flask を使った簡単なアプリケーションを pex 化してみる。

.
├── pexsample
│       ├── __init__.py
│       └── app.py
└── setup.py

pexsample というパッケージに app.py がある。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Flask

app = Flask(__name__)


@app.route('/')
def index():
    return 'hello'


def main():
    app.run()


if __name__ == '__main__':
    main()

普通に起動したら以下の用になる。

$ python pexsample/app.py 
 * Running on http://127.0.0.1:5000/

これを pex 化して、以下のように起動できるようにする。

$ ./app.pex
 * Running on http://127.0.0.1:5000/

自分のアプリケーションは当然 PyPi にあがっていないので、ローカルでビルドできるようにする。
そのために setup.py が必要になる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup, find_packages

setup(
    name='pexsample',
    version='1.0',
    description='A simple PEX example',
    packages=find_packages(),
    package_dir={'': '.'}
)
$ pex -v flask . -e pexsample.app:main -o app.pex 
  Werkzeug 0.10.4 :: Resolving distributions :: Packaging MarkupSafe  
  itsdangerous 0.24
  MarkupSafe 0.23
  Flask 0.10.1
  pexsample 1.1
  Jinja2 2.8
pex: Building pex: 3536.3ms                                         
pex:   Resolving distributions: 3121.4ms
pex:       Packaging Flask: 776.0ms
pex:       Packaging pexsample: 345.8ms
pex:       Packaging itsdangerous: 368.0ms
pex:       Packaging MarkupSafe: 510.1ms
Saving PEX file to app.pex

引数に flask と カレントディレクトリ(アプリケーション、つまり setup.py がある場所)を指定する*2

$ ls -la
drwxr-xr-x  4 heavenshell  admin     136  9  8 01:29 pexsample
-rw-r--r--  1 heavenshell  admin     388  9  8 01:33 setup.py
drwxr-xr-x  7 heavenshell  admin     238  9  8 01:34 pexsample.egg-info
-rwxr-xr-x  1 heavenshell  admin  976648  9  8 01:34 app.pex
$ deactivate
$ ./app.pex
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Flask を使ったアプリケーションが 1 バイナリになった*3
つまりこれをデプロイするだけで Flask がそのまま動く。
ただバイナリをそのまま持って行けば、Python の無い環境でも動くかは未確認*4
Python の C 拡張系はどうなんだろう。
一応オプションには --platform というのがあって Linux とか Mac とか選べるようだけど。
アーキテクチャを同じものなら、動くのかもしれない。

まとめ

pex を使えば 1 バイナリになる。
自分のアプリケーションを pex 化する際に最初上手く出来なくて色々ハマったけど、色々やってみたら出来た。
もっとも今回の例みたいな Flask のみを動かすのはほぼないと思うけど…。
単純に Web アプリケーションを動かしたいだけなら、フロントに Nginx だったり、アプリケーションサーバが必要だったりで、pex を使うメリットはあまりなさそう。
じゃあどういう所で有効そうというと、例えばお客さんの本番環境に Python で作ったツール*5を入れたいけど、Python 自体はともかく pip だのライブラリ等を入れての動かせる環境作るのが難しいというシナリオにハマりそう。

*1:FatJar は 動かすには JVM が必要だけど

*2:`.` でカレント

*3:egg が出来ているので、自動的に setup.py を実行されている

*4:Python 自体もバイナリに入っているのかは未確認

*5:自分のお仕事だと locust.io をお客さんの本番に入れたかったが断念した