Pyinstaller を自作ライブラリ/サービスに対して使用するときのチュートリアル① (階層構造モジュールの参照)

**@kenmaro**です。
普段は主に**秘密計算、準同型暗号などの記事について投稿しています**。
秘密計算に関連するまとめの記事に関しては以下をご覧ください。

> - [秘密計算エンジニアを始めて１年が経った。](https://qiita.com/kenmaro/items/416657efca2ff296169b)
-  [秘密計算エンジニアを始めて２年半が経った。](https://qiita.com/kenmaro/items/48059dd60f79e80f5ef8)
- [最先端の秘密計算技術、格子暗号スタディロードマップを公開！！（エンジニア、リサーチャー必読）](https://qiita.com/kenmaro/items/f2d4fb84833c308a4d29)

## はじめに

このチュートリアルは２本立てです。

次の記事はこちらから

- [Pyinstaller を自作ライブラリ/サービスに対して使用するときのチュートリアル② (pip モジュールの参照)](https://qiita.com/kenmaro/items/83d548ebde6d466345dd)

## 概要

https://www.pyinstaller.org/


python で書かれたスクリプト、ライブラリなどをバイナリ化し、ソースコードが見えないようにした状態で配布することはよくあると思います。
そんなときにpyinstallerを使うと便利であり、いろいろな解説記事なども転がっているため比較的取り組みやすいと思います。

しかしながら、解説記事などではあくまでも簡単なhello_world的なプログラムをバイナリ化するような記事が多く、

- 実際に割と大きめのライブラリ（階層構造などを持ったもの）をバイナリに書き出したい。
- pip install したライブラリを使用しているプログラムをバイナリに書き出したい。

と言うような実運用上必ず通るようなケースでどのようにpyinstallerを用いればよいか、
まとまっているものがあまり無いように感じたため、今回は運用の仕方についてまとめてみます。





## この記事でわかること
pyinstaller を用いてpython プログラムをバイナリ化して配布する際、

- 階層構造を持ったライブラリをバイナリ化するにはどうすればいいか


がわかります。


- プログラムがpip install したライブラリを使用しているときにそれらも含めてバイナリ化するにはどうしたらいいか

に関しては、少し説明が長くなってしまったため、別記事に分けますので、そちらもぜひご覧ください。




## 階層構造を持ったライブラリをバイナリ化するにはどうすればいいか

以下の２ステップを行うことが必要となります。

1. setuptools (setup.py) を用い、Cythonを使ってライブラリをso(shared object)ファイル化します。
2. 1で作成したsoファイルをpyinstaller にhookを使用して読み込ませ、ライブラリをビルドします。

やってみるとある程度単純なので、これから順を追って説明します。

### 階層構造

例として、このような階層構造があったとします。

```
main.py
myproject/
  core.py
  server/
    controller/
      controller.py
    service/
      service.py
```
例なので適当に書いていますが、server はコントローラーとサービスを持ち、
main.py にてserverを立ち上げるような構成にしています。
core.pyは、この例ではあまりいらないかもしれませんが、コアとなる関数はここに実装されているようなイメージにしてみました。

このとき、例えば

```
python main.py server
```

とやると、serverが立ち上がり（例えばgrpcサーバや、flaskサーバなどを想定しています）
ポート８０００とかでエンドポイントを作るようなサービスです。

このような構成を持つライブラリ（サービス？）をpyinstallerを用いてバイナリ化するのが今回の目的の１つめです。

### 1. setuptools を用いてsoファイル化

setuptools を用いて、ライブラリをビルドします。具体的には、

setup.pyファイルを用意し、
```
~/.pyenv/versions/myenv/bin/python setup.py build_ext --inplace
```
このコマンドを実行します。
ここではpyenv環境を使っていますが、どのpythonを呼び出すかはお使いの環境に合わせてください。

setup.py はこのように書きます。

```setup.py
from setuptools import setup, Extension

from Cython.Build import cythonize
import Cython.Compiler.Options
Cython.Compiler.Options.docstrings = False

ext_modules = cythonize(
    [
        #core
        Extension("myproject.core", ["myproject/core.py"]),

        #server controller
        Extension("myproject.server.controller.controller", ["myproject/server/controller/controller.py"]),

        #server service
        Extension("myproject.server.service.service", ["myproject/server/service/service.py"]),  


    ], compiler_directives=dict(
            language_level="3", 
            always_allow_keywords=True
        )
)

setup(
    name="myproject",
    version='1.0.0',
    ext_modules=ext_modules,
    author="X Inc.",
    author_email="info@xinc.co.jp",
)
```

これを実行することで、各階層にsoファイルが生成されます。
次のステップ２では、ここで作成したsoファイルをpyinstaller にhookを使用して読み込ませ、ライブラリをビルドします。


### 2. pyinstaller にhookを使用して読み込ませ、ライブラリをビルド
以上のsoファイルが準備できたら、pyinstaller を実際に使いまとめます。

#### プロジェクトをpyinstaller フォルダ下にコピー

以下のコマンドを実行します。

```bash
cp -R myproject ./pyinstaller/myproject

#delete original py
find pyinstaller/myproject/ -name "*.py" -type f -delete
find pyinstaller/myproject/ -name "*.c" -type f -delete

```

を行い、myprojectをまるごとpyinstallerフォルダの中にコピーします。
そのあと、必要のないソースコードや、setuptoolsのCythonによる中間出力であるC言語のファイルなどを消去しています。


#### `__init__.py` の配置
これが済んだら、各階層に `__init__.py` を配置します。

```bash
#myproject
touch pyinstaller/myproject/__init__.py

# server
touch pyinstaller/myproject/server/__init__.py
touch pyinstaller/myproject/server/controller/__init__.py
touch pyinstaller/myproject/server/service/__init__.py

```

#### hook の配置
これが済んだら、hookを用意します。
このhookは、pyinstallerがmain.pyをバイナリ化するときに、どのライブラリを依存関係として参照すればよいか、
明示的に書くものになります。

`pyinstaller/hooks` フォルダを用意し、
`pyinstaller/hooks/hook-myproject.py`
を用意します。

```pyinstaller/hooks/hook-myproject.py
hiddenimports = [
"myproject.core",
"myproject.server.controller.controller",
"myproject.server.service.service",
]
```

このように `hiddenimports`とすることで、
pyinstaller を実行する際、これらのモジュールを依存関係としてリンクし、目的であるmain.pyのバイナリ化を実行してくれます。


#### pyinstaller の実行

ここまで用意できたら、以下を実行します。

```
~/.pyenv/versions/myenv/bin/pyinstaller -D --clean --additional-hooks-dir ./hooks main.py
```

これにより、main.py をバイナリ化し、./hooks の中のhiddenimportsたちを依存関係としてリンクした上で、
実行ファイルを生成してくれます。
生成されたファイルは、`pyinstaller/dist/myproject/myproject`
となり、これを実行バイナリとして使うことができるようになります。


## まとめ
今回は、「pyinstaller を自作ライブラリ/サービスに対して使用するときのチュートリアル」
として、実際のサービスをpyinstallerを用いてバイナリ化して配布する際に、
どのように活用すればよいかについてまとめてみました。

今回は自作ライブラリ、サービスの「階層構造を持った」ものに対して、
hookを使って依存関係を含めてビルドする、というところに焦点を当てました。

次の記事では、自作ライブラリが pip install したライブラリに依存している場合（普通に考えて必ず何かのライブラリを使用しているはずなので、実運用上これは避けて通れないかと思います。）、
どのようにしてpyinstaller でバイナリ化するかについて書きたいと思います。


今回はこのへんで。

@kenmaro