KAGURA実験室

KAGURA開発者の個人ブログ

Azure ArtifactsでC++ライブラリのパッケージ管理

前回はパイプラインを使ってライブラリを複数ターゲットにビルドしました。今回は、ビルドしたライブラリをメインのプログラムで使用するにはどうしたらいいかを考えます。

KAGURAでは、たくさんの外部ライブラリが使用されていますが、中でも、boost、OpenCVwxWidgetsといった大きなライブラリはビルドに10分以上かかることもあり、全部のライブラリをビルドすると大変なことになります。

ライブラリを毎回ビルドする必要は無いので、ビルドしたものをパッケージにしてどこかに置いておき、本体のビルドのときに利用することで、ビルド時間を短縮したいです。

ライブラリは数カ月に1回くらいの頻度でバージョンアップします。バージョン間で互換性が無いこともあるので、ライブラリを利用するプログラム側は、どのバージョンを利用するのかを明確に指定して、正しいバージョンを取得する必要があります。

Azure DevOpsでこのようなことを実現するのに使えるのが、Azure Artifactsです。

docs.microsoft.com

Artifactsは「Maven、npm、NuGetのパッケージを作成、共有可能」とあります。MavenJava、npmはNode.js、NuGetは.NET用のパッケージ管理システムです。

C++はどうしたらいいの・・・?

よく見ると、Universal Packageという見慣れないものがありました。

Universal Packageはファイルの種類に関係なく、何でもパッケージにできるもののようです。C++のライブラリはUniversal Packageにすると良さそうです。

f:id:nkshigeru:20190131080028p:plain

Universal Packageについては、Qiitaにもまとめてみました。

qiita.com

Artifactsにフィードを作成

適当にtestfeedという名前を付けました。

f:id:nkshigeru:20190131142856p:plain

最初はフィードに何もない状態です。

f:id:nkshigeru:20190131143058p:plain

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

成功するとフィードに追加されます。 f:id:nkshigeru:20190129231552p:plain パッケージのバージョンを指定しなかったので、バージョン番号は自動で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

ビルドを実行すると・・・ f:id:nkshigeru:20190131000231p:plain

# 実行結果
> ./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++のコードを複数環境向け(UbuntuWindowsmacOS)にビルドできることまで確認しました。

複数のエージェントがMicrosoftによって用意されていて、設定ファイルを変更するだけで、ビルド環境を変えることができましたが、ビルドのたびに設定ファイルを書き換えるわけにはいかないので、一度に複数の環境でビルドを走らせてほしいです。

f:id:nkshigeru:20190125133515p:plain

今回は、zlibをビルドしてみます。なぜzlibかというと、KAGURAでも使用しているライブラリで、CMakeに対応し、比較的小さいライブラリであり、zlibほど多くの人に使われているライブラリのビルドが通らないなんてことは考えにくいからです。

ローカルでビルド

まずは、ローカルのUbuntu(WSL)環境でビルドできることを確認しておきます。ビルド手順はこのような流れになります。

  1. zlibソースコードのダウンロード
  2. ダウンロードファイルのチェック
  3. ファイルの展開
  4. CMakeの実行
  5. ビルド
  6. テスト
  7. インストール

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

これを実行すると、できました!

f:id:nkshigeru:20190124230625p:plain
Ubuntu環境のビルド結果

さて、Windows環境でビルドを実行するには、設定ファイルのvmImagevs2017-win2016変更するだけ・・・と言いたいところですが、問題が発生しました。 f:id:nkshigeru:20190125012145p:plain

shasumコマンドなんて無いと。そりゃそうです、Windowsなんだもの。その前のcurlが通っていることのほうがびっくりです。

そこで、PowerShellを使ってみます。 curlInvoke-WebRequestに置き換えます。PowerShellにおけるcurlInvoke-WebRequestエイリアスであり、bashcurlとは挙動が違うので、そのまま使えません。shasumGet-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は今やUbuntumacOSでも使えるのです。

複数環境で一括ビルド

ここからが今日の本題です。vmImageを毎回書き換えずに、複数環境のビルドを実行するにはどうしたらいいでしょうか。

f:id:nkshigeru:20190125140440p:plain

パイプラインは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

できました! f:id:nkshigeru:20190125001805p:plain

まとめ

  • Azure Pipelineでzlibをビルドすることができました。この調子で外部ライブラリをビルドすることができそうです。
  • 複数環境で共通の処理はPowerShellを使うとよさそうです。
  • ジョブを複数使って、複数環境のビルドを一度に行うことができました。

外部ライブラリのビルドができるようになったので、次回は、ビルド済みの外部ライブラリを本体のビルドで読み込むにはどうしたらいいのか調べてみたいと思います。

Azure PipelinesでCMakeを使ったC++ビルド

前回はAzure Pipelinesのビルド環境にCMakeがインストールされていることまで確認したので、実際にC++のコードをビルドしてみたいと思います。

まずはローカルでビルド

自動ビルドとはビルドコマンドを自動で叩いてくれるだけで、ビルドの設定まで自動でやってくれるわけではありません。まずは、ローカル環境でビルドが通るようにします。

KAGURAのビルド環境はWindowsMacなのですが、勉強のため、ここでは敢えて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. プロジェクトファイルの生成
  2. ビルドの実行
  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 StudioXcodeのプロジェクトファイルであっても同じように機能するので便利です。

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」ボタンからダウンロードできます。 f:id:nkshigeru:20190120231229p:plain

WindowsおよびMacの場合

CMakeはクロスプラットフォームなので、基本的にはそのままWindowsMac環境に持っていくことができます。azure-pipelines.ymlのvmImageを書き換えるだけです。

# azure-pipelines.yml(変更) Windows環境を使用
pool:
  vmImage: 'vs2017-win2016'
# azure-pipelines.yml(変更) Mac環境を使用
pool:
  vmImage: 'macOS-10.13'

実行すると、それぞれの環境の実行ファイルができました。 あっさり。

まとめ

Azure Pipelines上で、WindowsMac、ついでに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のビルドに必要な条件は以下のようなものです。

  • 言語は主にC++
  • WindowsmacOSのネイティブアプリ
  • CMakeを使いたい

これを満たす自動ビルド環境をできるだけ簡単に構築する方法を探しています。

Azure DevOpsを使ってみる

azure.microsoft.com

Azure DevOpsでプロジェクトを作る

適当に名前をつけて、プロジェクトを作ります。

gitリポジトリの作成

f:id:nkshigeru:20190118233959p:plain Azure Reposはプライベートなgitを無制限に作れます。

f:id:nkshigeru:20190118005621p:plain これまた適当な名前でgitリポジトリを作ります。

パイプラインの作成

f:id:nkshigeru:20190118010017p:plain

Azure Pipelinesは、ビルドなどのタスクを自動で実行してくれるものです。今回の話のメインです。

f:id:nkshigeru:20190118010625p:plain ビルドするコードの場所を指定します。 先ほど作ったAzure Reposを選択します。さすが、GitHubにも対応してます。

f:id:nkshigeru:20190118010825p:plain testリポジトリを選択します。

f:id:nkshigeru:20190118012437p:plain パイプラインのテンプレートを選択します。ここでは、minimalなパイプラインである「Starter pipeline」を選択してみます。

f:id:nkshigeru:20190118012621p:plain 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」ボタンを押して確かめてみましょう。 f:id:nkshigeru:20190118014714p:plain

パイプラインが実行され、ログが表示されます。 f:id:nkshigeru:20190118014937p:plain 確かに、サーバー上でechoコマンドが実行されています。

CMakeを使ってみる

早速、僕の大好きなCMakeが使えるのかどうか試してみます。

# azure-pipeline.yml(追加)

- script: cmake --version

とりあえず、コマンドライン上でcmakeコマンド(バージョンを表示)を叩いてみます。 f:id:nkshigeru:20190118225539p:plain 「Save and run」を実行。

f:id:nkshigeru:20190118230820p:plain おぉー、CMakeのバージョンが表示されました!

いや、ちょっと待て、喜ぶのはまだ早い。

# azure-pipeline.yml(一部)
pool:
  vmImage: 'Ubuntu-16.04'

vmImageに書いてある通り、これはUbuntuの環境で実行したものです。僕がやりたいのはWindowsおよびMacでビルドをすること。

Windows(Visual Studio)環境を使ってみる

https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=vsts&tabs=yaml#use-a-microsoft-hosted-agent

こちらのページに書いてある通り、ビルドを実行する環境を選ぶことができます。

# azure-pipeline.yml(変更)
pool:
  vmImage: 'vs2017-win2016'

'vs2017-win2016'、つまりWindows Saver 2016にVisual Studio 2017がインストールされた環境を使ってみます。

f:id:nkshigeru:20190118232206p:plain 「Save and run」を実行。

f:id:nkshigeru:20190118232310p:plain おぉ!バージョンが少し違いますが、CMakeコマンドを実行できました!

macOS(Xcode)環境を使ってみる

# azure-pipeline.yml(変更)
pool:
  vmImage: 'macOS-10.13'

f:id:nkshigeru:20190118232809p:plain 「Save and run」を実行。 f:id:nkshigeru:20190118232955p:plain おぉぉぉぉぉ!ちゃんと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

f:id:nkshigeru:20190117001841j:plain
このイラストはフィクションであり、実在の人物とは関係ありません。一切関係ありません。

「こんなことできたらいいな」というとき、実装自体はそんなに難しくなくても、製品に組み込むとなるとそう簡単ではないことがあります。本当に有用なものか自信がなかったり、バージョン間の互換性を考えると慎重にならざるを得なかったり・・・。

とりあえず実装してみて、使うかどうかは後から決めたい。そんなとき、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」を実行すると、追加したオプションのチェックボックスが表示されます。

f:id:nkshigeru:20190103231944p:plain
ENABLE_FEATURE_XYZのチェックボックスをOn/Offできる

Config.h.inはCMake独特の形式で、以下のようにします。

// Config.h.in
#cmakedefine ENABLE_FEATURE_XYZ

CMakeで「Generate」を実行すると、Config.hが生成されます。チェックのOn/OffによってConfig.hの結果が変わります。

f:id:nkshigeru:20190104000629p:plain
ENABLE_FEATURE_XYZがOnのとき
f:id:nkshigeru:20190104000633p:plain
ENABLE_FEATURE_XYZがOffのとき

ヘッダーファイルはコンパイルしないので、add_executableコマンドにConfig.hを追加する必要はありませんが、追加するとVisual Studioのファイル一覧に表示されるので便利です。 f:id:nkshigeru:20190103235657p:plain

最初はフォルダ構造を理解するのが難しいかもしれません。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カメラの画像に対して何らかの画像処理を行うプログラムを書いていると、うっかり自分の顔がこの世のものとは思えないものになっていることがあります。 f:id:nkshigeru:20190105165856p:plain

これは、画像成分Red・Blue・Greenの並び順(RGBとBGR)を間違えたために起こります。

f:id:nkshigeru:20190105171307p:plain

Windowsで非圧縮24bitビットマップといえば、BGRです。

//Webカメラの画像フォーマットを指定
AM_MEDIA_TYPE amt;
ZeroMemory(&amt, sizeof(amt));
amt.majortype = MEDIATYPE_Video;
amt.subtype = MEDIASUBTYPE_RGB24;

RGBという表記にもかかわらず、メモリ上はBGRの順です。これをRGBだと思って表示すると、赤いものが青、青いものが赤になってしまいます。

OpenCVの場合

OpenCVcvtColor関数を使って、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の開発元が提供するツールを使うのが無難であることが多いのです。

f:id:nkshigeru:20190102122311p:plain
ビルドツール比較
比較の結果、KAGURAの特性上、Visual StudioおよびXcodeに軍配が上がります。ところが、この方法ではVisual StudioXcodeそれぞれでプロジェクトを作る必要があります。ソースコードを追加したり削除したり、コンパイルの設定を変えたりする度に、両方のプロジェクトファイルを変更するのは辛いものです。C++ソースコードが増えれば増えるほど、プロジェクトファイルの管理は面倒になります。1カ所設定を変更すれば、Windows環境でもMac環境でも変更されてほしい!そこで役に立つのがCMakeです。

f:id:nkshigeru:20190102035612p:plain
CMakeでVisual StudioXcodeのプロジェクトを作成する

他のmake代替ツールであるSConsやRakeなどと異なり、CMakeはビルド設定ファイルから直接ビルドするのではなく、各ビルド環境用のプロジェクトファイルを作るのが仕事で、実際のビルドはVisual StudioXcodeが行います。このため、ビルド設定ファイルは共通でありながら、それぞれのプラットフォームの強力なツールのメリットを生かすことができるのです。

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」ボタンを押します。 f:id:nkshigeru:20190102020443p:plain ビルドに使用するツール(Visual StudioXcodeなど)を選択します。 f:id:nkshigeru:20190102020807p:plain 「Generate」ボタンを押します。 f:id:nkshigeru:20190102021939p:plain Visual Studioのソリューション(.sln)ができるので、開いてビルドしたら・・・ f:id:nkshigeru:20190102024149p:plain ビルド完了です。

https://cmake.org