Artifactory / Conan を使った C++ のパッケージ管理

<!-- Artifactory / Conan を使った C++ のパッケージ管理 -->

# はじめに

社内で開発しているプログラムのソースコードは GitHub や Bitbucket を使ってバージョン管理をすることが多いと思いますが、C++ などのプログラムをコンパイルしてできる成果物のライブラリについても同様にバージョン管理をしたくなることがあります。その際に弊社がどのようなやり方でそれを実現しているのかについて、まとめてみようと思います。

# ライブラリのバージョン管理

ライブラリのバージョン管理を行う際は概ね次のような要件を満たす必要があります。

1. バージョン番号に紐づけてライブラリをリポジトリに登録・取得ができること
1. ライブラリを取得する際、それが依存するライブラリも同時に取得ができること
1. 各言語のパッケージマネージャと連携が取れること

オープンソースにおける Python, Java 開発の場合、これらの要件は下記のように整理されています。

| 言語   | 成果物 | リポジトリ       | パッケージマネージャ |
|--------|--------|------------------|----------------------|
| Python | wheel  | PyPI             | pip                  |
| Java   | JAR    | Maven Repository | Maven                |

成果物リポジトリはプログラム言語だけでなく OS のパッケージ管理でも利用されています。

| ツール/OS | 成果物           | リポジトリ          | パッケージマネージャ |
|-----------|------------------|---------------------|----------------------|
| CentOS    | rpm              | 各種 rpm リポジトリ | yum                  |
| Ubuntu    | deb              | 各種 deb リポジトリ | apt                  |

オープンソースであればこれらを使用すればよいですが、社内限定で公開したいライブラリの場合は下記のような問題を考える必要があります。

- プライベートリポジトリを作りたい
- そもそも C++ にはデファクトの成果物リポジトリとパッケージマネージャがない

以前は成果物リポジトリを使ってなかったため、ライブラリの配布方法がチームによってまちまちになってしまい、共有しづらいという問題が起こっていました。このような問題を解消するために JFrog 社が提供している [Artifactory] を導入することにしました。

# Artifactory とは

Artifactory は成果物をバージョン管理するためのリポジトリが作れるツールです。各言語のパッケージマネージャとも連携が可能で Python 向けのリポジトリを作り、pip で取得するといったことが可能になります。また JFrog 社は C++ のライブラリも Artifactory で管理できるように [Conan] という C++ 向けのパッケージマネージャを開発しているため、C++ 開発でも問題なく使用できます。

Artifactory は有料版と無料版がありますが、無料版で作れるのは C++, Java 用のリポジトリだけになります。以下で Artifactory の構築方法と Conan を使ったパッケージの作り方について簡単に説明します。

# Artifactory のインストール

Artifactory は Docker イメージが提供されているので試しにコンテナを使って構築してみます。下記の通り、使用できる言語ごとにコンテナイメージが分かれています。

```shell:C++用のArtifactoryを構築する場合
$ docker run --name artifactory -d -p 8081:8081 docker.bintray.io/jfrog/artifactory-cpp-ce
```

```shell:Java用のArtifactoryを構築する場合
$ docker run --name artifactory -d -p 8081:8081 docker.bintray.io/jfrog/artifactory-oss
```

今回は C++ 用の Artifactory を使用することにします。起動には少し時間がかかるため、コンテナのログを確認しておくといいでしょう。

```shell
$ docker logs -f artifactory
...
2019-09-22 07:27:37,809 [art-init] [INFO ] (o.j.s.c.EncryptionWrapperFactory:33) - createArtifactoryKeyWrapper  EncryptionWrapperBase{ encodingType=ARTIFACTORY_MASTER, topEncrypter=BytesEncrypterBase{ Cipher='AES128',  keyId='33jpX'}, formatUsed=DotFormat, decrypters=[BytesEncrypterBase{ Cipher='AES128',  keyId='33jpX'}]}
2019-09-22 07:27:37,840 [art-init] [INFO ] (o.a.s.a.AccessServiceImpl:1590) - Successful register of Artifactory serviceId jfrt@01dnbxwq6mpcga0f7wdng10pmk in Access Federation
2019-09-22 07:27:37,859 [art-init] [INFO ] (o.a.w.s.ArtifactoryContextConfigListener:215) -
###########################################################
### Artifactory successfully started (13.921 seconds)   ###
###########################################################

2019-09-22 07:27:38,002 [art-exec-3] [INFO ] (o.a.c.CentralConfigServiceImpl:615) - Reloading configuration... old revision 0, new revision 1
2019-09-22 07:27:38,049 [art-exec-3] [INFO ] (o.a.c.CentralConfigServiceImpl:352) - Force updating new descriptor (old descriptor doesn't support diff). Old descriptor revision 0
2019-09-22 07:27:38,057 [art-exec-3] [INFO ] (o.a.c.CentralConfigServiceImpl:374) - New configuration with revision 1 saved.
2019-09-22 07:27:38,057 [art-exec-3] [INFO ] (o.a.s.ArtifactoryApplicationContext:515) - Artifactory application context set to NOT READY by reload
2019-09-22 07:27:38,185 [art-exec-3] [INFO ] (o.a.s.ArtifactoryApplicationContext:515) - Artifactory application context set to READY by reload
2019-09-22 07:27:38,185 [art-exec-3] [INFO ] (o.a.c.CentralConfigServiceImpl:633) - Configuration reloaded.
```

上記のようなログを確認したら http://localhost:8081 にアクセスすることで Web UI が表示されます。Web UI にアクセスしたら管理者アカウントを求められるので下記を入力してください。

| ユーザ名 | パスワード |
|----------|------------|
| `admin`  | `password` |

ログインするとダイアログが表示されるので下記のように入力します。

1. `Welcome to JFrog Artifactory!` : Next をクリック
1. `Configure a Proxy Server` : Skip をクリック
1. `Create Repositories` : Conan を選択して Create をクリック
1. `Artifactory on-boarding complete!` : Finish をクリック

これで Artifactory の構築は完了です。ダイアログのフローで `conan-local` という Conan のリポジトリが作成されます。

# Conan のインストール

続いて Conan をインストールします。Conan は Python で書かれているため pip でインストールします。

```shell
$ pip install conan
```

Conan が Artifactory にアクセスできるように Artifactory の URL を登録します。

```shell
$ conan remote add conan-local http://localhost:8081/artifactory/api/conan/conan-local
```

# パッケージを作成する

下記のような C++ プログラムを Conan でパッケージにしてみます。

```cpp:hello.h
#ifndef HELLO_H_
#define HELLO_H_

void Hello();

#endif  // HELLO_H_
```

```cpp:hello.cpp
#include "hello.h"

#include <iostream>

void Hello() {
    std::cout << "Hello, World!" << std::endl;
}
```

Conan の設定ファイルを作成します。

```shell
$ conan new libhello/1.0.0
```

`libhello/1.0.0` というのはパッケージ名が `libhello` でバージョンが `1.0.0` のパッケージを作成するという意味になります。

`conanfile.py` というファイルが生成されるので、下記のように編集します。

```python:conanfile.py
from conans import ConanFile


class LibhelloConan(ConanFile):
    name = "libhello"
    version = "1.0.0"
    settings = "os", "compiler", "build_type", "arch"

    def package(self):
        self.copy("*.h", dst="include")
        self.copy("*.a", dst="lib", keep_path=False)
```

編集が終わったら下記コマンドでパッケージを作成します。

```shell
$ g++ -c hello.cpp
$ ar rcs libhello.a hello.o
$ conan export-pkg . admin/stable
```

パッケージは `~/.conan/data/libhello/1.0.0/admin/stable/package/<hash>` 配下に作成されます。下記コマンドでもパッケージの存在を確認できます。

```shell
$ conan search libhello
Existing package recipes:

libhello/1.0.0@admin/stable
```

`admin/stable` というのはそれぞれ次のような値になります。

| 項目     | 意味                                                                     |
|----------|--------------------------------------------------------------------------|
| `admin`  | パッケージを作成するときのユーザ名                                       |
| `stable` | チャンネル名（stable, testing などのパッケージの属性を示す任意の文字列） |

ユーザ名は本来であれば `admin` ではなく新規にユーザを作成したほうがいいです。作成方法については [Artifactory ドキュメント] を参考にしてください。

# パッケージをアップロードする

パッケージができたらそれを Artifactory にアップロードしてみます。アップロードする前に Artifactory にはまだパッケージが存在してないことを確認しておきます。

```shell
$ conan search -r conan-local libhello
There are no packages matching the 'libhello' pattern
```

下記コマンドで Artifactory にアップロードします。

```shell
$ conan user -p password -r conan-local admin                   # ユーザを admin にする
$ conan upload libhello/1.0.0@admin/stable -r conan-local --all # パッケージをアップロード
```

成功したら Artifactory の左のアイコンから `Artifacts` を選択して `conan-local` 内のツリーを見ると `libhello` パッケージが登録されていると思います。`conan search` コマンドでも確認できます。

```shell
$ conan search -r conan-local libhello
Existing package recipes:

libhello/1.0.0@admin/stable
```

# パッケージを使用する

`libhello` パッケージを使用する新たな `conanfile.py` を作成します。

```shell
$ conan new hello/1.0.0
```

`conanfile.py` を下記のように編集します。

```python:conanfile.py
from conans import ConanFile


class HelloConan(ConanFile):
    name = "hello"
    version = "1.0.0"

    requires = (
        "libhello/1.0.0@admin/stable",
    )

    def imports(self):
        self.copy("*.h", src="include", dst="include")
        self.copy("*.a", src="lib", dst="lib")
```

`requires` フィールドに依存パッケージを記述します。ただし依存パッケージを書いてもパッケージの取得先は `~/.conan` 配下になるので、ワークスペースにコピーするために `imports()` という関数でコピーしておく必要があります（ちょうど `package()` と逆の挙動になります）。

`conanfile.py` を作成したら `libhello` パッケージをインストールします。

```shell
$ conan install .
```

これで `include, lib` 配下にヘッダとライブラリが取り込まれます。次に下記のようなソースコードを用意します。

```cpp:main.cpp
#include <hello.h>

int main() {
    Hello();
    return 0;
}
```

これをコンパイルすれば `Hello, World!` と出力されるプログラムができます。

```shell
$ g++ -Iinclude -Llib main.cpp -lhello -o hello
$ ./hello
Hello, World!
```

# さいごに

今回はコンパイルは `g++` を直接実行しましたが Conan は CMake と連携してコンパイルを行うこともできるため、プロジェクトを CMake で管理しておくことをおすすめします。

Artifactory / Conan を導入することで C++ 開発環境が劇的に改善できるのでぜひお試しください。もちろん他言語のパッケージ管理を目的に Artifactory を導入するのもおすすめです。弊社では JAR パッケージの管理でも Artifactory を導入しています。

[Artifactory]: https://jfrog.com/artifactory/
[Artifactory ドキュメント]: https://www.jfrog.com/confluence/display/RTF/Managing+Users
[Conan]: https://conan.io/