コナンがC++のシングルバイナリの問題解決!?


バイナリはいつも1つ!!

たった1つのバイナリ作る!見た目はコマンド!なかみはPython!その名は、パッケージマネージャー!コナン!

大体,言いたいことの9割ぐらいは言えたので満足です.

俺は高校生探偵のsudo sync which.幼なじみで同級生のrunと遊園地に遊びに行って・・・

先日,Photogrammetry on Docker ~ サーバ屋さんもXRしたい ~という記事を書きました.
これは,「フォトグラメトリができるDockerコンテナを作る」というのが主目的ではあったのですが,サブの課題として,「マルチステージビルドをする」「コンテナサイズの削減をする」といった目標がありました.何のため?ということを少し話をすると,「コンテナサイズを削減」すると,リリースする際のデプロイが速くなったり,オートスケーリングする際も早くスケールし,負荷へ対応しやすくするメリットがあります(らしい)
.そして,そのための手法として,マルチステージングビルドをすることで,コンテナのサイズを小さくすることがあります.今回は,実際にそういうことをやってみて感じたことがありました.

alpine linuxでglibcの依存が辛い

前々から,知り合いにそういう話を聞いていたのですが,実際に体験してみるとかなりしんどい.調べても調べてもうまくいかず,どうしようかな・・・と思ったところ,

全ての依存ライブラリを全てソースコードからコンパイルし直せば,こんな辛い思いしなくていいのでは?

という脳筋な解決策を思いつきました.
これは割と頭の悪い感じでもありますが,非現実的でもなくて,昨今の言語固有のパッケージマネージャーだとできるという肌感はありました.例えば,pipなどはnumpyをソースコード落としてきて自前でFortranをコンパイルしたりしますし.そこで,導入してみたのが,前述のC++用のパッケージマネージャーのCONANでした.というわけで,私が実際にやってみたCONANによる依存ライブラリのソースコードからの全ビルド・リンクの手順を書いておこうと思います.

注意事項

前の記事で紹介したDockerのリポジトリですが,シングルバイナリになっていません.

あくまでalpineLinuxでglibc等々のC++の依存ライブラリ関係が辛いことにならないように,できる限りライブラリを自前ビルドする手順を使い,動的にリンクするライブラリを減らしたり,glibcを静的リンクしたとき特有のエラーを回避するための手段として閲覧して下さい.

ALPN4869

今回,前の記事で作ったフォトグラメトリのリポジトリを題材に話します.

ベースイメージはalpine linuxを使っています.
まず事前準備として,各種ライブラリを先にインストールします.

$ apk add python3 python3-dev mesa-dev cmake alpine-sdk libexecinfo-dev

CONANの導入は非常に簡単です.pipで入ります.

$ pip install conan

CONANではconanfile.txtという設定ファイルを書きます.

conanfile.txt
[requires]
libjpeg/9b@bincrafters/stable
libpng/1.6.37@bincrafters/stable
libtiff/4.0.8@bincrafters/stable
libunwind/1.3.1@bincrafters/stable

[generators]
make

今回,ビルドしたmveはlibjpeg, libpng, libtiffが必要なので,それを指定します.あとalpineのビルド用にunwindが必要だったので,それを追加.また,mveはビルドシステムとして,makeを使っているので,それをgeneratorに指定します.

ライブラリのインストールは

$ conan install .

です.ただこの方法だとalpine linuxで動かないです.
なぜか.というとこの場合,conanのネット上にあるリポジトリからライブラリのバイナリをダウンロードしてきて,インストールしてしまうので,glibcへの依存が入ってしまいます.そのため,ソースコードからビルドする必要があります.そのコマンドは

$ conan install . --build

となります.依存関係が自動で解決され,依存するライブラリのソースコードが全てダウンロードされ,自動でビルドが走ります.正常に終了すると,

  • conan.lock
  • conanbuildinfo.mak
  • conanbuildinfo.txt
  • conaninfo.txt

の4つのファイルが生成されます.この中でconanbuildinfo.makが重要になってきます.
もともとmveのソフト自体,conanに対応していません.そのため,conanのMakefileを少し修正してやる必要があります.例えば,apps/bundle2pset/Makefileに関しては,

# ここから
include ../../conanbuildinfo.mak

#----------------------------------------
#     Prepare flags from variables
#----------------------------------------

CFLAGS          += $(CONAN_CFLAGS)
CXXFLAGS        += $(CONAN_CXXFLAGS)
CPPFLAGS        += $(addprefix -I, $(CONAN_INCLUDE_DIRS))
CPPFLAGS        += $(addprefix -D, $(CONAN_DEFINES))
LDFLAGS         += $(addprefix -L, $(CONAN_LIB_DIRS))
LDLIBS          += $(addprefix -l, $(CONAN_LIBS))
# ここまで

MVE_ROOT := ../..
TARGET := $(shell basename `pwd`)
include ${MVE_ROOT}/Makefile.inc

CXXFLAGS += -I${MVE_ROOT}/libs ${OPENMP}
LDLIBS += -lpng -ltiff -ljpeg ${OPENMP}

SOURCES := $(wildcard [^_]*.cc)
${TARGET}: ${SOURCES:.cc=.o} libmve.a libmve_util.a
    $(LINK.o) $^ $(LDLIBS) -o $@

clean:
    ${RM} ${TARGET} *.o Makefile.dep

.PHONY: clean

「ここから」「ここまで」のように書いてあるように,Makefile内にconanbuildinfo.makの
includeを貼り付けます.そして,CONANの管理しているコンパイラの依存フラグを追記してやります.mveでは,基本的にappの下のMakefileに対し,すべて付与しています.詳細はリポジトリをご確認ください.そのあと,

$ env OPENMP="-lunwind -lexecinfo -static-libgcc -static-libstdc++ -fopenmp" LDFLAGS="-s" make all -j8

というコマンドでビルドできます.

$ ldd apps/bundle2pset/bundle2pset
        /lib/ld-musl-x86_64.so.1 (0x7fe06846f000)
        libexecinfo.so.1 => /usr/lib/libexecinfo.so.1 (0x7fe067e04000)
        libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7fe06846f000)

lddで見ればわかりますが,依存関係が非常に少なくなっています.これで完成です.

ペロッ…こ、これは、Segmentation Fault!

おそらく,このようなバイナリの容量削減をやったことのある方はわかるかもしれませんが,

なんで-staticを付けないの?

と思われると思います.-staticはすべてのライブラリを静的リンクしてくれるオプションです.しかし,今回のmveの場合は動きませんでした.これがすごく厄介で,プログラム自体は途中まで動くのですが,突然Abortして死ぬという非常に追いにくい系のバグなので,調査を諦めました.
そのほかの解決法としましては,

GNU ldで一部をスタティックリンクにする

という記事があります.-Bdynamic-Bstaticオプションを駆使することで,動的なリンクと静的なリンクを使い分ける.といった方法があります.今回,これも採用しませんでした.理由は,Makefileをあまり書き換えたくなかったからです.mve自体,ほぼほぼ触ったことがなく,どういう依存関係でMakefileが作られているのか,わからなかったので,あまり凝ったことはしないでおこう.という思いがありました.また,

GCC static linking order

というstack overflowの質問がありますが,static linkをする際,そのライブラリのリンクの順番によっては,ビルドが通らないことがあります.このライブラリのリンクの順番を手動で探索するのが非常にめんどくさい.今回は,6つぐらいライブラリを利用していたので,6!(=720)パターンぐらい,リンクオプションの順番があります(実際はそこまで問題にはならないのですが).したがって,そのあたりを自動で解決したい.と思いまして,CONANを導入したところ,リンクオプションの依存関係やその順序は,ほぼ自動で解決することができました.
少し蛇足ではありますが,このバイナリサイズ削減の文脈で聞く,upxも試してみましたが,Segmentation Faultして,ダメでした.

Next CONAN's Hint!!

今回,3つビルドイメージを作ったので,その容量を比較してみます.

ubuntu-mve

Dockerfile
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y g++ make git libjpeg-dev libpng-dev libtiff-dev mesa-common-dev
RUN mkdir work
WORKDIR work
RUN git clone https://github.com/simonfuhrmann/mve.git
WORKDIR mve
RUN env CXXFLAGS="-flto -O3 -mtune=native -march=native" LDFLAGS="-flto" make -j16
RUN env HOME=/ make links
COPY run.sh run.sh

とりあえず適当に動くかubuntuをベースに作ってみたイメージ

alpine-mve(beta)

Dockerfile
FROM alpine
RUN apk add --no-cache --wait 10 alpine-sdk make git libjpeg tiff-dev libpng-dev mesa-dev libunwind-dev
RUN mkdir work
WORKDIR work
RUN git clone --depth=1 https://github.com/simonfuhrmann/mve.git
WORKDIR mve
RUN env OPENMP="-lpng -ltiff -ljpeg -lgomp -lexecinfo -lstdc++ -lz -fopenmp" LDFLAGS="-flto -s" make -j16
RUN mkdir bin
RUN find apps -type f | fgrep -v "." | grep -v Makefile | xargs -IXXXX cp XXXX bin/

FROM alpine
COPY --from=0 /work/mve/bin/* /bin/
RUN apk update --no-cache && apk add gcc g++ gmp libjpeg tiff libpng mesa libunwind libexecinfo
COPY run.sh run.sh


とりあえずubuntuでmveが動いたので,alpineでビルドし,マルチステージビルドを試したイメージ

alpine-mve(conan)
https://github.com/kotauchisunsun/alpine-mve
をビルドしたイメージ.今回のCONANを使ったもの.

alpine
オフィシャルのイメージ(持ってきただけ)

マルチステージビルドをしていないとはいえ,ubuntu-mveは重いですね.
alpine-mve(beta)はマルチステージビルドして171MBはちょっと重すぎる印象です(試行中だったので,ちょっと余計なライブラリも入れていますが・・・).そうすると,alpine-mve(conan)は37MBと130MB以上小さい.元のalpineが4MBということは,30MB程度しか増えていないので,割と健闘している方ではないでしょうか.

感想

もうコナンという名前を見てから,おもろーと思っていたのですが,実際にそこそこ動いてしまったところをみて,さらにおもろーとなってしまいました.そのうえで,風呂入っていたら色々とネタが下りてきたので,ふざけて書いてしまいました.楽しかったです.昔,mingwでwindowsでゲームを作っていた時,SDLのライブラリをダウンロードして,./configureして,makeして,make installして・・・その次は,libjpegを・・・その次はSDL Imageを・・・と手動で依存性を解決しながら,手動でビルドしていた時代と比べると隔世の感を禁じ得ないですね.それにしてもシングルバイナリは作るの大変・・・
割とネタのような記事ではありますが,何かの役に立てば幸いです.

俺は作りたいんだ・・・令和のシングル・バイナリをな!