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

CMakeでマルチプラットフォーム開発 (ラズパイ用クロスコンパイル含む)

More than 1 year has passed since last update.

この記事について

CMakeを使って、Windows PC、Linux PC、Raspberry Piネイティブコンパイル、Raspberry Piクロスコンパイル、に対応するプロジェクトを作ります。共通のソースコードを使って、CMakeによるビルド設定だけで自由にターゲットとなるプラットフォームを変えられるようにします。また、ターゲット毎に異なるソースコードを使用できるようにもします。

組み込み開発においても、ロジック部はデバッグのしやすいPCで開発した方が効率がいいので、結構有益な方法だと思います。

https://github.com/take-iwiw/cmake_samples/tree/master/MyMultiProject

ターゲット環境

  • 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でやります)

main.cpp
#include <stdio.h>
#include "ModuleA.h"

int main()
{
    printf("Hello\n");
    ModuleA_func();
}
ModuleA.cpp
#include <stdio.h>
#include "ModuleA.h"
#include "ModuleB.h"

void ModuleA_func()
{
    printf("ModuleA_func called\n");
    ModuleB_func();
}
ModuleB_PC.cpp
#include <stdio.h>
#include "ModuleB.h"

void ModuleB_func()
{
    printf("ModuleB_func for PC called\n");
}
ModuleB_RASPI.c
#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で判断しています。その後、ターゲット用の設定を記したファイルを読んでいます。

CMakeLists.txt
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_COMPILECMAKE_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や浮動小数点周りの設定をしておきました。

cmakes/common.cmake
# 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})
cmakes/PC_WIN.cmake
# 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)

cmakes/PC_LINUX.cmake
set(CMAKE_BUILD_TYPE release)
# set(CMAKE_BUILD_TYPE debug)
# or
# cmake ../project -DCMAKE_BUILD_TYPE=Release
# cmake ../project -DCMAKE_BUILD_TYPE=Debug
cmakes/RASPI_NATIVE.cmake
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}")
cmakes/RASPI_CROSS.cmake
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を呼び出しています。

main.cpp
#include <stdio.h>
#include "ModuleA.h"

int main()
{
    printf("Hello\n");
    ModuleA_func();
}

Main用のCMakeLists.txtです。ソースファイル、ヘッダファイル(今回はなし)を指定して、Mainという実行ファイルを作ります。
また、Mainを作るには、ModuleAというライブラリが必要です。また、Mainをコンパイルする際には、ModuleA/exportをインクルードパスに追加します。
ということを書いています。

CMakeLists.txt
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/という名前でもいいし、フォルダ分けしないでもいいです。

ModuleA/export/ModuleA.h
#ifndef _MODULE_A_H_
#define _MODULE_A_H_

void ModuleA_func();

#endif  /* _MODULE_A_H_ */
ModuleA/src/ModuleA.cpp
#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)。

ModuleA/CMakeLists.txt
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によって、使用するソースファイルを切り替えているだけです。

ModuleB/export/ModuleB.h
#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_ */
ModuleB/src/ModuleB_PC.cpp
#include <stdio.h>
#include "ModuleB.h"

void ModuleB_func()
{
    printf("ModuleB_func for PC called\n");
}
ModuleB/src/ModuleB_RASPI.c
#include <stdio.h>
#include "ModuleB.h"

void ModuleB_func()
{
    printf("ModuleB_func for RASPI called\n");
}
ModuleB/CMakeLists.txt
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をクリックしたら完了です。

01.png

Where to build the binaiesで指定した場所に、MyMultiProject.slnが作られているのでそれを開いて、後は普通にビルド、実行できます。
一つだけ注意点として、一番最初は、スタートアッププロジェクトがALL_BUILDになっているかもしれません。その場合は、main関数のあるMainプロジェクトで右クリックして、スタートアッププロジェクトに設定を選びます。

Windows (MSYS, MinGW)

msys
cd MyMultiProjectと同じ場所(実際はどこでもいい)
mkdir build && cd build
cmake ../MyMultiProject -G "MSYS Makefiles"
make
output/Main

Linux (Ubuntu)

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)

Ubuntuのシェル
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用バイナリはディレクトリ分けしていないので、実際にはもう少しちゃんとやる必要があります。)

CMakeLists
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上の環境変数として、CCCXXを設定してあげる方法もあります。この場合は、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)としてあげることで、後で使いやすくなります。

iwatake2222
クソコード、放置するのも、同罪です (自分への戒め)
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
ユーザーは見つかりませんでした