この記事について
CMakeを使って、Windows PC、Linux PC、Raspberry Piネイティブコンパイル、Raspberry Piクロスコンパイル、に対応するプロジェクトを作ります。共通のソースコードを使って、CMakeによるビルド設定だけで自由にターゲットとなるプラットフォームを変えられるようにします。また、ターゲット毎に異なるソースコードを使用できるようにもします。
組み込み開発においても、ロジック部はデバッグのしやすいPCで開発した方が効率がいいので、結構有益な方法だと思います。
ターゲット環境
- Windows10 64-bit
- Visual Studio 2017 Community
- MSYS
- Ubuntu 16.04 on VirtualBox on Windows 10
- Raspberry Pi 2
- Native Build
- Cross Build
- 注: 本記事の趣旨として、これらのどの環境向けにもビルドできるというだけで、全て整っている必要はありません
想定するプロジェクト
main関数、ModuleA_func()関数、ModuleB_func()関数の3つの関数を考えます。
依存関係は、main関数→ModuleA_func()関数→ModuleB_func()関数とします。
また、ターゲット(PC系 or ラズパイ)によって、ModuleB_func()の中身を変えたいとします。例えば、ModuleA_func()は何かの計算をするロジック関数、ModuleB_func()は結果を出力する関数と思ってください。そのため、ModuleB_func()関数はPC系ではprintfで出力、ラズパイではLEDに出力するなど挙動が違うことを想定しています。
(今回は簡単のため、全てprintfでやります)
# include <stdio.h>
# include "ModuleA.h"
int main()
{
printf("Hello\n");
ModuleA_func();
}
# include <stdio.h>
# include "ModuleA.h"
# include "ModuleB.h"
void ModuleA_func()
{
printf("ModuleA_func called\n");
ModuleB_func();
}
# include <stdio.h>
# include "ModuleB.h"
void ModuleB_func()
{
printf("ModuleB_func for PC called\n");
}
# include <stdio.h>
# include "ModuleB.h"
void ModuleB_func()
{
printf("ModuleB_func for RASPI called\n");
}
プロジェクト構成
build/
MyMultiProject/
│ CMakeLists.txt
│
├─cmakes/
│ common.cmake
│ PC_LINUX.cmake
│ PC_WIN.cmake
│ RASPI_CROSS.cmake
│ RASPI_NATIVE.cmake
│
├─Main/
│ CMakeLists.txt
│ main.cpp
│
├─ModuleA/
│ │ CMakeLists.txt
│ │
│ ├─export/
│ │ ModuleA.h
│ │
│ └─src/
│ ModuleA.cpp
│
└─ModuleB/
│ CMakeLists.txt
│
├─export/
│ ModuleB.h
│
└─src/
ModuleB_PC.cpp
ModuleB_RASPI.c
最上位のCMakeLists.txtとcmakes
CMakeLists.txt
色々と書き方や考え方はあると思いますが、僕は最上位のCMakeLists.txtにはビルドに関わる設定だけを書くようにしています。そして、その中で各プロジェクト(Main, ModuleA, ModuleB)をadd_subdirectoryします。
CMake実行時に-DBUILD_TARGET=XXX
と指定することで、対象とするターゲットを選択できるようにします。デフォルトはよく使うPC
にして、WindowsかLinuxは標準の環境変数WIN32
で判断しています。その後、ターゲット用の設定を記したファイルを読んでいます。
cmake_minimum_required(VERSION 2.8)
project(MyMultiProject)
# Switch build target
set(BUILD_TARGET PC CACHE STRING "Build target?")
# Common build settings
include(cmakes/common.cmake)
# Build settings for each target
if(${BUILD_TARGET} STREQUAL PC)
if(WIN32)
message("[BUILD] PC Windows")
include(cmakes/PC_WIN.cmake)
else()
message("[BUILD] PC Linux")
include(cmakes/PC_LINUX.cmake)
endif()
elseif(${BUILD_TARGET} STREQUAL RASPI_NATIVE)
message("[BUILD] Raspberry Pi Native")
include(cmakes/RASPI_NATIVE.cmake)
elseif(${BUILD_TARGET} STREQUAL RASPI_CROSS)
message("[BUILD] Raspberry Pi Cross")
include(cmakes/RASPI_CROSS.cmake)
else()
message(FATAL_ERROR "[BUILD] Invalid target")
endif()
# Include projects
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/output)
add_subdirectory(${PROJECT_SOURCE_DIR}/Main Main)
add_subdirectory(${PROJECT_SOURCE_DIR}/ModuleA ModuleA)
add_subdirectory(${PROJECT_SOURCE_DIR}/ModuleB ModuleB)
各ターゲット用の設定
cmakesディレクトリに保存しています。共通設定はcommon.cmakeに、各ターゲット用の設定はそれぞれ、XXX.cmakeに保存しています。
ここでは、コンパイラ用のフラグ設定をcommon.cmakeで設定しています。ただし、Windows(Visual Studio)用のPC_WIN.cmakeでは、これらの設定はVisual Studio標準のを使いたいので、unsetしています。
PC Linux(PC_LINUX.cmake)とRaspbery Piネイティブビルド(RASPI_NATIVE.cmake)用の設定では、標準のコンパイラをそのまま使えるので、基本的には何も書かないでOKです。ただ、Debug/Releaseの設定をコマンドから切り替えるのが面倒なので、僕は横着してここで指定してしまっています。
Raspberry Piクロスコンパイル(RASPI_CROSS)用の設定が一番キモになる所です。ここで、ラズパイのCrossCompiler設定をしています。クロスコンパイラとしては、git clone https://github.com/raspberrypi/tools
で~/
にダウンロードしてきたものを使います。まず、CROSS_COMPILE
やCMAKE_C_COMPILER
を設定することで、使用するコンパイラの設定をします。その後、CMAKE_FIND_ROOT_PATH
を設定することでtoolchainの場所を指定します。また、ビルドする際に、ライブラリとインクルードファイルはCMAKE_FIND_ROOT_PATH
で指定されたターゲット用のものだけを使用する用に設定しています。
ラズパイ用のCross Compilerに関してはこちらの記事もご参照ください(https://qiita.com/take-iwiw/items/5b20558f8ab3f27ca4a4 )。
追記: RASPI_NATIVEとRASPI_CROSSには一応、NEONや浮動小数点周りの設定をしておきました。
# Common compile options
set(CMAKE_C_FLAGS "-Wall")
set(CMAKE_C_FLAGS_DEBUG "-g -O0")
set(CMAKE_C_FLAGS_RELEASE "-O3 -s")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -std=c++11")
set(CMAKE_CXX_FLAGS_DEBUG ${CMAKE_C_FLAGS_DEBUG})
set(CMAKE_CXX_FLAGS_RELEASE ${CMAKE_C_FLAGS_RELEASE})
# Use visual studio default settings
unset(CMAKE_C_FLAGS)
unset(CMAKE_C_FLAGS_DEBUG)
unset(CMAKE_C_FLAGS_RELEASE)
unset(CMAKE_CXX_FLAGS)
unset(CMAKE_CXX_FLAGS_DEBUG)
unset(CMAKE_CXX_FLAGS_RELEASE)
set(CMAKE_BUILD_TYPE release)
# set(CMAKE_BUILD_TYPE debug)
# or
# cmake ../project -DCMAKE_BUILD_TYPE=Release
# cmake ../project -DCMAKE_BUILD_TYPE=Debug
set(CMAKE_BUILD_TYPE release)
# set(CMAKE_BUILD_TYPE debug)
# or
# cmake ../project -DCMAKE_BUILD_TYPE=Release
# cmake ../project -DCMAKE_BUILD_TYPE=Debug
set(ARM_COMPILE_OPTION "-mcpu=native -mfpu=neon-vfpv4 -mfloat-abi=hard")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${ARM_COMPILE_OPTION}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${ARM_COMPILE_OPTION}")
set(CMAKE_BUILD_TYPE release)
# set(CMAKE_BUILD_TYPE debug)
# or
# cmake ../project -DCMAKE_BUILD_TYPE=Release
# cmake ../project -DCMAKE_BUILD_TYPE=Debug
set(ARM_COMPILE_OPTION "-mcpu=cortex-a7 -mfpu=neon-vfpv4 -mfloat-abi=hard")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${ARM_COMPILE_OPTION}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${ARM_COMPILE_OPTION}")
# cross compiler settings
set(CMAKE_CROSSCOMPILING TRUE)
set(CROSS_COMPILE "arm-linux-gnueabihf-")
set(CMAKE_C_COMPILER ${CROSS_COMPILE}gcc)
set(CMAKE_CXX_COMPILER ${CROSS_COMPILE}g++)
set(CMAKE_LINKER ${CROSS_COMPILE}gcc)
# root path settings
set(CMAKE_FIND_ROOT_PATH ~/raspberry/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # use host system root for program
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) # use CMAKE_FIND_ROOT_PATH for library
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) # use CMAKE_FIND_ROOT_PATH for include
Main
main関数を持つモジュールをMainとします。そして、その中にmain.cppがあるとします。
main.cppの中で、下位モジュールであるModuleAを呼び出しています。
# include <stdio.h>
# include "ModuleA.h"
int main()
{
printf("Hello\n");
ModuleA_func();
}
Main用のCMakeLists.txtです。ソースファイル、ヘッダファイル(今回はなし)を指定して、Mainという実行ファイルを作ります。
また、Mainを作るには、ModuleAというライブラリが必要です。また、Mainをコンパイルする際には、ModuleA/exportをインクルードパスに追加します。
ということを書いています。
set(SOURCES
main.cpp
)
set(HEADERS
# main.h
)
add_executable(Main
${SOURCES}
${HEADERS}
)
target_link_libraries(Main
ModuleA
)
target_include_directories(Main PUBLIC
${PROJECT_SOURCE_DIR}/ModuleA/export
)
ModuleA
ModuleA_func()関数を持つモジュールをModuleAとします。ヘッダファイルはexport/の中に入れています。これは、個人的な志向なのですが、外部向けのヘッダファイルはそれ以外と明確に分けたいのでこのようにしています。別に、include/という名前でもいいし、フォルダ分けしないでもいいです。
# ifndef _MODULE_A_H_
# define _MODULE_A_H_
void ModuleA_func();
# endif /* _MODULE_A_H_ */
# include <stdio.h>
# include "ModuleA.h"
# include "ModuleB.h"
void ModuleA_func()
{
printf("ModuleA_func called\n");
ModuleB_func();
}
ModuleA用のCMakeLists.txtです。Mainとの違いは、作るのが実行ファイルではなくてライブラリという点です。add_library
の中でSTATIC
を指定しています。そのため、作られるのは静的ライブラリになります。(Windowsなら.lib、Linuxなら.a)。これを、SHARED
にすると共有ライブラリになります。(Windowsなら.dll、Linuxなら.so)。
set(SOURCES
src/ModuleA.cpp
)
set(HEADERS
export/ModuleA.h
)
add_library(ModuleA STATIC
${SOURCES}
${HEADERS}
)
target_link_libraries(ModuleA
ModuleB
)
target_include_directories(ModuleA PUBLIC
./export
${PROJECT_SOURCE_DIR}/ModuleB/export
)
ModuleB
ModuleBは、PC系かラズパイかで、挙動を変えるようにしてみました。ついでに、cpp/cも変えてみました。仕組みは簡単で、CMake実行時に指定したBUILD_TARGET
によって、使用するソースファイルを切り替えているだけです。
# ifndef _MODULE_B_H_
# define _MODULE_B_H_
# ifdef __cplusplus
extern "C" {
# if 0
}
# endif
# endif
void ModuleB_func();
# ifdef __cplusplus
}
# endif
# endif /* _MODULE_B_H_ */
# include <stdio.h>
# include "ModuleB.h"
void ModuleB_func()
{
printf("ModuleB_func for PC called\n");
}
# include <stdio.h>
# include "ModuleB.h"
void ModuleB_func()
{
printf("ModuleB_func for RASPI called\n");
}
if(${BUILD_TARGET} STREQUAL PC)
set(SOURCES
src/ModuleB_PC.cpp
)
elseif(${BUILD_TARGET} STREQUAL RASPI_NATIVE OR ${BUILD_TARGET} STREQUAL RASPI_CROSS)
set(SOURCES
src/ModuleB_RASPI.c
)
endif()
set(HEADERS
export/ModuleB.h
)
add_library(ModuleB STATIC
${SOURCES}
${HEADERS}
)
target_include_directories(ModuleB PUBLIC
./export
)
ビルドして動かしてみる
Windows (Visual Studio)
Visual Studio用のプロジェクト(ソリューション)を作るには、cmake-guiを使います。
Where is the source codeに、最上位のCMakeLists.txtがある場所を指定します。
Where to build the binaiesに、Visual Studio用のプロジェクトファイル一式を作る場所を指定します。どこでもいいです。
その後、Configureをクリックします。すると、何用のプロジェクトを作るか聞かれるので、ここではVisual Studioを選びます。
Finishをクリックし、最後にGenerateをクリックしたら完了です。
Where to build the binaiesで指定した場所に、MyMultiProject.slnが作られているのでそれを開いて、後は普通にビルド、実行できます。
一つだけ注意点として、一番最初は、スタートアッププロジェクトがALL_BUILD
になっているかもしれません。その場合は、main関数のあるMainプロジェクトで右クリックして、スタートアッププロジェクトに設定
を選びます。
Windows (MSYS, MinGW)
cd MyMultiProjectと同じ場所(実際はどこでもいい)
mkdir build && cd build
cmake ../MyMultiProject -G "MSYS Makefiles"
make
output/Main
Linux (Ubuntu)
cd MyMultiProjectと同じ場所(実際はどこでもいい)
mkdir build && cd build
cmake ../MyMultiProject
make
output/Main
Debug/Releaseを切り替えたい場合には、PC_LINUX.cmake内のset(CMAKE_BUILD_TYPE release)
をコメントアウトして、cmake ../MyMultiProject -DCMAKE_BUILD_TYPE=Release
またはcmake ../MyMultiProject -DCMAKE_BUILD_TYPE=Debug
ラズパイ (Native Build)
プロジェクト一式をコピーして、下記を実行
cd MyMultiProjectと同じ場所(実際はどこでもいい)
mkdir build && cd build
cmake ../MyMultiProject/ -DBUILD_TARGET=RASPI_NATIVE
make
output/Main
ラズパイ (Cross Compile)
cd MyMultiProjectと同じ場所(実際はどこでもいい)
mkdir build && cd build
cmake ~/Desktop/win_share/project/ -DBUILD_TARGET=RASPI_CROSS
make
scp output/Main pi@192.168.1.89:/home/pi
ラズパイのホームディレクトリに、Mainがコピーされる。
その他
コンパイルオプションの確認方法
make VERBOSE=1
Pre-built対応
各モジュールを毎回コンパイルするのではなく、一度ビルドしたらライブラリとして保存しておいて、次回以降はリンクするだけにしたいことが多々あります。目的はビルド時間の短縮だったり、コードを他の人に見せたくないなどがあります。また、動的ライブラリにすれば、モジュール部分だけを後から差し替えることもできます。
最上位のCMakeLists.txtを以下のようにして、2回目以降はcmake ../MyMultiProject -DLINK_ONLY=YES
とします。
(Linux用バイナリはディレクトリ分けしていないので、実際にはもう少しちゃんとやる必要があります。)
cmake_minimum_required(VERSION 2.8)
project(MyMultiProject)
# Switch build target
set(BUILD_TARGET PC CACHE STRING "Build target?")
set(LINK_ONLY OFF CACHE BOOL "Use pre-built library?")
# Common build settings
include(cmakes/common.cmake)
# Build settings for each target
if(${BUILD_TARGET} STREQUAL PC)
if(WIN32)
message("[BUILD] PC Windows")
include(cmakes/PC_WIN.cmake)
else()
message("[BUILD] PC Linux")
include(cmakes/PC_LINUX.cmake)
endif()
elseif(${BUILD_TARGET} STREQUAL RASPI_NATIVE)
message("[BUILD] Raspberry Pi Native")
include(cmakes/RASPI_NATIVE.cmake)
elseif(${BUILD_TARGET} STREQUAL RASPI_CROSS)
message("[BUILD] Raspberry Pi Cross")
include(cmakes/RASPI_CROSS.cmake)
else()
message(FATAL_ERROR "[BUILD] Invalid target")
endif()
# Use modules as pre-built libraries
if(${LINK_ONLY})
if(WIN32)
link_directories(${PROJECT_SOURCE_DIR}/ModuleA/export/${Configuration})
link_directories(${PROJECT_SOURCE_DIR}/ModuleB/export/${Configuration})
else()
link_directories(${PROJECT_SOURCE_DIR}/ModuleA/export/)
link_directories(${PROJECT_SOURCE_DIR}/ModuleB/export/)
endif()
else()
add_subdirectory(${PROJECT_SOURCE_DIR}/ModuleA ModuleA)
add_subdirectory(${PROJECT_SOURCE_DIR}/ModuleB ModuleB)
set_target_properties(ModuleA PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/ModuleA/export)
set_target_properties(ModuleB PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/ModuleB/export)
endif()
# Include projects
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/output)
add_subdirectory(${PROJECT_SOURCE_DIR}/Main Main)
クロスコンパイラの他の設定方法
bash上の環境変数として、CC
やCXX
を設定してあげる方法もあります。この場合は、CMake上では何も指定しないでOKです。
export CC=~/raspberry/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin/arm-linux-gnueabihf-gcc
export CXX=~/raspberry/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin/arm-linux-gnueabihf-g++
処理やオプションの切り替え方法
今回、ModuleBの挙動をターゲット毎に変えるために、cmake実行時に指定したBUILD_TARGET
を参照して切り替えていました。実際には、もう少し荒いレベルで切り替えたかったり、別の切り分け方をしたい場合もあります。
例えば、CPU種別によって切り替えたい場合には、最上位のCMakeLists.txtでBUILD_TARGET
を見ている所で、set(ARCH_TYPE ARM)
やset(ARCH_TYPE x86)
としてあげることで、後で使いやすくなります。