それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

MySQL 8.0.31 以降でビルドが通らないケースを git bisect で調べる

はじめに

mtr のオプションを拡張するパッチを出そうと最新の MySQL をビルドしようとしたら、またしてもすんなりと行かないケースが見つかったので何が原因でだめだったかをまとめる。今回は特に原因がわかりにくい部類だったのでその調べ方もついでに載せる。

問題のビルド

現在最新のバージョンである 8.0.33 をビルドすることを考える。いつもは配布されているソースコードを boost 共々ダウンロードしてきているが、今回は git clone で引っ張ってきたものを利用する。
ビルド自体は以下の通りで、基本的にいつもと同じ。

$ cd build && cd $_
$ mkdir boost
$ cmake ../ -DCMAKE_BUILD_TYPE=Debug -DWITH_BOOST=./boost -DDOWNLOAD_BOOST=1 -DWITH_DEBUG=1
$ cmake --build . --config debug -j7

すると、この状態ではわかりにくいが別途 make VERBOSE=1 で実行するとどうやらエラーを返していることがわかった。

[ 55%] Linking CXX executable ../runtime_output_directory/build_id_test
cd /home/lrf141/mysqlProject/mysql-server/build/mysys && /usr/bin/cmake -E cmake_link_script CMakeFiles/build_id_test.dir/link.txt --verbose=1
/usr/bin/c++ -std=c++17 -fno-omit-frame-pointer -ftls-model=initial-exec  -Wall -Wextra -Wformat-security -Wvla -Wundef -Wmissing-format-attribute -Woverloaded-virtual -Wcast-qual -Wimplicit-fallthrough=5 -Wstringop-truncation -Wsuggest-override -Wmissing-include-dirs -Wextra-semi -Wlogical-op -Werror -DSAFE_MUTEX -DENABLED_DEBUG_SYNC -g  -Wl,--build-id=sha1 CMakeFiles/build_id_test.dir/build_id_test.cc.o CMakeFiles/build_id_test.dir/build_id.cc.o -o ../runtime_output_directory/build_id_test 
Verifying build-id
cd /home/lrf141/mysqlProject/mysql-server/build/runtime_output_directory && ./build_id_test | egrep -o [0-9a-f]{40} > build_id_test_1
cd /home/lrf141/mysqlProject/mysql-server/build/runtime_output_directory && /usr/bin/readelf -n ./build_id_test | grep "Build ID:" | egrep -o [0-9a-f]{40} > build_id_test_2
make[2]: *** [mysys/CMakeFiles/build_id_test.dir/build.make:116: runtime_output_directory/build_id_test] エラー 1
make[2]: *** ファイル 'runtime_output_directory/build_id_test' を削除します
make[2]: ディレクトリ '/home/lrf141/mysqlProject/mysql-server/build' から出ます
make[1]: *** [CMakeFiles/Makefile2:7136: mysys/CMakeFiles/build_id_test.dir/all] エラー 2
make[1]: ディレクトリ '/home/lrf141/mysqlProject/mysql-server/build' から出ます

おそらく、最後の build_id_test 周りが悪さをしてビルドに失敗することまではわかるがそれ以上に情報がない状態である。

git bisect の準備

git bisect を使うと問題のある最初のコミットを二分探索で探すことができる。ただ、最後にビルドしたのは 8.0.30 で、そこから 3 バージョン分を対象にするとやや時間がかかりそうなのでどのバージョンまで素でビルドできるかを念の為確かめた。すると、8.0.31 から失敗することがわかったので問題の修正が入ったコミットは 8.0.30 の最新コミットから 8.0.31 の最新コミットのどこかにあることがわかる。
MySQLリポジトリでは 8.0 ブランチに各バージョンのタグが打たれ管理されている雰囲気を感じるので git bisect で探索するべき範囲は以下のようになる。

$ git bisect start mysql-8.0.31 mysql-8.0.30

テストスクリプト

git bisect で探すべき大体の範囲がわかったのでテストスクリプトを用意する。ビルドが通るかどうかという観点でコミットを探索したいので以下のようなスクリプトとなる。

#!/bin/bash

mkdir build && cd $_
mkdir boost

cmake ../ -DCMAKE_BUILD_TYPE=Debug -DWITH_BOOST=./boost -DDOWNLOAD_BOOST=1 -DWITH_DEBUG=1
cmake --build . --config debug -j7
BUILD_STATUS=$?
cd `git rev-parse --show-toplevel`
rm -rf build

exit $BUILD_STATUS

CMakeCache などが残っていると厄介なのでビルドしたときのステータスコードを保存しておき、build ディレクトリを最後に消している。ここでわざわざ exit を実行しているのは git bisect においてテストスクリプトの終了ステータスが 0 かどうかが重要らしいので rm -rf ./build の終了ステータスを参照しないようにするためである。
ここまで用意できれば元気に git bisect を走らせる。

$ git bisect run ../testMySQLBuild.sh

ビルドが通らなくなったであろうコミット

しばらくするとビルドがいつも通りの方法で通らなくなったコミットが見つかる。

321f106987a3fd62035057c8b544e7a874bc76d3 is the first bad commit
commit 321f106987a3fd62035057c8b544e7a874bc76d3
Author: Tor Didriksen <tor.didriksen@oracle.com>
Date:   Mon Apr 4 12:22:09 2022 +0200

    WL#15161 Add read-only build-id as system variable to mysqld
    
    For Linux builds: add --build-id=sha1 when linking executables.
    
    From 'man ld':
    
       --build-id=style
    
       Request the creation of a ".note.gnu.build-id" ELF note section or
       a ".buildid" COFF section.  The contents of the note are unique
       bits identifying this linked file.  style can be "uuid" to use 128
       random bits, "sha1" to use a 160-bit SHA1 hash on the normative
       parts of the output contents, ......
    
       The "md5" and "sha1" styles produces an identifier that is always
       the same in an identical output file, but will be unique among all
       nonidentical output files.  It is not intended to be compared as a
       checksum for the file's contents.  A linked file may be changed
       later by other tools, but the build ID bit string identifying the
       original linked file does not change.
    
    There is a new cmake option WITH_BUILD_ID to disable build-id on Linux.
    
    Any mysql executable linking with mysys/build_id.cc (in particular
    mysqld) can read its own BuildID.  The implementation is based on
    dl_iterate_phdr(3) which has been supported in glibc since version
    2.2.4.  The server will read the BuildID at startup, convert it to a
    hexidecimal string, and store it in a global character array.
    
    mysqld will print BuildID for 'mysqld --help'.
    We use this to extend the contents of the file INFO_BIN.
    
    mysqld will also log BuildID in the server log at startup.
    
    The signal handler, used for handling segfaults and the like, will
    also print the BuildID.
    
    There is a new @@GLOBAL.build_id read-only system variable,
    with a basic mtr test, verifying that it is read-only.
    
    Change-Id: I89ce4c63fed311422b7fbea1b12d29ebf7884a41

 CMakeLists.txt                                    |  24 +++--
 cmake/info_macros.cmake.in                        |  23 ++++
 cmake/mysql_add_executable.cmake                  |   4 +
 config.h.cmake                                    |   1 +
 mysql-test/include/have_build_id.inc              |   6 ++
 mysql-test/suite/sys_vars/r/build_id_basic.result |  34 ++++++
 mysql-test/suite/sys_vars/t/build_id_basic.test   |  34 ++++++
 mysys/CMakeLists.txt                              |  23 ++++
 mysys/build_id.cc                                 | 121 ++++++++++++++++++++++
 mysys/build_id.h                                  |  40 +++++++
 mysys/build_id_test.cc                            |  36 +++++++
 packaging/deb-in/control.in                       |   1 +
 packaging/rpm-docker/mysql.spec.in                |   1 +
 packaging/rpm-fedora/mysql.spec.in                |   1 +
 packaging/rpm-oel/mysql.spec.in                   |   1 +
 packaging/rpm-sles/mysql.spec.in                  |   1 +
 share/messages_to_error_log.txt                   |   3 +
 sql/mysqld.cc                                     |  19 ++++
 sql/mysqld.h                                      |   4 +
 sql/signal_handler.cc                             |   4 +
 sql/sys_vars.cc                                   |   8 ++
 21 files changed, 383 insertions(+), 6 deletions(-)
 create mode 100644 mysql-test/include/have_build_id.inc
 create mode 100644 mysql-test/suite/sys_vars/r/build_id_basic.result
 create mode 100644 mysql-test/suite/sys_vars/t/build_id_basic.test
 create mode 100644 mysys/build_id.cc
 create mode 100644 mysys/build_id.h
 create mode 100644 mysys/build_id_test.cc

どうやらこのコミットらしい。
github.com

対策(暫定)

cmake 実行時に -DWITH_BUILD_ID=0 をつけてやればとりあえず回避はできる。

おわりに

debuginfo とか gdbデバッグ情報をどうやって持ってくるか、みたいなところに関連してそうな雰囲気はあるがこの辺は詳しくないので、今回はとりあえず回避することだけ。
なんか build-id を取得して、それの verify でコケていそうな感じはするが単純に通すだけなら cmake のオプションでどうにかなったので良しとする。

追記 (2023/05/29 23:03)

このツイートによってすべてを理解してしまった…

何が起きたか

まず最初に LANG=C にした状態で -DWITH_BUILD_ID=1 、つまりデフォルトの状態でもう一度ビルドしてみる。
その後 make VERBOSE=1 でコケていた部分の特に readelf 関連に的を絞って実行すると、出力結果は次の通りになる。

$ cd /home/lrf141/mysqlProject/mysql-server/build/runtime_output_directory && ./build_id_test
BuildID[sha1]=55fc665151e52fbbddb526fe78e4f23d5d7c6e3c

$ cd /home/lrf141/mysqlProject/mysql-server/build/runtime_output_directory && /usr/bin/readelf -n ./build_id_test 
Displaying notes found in: .note.gnu.property
  Owner                Data size 	Description
  GNU                  0x00000020	NT_GNU_PROPERTY_TYPE_0
      Properties: x86 feature: IBT, SHSTK
	x86 ISA needed: x86-64-baseline

Displaying notes found in: .note.gnu.build-id
  Owner                Data size 	Description
  GNU                  0x00000014	NT_GNU_BUILD_ID (unique build ID bitstring)
    Build ID: 55fc665151e52fbbddb526fe78e4f23d5d7c6e3c

Displaying notes found in: .note.ABI-tag
  Owner                Data size 	Description
  GNU                  0x00000010	NT_GNU_ABI_TAG (ABI version tag)
    OS: Linux, ABI: 3.2.0

それでは、ここで LANG=ja_JP.UTF-8 で同じことをやってみると結果はこうなる。

$ cd /home/lrf141/mysqlProject/mysql-server/build/runtime_output_directory && ./build_id_test
BuildID[sha1]=55fc665151e52fbbddb526fe78e4f23d5d7c6e3c

$ cd /home/lrf141/mysqlProject/mysql-server/build/runtime_output_directory && /usr/bin/readelf -n ./build_id_test
Displaying notes found in: .note.gnu.property
  所有者            データサイズ	Description
  GNU                  0x00000020	NT_GNU_PROPERTY_TYPE_0
      Properties: x86 feature: IBT, SHSTK
	x86 ISA needed: x86-64-baseline

Displaying notes found in: .note.gnu.build-id
  所有者            データサイズ	Description
  GNU                  0x00000014	NT_GNU_BUILD_ID (一意なビルドID ビット列)
    ビルドID: 55fc665151e52fbbddb526fe78e4f23d5d7c6e3c

Displaying notes found in: .note.ABI-tag
  所有者            データサイズ	Description
  GNU                  0x00000010	NT_GNU_ABI_TAG (ABI バージョンタグ)
    OS: Linux, ABI: 3.2.0

そして make VERBOSE=1 でわかったコケているコマンドをもう一度見てみる。

cd /home/lrf141/mysqlProject/mysql-server/build/runtime_output_directory && ./build_id_test | egrep -o [0-9a-f]{40} > build_id_test_1
cd /home/lrf141/mysqlProject/mysql-server/build/runtime_output_directory && /usr/bin/readelf -n ./build_id_test | grep "Build ID:" | egrep -o [0-9a-f]{40} > build_id_test_2

お分かり頂けただろうか。LANG=C でないと二行目で grep するべき "Build ID:" などというものは見つからないのである。
git bisect で見つけたコミットをよく見てみると build_id_test_1 と build_id_test_2 の diff がこれらのコマンドのあとに待っていて、それがコケたとわかる。

最終的な対策

cmake 実行時に -DWITH_BUILD_ID=0 を付与するか export LANG=C でビルドが通るようになる。

反省文

ちょくちょく開発用マシンの OS をクリーンインストールするのだが、特段こだわりがあるわけではないので面倒になって LANG=C を明示的に設定しないまま放置していたらそれが仇となった。