CLOVER🍀

That was when it all began.

PyInstallerを使って、Pythonアプリケーションから単一の実行可能ファイルを作成する

Pythonで、実行可能なバイナリを作成するにはどうしたらいいのかな?と思って調べてみたのですが、PyInstallerというものを
使えばよさそうです。

PyInstaller Manual — PyInstaller 6.4.0 documentation

GitHub - pyinstaller/pyinstaller: Freeze (package) Python programs into stand-alone executables

Pythonスクリプトを単一実行ファイルにする方法 #Python - Qiita

python3 - Python のプログラムを実行可能バイナリにコンパイルするには? - スタック・オーバーフロー

PyInstallerとは

Pythonで書かれたアプリケーションを、スタンドアロンで実行可能な形式にまとめることができるツールです。

Python 2.7、3.4〜3.7で動作し、マルチプラットフォームで動作しますが、PyInstaller自体を実行するプラットフォームとは
異なるプラットフォーム向けのバイナリは作成することができません。クロスプラットフォーム向けにバイナリを作成することは
できないので、例えば手持ちがWindows環境で、Linux環境向けのバイナリを作成しようと思ったら、LinuxVMなどで用意する
必要があるということです。

前置きはこれくらいにして、さっそく試してみましょう。

環境

今回の環境は、こちら。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.2 LTS
Release:    18.04
Codename:   bionic


$ python3 -V
Python 3.6.7

仮想環境も利用することにします。

$ python3 -m venv venv
$ . venv/bin/activate

お題とサンプルプログラム、実行可能バイナリの作成

Pythonで、起動スクリプトとは別のスクリプト、外部ライブラリを使用した簡単なアプリケーションを作成して、それを
PyInstallerで実行可能なバイナリにまとめてみます。

まずは、PyInstallerのインストール。

$ pip3 install pyinstaller

バージョン。

$ pip3 freeze
...
PyInstaller==3.4

外部ライブラリとしては、requestsとBeatiful Soupを使うことにします。

$ pip3 install requests beautifulsoup4
$ pip3 freeze
...
beautifulsoup4==4.7.1
...
requests==2.21.0
...

これらのライブラリを使って、このブログのトップページを取得して、タイトルだけを返すような関数を作成します。
func.py

from bs4 import BeautifulSoup
import requests

def get_title(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.text, 'html.parser')

    return soup.title.text

作成した関数を利用する、起動スクリプト
app.py

from func import get_title

title = get_title('https://kazuhira-r.hatenablog.com/')

print("https://kazuhira-r.hatenablog.com/'s title = {}".format(title))

確認。

$ python3 app.py 
https://kazuhira-r.hatenablog.com/'s title = CLOVER🍀

これで、準備は完了です。

では、PyInstallerでバイナリを作成します。「pyinstaller」コマンドに、起動スクリプトを引数に渡して実行。

$ pyinstaller app.py

「dist」というディレクトリができるので、この中身を見ると実行可能ファイルやSOファイルが並んでいます。

$ ll -h dist/app
合計 12M
drwxrwxr-x 3 xxxxx xxxxx 4.0K  4月 24 23:39 ./
drwxrwxr-x 3 xxxxx xxxxx 4.0K  4月 24 23:39 ../
-rwxr-xr-x 1 xxxxx xxxxx  22K 10月 22  2018 _bz2.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx 147K 10月 22  2018 _codecs_cn.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx 155K 10月 22  2018 _codecs_hk.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx  27K 10月 22  2018 _codecs_iso2022.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx 267K 10月 22  2018 _codecs_jp.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx 135K 10月 22  2018 _codecs_kr.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx 111K 10月 22  2018 _codecs_tw.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx  30K 10月 22  2018 _hashlib.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx  73K 10月 22  2018 _json.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx  33K 10月 22  2018 _lzma.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx  56K 10月 22  2018 _multibytecodec.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx 6.2K 10月 22  2018 _opcode.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx 118K 10月 22  2018 _ssl.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx 1.7M  4月 24 23:39 app*
-rw-rw-r-- 1 xxxxx xxxxx 754K  4月 24 23:39 base_library.zip
drwxrwxr-x 2 xxxxx xxxxx 4.0K  4月 24 23:39 certifi/
-rwxr-xr-x 1 xxxxx xxxxx  66K  1月 30  2017 libbz2.so.1.0*
-rwxr-xr-x 1 xxxxx xxxxx 2.5M 12月  6 00:59 libcrypto.so.1.1*
-rwxr-xr-x 1 xxxxx xxxxx 199K 12月 20  2017 libexpat.so.1*
-rwxr-xr-x 1 xxxxx xxxxx 151K  6月 29  2017 liblzma.so.5*
-rwxr-xr-x 1 xxxxx xxxxx 4.5M 10月 22  2018 libpython3.6m.so.1.0*
-rwxr-xr-x 1 xxxxx xxxxx 288K  5月 16  2017 libreadline.so.7*
-rwxr-xr-x 1 xxxxx xxxxx 424K 12月  6 00:59 libssl.so.1.1*
-rwxr-xr-x 1 xxxxx xxxxx 167K  5月 23  2018 libtinfo.so.5*
-rwxr-xr-x 1 xxxxx xxxxx 115K  5月 23  2017 libz.so.1*
-rwxr-xr-x 1 xxxxx xxxxx  32K 10月 22  2018 readline.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx  16K 10月 22  2018 resource.cpython-36m-x86_64-linux-gnu.so*
-rwxr-xr-x 1 xxxxx xxxxx  25K 10月 22  2018 termios.cpython-36m-x86_64-linux-gnu.so*

また、カレントディレクトリを見ると、「dist」ディレクトリ以外には起動スクリプト名.spec」というファイルと「build」ディレクトリが
作成されています。

$ ll
合計 36
drwxrwxr-x  6 xxxxx xxxxx 4096  4月 24 23:39 ./
drwxrwxr-x 14 xxxxx xxxxx 4096  4月 24 23:13 ../
drwxrwxr-x  2 xxxxx xxxxx 4096  4月 24 23:39 __pycache__/
-rw-rw-r--  1 xxxxx xxxxx  156  4月 24 23:38 app.py
-rw-rw-r--  1 xxxxx xxxxx  950  4月 24 23:39 app.spec
drwxrwxr-x  3 xxxxx xxxxx 4096  4月 24 23:39 build/
drwxrwxr-x  3 xxxxx xxxxx 4096  4月 24 23:39 dist/
-rw-rw-r--  1 xxxxx xxxxx  183  4月 24 23:39 func.py
drwxrwxr-x  6 xxxxx xxxxx 4096  4月 24 23:13 venv/

実行してみましょう。

$ dist/app/app
https://kazuhira-r.hatenablog.com/'s title = CLOVER🍀

動きましたね。他のスクリプトや外部ライブラリの情報まで見て、ビルドしてくれるようです。

ただ、この方法でのビルドでは、「dist」ディレクトリ配下のファイルをまるごと使う必要があり、例えばエントリとなる
実行可能ファイルだけを持ち出しても、ライブラリが足りずにエラーになります。

$ ./app 
[12] Error loading Python lib '/tmp/libpython3.6m.so.1.0': dlopen: /tmp/libpython3.6m.so.1.0: cannot open shared object file: No such file or directory

これを避けるには、完全に単一のバイナリとしてビルドする必要があります。

What PyInstaller Does and How It Does It — PyInstaller 6.4.0 documentation

単一のバイナリを作成するには、「--onefile」オプションを指定します。

$ pyinstaller --onefile app.py 

今度は、「dist」ディレクトリ内に作成されるファイルがひとつになります。

$ ll dist
合計 6072
drwxrwxr-x 2 xxxxx xxxxx    4096  4月 24 23:48 ./
drwxrwxr-x 6 xxxxx xxxxx    4096  4月 24 23:48 ../
-rwxr-xr-x 1 xxxxx xxxxx 6207248  4月 24 23:48 app*

この生成方法だと、単一のファイルを配布するだけで実行可能になります。

$ ./app 
https://kazuhira-r.hatenablog.com/'s title = CLOVER🍀

なお、このようなビルド時の指定はspec拡張子のファイルに保存されているので
app.spec

# -*- mode: python -*-

block_cipher = None


a = Analysis(['app.py'],
             pathex=['/path/to/application-directory'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='app',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=False )

仮にdistディレクトリなどをなくしても、specファイルから同じオプションで再作成することができます。

$ pyinstaller app.spec

こんな感じで。