CMake ビルドを効率化!target_sources() を活用したソース管理テクニック

2025-03-21

target_sources() コマンドは、CMakeのビルドシステムにおいて、特定のターゲット(実行可能ファイル、ライブラリなど)にソースファイルを関連付けるために使用されます。簡単に言えば、「この実行可能ファイルやライブラリをビルドするには、これらのソースファイルが必要だよ」とCMakeに伝える役割を果たします。

基本的な構文

target_sources(<ターゲット名>
  <PRIVATE|PUBLIC|INTERFACE>
  <ソースファイル1>
  <ソースファイル2>
  ...
)

各要素の説明

  • <ソースファイル1>, <ソースファイル2>, ...: ターゲットに関連付けたいソースファイルのパスをリスト形式で指定します。相対パスまたは絶対パスを使用できます。CMakeLists.txt ファイルが存在するディレクトリからの相対パスが一般的です。

  • <PRIVATE|PUBLIC|INTERFACE>: これは、ソースファイルの可視性依存関係を制御するためのキーワードです。

    • PRIVATE: 指定されたソースファイルは、このターゲットの内部実装として扱われます。他のターゲットはこのソースファイルに直接アクセスしたり、このソースファイルから間接的な影響を受けたりすることはありません。通常、実行可能ファイルの .c, .cpp ファイルなどがこれに該当します。
    • PUBLIC: 指定されたソースファイルは、このターゲットのパブリックインターフェースの一部として扱われます。このターゲットにリンクする他のターゲットは、このターゲットのヘッダーファイル(通常は .h.hpp ファイル)の定義を利用できます。また、このターゲットのコンパイラフラグや定義なども、リンク先のターゲットに伝播する可能性があります。共有ライブラリのヘッダーファイルなどがこれに該当することが多いです。
    • INTERFACE: 指定されたソースファイルは、ターゲット自体のビルドには直接使用されませんが、このターゲットにリンクする他のターゲットに対してインターフェース情報(例えば、インクルードディレクトリ、コンパイラフラグ、定義など)を提供するために使用されます。ヘッダーオンリーライブラリや、他のターゲットに影響を与える設定のみを持つターゲットなどで使用されます。ソースファイル自体は通常指定されません(または、プレースホルダー的なファイルが指定されることがあります)。
  • <ターゲット名>: ソースファイルを関連付けたいターゲットの名前を指定します。これは add_executable()add_library() コマンドで定義したターゲットの名前です。

target_sources() の役割と利点

  • インターフェースの提供: PUBLICINTERFACE を使用することで、ライブラリなどのインターフェースを他のターゲットに適切に公開できます。
  • 可視性の制御: PRIVATE, PUBLIC, INTERFACE キーワードを使用することで、ターゲット間の依存関係を明確に定義し、意図しない依存関係の発生を防ぐことができます。これにより、よりモジュール化された、保守しやすいプロジェクトを構築できます。
  • 依存関係の追跡: CMakeは target_sources() で指定されたソースファイルの変更を監視し、必要に応じてターゲットを再ビルドします。
  • ソースファイルの管理: プロジェクトで使用するすべてのソースファイルをCMakeに明示的に伝えることで、ビルドプロセスを正しく管理できます。

使用例

# 実行可能ファイル 'my_app' を定義
add_executable(my_app main.cpp utils.cpp)

# ライブラリ 'my_lib' を定義
add_library(my_lib STATIC my_lib.cpp my_lib.h)

# 'my_app' ターゲットにソースファイルを追加 (PRIVATE)
target_sources(my_app
  PRIVATE
    main.cpp
    utils.cpp
)

# 'my_lib' ターゲットにソースファイルを追加 (PUBLIC ヘッダーファイルを含む)
target_sources(my_lib
  PRIVATE
    my_lib.cpp
  PUBLIC
    my_lib.h
)

# 'another_app' が 'my_lib' にリンクする場合、'my_lib.h' の定義を利用できる
add_executable(another_app another_main.cpp)
target_link_libraries(another_app my_lib)

この例では、my_appmain.cpputils.cpp を内部実装として使用し、my_libmy_lib.cpp を内部実装とし、my_lib.h をパブリックインターフェースとして公開しています。another_appmy_lib にリンクすると、my_lib.h の内容を利用できます。



ファイルが見つからない (File not found)

  • トラブルシューティング
    • 指定したファイルパスを再度確認し、スペルミスや大文字・小文字の間違いがないか確認する。
    • ファイルが実際にその場所に存在するか確認する。
    • 相対パスを使用している場合は、CMakeLists.txt ファイルからの相対的な位置関係が正しいか確認する。必要であれば、絶対パスを使用してみる。
    • CMake の設定 (configure) を実行するディレクトリが意図した場所であるか確認する。
  • 原因
    • target_sources() で指定したソースファイルのパスが間違っている。
    • ファイルが実際に指定された場所に存在しない。
    • 相対パスを使用している場合に、CMakeLists.txt ファイルからの相対位置が間違っている。
  • エラー内容
    CMake の構成時に、「指定されたファイルが見つかりません」というエラーメッセージが表示される。

同じソースファイルが複数のターゲットに追加されている

  • 原因
    同じ .c.cpp ファイルを複数の実行可能ファイルやライブラリに PRIVATE として追加している。
  • エラー内容
    ビルド時に、シンボルの重複などのリンカエラーが発生する可能性がある。CMake 自体は警告を出す場合と出さない場合がある。

ヘッダーファイルが PRIVATE として追加されている

  • トラブルシューティング
    • ヘッダーファイルは通常、target_sources() ではなく、必要に応じて install(FILES ... DESTINATION include) などでインストール先を指定し、他のターゲットからは target_include_directories() などでインクルードパスを追加します。
    • もしヘッダーファイルがターゲットのビルドに必要な内部的なもので、外部に公開する必要がない場合は PRIVATE でも構いませんが、通常は公開されるべきものです。
  • 原因
    ヘッダーファイル (.h, .hpp) を PRIVATE として target_sources() に追加している。PRIVATE は内部実装のソースファイルに対して使用するもので、ヘッダーファイルは通常、公開されるインターフェースの一部です。
  • エラー内容
    他のターゲットがこのターゲットのヘッダーファイルの内容を利用しようとしても、コンパイラがヘッダーファイルを見つけられないというエラーが発生する。

PUBLIC または INTERFACE の意図しない伝播

  • トラブルシューティング
    • 本当にそのヘッダーファイルが依存先のターゲットのインターフェースの一部であるべきか再検討する。内部実装の詳細を含むヘッダーファイルは PRIVATE に留めるべきです。
    • INTERFACE_INCLUDE_DIRECTORIES などのより細かな制御を行うコマンドの使用を検討する。
  • 原因
    PUBLICINTERFACE は、依存するターゲットに影響を与えるため、どのファイルをこれらのキーワードで指定するかを慎重に検討する必要があります。
  • エラー内容
    あるライブラリの PUBLIC または INTERFACE として追加したソースファイル(通常はヘッダーファイル)の変更が、意図しない依存関係を持つ他のターゲットの再ビルドを引き起こす。

ソースファイルの追加忘れ

  • トラブルシューティング
    • エラーメッセージからどのシンボルが未定義か特定し、そのシンボルが定義されているはずのソースファイルが target_sources() に含まれているか確認する。
  • 原因
    ターゲットに必要なソースファイルが target_sources() で指定されていない。
  • エラー内容
    ビルド時に、「未定義の参照」などのリンカエラーが発生する。

ビルドシステムがソースファイルの変更を認識しない

  • トラブルシューティング
    • 一度 CMake のキャッシュを削除して (ビルドディレクトリ内の CMakeCache.txt を削除するなど)、再度 CMake の構成を実行する。
    • target_sources() でソースファイルが正しく指定されているか確認する。
  • 原因
    • CMake のキャッシュが古い情報を持っている可能性がある。
    • ソースファイルが target_sources() に正しく登録されていない。
  • エラー内容
    ソースファイルを変更しても、CMake が再ビルドを行わない。
  • ビルドディレクトリをクリーンアップする
    古いビルド情報が残っている場合に、ビルドディレクトリを削除して再構築することで問題が解決することがあります。
  • CMake のドキュメントを参照する
    target_sources() コマンドの詳細や関連するコマンドについて、CMake の公式ドキュメントを確認する。
  • 簡単な例で試す
    問題が複雑な場合に、最小限のコードで問題を再現できるかどうか試してみることで、原因を特定しやすくなります。
  • CMake の出力メッセージを注意深く読む
    エラーメッセージや警告メッセージには、問題の原因に関する重要な情報が含まれています。


例1: 単純な実行可能ファイル

# CMakeの最小バージョンを指定
cmake_minimum_required(VERSION 3.10)

# プロジェクト名を定義
project(SimpleApp)

# 実行可能ファイル 'my_app' を定義し、ソースファイルを関連付け
add_executable(my_app main.cpp utils.cpp)

# 'my_app' ターゲットにソースファイルを追加 (PRIVATE)
target_sources(my_app
  PRIVATE
    main.cpp
    utils.cpp
)

説明

  • target_sources(my_app PRIVATE main.cpp utils.cpp): 同じターゲット my_app に対して、target_sources() を使用して再度ソースファイルを指定しています。ここでは PRIVATE キーワードを使用しています。これは、main.cpputils.cppmy_app の内部実装であり、他のターゲットからは直接的に利用されないことを意味します。
  • add_executable(my_app main.cpp utils.cpp): my_app という名前の実行可能ファイルを定義し、初期のソースファイルとして main.cpputils.cpp を指定しています。

例2: 静的ライブラリと実行可能ファイル

cmake_minimum_required(VERSION 3.10)
project(LibraryApp)

# 静的ライブラリ 'mylib' を定義
add_library(mylib STATIC mylib.cpp mylib.h)

# 'mylib' ターゲットにソースファイルを追加
target_sources(mylib
  PRIVATE
    mylib.cpp
  PUBLIC
    mylib.h
)

# 実行可能ファイル 'main_app' を定義
add_executable(main_app main.cpp)

# 'main_app' ターゲットにソースファイルを追加
target_sources(main_app
  PRIVATE
    main.cpp
)

# 'main_app' を 'mylib' にリンク
target_link_libraries(main_app mylib)

# 'mylib' のヘッダーファイルのインクルードディレクトリを追加
target_include_directories(main_app
  PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR} # mylib.h が存在するディレクトリ
)

説明

  • target_include_directories(main_app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}): main_appmylib.h をインクルードできるように、ヘッダーファイルが存在するディレクトリをインクルードパスに追加しています。ここでは PRIVATE を使用しているので、このインクルードパスは main_app のビルドにのみ影響し、main_app にリンクする他のターゲットには伝播しません。
  • target_link_libraries(main_app mylib): main_appmylib にリンクすることを指定しています。
  • add_executable(main_app main.cpp)target_sources(main_app PRIVATE main.cpp): 実行可能ファイル main_app とその内部実装のソースファイルを定義しています。
  • target_sources(mylib PRIVATE mylib.cpp PUBLIC mylib.h): mylib.cpp はライブラリの内部実装なので PRIVATEmylib.h はライブラリの公開インターフェースなので PUBLIC として関連付けています。これにより、mylib にリンクする他のターゲットは mylib.h の内容を利用できます。
  • add_library(mylib STATIC mylib.cpp mylib.h): mylib という名前の静的ライブラリを定義し、初期のソースファイルとして mylib.cppmylib.h を指定しています。

例3: ヘッダーオンリーライブラリ (INTERFACE)

cmake_minimum_required(VERSION 3.10)
project(HeaderOnlyLib)

# ヘッダーオンリーライブラリ 'header_lib' を定義 (ソースファイルは不要)
add_library(header_lib INTERFACE)

# インターフェースのインクルードディレクトリを設定
target_include_directories(header_lib
  INTERFACE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# 実行可能ファイル 'consumer_app' を定義
add_executable(consumer_app main.cpp)

# 'consumer_app' を 'header_lib' にリンク
target_link_libraries(consumer_app header_lib)

# 'consumer_app' のソースファイルを追加
target_sources(consumer_app
  PRIVATE
    main.cpp
)

# 'consumer_app' がヘッダーファイルを使用するためにインクルードディレクトリは不要
# なぜなら 'header_lib' の INTERFACE_INCLUDE_DIRECTORIES が伝播するから
  • target_link_libraries(consumer_app header_lib): consumer_appheader_lib にリンクしています。これにより、header_lib で設定されたインターフェース情報(この場合はインクルードディレクトリ)が consumer_app に伝播します。
  • add_executable(consumer_app main.cpp)target_sources(consumer_app PRIVATE main.cpp): 実行可能ファイルとそのソースファイルを定義しています。
  • target_include_directories(header_lib INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include): header_lib のインターフェースとしてインクルードディレクトリを設定しています。INTERFACE キーワードにより、header_lib にリンクするすべてのターゲットは、このインクルードディレクトリを自動的に利用できるようになります。
  • add_library(header_lib INTERFACE): header_lib という名前のインターフェースライブラリを定義しています。INTERFACE ライブラリは、自身は具体的なコードを持たず、主にコンパイラフラグやインクルードディレクトリなどの情報を他のターゲットに提供するために使用されます。


add_executable() および add_library() コマンドでの直接指定

最も基本的な代替方法は、add_executable()add_library() コマンドの引数として直接ソースファイルを指定する方法です。

# 実行可能ファイルを定義し、同時にソースファイルを指定
add_executable(my_app main.cpp utils.cpp)

# 静的ライブラリを定義し、同時にソースファイルとヘッダーファイルを指定
add_library(mylib STATIC mylib.cpp mylib.h)

説明

  • target_sources() との違い
    • 可視性 (PRIVATE, PUBLIC, INTERFACE) を指定することはできません。デフォルトでは、これらのコマンドで指定されたソースファイルはターゲットのプライベートなものとして扱われます。
    • 後からソースファイルを追加したり、可視性を変更したりする場合は、target_sources() を使用する必要があります。
  • この方法は、小規模なプロジェクトや、ターゲットのソースファイルが比較的固定されている場合に簡潔に記述できます。
  • add_executable()add_library() コマンドは、ターゲットの作成と同時に、そのターゲットに含めるべきソースファイルを直接指定できます。

変数を利用したソースファイルのリスト管理

ソースファイルのリストをCMake変数に格納し、それを add_executable()add_library()、または target_sources() に渡す方法です。

# ソースファイルのリストを変数に格納
set(MY_APP_SOURCES
  main.cpp
  utils.cpp
)

# ヘッダーファイルのリストを変数に格納
set(MY_LIB_HEADERS
  mylib.h
)

# ライブラリのソースファイルのリストを変数に格納
set(MY_LIB_SOURCES
  mylib.cpp
  ${MY_LIB_HEADERS} # ヘッダーファイルもリストに含めることは可能ですが、通常は分けて管理します
)

# 変数を使って実行可能ファイルを定義
add_executable(my_app ${MY_APP_SOURCES})

# 変数を使ってライブラリを定義し、target_sources() で可視性を指定
add_library(mylib STATIC)
target_sources(mylib
  PRIVATE
    ${MY_LIB_SOURCES}
  PUBLIC
    ${MY_LIB_HEADERS}
)

説明

  • ${変数名} の形式で、変数の内容を展開してコマンドの引数として渡すことができます。
  • set() コマンドを使って、ソースファイルのパスをリスト形式で変数に格納しています。

ファイルセット (File Sets)

CMake 3.13 以降で導入されたファイルセットは、論理的なグループとしてファイルを管理し、ターゲットに関連付けるためのより構造化された方法を提供します。

cmake_minimum_required(VERSION 3.13)
project(FileSetsExample)

# ファイルセットを定義
file_set(HEADERS TYPE HEADERS FILES
  include/mylib.h
  VISIBILITY PUBLIC
)

file_set(SOURCES TYPE CXX_SOURCES FILES
  src/mylib.cpp
  src/main.cpp
  VISIBILITY PRIVATE # デフォルトの可視性
)

# 静的ライブラリを定義し、ファイルセットを関連付け
add_library(mylib STATIC $<FILE_SET:HEADERS> $<FILE_SET:SOURCES>)

# 実行可能ファイルを定義し、ファイルセットを関連付け
add_executable(my_app $<FILE_SET:SOURCES>)

# 特定のターゲットに対して、ファイルセットの可視性を上書きすることも可能
target_sources(my_app
  PRIVATE
    $<FILE_SET:SOURCES> # ここでは PRIVATE として明示的に指定
)

# ヘッダーファイルのインクルードディレクトリを設定 (ファイルセットの可視性 PUBLIC を利用)
target_include_directories(mylib
  PUBLIC
    $<FILE_SET_TARGET:HEADERS> # ファイルセットに関連付けられたターゲットのパス
)

# 実行可能ファイルがライブラリにリンク
target_link_libraries(my_app mylib)

# 実行可能ファイルにインクルードディレクトリを追加 (PUBLIC なヘッダーを利用するため)
target_include_directories(my_app
  PRIVATE # または PUBLIC/INTERFACE に応じて
    include
)

説明

  • 利点
    • ファイルを論理的にグループ化できるため、大規模なプロジェクトでの管理が容易になります。
    • ファイルの種類ごとに異なる扱いを定義できます(例:ヘッダーファイルは自動的にインクルードパスに追加するなど)。
    • 可視性をファイルセットレベルで管理できます。
  • <FILE_SET_TARGET:ファイルセット名> を使うと、ファイルセットに関連付けられたターゲットのパスなどを取得できます。
  • <FILE_SET:ファイルセット名> というジェネレーター式を使って、ターゲットにファイルセットを関連付けます。
  • VISIBILITY 引数で、ファイルセットのデフォルトの可視性を設定できます。
  • TYPE 引数でファイルの種類を指定し、FILES 引数で具体的なファイルをリストアップします。
  • file_set() コマンドを使用して、HEADERSCXX_SOURCES という名前のファイルセットを定義しています。

ディレクトリ走査と file(GLOB) または file(GLOB_RECURSE)

cmake_minimum_required(VERSION 3.10)
project(GlobExample)

# 特定のディレクトリ内のすべての .cpp ファイルを検索
file(GLOB_RECURSE MY_SOURCES "src/*.cpp")

# 実行可能ファイルを定義し、検索されたソースファイルを関連付け
add_executable(my_app ${MY_SOURCES})

# 特定のディレクトリ内のすべてのヘッダーファイルを検索
file(GLOB MY_HEADERS "include/*.h")

# ライブラリを定義し、ソースファイルとヘッダーファイルを関連付け
add_library(mylib STATIC mylib.cpp ${MY_HEADERS}) # ヘッダーファイルは通常ソースとしてコンパイルされませんが、リストに含めることはできます
target_sources(mylib
  PRIVATE
    mylib.cpp
  PUBLIC
    ${MY_HEADERS}
)

# ヘッダーファイルのインクルードディレクトリを追加
target_include_directories(mylib
  PUBLIC
    include
)

target_link_libraries(my_app mylib)
target_include_directories(my_app
  PRIVATE
    include
)

説明

  • 注意点
    • GLOB および GLOB_RECURSE は、CMake の構成時にファイルリストを生成します。新しいファイルが追加されたり、既存のファイルが削除されたりした場合、CMake の構成を再実行するまでビルドシステムは変更を認識しません。
    • 大規模なプロジェクトや、頻繁にファイルが追加・削除されるプロジェクトでは、明示的にファイルをリストアップする方法やファイルセットの使用が推奨されます。GLOB は、ファイルリストが比較的静的な場合に便利です。
  • 検索されたファイルのリストを変数展開 ${MY_SOURCES} などを使って、add_executable()target_sources() に渡します。
  • file(GLOB_RECURSE) は指定されたパターンに一致するファイルを、指定されたディレクトリ以下のすべてのサブディレクトリから再帰的に検索します。
  • file(GLOB) は指定されたパターンに一致するファイル(サブディレクトリは除く)を検索し、その結果を変数に格納します。

target_sources() はソースファイルをターゲットに関連付けるための主要なコマンドですが、

  • ファイルセット
    大規模プロジェクトでの構造化されたファイル管理と可視性制御に強力。
  • 変数を利用したリスト管理
    ソースリストの再利用や管理の簡略化に有効。
  • add_executable() / add_library() で直接指定
    小規模プロジェクトや初期定義に便利。