PackageCompiler で GR バックエンドの Plots.jl を含めた Julia で書いたコードをコンパイルする


Deprecation Warning

  • この記事はすでに古くPackageCompilerが PackageCompilerX.jl の内容に 置き換わるといった大幅な改変が行われました.(2020/2/12現在)

本日は

Julia の Docker Hub で公開している Docker のイメージ を拡張して PackageCompiler.jl による Julia のスクリプトを実行形式にしてみましょう。

え、そんなことできちゃうの?

できちゃうんです。そう、JuliaとPackageCompilerならね。

PackageCompiler の exampleUnicodePlots.jl によるグラフ描画を可能にする例ですがコレを Plots.jl にしたらどうなるのかについて書いてみようと思います。

使い方

コンパイルしたいスクリプトを描く

Base.@ccallable function julia_main(ARGS::Vector{String})::Cint を持つ Julia のモジュールを作成してやれば良いみたいです。例えば下記のようにしておきます。

hello.jl
module Hello

# 依存するパッケージをここに書いておく
using Plots

# 動かしたい処理を記述関数名は main とする必要はない
function main()
    xs = -π:0.01:π
    p=plot(xs, sin.(xs));
    savefig(p, "sin_curve.png")
end

Base.@ccallable function julia_main(ARGS::Vector{String})::Cint
    # ここに書いても良い
    main()
    return 0
end

end

コレで sin 関数の描画結果を sin_curve.png という名前で保存することができます。

ビルドさせる処理を記述する

ここでは gen_executable.jl という名前で下記の処理を書いたスクリプトを hello.jl と同じ場所に保存しておきます。

gen_executable.jl
using PackageCompiler
build_executable("hello.jl","hello")

そしてターミタル上で下記を実行します。

$ julia gen_executable.jl

もちろん Julia の REPL で動かしても問題ないです。

残念

このままだとビルドができません。下記のようなメッセージを含む長いエラーメッセージが出ます。

fatal: error thrown and no exception handler available.
ErrorException("error compiling draw_legend: error compiling legend_size: could not load library "libGR.so"
dlopen(libGR.so.dylib, 1): image not found")

libGR がないと怒られてしまっています。 コレは Plots.jl が依存しているライブラリなのでPackageCompilerがおこなうビルド作業をする前にこのライブラリの場所をおしえないといけません。

一般的な処方箋

Mac/Linuxの場合は export LD_LIBRARY_PATH=/path/to/lib, Windows の場合は環境変数の Path に追加すればOKです。 Plots.jl に限らず C のライブラリに依存する場合に役にたつでしょう。

Julia のスクリプトで実現

Julia のスクリプトで閉じさせたい場合は ENV
という環境変数などの情報を記述している Base.EnvDict 型の変数に情報を記述すればOKです。 ENV 自体は PyCall.jl のビルドでよく使うので馴染みがある方も多いと思います。

余談ですが, PyCallが呼び出すPythonを既存のPython環境のそれを使いたい場合は

using Pkg
ENV["PYTHON"]=Sys.which("python")
pkg("build PyCall")

で実現できます。

さて、libGR 周りの解決ですが ~/.julia/packages 以下を find . | grep libgr みたいなのを実行すると見つかると思います。そのディレクトリを指定させればOKです。Juliaではわざわざそんなことをしなくても

julia> using GR
julia> pathof(GR)

で大元の場所を特定させることができます。

Ubuntu/Mac と Windows では住んでいる場所が微妙に違います。

# Ubuntu/Macの場合
julia> joinpath(dirname(dirname(pathof(GR))),"deps","gr","lib")
# Windows
julia> joinpath(dirname(dirname(pathof(GR))),"deps","gr","bin")

こういった環境の差異を吸収するために下記のスクリプトを用意しました。

# gen_executable.jl
using PackageCompiler
using Pkg

if in("GR",keys(Pkg.installed()))
    @eval using GR
else
    Pkg.add("GR")
    @eval using GR
end

if Sys.iswindows()
    s=joinpath(dirname(dirname(pathof(GR))),"deps","gr","bin")
    s*=';'
    if !endswith(ENV["PATH"],';')
        ENV["PATH"] *= ';'
    end
    ENV["PATH"] *= s
else
    s=joinpath(dirname(dirname(pathof(GR))),"deps","gr","lib")
    s*=':'
    if haskey(ENV, "LD_LIBRARY_PATH")
        ENV["LD_LIBRARY_PATH"] *= s
        if !endswith(ENV["LD_LIBRARY_PATH"],':')
            ENV["LD_LIBRARY_PATH"] *= ':'
        end
    else
        ENV["LD_LIBRARY_PATH"] = s
    end
end

build_executable("hello.jl","hello")
  • 環境によっては ENV["PATH"] の末尾が ';' で終わっていない可能性があるのでそういう処理も書いてあります。

このスクリプトを実行して All Done と出ると完了です。bindir/hello を実行させて動けばOKです。お疲れ様でした。
こちらの Gist も参考にどうぞ。

トラブルシューティング

  • あ、実行できない・・・。

何かエラーが出る場合は Julia としてのコードを動かした場合に何かおかしいことが起きているかもしれません。下記のように動かして何か異常なことがあれば Julia としてのコードに異常があるのでそれを修正する必要があります。

julia> include("hello.jl")
julia> using .Hello
julia> Hello.main()
# 何かおかしいことがあればそれを直す
  • 何かセグフォが起きるんだけど・・・

using PackageCompiler; compile_package("Plots.jl",force=true) で作った Julia の環境ではビルドじにセグフォする悲しい現象が起きました。なので(壊してもいい)一度クリーンな環境で試すのをお勧めします。

やっぱり無理なんだけれど

デスヨネー、 Dockerのイメージをここに公開しているので コレでなら行くはずです。

ラズパイでビルドできないんだけれど

Arm 32 bit 環境への対応はもう少し整備が必要そうですね。
追記:ここに書いたIssueで解決策を書きました。PackageCompilerのソースをいじる必要があります。