こんにちは。田原です。

今年もいよいよ本格的な夏が来ましたね。いや、本当に暑いです。
さて、今回はお手軽にテキスト・ファイルを加工できるconfigure_fileコマンドについて説明します。バイナリのファイル名やバージョン名など複数のソースへ反映したい様々な設定を1箇所にまとめたい時などによく使っています。お手軽で便利ですよ。

1.概要

まず、このコマンドはテキスト・ファイルを読み込んで加工してテキスト・ファイルへ出力するものです。
どんな加工するのか?ですが、単純です。入力テキスト・ファイルにある文字列「${CMake変数名}」と「@CMake変数名@」をそのCMake変数の値で置換し、指定のテキスト・ファイルへ出力します。
もし、当該CMake変数が定義されてなかったら空文字列へ置換されます。

#define APP_NAME	"@APP_NAME@"
#define VERSION_NO	"${APP_VERSION}"
#define	DUMMY		"${DUMMY}"
set(APP_NAME    "SampleApplication")
set(APP_VERSION "1.0.0")
unset(DUMMY)

configure_file(sample.cpp.in sample1.cpp)

cmake -P sample.cmake を実行すると sample1.cpp が出力されます。

#define APP_NAME	"SampleApplication"
#define VERSION_NO	"1.0.0"
#define	DUMMY		""

また、configure_fileはスクリプト・モードのコマンドですのでMakefile生成モード、スクリプト・モードの両方で機能します。

2.詳細

このコマンドはパラメータは少ししかありません。
書式は、公式の解説に次のように書かれています。

configure_file(<input> <output>
               [COPYONLY] [ESCAPE_QUOTES] [@ONLY]
               [NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ])

<input> は入力側のテキスト・ファイルのパス名です。
<output> は出力側のテキスト・ファイルのパス名です。
この2つは省略できません。

2-1. @ONLY

configure_fileはデフォルトでは、「${CMake変数名}」と「@CMake変数名@」の両方の文字列を置換しますが、このオプションを指定すると読んで字の如く「@CMake変数名@」だけを置換します。

例えばbashスクリプトは ${変数名} という記述でもbash変数を参照します。ですので、bashスクリプトには「@{変数名}」のような記述が多数あり、configure_fileに入力すると置換されるので困ったことになります。そのような時 @ONLY オプションを指定して、「${CMake変数名}」を置換しないようにします。

2-2. NEWLINE_STYLE

NEWLINE_STYLEは、出力するテキスト・ファイルの改行コードを指定するものです。
NEWLINE_STYLEオプションを指定しなかった場合は、使っているOSの標準のコードとなります。
対応している改行コードは、LF(0A)、CRLF(0D0A)の2種類だけのようです。

NEWLINE_STYLE 改行コード
UNIX LF (0A)
LF LF (0A)
DOS CRLF (0D0A)
WIN32 CRLF (0D0A)
CRLF CRLF (0D0A)

MacのCRには対応していません。configure_fileは、Mac以外のOSでMac改行コードのテキスト・ファイルを出力できないということです。

2-3. COPYONLY

これは文字列の置換を一切行わず、入力ファイルを出力ファイルへ単純にコピーします。
ちょっと残念なことにCOPYONLYを指定した時、NEWLINE_STYLEは機能しません。つまり、改行コードだけを修正し、その他の文字列を間違って置換したくないような時にconfigure_fileは使えません。

2-4. ESCAPE_QUOTES

このオプションは分かりにくいです。公式の説明は次のように書かれています

ESCAPE_QUOTES
  Escape any substituted quotes with backslashes (C-style).
  バックスラッシュを使用して置換引用符をエスケープします(Cスタイル)。(Google翻訳)
  置換後のダブルコーテーションをバックスラッシュでエスケープします。(調査して意訳)

インターネットをあれこれ検索してみましたが、詳しい解説を見つけることができませんでした。そこで、あれこれやって振る舞いを調べてみました。
結論としては、CMake変数内に “(ダブルコーテーション)が有った場合、バックスラッシュでエスケープするというオプションのようです。

例えば、FOO変数に 「abc”def」 という文字列(ダブルコーテーションが真ん中に1つ)が設定されている時に、「${FOO}」は「abc\”def」へ置換されます。
因みにシングルコーテーションはエスケープされませんでした。

ESCAPE_QUOTES : ${FOO}
set(FOO abc\"def'ghi) # FOOの内容は「abc"def'ghi」

configure_file(escape_quotes.txt.in escape_quotes1.txt)
configure_file(escape_quotes.txt.in escape_quotes2.txt ESCAPE_QUOTES)

cmake -P escape_quotes.cmake を実行すると escape_quotes1.txt と escape_quotes2.txt が出力されます。

ESCAPE_QUOTES : abc"def'ghi
ESCAPE_QUOTES : abc\"def'ghi

ESCAPE_QUOTESを指定するとCMake変数ないの “(ダブルコーテーション)が \(バックスラッシュ=日本語フォントなら円マーク)でエスケープされています。

2-5. ありがたい追加仕様

他にありがたい仕様があります。configure_fileで生成された出力ファイルが前回のものと変化がなかった時、上書きしないようです。そのため出力ファイルの最終更新日時が変わりません。

通常のビルド・システムはソース・ファイルよりオブジェクト・ファイルの方が新しい時(ソース・ファイル更新後、コンパイルされていない時)だけコンパイルすることで、ビルド時間を短縮します。

そして、configure_fileで出力したテキスト・ファイルをコンパイルするような使い方をすることが多いです。つまり、変化がない時に最終更新日時が更新されなければ無駄なコンパイルが省略されます。
地味ですが、非常にありがたい仕様です。(これがなかったら、下手すると毎回フルコンパイルになります。)

3.サンプル

Makefile生成モードでの使用例を示します。

#include <iostream>

#define APP_NAME	"@APP_NAME@"
#define VERSION_NO	"${APP_VERSION}"
#define	DUMMY		"${DUMMY}"

int main()
{
    std::cout << APP_NAME " ver." VERSION_NO << "\n";
    std::cout << "@STRING@" << "\n";
}
cmake_minimum_required(VERSION 3.5.1)
project(sample)

if(MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc")
else()
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11")
endif()

set(APP_NAME    "SampleApplication")
set(APP_VERSION "1.0.0")
unset(DUMMY)

configure_file(sample.cpp.in sample1.cpp NEWLINE_STYLE LF)
configure_file(sample.cpp.in sample2.cpp @ONLY)

add_executable(sample1 sample1.cpp)
add_executable(sample2 sample2.cpp)

ubuntuの方は「NEWLINE_STYLE CRLF」を指定してみて下さい。

> mkdir build
> cd build
> cmake ..
> cmake --build . --config Release
#define APP_NAME	"SampleApplication"
#define VERSION_NO	"1.0.0"
#define	DUMMY		""

Make生成モードの時、configure_file()はビルド・フォルダ(上記の場合は build )へテキスト・ファイルを出力します。

#include <iostream>

#define APP_NAME    "SampleApplication"
#define VERSION_NO  "1.0.0"
#define DUMMY       ""

int main()
{
    std::cout << APP_NAME " ver." VERSION_NO << "\n";
    std::cout << "" << "\n";
}

ここでは分かりませんが、sample1.cppの改行コードは、LF になっています。最近のWindows 10のメモ帳は改行コードがLFならばその旨を表示してくれますので、メモ帳で見ることもできます。

#include <iostream>

#define APP_NAME    "SampleApplication"
#define VERSION_NO  "${APP_VERSION}"
#define DUMMY       "${DUMMY}"

int main()
{
    std::cout << APP_NAME " ver." VERSION_NO << "\n";
    std::cout << "" << "\n";
}

4.まとめ

configure_fileは、以上のような簡単な機能を提供するだけのコマンドですが、これが実に重宝します。
例えば、バージョン番号はアプリ本体のexeとインストーラの両方に同じものを反映したいです。インストーラ開発がC++の「#define」や「static const char* kVersion=”xxxx”;」などを解釈できることはまず無いので、異なる書式で指定する必要があります。
そのような時、CMakeLists.txt等でVERSION_NO変数を設定しておき、それからC++ヘッダとインストーラ開発ツールの設定ファイルを生成することで反映漏れが失くなります。
なかなかありがたいコマンドなのです。

さて、昔は気温が30度を超える日なんてほとんどなかったと思うのですが、最近は30度どころか体温を超える気温が日常茶飯事です。みなさんも熱中症には気をつけて、この暑い夏を乗り切って下さい。
それでは今回はここで終わります。お疲れ様でした!