SONiCのビルド環境に手を加える

# はじめに
SONiCになんらか追加機能を加えようとするとき、ビルド環境にも手を加えなければならない場面が出てくることがあります。今回はビルドに必要なツールが現在ビルド環境にない場合、どうやって追加すればいいかについて説明します。

# ビルド環境の整備
SONiCをソースコードからビルドするには、まず環境の整備が必要です。[General Building Guide](https://github.com/Azure/sonic-buildimage/blob/master/README.md)によれば`pip`、`jinja2`、`j2cli`に加えてDockerが必要とのことです(`make`と`git`も必要です)。まずは無変更でビルドできることを確認して、そこがスタート地点です。本当に無変更だと下手すると半日以上かかりますので、焦らず放置して待ちましょう。

# ビルドに必要なツールがあるか確認する
SONiCのビルド環境はDockerコンテナで構成され必要なツールがそこにインストールされますので、指定されたバージョンのC++コンパイラを予めインストールすると言った事前の作業は不要です。下記のファイルを使ってビルド環境のDockerコンテナは作成されます。

- `sonic-slave-buster/Dockerfile.j2`
- `sonic-slave-jessie/Dockerfile.j2`
- `sonic-slave-stretch/Dockerfile.j2`

`.j2`というサフィックスは[jinja2](https://jinja.palletsprojects.com/en/2.11.x/)テンプレートを意味していて、ファイル内の`{%- ... %}`で囲まれた部分を`j2`コマンド等で処理することで、本来ファイル内に固定文字列で記述される部分を差し替えたりできるというものです。C言語のプリプロセッサのようなものと思っていただければいいかと思います。今回はテンプレート部分には手を付けませんので、無視して普通の`Dockerfile`と思っていただいて問題ありません。

中を見ると、`apt-get -y install`で様々なツールがインストールされていることがわかります。**もし使いたいツールが含まれていれば、以降の作業は不要です**。

# ビルドに必要なツールを加える

先ほどの`Dockerfile.j2`を変更することで、ビルドに必要なツールを追加します。2020年12月時点での最新のSONiCでは、buster (Debian10)ベースのdebパッケージと stretch (Debian9)ベースのdebパッケージが混在しています。ビルドするdebパッケージがどちらを使うかによって適切な`Dockerfile.j2`を変更します。

あくまでビルドの際に使われるということで、ビルドの結果できあがったSONiCのバイナリイメージには当然ですが加えたツールは含まれないことに注意してください。

## `apt-get`でインストールできるツール

`apt-get install -y` の行に加えます。

```Dockerfile:Dockerfile.j2
## Make apt-get non-interactive
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y \
        apt-utils \
        default-jre-headless \
        openssh-server \
        curl \
        wget \
        unzip \
        git \
        build-essential \
```

## アーカイブをダウンロードして展開するツール

幸いなことに、Go言語コンパイラがこのタイプです。これを参考にするといいでしょう。

```Dockerfile:Dockerfile.j2
# For gobgp and telemetry build
RUN export VERSION=1.14.2 \
{%- if CONFIGURED_ARCH == "armhf" %}
 && wget https://storage.googleapis.com/golang/go$VERSION.linux-armv6l.tar.gz \
 && tar -C /usr/local -xzf go$VERSION.linux-armv6l.tar.gz \
{%- elif CONFIGURED_ARCH == "arm64" %}
 && wget https://storage.googleapis.com/golang/go$VERSION.linux-arm64.tar.gz \
 && tar -C /usr/local -xzf go$VERSION.linux-arm64.tar.gz \
{%- else %}
 && wget https://storage.googleapis.com/golang/go$VERSION.linux-amd64.tar.gz \
 && tar -C /usr/local -xzf go$VERSION.linux-amd64.tar.gz \
{%- endif %}
 && echo 'export GOROOT=/usr/local/go' >> /etc/bash.bashrc \
 && echo 'export PATH=$PATH:$GOROOT/bin' >> /etc/bash.bashrc \
 && rm go$VERSION.linux-*.tar.gz
```

## インストーラを実行するタイプのツール

`curl`や`wget`でインストーラを取得して、`RUN`コマンドで実行します。

## 実際に追加してみる(例)

### Rustコンパイラ

[Rust言語の公式ページ](https://rust-lang.org/)によると、一番簡単なインストール方法は

```shell-session
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

です。やってみるとわかりますが、これをそのまま実行すると、対話形式でどうするか問い合わせがあり答える必要があります。

```shell-session
   default host triple: x86_64-unknown-linux-gnu
     default toolchain: stable (default)
               profile: default
  modify PATH variable: yes

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
>
```

`dockedr build`の中でこれを実行すると、対話できないためエラーになります。実行される`rust-init.sh`スクリプトは、パラメータとして`-y`を指定すると問い合わせなくインストールを実行できます。直接パイプで`sh`に食わせる場合、`sh -s -- -y`とします。

実際にビルドで`rustc`や`cargo`などを呼び出す場合、`PATH`を通しておく必要があります。`golang`と同様に、`/etc/bash.bashrc`に書き加えます。

Rustの場合、これらを`Dockerfile.j2`の末尾に加えるとうまく動きません。

Rustツール群はにコンパイラだけでなくダウンロードしたモジュールなどもユーザのホームディレクトリの下に置くよう作られています。`Dockerfile.j2`を使ってDockerイメージを構築しているときはSONiCビルド時のユーザアカウントではなく`root`で構築されるため、本来インストールしたかった場所に配置されずアクセス権限もRustツール群の想定外となるというのが動かない理由です。

`Dockerfile.j2`で`docker build`する際にはビルド時のユーザアカウント情報がないためうまく処理できません。同じディレクトリに`Dockerfile.user`があり、そちらはユーザアカウントに関連するあれこれが書かれてますので、そちらに追加するのが素直かと思います。ファイル末尾付近、**`USER $user` の前に追加します**。(`USER $user`の後ろだと`/etc/`への書き込み権限が失われるため、Dockerイメージの作成に失敗します)

まとめるとこうなります。

```Dockerfile:Dockerfile.user
# Install Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | su $user -c "sh -s -- -y" \
 && echo 'export PATH=$PATH:$HOME/.cargo/bin' >> /etc/bash.bashrc

USER $user
```


ちゃんとやるなら`golang`と同じようにターゲットのアーキテクチャに対応するバイナリを取得する必要がありますが、今回は手抜きです。

もし`arm64`や`armhf`でトライされる場合は、Rustドキュメントの[Other installation methods](https://forge.rust-lang.org/infra/other-installation-methods.html)を参考にするといいでしょう。`rust-init.sh`にオプションを指定すればターゲットを変更できるので、対応自体はそれほど難しくないと思いますが、`Dockerfile.user`はテンプレート化されていませんのでjinja2のテンプレート置換が使えません。Dockerイメージの作成は`Makefile.work`で処理されますので、そちらを変更してユーザ名などの情報を渡すようにすれば、`Dockerfile.j2`への変更で綺麗にまとめられると思います。

### DDlogコンパイラ

Differential Datalog(Ddlog)コンパイラのバイナリリリースは`amd64`しか提供されていません。

`arm64`や`armhf`でDDlogを利用するためにはソースコードからビルドする必要がありますが、DDlogのソースコードは`Rust`で書かれている部分もあれば`Haskell`で書かれている部分もあり`JDK`も使うようです。

手順通りにやればビルドできるとは思いますが、`arm64`や`armhf`のターゲットマシンを持ってないため確認できないこともあり、ここは`Rust`と同様、手抜きさせていただきます。

`/usr/local/ddlog/`に展開していますので、`PATH`を通しておきます。また、公式のインストール手順に書かれているとおり、環境変数`DDLOG_HOME`も設定しておきます。

```Dockerfile:Dockerfile.j2
RUN wget https://github.com/vmware/differential-datalog/releases/download/v0.32.0/ddlog-v0.32.0-20201212054437-linux.tar.gz \
 && tar -C /usr/local -xzf ddlog-v0.32.0-20201212054437-linux.tar.gz \
 && echo 'export PATH=$PATH:/usr/local/ddlog/bin' >> /etc/bash.bashrc \
 && echo 'export DDLOG_HOME=/usr/local/ddlog' >> /etc/bash.bashrc \
 && rm ddlog-v0.32.0-20201212054437-linux.tar.gz
```

これを`Dockerfile.j2`の末尾に加えます。

### ビルド環境に組み込めたかの確認
今回はツールを加えただけですが、この状態でSONiCをビルドすると、ビルド環境のDockerコンテナ再構築の際にRustコンパイラやDDlogコンパイラがインストールされる様子を見ることができます。

<details><summary>ビルドログ</summary><div>

```:Rustがインストールされる様子
 ---> bb0f00a37f11
Step 18/19 : RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | su $user -c "sh -s -- -y"  && echo 'export PATH=$PATH:$HOME/.cargo/bin' >> /etc/bash.bashrcwnloading installer
info: profile set to 'default'
info: default host triple is x86_64-unknown-linux-gnu
info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu'
info: latest update on 2020-11-19, rust version 1.48.0 (7eac88abb 2
020-11-16)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: using up to 500.0 MiB of RAM to unpack components
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
info: default toolchain set to 'stable-x86_64-unknown-linux-gnu'

  stable-x86_64-unknown-linux-gnu installed - rustc 1.48.0 (7eac88abb 2020-11-16)


Rust is installed now. Great!

To get started you need Cargo's bin directory ($HOME/.cargo/bin) in your PATH
environment variable. Next time you log in this will be done
automatically.

To configure your current shell, run:
source $HOME/.cargo/env
```

```
 ---> 192e550224ee
Step 62/62 : RUN wget https://github.com/vmware/differential-datalog/releases/download/v0.32.0/ddlog-v0.32.0-20201212054437-linux.tar.gz  && tar -C /usr/local -xzf ddlog-v0.32.0-20201212054437-linux.tar.gz  && echo 'export PATH=$PATH:/usr/local/ddlog/bin' >> /etc/bash.bashrc  && echo 'export DDLOG_HOME=/usr/local/ddlog' >> /etc/bash.bashrc  && rm ddlog-v0.32.0-20201212054437-linux.tar.gz
 ---> Running in 5b01001431b8--2020-12-14 06:11:44--  https://github.com/vmware/differential-datalog/releases/download/v0.32.0/ddlog-v0.32.0-20201212054437-linux.tar.gz
Resolving github.com (github.com)... 52.192.72.89
Connecting to github.com (github.com)|52.192.72.89|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://github-production-release-asset-2e65be.s3.amazonaws.com/126076620/1b406580-3bfa-11eb-9790-bb8d226f5ef3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201214%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20201214T061144Z&X-Amz-Expires=300&X-Amz-Signature=5d4b80217d6d631cebeecca5cde3de602be37b94ef45c722eee98b5c9c2fd6d6&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=126076620&response-content-disposition=attachment%3B%20filename%3Dddlog-v0.32.0-20201212054437-linux.tar.gz&response-content-type=application%2Foctet-stream [following]
--2020-12-14 06:11:44--  https://github-production-release-asset-2e65be.s3.amazonaws.com/126076620/1b406580-3bfa-11eb-9790-bb8d226f5ef3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201214%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20201214T061144Z&X-Amz-Expires=300&X-Amz-Signature=5d4b80217d6d631cebeecca5cde3de602be37b94ef45c722eee98b5c9c2fd6d6&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=126076620&response-content-disposition=attachment%3B%20filename%3Dddlog-v0.32.0-20201212054437-linux.tar.gz&response-content-type=application%2Foctet-stream
Resolving github-production-release-asset-2e65be.s3.amazonaws.com (github-production-release-asset-2e65be.s3.amazonaws.com)... 52.216.145.123
Resolving github-production-release-asset-2e65be.s3.amazonaws.com (github-production-release-asset-2e65be.s3.amazonaws.com)... 52.216.145.123
Connecting to github-production-release-asset-2e65be.s3.amazonaws.com (github-production-release-asset-2e65be.s3.amazonaws.com)|52.216.145.123|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 30424097 (29M) [application/octet-stream]
Saving to: 'ddlog-v0.32.0-20201212054437-linux.tar.gz'
```
</div></details>

また、`docker exec`等でコンテナ内に入って、動かせるかの確認ができます。

<details><summary>Dockerコンテナでの操作ログ</summary><div>

```shell-session
$ docker run -it sonic-slave-buster-masaru:a1ccaf7d3a8 bash
masaru@4ea4efaa90e0:/$ cargo
Rust's package manager

USAGE:
    cargo [+toolchain] [OPTIONS] [SUBCOMMAND]

OPTIONS:
    -V, --version           Print version info and exit
        --list              List installed commands
        --explain <CODE>    Run `rustc --explain CODE`
    -v, --verbose           Use verbose output (-vv very verbose/build.rs output)
    -q, --quiet             No output printed to stdout
        --color <WHEN>      Coloring: auto, always, never
        --frozen            Require Cargo.lock and cache are up to date
        --locked            Require Cargo.lock is up to date
        --offline           Run without accessing the network
    -Z <FLAG>...            Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
    -h, --help              Prints help information

Some common cargo commands are (see all commands with --list):
    build, b    Compile the current package
    check, c    Analyze the current package and report errors, but don't build object files
    clean       Remove the target directory
    doc         Build this package's and its dependencies' documentation
    new         Create a new cargo package
    init        Create a new cargo package in an existing directory
    run, r      Run a binary or example of the local package
    test, t     Run the tests
    bench       Run the benchmarks
    update      Update dependencies listed in Cargo.lock
    search      Search registry for crates
    publish     Package and upload this package to the registry
    install     Install a Rust binary. Default location is $HOME/.cargo/bin
    uninstall   Uninstall a Rust binary

See 'cargo help <command>' for more information on a specific command.
masaru@4ea4efaa90e0:/$ ddlog
Usage: ddlog [OPTION...]
  -h       --help                           Display help message.
  -v       --version                        Display DDlog version.
  -i FILE                                   DDlog program to compile.
           --action=ACTION                  Action: [validate, compile]
  -L PATH                                   Extra DDlog library directory.
  -o DIR   --output-dir=DIR                 Output directory (default based on program name).
           --dynlib                         Generate dynamic library.
  -j       --java                           Generate Java bindings. Implies '--rust-flatbuffers'.
           --output-internal-relations      All non-input relations are marked as output relations.
           --output-input-relations=PREFIX  Mirror each input relation into an output relation named by prepending the prefix.
           --no-dynlib                      Do not generate dynamic library (default).
           --staticlib                      Generate static library (default).
           --no-staticlib                   Do not generate static library.
  -g                                        Enable debugging hooks.
           --pp-flattened                   Dump the source after compilation pass 1 (flattening module hierarchy) to PROG.flat.ast.
           --pp-validated                   Dump the source after compilation pass 2 (validation, including several source transformations) to PROG.valid.ast.
           --pp-debug                       Dump the source after compilation pass 3 (injecting debugging hooks) to FILE.debug.ast.  If the '-g' option is not specified, then pass 3 is a no-op and will produce identical output to pass 2.
           --pp-optimized                   Dump the source after compilation pass 4 (optimization) to FILE.opt.ast.
           --re-validate                    [developers only] Re-validate the program after type inference and optimization passes.
           --omit-profile                   Skip adding a Cargo profile (silences warnings for some rust builds, included by default)
           --omit-workspace                 Skip adding a Cargo workspace (silences errors for some rust builds, included by default)
           --run-rustfmt                    Run rustfmt on the generated code
           --rust-flatbuffers               Build flatbuffers bindings for Rust
           --nested-ts-32                   Use 32-bit instead of 16-bit nested timestamps. Supports recursive programs that may perform >65,536 iterations. Slightly increases the memory footprint of the program.

ddlog: input file not specified
masaru@4ea4efaa90e0:/$ echo $DDLOG_HOME
/usr/local/ddlog
masaru@4ea4efaa90e0:/$ 
```
</div></details>

# おわりに
SONiCのビルド環境として構築されるDockerコンテナは、一度ビルドを走らせると次回以降再利用されます。それによってビルド時間が短縮されるのですが、今回のように`Dockefile.j2`に手を加えるとコンテナイメージは作り直しになります(`make`が勝手にやってくれます)。最初の無変更ビルドで時間がかかるのは実感されてると思いますが、気長に待ちましょう。

追加したビルドツールを呼び出す件については、別途記事を用意したいと思います。