Python+Nimのハイブリッド環境(Nimporter)で、PyInstallerによる実行ファイル(EXE)を作成する


ずっと気になっていたハイブリッド環境

こちらの記事にて、nimporterなるクールなライブラリがあると知り、ほんわかなコメントをしたものの、ずっと気になっていたので検証してみました。

サンプルソースはこちら

PyInstallerの記事もQiita内にたくさんありますので、そちらも参照いただければと。

環境

PyInstallerとpyenvやらvirtualenv系とは相性が悪いらしいので、Pythonは仮想環境なしで動作させています。

  • Windows 10
  • Nim 1.4.2
  • Python 3.9
  • PyInstaller 4.1
  • Nimporter 1.0.2
  • Visual Studio Community 2019

開発時のトラブルなど

  • NimporterがWindows環境だとVCCを使う設定になっているようなので、VisualStudioのCommunity Editionを入れました。
  • PyInstallerが起動しないため、PythonインストールフォルダのScriptsフォルダにもパスを通したりしていました。

まずはNimporterで遊ぶ

今回のサンプルのディレクトリ構成はこちら。

ファイル・フォルダ 説明
└─ nimporter-sample プロジェクトディレクトリ
  ├─nimutils nimソース用パッケージ
  │ ├─__init__.py お約束ファイル
  │ ├─ calc.nim 計算サンプル
  │ ├─ thread_test.nim スレッドサンプル
  │ └─ uuid.nim 別ライブラリ呼び出しサンプル
  ├─ nimporter_sample.py メインのPythonスクリプト
  └─ nimporter_sample.spec.sample PyInstaller用Specファイルのサンプル

Pythonからパッケージ内のNimモジュールをインポートする

Pythonファイルと同じディレクトリになくても、パッケージ(フォルダ)内にあるNimのメソッドへのアクセスも、通常のPythonと同じようにできます。

nimutils/calc.nim
import nimpy
import strformat

proc add(a: int, b: int): int {.exportpy.} =
    echo fmt("{ a + b = }")
    return a + b
nimporter_sample.py
import nimporter
from nimutils import calc # nimutils/calc.nimをインポート

# call nim method
print(calc.add(2, 4))  # 6

Nimbleでインストールしたモジュールを利用してみる

あらかじめnimbleコマンドでインストールしたnuuidというモジュールを、nimソースでインポートし、実行結果をPythonに返すということもできます。

# モジュールインストール
$ nimble install nuuid
nimutils/uuid.nim
import nimpy
import strformat
import nuuid      # import uuid library 

proc generate(): string {.exportpy.} =
    return generateUUID()
nimporter_sample.py
import nimporter
from nimutils import uuid

print(uuid.generate())

PythonからNimのマルチスレッドを実行してみる

こちらも問題なく動作しました。

nimutils/calc.nim
import nimpy
import strformat
import os

proc threadFunc(param: tuple[a, b: int]) {.thread.} = 
    echo fmt("This is Thread-{param.a}")

proc threadTest(): int {.exportpy.} =
    var thr: array[0..1, Thread[tuple[a, b: int]]]
    echo "start threads"
    defer:
        echo "wait threads"
    thr[0].createThread(threadFunc, (1, 1000))
    thr[1].createThread(threadFunc, (2, 1000))
    sleep(1000)
    joinThreads(thr)
nimporter_sample.py
from nimutils import thread_test

# スレッド生成&実行しているメソッドを呼び出す
thread_test.threadTest()

ただし、PythonのスレッドからNimのメソッドは呼べないようです。(Issueはこちら)
そのため、現在のところNimporterを利用する場面においては、Pythonのメインスレッドからしか呼べないようです。
Webフレームワークでリクエストハンドラの中からNimのモジュールを呼ぶっていうことはできないみたいですね。

PyInstallerによるシングルExeファイルの作成

上記3パターンの呼び出しを行ったPythonファイルを、PyinstallerにてExe化し、別のWindows10環境でも動作することを確認します。

1発でExeが出来上がらないので、以下の手順で作成していきます。

  1. Pyinstallerを起動し、Specファイルを作成
  2. Pydファイル情報をSpecファイルに追加する
  3. PyinstallerをSpecファイルで起動し、Exeファイルを作成
  4. エラーが出たら足りないモジュールをSpecファイルに追加

以下、Exe起動時にエラーが出なくなるまで、3,4を繰り返します。

Specファイルの作成

シンプルな構成のPythonスクリプトであれば、Pyinstallerで1発でEXEファイルができるかもしれませんが、Pyinstallerを起動して生成されるSpecファイルを適切に修正して、Exeを作る環境を整えていきます。

まずは、エントリポイントとなるPythonスクリプトを指定して、PyInstallerを実行すると、スクリプトと同じフォルダにspecファイルが生成され、distフォルダにもExeファイルが出来上がります。
ただし、出来上がったExeファイルを起動してもエラーが出て終了してしまうため、生成されたSpecファイルに足りないモジュールなどを記述していきます。

$ pyinstaller nimporter_sample.py --onefile
・・・

$ dir dist
2021/01/04  19:36    <DIR>          .
2021/01/04  19:36    <DIR>          ..
2021/01/04  19:36         7,725,805 nimporter_sample.exe
               1 File(s)      7,725,805 bytes
               2 Dir(s)  232,853,856,256 bytes free

$ dist\nimporter_sample.exe

# 起動するとエラーが発生してしまう
Traceback (most recent call last):
  File "nimporter_sample.py", line 1, in <module>
    import nimporter
  File "c:\apps\python\python39\lib\site-packages\PyInstaller\loader\pyimod03_importers.py", line 493, in exec_module
    exec(bytecode, module.__dict__)
  File "nimporter.py", line 13, in <module>
  File "c:\apps\python\python39\lib\site-packages\PyInstaller\loader\pyimod03_importers.py", line 493, in exec_module
    exec(bytecode, module.__dict__)
  File "setuptools\__init__.py", line 24, in <module>
  File "c:\apps\python\python39\lib\site-packages\PyInstaller\loader\pyimod03_importers.py", line 493, in exec_module
    exec(bytecode, module.__dict__)
  File "setuptools\depends.py", line 6, in <module>
ModuleNotFoundError: No module named 'setuptools.py33compat'
[431764] Failed to execute script nimporter_sample

PydファイルをSpecのbinariesに追加する

Nimporterが生成するのは、Python拡張モジュール(拡張子がpyd)なので、これをPyinstallerがExeモジュールに含めるように修正します。
Specファイルは、実際はPythonスクリプトであるため、Pythonのコードを記述して設定ファイルを修正することができます。

今回のサンプルでは、nimutils フォルダ内にNimファイルを配置したので、nimutils\__pycache__ フォルダにpydが出力されたので、Specファイルを以下のように修正します。

nimporter_sample.spec
# -*- mode: python ; coding: utf-8 -*-
import os

curDir = os.getcwd()
cacheDir = os.path.join(curDir, 'nimutils', '__pycache__')
pydDir = os.path.join('.', 'nimutils')
block_cipher = None

a = Analysis(['nimporter_sample.py'],
             pathex=[curDir],
             # nimporter が生成したpydファイルをバイナリとして追加する
             binaries=[
               (os.path.join(cacheDir, 'calc.pyd'), pydDir),
               (os.path.join(cacheDir, 'thread_test.pyd'), pydDir),
               (os.path.join(cacheDir, 'uuid.pyd'), pydDir),
             ],

バイナリファイルの設定は複数指定でき、ファイル毎にタプルで指定(Pydファイルの場所, EXE起動時にどこに配置するか)します。
今回の例だと、nimutils/__pycache__/に入っているpydファイルを、Exe起動時にはnimutilsフォルダに展開せよという指定です。
展開後も__pycache__フォルダなのでは?と思ったのですが、Exe実行時は__pycache__フォルダにpydがあったとしてもそちらからは読み込まないようです。

PyinstallerをSpecファイルで起動し、Exeファイルを作成

PyInstaller の引数に、Specファイルを指定して実行します。

# Specファイルを指定して実行
$ pyinstaller nimporter_sample.spec

# distにできたExeを起動する
$ dist\nimporter_sample.exe

エラーが出たら足りないモジュールをSpecファイルに追加

PyInstallerで作成したExeを起動した際に、以下のようなエラーが出た場合、PyInstallerのSpecファイルのhiddenimportsに、モジュール名を追加することで、エラーが解消されます。


ModuleNotFoundError: No module named 'setuptools.py33compat'

何度か繰り返し、2つのモジュールをhiddenimportsに追加することでエラーが出なくなりました。

nimporter_sample.spec
  hiddenimports=['setuptools.py33compat','setuptools.py27compat'],

実行の様子

雑なキャプチャですが、ご参考まで。

まとめ

Nimporterを利用したPythonスクリプトをPyInstallerでExe化する手順を紹介しました。
ポイントとしては、Nimporterが生成したPydファイルをバイナリとして含めてあげることでしたね。

PythonのマルチスレッドからはNimporterで作成したモジュールは呼べない制限があるものの、Pythonの魅力的なライブラリを利用し、高速処理させたい部分をNimで作成するなど、ちょっとしたユーティリティをExeとして配布できるのは魅力的な環境ではないでしょうか。

とはいえ、本番で利用できる技術ではなく、ホビーユース(趣味的)な利用に限られるとは思いますが。

参考にしたサイト