Conan C/C++ Package Managerでzmqppのpackageを作ってみた

# はじめに

[前回の投稿](http://qiita.com/gasuketsu/items/6bea2465f0425c4cf5ab)に引き続き、Conan C/C++ Package Managerを使ってできることを紹介します。

ZeroMQのC++ bindingの1つであるzmqppのpackageがなかったため、Conanで利用できるpackageを自作し、conan.ioにuploadするところまで行いました。今回はその過程をまとめてみました。

## package作成までの流れ

### recipeの作成

ゼロからすべてrecipeを書いてもいいのですが、Conanにはrecipeのテンプレートを準備してくれるコマンドが備わっているので利用してみます。

```shell-session
tharada@xubuntu:~/conan-zmqpp$ conan new zmqpp/4.1.2@gasuketsu/testing
File saved: conanfile.py
```

`conanfile.py` という名前のpythonファイルが作られました。このファイルがrecipeになります。templateとして作られた状態のrecipeの中身はこんなかんじになっています。

```py:conanfile.py
from conans import ConanFile, CMake, tools
import os


class ZmqppConan(ConanFile):
    name = "zmqpp"
    version = "4.1.2"
    license = "<Put the package license here>"
    url = "<Package recipe repository url here, for issues about the package>"
    settings = "os", "compiler", "build_type", "arch"
    options = {"shared": [True, False]}
    default_options = "shared=False"
    generators = "cmake"

    def source(self):
        self.run("git clone https://github.com/memsharded/hello.git")
        self.run("cd hello && git checkout static_shared")
        # This small hack might be useful to guarantee proper /MT /MD linkage in MSVC
        # if the packaged project doesn't have variables to set it properly
        tools.replace_in_file("hello/CMakeLists.txt", "PROJECT(MyHello)", '''PROJECT(MyHello)
include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()''')

    def build(self):
        cmake = CMake(self.settings)
        shared = "-DBUILD_SHARED_LIBS=ON" if self.options.shared else ""
        self.run('cmake hello %s %s' % (cmake.command_line, shared))
        self.run("cmake --build . %s" % cmake.build_config)

    def package(self):
        self.copy("*.h", dst="include", src="hello")
        self.copy("*hello.lib", dst="lib", keep_path=False)
        self.copy("*.dll", dst="bin", keep_path=False)
        self.copy("*.so", dst="lib", keep_path=False)
        self.copy("*.a", dst="lib", keep_path=False)

    def package_info(self):
        self.cpp_info.libs = ["hello"]
```

見ての通り、pythonで書かれた1つのclassという形でrecipeを定義しています。
[officialのドキュメント](http://docs.conan.io/en/latest/index.html)の "Creating packages" のセクションや "Reference" にそれぞれのメソッドやメンバー変数の説明がされていますが、大まかに説明すると以下のようになると思います。

|変数/メソッド|説明|
|:-----------|:------------------------------------------------------|
|`name`|このpackageの名前|
|`version`|packageのバージョン|
|`license`|packageのライセンス| 
|`options`|このpackageをインストールする時に指定可能なオプション (packaging するプロジェクトのbuild時に指定可能なオプションに応じてここで定義すると良いと思います)|
|`default_options`|各オプションのデフォルト値|
|`generators`|プロジェクトのbuildに必要な設定ファイルの出力形式|
|`source()`|packagingするプロジェクトのソースファイルの取得方法と、build開始前にやっておくこと(もしあれば)をこのメソッドで定義します。|
|`build()`|プロジェクトのbuild方法|
|`package()`|ライブラリやヘッダなど、packageに含めたいファイルとその格納先|
|`package_info()`|このpackageを利用する時に必要なライブラリやinclude pathなどの情報|

zmqppの内容に合わせてrecipeを編集した結果、以下のようになりました。

```py:conanfile.py
from conans import ConanFile, CMake, tools
import shutil
import os


class ZmqppConan(ConanFile):
    name = "zmqpp"
    description = "0mq 'highlevel' C++ bindings"
    version = "4.1.2"
    license = "MPLv2"
    url = "https://github.com/gasuketsu/conan-zmqpp"
    settings = "os", "compiler", "build_type", "arch"
    generators = "cmake", "txt", "env"
    exports = "CMakeLists.txt"
    options = {"shared": [True, False],
               "zmqpp_libzmq_cmake": [True, False],
               "zmqpp_build_examples": [True, False],
               "zmqpp_build_client": [True, False],
               "zmqpp_build_tests": [True, False]}
    default_options = '''
shared=False
zmqpp_libzmq_cmake=False
zmqpp_build_examples=False
zmqpp_build_client=False
zmqpp_build_tests=False
'''

    def requirements(self):
        self.requires("libzmq/4.1.5@memsharded/stable")

    def source(self):
       self.run("git clone https://github.com/zeromq/zmqpp.git")
       self.run("cd zmqpp && git checkout 4.1.2")
       shutil.move("zmqpp/CMakeLists.txt", "zmqpp/CMakeListsOriginal.cmake")
       shutil.copy("CMakeLists.txt", "zmqpp/CMakeLists.txt")

    def build(self):
        cmake = CMake(self.settings)
        cmake_options = []
        for option_name in self.options.values.fields:
            activated = getattr(self.options, option_name)
            the_option = "%s=" % option_name.upper()
            if option_name == "shared":
                the_option = "ZMQPP_BUILD_SHARED=ON" if activated else "ZMQPP_BUILD_SHARED=OFF"
            else:
                the_option += "ON" if activated else "OFF"
            cmake_options.append(the_option)
        options_zmqpp = " -D".join(cmake_options)
        conf_command = 'cd zmqpp/cmake_build && cmake .. %s -D%s' % (cmake.command_line, options_zmqpp)
        self.output.warn(conf_command)
        self.run("mkdir -p zmqpp/cmake_build")
        self.run(conf_command)
        self.run("cd zmqpp/cmake_build && cmake --build . %s" % cmake.build_config)

    def package(self):
        self.copy("*.hpp", dst="include/zmqpp", src="zmqpp/src/zmqpp")
        self.copy("*.lib", dst="lib", keep_path=False)
        self.copy("*.dll", dst="bin", keep_path=False)
        self.copy("*.so", dst="lib", keep_path=False)
        self.copy("*.a", dst="lib", keep_path=False)

    def package_info(self):
        self.cpp_info.libs = ["libzmqpp.so"] if self.options.shared else ["libzmqpp-static.a"]
```

今回の変更のポイントとしては、以下の2点です

* 今回packagingするzmqppのbuildにはlibzmqが必要となるため、 `requirements()` を追加し、ここでこのpackageが依存するlibzmqのpackageを定義しました。
* 依存packageのヘッダやライブラリを正しく引けるよう、zmqppのオリジナルの`CMakeLists.txt`
に手を加えて `conanbuildinfo.cmake` をincludeさせる必要があります。そこで、以下の `CMakeLists.txt` を recipeとセットで用意するようにし、`source()` で zmqppのプロジェクトのソースをclone後
 1. zmqppオリジナルの `CMakeLists.txt` を `CMakeListOriginal.cmake` にrename
 2. recipeとセットで用意した↓の `CMakeLists.txt` を zmqppオリジナルの `CMakeLists.txt`があった場所にコピー
という細工を施すことにしました。

```cmake:CMakeLists.txt
project(conanzmqpp)
cmake_minimum_required(VERSION 2.8)
include(${CMAKE_SOURCE_DIR}/../conanbuildinfo.cmake)
conan_basic_setup()

set(ZEROMQ_LIB_DIR ${CONAN_LIB_DIRS_LIBZMQ} CACHE PATH "libzmq path")
include("${CMAKE_SOURCE_DIR}/CMakeListsOriginal.cmake")
```

### recipeからbuild出来ることを確認する

作成したrecipeから、Conanを利用してソース取得～buildが期待通り出来ることを確認してみます。作成したrecipe (`conanfile.py`) が置いてあるディレクトリで次の3つを順に実行することで確認します。
1. `conan install` でrecipeから設定ファイルを生成
2. `conan source` でソース取得 (recipeで定義した `source()` の実行)
3. `conan build` でbuild実施 (recipeで定義した `build()` の実行)

```
tharada@xubuntu:~/repos/conan-zmqpp$ conan install 
Requirements
    libzmq/4.1.5@memsharded/stable from conan.io
Packages
    libzmq/4.1.5@memsharded/stable:81607951755e61cf17149d40bed6aba5b3a079a9

libzmq/4.1.5@memsharded/stable: Already installed!
PROJECT: Generated cmake created conanbuildinfo.cmake
PROJECT: Generated txt created conanbuildinfo.txt
PROJECT: Generated env created conanenv.txt
PROJECT: Generated conaninfo.txt
tharada@xubuntu:~/repos/conan-zmqpp$ conan source
PROJECT: Configuring sources in /home/tharada/repos/conan-zmqpp
Cloning into 'zmqpp'...
remote: Counting objects: 2761, done.
remote: Total 2761 (delta 0), reused 0 (delta 0), pack-reused 2761
Receiving objects: 100% (2761/2761), 5.54 MiB | 521.00 KiB/s, done.
Resolving deltas: 100% (1622/1622), done.
Checking connectivity... done.
Note: checking out '4.1.2'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 4784589... Merge pull request #122 from xaqq/doxygen
tharada@xubuntu:~/repos/conan-zmqpp$ conan build
Project: WARN: cd zmqpp/cmake_build && cmake .. -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release  -DCONAN_COMPILER="gcc" -DCONAN_COMPILER_VERSION="5.4" -DCONAN_LIBCXX="libstdc++" -Wno-dev -DZMQPP_BUILD_SHARED=OFF -DZMQPP_BUILD_CLIENT=OFF -DZMQPP_BUILD_EXAMPLES=OFF -DZMQPP_BUILD_TESTS=OFF -DZMQPP_LIBZMQ_CMAKE=OFF
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Conan: Using cmake global configuration
-- Conan C++ stdlib: libstdc++
-- Configuring done
-- Generating done
-- Build files have been written to: /home/tharada/repos/conan-zmqpp/zmqpp/cmake_build
Scanning dependencies of target zmqpp-static
[  7%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/actor.cpp.o
[ 14%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/context.cpp.o
[ 21%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/curve.cpp.o
[ 28%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/frame.cpp.o
[ 35%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/message.cpp.o
[ 42%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/poller.cpp.o
[ 50%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/reactor.cpp.o
[ 57%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/signal.cpp.o
[ 64%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/socket.cpp.o
[ 71%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/z85.cpp.o
[ 78%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/zap_request.cpp.o
[ 85%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/auth.cpp.o
[ 92%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/zmqpp.cpp.o
[100%] Linking CXX static library lib/libzmqpp-static.a
[100%] Built target zmqpp-static
tharada@xubuntu:~/repos/conan-zmqpp$ 
```

無事にbuildできました。

### packagingする

recipeからbuild出来るところまで確認ができたので、recipeをもとにpackageを作ります。
1. `conan export` でrecipeをlocal cache (通常は `$HOME/.conan/data` 配下) にコピー
2. `conan install` でlocal cache上のrecipeからbuildを行いpackage作成
という流れで作ってみます。

```shell-session
tharada@xubuntu:~/repos/conan-zmqpp$ conan export gasuketsu
zmqpp/4.1.2@gasuketsu/testing export: Copied 1 '.txt' files: CMakeLists.txt
zmqpp/4.1.2@gasuketsu/testing: A new conanfile.py version was exported
zmqpp/4.1.2@gasuketsu/testing: Folder: /home/tharada/.conan/data/zmqpp/4.1.2/gasuketsu/testing/export
zmqpp/4.1.2@gasuketsu/testing: Package recipe modified in export, forcing source folder removal
zmqpp/4.1.2@gasuketsu/testing: Use the --keep-source, -k option to skip it
zmqpp/4.1.2@gasuketsu/testing: Removing 'source' folder, this can take a while for big packages
tharada@xubuntu:~/repos/conan-zmqpp$ cd
tharada@xubuntu:~$ conan install zmqpp/4.1.2/gasuketsu/testing --build=zmqpp
Requirements
    libzmq/4.1.5@memsharded/stable from conan.io
    zmqpp/4.1.2@gasuketsu/testing from conan.io
Packages
    libzmq/4.1.5@memsharded/stable:81607951755e61cf17149d40bed6aba5b3a079a9
    zmqpp/4.1.2@gasuketsu/testing:1f1adf131e6c0c9fb34450c5f3e0970d57e40fc1

libzmq/4.1.5@memsharded/stable: Already installed!
zmqpp/4.1.2@gasuketsu/testing: WARN: Forced build from source
zmqpp/4.1.2@gasuketsu/testing: Building your package in /home/tharada/.conan/data/zmqpp/4.1.2/gasuketsu/testing/build/1f1adf131e6c0c9fb34450c5f3e0970d57e40fc1
zmqpp/4.1.2@gasuketsu/testing: Configuring sources in /home/tharada/.conan/data/zmqpp/4.1.2/gasuketsu/testing/source
Cloning into 'zmqpp'...
remote: Counting objects: 2761, done.
remote: Total 2761 (delta 0), reused 0 (delta 0), pack-reused 2761
Receiving objects: 100% (2761/2761), 5.54 MiB | 1.45 MiB/s, done.
Resolving deltas: 100% (1622/1622), done.
Checking connectivity... done.
Note: checking out '4.1.2'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 4784589... Merge pull request #122 from xaqq/doxygen
zmqpp/4.1.2@gasuketsu/testing: Copying sources to build folder
zmqpp/4.1.2@gasuketsu/testing: Generated cmake created conanbuildinfo.cmake
zmqpp/4.1.2@gasuketsu/testing: Generated txt created conanbuildinfo.txt
zmqpp/4.1.2@gasuketsu/testing: Generated env created conanenv.txt
zmqpp/4.1.2@gasuketsu/testing: WARN: cd zmqpp/cmake_build && cmake .. -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release  -DCONAN_COMPILER="gcc" -DCONAN_COMPILER_VERSION="5.4" -DCONAN_LIBCXX="libstdc++" -Wno-dev -DZMQPP_BUILD_SHARED=OFF -DZMQPP_BUILD_CLIENT=OFF -DZMQPP_BUILD_EXAMPLES=OFF -DZMQPP_BUILD_TESTS=OFF -DZMQPP_LIBZMQ_CMAKE=OFF
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Conan: Using cmake global configuration
-- Conan C++ stdlib: libstdc++
-- Configuring done
-- Generating done
-- Build files have been written to: /home/tharada/.conan/data/zmqpp/4.1.2/gasuketsu/testing/build/1f1adf131e6c0c9fb34450c5f3e0970d57e40fc1/zmqpp/cmake_build
Scanning dependencies of target zmqpp-static
[  7%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/actor.cpp.o
[ 14%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/context.cpp.o
[ 21%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/curve.cpp.o
[ 28%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/frame.cpp.o
[ 35%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/message.cpp.o
[ 42%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/poller.cpp.o
[ 50%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/reactor.cpp.o
[ 57%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/signal.cpp.o
[ 64%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/socket.cpp.o
[ 71%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/z85.cpp.o
[ 78%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/zap_request.cpp.o
[ 85%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/auth.cpp.o
[ 92%] Building CXX object CMakeFiles/zmqpp-static.dir/src/zmqpp/zmqpp.cpp.o
[100%] Linking CXX static library lib/libzmqpp-static.a
[100%] Built target zmqpp-static

zmqpp/4.1.2@gasuketsu/testing: Package '1f1adf131e6c0c9fb34450c5f3e0970d57e40fc1' built
zmqpp/4.1.2@gasuketsu/testing: Build folder /home/tharada/.conan/data/zmqpp/4.1.2/gasuketsu/testing/build/1f1adf131e6c0c9fb34450c5f3e0970d57e40fc1
zmqpp/4.1.2@gasuketsu/testing: Generated conaninfo.txt
zmqpp/4.1.2@gasuketsu/testing: Generated conanbuildinfo.txt
zmqpp/4.1.2@gasuketsu/testing: Generated conanenv.txt
zmqpp/4.1.2@gasuketsu/testing: Generating the package
zmqpp/4.1.2@gasuketsu/testing: Package folder /home/tharada/.conan/data/zmqpp/4.1.2/gasuketsu/testing/package/1f1adf131e6c0c9fb34450c5f3e0970d57e40fc1
zmqpp/4.1.2@gasuketsu/testing package(): Copied 1 '.a' files: libzmqpp-static.a
zmqpp/4.1.2@gasuketsu/testing package(): Copied 20 '.hpp' files
zmqpp/4.1.2@gasuketsu/testing: Package '1f1adf131e6c0c9fb34450c5f3e0970d57e40fc1' created
tharada@xubuntu:~$
```


## 作成したpackageをuploadして公開する

今回作成したzmqppのpackageはOSSであり、publicなpackageとして他のconanユーザに公開したかったので、デフォルトのupload先であるconan.ioにuploadします。

```shell-session
tharada@xubuntu:~$ conan upload --all zmqpp/4.1.2@gasuketsu/testing
Uploading zmqpp/4.1.2@gasuketsu/testing
Compressing exported files...                                         Please log in to "conan.io" to perform this action. Execute "conan user" command.
If you don't have an account sign up here: http://www.conan.io
Please enter a password for "gasuketsu" account: 
Current 'conan.io' user already: gasuketsu
Uploading conanmanifest.txt                                           
[==================================================]                  
Uploading conanfile.py                                                
[==================================================]                  
Uploading conan_export.tgz                                            
[==================================================]                  
Uploaded conan recipe 'zmqpp/4.1.2@gasuketsu/testing' to 'conan.io': https://www.conan.io/source/zmqpp/4.1.2/gasuketsu/testing
Uploading package 1/1: 1f1adf131e6c0c9fb34450c5f3e0970d57e40fc1
Package integrity OK!                                                 
Requesting upload permissions...Done!                                 
Uploading conanmanifest.txt                                           
[==================================================]                  
Uploading conaninfo.txt                                               
[==================================================]                  
tharada@xubuntu:~$ 
```

# さいごに

今回作成したrecipeは [こちら](https://github.com/gasuketsu/conan-zmqpp)で公開しております。

後から気づいたのですが 作成したrecipe/packageの確認手段としては `conan test_package` を使ったほうが良さそうなので、`test_package` 用のコードを準備しておこうと思います。