Julia

Juliaのパッケージをバイナリ同封で配布する

Juliaのパッケージを開発していると,どうしても依存ライブラリが発生することがあります。そういう場合,ユーザの環境に依存ライブラリを何とかしてインストールしなければならないのですが,これがすこぶる大変です。細かい環境の違い・バージョンの違いでトラブルに巻き込まれることもあるでしょう。この問題を99%解決するのがBinaryBuilder.jlBinaryProvider.jlという2つのパッケージです。この記事ではこれらの使い方を簡単に説明します。なお,この方法はJulia 0.6.2の時点でのものです。まだ未完成のパッケージなので,近い将来使い方が大きく変わる可能性があります。しかし,これらのツールが今後標準的なツールになることはほとんど間違いないでしょう。

BinaryBuilder.jlは名前の通り,依存バイナリをビルドするツールです。不思議な力を使ってLinux上で他のアーキテクチャのLinux・macOS・Windowsのバイナリをクロスコンパイルすることができます(QEMUなどを使っているようですが,詳細は謎です)。BinaryProvider.jlはBinaryBuilder.jlでビルドしたバイナリを開発しているパッケージがら使うためのパッケージです。適切なバイナリをダウンロードしたり,ハッシュ値をでバイナリファイルに問題(破損や改竄)が生じていないかを確認してくれます。

現在の時点では,次のプラットフォームのビルドをサポートしているようです。
- Linux: aarch64, armv7l, i686, powerpc64le, x86_64
- MacOS
- Windows: i686, x86_64

今回はEzXML.jlというJuliaのパッケージにこれらのツールを使用してみます。EzXML.jlはlibxml2というデファクト・スタンダード的なXMLを扱うためのC言語で書かれたライブラリに依存しています。既にインストールしてある環境も多いですが,確実ではありません。

ローカルでのバイナリのビルド

まずはBinaryBuilder.jlを使って依存ライブラリをビルドします。BinaryBuilder.jlは将来的には様々なプラットフォームで使えるようになるようですが,私が使った時点ではUbuntu 16.04では動作しましたが,macOSやCentOSでは動作しませんでした。ですので,Ubuntu 16.04が使える環境を用意する必要があります。もしそういうマシンが無いようでしたら,VirtualBoxに仮想マシンとしてUbuntu 16.04をインストールして使用するのが良いでしょう(私はそうしました)。

Juliaのセッションを立ち上げてBinaryBuilder.jlをインストールし,次のようにします。

julia> using BinaryBuilder

julia> BinaryBuilder.run_wizard()

すると,対話的にビルド対象のライブラリを設定するセッションに移行し,必要な入力を与えていくだけでビルドできます。例えば,libxml2をビルドする場合,ダウンロードページ(ftp://xmlsoft.org/libxml2/)に行って,ソースコードのURLをコピーし(例えばftp://xmlsoft.org/libxml2/libxml2-2.9.7.tar.gz),それを上の対話セッションでURLの入力を要求されたときに貼り付けます。他に依存ライブラリがある場合にはそのURLも聞かれますので,それも貼り付けます。その後はどのようにビルドするかを指定するのですが,大体の場合はソースコードを展開したディレクトリに行き,./configure --prefix=/make installを実行するように設定するだけでOKでしょう。libxml2の場合は一部の機能がzlibに依存するので,それも事前にインストールするようにします。この辺はなんとか頑張ってサポートするプラットフォームでビルドができるようにします(ここが一番の難関です)。結果的にlixml2の場合は以下のようなビルドスクリプトになりました。

# Linux・macOSの場合
cd $WORKSPACE/srcdir
cd zlib-1.2.11/
./configure --prefix=/
make install
cd ../libxml2-2.9.7/
./configure --prefix=/ --host=$target --without-python --with-zlib=$(pwd)/../../destdir
make install

ビルドがうまくいくと,最後にGitレポジトリのパスが出力されます。このディレクトリをコピーしてきて中身を見ると,build_tarballs.jlというファイルがあります。このファイルの最初の中身は以下のような感じです。

using BinaryBuilder

# These are the platforms built inside the wizard
platforms = [
    BinaryProvider.Linux(:i686, :glibc),
  BinaryProvider.Linux(:x86_64, :glibc),
  BinaryProvider.Linux(:aarch64, :glibc),
  BinaryProvider.Linux(:armv7l, :glibc),
  BinaryProvider.Linux(:powerpc64le, :glibc),
  BinaryProvider.MacOS(),
  BinaryProvider.Windows(:i686),
  BinaryProvider.Windows(:x86_64)
]


# If the user passed in a platform (or a few, comma-separated) on the
# command-line, use that instead of our default platforms
if length(ARGS) > 0
    platforms = platform_key.(split(ARGS[1], ","))
end
info("Building for $(join(triplet.(platforms), ", "))")

# Collection of sources required to build XML2Builder
sources = [
    "ftp://xmlsoft.org/libxml2/libxml2-2.9.7.tar.gz" =>
    "f63c5e7d30362ed28b38bfa1ac6313f9a80230720b7fb6c80575eeab3ff5900c",

    "https://zlib.net/zlib-1.2.11.tar.gz" =>
    "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1",
]

script = raw"""
cd $WORKSPACE/srcdir
cd zlib-1.2.11/
./configure --prefix=/
make
make install
cd ../libxml2-2.9.7/
./configure --prefix=/ --host=$target --without-python --with-zlib=$(pwd)/../../destdir
make
make install
"""

products = prefix -> [
    LibraryProduct(prefix,"libxml2")
]


# Build the given platforms using the given sources
hashes = autobuild(pwd(), "XML2Builder", platforms, sources, script, products)

このファイルが,ライブラリのビルドに必要なすべての情報を含んでいます。これをJuliaで実行することで,各種プラットフォームのバイナリが完成します。試して見る場合はjulia build_tarballs.jlを実行してみて下さい。なお,macOSのビルドについては,ライセンスに同意する必要があるためBINARYBUILDER_AUTOMATIC_APPLE=trueと環境変数を設定する必要があります。

実は後ほど上のビルド方法ではWindows上でzlibが正しくビルドされないことがわかったため,Windows用に一部ビルドスクリプトを以下のように書き換えました。

# Windowsの場合
cd $WORKSPACE/srcdir
cd zlib-1.2.11/
./configure --prefix=/
make install LDSHAREDLIBC=''
cd ../libxml2-2.9.7/
./configure --prefix=/ --host=$target --without-python --with-zlib=$(pwd)/../../destdir
make install

最終的なbuild_tarballs.jlは以下のようになりました。

using BinaryBuilder

sources = [
    "ftp://xmlsoft.org/libxml2/libxml2-2.9.7.tar.gz" =>
    "f63c5e7d30362ed28b38bfa1ac6313f9a80230720b7fb6c80575eeab3ff5900c",
    "https://zlib.net/zlib-1.2.11.tar.gz" =>
    "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1",
]

products = prefix -> [
    LibraryProduct(prefix, "libxml2")
]


# Linux and Unix
# --------------

platforms = [
    BinaryProvider.Linux(:i686, :glibc),
    BinaryProvider.Linux(:x86_64, :glibc),
    BinaryProvider.Linux(:aarch64, :glibc),
    BinaryProvider.Linux(:armv7l, :glibc),
    BinaryProvider.Linux(:powerpc64le, :glibc),
    BinaryProvider.MacOS(),
]
script = raw"""
cd $WORKSPACE/srcdir
cd zlib-1.2.11/
./configure --prefix=/
make install
cd ../libxml2-2.9.7/
./configure --prefix=/ --host=$target --without-python --with-zlib=$(pwd)/../../destdir
make install
"""
autobuild(pwd(), "XML2Builder", platforms, sources, script, products)


# Windows
# -------

platforms = [
    BinaryProvider.Windows(:i686),
    BinaryProvider.Windows(:x86_64),
]
script = raw"""
cd $WORKSPACE/srcdir
cd zlib-1.2.11/
./configure --prefix=/
make install LDSHAREDLIBC=''
cd ../libxml2-2.9.7/
./configure --prefix=/ --host=$target --without-python --with-zlib=$(pwd)/../../destdir
make install
"""
autobuild(pwd(), "XML2Builder", platforms, sources, script, products)

https://github.com/bicycle1885/XML2Builder/blob/c3c28eb800c85fce3ebd8a584a8e463c773ea7a1/build_tarballs.jl

Travis CIでのビルドとデプロイ

上のようにするとローカルに目的のバイナリファイルが出来上がるのですが,これをTravis CI上で行うこともできます。その際にデプロイも設定しておくと,git pushするだけでビルドとデプロイが完了して便利です。

まず,前のセクションでできたGitレポジトリをGitHub上にホストします。私のlibxml2の場合,https://github.com/bicycle1885/XML2Builderにホストしました。次に,Travis CIでデプロイできるようにAPIキーを生成します。これはgem install travistravisというコマンドラインツールをインストールし,travis setup releasesを実行すると生成されます。このキーを.travis.ymlに次のような感じになるようにします。ついでに,環境変数BINARYBUILDER_AUTOMATIC_APPLE=trueも設定しておくと良いかもしれません。

deploy:
  provider: releases
  api_key:
    secure: {{ここにAPIキー}}
  file_glob: true
  file: products/*.tar.gz
  skip_cleanup: true
  on:
    repo: bicycle1885/XML2Builder
    tags: true

https://github.com/bicycle1885/XML2Builder/commit/0e19e58cfcdfb27e3f3eb67e7b6e2f40f67c0529

あとはTravis CIのビルドを有効にすれば,自動的にバイナリをビルドしてバイナリファイルをGitHubのhttps://github.com/bicycle1885/XML2Builder/releasesにこんな感じでデプロイしてくれます(これにはタグをつける必要があります)。

image.png

作ったバイナリをパッケージで使う

バイナリをビルドするログをよく見ると,deps/build.jlの書き方が記載されています。これをコピペしてきて上手いこと編集し,以下のようなファイルを作ります。これは,EzXML.jlをインストールするときに実行されるスクリプトです。このスクリプトが,インストールされたマシンに適切なライブラリのバイナリをダウンロードし,ハッシュ値をチェックしてインストールしてくれます。

using BinaryProvider

# This is where all binaries will get installed
prefix = Prefix(!isempty(ARGS) ? ARGS[1] : joinpath(@__DIR__, "usr"))

# Instantiate products here.  Examples:
libxml2 = LibraryProduct(prefix, "libxml2")

# Assign products to `products`:
products = [libxml2]

# Download binaries from hosted location
bin_prefix = "https://github.com/bicycle1885/XML2Builder/releases/download/v1.0.0"

# Listing of files generated by BinaryBuilder:
download_info = Dict(
    BinaryProvider.Linux(:aarch64, :glibc)     => ("$bin_prefix/XML2Builder.aarch64-linux-gnu.tar.gz", "cc64013edbf308f1d26e02b05b29c2072545276122884024a2e79871c4f23be6"),
    BinaryProvider.Linux(:armv7l, :glibc)      => ("$bin_prefix/XML2Builder.arm-linux-gnueabihf.tar.gz", "08c1b599aeda68e1783479b147e68a79224167ba4f693f431d02f734523b11a5"),
    BinaryProvider.Linux(:i686, :glibc)        => ("$bin_prefix/XML2Builder.i686-linux-gnu.tar.gz", "f6a076cfa78cc04f782428e23f1492b8c583fcf3d177a1b7eea206721666cecb"),
    BinaryProvider.Linux(:powerpc64le, :glibc) => ("$bin_prefix/XML2Builder.powerpc64le-linux-gnu.tar.gz", "2c04d6dd67107b933ed5fb2b2afd50892b6f3a94bdf2412604a49654f033947b"),
    BinaryProvider.Linux(:x86_64, :glibc)      => ("$bin_prefix/XML2Builder.x86_64-linux-gnu.tar.gz", "d3125723e5c75ded1eb01e5ce06c7dfae149ee276aa2d3f82a64edb2e8df23b8"),
    BinaryProvider.MacOS()                     => ("$bin_prefix/XML2Builder.x86_64-apple-darwin14.tar.gz", "c29fc38446b74830ce26d135974cc5043b636e0fe350bfa0e26e166edcebd7b7"),
    BinaryProvider.Windows(:i686)              => ("$bin_prefix/XML2Builder.i686-w64-mingw32.tar.gz", "d09490cc615b541c236485b170007911587501ee8c3a3e3edc8d054e5412553c"),
    BinaryProvider.Windows(:x86_64)            => ("$bin_prefix/XML2Builder.x86_64-w64-mingw32.tar.gz", "37f5b9d4df9e919a196dacbc2ede80b75a3aae7058890701958925336d145fb6"),
)
if platform_key() in keys(download_info)
    # First, check to see if we're all satisfied
    if any(!satisfied(p; verbose=true) for p in products)
        # Download and install binaries
        url, tarball_hash = download_info[platform_key()]
        install(url, tarball_hash; prefix=prefix, force=true, verbose=true)
    end

    # Finally, write out a deps.jl file that will contain mappings for each
    # named product here: (there will be a "libxml2" variable and a "xml2ifier"
    # variable, etc...)
    @write_deps_file libxml2
else
    error("Your platform $(Sys.MACHINE) is not supported by this package!")
end

https://github.com/bicycle1885/EzXML.jl/blob/3a7ea722f703814f1ef2ee2e6d878067b61a8126/deps/build.jl

build.jlが実行されると,ユーザの環境には次のようなdeps/deps.jlというファイルができます。

## This file autogenerated by BinaryProvider.@write_deps_file.
## Do not edit.
const libxml2 = "/Users/kenta/.julia/v0.6/EzXML/deps/usr/lib/libxml2.2.dylib"
function check_deps()
    global libxml2
    if !isfile(libxml2)
        error("$(libxml2) does not exist, Please re-run Pkg.build(\"EzXML\"), and restart Julia.")
    end

    if Libdl.dlopen_e(libxml2) == C_NULL
        error("$(libxml2) cannot be opened, Please re-run Pkg.build(\"EzXML\"), and restart Julia.")
    end

end

最後にdeps/deps.jlを読み込むために次のようなコードをパッケージ内に書いておくと,バイナリを読み込めます。

# Load libxml2.
const libxml2path = joinpath(dirname(@__FILE__), "..", "deps", "deps.jl")
if !isfile(libxml2path)
    error("EzXML.jl is not installed properly, run Pkg.build(\"EzXML\") and restart Julia.")
end
include(libxml2path)

https://github.com/bicycle1885/EzXML.jl/blob/3a7ea722f703814f1ef2ee2e6d878067b61a8126/src/EzXML.jl#L98-L103

これで,サポートしている環境であればどのマシンでも同じライブラリのバイナリが使われるようになります。