こんにちは。田原です。

今回から良く使うCMakeのコマンドについて解説していきます。まずは最も重要なビルド・ターゲットの定義方法です。ほとんどの場合、実行ファイルとライブラリのビルド・ターゲットの定義を行います。それらの他に自由に生成コマンドを指定できるカスタム・ターゲットの定義についても解説します。各コマンドの詳しい解説は当該コマンド名でググれば結構でてきますので、それらであまり見かけないEXCLUDE_FROM_ALLやALL指定に焦点を当ててみます。

1.ビルド・ターゲットを指定するコマンド3つ

ターゲットを定義する主なコマンドとして次の3つがあります。(前2つに比べると最後のadd_custom_targetの使用頻度は低いですが1つのプロジェクトで1つ~2つ使うことはそこそこあると思います。)

  1. add_executable
    ビルドする実行ファイル(いわゆる.exe)を定義します。
    使用するソース・ファイルをこのコマンドで指定します。
    リンクするライブラリはtarget_link_librariesを使って指定します。

  2. add_library
    ビルドするライブラリ(.lib、.dll、.a、.so、.dylibなどなど)を定義します。共有(linux系)/動的リンク(Windows)ライブラリ、静的リンク・ライブラリを指定できます。
    使用するソース・ファイルやリンクするライブラリについてはadd_executableと同様です。(ライブラリに他のライブラリをリンクする件は意外に奥が深いのですが、後々解説していきます。ここでは.exeの場合と同様とお考え下さい。)

  3. add_custom_target
    殆どの場合、上述のadd_executableとadd_libraryで足りるのですが、例えばDoxygenなどのコンパイラやリンカ以外のツールで何らかのファイルを生成する場合に用いることが多いです。

Doxygen
FOSS界隈でよく使われるドキュメント作成ツールです。C++のソース・コードの中にDoxygenの書式で書いたコメントなどから自動的にドキュメントを生成するツールです。Theolizerもこれを使っています。関連情報のリンクを自動的に貼ってくれますし、クラス毎のドキュメントなども自動生成してくれるので「リファレンス・マニュアル」を作る際の強い味方です。

2.EXCLUDE_FROM_ALL/ALL指定について

ビルドする際、主なビルド・ターゲットについては「デフォルト」でビルド指定し、いちいちターゲットを指定しないで良いようにすることが多いと思います。

例えば、次のようにしてビルドすると「デフォルト」のビルド・ターゲットが全てビルドされます。

// VC++
> cmake --build .

// gcc
$ make

では、「デフォルト」のビルド・ターゲットってどうやって指定するのでしょうか?
指定したくなることが少ないためか、調べても意外に分からないので苦労しました。
主にEXCLUDE_FROM_ALL、ALLを使います。Visual Studio専用のEXCLUDE_FROM_DEFAULT_BUILDもあります。

2019/01/06 以下の修正しました。
誤>$ make sample
正>$ make

2-1.add_executable と add_library と EXCLUDE_FROM_ALL

何も指定しなければ「デフォルト」のビルド・ターゲットとなります。
ターゲット名の直後で EXCLUDE_FROM_ALL を指定すると「デフォルト」のビルド・ターゲットから外れます。

add_library(lib EXCLUDE_FROM_ALL lib.cpp lib.h)
add_executable(main EXCLUDE_FROM_ALL main.cpp lib.h)
target_link_libraries(main lib)     # mainがlibをリンクすることの指定

ただし、以下の時はちゃんとビルドされます。

  1. makeや–buildで明示的にターゲット指定した時
> cmake --build . --target main
$ make main
  1. 他のビルドされるターゲットから依存されている時
    例えば、あるライブラリlibをEXCLUDE_FROM_ALL しても、そのライブラリlibを使う実行ファイルmainがビルドされた場合、このライブラリlibもビルドされます。
add_library(lib EXCLUDE_FROM_ALL lib.cpp lib.h) # mainが依存しているのでmainがビルドされる時ビルドされる
add_executable(main main.cpp lib.h) # EXCLUDE_FROM_ALLしていないのでデフォルトでビルドされる
target_link_libraries(main lib)     # mainがlibをリンクすることの指定
> cmake --build .
$ make

2-2.add_custom_target と ALL

何も指定しなければ「デフォルト」のビルド・ターゲットではありません。
ターゲット名の直後で ALL を指定すると「デフォルト」のビルド・ターゲットになります。

サンプルは後述します。

2-3.Visual Studio と EXCLUDE_FROM_DEFAULT_BUILD

VC++の場合でも、cmake –build . でビルドする時は、EXCLUDE_FROM_ALL は適切に機能するのですが、Visual Studio上でビルドする時は残念なことに、EXCLUDE_FROM_ALL が機能しません。Visual StudioではEXCLUDE_FROM_ALL相当の機能を実現できないのだと思います。
代わりにEXCLUDE_FROM_DEFAULT_BUILD という指定があります。こちらはちょっと残念な機能です。

EXCLUDE_FROM_DEFAULT_BUILDを指定されたターゲットはVisual Studio上の「ソリューションのビルド」のビルド・ターゲットから外れます。そして、残念な点はEXCLUDE_FROM_DEFAULT_BUILDを指定したターゲットは他のターゲットから依存されていてもビルドされません。なので、デフォルト・ビルド時にはビルド不要だけど、他の特定のターゲットをビルドする時だけビルドするという設定ができません。

add_library(lib EXCLUDE_FROM_ALL lib.cpp lib.h)
set_target_properties(lib PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD TRUE)

add_executable(main EXCLUDE_FROM_ALL main.cpp lib.h)
set_target_properties(main PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD TRUE)
target_link_libraries(main lib)     # mainがlibをリンクすることの指定

add_executable(always always.cpp)

EXCLUDE_FROM_DEFAULT_BUILD は set_target_propertiesコマンドで指定しますので、上記のように設定してmainとlibをVisual Studioのビルド・ターゲットから外すと次のように振る舞います。

  1. Visual Studio上の「ビルド(B)→構成マネージャ(O)…」でみると、mainとlibはビルド・ターゲットから外れています。

  2. ソリューションをビルドした時、mainとlibはビルドされません。

  3. ソリューション エクスプローラで mainを指定してビルドしても lib はビルドされません。なので、libがビルドされていない場合、mainはリンク・エラーになります。更に嫌なことですがlibのビルド後lib のソースを修正していてもlibはビルドされません。

  4. ソリューション・エクスプローラで libを指定してビルドして初めて lib がビルドされます。

上記の 3. の振る舞いは結構ハマる(ビルドしたつもりがビルドされていない)ので、他から依存されているターゲットは普段はビルド不要でもEXCLUDE_FROM_DEFAULT_BUILDを指定するのは避けた方が無難です。

3.以上のサンプルCMakeLists.txt

3-1.その前にちょっとだけ準備

  • includeコマンド
    定型的に記述するコードをサンプルからなるべく排除したいので、includeを使います。
    これはC/C++の#includeと同じような機能です。(モジュールを取り込む機能ということなのでもう少し高度なようですが、ここでは定形処理を記述したファイルを単純に取り込む目的で使います。)

  • cmake -Eコマンド
    今回のサンプルでは、CMake内蔵のcopyコマンドを使います。大抵のOSがcopyコマンドを持っていますが、微妙にコマンド名や構文が異なるのでマルチプラットフォームで記述するのが面倒な部分です。
    その辺を CMake 自身が提供しているので、これを使うと楽できます。
    cmake -Eコマンドです。CMakeの中で使う時は、`${CMAKE_COMMAND}でcmakeを呼び出せます。パスを切っていない時にも対応しているのだろうと思います。

3-2.サンプル・コード

cmake_minimum_required(VERSION 3.5.1)
project(sample)

include("${CMAKE_CURRENT_SOURCE_DIR}/base.cmake")

add_library(lib lib.cpp lib.h)
#add_library(lib EXCLUDE_FROM_ALL lib.cpp lib.h)
#set_target_properties(lib PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD TRUE)

add_executable(main main.cpp lib.h)
#add_executable(main EXCLUDE_FROM_ALL main.cpp lib.h)
#set_target_properties(main PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD TRUE)

target_link_libraries(main lib)     # mainにlibをリンクする指定

add_custom_target(copy ALL
    COMMAND echo ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/license.txt" "$<TARGET_FILE_DIR:main>"
    COMMAND      ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/license.txt" "$<TARGET_FILE_DIR:main>"
)

add_executable(always always.cpp)

add_executableとadd_libraryにEXCLUDE_FROM_ALLを指定してみたり、add_custom_targetからALLを外してみたり、EXCLUDE_FROM_DEFAULT_BUILD を有効にして Visual Studioで *.sln を読み込んで見たりしてみてください。

#include <string>

class Lib
{
    std::string mExeDir;
public:
    Lib(std::string const& iArgv0);
    void print_license();
};
#include <iostream>
#include <fstream>
#include <string>

#include "lib.h"

Lib::Lib(std::string const& iArgv0)
{
    mExeDir = ".";
    auto pos = iArgv0.find_last_of("/\\");
    if (pos != std::string::npos)
    {
        mExeDir = iArgv0.substr(0, pos);
    }
}

void Lib::print_license()
{
    std::ifstream ifs(mExeDir+"/license.txt");

    std::string buff;
    while(std::getline(ifs, buff))
    {
        std::cout << buff << std::endl;
    }
}
#include "lib.h"

int main(int argc, char const* argv[])
{
    (void)argc; // 警告抑止

    Lib aLib(argv[0]);
    aLib.print_license();
}
その他のファイル
if(MSVC)
    string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc")

    if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DBUILD_TYPE=\\\"Debug\\\"")
    else()
        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DBUILD_TYPE=\\\"Release\\\"")
    endif()
else()
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11")

    if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBUILD_TYPE=\\\"Debug\\\"")
    else()
        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBUILD_TYPE=\\\"BUILD_TYPE=Release\\\"")
    endif()
endif()
Boost Software License - Version 1.0 - August 17th, 2003

Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:

The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

これは、boostのライセンスです。ライセンス・ファイルのコピーは時には行うと思いますので。
(日本語を使うと、日本語対処が必要になるのでサンプルの焦点がぼけるため、英語の比較的使い勝手の良いライセンスを借用しました。)

#include <iostream>

int main()
{
    std::cout << "This is allways target!!\n";
}

4.まとめ

今回はちょっと短めです。法人化したのでそのあれやこれやの処理に苦労してて時間が余り取れていません。申し訳ないです。

実は、一昨日からペットのミリが寝込んでしまい病院で点滴を打ってもらってます。今日は少し回復しつつあるのでちょっとほっとしているところです。(いつもより誤字脱字が多いかも。)

次回は、もともと今回に含めたかった依存関係やadd_custom_targetと似ているadd_custom_command等について解説したいと思います。お楽しみに!