Azure ArtifactsでC++ライブラリのパッケージ管理
前回はパイプラインを使ってライブラリを複数ターゲットにビルドしました。今回は、ビルドしたライブラリをメインのプログラムで使用するにはどうしたらいいかを考えます。
KAGURAでは、たくさんの外部ライブラリが使用されていますが、中でも、boost、OpenCV、wxWidgetsといった大きなライブラリはビルドに10分以上かかることもあり、全部のライブラリをビルドすると大変なことになります。
ライブラリを毎回ビルドする必要は無いので、ビルドしたものをパッケージにしてどこかに置いておき、本体のビルドのときに利用することで、ビルド時間を短縮したいです。
ライブラリは数カ月に1回くらいの頻度でバージョンアップします。バージョン間で互換性が無いこともあるので、ライブラリを利用するプログラム側は、どのバージョンを利用するのかを明確に指定して、正しいバージョンを取得する必要があります。
Azure DevOpsでこのようなことを実現するのに使えるのが、Azure Artifactsです。
Artifactsは「Maven、npm、NuGetのパッケージを作成、共有可能」とあります。MavenはJava、npmはNode.js、NuGetは.NET用のパッケージ管理システムです。
C++はどうしたらいいの・・・?
よく見ると、Universal Packageという見慣れないものがありました。
Universal Packageはファイルの種類に関係なく、何でもパッケージにできるもののようです。C++のライブラリはUniversal Packageにすると良さそうです。
Universal Packageについては、Qiitaにもまとめてみました。
Artifactsにフィードを作成
適当にtestfeedという名前を付けました。
最初はフィードに何もない状態です。
Pipelines→Artifactsにアップロード
zlibをビルドするジョブにUniversalPackagesタスクを追加します。vstsFeedPublish
で作成したフィード名を指定します。
# build-zlib.yml (一部) - task: UniversalPackages@0 displayName: Universal Publish inputs: command: publish publishDirectory: '$(Build.ArtifactStagingDirectory)' vstsFeedPublish: 'testfeed' vstsFeedPackagePublish: 'zlib.$(PlatformName)' packagePublishDescription: 'zlib-1.2.11 for $(PlatformName)'
vstsFeedPackagePublish
はパッケージ名です。各環境用にパッケージの名前を変えたいので、PlatformNameという変数を作ってパッケージ名に使いました。テンプレートの呼び出し元で変数を定義しています。
# azure-pipelines.yml(一部) jobs: - job: 'ubuntu1604' variables: PlatformName: ubuntu pool: vmImage: 'ubuntu-16.04' steps: - template: build-zlib.yml - job: 'win2016_vs2017' variables: PlatformName: win pool: vmImage: 'vs2017-win2016' steps: - template: build-zlib.yml - job: 'macos1013' variables: PlatformName: mac pool: vmImage: 'macOS-10.13' steps: - template: build-zlib.yml
成功するとフィードに追加されます。 パッケージのバージョンを指定しなかったので、バージョン番号は自動で0.0.1とつけられました。ビルドのたびに自動でバージョン番号が上がっていくようです。
Artifacts→Pipelinesにダウンロード
次に、ビルドしたライブラリを使う側を考えます。
まずはローカルで動作確認。ArtifactsからローカルにUniversal PackageをダウンロードするにはVSTS CLIを使います。
> vsts package universal download ` --instance "https://dev.azure.com/{myorg}" ` --feed "testfeed" ` --name "zlib.ubuntu" ` --version "0.0.1" ` --path thirdparty
どこでもいいですが、ダウンロードしたライブラリはthirdpartyディレクトリに配置することにしました。
zlibを使用するプログラムのサンプルとして、次のようなzlibのバージョンを表示するだけのC++コードがあるとします。
// main.cpp #include <iostream> #include <zlib.h> int main() { std::cout << "Hello zlib: " << zlibVersion() << std::endl; return 0; }
これをビルドするCMakeの設定ファイルはこのようになります。
# CMakeLists.txt cmake_minimum_required (VERSION 3.0) project(hello) include_directories( ${CMAKE_CURRENT_LIST_DIR}/thirdparty/include ) if(MSVC) set(ZLIB_LIBRARY ${CMAKE_CURRENT_LIST_DIR}/thirdparty/lib/zlibstatic.lib) else() set(ZLIB_LIBRARY ${CMAKE_CURRENT_LIST_DIR}/thirdparty/lib/libz.a) endif() add_executable(hello main.cpp) target_link_libraries(hello ${ZLIB_LIBRARY} ) install(TARGETS hello RUNTIME DESTINATION bin )
以上のことをパイプラインで実行するには、CMakeのビルドの前に「Artifactsからライブラリをダウンロードしてthirdpartyディレクトリに配置する」というタスクを追加する必要があります。
設定ファイルはこのようになります。
# build-hello.yml steps: - task: UniversalPackages@0 displayName: 'Universal download' inputs: command: download downloadDirectory: '$(Build.SourcesDirectory)/thirdparty' vstsFeed: 'testfeed' vstsFeedPackage: 'zlib.$(PlatformName)' vstsPackageVersion: 0.0.1 - task: CMake@1 inputs: cmakeArgs: > -DCMAKE_INSTALL_PREFIX=$(Build.ArtifactStagingDirectory) $(Build.SourcesDirectory) - task: CMake@1 inputs: cmakeArgs: > --build ./ --config Release --target install - task: PublishBuildArtifacts@1 inputs: pathtoPublish: $(Build.ArtifactStagingDirectory) artifactName: hello_zlib_$(PlatformName)
# azure-pipelines.yml trigger: - master jobs: - job: 'ubuntu1604' variables: PlatformName: ubuntu pool: vmImage: 'ubuntu-16.04' steps: - template: build-hello.yml - job: 'win2016_vs2017' variables: PlatformName: win pool: vmImage: 'vs2017-win2016' steps: - template: build-hello.yml - job: 'macos1013' variables: PlatformName: mac pool: vmImage: 'macOS-10.13' steps: - template: build-hello.yml
ビルドを実行すると・・・
# 実行結果 > ./hello Hello zlib: 1.2.11
できました。
まとめ
- ライブラリのビルド生成物をArtifactsに保存し、別のパイプラインのビルドで利用することができました。
- C++のライブラリのパッケージ管理には、Universal Packageが使えます。
これでKAGURAのCI/CD化に向けて、どういう手順でやっていけばいいか大体分かりました。Azure DevOpsは特に難しいことはなく、KAGURAの自動ビルドに導入できそうです。
ただ、Azure DevOps自体は簡単ですが、今までローカルで構築してきた複雑なビルド手順を整理して自動化するのはちょっと気が遠くなるなあ。
次に新しいプロジェクトを起ち上げるときは、最初っからCI/CDでやるべきと思いました。
Azure Pipelinesでzlibをマルチプラットフォームに自動ビルドする
KAGURAの自動ビルドをAzure Pipelinesで実現できるかどうか、検討しています。前回は、CMakeを使ってC++のコードを複数環境向け(Ubuntu、Windows、macOS)にビルドできることまで確認しました。
複数のエージェントがMicrosoftによって用意されていて、設定ファイルを変更するだけで、ビルド環境を変えることができましたが、ビルドのたびに設定ファイルを書き換えるわけにはいかないので、一度に複数の環境でビルドを走らせてほしいです。
今回は、zlibをビルドしてみます。なぜzlibかというと、KAGURAでも使用しているライブラリで、CMakeに対応し、比較的小さいライブラリであり、zlibほど多くの人に使われているライブラリのビルドが通らないなんてことは考えにくいからです。
ローカルでビルド
まずは、ローカルのUbuntu(WSL)環境でビルドできることを確認しておきます。ビルド手順はこのような流れになります。
- zlibソースコードのダウンロード
- ダウンロードファイルのチェック
- ファイルの展開
- CMakeの実行
- ビルド
- テスト
- インストール
Bash上で、次のようなコマンドを実行します。
# Download curl -sLO https://www.zlib.net/zlib-1.2.11.tar.gz # Verify SHA256 echo "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1 *zlib-1.2.11.tar.gz" | shasum -a 256 --check # Extract tar -xvf zlib-1.2.11.tar.gz mkdir build cd build # Run CMake cmake -DCMAKE_INSTALL_PREFIX=../dist ../zlib-1.2.11 # Build cmake --build ./ --config Release # Test ctest --build-config Release # Install cmake --build ./ --config Release --target install
成功するとdistフォルダにビルド結果が出力されています。
dist ├── include │ ├── zconf.h │ └── zlib.h ├── lib │ ├── libz.a │ ├── libz.so -> libz.so.1 │ ├── libz.so.1 -> libz.so.1.2.11 │ └── libz.so.1.2.11 └── share ├── man │ └── man3 │ └── zlib.3 └── pkgconfig └── zlib.pc
Azure Pipelinesでビルド
上と同じことを実行するAzure Pipelinesの設定ファイルはこのようになります。
# azure-pipelines.yml trigger: - master pool: vmImage: 'ubuntu-16.04' #vmImage: 'vs2017-win2016' #vmImage: 'macOS-10.13' steps: - script: | curl -sLO https://www.zlib.net/zlib-1.2.11.tar.gz echo "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1 *zlib-1.2.11.tar.gz" | shasum -a 256 --check tar -xvf zlib-1.2.11.tar.gz displayName: Download zlib - task: CMake@1 inputs: cmakeArgs: > -DCMAKE_INSTALL_PREFIX=$(Build.ArtifactStagingDirectory) $(Build.SourcesDirectory)/zlib-1.2.11 - task: CMake@1 inputs: cmakeArgs: > --build ./ --config Release - script: ctest --build-config Release workingDirectory: build displayName: Test - task: CMake@1 inputs: cmakeArgs: > --build ./ --config Release --target install - task: PublishBuildArtifacts@1 inputs: pathtoPublish: $(Build.ArtifactStagingDirectory) artifactName: zlib-1.2.11
これを実行すると、できました!
さて、Windows環境でビルドを実行するには、設定ファイルのvmImage
をvs2017-win2016
変更するだけ・・・と言いたいところですが、問題が発生しました。
shasum
コマンドなんて無いと。そりゃそうです、Windowsなんだもの。その前のcurl
が通っていることのほうがびっくりです。
そこで、PowerShellを使ってみます。
curl
はInvoke-WebRequest
に置き換えます。PowerShellにおけるcurl
はInvoke-WebRequest
のエイリアスであり、bashのcurl
とは挙動が違うので、そのまま使えません。shasum
はGet-FileHash
に置き換え。tar
はそのまま使えます。
# PowerShell Invoke-WebRequest -Uri https://www.zlib.net/zlib-1.2.11.tar.gz -O zlib-1.2.11.tar.gz if ((Get-FileHash zlib-1.2.11.tar.gz -Algorithm SHA256).Hash -ne "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1") { exit 1 } tar -xvf zlib-1.2.11.tar.gz
azure-pipelines.ymlに組み込みます。variables
で変数を定義し、少し整理しました。
# azure-pipelines.yml (変更) variables: DOWNLOAD_URL: https://www.zlib.net/zlib-1.2.11.tar.gz DOWNLOAD_FILE: zlib-1.2.11.tar.gz DOWNLOAD_SHA256: c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1 - powershell: | Invoke-WebRequest -Uri $env:DOWNLOAD_URL -O $env:DOWNLOAD_FILE if ((Get-FileHash $env:DOWNLOAD_FILE -Algorithm SHA256).Hash -ne $env:DOWNLOAD_SHA256) { exit 1 } tar -xvf $env:DOWNLOAD_FILE displayName: Download zlib
Windowsでもビルドできました!
そして、なんと同じ設定でUbuntuでもmacOSでもビルドできました!!PowerShellは今やUbuntuやmacOSでも使えるのです。
複数環境で一括ビルド
ここからが今日の本題です。vmImage
を毎回書き換えずに、複数環境のビルドを実行するにはどうしたらいいでしょうか。
パイプラインは1つ以上のジョブで構成されています。ジョブは1つ以上のステップで構成されます。ここではじめてジョブというものが出てきましたが、これまでのシンプルなパイプラインではジョブが1つしかないため、ジョブの記述を省略できていたのでした。
# azure-pipelines.yml trigger: - master jobs: - job: 'ubuntu1604' pool: vmImage: 'ubuntu-16.04' steps: - script: echo Ubuntu-16.04 - job: 'win2016_vs2017' pool: vmImage: 'vs2017-win2016' steps: - script: echo vs2017-win2016 - job: 'macos1013' pool: vmImage: 'macOS-10.13' steps: - script: echo macOS-10.13
このように、ジョブごとに異なるvmImage
を設定することができます。
ビルドステップは共通化してあるので、別のymlファイルに切り出します。
# build-zlib.yml steps: - powershell: | Invoke-WebRequest -Uri $env:DOWNLOAD_URL -O $env:DOWNLOAD_FILE if ((Get-FileHash $env:DOWNLOAD_FILE -Algorithm SHA256).Hash -ne $env:DOWNLOAD_SHA256) { exit 1 } tar -xvf $env:DOWNLOAD_FILE displayName: Download zlib - task: CMake@1 inputs: cmakeArgs: > -DCMAKE_INSTALL_PREFIX=$(Build.ArtifactStagingDirectory) $(Build.SourcesDirectory)/zlib-1.2.11 - task: CMake@1 inputs: cmakeArgs: > --build ./ --config Release - script: ctest --build-config Release workingDirectory: build displayName: Test - task: CMake@1 inputs: cmakeArgs: > --build ./ --config Release --target install - task: PublishBuildArtifacts@1 inputs: pathtoPublish: $(Build.ArtifactStagingDirectory) artifactName: zlib-1.2.11_$(Agent.OS)
このbuild-zlib.ymlをテンプレートとして各ジョブで読み込みます。
# azure-pipelines.yml trigger: - master variables: DOWNLOAD_URL: https://www.zlib.net/zlib-1.2.11.tar.gz DOWNLOAD_FILE: zlib-1.2.11.tar.gz DOWNLOAD_SHA256: c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1 jobs: - job: 'ubuntu1604' pool: vmImage: 'ubuntu-16.04' steps: - template: build-zlib.yml - job: 'win2016_vs2017' pool: vmImage: 'vs2017-win2016' steps: - template: build-zlib.yml - job: 'macos1013' pool: vmImage: 'macOS-10.13' steps: - template: build-zlib.yml
できました!
まとめ
- Azure Pipelineでzlibをビルドすることができました。この調子で外部ライブラリをビルドすることができそうです。
- 複数環境で共通の処理はPowerShellを使うとよさそうです。
- ジョブを複数使って、複数環境のビルドを一度に行うことができました。
外部ライブラリのビルドができるようになったので、次回は、ビルド済みの外部ライブラリを本体のビルドで読み込むにはどうしたらいいのか調べてみたいと思います。
Azure PipelinesでCMakeを使ったC++ビルド
前回はAzure Pipelinesのビルド環境にCMakeがインストールされていることまで確認したので、実際にC++のコードをビルドしてみたいと思います。
まずはローカルでビルド
自動ビルドとはビルドコマンドを自動で叩いてくれるだけで、ビルドの設定まで自動でやってくれるわけではありません。まずは、ローカル環境でビルドが通るようにします。
KAGURAのビルド環境はWindowsとMacなのですが、勉強のため、ここでは敢えてUbuntu(WSL)環境でやってみます。
適当なC++のコードがあるとして、
// main.cpp #include <iostream> int main() { std::cout << "Hello" << std::endl; return 0; }
これをビルドするCMakeの設定ファイルはこんな感じです。
# CMakeLists.txt cmake_minimum_required (VERSION 3.0) project(hello) # main.cppからhelloという実行ファイルを作る add_executable(hello main.cpp) # 実行ファイルをbinディレクトリにインストールする install(TARGETS hello RUNTIME DESTINATION bin )
インストール先のディレクトリが相対パスの場合、変数CMAKE_INSTALL_PREFIXからの相対パスになります。
フォルダ構造はこのようにします。
- (source dir) |- CMakeLists.txt |- main.cpp |- build/ ← ビルドディレクトリ(作業ディレクトリ) |- Makefile ← CMakeによって生成される |- dist/ ← インストール先(CMAKE_INSTALL_PREFIX) |- bin/ |- hello ← インストールされた実行ファイル
CMakeを使ったビルドは、次の3ステップを実行することになります。
- プロジェクトファイルの生成
- ビルドの実行
- ビルド結果を取得
1. プロジェクトファイルの作成
cmakeコマンドは基本的にはビルドディレクトリで実行します。
$ mkdir build $ cd build $ cmake -DCMAKE_INSTALL_PREFIX=../dist ../
-D
オプションでCMAKE_INSTALL_PREFIX(インストール先)を指定、最後の引数はCMakeLists.txtファイルのある場所を指定します。
cmake -DINSTALL_PREFIX={インストール先} {ソースディレクトリ}
このコマンドでビルドディレクトリにMakefieが生成されます。
2. ビルドの実行
生成されたMakefileを叩くために、make
コマンドを実行してもいいのですが、より汎用的な方法があります。
$ cmake --build ./ --target install
これはmake install
を実行するのと同じですが、Visual StudioやXcodeのプロジェクトファイルであっても同じように機能するので便利です。
cmake --build {ビルドディレクトリ} --target {ターゲット名}
3. ビルド結果の取得
ビルドが成功すると、インストール先に実行ファイルがコピーされています。
$ ../dist/bin/hello => hello
パイプラインでビルド
上と同じことをパイプラインで実行するためのazure-pipelines.ymlは次のようになります。
# azure-pipelynes.yml trigger: - master pool: vmImage: 'ubuntu-16.04' steps: - task: CMake@1 inputs: cmakeArgs: > -DCMAKE_INSTALL_PREFIX=$(Build.ArtifactStagingDirectory) $(Build.SourcesDirectory) - task: CMake@1 inputs: cmakeArgs: > --build ./ --target install - task: PublishBuildArtifacts@1 inputs: pathtoPublish: $(Build.ArtifactStagingDirectory) artifactName: out
3つのタスクのうち、上の2つがCMakeタスクです。詳しくはこちら CMake build and release task - Azure Pipelines | Microsoft Docs
cmakeコマンドは、作業ディレクトリがビルドディレクトリになります。Azure PipelinesのCMakeタスクでは、何も指定しないと、$(Build.SourcesDirectory)\build
が作業ディレクトリになります。
CMAKE_INSTALL_PREFIX(インストール先)には$(Build.ArtifactStagingDirectory)
を指定しました。Artifacts(生成物)をまとめるために用意されているディレクトリのようです。
Artifactsを出力するためのタスクがPublishBuildArtifactsです。詳しくはこちら Publish Build Artifacts task - Azure Pipelines | Microsoft Docs
azure-pipelines.ymlを編集してmasterにコミットすると、パイプラインが実行されます。成功すると、ビルドログの右上の「Artifacts」ボタンからダウンロードできます。
WindowsおよびMacの場合
CMakeはクロスプラットフォームなので、基本的にはそのままWindowsやMac環境に持っていくことができます。azure-pipelines.ymlのvmImage
を書き換えるだけです。
# azure-pipelines.yml(変更) Windows環境を使用 pool: vmImage: 'vs2017-win2016'
# azure-pipelines.yml(変更) Mac環境を使用 pool: vmImage: 'macOS-10.13'
実行すると、それぞれの環境の実行ファイルができました。 あっさり。
まとめ
Azure Pipelines上で、Windows、Mac、ついでにUbuntu環境でCMakeを使ったC++のビルドを実行することができました。どこに落とし穴があるか分からないので、慎重にUbuntu環境から進めてみましたが、何にも問題ありませんでした。
C/C++でもCI/CDしたい!
CI/CD
CI/CD(継続的インテグレーションと継続的デリバリー)とは、ソフトウェア開発の作業単位を小さくし、頻繁に統合・ビルド・テストをして常にリリースできる状態に保つことです。開発の無駄を省き、ユーザーに価値を届けることを大切にする、いまどきの開発のあるべき姿です。
しかしながら、現在のKAGURA開発ではこれが実践できているとは言い難い状態です。自動ビルドの仕組みがないため、リリースのための準備に時間がかかったり、人的ミスが起こるリスクがあったりして、時間とリソースに無駄が生じてしまいます。
KAGURAにCI/CDを取り入れるのは、簡単ではありません。 開発言語がC++であるため、
- 今流行りの言語やフレームワークに比べて情報が少ない
- OS・プラットフォームごとにバイナリの互換性がない
- ビルドに時間がかかる
という問題があります。近年、CI/CDがトレンドになっても、KAGURAには関係ないことだと思っていました。
しかし、この先もKAGURAを進化させていくために、CI/CDを導入したい!
C++でもモダンな開発スタイルを手に入れるんだ!!
と意気込んでいる今日この頃。
まずは自動ビルド
KAGURAのビルドに必要な条件は以下のようなものです。
これを満たす自動ビルド環境をできるだけ簡単に構築する方法を探しています。
Azure DevOpsを使ってみる
Azure DevOpsでプロジェクトを作る
適当に名前をつけて、プロジェクトを作ります。
gitリポジトリの作成
Azure Reposはプライベートなgitを無制限に作れます。
これまた適当な名前でgitリポジトリを作ります。
パイプラインの作成
Azure Pipelinesは、ビルドなどのタスクを自動で実行してくれるものです。今回の話のメインです。
ビルドするコードの場所を指定します。 先ほど作ったAzure Reposを選択します。さすが、GitHubにも対応してます。
testリポジトリを選択します。
パイプラインのテンプレートを選択します。ここでは、minimalなパイプラインである「Starter pipeline」を選択してみます。
azure-pipeline.ymlが表示されました。どうやらこれが、パイプラインの設定ファイルのようです。
# azure-pipeline.yml(一部) steps: - script: echo Hello, world! displayName: 'Run a one-line script'
azure-pipeline.ymlでは、steps
に実行したいタスクを順に記述します。そのうちの一つ、script
は任意のコマンドをコマンドライン(シェル)で実行するタスクです。
「Save and run」ボタンを押して確かめてみましょう。
パイプラインが実行され、ログが表示されます。 確かに、サーバー上でechoコマンドが実行されています。
CMakeを使ってみる
早速、僕の大好きなCMakeが使えるのかどうか試してみます。
# azure-pipeline.yml(追加) - script: cmake --version
とりあえず、コマンドライン上でcmake
コマンド(バージョンを表示)を叩いてみます。
「Save and run」を実行。
おぉー、CMakeのバージョンが表示されました!
いや、ちょっと待て、喜ぶのはまだ早い。
# azure-pipeline.yml(一部) pool: vmImage: 'Ubuntu-16.04'
vmImage
に書いてある通り、これはUbuntuの環境で実行したものです。僕がやりたいのはWindowsおよびMacでビルドをすること。
Windows(Visual Studio)環境を使ってみる
こちらのページに書いてある通り、ビルドを実行する環境を選ぶことができます。
# azure-pipeline.yml(変更) pool: vmImage: 'vs2017-win2016'
'vs2017-win2016'
、つまりWindows Saver 2016にVisual Studio 2017がインストールされた環境を使ってみます。
「Save and run」を実行。
おぉ!バージョンが少し違いますが、CMakeコマンドを実行できました!
macOS(Xcode)環境を使ってみる
# azure-pipeline.yml(変更) pool: vmImage: 'macOS-10.13'
「Save and run」を実行。 おぉぉぉぉぉ!ちゃんとCMakeが入ってる!
まとめ
- Azure Pipelinesはgitリポジトリと連携して自動ビルドできる
- パイプラインの設定は、azure-pipelines.ymlを編集する
- WindowsおよびMac環境でCMakeを使うことができる
僕はCI/CD初心者ですが、Azure Pipelinesはとても分かりやすかったです。ここまで試してみて、つまづくことはほとんどありませんでした。
それに、Microsoftが用意したWindows環境にCMakeがデフォルトで入っていたり、macOSの環境がクラウドで使えたりするのって、凄くないですか?いつの間にそんな時代になっていたんですか???
KAGURAの開発で使えるかどうかはまだ分かりませんが、Azure DevOpsすごい。
次は実際にC++のビルドができるのか試してみたいと思います。
補足
azure-pipelines.ymlの詳しい仕様はこちら YAML schema - Azure Pipelines | Microsoft Docs
今回はとりあえず、コマンドラインでcmakeコマンドを実行したけど、CMake用のタスクがあるみたい。 CMake build and release task - Azure Pipelines | Microsoft Docs
CMakeの便利機能 configure
「こんなことできたらいいな」というとき、実装自体はそんなに難しくなくても、製品に組み込むとなるとそう簡単ではないことがあります。本当に有用なものか自信がなかったり、バージョン間の互換性を考えると慎重にならざるを得なかったり・・・。
とりあえず実装してみて、使うかどうかは後から決めたい。そんなとき、CMakeの機能が使えます。
configure
同じソースコードで機能XYZの有効/無効を切り替えたい場合、機能XYZを実装しているところを#ifdef~#endifで囲ったりします。
// main.cpp #include <iostream> #include "Config.h" int main() { #ifdef ENABLE_FEATURE_XYZ std::cout << "xyz" << std::endl; #endif std::cout << "Hello" << std::endl; return 0; }
こうすれば、ENABLE_FEATURE_XYZ
というマクロを定義するかどうかで、挙動を変えられます。
ここでは、その定義をConfig.hというファイルに分けることにします。
// Config.h #define ENABLE_FEATURE_XYZ
機能XYZを有効にするかどうか、ビルドする人の気分(?)で簡単に変えられたら便利です。そこで、Config.hの生成をCMakeにお願いします。
# CMakeLists.txt cmake_minimum_required (VERSION 3.0) project(hello) # ビルドする人が設定できるオプションを追加する option(ENABLE_FEATURE_XYZ "機能XYZを有効にする" ON) # Config.h.inからConfig.hを生成する configure_file( "${PROJECT_SOURCE_DIR}/Config.h.in" "${PROJECT_BINARY_DIR}/Config.h" ) # 生成されたConfig.hをC++ソースから読み込むため include_directories("${PROJECT_BINARY_DIR}") add_executable(hello main.cpp "${PROJECT_BINARY_DIR}/Config.h" )
CMakeで「Configure」を実行すると、追加したオプションのチェックボックスが表示されます。
Config.h.inはCMake独特の形式で、以下のようにします。
// Config.h.in
#cmakedefine ENABLE_FEATURE_XYZ
CMakeで「Generate」を実行すると、Config.hが生成されます。チェックのOn/OffによってConfig.hの結果が変わります。
ヘッダーファイルはコンパイルしないので、add_executableコマンドにConfig.hを追加する必要はありませんが、追加するとVisual Studioのファイル一覧に表示されるので便利です。
最初はフォルダ構造を理解するのが難しいかもしれません。PROJECT_SOURCE_DIRとPROJECT_BINARY_DIRの違いに注意してください。
- source <== PROJECT_SOURCE_DIR
|- CMakeLists.txt
|- main.cpp
|- Config.h.in
|- build/ <== PROJECT_BINARY_DIR ・・・自分で設定した任意の場所。sourceの中とは限らない
|- hello.sln (プロジェクトファイルなど)
|- Config.h
何が嬉しいのか?
なぜこんな回りくどいことをするのでしょうか。Config.hを直接変更したらいいのでは?
一時的なビルドの設定のために、いちいちソースコードを変更していると、他の変更と一緒にgitなどにコミットしてしまうかもしれません。Config.hを自動生成すれば、ソースコードの変更履歴を汚さずに済みます。
Unix系の環境では、そのためにconfigureスクリプトが用意されていたり、autotools系のツールを使ったりすることが多いですが、CMakeを使うと、Windows(Visual Studio)環境でも同じようなことができるようになります。
画像処理あるある① カメラに映った自分の顔色が悪すぎてびっくりする
Webカメラの画像に対して何らかの画像処理を行うプログラムを書いていると、うっかり自分の顔がこの世のものとは思えないものになっていることがあります。
これは、画像成分Red・Blue・Greenの並び順(RGBとBGR)を間違えたために起こります。
Windowsで非圧縮24bitビットマップといえば、BGRです。
//Webカメラの画像フォーマットを指定 AM_MEDIA_TYPE amt; ZeroMemory(&amt, sizeof(amt)); amt.majortype = MEDIATYPE_Video; amt.subtype = MEDIASUBTYPE_RGB24;
RGBという表記にもかかわらず、メモリ上はBGRの順です。これをRGBだと思って表示すると、赤いものが青、青いものが赤になってしまいます。
OpenCVの場合
OpenCVのcvtColor
関数を使って、BGRをRGBに変換します。
/* imgproc.hppの定義 COLOR_BGR2RGBもCOLOR_RGB2BGRも同じ enum ColorConversionCodes { ... COLOR_BGR2RGB = 4, COLOR_RGB2BGR = COLOR_BGR2RGB, ... }; */ cv::cvtColor(src, dst, cv::COLOR_BGR2RGB);
OpenGLの場合
OpenGLで描画する場合、メモリ上ではBGRのまま処理し、OpenGLのテクスチャに渡すときに、formatがBGRであることを指定する方法もあります。メモリ上でRGBに変換する必要がないならば、この方が高速です。
glTexImage2D(GL_TEXTURE_2D, // target 0, // level GL_RGB8, //internalformat width, height, 0, // border GL_BGR, // format <-- RGBのときはGL_RGB GL_UNSIGNED_BYTE, // type data);
KAGURAでは、メモリ上にRGBとBGRが混在するとややこしくなるので、前者のOpenCVを使って、RGBに統一することにしています。
RGBとBGRのどちらが良いのか。RGBとBGRで、OpenGLのパフォーマンスが変わるのか検証したことがありますが、どちらも描画のパフォーマンスは変わりませんでした。
KAGURAを支える技術① CMake
クロスプラットフォーム
KAGURAはWindows版とMac版があり、同時に開発されています。 開発チームはとても小規模なので、効率的に開発を進める必要があります。 言語は主にC++です。
WindowsでもMacでも動くビルド環境を作ろうとした場合、1つはGCCとmakeのようなUnix系のツールを使う方法が考えられます。WindowsではMinGWというGCCの移植版、MacではXcodeのCommand Line Toolsをインストールすれば、これらのツールが使えます。
しかし、僕の経験上、OS固有の機能や新しい機能を使ったり、デバイスをできるだけ低レベルで扱いたい場合、OSの開発元が提供するツールを使うのが無難であることが多いのです。 比較の結果、KAGURAの特性上、Visual StudioおよびXcodeに軍配が上がります。ところが、この方法ではVisual Studio、Xcodeそれぞれでプロジェクトを作る必要があります。ソースコードを追加したり削除したり、コンパイルの設定を変えたりする度に、両方のプロジェクトファイルを変更するのは辛いものです。C++のソースコードが増えれば増えるほど、プロジェクトファイルの管理は面倒になります。1カ所設定を変更すれば、Windows環境でもMac環境でも変更されてほしい!そこで役に立つのがCMakeです。
他のmake代替ツールであるSConsやRakeなどと異なり、CMakeはビルド設定ファイルから直接ビルドするのではなく、各ビルド環境用のプロジェクトファイルを作るのが仕事で、実際のビルドはVisual StudioやXcodeが行います。このため、ビルド設定ファイルは共通でありながら、それぞれのプラットフォームの強力なツールのメリットを生かすことができるのです。
CMakeの使い方
何か適当なC++のコード(main.cpp)があるとします。
#include <iostream> int main() { std::cout << "Hello" << std::endl; return 0; }
これをビルドするための最小の設定ファイルはこんな感じ。CMakeLists.txtという名前のテキストファイルを作成します。
project(hello) add_executable(hello main.cpp)
CMakeを起動し、ソースコードのディレクトリ(CMakeLists.txtのある場所)とビルドディレクトリを指定し、「Configure」ボタンを押します。 ビルドに使用するツール(Visual Studio、Xcodeなど)を選択します。 「Generate」ボタンを押します。 Visual Studioのソリューション(.sln)ができるので、開いてビルドしたら・・・ ビルド完了です。