CMake で自動生成ファイルを扱う方法

# はじめに

開発時に、ある設定ファイルからソースファイルを自動生成してそれを使用するようにプロジェクトを整備したいことがあります。

例えば、 Protocol Buffers を使うプロジェクトの場合、 .proto ファイルを protoc コマンドでコンパイルしてソースファイル（.pb.cc）とヘッダーファイル（.pb.h）を生成し、そのソースファイルを C++ コンパイラでコンパイルする必要があります。

このようなプロジェクトでは、元になる .proto ファイルを更新するたびにソースファイルを生成し直す必要がありますが、その仕組みを CMake で自動化する方法について紹介します。

## サンプルプロジェクトについて

この記事で紹介する内容を含んだサンプルプロジェクトを [hotwatermorning/CMakeAutoGenTest](https://github.com/hotwatermorning/CMakeAutoGenTest) に用意しています。

# CMake の設定方法

CMake でファイルを自動生成するには、[add_custom_command()](https://cmake.org/cmake/help/latest/command/add_custom_command.html) コマンドと [add_custom_target()](https://cmake.org/cmake/help/latest/command/add_custom_target.html) コマンドをあわせて使用します。

具体的には以下のようにします。

## add_custom_command() コマンドの設定方法

add_custom_command() コマンドを以下のように設定します

* OUTPUT 引数に、自動生成されるファイル（.pb.h, .pb.cc）を指定する。
* DEPENDS 引数に、自動生成元のファイル（.proto）を指定する。
* COMMAND 引数に、自動生成を行うコマンドを設定する。

```cmake
add_custom_command(
  OUTPUT <自動生成されるファイル (e.g., .pb.h, .pb.cc)>
  DEPENDS <自動生成元のファイル (e.g., .proto)>
  COMMAND <自動生成処理のコマンド (e.g., protoc -I/usr/local/include ...)>
  )
```

## add_custom_target() コマンドの設定方法

add_custom_target() コマンドを以下のように設定します。

* DEPENDS 引数に、自動生成されるファイルを指定する。
* SOURCES 引数や [target_sources()](https://cmake.org/cmake/help/latest/command/target_sources.html) コマンドに、元になるファイルを指定する。[^nessessity-of-sources]

[^nessessity-of-sources]: SOURCES 引数や target_sources() コマンドの設定は必須ではないですが、これを行うと IDE 上で自動生成元のファイルがファイル一覧に現れるようになります。これを設定しておくと IDE 上で自動生成元のファイルを開いて編集できるようになり、開発の利便性が向上します。

```cmake
add_custom_target(<ターゲット名>
  DEPENDS <自動生成されるファイル (e.g., .pb.h, pb.cc)>
  )

target_sources(<ターゲット名>
  PRIVATE
  <自動生成元のファイル (e.g., .proto)>
  )
```

このようにすることで、 .proto ファイルに変更があったときに .pb.h や .pb.cc ファイルを自動生成するターゲットを定義できます。

あとは実行ファイルやライブラリを生成するターゲットで上記の add_custom_target() コマンドで定義したターゲットを依存ターゲットに設定することで、実行ファイルやライブラリのビルド時に、必要に応じて .proto ファイルのコンパイル処理が走るようになります。

# CMakeLists.txt の全体像

以上を踏まえて、 CMakeLists.txt は以下のようになります。
ただし、プロジェクト構成は以下のようになっています。

```
CMakeAutoGenTest
├── CMakeLists.txt    # CMake 設定ファイル
├── README.md
├── Schemas           # .proto ファイルの配置ディレクトリ
│   ├── MyData.proto
│   └── User.proto
└── Sources           # ソースファイルの配置ディレクトリ
    └── main.cpp
```

```cmake
cmake_minimum_required(VERSION 3.20)
set(CMAKE_CXX_STANDARD 17)

project(CMakeAutoGenTestProject VERSION 0.0.1)

##########################################################################

# .proto ファイルの配置ディレクトリ
set(PROTO_FILES_DIR "${CMAKE_CURRENT_LIST_DIR}/Schemas")

# .proto ファイルをコンパイルしたソースファイルの出力パス
set(PROTO_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/Schemas)

# Protobuf のインストール情報を取得
find_package(Protobuf REQUIRED)

# .proto ファイルの一覧
set(PROTO_FILES
  ${PROTO_FILES_DIR}/MyData.proto
  ${PROTO_FILES_DIR}/User.proto
  )

foreach(PROTO_FILE ${PROTO_FILES})
  get_filename_component(NAME ${PROTO_FILE} NAME_WE)
  list(APPEND PROTO_OUTPUT_FILES
    ${PROTO_OUTPUT_DIR}/${NAME}.pb.h
    ${PROTO_OUTPUT_DIR}/${NAME}.pb.cc
    )
endforeach()

# .proto ファイルをコンパイルするコマンドを定義
add_custom_command(
  OUTPUT ${PROTO_OUTPUT_FILES}  # 生成するソースファイルを OUTPUT に指定しておく。
  DEPENDS ${PROTO_FILES}        # 生成するソースファイルの元になるファイルを DEPENDS に指定する。
                                # DEPENDS に指定したファイルが更新されると、
                                # 次回 add_custom_target() で定義したターゲットのビルド時に
                                # add_custom_command() で定義したコマンドが再実行される。

  # .proto ファイルをコンパイルするコマンド
  COMMAND protobuf::protoc --proto_path=${PROTO_FILES_DIR} ${PROTO_FILES} --cpp_out=${PROTO_OUTPUT_DIR}
  VERBATIM
  )

# .proto ファイルをコンパイルするターゲットを定義。
add_custom_target(CompileSchemas
  DEPENDS ${PROTO_OUTPUT_FILES} # add_custom_command() の OUTPUT に指定したファイルを DEPENDS に指定する。
  )

# CompileSchemas ターゲットのソースファイルを設定する。
# add_custom_target() コマンドの SOURCES 引数に ${PROTO_FILES} を設定するのでも問題ない。
source_group(TREE ${PROTO_FILES_DIR} FILES ${PROTO_FILES})
target_sources(CompileSchemas PRIVATE ${PROTO_FILES})

##########################################################################

add_executable(AutoGenTest)

set(SOURCE_FILES
  Sources/main.cpp
  )

# CompileSchemas ターゲットで生成したソースファイルを
# ソースリストに追加する。
list(APPEND SOURCE_FILES ${PROTO_OUTPUT_FILES})
source_group(TREE ${CMAKE_CURRENT_LIST_DIR} FILES ${SOURCE_FILES})
target_sources(AutoGenTest PRIVATE ${SOURCE_FILES})

# 実行ファイルやライブラリを作成するターゲットに対して、
# CompileSchemas ターゲットを依存ターゲットに設定する。
add_dependencies(AutoGenTest CompileSchemas)

# CompileSchemas ターゲットで生成したヘッダーファイルを
# 探索可能にする。
target_include_directories(AutoGenTest
  PUBLIC
  ${PROTO_OUTPUT_DIR}
  )

target_link_libraries(AutoGenTest
  PUBLIC
  protobuf::libprotobuf
  protobuf::libprotobuf-lite
  protobuf::libprotoc
  )
```