colcon について調べた

[諸事情](https://qiita.com/nonanonno/items/a2af041fdfe8b5a91bb9) により ROS/ROS2 で使われているメタビルドシステムである [colcon](https://colcon.readthedocs.io/en/released/index.html) について調べていた。あまりドキュメントがなくなかなか苦戦していたのだが、折角なのでその時まとめていたメモを公開します。

ちなみに https://github.com/nonanonno/colcon_d を作ってました。(修正しなきゃ

Q. 誰向けの記事？
A. colcon を開発したい皆さん

とは言え、Getting Started 的なものではない。colcon の概要は把握したが結局何ができるのかを調べたい、あるいは、なんとなく colcon の拡張機能を作る準備はできた(↓のツイートみたいに)が、どこを開発したら良さそうなのかわからないといった時に参考にできるかもしれない。私自身が開発していて気になった部分をコードを見ながら(コードを見るしかないのである！)解析していたメモが含まれている。力尽きている箇所があったりするがそこはご了承願いたい。

<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://t.co/IEzfK2F2nO">pic.twitter.com/IEzfK2F2nO</a></p>&mdash; nonno() (@nonanonno) <a href="https://twitter.com/nonanonno/status/1359839549308866565?ref_src=twsrc%5Etfw">February 11, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

では、本編。

# colcon

以降では D言語向けビルドツール権パッケージ管理ツールである [DuB](https://code.dlang.org/) 向けの拡張を開発するものとします。

## 概要

複数のパッケージ間の依存関係を解決しつつ各パッケージのビルドやテストを行うメタビルドシステム。やるのは依存関係解決とビルドやテストなどのキックまでで、実際にビルドを行うのは CMake など、各パッケージに対応するツールになる。

- [公式ドキュメント](https://colcon.readthedocs.io/en/released/)
- [colcon repositories](https://github.com/colcon)
    - [colcon-core](https://github.com/colcon/colcon-core)

colcon は Python([>=3.5](https://github.com/colcon/colcon-core/blob/master/setup.cfg#L28))で作られており、拡張可能である。[拡張ポイント](https://colcon.readthedocs.io/en/released/developer/extension-point.html)は以下の通り。

| Point                 | Who use                                 | Description                                                                                                                                         |
| :-------------------- | :-------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
| Verb                  | colcon                                  | `colcon <verb>` を作成                                                                                                                              |
| PackageDiscovery      | Verb                                    | package を探すルール(デフォルト`./` とか?)                                                                                                          |
| PackageIdentification | Discovery                               | package の種類を解釈(cmake とか ament_cmake とか)                                                                                                   |
| PackageAugmentation   | package 識別した後の Verb               | 任意の情報を [package descriptor](https://github.com/colcon/colcon-core/blob/master/colcon_core/package_descriptor.py#L12) に追加(追加の依存先など) |
| PackageSelection      | package 識別し追加情報を与えた後の Verb | `--packages-select` とかのあれ                                                                                                                      |
| Task                  | package識別し追加情報を与えた後のVerb   | verb と package type の組み合わせに対応する処理を定義(build と cmake など)                                                                          |
| Executor              | some verb extensions                    | `--parallel-workers` とかのあれ                                                                                                                     |
| Shell                 | some task extensions                    | bash や zsh など、それぞれ向けの環境変数設定スクリプトを作るためのもの                                                                              |
| Environment           | some task extensions                    | environment や environment hook を作るもの？                                                                                                        |
| EventHandler          |                                         | `--event-handlers` とかのあれ                                                                                                                       |
| AugmentParser         |                                         | undocumented                                                                                                                                        |
| PrefixPath            |                                         | undocumented                                                                                                                                        |

[ここ](https://github.com/colcon/colcon-core/tree/master/colcon_core)のフォルダがそれぞれの拡張ポイントに対応していそう。
また、インストールされている拡張は `colcon extensions` で確認することができる。

## 詳細

新たなビルドツール向けの拡張を作る際に必要になりそうな拡張ポイントについて少し詳細に書いている。

### Verb

`colcon <verb>` を作成するための拡張ポイント。colcon-core では `build` と `test` が[最初から定義](https://github.com/colcon/colcon-core/tree/master/colcon_core/verb)されている。

### PackageDiscovery

パッケージを探す場所に関するルール。`colcon build` や `colcon test` など、各 Verb によって使用される。`colcon build --help` で見つけることができる `Discovery arguments` セクションのオプションを提供する。探索位置を指定するオプションを提供したり、[ディレクトリを再帰的に探す機能](https://github.com/colcon/colcon-recursive-crawl)の提供を行っている。

### PackageIdentification
#### 概要
パッケージの種類を解釈するための拡張ポイント。PackageDiscovery によって得られたディレクトリが何らかのパッケージであるか、それが何の種類なのかを解釈する。`setup.py` と `setup.cfg` が Python, `CMakeLists.txt` があれば cmake プロジェクト、など。

#### 使い方

[colcon_core.package_identification.PackageIdentificationExtensionPoint](https://github.com/colcon/colcon-core/blob/master/colcon_core/package_identification/__init__.py#L31) を継承し、`identify` を override する。

```python
from colcon_core.package_identification import PackageIdentificationExtensionPoint

class DubPackageIdentificationExtensionPoint(PackageIdentificationExtensionPoint):
    PRIORITY = 100

    def identify(self,desc: PackageDescriptor) -> None:
        ...
```

- `PRIORITY` : 識別のための優先度
    - 数字が高い程優先度が高い
    - 複数の PackageIdentification にマッチした時にどれを選択するかをする
        - `CMakeLists.txt` と `package.xml` がある時、cmake 向けではなく ros 向けを優先する、といった
    - 同じ優先度で複数の PackageIdentification がマッチしたら警告とともにそのパッケージは無視される

- `identify` : 識別処理を行う
    - desc: [PackageDescriptor](https://github.com/colcon/colcon-core/blob/master/colcon_core/package_descriptor.py#L12) が渡される
        - フィールド
            - `path` : パッケージパス
            - `type` : パッケージの種類 (cmake とか ros.ament_cmake とか)
            - `name` : パッケージ名
            - `dependencies` : `defaultdict(set)`
            - `hooks` : Each item in 'hooks' must be a relative path in the installation space.
            - `matadata` : any additional information
        - パッケージのユニーク性は `path` と `type` と `name` の triplet で表現される
            - `path` は、文字列として違っていても同じ絶対パスを指していれば同じと見なす
            - `identify` 関数でこの3つに値を入れれば、識別した、と解釈される
            - `path` には最初から値が入っている
        - `dependencies` は依存パッケージのリストではない。`DependencyDescriptor` あるいは文字列で表現されるカテゴリをキーとして、その値として依存パッケージのリストが入っている
            - [colcon_ros](https://github.com/colcon/colcon-ros/blob/master/colcon_ros/package_identification/ros.py) では `run` と `test` と `build` がキーとして設定される。[それぞれ以下のように対応している](https://github.com/colcon/colcon-ros/blob/eb0b932cf7b1f3f5a720804e67f3c1a0f8995ca8/colcon_ros/package_identification/ros.py#L86-L106)
                - `run` : `<build_export_depend>`, `<buildtool_export_depend>`, `<exec_depend>`
                - `test` : `<test_depend>`
                - `build` : `<build_depend>`, `<buildtool_depend>`
        - `hooks` の PackageIdentification での使い方はよくわからなかった
        - `matadata` は好きに使えば良いと思う。colcon_ros では `version` と `get_python_setup_options` を設定している。

他の `PackageIdentification` の `identify` を呼ぶ、といった使い方も可能ではある。例えば、colcon_ros のを使うと `package.xml` を解釈した。 PackageDescriptor が得られるなど。([_get_package を呼べば良い](https://github.com/colcon/colcon-ros-gradle/blob/master/colcon_ros_gradle/package_identification/ament_gradle.py#L77)気もする)

### PackageArgumentation

PackageDiscovery でフォルダを得て、PackageIdentification で何のパッケージであるかを、例えばPython と解釈し、その後 Python パッケージ向けに追加の依存を設定する、といったのがこの拡張ポイントの役割。

[Python では](https://github.com/colcon/colcon-core/blob/master/colcon_core/package_augmentation/python.py) `PackageDescriptor` に対して `metadata['version']` の設定と、`setup.cfg` に書かれている依存パッケージを`dependencies['setup_requires|install_requires|test_requires']` に設定するのをやっている。

あるいは、[package_information](https://github.com/colcon/colcon-package-information/blob/master/colcon_package_information/package_augmentation/check_dependency_constraint.py) ではパッケージ同士のバージョン整合性の確認に利用しているようだ。

パッケージを識別した後、依存関係を計算してそれぞれ処理する前に何かをやりたい時に利用する、といったところか。

### PackageSelection

`--packages-select` などを提供するための拡張ポイント。タイミングは PackageAugmentation の後。

### Task

`verb` と `package type` の組み合わせで処理を行うためのポイント、`verb` の部分については、[ここ](https://github.com/colcon/colcon-core/blob/91c6e2b49b001bcaeaa2aab3732a71e7392c0215/colcon_core/verb/build.py#L177)を見る限り `setup.cfg` で設定することになっている。例えば以下のようなセクションを書く。

```cfg
[options.entry_points]
colcon_core.task.build =
    dub = colcon_dub.task.dub.build:DubBuildTask
```

#### 使い方

[colcon_core.task.TaskExtensionPoint](https://github.com/colcon/colcon-core/blob/master/colcon_core/task/__init__.py#L52) を継承する。

- `add_arguments(self, *, parser: ArgumentParser)` : コマンドライン引数を追加する。必要なら override

- `<verb>` : タスク名のメンバ関数を定義する(`getattr(self, self.TASK_NAME)` が呼ばれる)
    - 引数は、`(self, *, hoge=None, fuga=0)` といった形でデフォルト値付きで設定する。
    - `verb` 自体は引数なしで呼び出すが、他の拡張のタスクを呼ぶ時に伝搬してもらう必要があるものを引数として定義する
    - `async` である必要あり
- 情報には `self.context` 経由でアクセスする
    - フィールド
        - `pkg` : `PackageDescriptor`
        - `args` : `<verb?` 毎に違うらしい build の場合、 [BuildPackageArguments](https://github.com/colcon/colcon-core/blob/master/colcon_core/verb/build.py#L34)
        - `dependencies` : 再帰的に解決されたタスクに必要最終的な依存パッケージ
- 何をやる必要があるのかは、`<verb>` 次第(次節)

#### Task.build

`colcon build` で実行されるタスクを定義する。`TaskExtensionPoint` を継承し、`build` 関数を定義する。

- `self.context.dependencies` には自身が直接依存しているパッケージ + `run` カテゴリの依存パッケージが再帰的に入っている
    - [コード](https://github.com/colcon/colcon-core/blob/91c6e2b49b001bcaeaa2aab3732a71e7392c0215/colcon_core/verb/build.py#L129)
- インストールまで実装する必要がある
- 必要に応じて [create_environment_script](https://github.com/colcon/colcon-core/blob/master/colcon_core/environment/__init__.py#L65) を呼ぶ
    - 自身に依存する別パッケージをビルドする際に必要となる環境変数(例えば、`CMAKE_PREFIX_PATH`)を設定するためのスクリプトを作るため

#### Task.test

`colcon test` で実行されるタスクを定義する。`TaskExtensionPoint` を継承し、`test` を定義する。

- `self.context.dependencies` には build のと同様のに加えて自分自身も入っている。
    - テストコードをビルドするのに自分自身が必要だからか


### Executor

Job 実行の仕組みを提供しているはず。`--parallel-workers` と `--executor` を提供しているあれ。

### Shell

bash や zsh など、各shell 向けの shell script や hook を作るための拡張ポイント。必要な機能を持つ(例えば setup.bash といった)スクリプトを作るためのルールを作る。

### Environment

environment hook を定義する。実際の environment hooks script は shell extension を使って作られる。

これはビルドタイプとは関係ない。`build` タスクなどが `create_environment_scripts` を呼んだ時に全ての Environment 拡張が実行される。

#### 使い方

[colcon_core.environment.EnvironmentExtensionPoint](https://github.com/colcon/colcon-core/blob/master/colcon_core/environment/__init__.py#L18) を継承し、`create_environment_hooks(self, prefix_path, pkg_name) -> OrderedDict` を override する。

`create_environment_hooks` 内で[必要に応じて必要な環境変数](https://github.com/colcon/colcon-cmake/blob/8f1b92a190b2ad4289ecf837c3200d540c13fdd9/colcon_cmake/environment/cmake_prefix_path.py#L24-L30)を設定する。

### EventHandler

`--event-handlers` のあれ。