PyInstaller より圧倒的に優れている Nuitka の使い方とハマったポイント

この記事は Python Advent Calendar 2021 の24日目(???)の記事です。
Qiita の仕様なのかわかりませんが、25日を過ぎたあとでもなぜか申し込めてしまったので、記念として空いてた枠に入れさせてもらいました。

大晦日ギリギリに何の記事を書いてるんだ?となりそうですが、皆さんは PyInstaller で Python で作ったソフトを exe 化したい!と思った事はありますか? 私は何度かあります。

ですが、PyInstaller で作ったソフトは ファイルサイズがデカい・起動が遅い・ウイルス判定されやすい とちょっと微妙な点が多いです。
それでも比較的簡単に exe 化できるのでよく使われているわけですが、とはいえネイティブの exe ほどパフォーマンスは上がりませんし、むしろ普通に Python で実行したときより遅くなります。

ここで、PyInstaller の仕組みを簡潔に説明します。
PyInstaller は基本 --onefile オプションをつけて実行することが多いと思いますが、あれは exe の中に自己解凍機能が入っていて、実行すると Windows なら AppData\Local\Temp あたりに exe の中に入っていたパッケージ一式が解凍されます。その中に入っているスクリプトを実行することで、見た感じあたかも単一の exe ファイルで実行できるように見せかけている、というものです。
解凍されたパッケージ一式は終了時に削除されるため、ダブルクリックで実行した際にそうしたパッケージを毎回解凍することになり、それが起動時のオーバーヘッドになっています。

ほかに Python を exe 化するソフトとしてはたとえば py2exe という Windows 専用のツールなどもあったりしますが、Windows 専用な上に使い方が面倒くさく、あまり手軽とはいえません。

Sponsored Link

Nuitka の導入

GitHub - Nuitka/Nuitka: Nuitka is a Python compiler written in Python. It's fully compatible with Python 2.6, 2.7, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10. You feed it your Python app, it does a lot of clever things, and spits out an executable or extension module.
Nuitka is a Python compiler written in Python. It's fully compatible with Python 2.6, 2.7, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10. You feed it your P...
Nuitka - Wikipedia

そこで Python の公式ドキュメントにはどう書いてあるのかな?と思い漁ってみると、「Nuitka」というソフトがいくつかある中の一番上で紹介されていました。

そもそもどういうツールなのか分かりにくいのですが、PyInstaller と同じ、Python プログラムの実行ファイル化ができるツールになります。なんと Python コードを一度C言語にトランスパイルし、それをさらに gcc などのコンパイラでコンパイルしたものを実行ファイルとして出力するというんだから驚きです。

今までまったく知らなかったのですが、調べると2013年頃にはすでに開発が始まっていたようで、それなりに老舗のツールのようでした。いまだに PyInstaller 一強になっているのが不思議なくらい…。
一度C言語にコンパイルするため(Python のコードでない)ネイティブな実行ファイルとして使える上に、C言語向けのパフォーマンスチューニングが行われるため、ファイルサイズが小さくなる上に実行速度も普通に Python を実行したときよりも速くなるんだそう。すごい。
ただ、日本だと VOICEVOX のビルドに使われていたりを見かけた以外は、知名度も利用例もあまりなさそうな印象でした。

とりあえず良さそうだし、まずはインストールして使ってみましょう。
今回は Windows 上で pipenv を使います。理由は PyInstaller と同様、グローバルの pip パッケージを使ってしまうとそこにインストールされている余計なパッケージも同梱されてしまうおそれがあったためです。
とはいえ、Nuitka は賢くて、 --follow-imports オプションをつければ Python ファイルを静的解析して自動的に使ってるライブラリだけを読み込んでくれるっぽいので、あまり関係なさそうだとは思います。

ここは PyInstaller でも概ね同じだと思うのですが、Nuitka は残念ながら OS をまたいだクロスコンパイルはできないみたいです。なので、Windows 用の .exe であれば Windows 上でビルドする必要があるし、Mac や Linux の場合も然りです。

pipenv install nuitka zstandard

pipenv で、nuitka と zstandard をインストールします。

ビルド時につまづいた点なのですが、後述する --onefile オプションで単一の実行ファイルにまとめる場合、Zstandard という圧縮形式(見慣れない圧縮形式だが、そこそこ圧縮率が高くてめちゃくちゃ解凍が速いのが特徴らしい)を使ってコード全体を圧縮するみたいです。

Nuitka も単一の実行ファイルにまとめる場合は、PyInstaller と同じく Temp フォルダに自己解凍してそれを実行するような形になっています。Zstandard の解凍が速いという特性は、前述した解凍時のオーバーヘッドを抑えつつ、ファイルサイズを圧縮するのに便利なのでしょう。
Zstandard が使えなくても単一の実行ファイルにはまとめられますが、ファイルサイズが圧縮する場合の2~3倍にまで膨れあがります…。

注意点として、Python のアーキテクチャと後述する MinGW64 などのコンパイラのアーキテクチャは一致している必要があるらしいです。64bit 環境なのに Python の 32bit 版をインストールしている場合は注意してください。
今どき 32bit の exe は多くのユースケースで不要だと思うので、切り捨てちゃっていいとは思います。

Nuitka の使い方

pipenv shell

事前に pipenv の仮想環境の中に入っておきます。

nuitka --mingw64 --follow-imports --onefile (エントリーポイントにしたい Python ファイル).py

あとは上記のコマンドを実行するだけです。

--mingw64 オプションでは明示的に C コンパイラに MinGW64 内の GCC を指定します。
Visual Studio が入っているなどで MSVC が使える状況にあればそちらが自動で利用されるのですが、どうやら MSVC の言語が日本語になっていると、CP932🤮諸々の文字コード問題で Nuitka 側がビルドに失敗してしまうようです。
MSVC (MSBuild とかそのへん) は日本語環境だとコンソールに CP932 (Shift-JIS) で出力するみたいなのですが、Nuitka は英語しか想定していないため、予想されていない文字コードという事で落ちてしまうようでした。MSVC を英語版で入れてねみたいな事が公式ドキュメントに書いてありましたが、さすがにそこまでのやる気もなく…。

そもそも MSVC が入ってない場合は自動で MinGW64 を使うようになっているので、そちらを明示的に指定します。MinGW64 というのは要は Windows 上で GCC を使うためのやつなんですが、Nuitka を使うために MinGW64 をインストールしておく必要はありません。

Nuitka はとても賢くて、ビルド環境に GCC がないと分かった場合、「GCC を AppData 以下にダウンロードするけどいいですか?一回限りでキャッシュされます」みたいな事を訊かれます。そこで YES を入力すると、Nuitka 側がちゃんと動作する MinGW64 と GCC を環境を汚さずインストールしてくれて、さらにそれを自動で使ってくれます。ありがたすぎる…。
他にも depends っていうやつも自動でインストールするか訊かれるので、それも YES と答えておきましょう。2回目以降は自動でダウンロードされた GCC や depends が使われます。

最初こちらで GCC 8.1.0 の MinGW64 をインストールしたりしたのですが、最新の Nuitka は GCC 11.2 以降にしか対応していないようで、使えないと判断されて自動で最新の GCC のインストールを求められたりしてちょっと嵌まりました。Nuitka にまかせておけば自動でいい感じに GCC を使ってくれるので、任せてしまうのが手っ取り早いと思います。

--follow-imports は Python ファイルを静的解析して、インポートしているファイルを再帰的にビルド対象にいれてくれるオプションみたいです。単一ファイルと標準ライブラリで収まるソフトでないならば、これをつけないとうまく動かなくて壊れます(そりゃそう)。

最後の --onefile は PyInstaller と同様に単一の実行ファイルにまとめるオプションです。これをつけないと、現在の環境にインストールされている Python を利用する exe ファイルが生成されてしまいます。

単一の実行ファイルにまとめなくてもいい場合は --standalone オプションを代わりに指定します。
何も出力フォルダを指定しない場合、デフォルトで (エントリーポイントの py ファイルの名前).dist/ フォルダ以下に出力されます。

--standalone の場合、Python のコードはすべて exe に統合されていますが、Python 本体や依存している Visual C++ のランタイム、Tcl/Tk などのネイティブ拡張に関してはファイルとしてそのまま配置されます。実行する場合はこれら全てをセットで配置する必要があるみたいです。
今回検証に利用した EDCBNotifier の場合、サイズはフォルダ全体で 36MB ほどでした。
内訳は本体が 18MB 、Python 本体と標準ライブラリ諸々が残りの 18MB を閉めています。ファイルに何も圧縮が掛かっていないので当然っちゃそう。結構細々としたファイルでぐちゃぐちゃになるので、それが嫌な人は --onefile を使う事になると思います。

--onefile オプションを指定した場合、--standalone のファイルを生成した後にそれを前述の Zstandard で圧縮して一つの exe にまとめます。PyInstaller 同様に一度 Temp フォルダに展開してから実行する点は代わりませんが、Zstandard のおかげでファイルサイズが --standalone と比較して2倍近く圧縮される上に、PyInstaller よりも起動がかなり速いのが大きな差です。
–onefile オプションを指定した場合は、実行ファイルが (エントリーポイントの py ファイルの名前).exe に生成されます。今回の場合ファイルサイズは 12MB 弱で、1/3 ほどに圧縮できている事になります。

--onefile オプションは --standalone オプションの動作を包含するため、--onefile で実行した場合にも (エントリーポイントの py ファイルの名前).dist/ に単一の実行ファイル化されていないファイルが出力されます。
大は小を兼ねると言いますし、基本的に --onefile で良いんじゃないでしょうか。

試しに python EDCBNotifier.py PostAddReserve./EDCBNotifier.exe PostAddReserve の差を測ってみたところ、前者が 2.0 秒、後者が 2.5 ~ 2.7 秒ほどでした。解凍に 0.5 秒ほどかかるようで直接 Python コードを実行するよりも起動は遅くなりますが、それでも PyInstaller よりかはかなり速くなるんじゃないかと思います。コードの規模にもよると思いますが、C 言語に変換した分コードも速くなっているはずなので、そのあたりの恩恵もあるかもしれません。

ちなみに、Nuitka にも PyInstaller と同様に、オプションからカスタムのアプリアイコンを設定することが可能です。さらに Windows の exe のファイルバージョンのメタデータなどもオプションから細かく指定することができたりなど、至れり尽くせりで助かります。

使ってみた感想とまとめ

動作感やパフォーマンスは PyInstaller より良好で、PyInstaller を使うならこっちを使った方がよりスマートになりそうだな、という感想を持ちました。動作自体にも今のところ特に問題はなさそうに見えます。

ただ、一番のネックは「とにかくビルドが重い」という事に尽きます。PyInstaller もそれなりに時間がかかりますが、Nuitka のビルド時間はそれの比ではありません。
そもそも PyInstaller は特にソースコードをコンパイルせず単一ファイルにまとめてそれを Temp に展開しているだけなので、わざわざ C のネイティブコードに変換してそれを更にビルドしている Nuitka と比較するほうが無理がありそうです…。

一度ビルドすると、ビルド済みの C コードが (エントリーポイントの py ファイルの名前).build/ 以下に、CCache というビルドキャッシュが AppData\Local\Nuitka 以下に生成されます。
変更されたコードだけを再ビルドして変更のないコード(ライブラリ)などはそのまま使うようにして一定の時間短縮を図っているようですが、コード量も依存ライブラリもそこまで多くない EDCBNotifier ですら、何もソースコードをいじらずビルドキャッシュが効いている状態なのにも関わらず、ビルドが終わるまでに 2 分 30 秒も掛かってしまいました。
たとえば VOICEVOX のようなクソデカソフトの場合、さらに機械学習系のクソデカライブラリもビルドする必要があるので、ビルド時間が恐ろしいことになるのは想像に難くありません。

ただ、逆を言えば遅いのはビルドするときだけで、それ以外では高速に動作します。当然ユーザーに配布する場合も高速に動作することが期待できますし、開発マシンのスペックがクソ重いとかでないならば、十二分に検討する価値はあると思います。

あと、私が確認した限りでは、--onefile オプションをつけて単一の実行ファイルにまとめると、なぜか Windows 7 で実行できなくなってしまっていました。Windows 7 がサポートされているはずの Python 3.8 でもダメだったため、おそらく Nuitka 側に何らかの問題があると考えられます。
ただ、Windows 7 はすでにサポート終了から2年近く経ちますし、もうサポートを切ってもいい頃合いだとは思います。

冒頭で上げた PyInstaller のデメリットのうち、「ファイルサイズがデカい」と「起動が遅い」に関しては Nuitka を使うことで大幅な改善が期待できると思います。「ウイルス判定されやすい」という点に関してはまだわかりませんが、すくなくとも Nuitka は C 言語にコンパイルしたものを exe にしているため、コードがそのまま載っている PyInstaller よりも誤検知されにくいことが期待できるはずです。

2022 年の Python の exe 化には Nuitka を使っていこう!!ということで、今年最後の記事を〆たいと思います。

コメント