cmakeの使い方

この文章はGitLab上で開発されており継続的に更新されていきます。

Contribution

この本は下記のようにGNU フリー文書利用許諾契約書の元で配布されています。 タイポの指摘や内容に関する不明瞭な点の指摘などがあればGitLab/issueに報告してください。

License

Copyright (C) 2020 Toshiki Teramura

この文書を、フリーソフトウェア財団発行の GNU フリー文書利用許諾契約書(バージョン1.2かそれ以降から一つを選択)が定める条件の下で複製、頒布、あるいは改変することを許可する。変更不可部分、表カバーテキスト、裏カバーテキストは存在しない。

cmake

cmakeはC++やFortranのプログラムでよく使用されているビルドツールの一つです。

cmakeで出来ること

  • プログラムのビルドを行うためのスクリプトの生成
    • cmake自体はプログラムのビルドを行うツール ではなく makeやVisual Studioのように実際にビルドコマンドを実行するプログラムの為のスクリプトを生成するためのツールです
  • システムにインストールされているライブラリを検索し、コンパイルオプションを生成する
    • 必要なライブラリをcmakeがインストールする事は出来ません

cmakeで出来ない事

  • cmakeはPythonやNode.jsにおけるpipnpm, yarnに対応するものではありません。
  • そもそもC++やFortranには配布用のパッケージ形式は無く、PyPIのようなレジストリもありません。
  • cmakeは必要なライブラリをインストールする事は出来ません。インストールされているライブラリを探してくるだけです。
  • ライブラリを探してくる方法は基本的にcmake本体に含まれるスクリプトにハードコードされています。

cmakeを使うべきか?

新しくC++のプロジェクトを始める

新規のC++プロジェクトにcmakeを使うのは良い考えですが、そもそも新たにC++プロジェクトを始めるのは良い考えではありません。 より環境の整った言語は多くあり、C++を使う事でこの本にあるような退屈な作業に多くの時間を取られることになります。

Autotoolsプロジェクト

Autotoolsはcmakeと同様にMakefileを生成するためのツールです。 既にAutotoolsで管理されているプロジェクトをcmakeに移行する場合、コストに見合う利益は得られない事が多いでしょう。

独自Makefile

多くのレガシーなプロジェクトでMakefileに足りない機構を実現するために独自の拡張が行われておりメンテナンス性を著しく下げています。 これらをcmakeに置き換える作業は非常に有益です。

Links

はじめてのcmake

ここではcmakeを使い始めるために小さいプロジェクトをビルドしてみましょう。 まず単一のC++ソースコードからなるプロジェクトを考えます:

${PROJECT_HOME}/
  main.cpp

この時は以下のように設定を記述します

cmake_minimum_required(VERSION 3.0)  # cmakeの最小バージョン
project("HelloCMake")                # プロジェクトの名前
add_executable(Main main.cpp)        # 実行ファイルを追加する

これをCMakeLists.txtとしてmain.cppと同じディレクトリにおきます。 このファイル名は特別で CMakeLists.txt な事に注意してください

${PROJECT_HOME}/
  main.cpp
  CMakeLists.txt

これで準備が出来ました。同じディレクトリでを次のコマンドを実行すれば実行ファイルMainが作成されます

cmake . # .を忘れずに
make

複数のソースとヘッダの場合

${PROJECT_HOME}/
  main.cpp
  mod.hpp
  mod_func1.cpp
  mod_func2.cpp

この場合も同じディレクトリに以下のCMakeLists.txtを追加する。

cmake_minimum_required(VERSION 3.0)
project("HelloCMake")
add_executable(Main
  main.cpp
  mod_func1.cpp
  mod_func2.cpp
)

add_executableに引数を追加しました。cmakeのスクリプトではスペースが引数の区切り文字になります。 この時ヘッダーファイル mod.hpp を追加する必要はありません。 ソースファイルが依存しているヘッダファイルはcmakeが自動的に検出して適切に依存関係を構築してくれます。

コンパイルフラグを指定する

常にコンパイル必要な場合はadd_definitionsを使う

cmake_minimum_required(VERSION 3.0)
project("HelloCMake")
add_executable(Main
  main.cpp
  mod_func1.cpp
  mod_func2.cpp
)

Links

複数のディレクトリを管理する

ディレクトリが分かれている場合

${PROJECT_HOME}/
  main.cpp
  mod1.hpp
  mod1/
    func1.cpp
    func2.cpp
  mod2.hpp
  mod2/
    func1.cpp
    func2.cpp

このくらいになってくるとcmakeによる管理の効果がでてくる。 この場合、

  • 単一のCMakeLists.txtを使う方法
  • 各ディレクトリにCMakeLists.txtを作る方法

がある。個人的には後者がおすすめである。

単一のCMakeLists.txtを用いる

${PROJECT_HOME}/
  CMakeLists.txt <- new
  main.cpp
  mod1.hpp
  mod1/
    func1.cpp
    func2.cpp
  mod2.hpp
  mod2/
    func1.cpp
    func2.cpp

上述のケースと同様にプロジェクトのトップにCMakeLists.txtを作り、

cmake_minimum_required(VERSION 2.8)
add_executable(Main
  main.cpp
  mod1/func1.cpp
  mod1/func2.cpp
  mod2/func1.cpp
  mod2/func2.cpp
)

のようにする。上述のケースと本質的に同じである。

各ディレクトリにCMakeLists.txtを作る

${PROJECT_HOME}/
  CMakeLists.txt <- new
  main.cpp
  mod1.hpp
  mod1/
    CMakeLists.txt <- new
    func1.cpp
    func2.cpp
  mod2.hpp
  mod2/
    CMakeLists.txt <- new
    func1.cpp
    func2.cpp
cmake_minimum_required(VERSION 2.8)
add_subdirectory(mod1)                # <- new
add_subdirectory(mod2)                # <- new
add_executable(Main main.cpp)
target_link_libraries(Main Mod1 Mod2) # <- new
cmake_minimum_required(VERSION 2.8)
add_library(Mod1 STATIC
  func1.cpp
  func2.cpp
)
cmake_minimum_required(VERSION 2.8)
add_library(Mod2 STATIC
  func1.cpp
  func2.cpp
)

のようにする。少し手間かもしれないが、この方がモジュール化が分りやすいし増えてくると管理が楽。 cmakeでは静的ライブラリが簡単に作れ、さらにリンクも簡単。

add_library

STATICを付けると静的ライブラリを作る。 上の例だとUnix上ではmod1/libMod1.a,mod2/libMod2.aを作る。

target_link_libraries

ライブラリを実行ファイルにリンクする。 具体的にはフラグに-lMod1 -lMod2が追加される形。 名前の解決はcmakeが行い、cmakeは自分で作ったライブラリの名前は (ディレクトリ関係なく)グローバルに保持するので、 ここでmod1/Mod1のようにする必要はない。

add_subdirectory

ディレクトリをcmakeの管理に追加する。 そのディレクトリにCMakeLists.txtが存在しないとエラーになる。 add_definitionssetによる変数の定義は子ディレクトリには伝わりますが、 親ディレクトリには伝わらない。

Out-of-Source Build

ここまで簡単のために

cmake .
make

のようにビルドして来ましたがこれについて少し詳しく見ていきましょう。 cmakeコマンドは引数にディレクトリを取ります。 まずシェル上では.は現在のディレクトリ $PWD を指すので、 上のコマンドはcmakeに現在のディレクトリにあるCMakeLists.txtを探しに行けという命令をしていることに成ります。

さて cmake . を実行すると次のようなファイルが$PWDに生成されているはずです

CMakeFiles/
Makefile
cmake_install.cmake
CMakeCache.txt

これらはcmakeが生成したビルド用の設定ファイル群で、ユーザーは中身を見る必要の無いものです。 これらをどこに出力するかはcmakeの-B, --buildオプションで指定でき、デフォルトでcmakeコマンドを実行したディレクトリになります。

cmake . -B build

このようにCMakeLists.txtを探すパス.と設定を出力するパスbuildを指定することが出来ます。 それぞれcmakeのスクリプト中では${CMAKE_SOURCE_DIR}, ${CMAKE_BINARY_DIR} として参照できます

Links

シェルスクリプトの実行

他のファイルを生成しない場合

ctagsやdoxygenのように、他のソースのコンパイルに関係ない物を生成するためには add_custom_targetを使います:

add_custom_target(ctags ALL COMMAND "ctags" "-R" "-f" ".tags" "--languages=C++,python" "--exclude='CMake*'")
add_custom_target(document COMMAND "doxygen" "doc/Doxyfile")

COMMAND以下に実行したいシェルスクリプトを記述します。 ALLを付けると毎回のmakeで自動的に実行されます。

ファイルを生成する場合

例えばプロジェクトKSEで使用するProtocol buffersのコードを生成したい場合、 KSE.pb.ccKSE.pb.hが生成され、それをコンパイルする必要があります。 この場合はadd_custom_commandを使用します:

add_custom_command(
  OUTPUT KSE.pb.cc KSE.pb.h
  DEPENDS KSE.proto
  COMMAND "protoc" "KSE.proto" "--cpp_out=." "--python_out=."
)

これによりターゲットにKSE.pb.cc, KSE.pb.hが追加され

make KSE.pb.cc

KSE.pb.ccが生成されます。 もちろん依存関係は自動で解決されますので、

add_library(KSE STATIC
  logger.cpp
  KSE.pb.cc
)

のようにある場合は勝手にKSE.pb.ccを生成してくれます。

find_packageの動作を理解する

https://qiita.com/osamu0329/items/bd3d1e50edf37c277fa9 これが詳しい

オプション付きの関数を定義する

cmakeの組み込み関数にはオプション引数をとれるものがありますね。例えばinstall

install(TARGETS targets... [EXPORT <export-name>]
        [[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|
          PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE]
         [DESTINATION <dir>]
         [PERMISSIONS permissions...]
         [CONFIGURATIONS [Debug|Release|...]]
         [COMPONENT <component>]
         [NAMELINK_COMPONENT <component>]
         [OPTIONAL] [EXCLUDE_FROM_ALL]
         [NAMELINK_ONLY|NAMELINK_SKIP]
        ] [...]
        [INCLUDES DESTINATION [<dir> ...]]
        )

のようにたくさんのオプションを持ちます。これを自分で実装する話です。

cmakeの関数の引数

まずfunctionを使って関数を定義してみましょう:

function(myfunc arg)
  message("ARG1 = ${arg}")
endfunction()

myfunc("a1")
$ cmake .
ARG1 = a1
...

名前を付けた引数は${arg}のように参照できますね。実はcmakeは名前を付けなかった引数はすべて${ARGN}に入れます

function(myfunc arg)
  message("ARG1 = ${arg}")
  message("ARGN = ${ARGN}")
endfunction()

myfunc("a1" "a2" 3)

${ARGN}はリスト型になるので、表示するときは各成分が;でつながれた形になります

$ cmake .
ARG1 = a1
ARGN = a2;3
...

cmake_parse_arguments

この${ARGN}は手動でパースしないといけないのでしょうか?ここで登場するのがcmake_parse_argumentsです。 この関数はちょっとややこしい引数をとるのでドキュメントを読んでも分かりにくいです。サンプルをいくつか試してみましょう。

function(myfunc arg)
  cmake_parse_arguments(MYFUNC "" "OUTPUT" "" ${ARGN})
  message("ARG1 = ${arg}")
  message("ARGN = ${ARGN}")
  message("OUTPUT = ${MYFUNC_OUTPUT}")
endfunction()

myfunc("a1" "a2" 3 OUTPUT "/path/to/output")

これでOUTPUTというオプションを持たせることが出来ます。

$ cmake .
ARG1 = a1
ARGN = a2;3;OUTPUT;/path/to/output
OUTPUT = /path/to/output
...

OUTPUTは値を一つとるオプションです。関数に渡したオプションの値は${MYFUNC_OUTPUT}として参照できます。このMYFUNCはcmake_parse_argumentsの最初の引数で上げた文字列で、cmakeには名前空間が無いので関数名を付けて名前の衝突を回避します。

cmake_parse_argumentsの第2,3,4引数はそれぞれbool値のみを保持するフラグオプション、値を一つとるオプション、値を複数とるオプションです。

function(myfunc arg)
  cmake_parse_arguments(MYFUNC "" "OUTPUT" "SOURCES" ${ARGN})
  message("ARG1 = ${arg}")
  message("ARGN = ${ARGN}")
  message("OUTPUT = ${MYFUNC_OUTPUT}")
  message("SOURCES = ${MYFUNC_SOURCES}")
endfunction()

myfunc("a1" "a2" 3
  OUTPUT "/path/to/output"
  SOURCES "vim" "emacs"
  )
$ cmake .
ARG1 = a1
ARGN = a2;3;OUTPUT;/path/to/output;SOURCES;vim;emacs
OUTPUT = /path/to/output
SOURCES = vim;emacs
...

それぞれの引数にリストを上げると複数のオプションが定義できます

function(myfunc arg)
  cmake_parse_arguments(MYFUNC "" "OUTPUT" "SOURCES;DEPENDENCIES" ${ARGN})
  message("ARG1 = ${arg}")
  message("ARGN = ${ARGN}")
  message("OUTPUT = ${MYFUNC_OUTPUT}")
  message("SOURCES = ${MYFUNC_SOURCES}")
  message("DEPENDENCIES = ${MYFUNC_DEPENDENCIES}")
endfunction()

myfunc("a1" "a2" 3
  OUTPUT "/path/to/output"
  SOURCES "vim" "emacs"
  DEPENDENCIES "glibc" "linux"
  )
$ cmake .
ARG1 = a1
ARGN = a2;3;OUTPUT;/path/to/output;SOURCES;vim;emacs;DEPENDENCIES;glibc;linux
OUTPUT = /path/to/output
SOURCES = vim;emacs
DEPENDENCIES = glibc;linux
...

ccmake, cmake-gui

コンパイル毎に変えたい場合はccmakeコマンドあるいはcmake-guiコマンドを使う

ccmake .
cmake-gui .

cmake本体とは別パッケージになっている事が多いのでapt,yum,pacman等で検索する。 ccmakeを実行すると CMAKE_BUILD_TYPE CMAKE_INSTALL_PREFIX の2つだけ表示される。変更したいのは CMAKE_CXX_FLAGSであって、これはtを入力すると出現する。 ここで必要なフラグを調整する。

Git submodule

複数のプロジェクトで共通して必要なプロジェクトや、外部のライブラリを使用したいが、システムにインストールしたく無い場合の方法として、 Git Submoduleの機能を使う方法があります。

前提条件

C++プロジェクトprojに外部のC++プロジェクトsubを使用する場合を例に取って考えます。 上述のように以下の方法を実行するには

  • projがcmakeで管理されている事
  • proj, subがgitで管理されている事
  • subがcmakeで管理されているか、ヘッダのみで構成されているテンプレートライブラリである事

が必要です。

手順

まずgitのリポジトリは

  • yourhost:repos/proj.git
  • anotherhost:repos/sub.git

のようになっているとします。各自の環境に合わせて読み替えてください。

アイデアは以下の通りです。

  • git submoduleを使用すれば外部のプロジェクトを取得できる
  • cmakeのinclude_directoriesマクロを使用すればgit submoduleで取得したライブラリを簡単にincludeできる
  • 外部プロジェクトがヘッダだけなら、それらをコンパイルする必要がないからincludeできれば十分
  • 外部プロジェクトがcmakeで管理されていればそのツリーを親プロジェクトのツリーに取り込める

よって手順としては、

  1. projにおいてgit submoduleによりsubを取得する
  2. projのCMakeLists.txtinclude_directoriesを記述する
  3. subがcmake管理の場合はadd_subdirectoryマクロで取り込む

のみです。

proj/CMakeLists.txt
     mod1/CMakeLists.txt
          func1.cpp
          func2.cpp
     main.cpp
     sub/
sub/mod1/func1.hpp
         func2.hpp
    mod2/func1.hpp

のような場合

...
include_directories(.)
...

としておけば例えばmod1/func1.cppにおいてincludeパスを指定する事なく:

add_library(mod1 STATIC
  func1.cpp
  func2.cpp
)
# -Iは特に指定しなくていい
#include "sub/mod2/func1.hpp"
...

のようにincludeする事が可能となります。

ctest

はじめてのctest

cpack

はじめてのcpack

scikit-build

scikit-buildというPythonの拡張モジュールをビルドするための補助ツールがあります。

Improved build system generator for CPython C, C++, Cython and Fortran extensions http://scikit-build.org

これを使ってcmakeでビルドされたプロジェクトをPythonの拡張モジュールとして配布する方法についてまとめます。まとめたものは以下にあります:

https://gitlab.com/termoshtt/skbuild-example

全体のディレクトリ構成は以下の通りです:

├── CMakeLists.txt
├── hello
│   ├── CMakeLists.txt
│   ├── _hello.cpp      # これを拡張モジュールにコンパイル
│   ├── __init__.py
│   └── __main__.py     # python -m hello で呼ばれたときに実行されるファイル
├── LICENSE
├── pyproject.toml
├── README.md
└── setup.py    

setup.py

まずは通常のPythonのパッケージと同様にビルド用のスクリプトしてsetup.pyを用意しますが、setuptoolsではなく、skbuildsetupを使います:

from skbuild import setup

setup(
    name="hello",
    version="1.2.3",
    description="a minimal example package",
    author="Toshiki Teramura <toshiki.teramura@gmail.com>",
    license="MIT",
    packages=["hello"],
)

pyproject.toml

依存関係は requirements.txt ではなく公式のドキュメントに従って pyproject.toml に記述します

[build-system]
requires = ["setuptools", "wheel", "scikit-build", "cmake", "ninja"]

最近はpyproject.tomlの制定によってsetup.pyを書かなくてもpip installできるようになってきているらしいですが、今回はまだsetup.pyの拡張として提供されている機能を使用します。またPoetryにはscikit-buildがどうも対応してなさそう(?)なので今回は使いません。

CMakeLists.txt

元のコード(scikit-build/tests/samples/hello-cpp)がCMakeLists.txtを分割していたので分けていますが、特に意味はありません。順番に見ていきましょう。

cmake_minimum_required(VERSION 3.5.0)
project(hello)
find_package(PythonExtensions REQUIRED)
add_subdirectory(hello)
add_library(_hello MODULE _hello.cpp)
python_extension_module(_hello)
install(TARGETS _hello LIBRARY DESTINATION hello)

ポイントとしては PythonExtensions を探してきている部分と、python_extension_module(_hello)ですね。中身は調べられていないのですが、Pythonの拡張モジュールとして公開する際には必要のようです。

Build

setup関数がsetuptoolsからskbuildに切り替わっていますが、基本的な使い方は同じで、

python setup.py bdist_wheel

とすればwheelを作成してくれます。wheelはPEP 427で定義された配布形式で拡張子は *.whl となります。whlファイルは実体としてはZIPアーカイブで、コンパイルされた共有ライブラリや他の静的なファイルを含めることができます。 上のコマンドで、dist/hello-1.2.3-cp37-cp37m-linux_x86_64.whlのようなファイルが出来上がっているはずです。これはそのままインストールが可能です

pip install dist/hello-1.2.3-cp37-cp37m-linux_x86_64.whl --user

--userはお好みで)これでGlobalにインストールされたので 別のディレクトリに行って

$ python -m hello
Hello, World! :)  

となると成功です。別のディレクトリに行かないと現在のディレクトリ以下にある hello モジュールを読み込みますが、ここにはcmakeで生成された拡張モジュールがありません…(´・ω・`)

Links