先日、何気なくはてブを見ていると、マイナビニュースで PyOxidizer なるツールが紹介されているのが目に止まりました。
PyOxidizer とはなにか
記事に書かれていた説明によると、
「PyOxidizer」はPythonスクリプトをそのスクリプトを実行するのに必要になるパッケージやモジュールも含めて単一のバイナリファイルにまとめるツール。
だとか。
ふむふむ、これはきっと PyInstaller 的なやつだな。
の説明を読む限り、これもきっとバイナリサイズが大きくなりそうだけど、PyInstaller と比べてどうなんだろう? ってことで、何はともあれ試してみましょう。
バイナリ作ってみよう
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
ほぅ なんか難しそうだ。
ドキュメントに従って 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
あれ?
どうやらも必要みたい。
[[packaging_rule]]
type = "package-root"
path = "."
packages = ["helloworld"]
で
hello world.
いぇーい
pyapp/build/apps/pyapp/debug
にもちゃんとバイナリできてる。
ただ hello world
を出力するだけなら、みたく直接スクリプトの記述もできるみたい。
[[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
やったー
その後、matplotlib や numpy など試してみたんですがうまくできず、もう少し調べてみる必要がありそうです。
PyInstaller と比べてどうか
上記で使用した modsample.py を windows 上で PyInstaller --onefile
すると、大体6MBくらいになります。一方で、PyOxidizer で今回の手順で作成したバイナリは20MBほどあり、requests モジュールが別ファイルになってしまっていたので、今回の結果だけで単純には比較できませんでした。
install_location = "embedded"
で単一バイナリになるみたいですが、それができないモジュールもあるみたい…
もう少しドキュメントを読みこんで、いろいろ設定をいじってみる必要がありそうです