BinaryBuilder.jlでLapackを使うコードをコンパイルする

[BinaryBuilder.jlがすごい〜自動でDockerでクロスコンパイルしてcやFortranのコードをJuliaライブラリ化](https://qiita.com/cometscome_phys/items/46d8c40ae840239b4c1d)の続編です。

Dockerを使うことで、Mac、Windows、Linuxの様々なプラットフォームでのクロスコンパイルを行い、さまざまなOSの上で動くJuliaでバイナリを提供します。バイナリは別の言語でコンパイルされたものを用意することができますので、例えばFortranのライブラリなどをコンパイルしておけばJuliaで自由に使えるようになるわけです。

簡単な方法はすでに上の記事で紹介しました。今回はLapackを使うライブラリをコンパイルする方法について述べます。なぜなら、Lapackをリンクさせるのにかなり手こずったからです。他の人の助けになればと思います。

# 導入したソフトウェア
[z-Pares](https://zpares.cs.tsukuba.ac.jp)と呼ばれる一般化固有値問題を解くFortranで書かれたライブラリをBinaryBuilder.jlでコンパイルし、Juliaで使えるようにすることを目標としました。このコードはSakurai-Sugiura法と呼ばれる方法で固有値を計算しており、複素平面上の自分で指定した領域の中にある固有値を全て取り出すことができるというソフトウェアです。解説は[こちら](http://park.itc.u-tokyo.ac.jp/kato-yusuke-lab/nagai/note_140502_SS.pdf)にあります。なお、Fortranで通常のようにコンパイルする方法の解説は[こちら](http://park.itc.u-tokyo.ac.jp/kato-yusuke-lab/nagai/note_140903_SSzpares.pdf)にあります。

# 基本方針
基本的にはBinaryBuilder.jlを用意して、run_wizardをすれば作れます。ただし、Lapackを呼ぶときに少し注意が必要です。LapackはOpenBLASに入っていますからOpenBLASを呼べばいいのかと思ったら、実はこれではうまくいきません。OpenBLAS32_jllが必要です。必要なバイナリとしてこれを指定すれば、コンパイルが通るようになります。

# BinaryBuilder.jlの仕組み
BinaryBuilder.jlはrun_wizardを使ってやるのが一番簡単ですが、これが何をやっているかいまいちよくわからなかったり、あるいはwizardで対応していないことをしたい場合にはどうすればいいのかわからなかったりします。web上の情報も日本語英語含めてほとんどありません。
基本的にはrun_wizardはbuild_tarballs.jlというファイルを作るもの、と考えておけば大丈夫です。言い換えれば、build_tarballs.jlを作りさえすれば、run_wizardを使う必要はありません。例えば、M1 Macに対応したM1のCPU用のバイナリを作りたい場合、現時点ではrun_wizardでは作れませんので、run_wizardで作ったbuild_tarballs.jlを修正して使うことになります。

# 例
うまくいったbuild_tarballs.jlをここに書いておきます。試行錯誤の結果動いたものですので、これが最適かはわかりません。

```julia:build_tarballs.jl
# Note that this script can accept some limited command-line arguments, run
# `julia build_tarballs.jl --help` to see a usage message.
using BinaryBuilder, Pkg

name = "ZPares"
version = v"0.1.0"

# Collection of sources required to complete build
sources = [
    GitSource("https://github.com/cometscome/zpares_mirror.git", "ab453f5c3aa295bb43f6396e796db8d03964b5b3")
]

# Bash recipe for building across all platforms
script = raw"""
cd $WORKSPACE/srcdir
cd zpares_mirror/
cd originalfile/
tar -xvf zpares_0.9.6a.tar.gz 
cd zpares_0.9.6a
cp ../../wrapper/zpares_wrapper.f90 ./
if [[ $target == *"aarch64-apple-darwin"* ]]; then 
    Rankmismatch="-fallow-argument-mismatch"
else
    Rankmismatch="" 
fi


if [[ $target == *"apple-darwin"* ]]; then
    cp Makefile.inc/make.inc.gfortran.seq ./make.inc
    make BLAS="-framework accelerate" USE_MPI="0" FFLAG="-O3 $Rankmismatch -dynamiclib -fPIC -framework accelerate" LAPACK="-L./"
    gfortran -O3 -dynamiclib -fPIC zpares_wrapper.f90 -I./include -L./lib -lzpares -framework accelerate  -o libzpares.dylib
    cp libzpares.dylib lib/
    cp lib/libzpares.dylib $prefix/lib/
    cp include/zpares.mod $prefix/include/
    cp zpares_wrapper.mod $prefix/include/
    cp lib/libzpares.a $prefix/lib/
elif [[ $target == *"w64-mingw32"* ]]; then 
    cp Makefile.inc/make.inc.gfortran.seq ./make.inc
    make BLAS="-L$libdir -lopenblas" LAPACK="-L$libdir -lopenblas" FFLAG="-O3 -shared -fPIC"
    gfortran -O3 -shared -fPIC zpares_wrapper.f90 -I./include -L./lib -lzpares  -L$libdir -lopenblas -o zpares_wrapper.a
    ld -shared -o zpares.so --whole-archive zpares_wrapper.a 
    cp lib/* $prefix/bin/
    cp zpares.so lib/
    cp lib/zpares.so $prefix/lib/libzpares.dll
    cp include/zpares.mod $prefix/include/
    cp zpares_wrapper.mod $prefix/include/
    cp lib/libzpares.a $prefix/lib/
else
    cp Makefile.inc/make.inc.gfortran.seq ./make.inc
    make BLAS="-L$libdir -lopenblas" LAPACK="-L$libdir -lopenblas" FFLAG="-O3 -shared -fPIC"
    gfortran -O3 -shared -fPIC zpares_wrapper.f90 -I./include -L./lib -lzpares  -o zpares_wrapper.a
    ld -shared -o zpares.so --whole-archive zpares_wrapper.a 
    cp zpares.so lib/
    cp lib/zpares.so $prefix/lib/libzpares.so
    cp include/zpares.mod $prefix/include/
    cp zpares_wrapper.mod $prefix/include/
    cp lib/libzpares.a $prefix/lib/
fi
"""
#    cp -r examples $prefix/
# These are the platforms we will build for by default, unless further
# platforms are passed in on the command line
platforms = [
    Platform("x86_64", "windows"; ),
    Platform("i686", "windows"; ),
    Platform("x86_64", "macos"; ),
    Platform("aarch64", "macos"; ),
    Platform("i686", "linux"; libc = "glibc"),
    Platform("x86_64", "linux"; libc = "glibc"),
    Platform("aarch64", "linux"; libc = "glibc"),
    Platform("armv7l", "linux"; call_abi = "eabihf", libc = "glibc"),
    Platform("powerpc64le", "linux"; libc = "glibc"),
    Platform("i686", "linux"; libc = "musl"),
    Platform("x86_64", "linux"; libc = "musl"),
    Platform("aarch64", "linux"; libc = "musl"),
    Platform("armv7l", "linux"; call_abi = "eabihf", libc = "musl"),
    Platform("x86_64", "freebsd"; )
]

platforms = expand_gfortran_versions(platforms)


# The products that we will ensure are always built
products = [
    LibraryProduct("libzpares", :libzpares)]

# Dependencies that must be installed before this package can be built
dependencies = [
    Dependency(PackageSpec(name="OpenBLAS_jll", uuid="4536629a-c528-5b80-bd46-f80d51c5b363"))
    Dependency(PackageSpec(name="OpenBLAS32_jll", uuid="656ef2d0-ae68-5445-9ca0-591084a874a2"))
    Dependency(PackageSpec(name="CompilerSupportLibraries_jll", uuid="e66e0078-7015-5450-92f7-15fbd957f2ae"))

]

# Build the tarballs, and possibly a `build.jl` as well.
build_tarballs(ARGS, name, version, sources, script, platforms, products, dependencies; julia_compat="1.6", preferred_gcc_version = v"9.1.0")

```

# プルリクエスト
出来上がったらbuild_tarballs.jlを
https://github.com/JuliaPackaging/Yggdrasil
に登録してもらえれば、バイナリを公式パッケージとして使えるようになります。そのためには、github上のYggdrasilをforkして自分の手元でこの新しいパッケージをpushし、プルリクエストします。プルリクエストすると、そのコードはレビューされます。

# プルリクエスト後
https://github.com/JuliaPackaging/Yggdrasil
にプルリクエストした後のやり取りは
https://github.com/JuliaPackaging/Yggdrasil/pull/4032
こんな感じでした。
かなり丁寧にレビューしてもらいました。

結果的には、

```julia:build_tarballs.jl
# Note that this script can accept some limited command-line arguments, run
# `julia build_tarballs.jl --help` to see a usage message.
using BinaryBuilder, Pkg

name = "ZPares"
version = v"0.1.0"

# Collection of sources required to complete build
sources = [
    GitSource("https://github.com/cometscome/zpares_mirror.git", "1e84be7cd0f8368da09ac0c262f815583ce04b7d")
    FileSource("https://zpares.cs.tsukuba.ac.jp/?download=242","3c34257d249451b0b984abc985e296ebb73ae5331025f1b8ea08d50301c7cf9a",filename="zpares_0.9.6a.tar.gz")
]

# Bash recipe for building across all platforms
script = raw"""
cd $WORKSPACE/srcdir
tar -xvf zpares_0.9.6a.tar.gz 
cd zpares_mirror/
install_license ./LICENSE
cd ../zpares_0.9.6a
cp ../zpares_mirror/wrapper/zpares_wrapper.f90 ./

if [[ $target == *"aarch64-apple-darwin"* ]]; then 
    Rankmismatch="-fallow-argument-mismatch"
fi

cp Makefile.inc/make.inc.gfortran.seq ./make.inc
make BLAS="-L${libdir} -lopenblas" USE_MPI="0" FFLAG="-O3 ${Rankmismatch} -shared -fPIC -L${libdir} -lopenblas" LAPACK="-L./"
gfortran -O3 -shared -fPIC zpares_wrapper.f90 -I./include -L./lib -lzpares -L${libdir} -lopenblas -o "${libdir}/libzpares.${dlext}"
cp include/zpares.mod "${includedir}"
cp zpares_wrapper.mod "${includedir}"
"""

# These are the platforms we will build for by default, unless further
# platforms are passed in on the command line
platforms = supported_platforms(;experimental=true)
platforms = expand_gfortran_versions(platforms)


# The products that we will ensure are always built
products = [
    LibraryProduct("libzpares", :libzpares),
]

# Dependencies that must be installed before this package can be built
dependencies = [
    Dependency(PackageSpec(name="OpenBLAS32_jll", uuid="656ef2d0-ae68-5445-9ca0-591084a874a2"))
    Dependency(PackageSpec(name="CompilerSupportLibraries_jll", uuid="e66e0078-7015-5450-92f7-15fbd957f2ae"))

]

# Build the tarballs, and possibly a `build.jl` as well.
build_tarballs(ARGS, name, version, sources, script, platforms, products, dependencies; julia_compat="1.6", preferred_gcc_version=v"5")
```
こんな感じになりました。最初のと比べるとずいぶんシンプルになっていることがわかると思います。主な変更点は使うソースをオリジナルから参照するようにしたことです。```FileSource```というものを使うとダウンロードをいい感じにしてくれるようです。あとは、includedirとかdlextとかを使うことで環境ごとの違いをうまいこと吸収できるようになっているようですね。

# 出来上がり
プルリクエストが無事マージされると、
https://github.com/JuliaBinaryWrappers/ZPares_jll.jl
こんな感じのZPares_jllというものが自動生成されます。これで、

```
add ZPares_jll
```
でJuliaのパッケージとして使えるようになりました。Fortranをコンパイルしたバイナリですが、ラッパーを作っておいたのでc言語互換で呼ぶことができ、したがってJuliaからはccallで呼べるようになっています。これを使って固有値問題を解けるようにしたパッケージがZPares.jlです。

https://github.com/cometscome/ZPares.jl

こちらはJuliaの行列を対角化して固有値固有ベクトルを計算できます。疎行列なども対応しています。