CLOVER🍀

That was when it all began.

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

これは、なにをしたくて書いたもの?

Pythonアプリケーションから実行可能バイナリを生成することができるものとしては、PyInstallerが有名です。

PyInstaller Manual — PyInstaller 6.3.0 documentation

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

このブログでも扱ったことがあります。

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

なのですが、別のツールとしてNuitkaというものがあることを知ったので、今回はこちらを試してみたいと思います。

Nuitka

NuitkaのWebサイトはこちら。

Nuitka the Python Compiler — Nuitka the Python Compiler documentation

GitHubリポジトリーはこちらです。

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

NuitkaはPythonで書かれた最適化Pythonコンパイラーで、インストーラーを必要としない実行可能バイナリを作成できるものだと
されています。

Nuitka is the optimizing Python compiler written in Python that creates executables that run without an need for a separate installer.

Nuitka the Python Compiler / What is Nuitka

形態としてはスタンダード版と商用版があり、スタンダード版でコード、依存関係およびデータをひとつの実行可能ファイルに
バンドルできます。高速化や拡張モジュールの利用もできるとされています。

商用版では実行可能ファイルやコード、データなどを保護することができるようになるとされています。

ユーザーマニュアルはこちら。

Nuitka User Manual — Nuitka the Python Compiler documentation

Nuitkaに必要なものは、以下のようです。

Nuitka User Manual / Requirements

ちなみに、ユースケースを見てみると実行可能ファイルを生成するだけではなくて、拡張モジュールを生成したりもできるようです。

Nuitka User Manual / Use Cases

今回は単純なPythonスクリプトと、PyInstallerの時にも行った外部ライブラリーにrequestsとBeatiful Soupを使った例を試してみたいと
思います。

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

環境

今回の環境はこちら。

$ python3 --version
Python 3.10.12


$ pip3 --version
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

仮想環境も使いますが、作成は省略します。

OSはUbuntu Linux 22.04 LTSです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.4 LTS
Release:        22.04
Codename:       jammy


$ uname -srvmpio
Linux 5.15.0-94-generic #104-Ubuntu SMP Tue Jan 9 15:25:40 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

動作確認は…Pythonが入っていない環境ということで、Ubuntu Linuxのコンテナ内で行うことにします。

$ docker container run -it --rm -v $(pwd):/host ubuntu:22.04 bash
# python
bash: python: command not found
# python3
bash: python3: command not found

Nuitkaをインストールする

Nuitkaのインストール方法ですが、pip、Linux OSの場合は外部リポジトリーを追加することでOSパッケージとしてもインストール
することができます。

Nuitka Downloads — Nuitka the Python Compiler documentation

macOSまたはWindowsの場合は、pipでのインストールになります。

今回はpipでインストールすることにします。

$ pip3 install nuitka

インストールされたバージョン。

$ pip3 list
Package     Version
----------- -------
Nuitka      2.0.3
ordered-set 4.1.0
pip         22.0.2
setuptools  59.6.0
zstandard   0.22.0

インストールすると、nuitka3およびnuitka3-runというコマンドが使えるようになりました。

$ nuitka3 --version
2.0.3
Commercial: None
Python: 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]
Flavor: Debian Python
Executable: /path/to/venv/bin/python3
OS: Linux
Arch: x86_64
Distribution: Ubuntu (based on Debian) 22.04.4
Version C compiler: /usr/bin/gcc (gcc 11).


$ nuitka3-run --version
2.0.3
Commercial: None
Python: 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]
Flavor: Debian Python
Executable: /path/to/venv/bin/python3
OS: Linux
Arch: x86_64
Distribution: Ubuntu (based on Debian) 22.04.4
Version C compiler: /usr/bin/gcc (gcc 11).

nuitka3-runは、Pythonスクリプトをコンパイルして直接実行しようとするコマンドだそうです。

ヘルプも確認できます。

$ nuitka3 --help
Usage: nuitka3 [--module] [--run] [options] main_module.py

Options:
  --help                show this help message and exit
  --version             Show version information and important details for bug
                        reports, then exit. Defaults to off.

  〜省略〜

…膨大なオプションが並びますが。

シンプルなPythonスクリプトで試す

ファイルを読み込んで、ファイル名と行番号を出力するスクリプトを書いてみます。

app.py

import sys

path = sys.argv[1]

print(f"print: {path}")
print()

with open(path, "r") as f:
    count = 0

    for line in f:
        count += 1
        print(f"{count}: {line}", end="")

スクリプト自身を指定して、動作確認。

$ python3 app.py app.py
print: app.py

1: import sys
2:
3: path = sys.argv[1]
4:
5: print(f"print: {path}")
6: print()
7:
8: with open(path, "r") as f:
9:     count = 0
10:
11:     for line in f:
12:         count += 1
13:         print(f"{count}: {line}", end="")

せっかくなので、まずはnuitka3-runで試してみましょう。

$ nuitka3-run app.py app.py
Nuitka-Options: Used command line options: app.py
Nuitka-Options:WARNING: You did not specify to follow or include anything but main program. Check options and make sure that is intended.
Nuitka: Starting Python compilation with Nuitka '2.0.3' on Python '3.10' commercial grade 'not installed'.
Nuitka: Completed Python level compilation and optimization.
Nuitka: Generating source code for C backend compiler.
Nuitka: Running data composer tool for optimal constant value handling.
Nuitka: Running C compilation via Scons.
Nuitka-Scons: Backend C compiler: gcc (gcc 11).
Nuitka-Scons: Backend linking program with 6 files (no progress information available for this stage).
Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package manager to install it.
Nuitka: Keeping build directory 'app.build'.
Nuitka: Successfully created 'app.bin'.
Nuitka: Launching 'app.bin'.
print: app.py

1: import sys
2:
3: path = sys.argv[1]
4:
5: print(f"print: {path}")
6: print()
7:
8: with open(path, "r") as f:
9:     count = 0
10:
11:     for line in f:
12:         count += 1
13:         print(f"{count}: {line}", end="")

動作を見ていると、Cコンパイラーが動いてから実行しているようです。

なので、実行時間はかなり伸びます。

$ time nuitka3-run app.py app.py

〜省略〜

real    0m5.903s
user    0m9.197s
sys     0m0.532s

ちなみに、実行すると[スクリプト名].binという実行可能ファイルとapp.buildというディレクトリができていました。

$ ll
合計 5872
drwxrwxr-x 4 xxxxx xxxxx    4096  2月 21 15:41 ./
drwxrwxr-x 4 xxxxx xxxxx    4096  2月 21 14:51 ../
-rwxrwxr-x 1 xxxxx xxxxx 5988376  2月 21 15:41 app.bin*
drwxrwxr-x 3 xxxxx xxxxx    4096  2月 21 15:41 app.build/
-rw-rw-r-- 1 xxxxx xxxxx     191  2月 21 15:30 app.py
drwxrwxr-x 5 xxxxx xxxxx    4096  2月 21 15:19 venv/

このapp.binだけで実行できますね…。

$ ./app.bin app.py
print: app.py

1: import sys
2:
3: path = sys.argv[1]
4:
5: print(f"print: {path}")
6: print()
7:
8: with open(path, "r") as f:
9:     count = 0
10:
11:     for line in f:
12:         count += 1
13:         print(f"{count}: {line}", end="")

作成されたファイルとディレクトリは、1度削除。

$ rm -rf app.bin app.build/

では、実行可能バイナリを作成してみます。--standaloneオプションを付けるみたいです。

$ nuitka3 --standalone app.py

すると、patchelfというパッケージがないと怒られました…。

Nuitka-Options: Used command line options: --standalone app.py
FATAL: Error, standalone mode on Linux requires 'patchelf' to be installed. Use 'apt/dnf/yum install patchelf' first.

インストールします。

$ sudo apt install patchelf

今度は動きますが、nuitka3-runとは比にならないくらい実行時間が伸びます。

$ nuitka3 --standalone app.py

実行ログ。

Nuitka-Options: Used command line options: --standalone app.py
Nuitka: Starting Python compilation with Nuitka '2.0.3' on Python '3.10' commercial grade 'not installed'.
Nuitka-Plugins:anti-bloat: Not including '_json' automatically in order to avoid bloat, but this may cause: may slow down by using fallback implementation.
Nuitka: Completed Python level compilation and optimization.
Nuitka: Generating source code for C backend compiler.
Nuitka: Running data composer tool for optimal constant value handling.
Nuitka: Running C compilation via Scons.
Nuitka-Scons: Backend C compiler: gcc (gcc 11).
Nuitka-Scons: Backend linking program with 7 files (no progress information available for this stage).
Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package manager to install it.
Nuitka: Keeping build directory 'app.build'.
Nuitka: Successfully created 'app.dist/app.bin'.

2回目の実行時間はこれくらいでしたが、初回はもっと遅かったように思います。

real    0m21.296s
user    0m21.324s
sys     0m0.592s

app.buildとapp.distという2つのディレクトリができています。

$ ll -h
合計 24K
drwxrwxr-x 5 xxxxx xxxxx 4.0K  2月 21 15:45 ./
drwxrwxr-x 4 xxxxx xxxxx 4.0K  2月 21 14:51 ../
drwxrwxr-x 3 xxxxx xxxxx 4.0K  2月 21 15:47 app.build/
drwxrwxr-x 2 xxxxx xxxxx 4.0K  2月 21 15:47 app.dist/
-rw-rw-r-- 1 xxxxx xxxxx  191  2月 21 15:30 app.py
drwxrwxr-x 5 xxxxx xxxxx 4.0K  2月 21 15:19 venv/

コマンドの実行結果を見ると、app.distディレクトリにあるapp.binというファイルを使うようです。

$ ll -h app.dist
合計 11M
drwxrwxr-x 2 xxxxx xxxxx 4.0K  2月 21 15:47 ./
drwxrwxr-x 5 xxxxx xxxxx 4.0K  2月 21 15:45 ../
-rw-rw-r-- 1 xxxxx xxxxx 154K  2月 21 15:47 _codecs_cn.so
-rw-rw-r-- 1 xxxxx xxxxx 162K  2月 21 15:47 _codecs_hk.so
-rw-rw-r-- 1 xxxxx xxxxx  34K  2月 21 15:47 _codecs_iso2022.so
-rw-rw-r-- 1 xxxxx xxxxx 270K  2月 21 15:47 _codecs_jp.so
-rw-rw-r-- 1 xxxxx xxxxx 142K  2月 21 15:47 _codecs_kr.so
-rw-rw-r-- 1 xxxxx xxxxx 114K  2月 21 15:47 _codecs_tw.so
-rw-rw-r-- 1 xxxxx xxxxx  57K  2月 21 15:47 _multibytecodec.so
-rwxrwxr-x 1 xxxxx xxxxx 9.1M  2月 21 15:47 app.bin*
-rw-rw-r-- 1 xxxxx xxxxx 196K  2月 21 15:47 libexpat.so.1

では、このファイルがPythonなしで実行できるか確認してみましょう。

$ docker container run -it --rm -v $(pwd):/host ubuntu:22.04 bash

ファイルをコピーして

# cd
# cp /host/app.dist/app.bin ./.
# cp /host/app.py ./.

実行。

# ./app.bin app.py
./app.bin: error while loading shared libraries: libexpat.so.1: cannot open shared object file: No such file or directory

怒られました…。app.distディレクトリ内にあったlibexpat.so.1というファイルがないと言っています。

ということは、app.distディレクトリを丸ごとコピーすればよさそうですね。

# cp -R /host/app.dist ./.

実行。

# ./app.dist/app.bin app.py
print: app.py

1: import sys
2:
3: path = sys.argv[1]
4:
5: print(f"print: {path}")
6: print()
7:
8: with open(path, "r") as f:
9:     count = 0
10:
11:     for line in f:
12:         count += 1
13:         print(f"{count}: {line}", end="")

今度はうまくいきました。

が、ちょっとやり方が間違っている気がします。どうやらこの目的では--onefileというオプションを使うのが正解のようです。

1度生成されたファイルを削除。

$ rm -rf app.build app.dist

--onefileオプションを指定して、再度ビルド。

$ nuitka3 --onefile app.py

実行ログ。

Nuitka-Options: Used command line options: --onefile app.py
Nuitka: Starting Python compilation with Nuitka '2.0.3' on Python '3.10' commercial grade 'not installed'.
Nuitka: Completed Python level compilation and optimization.
Nuitka: Generating source code for C backend compiler.
Nuitka: Running data composer tool for optimal constant value handling.
Nuitka: Running C compilation via Scons.
Nuitka-Scons: Backend C compiler: gcc (gcc 11).
Nuitka-Scons: Backend linking program with 7 files (no progress information available for this stage).
Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package manager to install it.
Nuitka-Postprocessing: Creating single file from dist folder, this may take a while.
Nuitka-Onefile: Running bootstrap binary compilation via Scons.
Nuitka-Scons: Onefile C compiler: gcc (gcc 11).
Nuitka-Scons: Onefile linking program with 1 files (no progress information available for this stage).
Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package manager to install it.
Nuitka-Onefile: Using compression for onefile payload.
Nuitka-Onefile: Onefile payload compression ratio (30.98%) size 10628308 to 3292890.
Nuitka-Onefile: Keeping onefile build directory 'app.onefile-build'.
Nuitka: Keeping dist folder 'app.dist' for inspection, no need to use it.
Nuitka: Keeping build directory 'app.build'.
Nuitka: Successfully created 'app.bin'.

今度は、先ほどのapp.build、app.distに加えてapp.binというファイルとapp.onefile-buildというディレクトリが増えました。

$ ll -h
合計 3.4M
drwxrwxr-x 6 xxxxx xxxxx 4.0K  2月 21 15:57 ./
drwxrwxr-x 4 xxxxx xxxxx 4.0K  2月 21 14:51 ../
-rwxrwxr-x 1 xxxxx xxxxx 3.3M  2月 21 15:57 app.bin*
drwxrwxr-x 3 xxxxx xxxxx 4.0K  2月 21 15:56 app.build/
drwxrwxr-x 2 xxxxx xxxxx 4.0K  2月 21 15:57 app.dist/
drwxrwxr-x 3 xxxxx xxxxx 4.0K  2月 21 15:57 app.onefile-build/
-rw-rw-r-- 1 xxxxx xxxxx  191  2月 21 15:30 app.py
drwxrwxr-x 5 xxxxx xxxxx 4.0K  2月 21 15:19 venv/

app.onefile-buildの中身はこんな感じです。

$ tree app.onefile-build
app.onefile-build
├── @link_input.txt
├── onefile_definitions.h
├── scons-report.txt
└── static_src
    ├── OnefileBootstrap.c -> /path/to/venv/lib/python3.10/site-packages/nuitka/build/static_src/OnefileBootstrap.c
    └── OnefileBootstrap.o

1 directory, 5 files

それで、app.binファイルを使えばよいのでしょうか?

再度確認してみます。

$ docker container run -it --rm -v $(pwd):/host ubuntu:22.04 bash
# cd
# cp /host/app.bin ./.
# cp /host/app.py ./.

実行。

# ./app.bin app.py
print: app.py

1: import sys
2:
3: path = sys.argv[1]
4:
5: print(f"print: {path}")
6: print()
7:
8: with open(path, "r") as f:
9:     count = 0
10:
11:     for line in f:
12:         count += 1
13:         print(f"{count}: {line}", end="")

できました。これで良さそうです。

--standalone、--onefileのどちらのオプションを使ってもPythonランタイムなしにできるようですが、単一のファイルにまとめるには
--onefileオプションを使うことになるようです。

--standaloneオプションの場合はdistディレクトリごとコピーのが正解のようです。

では--standalone、--onefileの違いは?ということなのですが、--onefileオプションの場合はデータファイルが含まれなくなるようです。

For data files to be included, use the option --include-data-files== where the source is a file system path, but the target has to be specified relative. For the standalone mode, you can also copy them manually, but this can do extra checks, and for the onefile mode, there is no manual copying possible.

Use Case 4 - Program Distribution

なので、ビルドして動くかどうかはまず--standaloneオプションで確認し、問題なさそうで必要なら--onefileオプションを試した方が
よいとされています。

依存ライブラリーを使って試す

次は、他にライブラリーをインストールして試しましょう。

元ネタはこちらのエントリーです。そういえば、Pythonスクリプトを分割してどうなるかも試していたので、そちらも踏襲しましょう。

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

Nuitkaをインストールした後、こちらを追加。

$ pip3 install requests beautifulsoup4

インストールされたバージョン。

$ pip3 list
Package            Version
------------------ --------
beautifulsoup4     4.12.3
certifi            2024.2.2
charset-normalizer 3.3.2
idna               3.6
Nuitka             2.0.3
ordered-set        4.1.0
pip                22.0.2
requests           2.31.0
setuptools         59.6.0
soupsieve          2.5
urllib3            2.2.1
zstandard          0.22.0

指定されたURLにアクセスして、titleを取得するスクリプト。

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(f"https://kazuhira-r.hatenablog.com/'s title = {title}")

まずは実行確認。

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

OKですね。

では、今度はNuitkaを使っていきなり--onefileで単一の実行可能ファイルにしてみます。

$ nuitka3 --onefile app.py

実行ログ。

Nuitka-Options: Used command line options: --onefile app.py
Nuitka: Starting Python compilation with Nuitka '2.0.3' on Python '3.10' commercial grade 'not installed'.
Nuitka-Plugins:anti-bloat: Not including '_json' automatically in order to avoid bloat, but this may cause: may slow down by using fallback implementation.
Nuitka: Completed Python level compilation and optimization.
Nuitka: Generating source code for C backend compiler.
Nuitka: Running data composer tool for optimal constant value handling.
Nuitka: Running C compilation via Scons.
Nuitka-Scons: Backend C compiler: gcc (gcc 11).
Nuitka-Scons: Backend linking program with 87 files (no progress information available for this stage).
Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package manager to install it.
Nuitka-Plugins:data-files: Included data file 'certifi/cacert.pem' due to package data for 'certifi'.
Nuitka-Postprocessing: Creating single file from dist folder, this may take a while.
Nuitka-Onefile: Running bootstrap binary compilation via Scons.
Nuitka-Scons: Onefile C compiler: gcc (gcc 11).
Nuitka-Scons: Onefile linking program with 1 files (no progress information available for this stage).
Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package manager to install it.
Nuitka-Onefile: Using compression for onefile payload.
Nuitka-Onefile: Onefile payload compression ratio (23.73%) size 48561227 to 11524727.
Nuitka-Onefile: Keeping onefile build directory 'app.onefile-build'.
Nuitka: Keeping dist folder 'app.dist' for inspection, no need to use it.
Nuitka: Keeping build directory 'app.build'.
Nuitka: Successfully created 'app.bin'.

2回目の計測結果ですが、ビルドにかかった時間。

real    2m9.833s
user    6m7.061s
sys     0m7.561s

結果。

$ ll -h
合計 12M
drwxrwxr-x 7 xxxxx xxxxx 4.0K  2月 21 16:19 ./
drwxrwxr-x 4 xxxxx xxxxx 4.0K  2月 21 14:51 ../
drwxrwxr-x 2 xxxxx xxxxx 4.0K  2月 21 16:16 __pycache__/
-rwxrwxr-x 1 xxxxx xxxxx  12M  2月 21 16:19 app.bin*
drwxrwxr-x 3 xxxxx xxxxx  20K  2月 21 16:18 app.build/
drwxrwxr-x 5 xxxxx xxxxx 4.0K  2月 21 16:19 app.dist/
drwxrwxr-x 3 xxxxx xxxxx 4.0K  2月 21 16:19 app.onefile-build/
-rw-rw-r-- 1 xxxxx xxxxx  148  2月 21 16:13 app.py
-rw-rw-r-- 1 xxxxx xxxxx  183  2月 21 16:12 func.py
drwxrwxr-x 5 xxxxx xxxxx 4.0K  2月 21 16:11 venv/

確認してみます。

$ docker container run -it --rm -v $(pwd):/host ubuntu:22.04 bash
# cd
# cp /host/app.bin ./.

実行。

# ./app.bin

エラーになりました…が、これは絵文字を出力するためのcodecが足りないからですね…。ブログタイトルに絵文字を入れてるから…。

Traceback (most recent call last):
  File "/tmp/onefile_11_1708500050_281074/app.py", line 5, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\U0001f340' in position 51: ordinal not in range(128)

というか、トレースバックを見るとPythonのスクリプト名が出ていることにちょっと驚きました。内部的には元のスクリプトが
含まれていて、商用版だと暗号化できるというのはこのあたりの話なのでしょうね。

仕方がないので日本語の言語パックをインストールして、言語設定。

# apt update && apt install language-pack-ja
# export LANG=ja_JP.UTF-8
# export LANGUAGE=ja_JP:

今度は動きました!

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

なので、依存ライブラリーおよび複数ファイルの利用もOKですね。

これでやりたいことはひととおりできた感じです。

おわりに

Nuitkaを使って、Pythonアプリケーションから単一の実行可能ファイルを作成することを試してみました。

動作確認にコンテナを使ったので変なハマり方をしたりしましたが、使い方自体はそれほど戸惑わなかったです。ソースコード以外の
ファイルを含めると、どういう挙動になるのかは確認していませんが。

今回はこれくらいでいいかな、と。