BinaryBuilder.jlがすごい〜自動でDockerでクロスコンパイルしてcやFortranのコードをJuliaライブラリ化の続編です。
Dockerを使うことで、Mac、Windows、Linuxの様々なプラットフォームでのクロスコンパイルを行い、さまざまなOSの上で動くJuliaでバイナリを提供します。バイナリは別の言語でコンパイルされたものを用意することができますので、例えばFortranのライブラリなどをコンパイルしておけばJuliaで自由に使えるようになるわけです。
簡単な方法はすでに上の記事で紹介しました。今回はLapackを使うライブラリをコンパイルする方法について述べます。なぜなら、Lapackをリンクさせるのにかなり手こずったからです。他の人の助けになればと思います。
導入したソフトウェア
z-Paresと呼ばれる一般化固有値問題を解くFortranで書かれたライブラリをBinaryBuilder.jlでコンパイルし、Juliaで使えるようにすることを目標としました。このコードはSakurai-Sugiura法と呼ばれる方法で固有値を計算しており、複素平面上の自分で指定した領域の中にある固有値を全て取り出すことができるというソフトウェアです。解説はこちらにあります。なお、Fortranで通常のようにコンパイルする方法の解説はこちらにあります。
基本方針
基本的には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をここに書いておきます。試行錯誤の結果動いたものですので、これが最適かはわかりません。
# 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
こんな感じでした。
かなり丁寧にレビューしてもらいました。
結果的には、
# 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です。
こちらはJuliaの行列を対角化して固有値固有ベクトルを計算できます。疎行列なども対応しています。