PyOxidizer を試してみた


先日、何気なくはてブを見ていると、[マイナビニュース](https://news.mynavi.jp/article/20190701-851010/)で PyOxidizer なるツールが紹介されているのが目に止まりました。

## PyOxidizer とはなにか :question:

記事に書かれていた説明によると、
> 「PyOxidizer」はPythonスクリプトをそのスクリプトを実行するのに必要になるパッケージやモジュールも含めて単一のバイナリファイルにまとめるツール。

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

[公式ドキュメント](https://pyoxidizer.readthedocs.io/en/latest/index.html)

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

PyOxidizer は Rust で作られています。
なので、環境ができていれば導入は簡単。

```bash
cargpo install pyoxidizer
```

rust には詳しくないんですが、凄まじい数の依存パッケージ数です…
しばらくまってると、pyoxidizer のビルドが完了します。
windows だと PATH まで通してくれるようですが、mac は PATH 追加してください。

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

```bash
$ 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 というディレクトリを作って、

```bash
pyoxidizer init pyapp
cd ./pyapp
```

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

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

ここまでの状態で

```bash
pyoxidizer run
```

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

```bash
>>> 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で試してみます。

<details><summary>helloworld.py</summary><div>

```python
import sys


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


if __name__ == "__main__":
    sys.exit(main())
```
</div></details>

<details><summary>modsample.py</summary><div>

```python
import requests

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

print(r.status_code)
```
</div></details>



`pyapp/pyoxidizer.toml`が設定ファイルのようなので、これの82行目にある`[[embedded_python_run]]`を修正します。
バイナリ化するファイルは `pyapp/helloworld.py`に配置して、

```toml
[[embedded_python_run]]
# mode = "repl"
mode = "module"
module = "helloworld"
```

して、runすると、、、

``` bash
AttributeError: 'NoneType' object has no attribute 'loader'
error: cargo run failed
```

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

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

で

```toml
hello world.
```

いぇーい :tada::tada::tada:
`pyapp/build/apps/pyapp/debug`にもちゃんとバイナリできてる。

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

```toml
[[embedded_python_run]]
# Evaluate some Python code.
mode = "eval"
code = "print('hello world')"
```

続いて`pyapp/modsample.py`
さっきの設定に加えて import するモジュールの設定が必要です。

```toml
[[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"
```

これで、

```bash
200
```

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

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

## PyInstaller と比べてどうか :thinking: 

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

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

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