Vimプラグインを作っていると、特定の機能がどのバージョンで実装されたか調べるのが難しいことがあります。
ヘルプ (:h vim7
, :h vim8
) や git log
のコミットメッセージから探せば多くのケースでは見つかるのですが、うまく検索できなくて見つからないこともあります。
また、急にテストが古いVimで落ちるようになったときに、どの機能が原因で落ちているのかつかめないこともあります。
確実な方法は git bisect
すれば良いのですが、これには2つの問題があります。
- bisect時にビルドする必要があるので時間がかかる
- 古いバージョンは最新のコンパイラではコンパイルできず、bisectスクリプトにいちいちパッチを書くのも大変
後は純粋な興味もあって、古いバージョンのビルド済みバイナリーを作っておこうと思い立ちました。
しかし1パッチごとにビルドするのはあまりに大変です。
そもそも当時のコンパイルでもビルドできず、その後のパッチで直ったりするバージョンがあるので、どうしてもビルドできないバージョンは存在します。
後はディスク容量の問題もあります。
まずは以下のようなスクリプトで100パッチごとにビルドしてみることにしました。
macOS・clang 12.0.0 (clang-1200.0.32.27) で v7.3.000 から v8.2.2000 までの103バージョンをビルドすることができました。
vim-backcompile.sh
set -euxo pipefail
if ! command -v gsed >/dev/null; then
echo 'gsed required' >&2
exit 1
fi
if [[ ! -d /tmp/vim ]]; then
git clone https://github.com/vim/vim.git /tmp/vim
fi
cd /tmp/vim
backup_dir=/tmp/vim-backup
mkdir -p "$backup_dir"
read -r -d '' -a tags < <(git tag --sort=-committerdate |
grep -E '^v\d[.]\d(?:[.]\d(?:\d*00)?)?$' && printf '\0')
for tag in "${tags[@]}"; do
git restore .
git switch --detach "$tag"
gsed -i '/sizeof(uint32_t) != 4/,+1s/exit/return/' src/auto/configure
gsed -i '/if (\(p\|res\|e1.*\) [!=]= NUL)/s/NUL/NULL/g' src/*.c
gsed -i '/#define VV_NAME/s/, {0}$//' src/eval.c
gsed -i '/static struct vimvar/,+4s/dictitem_T/dictitem16_T/;/char\s*vv_filler.*/d' src/eval.c
if ! git grep -q 'struct dictitem16_S' src/structs.h; then
gsed -i '/typedef struct dictitem_S dictitem_T;/a struct dictitem16_S {\n typval_T di_tv;\n char_u di_flags;\n char_u di_key[17];\n};\ntypedef struct dictitem16_S dictitem16_T;' src/structs.h
fi
gsed -i 's/__ARGS(\(.*\))/\1/' src/proto/os_mac_conv.pro
gsed -i 's/!builtin_first == i/(!builtin_first) == i/' src/term.c
gsed -i '/extern int sigaltstack/s/^/\/\//' src/os_unix.c
gsed -i '/+multi_byte/,+6s/FEAT_BIG/FEAT_NORMAL/' src/feature.h
if ! ./configure; then
make distclean || :
rm -f auto/config.cache
./configure
fi
make -j8
src/vim --version
major=$(./src/vim --version | head -n 1 | awk '{ print $5 }')
minor=$(./src/vim --version | grep patch | awk '{ print $3 }' | sed -e 's/^[0-9]*-//g' || :)
cp src/vim "$backup_dir/$(printf "vim-%s.%04d" "$major" "$minor")"
done
- ビルドが通っても起動できない事があったので、
src/vim --version
で起動チェックを行う
- 変わっていくソースコードにパッチファイルは使いにくいので、今のコンパイラでコンパイルできない箇所は
sed
で雑にパッチを当てる。
実行ファイルのサイズが少しずつ増える様子が分かりました。
今は上のスクリプトを少し変えて、25パッチごとにビルドして実行ファイルを保存しています。
ビルドできなかったのは 8.0.0750
(8.0.0751
で修正), 7.4.1625
(7.4.1626
で修正) のみでした。
ビルド済み実行ファイルが揃ったので、次はこれを使ってbisectします。
vim-bisect.sh
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
read -r -d '' -a VERSIONS < <(cd "$script_dir" && ls vim-[0-9]* |
sort --version-sort --field-separator=. && printf '\0')
find_index() {
for i in "${!VERSIONS[@]}"; do
if [[ "${VERSIONS[$i]}" = "$1" ]]; then
echo "$i"
fi
done
}
exec_command="${1:?specify bisecting command}"
low_version="${2:-${VERSIONS[0]}}"
high_version="${3:-${VERSIONS[${#VERSIONS[@]}-1]}}"
if [[ ! "$low_version" =~ ^vim- ]]; then
low_version="vim-$low_version"
fi
if [[ ! "$high_version" =~ ^vim- ]]; then
high_version="vim-$high_version"
fi
low_index="$(find_index "$low_version")"
high_index="$(find_index "$high_version")"
test_version() {
env VIM="$script_dir/$1" bash -c "$exec_command"
}
if test_version "$high_version"; then
echo "$high_version is good"
exit 1
fi
if ! test_version "$low_version"; then
echo "$low_version is bad"
exit 1
fi
bad_version="$high_version"
while [[ "$low_index" -lt "$high_index" ]]; do
mid_index="$(((low_index + high_index) / 2))"
version="${VERSIONS[$mid_index]}"
if test_version "$version"; then
echo "$version: good"
low_index="$((mid_index + 1))"
else
echo "$version: bad"
high_index="$((mid_index))"
bad_version="$version"
fi
done
echo "$bad_version is the bad version"
$VIM
という環境変数に実行ファイルのパスを入れるので、 vim-bisect.sh '! env THEMIS_VIM=$VIM /tmp/vim-themis/bin/themis --reporter spec >/dev/null'
のようなコマンドで vim-themis を使ったテストを回すことができます。
すべてのパッチバージョンに対する実行ファイルを用意できるわけではないので、git bisect
みたいにこのパッチがbad commitというところまではいけませんが、50パッチや25パッチの範囲くらいならコミットログに全部目を通すことはできます。
ビルド済みbisectはめちゃくちゃ便利なので、ぜひ試してみてください。
おしまい