Posted at

PyOxidizer を試してみた

先日、何気なくはてブを見ていると、マイナビニュースで PyOxidizer なるツールが紹介されているのが目に止まりました。


PyOxidizer とはなにか :question:

記事に書かれていた説明によると、


「PyOxidizer」はPythonスクリプトをそのスクリプトを実行するのに必要になるパッケージやモジュールも含めて単一のバイナリファイルにまとめるツール。


だとか。

ふむふむ、これはきっと PyInstaller 的なやつだな。

:point_up_2: の説明を読む限り、これもきっとバイナリサイズが大きくなりそうだけど、PyInstaller と比べてどうなんだろう? ってことで、何はともあれ試してみましょう。

公式ドキュメント


バイナリ作ってみよう :ten:

PyOxidizer は Rust で作られています。

なので、環境ができていれば導入は簡単。

cargpo install pyoxidizer

rust には詳しくないんですが、凄まじい数の依存パッケージ数です…

しばらくまってると、pyoxidizer のビルドが完了します。

windows だと PATH まで通してくれるようですが、mac は PATH 追加してください。

まずはヘルプを見てみましょう。

$ pyoxidizer --help

PyOxidizer 0.2.0
Gregory Szorc <gregory.szorc@gmail.com>
Build and distribute Python applications

USAGE:
pyoxidizer [FLAGS] [SUBCOMMAND]

FLAGS:
-h, --help
Prints help information

-V, --version
Prints version information

--verbose
Enable verbose output

SUBCOMMANDS:
add Add PyOxidizer to an existing Rust project. (EXPERIMENTAL)
analyze Analyze a built binary
build Build a PyOxidizer enabled project
build-artifacts Process a PyOxidizer config file and build derived artifacts
help Prints this message or the help of the given subcommand(s)
init Create a new Rust project embedding Python.
python-distribution-extract Extract a Python distribution archive to a directory
python-distribution-licenses Show licenses for a given Python distribution
run Build and run a PyOxidizer application
run-build-script Run functionality that a build script would perform

ほぅ:frowning: なんか難しそうだ。

ドキュメントに従って pyapp というディレクトリを作って、

pyoxidizer init pyapp

cd ./pyapp

すると、pyapp の中にわんさかファイルができています。


インタプリタを起動するバイナリ

ここまでの状態で

pyoxidizer run

すると、python のインタプリタが起動しました。

>>> import sys

>>> print(sys.version)
3.7.3 (default, Jun 17 2019, 22:24:24)
[Clang 6.0.1 (tags/RELEASE_601/final)]
>>>

ふむ、細かいことはちゃんと調べねばなりませんが、ローカルでインストールしている python とはバージョンが異なります。

さて、run コマンドのヘルプメッセージにあるように、Build & Run なので、

pyapp/build/apps/pyapp/debug

にもインタプリタを起動するバイナリが作成されていると思います。

最初は「これいつ使うねん」とか思ったけど、python をインストールできないような環境に既存の python スクリプトを持ってって実行できたら幸せなシチュエーションも確かにあるわけで、ひょっとするとすごく便利かもしれないと思い始めてる。ワナワナ


python スクリプトのバイナリ

では、いよいよ本題の、スクリプトのバイナリを作りましょう。

今回は helloworld.py と、requestsのモジュールをimportするmodsample.pyで試してみます。

helloworld.py

import sys

def main():
print("hello world.")

if __name__ == "__main__":
sys.exit(main())


modsample.py

import requests

target_url = "https://www.google.com/"
r = requests.get(target_url)

print(r.status_code)


pyapp/pyoxidizer.tomlが設定ファイルのようなので、これの82行目にある[[embedded_python_run]]を修正します。

バイナリ化するファイルは pyapp/helloworld.pyに配置して、

[[embedded_python_run]]

# mode = "repl"
mode = "module"
module = "helloworld"

して、runすると、、、

AttributeError: 'NoneType' object has no attribute 'loader'

error: cargo run failed

あれ?

どうやら:point_down:も必要みたい。

[[packaging_rule]]

type = "package-root"
path = "."
packages = ["helloworld"]

hello world.

いぇーい :tada::tada::tada:

pyapp/build/apps/pyapp/debugにもちゃんとバイナリできてる。

ただ hello worldを出力するだけなら、:point_down:みたく直接スクリプトの記述もできるみたい。

[[embedded_python_run]]

# Evaluate some Python code.
mode = "eval"
code = "print('hello world')"

続いてpyapp/modsample.py

さっきの設定に加えて import するモジュールの設定が必要です。

[[embedded_python_config]]

sys_paths = ["$ORIGIN/lib"]

[[packaging_rule]]
type = "pip-install-simple"
package = "requests"
install_location = "app-relative:lib"
extra_args = ["--proxy=url:port and other args"]

[[packaging_rule]]
type = "package-root"
path = "."
packages = ["modsample"]

[[embedded_python_run]]
mode = "module"
module = "modsample"

これで、

200

やったー :tada::tada::tada:

その後、matplotlib や numpy など試してみたんですがうまくできず、もう少し調べてみる必要がありそうです。


PyInstaller と比べてどうか :thinking:

上記で使用した modsample.py を windows 上で PyInstaller --onefile すると、大体6MBくらいになります。一方で、PyOxidizer で今回の手順で作成したバイナリは20MBほどあり、requests モジュールが別ファイルになってしまっていたので、今回の結果だけで単純には比較できませんでした。

install_location = "embedded"で単一バイナリになるみたいですが、それができないモジュールもあるみたい…

もう少しドキュメントを読みこんで、いろいろ設定をいじってみる必要がありそうです:muscle: