【実務・中級編】GitHub ActionsでGCC/Clangを自動化!C言語プロジェクトのCI/CD導入術 – 実行環境・ランタイム・コンパイラ生産性向上バイブル

はじめに:なぜC言語プロジェクトにモダンなCI/CDが必要なのか

C言語の開発現場において、「手元ではビルドが通ったのに、CIサーバー(あるいは別の開発者の環境)でコンパイルエラーになる」「警告(Warning)のポリシーが人によって異なり、レガシーなコードベースが徐々に腐敗していく」といった問題に直面したことはないだろうか。

C言語は、OSのカーソルやメモリレイアウトに直接触れる極めて強力な言語である一方、コンパイラのバージョン、アーキテクチャ(x86_64, ARMなど)、さらにはプラットフォームごとのABI(Application Binary Interface)の差異によって、挙動が微妙に変化するという特性を持つ。

これを人間の目と手動のテストだけで担保しようとすれば、スケールしないことは火を見るよりも明らかだ。

本稿では、世界中のトップエンジニアが実践しているGitHub Actionsを用いたC言語のCI/CDパイプライン構築術を解説する。単に「ビルドが通ることを確認する」だけの稚拙なワークフローではなく、GCCとClangのマルチコンパイラチェック、厳格な警告オプションの強制、そしてAddressSanitizer(ASan)を用いたメモリリーク・不正アクセスの自動検出までを統合した、実戦投入レベルのベストプラクティスを提示する。

—

1. チーム開発の生産性を爆発させるプロジェクト構造

C言語のCI/CDを美しく回すためには、まずプロジェクトのディレクトリ構造が洗練されていなければならない。ビルドシステムには、依存関係の解決とクロスプラットフォームビルドの観点から `CMake` を採用する。

以下に、大規模な組み込みやシステムプログラミングにも耐えうる、実用的なディレクトリ構成を示す。

my_c_project/
├── .github/
│ └── workflows/
│ └── ci.yml # 本記事の核心:GitHub Actionsワークフロー
├── CMakeLists.txt # ルートCMake設定
├── cmake/
│ └── CompilerWarnings.cmake # コンパイラ警告の厳格化定義
├── include/
│ └── my_lib.h # 公開ヘッダー
├── src/
│ ├── CMakeLists.txt # ライブラリ本体のビルド定義
│ └── my_lib.c # 実装ファイル
└── tests/
├── CMakeLists.txt # テストスイートのビルド定義
└── test_main.c # テストコード(UnityやCheck等のフレームワーク想定)

なぜこの構造が優れているのか

  • 関心事の分離: ソースコード (`src`)、ヘッダー (`include`)、テスト (`tests`) が明確に分離されており、CMakeの `target_include_directories` でスコープを厳密に制御できる。
  • ビルドロジックのモジュール化: コンパイラ警告の設定を `cmake/CompilerWarnings.cmake` として切り出すことで、ローカル開発環境でもCI環境でも全く同一の「厳しい警告ポリシー」を強制できる。

—

2. 実戦投入仕様:GitHub Actions ワークフロー設定(YAML)

それでは、本記事のメインである `.github/workflows/ci.yml` の全貌を公開しよう。
この設定ファイルでは、Ubuntu環境上で GCC最新版 と Clang最新版 をマトリクスビルドさせ、さらに未定義動作やメモリ破壊を検知するSanitizerを有効化したテストを実行する。

name: C-Project CI/CD

トリガー設定:mainブランチへのプッシュ、およびすべてのプルリクエストで発動
on:
push:
branches: [ “main” ]
pull_request:
branches: [ “main” ]

ジョブの並列実行と環境設定
jobs:
build-and-test:
name: ${{ matrix.os }} / ${{ matrix.compiler }}
runs-on: ${{ matrix.os }}

# マトリクス戦略:OSとコンパイラの組み合わせを網羅的にテスト
strategy:
fail-fast: false # 片方のビルドが失敗しても、もう片方の結果を最後まで見届ける
matrix:
os: [ubuntu-latest]
compiler:

  • { name: “gcc”, c: “gcc”, cxx: “g++” }
  • { name: “clang”, c: “clang”, cpp: “clang++” }

steps:
# 1. リポジトリのソースコードをランナーにチェックアウト

  • name: Checkout repository

uses: actions/checkout@v4
with:
submodules: recursive # サブモジュールがある場合を考慮して再帰的に取得

# 2. ビルドツールとテストランナーの依存関係インストール(Ninjaでビルド高速化)

  • name: Install Dependencies (Ubuntu)

if: startsWith(matrix.os, ‘ubuntu’)
run: |
sudo apt-get update
sudo apt-get install -y cmake ninja-build valgrind

# 3. CMakeによる設定フェーズ(コンパイラを明示的に指定)

  • name: Configure CMake

env:
CC: ${{ matrix.compiler.c }}
CXX: ${{ matrix.compiler.cpp }}
run: |
cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DENABLE_WARNINGS_AS_ERRORS=ON \
-DENABLE_SANITIZER=ON

# 4. コンパイル実行(CPUコアをフル活用してビルド時間を極限まで短縮)

  • name: Build with CMake

run: cmake –build build –parallel $(nproc)

# 5. 単体テストの実行(Ctestを使用)

  • name: Run Unit Tests

run: |
cd build
ctest –output-on-failure –timeout 60

# 6. Valgrindによるメモリリーク静的/動的解析(GCCの場合のみ追加実行)

  • name: Run Valgrind Memory Check

if: matrix.compiler.name == ‘gcc’
run: |
valgrind –leak-check=full \
–show-leak-kinds=all \
–error-exitcode=1 \
./build/tests/unit_tests

このYAML設計の急所と技術的解説

1. `fail-fast: false` の重要性:
開発チームにとって、「GCCでは通るがClangでは落ちる」といったコンパイラ依存のバグを一度のCI実行で全て洗い出すことが極めて重要である。これを `true` にすると、最初にエラーを出したジョブで他のビルドが中断されてしまうため、必ず `false` に設定する。
2. `Ninja` ジェネレータの採用:
デフォルトのUnix Makefilesよりも並列ビルドのオーバーヘッドが少なく、数秒単位でCIのフィードバックループを高速化する。
3. Valgrindによる二重の防壁:
AddressSanitizerに加え、GCCのビルド時には `valgrind` を走らせることで、ヒープの解放漏れや不正なメモリアクセスをCIパイプライン上で完全にブロックする。

—

3. 妥協なきコード品質を保つ:コンパイラ警告の厳格化(CMake設定)

「警告(Warning)はエラーではない」という甘い認識は、C言語開発においては致命傷になり得る。未初期化変数の参照や暗黙の型変換は、しばしば深刻なセキュリティ脆弱性(バッファオーバーフロー等)に直結するからだ。

チーム全体で「警告を一切許容しない(Warning as Errors)」文化を強制するため、以下のCMakeモジュールを `cmake/CompilerWarnings.cmake` として配置する。

cmake/CompilerWarnings.cmake

function(set_project_warnings target_name)
# 開発者が指定したオプションに基づき、警告をエラーとして扱うかを決定
option(ENABLE_WARNINGS_AS_ERRORS “Treat compiler warnings as errors” ON)

# GCCおよびClang共通の厳格な警告フラグセット
set(CLANG_GCC_COMMON_WARNINGS
-Wall
-Wextra
-Wshadow # ローカル変数が外側のスコープの変数を隠す場合に警告
-Wnonnull # 引数にNULLが渡されないことが期待される箇所でNULLを検知
-Wcast-align # ポインタのキャストによってアライメントが厳しくなる場合に警告
-Wunused # 未使用の変数や関数を検知
-Wpedantic # ISO Cの標準規格に厳密に従っているかをチェック
-Wformat=2 # printf系のフォーマット文字列の脆弱性を厳しくチェック
-Wconversion # 予期せぬ型縮小変換(例: 64bitから32bitへのint代入)を検知
-Wsign-conversion # 符号付き/符号なしの暗黙の変換を検知
-Wnull-dereference # 明らかなヌルdereferenceを検知
)

# GCC固有の高度な警告
set(GCC_SPECIFIC_WARNINGS
${CLANG_GCC_COMMON_WARNINGS}
-Wduplicated-cond # if-elseチェーンで重複する条件式を検知
-Wlogical-op # 論理演算子の不審な使用を検知
-Wuseless-cast # 無意味なキャストを検知
)

# Clang固有の警告
set(CLANG_SPECIFIC_WARNINGS
${CLANG_GCC_COMMON_WARNINGS}
)

# コンパイラの種類に応じたフラグの適用
if(CMAKE_C_COMPILER_ID MATCHES “Clang”)
set(PROJECT_WARNINGS ${CLANG_SPECIFIC_WARNINGS})
elseif(C_COMPILER_ID STREQUAL “GNU” OR CMAKE_C_COMPILER_ID MATCHES “GNU”)
set(PROJECT_WARNINGS ${GCC_SPECIFIC_WARNINGS})
else()
message(AUTHOR_WARNING “No aggressive warnings set for compiler: ${CMAKE_C_COMPILER_ID}”)
return()
endif()

# 警告をエラーに昇格させるフラグの付加
if(ENABLE_WARNINGS_AS_ERRORS)
list(APPEND PROJECT_WARNINGS -Werror)
endif()

# ターゲットに対して警告フラグを設定
target_compile_options(${target_name} PRIVATE ${PROJECT_WARNINGS})
endfunction()

この設定をルートの `CMakeLists.txt` からインクルードし、自作のライブラリやテストターゲットに対して `set_project_warnings(my_library_target)` と呼び出すだけで、CI環境・ローカル環境を問わず、鉄壁のコードレビュー体制が自動構築される。

—

4. プロの現場で即座に役立つ実践テクニック

A. ローカル環境でのCI完全再現(Actの活用)

GitHub Actionsのワークフローを書くたびに「リモートプッシュして結果を待つ」という非効率なループを回していないだろうか?
ローカル環境でGitHub Actionsをそのまま実行できるツール `nektos/act` を導入せよ。

プロジェクトのルートディレクトリで以下のコマンド叩くだけで、Dockerコンテナ上で遠隔CIと全く同一のマルチコンパイラビルドをローカル実行できる。

ローカルでGCC/ClangのCIビルドを完全再現
act pull_request

B. AddressSanitizer(ASan)の有効化でセグフォを撲滅する

C言語のバグの代名詞である「セグメンテーション違反(Segfault)」や「メモリ破壊」。これをデバッグモード時に自動検出するため、CMakeLists.txtに以下のSanitizer設定を組み込んでおくと、CIのテスト実行時に不正メモリアクセスが即座に検知され、スタックトレースが出力される。

Sanitizerの有効化設定例
option(ENABLE_SANITIZER “Enable AddressSanitizer” ON)
if(ENABLE_SANITIZER)
if(CMAKE_C_COMPILER_ID MATCHES “GNU|Clang”)
target_compile_options(my_library_target PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
target_link_options(my_library_target PRIVATE -fsanitize=address,undefined)
endif()
endif()

—

おわりに:CI/CDがもたらすC言語開発のパラダイムシフト

C言語のような低レイヤ言語を扱うプロジェクトにおいて、CI/CDの導入は「贅沢品」ではなく、開発チームの精神衛生とプロダクトの寿命を担保するための「インフラストラクチャ」である。

今回紹介した、
1. GCC / Clangのマルチコンパイラ・マトリクスビルド
2. 妥協のないコンパイラ警告の厳格化(`-Werror` と詳細なフラグ群)
3. AddressSanitizerおよびValgrindによるメモリ安全性の自動検証

これらをGitHub Actionsに定着させることで、レビューアは「メモリリークしていないか」「コンパイル警告が出ていないか」という機械的な確認作業から解放され、より本質的なアルゴリズムの設計やアーキテクチャの改善に集中できるようになる。

今日からあなたのC言語リポジトリにも `.github/workflows/ci.yml` を配置し、モダンで堅牢な開発パイプラインを手に入れてほしい。

タイトルとURLをコピーしました