Help us understand the problem. What is going on with this article?

qmakeユーザーのためのCMake入門その2

はじめに

体調不良ですっかり遅れてしまいましたが、Qt Advent Calendar 2019の10日目として、9日目に引き続きCMake入門をお送りします。

9日目は、現在のQt6に向けた流れからCMakeを学習する必要性と、qmakeと対比しながらCMakeの大雑把な特徴を紹介しました。本日は、引き続きqmakeではこうしていたことがCMakeではこうなりますという紹介です。

なお、こちらの記事はqmakeを使っていた方向けの説明ですので、新たにCMakeから始める人は、正規のドキュメントやCMakeのタグから別の記事を探していただいた方が良いかと思います。

TEMPLATE=.*

qmakeでは、プロジェクトファイルの挙動を決めるため、ますTEMPLATE変数で何をするプロジェクトなのかを選択していました。

  • app/vcapp
  • lib/vclib
  • subdirs

これは、qmakeのプロジェクトファイルが原則として1ファイル1ターゲットとなっているため、プロジェクトがアプリケーションの作成のものなのか、ライブラリ作成のものなのか、サブディレクトリを制御するものなのか、それ以外の汎用プロジェクトなのかといった選択をするものでした。

選択されたテンプレートと対応する変数に値を設定することでMakefileやプロジェクトファイルが生成されていました。

それに対し、CMakeは1ファイルに複数ターゲットに対応しており、CMakeLists.txtが何のためのCMakeソースなのかという選択はありません。qmakeとCMakeの最初の大きな違いといえるでしょう。

TEMPLATE=app(vcapp)

qmakeにおける TEMPLATE=app は、アプリケーションを作成するためのテンプレートで、以下の変数を使いアプリケーションのビルドについて記述していました。

  • SOURCES
  • HEADERS
  • FORMS
  • RESOURCES
  • LEXSOURCES
  • YACCSOURCES
  • TARGET
  • DETSDIR
  • DEFINES
  • INCLUDEPATH
  • DEPENDPATH
  • VPATH

これらの変数相当のコマンドについてCMakeでの記述を見ていきましょう。

SOURCES/HEADERS

これは、9日目の記事でも触れましたが、add_executableコマンドに引き渡します。

hello.pro
TEMPLATE = app
CONFIG -= qt
SOURCES = main.cpp hello.cpp
HEADERS = hello.h
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloWorld LANGUAGES C++)
add_executable(helloworld main.cpp hello.cpp hello.h)

なお、QtでQObject系(メタオブジェクトを必要とするクラス)を利用する場合は、もう少し記述を増やす必要があります。

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloWorld LANGUAGES C++)
set(CMAKE_AUTOMOC ON)
find_package(Qt5 COMPONENTS Core REQUIRED)
add_executable(helloworld main.cpp hello.cpp hello.h)

QObjectを継承するクラスを定義したヘッダファイルは、moc(Meta Object Compiler)というプリプロセッサの対象となります。qmakeは指定されたソースコードとヘッダを検証し、対象の場合、mocを使いメタオブジェクトを記述したソースコードを自動生成するビルドルールへと変換します。

CMakeでは、CMAKE_AUTOMOC変数にONを指定しておくことで、add_executableに渡したソースコードを精査しmoc周りのルール追加するようになります。

FORMS

これは、デザイナーで作成した *.uiファイルを指定する変数です。指定したuiファイルは、uicプログラムに渡されui_xxxxx.hに変換され、ユーザーがインクルードして利用することになります。

hello.pro
TEMPLATE = app
SOURCES = main.cpp mainwindow.cpp
HEADERS = mainwindow.h
FORMS = mainwindow.ui

CMakeでは、CMAKE_AUTOUIC変数にONを指定して、uiファイルもadd_executableコマンドで指定します。なお、生成されるヘッダファイルのパスが通るようにCMAKE_INCLUDE_CURRENT_DIR もONにしておきましょう。

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloWorld LANGUAGES C++)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt5 COMPONENTS Widgets REQUIRED)
add_executable(helloworld 
    main.cpp 
    mainwindow.cpp 
    mainwindow.h 
    mainwindow.ui
)

RESOURCES

RESOURCES変数は、The Qt Resource Systemを使った Resource Collection Files (.qrc)を登録するものです。この変数に登録するとrcc(resource compiler)を使ってC++ソースコードに変換し、ターゲットのソースコードに追加されます。

hello.pro
TEMPLATE = app
SOURCES = main.cpp mainwindow.cpp
HEADERS = mainwindow.h
FORMS = mainwindow.ui
RESOURCES = resource_file.qrc

CMakeでは、CMAKE_AUTORCC変数にONを指定して、やはりadd_executableコマンドに引き渡します。

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloWorld LANGUAGES C++)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
find_package(Qt5 COMPONENTS Widgets REQUIRED)
add_executable(helloworld 
    main.cpp 
    mainwindow.cpp 
    mainwindow.h 
    mainwindow.ui
    resource_file.qrc
)

LEXSOURCES/YACCSOURCES

レキシカルアナライザとパーサージェネレータ用のソースコードを指定するための変数です。CMakeではFindBISON, FindFLEXを使うことになりますが、試したことが無いので割愛します。興味がある方は、CMakeのドキュメントを参考にしながら試してください。

TARGET

TARGET変数は、ターゲット名を指定する変数です。デフォルトではプロジェクトファイル名=ディレクトリ名になりますが、都合が悪い場合は、TARGET変数で変更できました。

CMakeの場合、add_executableコマンドの第一引数がターゲット名となっています。

add_executable(<name> [WIN32] [MACOSX_BUNDLE]
               [EXCLUDE_FROM_ALL]
               [source1] [source2 ...])

なお、CMakeのプロパティという機構を使ってCMake上のターゲット名と出力ファイル名を制御できます。CMakeのプロパティとqmakeのプロパティとでは大きく意味が異なるので注意が必要です。

set_propertyを使い、OUTPUT_NAME系の変数を変更して対応も可能です。

set_property(TARGET <name> PROPERTY OUTPUT_NAME hogehoge)

DESTDIR

DESTDIR変数は生成したターゲットファイルの生成先を指定する変数です。CMakeではCMAKE_RUNTIME_OUTPUT_DIRECTORYで指定します。なお、install先(make installなどで置かれる先)ではなく、ビルドした結果のバイナリが置かれる場所になります。

DEFINES

DEFINES変数はビルド時に追加されるプロプロセッサdefineを格納する変数です。

hello.pro
TEMPLATE = app
CONFIG -= qt
SOURCES = main.cpp hello.cpp
HEADERS = hello.h
DEFINES += USE_A_FUNC "MY_DEFINE=\"FOO BAR\""

として定義するとビルド時に

gcc -DUSE_A_FUNC -DMY_DEFINE="FOO BAR" ....

のようになります。これをCMakeで行うには、add_compile_definitionstarget_compile_definitions を使うことになります。

なお、CMakeのバージョンが3.12以前ですと、add_compile_definitions が無くadd_definitions というコマンドを使う必要があります。

add_definitions と add_compile_definitions は、複数のターゲットのdefineに影響を与えます。古い方の定義では、"-D"を付けて指定する必要がありました。

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloWorld LANGUAGES C++)
add_definitions(-DUSE_A_FUNC -DMY_DEFINE="FOO BAR")
add_executable(helloworld main.cpp hello.cpp hello.h)

add_compile_definitions では、"-D"が不要になっています。

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloWorld LANGUAGES C++)
add_compile_definitions(USE_A_FUNC MY_DEFINE="FOO BAR")
add_executable(helloworld main.cpp hello.cpp hello.h)

target_compile_definitionsは、複数のターゲット向けではなく、特定のターゲットのdefineを制御します。

target_compile_definitions(<target>
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

<target>と<INTERFACE | PUBLIC | PRIVATE>を指定する必要がありますが、アプリケーションのビルドの時はPRIVATEを指定しておけば問題ありません。

cmake_minimum_required(VERSION 3.15)
project(HelloWorld LANGUAGES C++)
add_executable(helloworld main.cpp hello.cpp hello.h)
target_compile_definitions(helloworld PRIVATE USE_A_FUNC MY_DEFINE="FOO BAR")

INCLUDEPATH

INCLUDEPATHはその名の通り、includeされるヘッダファイルの検索パスリストを指定する変数です。CMakeでは include_directoriesまたは target_include_directoriesコマンドを利用します。

hello.pro
TEMPLATE = app
CONFIG -= qt
SOURCES = main.cpp hello.cpp
HEADERS = hello.h
INCLUDES += inc
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloWorld LANGUAGES C++)
include_directories(inc)
add_executable(helloworld main.cpp hello.cpp hello.h)
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloWorld LANGUAGES C++)
add_executable(helloworld main.cpp hello.cpp hello.h)
target_include_directories(helloworld PRIVATE inc)

include_directoriesが複数のターゲットのプロパティにインクルードパスを設定するのに対し、target_include_directoriesは、指定したターゲットにのみインクルードパスを追加します。

これらのAPIには呼び出し前に定義されていたinclude pathより前に追加するか、後に追加するかのオプションと、対応しているプラットフォームの場合システムパスであることを示すオプションが用意されています。

include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

target_include_directories(<target> [SYSTEM] [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

なお、このAPIにも、INTERFACE,PUBLIC,PRIVATEの指定がありますが、アプリケーションの場合はPRIVATEを指定しておけば問題ありません。

DEPENDPATH

qmakeでは、ヘッダファイルの依存関係を追いかけるべきパスをINCLUDEPATHとは別に設定できていました。これは、変更される可能性の低い外部ライブラリのヘッダファイルまで探索すると時間がかかるためです。CMakeではこの仕組みを見つけられていません。
そのような対処がされているのをご存知の方がいれば、ぜひご連絡ください。

VPATH

qmakeでは、開くことのできないファイルがあった場合、VPATHに指定されたパスに同名のファイルがないかを探しに行く機能が提供されていましたが、CMakeではこのような仕組みは見つけられませんでした。

ファイルの存在確認や検索は可能なので、同じようなことをやろうと思えばできますが、自動的に行う仕組みはなさそうです。

TEMPLATE=lib

qmakeでは、ライブラリ用のプロジェクトとして定義するには、TEMPLATE変数にlibを指定し、以下の変数を設定していました。

  • CONFIG
  • SOURCES
  • HEADERS
  • RESOURCES
  • TARGET
  • DETSDIR
  • DEFINES
  • INCLUDEPATH
  • VERSION

CONFIG, SOURCES, HEADERS, RESOURCES

CONFIG変数にstatic, shared(dll)またはstatic_and_sharedを設定すると静的ライブラリや共有ライブラリのビルドを行うことを指定できました。

CMakeでは、静的ライブラリか、共有ライブラリかは、add_libraryコマンドの引数で指定します。

add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            [source1] [source2 ...])

SOURCESや、HEADERS, RESOURCESの代わりにadd_libraryの引数に渡すのは、add_executableと同じです。

たとえば、qmakeで共有ライブラリを作るための以下の指定は、

hello.pro
TEMPLATE = lib
TARGET = hello
CONFIG += shared
SOURCES = hello.cpp
HEADERS = hello.h

CMakeでは以下のようになります。

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloLib LANGUAGES C++)
add_library(hello SHARED hello.cpp hello.h)

ちなみにMODULEは、SHAREDに近い共有ライブラリですが、直接リンクを行わず、プラグインなどのようにdlopenなどで読み込む用途に使われるビルド用です。例えばLinuxの場合ですとSONAMEが設定されないようなオプション構成になります。

ところで、staticとsharedを両方同時にビルドしたいという場合には、CMakeでは少し手がかかります。

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloLib LANGUAGES C++)
set(HELLO_SRCS hello.cpp hello.h)
add_library(hello SHARED ${HELLO_SRCS})
add_library(helloA STATIC ${HELLO_SRCS})
set_property(TARGET helloA PROPERTY ARCHIVE_OUTPUT_NAME hello)

add_libraryを異なるターゲット名で呼び出し、出力ファイル名のプロパティを変更するという記述になります。

INCLUDEPATHやDEFINESは、アプリケーションの場合と同じです。INTERFACEやPUBLICの扱いが意味を持ってくるのですが、そのあたりは後述します。

TEMPLATE=subdirs

qmakeでは、異なる.proは異なるプロジェクトとして扱われ、1ファイル1プロジェクトという考え方です。そのため、ライブラリとそれとリンクする実行ファイルを持つプロジェクトの場合、ライブラリと実行ファイルのソースをそれぞれのディレクトリに分け、複数のサブプロジェクトを持つプロジェクト作成には、TEMPLATE=subdirsを使うということになっていました。

CMakeの場合、この辺りの考え方がqmakeとは異なりますが、ディレクトリ分割され、それぞれに用意されたCMakeソースコードを読み込ませていくことが可能です。

これには、add_subdirectoryを使います。

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

名称から想像がつくかもしれませんが、指定できるのは1コマンドにつき1ディレクトリになります。

例えば、以下のようなソースツリーがあるとします。

  • example
    • app
      • main.cpp
    • lib
      • hello.h
      • hello.cpp

qmakeの場合は、アプリケーションのインクルード・リンクルールを.priファイルで提供するライブラリプロジェクトにすると、プロジェクトファイルは以下のような構成になります。

lib/lib.pro
TEMPLATE = lib
TARGET = hello
CONFIG -= qt
CONFIG += shared
SOURCES = hello.cpp
HEADERS = hello.h
lib/lib.pri
LIBNAME = $$fromfile($${PWD}/lib.pro, TARGET)
CONFIG(debug,debug|release) {
     unix: LIB_NAME = $$join(LIB_NAME,,,_debug)
    win32: LIB_NAME = $$join(LIB_NAME,,,d)
}
INCLUDEPATH += $${PWD}
DEPENDPATH  += $${PWD}
LIBS += -l$${LIBNAME}
unix: QMAKE_RPATHDIR += $$shadowed($${PWD})
unix:LIBS += -L$${QMAKE_RPATHDIR}
app/app.pro
SOURCES = main.cpp
CONFIG -= qt
TARGET = HelloWorld
include(../lib/lib.pri)
example.pro
TEMPLATE=subdirs
SUBDIRS = app lib
app.depends = lib

同等のプロジェクトをCMakeで記述すると、以下のようになります。

lib/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
add_library(hello SHARED hello.h hello.cpp)
target_include_directories(hello INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
add_executable(HelloWorld main.cpp)
target_link_libraries(HelloWorld PRIVATE hello)
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(HelloWorld)
add_subdirectory(app)
add_subdirectory(lib)

qmakeの場合、アプリケーションからライブラリをリンクするのはLIBS変数に設定することになっていました。上述の例では、.priを使ってライブラリ側がルールを提供し、app.proでこれをincludeしていました。

CMakeの場合は、app側でtarget_link_librariesコマンドを使い、リンクするライブラリのターゲット名を指定します。実際にどんな出力名になっているのかなどは、ターゲット名で解決されるので、余計な努力は不要です。

INCLUDEPATHですが、これが説明を保留していたINTERFACEの役割です。

以下の1行で、helloターゲットとリンクするアプリケーションは、libディレクトリをインクルードディレクトリとしてパスに追加するようになります。

lib/CMakeLists.txt
target_include_directories(hello INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

この例だと少しわかりにくいので、もう1行追加してみましょう。
cmake:lib/CMakeLists.txt
target_compile_definitions(hello PUBLIC HOGE INTERFACE FUGA PRIVATE UGE)

INTERFACEは、helloには影響を与えず、helloとリンクするターゲットにのみ伝播します。
PUBLICは、helloおよびhelloとリンクするターゲットに伝播します。
PRIVATEは、helloにのみ伝播します。

つまり、以下のようなことになります。

${CXX} ... -DHOGE -DUGE lib/hello.cpp
${CXX} ... -DHOGE -DFUGA app/main.cpp

このように、CMakeではターゲット間でリンクする場合のルールを、ライブラリ側に設定することができます。target_link_librariesのINTERFACE,PUBLIC,PRIVATEも同じような挙動を示します。

このあたりは、qmakeには不足している大規模プロジェクト向けの機能と言えるでしょう。

まとめ

今回は、Qtのアドベントカレンダーということで、qmakeをベースにqmakeのTEMPLATEで指定するようなサブディレクトリ指定、アプリケーション作成、ライブラリ作成に関わるコマンドとその挙動を大雑把に解説しました。

現在、qmakeを知らない人がCMakeから入門するための薄い本を書こうと鋭意努力中ですので、CMakeから新たに勉強をはじめようという方は、あまり期待せず、お待ちいただければと思います。

11日目は、@Helicobacter_pylori さんが、Qtの記録代わりにネタを投稿してくださる予定です。今年は私も含め、期日に間に合わない方も多いですが、師も走る12月というわけで、焦らずのんびりお待ちいただければと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした