あけましておめでとうございます! 今年もよろしくお願い致します。

さて、CMakeについてもそろそろネタがなくなってきました。私自身が良く使っている機能について一通り解説が終わったという状況です。そこで、今回でCMake講座を終了とし、今までの「まとめ」的なサンプルと、もう少し残っている解説した方が良い項目について解説します。

1.お題

CMakeの機能を使って、次のようなプロジェクトを作ります。

  1. boostをダウンロードし、解凍する。
  2. そのboostをビルドする。(今回は静的リンク用)
  3. 上記処理をCMakeLists.tstから呼び出して、Makefile生成時に自動実行する。
  4. そのCMakeLists.txtは、上記boostのFormatを用いるプロジェクトとする。
  5. ビルド完了時にターゲットを自動実行する。

そこそこ複雑なプロジェクトなので、今まで解説してきたことをそれなりに使います。講座最後のまとめとして良いかなと思います。

2.boostを準備するCMakeスクリプト

上記のboostのダウンロード~ビルドまでの処理(boostを準備する処理)はCMakeスクリプトで処理します。「流れ作業」なので分離すると分かりやすく使いやすいと思います。

2-1. CMakeスクリプトへのパラメータの渡し方

CMakeLists.txtから呼び出すのでboostをダウンロードする先等をCMakeLists.txtから指定したいです。
CMakeスクリプトは、当講座の最初に解説したようにCMake -Pコマンドで呼び出します。その時は説明していませんが、パラメータも渡せます。

ビルド・ツールらしく、-D オプションでCMake変数を定義します。まるでC++コンパイラで -Dオプションを使ってマクロを定義するのと良く似ています。

message(STATUS "ARG0=${ARG0}")
 > cmake "-DARG0=Hello, world!" -P sample.cmake
-- ARG0=Hello, world!

注意事項
注意事項があります。-Dオプションは-Pオプションより前で指定して下さい。後ろで指定すると無視されるようです。コマンド・ラインで使う時はつい後ろに書きたくなるので、始めて使った時かなり悩みました。

2-2. フォルダ生成

boostの準備はビルド・フォルダにておこなうので、まず最初にダウンロード先のフォルダを生成します。
これは前回解説したfileコマンドのMAKE_DIRECTORYを使います。

file(MAKE_DIRECTORY "フォルダのフルパス")

2-3. ファイルのダウンロード

ダウンロード元のURLとダウンロード先ファイルのパスを指定してダウンロードします。

file(DOWNLOAD "URL" "ダウンロード先ファイル・パス" SHOW_PROGRESS)

SHOW_PROGRESSを指定するとダウンロードの進行状況が表示されます。ダウンロードに数分かかることがあるのでハングアップと勘違いしないよう指定することをお勧めします。

2-4. アーカイブ・ファイルの解凍

CMakeには tarコマンド(圧縮/解凍ツール)が内蔵されています。CMake -E コマンドで呼び出すことができます。
CMakeスクリプトから呼び出すのでexecute_process()コマンドで呼び出します。

execute_process(COMMAND
    ${CMAKE_COMMAND} -E tar xvf "圧縮ファイルのパス"
    WORKING_DIRECTORY "解凍先のフォルダ"
)

${CMAKE_COMMAND}は CMakeのフル・パスへ展開されます。(例えばWindowsの場合cmake.exeのフル・パス)
WORKING_DIRECTORYはコマンドを起動する時の作業フォルダを指定します。tarコマンドでは作業フォルダへ解凍します。

2-5. boostをビルドする

boostをビルドする時は、bootstrapスクリプトで b2 コマンドを生成し、b2コマンドで様々なパラメータを指定して必要なboostをビルドします。詳しくはタイトルのリンク先をご覧下さい。

if(WIN32)
    execute_process(COMMAND bootstrap.bat WORKING_DIRECTORY "boostのソース・フォルダ")
else()
    execute_process(COMMAND ./bootstrap.sh WORKING_DIRECTORY "boostのソース・フォルダ")
endif()
execute_process(
    COMMAND ./b2
        "--prefix=ビルドしたboostのインストール先フォルダのフル・パス"
        install
        variant=release
        link=static runtime-link=shared
        threading=multi
        -a
        -j 8
    WORKING_DIRECTORY "boostのソース・フォル"
)

ここでは静的リンク用(fPIC無し)で、リリース・ビルドのみ指定しています。
variantでdebugを指定するとデバッグ・ビルド用にビルドします。msvcでは「variant=release,debug」という指定が可能ですが、gccではどちらか一方のみ指定できます。(ビルド・システムの考え方の相違が原因と思います。)

2-6. ここまでのCMakeスクリプト・ファイル

message(STATUS "BOOST_VERSION=${BOOST_VERSION}")
message(STATUS "BOOST_PATH=${BOOST_PATH}")

#       ダウンロードと解凍

string(REPLACE "." "_" VERSION ${BOOST_VERSION})
set(FILE_NAME "boost_${VERSION}")
if(WIN32)
    set(FILE_NAME "${FILE_NAME}.7z")
else()
    set(FILE_NAME "${FILE_NAME}.tar.bz2")
endif()
message(STATUS "FILE_NAME=${FILE_NAME}")

set(URL "https://sourceforge.net/projects/boost/files/boost/${BOOST_VERSION}/${FILE_NAME}")
message(STATUS "URL=${URL}")

if(NOT IS_ABSOLUTE "${BOOST_PATH}")
    set(BOOST_PATH "${CMAKE_SOURCE_DIR}/${BOOST_PATH}")
endif()
set(FILE_PATH "${BOOST_PATH}/${FILE_NAME}")
message(STATUS "FILE_PATH=${FILE_PATH}")

if(NOT EXISTS ${FILE_PATH})
    # ダウンロード
    file(MAKE_DIRECTORY "${BOOST_PATH}")
    file(DOWNLOAD "${URL}" "${FILE_PATH}" SHOW_PROGRESS)

    # 解凍
    message(STATUS "extracting...")
    execute_process(
        COMMAND ${CMAKE_COMMAND} -E tar xvf "${FILE_PATH}"
        WORKING_DIRECTORY "${BOOST_PATH}"
    )
    message(STATUS "extracted.")
else()
    message(STATUS "${FILE_NAME} already downloaded.")
endif()

#       ビルド

set(BOOST_SOURCE "${BOOST_PATH}/boost_${VERSION}")
message(STATUS "BOOST_SOURCE=${BOOST_SOURCE}")

if(WIN32)
    set(VARIANT "release,debug")
else()
    set(VARIANT "release")
endif()

if(WIN32)
    if(NOT EXISTS "${BOOST_SOURCE}/b2.exe")
        message(STATUS "boost:bootstrap ...")
        execute_process(
            COMMAND bootstrap.bat
            WORKING_DIRECTORY "${BOOST_SOURCE}"
        )
    endif()
else()
    if(NOT EXISTS "${BOOST_SOURCE}/b2")
        message(STATUS "boost:bootstrap ...")
        execute_process(
            COMMAND ./bootstrap.sh
            WORKING_DIRECTORY "${BOOST_SOURCE}"
        )
    endif()
endif()

message(STATUS "boost:Build   ...")
if(NOT EXISTS "${BOOST_PATH}/include")
    execute_process(
        COMMAND ./b2
            "--prefix=${BOOST_PATH}"
            install
            variant=${VARIANT}
            link=static runtime-link=shared
            threading=multi
            -a
            -j 8
        WORKING_DIRECTORY "${BOOST_SOURCE}"
    )
endif()

3.CMakeLists.txtのサンプル

上記の boost_build.cmake をMakefile生成時に呼び出してboostの準備を整え、そのboostを用いるサンプルをビルドして実行するCMakeLists.txtです。
今回は以前ご紹介した boost::format を用いるサンプル・ソースを微修正したものを用います。

3-1. boostの準備

boostは、ビルド・フォルダに専用フォルダ(boost)を用意して、そこへダウンロード→解凍→ビルド→インストールして使います。

set(BOOST_PATH "${CMAKE_CURRENT_BINARY_DIR}/boost")
execute_process(
    COMMAND ${CMAKE_COMMAND}
        -DBOOST_VERSION=1.67.0
        -DBOOST_PATH=${BOOST_PATH}
        -P ${CMAKE_CURRENT_SOURCE_DIR}/boost_build.cmake
)

execute_process()はビルド時ではなくMakefile生成時に実行されることをお忘れなきよう。

3-2. boostのインクルード・パス等をCMake変数へ設定する

そのためのCMakeのコマンドfind_packageを使います。

set(BOOST_INCLUDEDIR "${BOOST_PATH}/include")
find_package(Boost)
message(STATUS "Boost_INCLUDE_DIRS   =${Boost_INCLUDE_DIRS}")
message(STATUS "Boost_LIBRARIES      =${Boost_LIBRARIES}")

実は、boost::formatはヘッダ・オンリーなのでライブラリを指定していないため、Boost_LIBRARIESは空です。

3-3. ターゲットのビルド指定

boost::formatのサンプル・ソースをビルドするための指定です。

if(MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc")
else()
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11")
endif()
include_directories("${Boost_INCLUDE_DIRS}")
add_executable(sample sample.cpp xprintf.cpp)
target_link_libraries(sample "${Boost_LIBRARIES}")

3-4. 最後にターゲットの実行コマンド(とgenerator-expressions)

これは今回始めて解説します。ターゲットをビルドした後、実行したいこともあります。Visual Studio で Go ボタンを押すようなイメージですね。

CMakeに含まれるテスト・ツールCTestを呼び出すなどいくつかの方法が考えられますが、ここではビルド後イベント(POST_BUILD)を用います。

CTestの問題点
CTestをexecute_processで呼び出す場合、日本語が文字化けするという問題がありました。既に治っている可能性もありますが、そもそも「CTestをexecute_processから呼び出す」という使い方を想定していない雰囲気でしたので修正されていない可能性もあります。

さて、CMake本家のマニュアルを見ていると、時々 「cmake-generator-expressionsを使用できます」との記述があることに気がついた方もいらっしゃると思います。
ざっくりいうと、Makefile生成モードで定義された値(主に各種パス)へ展開できる式です。つまり、スクリプト・モードでは使えません。
今回のまとめサンプルでは boost_build.cmakeというCMakeスクリプトと CMakeLists.txt を用いますが、前者では使えず、後者では使える式です。
これについてQiitaで解説されている方がいらっしゃいますので参考になると思います。

ここでは、$<TARGET_FILE:ターゲット名>を使います。これは指定したターゲット(add_executableやadd_library等で指定したもの)で最終生成ファイル(exeやdll等)へのフル・パスへ展開されます。

つまり、add_custom_command()のPOST_BUILDのCOMMANDへ指定することで、ターゲットのビルド完了後に自動的に呼び出されます。

add_custom_command(TARGET sample POST_BUILD COMMAND $<TARGET_FILE:sample>)

3-5. CMakeLists.txtサンプル全体

cmake_minimum_required(VERSION 3.10.2)
project(sample)

message(STATUS "----- CMakeLists.txt")
message(STATUS "CMAKE_CXX_COMPILER      =${CMAKE_CXX_COMPILER}")
message(STATUS "CMAKE_CXX_COMPILER_ID   =${CMAKE_CXX_COMPILER_ID}")
message(STATUS "CMAKE_GENERATOR_TOOLSET =${CMAKE_GENERATOR_TOOLSET}")
 
# boostの準備
set(BOOST_PATH "${CMAKE_CURRENT_BINARY_DIR}/boost")
set(Boost_USE_STATIC_LIBS    ON)
set(Boost_USE_MULTITHREADED  ON)
execute_process(
    COMMAND ${CMAKE_COMMAND}
        -DBOOST_VERSION=1.67.0
        -DBOOST_PATH=${BOOST_PATH}
        -P ${CMAKE_CURRENT_SOURCE_DIR}/boost_build.cmake
)

# boostのインクルード・パスとライブラリ設定
set(BOOST_INCLUDEDIR "${BOOST_PATH}/include")
find_package(Boost)
message(STATUS "Boost_INCLUDE_DIRS   =${Boost_INCLUDE_DIRS}")
message(STATUS "Boost_LIBRARIES      =${Boost_LIBRARIES}")

# ターゲットのビルド
if(MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc")
else()
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11")
endif()
include_directories("${Boost_INCLUDE_DIRS}")
add_executable(sample sample.cpp xprintf.cpp)
target_link_libraries(sample "${Boost_LIBRARIES}")

# 実行
add_custom_command(TARGET sample POST_BUILD COMMAND $<TARGET_FILE:sample>)

4.サンプル・ソースと実行結果

サンプル・ソースは冒頭にも書いたようにboostのFormatを用いたちょっとした便利ツールです。

サンプル・ソース群
#include <iostream>
#include "xprintf.h"

enum class Enum { Idle, Test };

class Ratio
{
    int mFraction;
    int mDenominator;
public:
    Ratio(int iFraction, int iDenominator) :
        mFraction(iFraction), mDenominator(iDenominator)
    { }
    friend std::ostream& operator<<(std::ostream& iOStream, Ratio const& iRatio)
    {
        iOStream << iRatio.mFraction << "/" << iRatio.mDenominator;
        return iOStream;
    }
};

int main()
{
    std::cout << xprintf("%10s = %10.2f\n", "double", 2345.678);

    Enum aEnum = Enum::Test;
    std::cout << xprintf("%10s = %10d\n", "aEnum", aEnum);

    Ratio   aRatio(16, 9);
    std::cout << xprintf("%10s = %10s\n", "aRatio", aRatio);

    std::string aString="test";
    std::cout << xprintf("%10s = %10s\n", "aString", aString);
}
#include <type_traits>
#include <sstream>

namespace boost
{
    template<class charT, class Traits, class Alloc> class basic_format;
    typedef basic_format<char, std::char_traits<char>, std::allocator<char>>  format;
}
namespace internal
{
    std::string getString(boost::format& iFormat);
    template<typename tType>
    boost::format& output(boost::format& iFormat, tType iParam);
    boost::format& makeFormat(std::string const& iFormat);

    // ユーザ定義型対応
    template<typename tType, typename tEnable=void>
    struct Out
    {
        static boost::format& put(boost::format& iFormat, tType& iParam)
        {
            std::stringstream ss;
            ss << iParam;
            return output(iFormat, ss.str().c_str());
        }
    };
    template<typename tType>
    struct Out<tType, typename std::enable_if<std::is_arithmetic<tType>::value>::type>
    {
        static boost::format& put(boost::format& iFormat, tType& iParam)
        {
            return output(iFormat, iParam);
        }
    };
    // scoped enum型にも対応
    template<typename tType>
    struct Out<tType, typename std::enable_if<std::is_enum<tType>::value>::type>
    {
        static boost::format& put(boost::format& iFormat, tType& iParam)
        {
            return output(iFormat, static_cast<typename std::underlying_type<tType>::type>(iParam));
        }
    };

    inline std::string xprintf_impl(boost::format& iFormat)
    {
        return getString(iFormat);
    }
    template<typename tFirst, typename... tParams>
    inline std::string xprintf_impl(boost::format& iFormat, tFirst iFirst, tParams... iParams)
    {
        return xprintf_impl(Out<tFirst>::put(iFormat, iFirst), iParams...);
    }
}
template<typename... tParams>
inline std::string xprintf(std::string const& iFormat, tParams... iParams)
{
    return internal::xprintf_impl(internal::makeFormat(iFormat), iParams...);
}
#include <boost/format.hpp>
#include "xprintf.h"

namespace internal
{
    std::string getString(boost::format& iFormat)
    {
        return iFormat.str();
    }

    template<typename tType>
    boost::format& output(boost::format& iFormat, tType iParam)
    {
        return iFormat % iParam;
    }

    boost::format& makeFormat(std::string const& iFormat)
    {
        static boost::format aFormat("");
        aFormat.parse(iFormat);
        return aFormat;
    }

    // 明示的実体化
    template boost::format& output(boost::format& iFormat, bool                 iParam);
    template boost::format& output(boost::format& iFormat, char                 iParam);

    template boost::format& output(boost::format& iFormat, signed char          iParam);
    template boost::format& output(boost::format& iFormat, short                iParam);
    template boost::format& output(boost::format& iFormat, int                  iParam);
    template boost::format& output(boost::format& iFormat, long                 iParam);
    template boost::format& output(boost::format& iFormat, long long            iParam);

    template boost::format& output(boost::format& iFormat, unsigned char        iParam);
    template boost::format& output(boost::format& iFormat, unsigned short       iParam);
    template boost::format& output(boost::format& iFormat, unsigned int         iParam);
    template boost::format& output(boost::format& iFormat, unsigned long long   iParam);

    template boost::format& output(boost::format& iFormat, float                iParam);
    template boost::format& output(boost::format& iFormat, double               iParam);
    template boost::format& output(boost::format& iFormat, long double          iParam);

    template boost::format& output(boost::format& iFormat, char const*          iParam);
}
      double =    2345.68
       aEnum =          1
      aRatio =       16/9
     aString =       test

「Windows 10 + Visual Studio 2017」と「ubuntu 18 + gcc 7.4.0」にて動作確認しています。

5.まとめのまとめ

概ね1ヶ月に1回更新し、約1年間CMakeの使い方について解説してきましたが、とうとうネタも付きてしまいましたので、CMake編はこれで終了とさせて頂きます。長い間お付き合いありがとうございました!!

さて、今後のC++講座についてはまだ決めていませんので、次回開催がいつになるか未定ですが、解説できるネタが十分にたまったらQtについて解説したいとつらつら考えているところです。C++は非常に強力なマルチプラットフォームな言語ですが、GUIについてはからきしです。そのC++の超巨大な弱点に光を当てる最も完成度の高いプロジェクトがQtと思います。
Qtを使えるC++プログラマーへの需要が徐々に高まりつつあるように思いますし、少し後になると思いますが、再開したいと考えています。

それまで皆さんもごきげんよう!!