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でやるべきと思いました。