🔺

CMake事始め

2023/05/09に公開
2

はじめに

ビルドは自動化したいものです.
ではCMakeを使いましょう.

前提

環境

$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0

$ cmake --version
cmake version 3.22.1

知識

  • コンパイル によってソースファイルからオブジェクトファイルを生成する
  • リンク によってオブジェクトファイル・静的ライブラリから実行ファイルを生成する
  • コンパイル・リンクによってソースファイルから実行ファイルを生成することを ビルド と言う

本記事の流れ

本記事はいくつかのステップにより,段階的に CMakeLists.txt のコマンドについて解説しています.
各ステップ (Step0 を除く) はサンプルプログラムをビルドするための

  1. コマンド
  2. CMakeLists.txt

を順に解説しており,コマンドと CMakeLists.txt とを対応付けて理解できるようになっています.

サンプルプログラムは各ステップの冒頭に折り畳んで示しています.

Step0

まず CMake を利用する動機について解説します.

ツールを利用するとき「何故そのツールを利用するのか」を意識することは重要です.

Step0 は本記事の趣旨からやや外れるため,読み飛ばせるよう折り畳んでいます.
読み飛ばして頂いても問題ありませんが,後のステップで登場する用語・操作の解説を含んでいるため,ご一読ください.

CMake を利用する動機

main.cpp をビルドするとき,次のようにします.

g++ main.cpp

簡単ですね.

しかし多くの場合は次のようにするでしょう.

g++ -Wall -Wextra -Werror -c main.cpp
g++ -o main main.o

もうコマンドを入力したくなくなってきました.

では次のような構成の main.hpp, main.cpp, foo.cpp, bar.cpp から実行ファイル main をビルドするなら?

.
├── main.hpp
├── main.cpp
├── foo.cpp
└── bar.cpp

考えたくもありません.

g++ -o main main.cpp foo.cpp bar.cpp でできますが,ここではコンパイルとリンクとを別で実行することにします.

ではどうすれば良いでしょうか.
まず思いつくのは「シェルスクリプトを書く」でしょう.
コンパイル・リンクに必要なコマンドを纏めたシェルスクリプトを作成すればビルドを効率化できます.

シェルスクリプト

次の build.bash は,前述の main.hpp, main.cpp, foo.cpp, bar.cpp をビルドするシェルスクリプトの一例です.

build.bash
#!/bin/bash

srcs=("main.cpp" "foo.cpp" "bar.cpp")

rm -f main *.o *.d

g++ -g -Wall -Wextra -Werror -c ${srcs[@]}
g++ -o main ${srcs[@]/%.cpp/.o}

このようなシェルスクリプトを作成しておくと

./build.bash

のみでビルドを実行できます.

しかし,このシェルスクリプトには次のような問題があります.

  • bash 以外のユーザを考慮していない
    共同開発者が bash と互換性のないシェルのユーザであれば利用できません.
  • ワーキングディレクトリを考慮していない
    プロジェクトのルートディレクトリ以外から実行できるため,予期せぬ問題が発生するかもしれません.
  • リビルドの実行時間を考慮していない
    例えば foo.cpp のみを変更した場合でも,全てのソースファイルをリコンパイルします.大きなプロジェクトではコンパイルに数時間を要することがあり,変更のないソースファイルをリコンパイルするのは非効率的です.

これらの問題を解決するビルドシステムに make があります.

make

make は広く利用されているビルドシステムで,Makefile に記述したビルドルールに従ってビルドを実行します.
次の Makefile は,make によって前述の main.hpp, main.cpp, foo.cpp, bar.cpp をビルドするための Makefile の一例です.

CXX		:= g++
CXXFLAGS:= -g -Wall -Wextra -Werror

TARGET	:= main
SRCS	:= main.cpp foo.cpp bar.cpp
OBJS	:= $(SRCS:.cpp=.o)

INCDIR	:= 
LIBDIR	:= 
LIBS	:= 

.PHONY: all
all: clean $(TARGET)

$(TARGET): $(OBJS)
	$(CXX) -o $@ $^ $(LIBDIR) $(LIBS)

$(OBJS): $(SRCS)
	$(CXX) $(CXXFLAGS) $(INCDIR) -c $^

.PHONY: clean
clean:
	rm -f $(TARGET) *.o *.d

Makefile を記述しておくと

make

のみでビルドを実行できます.

make によるビルドは Makefile を配置したディレクトリ以外から実行できず,またリビルドで変更のないソースファイルをコンパイルし直すこともありません.

make はファイルの更新の有無をタイムスタンプから確認しています.

しかし,この Makefile にも次のような問題があります.

  • プラットフォームに依存する
    make と同様に Makefile に従ってビルドを実行するビルドシステムに NMAKE があり,Unix 環境では一般に make が利用されますが,Windows 環境では NMAKE が利用されることがあります.両者の Makefile の文法には互換性がなく,共同開発者が互換性のないプラットフォームのユーザであれば利用できません.
  • 文法が独特
    自由度は高いですが,可読性は高くありません.

NMAKE では nmake でビルドを実行します.

これらの問題を解決するのが CMake です.

CMake

CMake はビルドシステムのジェネレータで,CMakeLists.txt に記述した内容に従って Makefile を生成します.
このとき CMake はプラットフォームに応じた Makefile を生成するため,CMake を利用するとクロスプラットフォームでのビルドが容易になります.

CMake はプラットフォームに応じて異なるビルドシステムを利用するため「プラットフォームが利用するビルドシステムにおいて Makefile に相当するファイルを生成する」と表現する方が適切なのですが,簡単のため「Makefile を生成する」と表現しています.

次の CMakeLists.txt は,CMake によって前述の main.hpp, main.cpp, foo.cpp, bar.cpp をビルドするための Makefile を生成する CMakeLists.txt の一例です.

CMakeListe.txt
cmake_minimum_required(VERSION 3.22)

project(main
	VERSION 1.0
	LANGUAGES CXX
)

add_executable(${PROJECT_NAME}
	main.cpp
	foo.cpp
	bar.cpp
)

CMakeLists.txt を記述して

cmake -S . -B build

を一度実行しておくと

cmake --build build

のみでビルドを実行できます.

cmake --build build は内部で makeを実行し,事前に生成された Makefile に従って実行ファイルを出力します.

ここでも「各プラットフォームが利用するビルドシステムにおいて make に相当するコマンドを実行する」と表現する方が適切なのですが,やはり簡単のため「make を実行する」と表現しています.

CMakeLists.txt は Makefile と同様に独自の文法を持っていますが,Makefile と比較して可読性が高く,新たに習得するのであれば Makefile より CMakeLists.txt の文法の方が容易でしょう.

Step1

Step1 では次のような構成を考えます.

.
├── main.hpp
├── main.cpp
├── foo.cpp
└── bar.cpp
サンプルプログラム
main.hpp
#pragma once

auto foo() -> void;
auto bar() -> void;
main.cpp
#include "main.hpp"

auto main() -> int {
	foo();
	bar();
}
foo.cpp
#include "main.hpp"

#include <iostream>

auto foo() -> void {
	std::cout << "foo" << std::endl;
}
bar.cpp
#include "main.hpp"

#include <iostream>

auto bar() -> void {
	std::cout << "bar" << std::endl;
}
  • command ver.

    次のコマンドを実行します.

    # main.cpp (ソースファイル) から main.o (オブジェクトファイル) を生成
    g++ -c main.cpp foo.cpp bar.cpp
    # main.o (オブジェクトファイル) から main (実行ファイル) を生成
    g++ -o main main.o foo.cpp bar.cpp
    

    これで実行ファイル main が生成されます.

  • CMake ver.
    次の位置に CMake Lists.txt を作成します.

    .
    ├── main.hpp
    ├── main.cpp
    ├── foo.cpp
    ├── bar.cpp
    └── CMakeLists.txt
    

    CMakeLists.txt を次のように記述します.

    CMakeListe.txt
    # 各行のシャープ記号より後はコメントと認識されます
    cmake_minimum_required(VERSION 3.22)
    
    project(main
    	VERSION 1.0
    	LANGUAGES CXX
    )
    
    add_executable(main
    	main.cpp
    	foo.cpp
    	bar.cpp
    )
    

    この後に . で次のコマンドを実行します.

    cmake -S . -B build
    cmake --build build
    

    これで ./build に実行ファイル main が生成されます.

    後述しますが,cmake -S . -B build を毎回実行する必要はなく

    cmake --build build
    

    のみでリビルドを実行できます.

    以降のステップではCMakeLists.txt のみを掲載しますが,ビルド手順は同じです.

in-source ビルド・out-of-source ビルド

cmake -S . -B build

はソースディレクトリを . に,ビルドディレクトリを build にそれぞれ設定し,CMakeLists.txt に記述した内容に従ってMakefile 等のビルドに必要なファイルを build ディレクトリに出力します.build ディレクトリが存在しなければ同時に作成します.
このようにビルドディレクトリがソースディレクトリと異なるビルドを out-of-source ビルド と言います.

ソースファイルの存在するディレクトリをソースディレクトリ,cmake によって生成される Makefile 等のファイルやビルドによって生成される実行ファイルが出力されるディレクトリをビルドディレクトリと言います.

代えて

cmake .

はソースディレクトリ・ビルドディレクトリを共に . に設定します.
このようにビルドディレクトリがソースディレクトリと同じであるビルドを in-souce ビルド と言います.

in-source ビルドと out-of-source ビルドとでは out-of-source ビルドをすべき です.
in-source ビルドではソースファイルと生成ファイルとが単一のディレクトリに混在するため,ファイル管理が困難です.
対して out-of-source ビルドは rm -r build のみで全ての生成ファイルを削除できるため,クリーンビルドが容易です.

これらのコマンドは build ディレクトリが作成されていない場合や CMakeLists.txt を更新した場合に実行します.


このステップで登場したコマンドは次の3つです.

cmake_minimum_required()

次のように最低要件を設定します.

cmake_minimum_required(
	VERSION 3.22 # バージョン
)

バージョンは必須の項目で,設定したバージョンより古い CMake を利用した場合にエラーを発生させます.

cmake_minimum_required() はトップレベルのCMakeLists.txt の先頭に記述します.

「トップレベルの」は「ソースディレクトリの」と同義です.
敢えてこのような表現をするのは,ソースディレクトリより下の階層に別の CMakeLists.txt を作成する場合があるためです.

project()

次のようにプロジェクトの情報を設定します.

project(cmake_tutorial # プロジェクト名
	VERSION 1.0 # バージョン
	DESCRIPTION "This project is a tutorial on CMake" # 説明文
	HOMEPAGE_URL "example.com" # ホームページURL
	LANGUAGES C CXX # 使用言語
)

プロジェクト名は必須の項目です.また多くの場合,バージョン・使用言語の項目を設定しておく必要があります.

使用言語は

  • C なら C
  • C++ なら CXX

のように設定します.

C CXX はデフォルトで設定されています.

project() はトップレベルの CMakeLists.txt の cmake_minimum_required() に続けて記述します.

add_executable()

executable は実行ファイルの意で,次のように生成する実行ファイルについての情報をビルドルールに追加します.

add_executable(main # 実行ファイル
	main.cpp # ソースファイル1
	foo.cpp # ソースファイル2
	bar.cpp # ソースファイル3
)

これは

  • 実行ファイル main を生成すること
  • ソースファイルが main.cpp, foo.cpp, bar.cpp であること

を設定しており,コマンドの

g++ -c main.cpp foo.cpp bar.cpp
g++ -o main main.o foo.cpp bar.cpp

の部分に相当します.

変数

変数について簡単に解説します.
CMakeLists.txt に登場するコマンドの多くは内部で変数の値を設定しており,例えば

  • cmake_minimum_required(VERSION 3.22)
    • CMAKE_MINIMUM_REQUIRED_VERSION という変数の値を 3.22
  • project(cmake_tutorial VERSION 1.0)
    • PROJECT_NAME という変数の値を cmake_tutorial
    • PROJECT_VERSION および cmake_tutorial_VERSION という変数の値を 1.0

それぞれ設定しています.

各変数の値は次のように ${変数名} で取り出すことができます.

message("${CMAKE_MINIMUM_REQUIRED_VERSION}")
message("${PROJECT_NAME}")
message("${PROJECT_VERSIOIN}")
message("${${PROJECT_NAME}_VERSIOIN}")

message() は CMake における ログ出力のようなもので,message("Hello, world!") とすると実行ログに Hello, world! を出力します

これにより,生成する実行ファイル名がプロジェクト名と同じである場合,前述の add_executable() を次のように記述できます.

add_executable(${PROJECT_NAME}
	main.cpp
	foo.cpp
	bar.cpp
)

このステップで解説したコマンドの詳細は次のリンクから確認できます.

リンク先の内容はそれぞれ最新バージョンの CMake に準拠しているため,注意してください.

Step2

GitHubで編集を提案

Discussion

dspusrdspusr

後述しますが,cmake -S . -B build を毎回実行する必要はなく
cmake -S . -B build
のみでリビルドを実行できます

cmake -S . -B build を毎回実行する必要はなく
cmake --build build
のみでリビルドを実行できます

が正解ですか?

蒼百合蒼百合

仰る通りです.
ご指摘ありがとうございます🙏
修正しました.