Ubuntu + pyenv + PyCall.jl でエラー

# TL;DR

+ `CONFIGURE_OPTS="--enable-shared" pyenv install 3.x.x`
+ `const PYTHONHOME = ""`

# はじめに

[Julia](https://julialang.org) で Python 関連のライブラリを利用するとき、シームレスに使える便利な [PyCall.jl](https://github.com/JuliaPy/PyCall.jl) というパッケージがあります。
単独でも使えますが、たいてい何か他の（裏で Python を利用する）パッケージをインストールするときに、一緒にインストールされたりしますし、たいていの場合何も考えずにそれを利用しても問題ありません。

ところが自分でインストールした Python（特に [pyenv](https://github.com/pyenv/pyenv) を利用してインストールした Python3.x 系）を利用したい場合に、気をつけないとハマります。
前はそれが Ubuntu（等の Linux ディストリビューション）ならそんなにハマることなかった気がするのですが、今やったらエラーに遭遇したので対処法と共に晒します。

## 環境等

+ Ubuntu 14.04 / 16.04
    + 確認したのは↑ですが、Ubuntu 18.04 とか他の Linux ディストリビューションとかでもたぶん同様
+ Julia 0.6.3
+ pyenv

# 遭遇したエラー1

pyenv で適当にインストールした Python 3.6.x を指定して `PyCall.jl` を構築しようとしたら↓のようなエラーになりました（サンプル）。

```jl
julia> ENV["PYTHON"] = "/path/to/user_home/.pyenv/versions/3.6.x/bin/python";

julia> Pkg.add("PyCall")
《中略》
INFO: Building Conda
INFO: Building PyCall
===============================[ ERROR: PyCall ]================================

LoadError: Couldn't find libpython; check your PYTHON environment variable.

The python executable we tried was python (= version 3.6);
the library names we tried were String["libpython3.6m.a", "libpython3.6m", "libpython3.6", "libpython"]
and the library paths we tried were String["/path/to/user_home/.pyenv/versions/3.6.x/lib", "/path/to/user_home/.pyenv/versions/3.6.x/lib", "/path/to/user_home/.pyenv/versions/3.6.x", "/path/to/user_home/.pyenv/versions/3.6.x/lib"]
while loading /path/to/user_home/.julia/v0.6/PyCall/deps/build.jl, in expression starting on line 254

================================================================================


================================[ BUILD ERRORS ]================================

WARNING: PyCall had build errors.

 - packages with build errors remain installed in /path/to/user_home/.julia/v0.6
 - build the package(s) and all dependencies with `Pkg.build("PyCall")`
 - build a single package by running its `deps/build.jl` script

================================================================================

```

ただ、このエラーで言っている「`libpython3.6m.a`」は「`~/.pyenv/versions/3.6.x/lib`」にちゃんとあるんですよね。

```bash
USERNAME@HOST:~/.pyenv$ find . | grep libpython
./versions/3.6.x/lib/libpython3.6m.a
./versions/3.6.x/lib/python3.6/config-3.6m-x86_64-linux-gnu/libpython3.6m.a
```

あるんですけれど、逆に「`libpython3.6m.a`」しかない。
このファイルは静的リンクライブラリであって、Julia から利用するためには動的ライブラリである「`libpython3.6m.so`」とかが必要なはずなんだけれどそれがない。
それで必要なファイルがないからエラーになっている模様。

## 対処法1

pyenv での Python インストール時に `--enable-shared` オプションを有効にしなければならない模様。
具体的には、インストール時に以下のようにすればOK。これ結局 [macOSでpyenvで入れたpythonをPyCallで使うには](https://qiita.com/kjunichi/items/7453814cb902158f9836) と同じ状況ってことになりますね。前はこんなオプション付けなくても問題なかったような気がするんだけどなー。

```bash
$ CONFIGURE_OPTS="--enable-shared" pyenv install 3.6.x
```

ただ、既にインストールしてあるものにあとから `.so` ファイルを追加は出来ない（はず）なので、その場合は結局インストールしなおしになります。
具体的な手順は以下の通り。

1. すでに pip で色々パッケージインストールしてたら、`pip freeze > requirements.txt` でインストールされているものとそのバージョン番号を待避する。
    + ↑きちんと `pyenv local 3.6.x` 等でその環境に入ってから処理すること。
1. `pyenv uninstall 3.6.x`
    + 「本当に消す？」て聞かれるから「`y`<kbd>Enter</kbd>」する。
    + あと pyenv-virtualenv で仮想環境作ってたら先にそっちを削除するか聞かれるのでよしなに対処。
1. `CONFIGURE_OPTS="--enable-shared" pyenv install 3.6.x`
1. `pip install -U pip` → `pip install -r requirements.txt` で環境復活
    + ↑きちんと `pyenv local 3.6.x` 等でその環境に入ってから処理すること。

libpython3.6m.so が出来てるか、念のため確認：

```bash
USERNAME@HOST:~/.pyenv$ find . | grep libpython
./versions/3.6.x/lib/libpython3.6m.so
./versions/3.6.x/lib/libpython3.6m.so.1.0
./versions/3.6.x/lib/libpython3.so
./versions/3.6.x/lib/python3.6/config-3.6m-x86_64-linux-gnu/libpython3.6m.a
```

うん、出来てますね。てかさっきとファイル構成全然違う…。

で、もう一度 `PyCall.jl` をビルド（`add`済なので`build`のみ実行）：

```jl
julia> ENV["PYTHON"] = "/path/to/user_home/.pyenv/versions/3.6.x/bin/python";

julia> Pkg.build("PyCall")
INFO: Building Conda
INFO: Building PyCall
Info: PyCall is using /path/to/user_home/.pyenv/versions/3.6.x/bin/python (Python 3.6.x) at /path/to/user_home/.pyenv/versions/3.6.x/bin/python, libpython = /path/to/user_home/.pyenv/versions/3.6.x/lib/libpython3.6m
Info: /path/to/user_home/.julia/v0.6/PyCall/deps/deps.jl has been updated
Info: /path/to/user_home/.julia/v0.6/PyCall/deps/PYTHON has been updated

```

無事にビルド出来ました。

# 遭遇したエラー2

PyCall.jl のビルドには成功しましたが、そのまま利用しようとした途端にコアダンプ吐いて Julia が落ちます＞＜

```jl
julia> using PyCall
Fatal Python error: Py_Initialize: Unable to get the locale encoding
ModuleNotFoundError: No module named 'encodings'

Current thread 0x0000xxxxxxxxxxxx (most recent call first):
 : 《以下略》

```

## 対処法2

pyenv 等の Python を利用するときは、`Pkg.build("PyCall")` した後にもう一工夫必要です。具体的には、ビルドで自動生成された `deps.jl` を編集する必要があります。これよく忘れて毎回ググってた。

具体的には、`~/.julia/v0.6/PyCall/deps/deps.jl` の以下の部分：

```jl:deps.jl（修正前、抜粋）
const PYTHONHOME = "/path/to/user_home/.pyenv/versions/3.6.x:/path/to/user_home/.pyenv/versions/3.6.x"
const wPYTHONHOME = Base.cconvert(Cwstring, "/path/to/user_home/.pyenv/versions/3.6.x:/path/to/user_home/.pyenv/versions/3.6.x")
```

を以下のように修正（）：

```jl:deps.jl（修正後、抜粋）
const PYTHONHOME = ""
const wPYTHONHOME = Base.cconvert(Cwstring, "")
```

確認：

```jl
julia> using PyCall
INFO: Recompiling stale cache file /path/to/user_home/.julia/v0.6/PyCall.ji for module PyCall.

julia> @pyimport numpy as np # Python 側で `pip install numpy` 済

julia> np.array([1,2,3])
3-element Array{Int64,1}:
 1
 2
 3

julia> 
```

きちんと動きました。

これ何かの拍子に裏で PyCall.jl のビルドが走る度に再設定されてしまうので、その都度再編集して `const PYTHONHOME = ""` してあげる必要があります（めんどくさい）。

# まとめに代えて

pyenv 便利なんだけれど、Julia とか他の言語/環境との連携でたまにこういうことが起きるんですよね。
あと試してないけれどまず間違いなく Julia v0.7/v1.0 でも同様だと思われます。

# 参考

+ [\`ImportError: No module named &#39;encodings&#39;\` when using with virtualenv + python3 · Issue #410 · JuliaPy/PyCall.jl](https://github.com/JuliaPy/PyCall.jl/issues/410#issuecomment-356756577)