はじめに
本記事は全5部構成のシリーズです。
- #1:概要・構成編
- #2:ビルド・単体テスト編
- #3:静的解析・カバレッジ編
- #4(本記事):ハマりどころ編 ← いまここ
- #5:GitLab経験者向けワークフロー構文解説
本記事では、CI環境の構築中に実際に遭遇したエラーとその対処法をまとめます。同じところでつまずいた方の参考になれば幸いです。また最後に、今回の構築でGitHub Copilotをどう活用したかについても触れます。
サンプルコードはこちらです。
https://github.com/YusukeHarada/c-sample
ハマりどころと対処
make test が成功するのにテストが1件も実行されない
状況: make test がexit 0で終了してCIも成功しているが、GoogleTestの [==========] 出力が一切出ない。
原因: main.c に main() が定義されており、-lgtest_main が提供する main() より優先されていた。リンカは .o ファイルをライブラリより先に処理するため、main_obj.o の main() が採用され、GoogleTestのランナーが起動しなかった。
この問題の厄介な点はCIが成功しているように見えること。 テストが0件でもexit 0で終了するため気づきにくい。
対処: main.c の main() をテストビルド時にマクロで除外する。
#ifndef TESTING
int main()
{
run_program();
return 0;
}
#endif // TESTING
$(OUT_DIR)/main_obj.o: $(SRC)
$(CC) $(CFLAGS) -DTESTING -c $(SRC) -o $(OUT_DIR)/main_obj.o
リンクエラー: undefined reference to 'add' / 'multiply' / 'run_program'
状況: make test を実行するとリンク時にエラーが発生する。
undefined reference to 'add'
undefined reference to 'multiply'
undefined reference to 'run_program'
原因: テストバイナリのリンク時に main.c のオブジェクトファイルが含まれていなかった。また、test_main.cc 側に main() が定義されており、GoogleTestの main() と競合していた。
対処:
-
test_main.ccから独自のmain()を削除する(GoogleTestが提供するmain()を使う) -
main.hにextern "C"ガードを追加する
#ifdef __cplusplus
extern "C" {
#endif
int add(int a, int b);
int multiply(int a, int b);
void run_program();
#ifdef __cplusplus
}
#endif
- makefileでテストビルド時に
main.cのオブジェクトを明示的にリンクする
$(OUT_DIR)/$(TEST_TARGET): $(OUT_DIR)/main_obj.o $(OUT_DIR)/main_test.o
$(CXX) $(CXXFLAGS) -o $(OUT_DIR)/$(TEST_TARGET) \
$(OUT_DIR)/main_obj.o $(OUT_DIR)/main_test.o $(GTEST_FLAGS)
$(OUT_DIR)/main_obj.o: $(SRC)
$(CC) $(CFLAGS) -DTESTING -c $(SRC) -o $(OUT_DIR)/main_obj.o
main.c は必ず $(CC)(gcc)でコンパイルする点がポイントです。$(CXX)(g++)でコンパイルすると後述のカバレッジ問題にもつながります。
gcov がカバレッジデータを出力しない
状況: make coverage を実行してもカバレッジが表示されない。No executable lines というメッセージが出る。
原因: main.c がC++コンパイラ(g++)でコンパイルされていた。gcovはコンパイル時と収集時のコンパイラが一致している必要があり、C言語のソースはgccでコンパイルしないと .gcda / .gcno ファイルが正しく生成されない。
対処: makefileの coverage ターゲットで main.c を必ず $(CC)(gcc)でコンパイルするよう修正。また CFLAGS と CXXFLAGS の両方にカバレッジフラグを付加する。
coverage: CXXFLAGS += $(COVERAGE_FLAGS)
coverage: CFLAGS += $(COVERAGE_FLAGS)
coverage: clean
$(CC) $(CFLAGS) -DTESTING -c $(SRC) -o $(OUT_DIR)/main_obj.o # gcc でコンパイル
$(CXX) $(CXXFLAGS) -c $(TEST_SRC) -o $(OUT_DIR)/main_test.o
...
確認方法として、.gcda / .gcno ファイルの有無を確認してみてください。
find . -name '*.gcda' -o -name '*.gcno' -print
これらのファイルが生成されていない場合、カバレッジフラグが正しく付与されていません。make clean && make coverage で再ビルドしてみてください。
lcov がエラーで終了する
状況: make coverage-html を実行すると lcov がエラーで終了する。
lcov: ERROR: mismatch while reading ...
または
lcov: ERROR: unused input files ...
原因: 新しいバージョンの lcov では、コンパイル時とカバレッジ収集時のソースパスの不一致(mismatch)や、フィルタ後にカバレッジデータが空になった場合(unused)にデフォルトでエラーを返すようになっている。
対処: 各lcovコマンドに --ignore-errors オプションを追加する。
# キャプチャ時
lcov --directory $(OUT_DIR) --capture \
--output-file $(OUT_DIR)/coverage/coverage.info \
--ignore-errors mismatch
# フィルタ時
lcov --remove $(OUT_DIR)/coverage/coverage.info \
'/usr/include/*' '/usr/local/*' '*/test_main.cc' \
--output-file $(OUT_DIR)/coverage/coverage_filtered.info \
--ignore-errors mismatch,unused
# HTML生成時
genhtml $(OUT_DIR)/coverage/coverage_filtered.info \
--output-directory $(OUT_DIR)/coverage/html \
--ignore-errors mismatch
actions/upload-artifact@v3 非推奨によるCI失敗
状況: GitHub ActionsのワークフローがNode.jsの非推奨警告とともに失敗する。
原因: actions/upload-artifact@v3 および actions/checkout@v3 がNode.js 16を使用しており、非推奨になっていた。
対処:
-
actions/checkout@v4に更新する -
actions/upload-artifact@v4に更新する
CI環境でのGoogleTestビルド失敗
状況: ローカルでは通るのにCI(ubuntu-latest)でGoogleTestのリンクに失敗する。
原因: libgtest-dev のライブラリパスがディストリビューションのバージョンによって異なるため、ハードコードしたパスが通らないケースがある。
対処: Makefileの GTEST_FLAGS にライブラリパスを明示的に指定する。
GTEST_FLAGS = -L/usr/lib/x86_64-linux-gnu -lgtest -lgtest_main -pthread
cppcheck によるCI失敗
状況: ローカルでは make check が通るのにCIで失敗する。
原因: ローカルの cppcheck とCI環境の cppcheck のバージョンが異なり、検出される警告が異なっていた。また --error-exitcode=1 が付いていなかったため、ローカルでは警告が出ていても気づかなかった。
対処: makefileの check ターゲットに --error-exitcode=1 を追加し、ローカルとCIで同じ挙動にする。
check:
cppcheck --enable=all --suppress=missingIncludeSystem --error-exitcode=1 $(SRC) main.h
おまけ:GitHub Copilotの活用
今回の構築ではGitHub Copilotを以下の場面で活用しました。
- makefileの初期テンプレートの生成
- ci.ymlのステップ構成の叩き台作成
- GoogleTestのテストケース追加
特にci.ymlは「ゼロから書き起こす手間」が大幅に減りました。各ステップの基本的な構成はCopilotに任せて、細かい調整だけ自分で行うという流れが効率的でした。
一方で、C/C++混在のリンクエラーやlcovのバージョン差異によるエラーはCopilotだけでは解消できず、エラーメッセージを読んで自分で原因を追う必要がありました。「生成してもらったものが動かない場合の調査」は依然として自分のスキルが問われる部分です。
Copilotは「調べる時間・書き起こす時間を減らす」ツールとして非常に有効ですが、そのまま動くとは限らないという点は念頭に置いておく必要があります。
まとめ
本記事まで(#1〜#4)で、C言語プロジェクトへのGitHub Actions CI環境構築を一通り解説しました。
| 項目 | 使用ツール |
|---|---|
| ビルド | gcc / make |
| 単体テスト | GoogleTest |
| 静的解析 | cppcheck |
| カバレッジ計測 | gcovr / lcov |
| CI | GitHub Actions |
C言語とC++(GoogleTest)の混在環境やlcovのバージョン差異など、いくつかハマりどころはありましたが、一度整備してしまえばpushのたびに自動で品質チェックが走る環境が手に入ります。
次回は、GitLab CI経験者向けにGitHub Actionsのワークフロー構文を対比形式で解説します。