Linux版Siv3DをDockerで動かしたい

# きっかけ
Siv3Dを使って通信対戦ゲームを作りたい。
Siv3Dに実装されているTCPServerをサーバー上で稼働する必要がありました。
そこで、Linux版Siv3Dをインストールしたdocker imageを作成することにしました。

# Linux版Siv3Dについて
Linux版Siv3Dは[@wynd2608](https://qiita.com/wynd2608)さんが有志でLinuxに移植しているものです。

# さっそく作ってみる！
Linux版Siv3Dにはコンパイル済みバイナリの配布がありません。そのため、自前でビルドする必要があります。
今回はDockerfileを記述し、ビルドすることでdocker imageを作成します。
Siv3Dのホームページ(?)にLinux版のインストール手順が記載されてましたので参考にしました。
([Ubuntu で Siv3D プログラミングを始める](https://siv3d.github.io/ja-jp/download/ubuntu/))
## Dockerfileの記述
まずは任意のディレクトリに`dockerfile`という名前のファイルを作り、以下のように記述しました。
結論から言うと、**このDockerfileではうまくいきませんでした。**
```dockerfile
FROM ubuntu:22.04

RUN apt update && apt upgrade -y
RUN apt install -y \
    ninja-build \
    libasound2-dev \
    libavcodec-dev \
    libavformat-dev \
    libavutil-dev \
    libboost-dev \
    libcurl4-openssl-dev \
    libgtk-3-dev \
    libgif-dev \
    libglu1-mesa-dev \
    libharfbuzz-dev \
    libmpg123-dev \
    libopencv-dev \
    libopus-dev \
    libopusfile-dev \
    libsoundtouch-dev \
    libswresample-dev \
    libtiff-dev \
    libturbojpeg0-dev \
    libvorbis-dev \
    libwebp-dev \
    libxft-dev \
    uuid-dev \
    xorg-dev

RUN mkdir /root/download
RUN mkdir /root/download/OpenSiv3D
RUN git clone https://github.com/Siv3D/OpenSiv3D.git /root/download/OpenSiv3D

RUN mkdir /root/download/OpenSiv3D/Linux/build
WORKDIR /root/download/OpenSiv3D/Linux/build
RUN cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
WORKDIR /root/download/OpenSiv3D/Linux
RUN cmake --build build

WORKDIR /root/download/OpenSiv3D/Linux
RUN cmake --install build

RUN mkdir /root/download/OpenSiv3D/Linux/App/build
WORKDIR /root/download/OpenSiv3D/Linux/App/build
RUN cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
WORKDIR /root/download/OpenSiv3D/Linux/App
RUN cmake --build build
```
この`Dockerfile`があるディレクトリに移動し、以下のコマンドを実行してビルドします。
```bash
$ sudo docker build -t siv3d .
```
:::note warn
　
今回は試験的に作成しているのでイメージのタグは付けていないので自動的にlatestになりますが、基本的には付けた方が良いらしいです。
タグをつける場合には以下のコマンドを利用してください。
```bash
$ sudo docker build -t siv3d:1 .
```
`siv3d:1`の`1`の部分がタグになります。
:::
## 一度目の失敗
`RUN apt update && apt upgrade`で止まってしまいました。
エラーログは申し訳ないけれども控えてないですが、原因を調べたところWSL2上のubuntuの時刻のずれが原因でした。

:::note info
**Topic**
WSL2ではPCをスリープすると時刻がずれるバグが存在します！
:::
時刻のずれを修正するには以下のコマンドを実行すればいいです。
```bash
$ sudo hwclock --hctosys
```
これでも治らない場合はwslを再起動してみたり、タイムゾーンが正しいかどうか確認しましょう。

## 二度目の失敗
改めてビルドします。
……おや、今度は以下のようなログの状態で`apt install`が途中で止まってしまいましたね……。
```
 => => # Please select the geographic area in which you live. Subsequent configuration
 => => # questions will narrow this down by presenting a list of cities, representing
 => => # the time zones in which they are located.
 => => #   1. Africa   3. Antarctica  5. Arctic  7. Atlantic  9. Indian    11. US
 => => #   2. America  4. Australia   6. Asia    8. Europe    10. Pacific  12. Etc
 => => # Geographic area:
```
これはタイムゾーンが指定されていないため起こります。
数字を入力すれば続きに行けそうな雰囲気はありますが、ここで数字を入力しても受け付けてくれません。
一旦`Ctrl+C`でビルドをキャンセルし、Dockerfileの2行目に以下の文言を追加します。
```dockerfile
RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
```
`ln`コマンドはシンボリックリンクを作成するコマンドです。
おそらくinstall時に参照するディレクトリに対してタイムゾーンを示すシンボリックリンクを作成しているようですが、私もよくわかっておりません……。
しかしこれを記述すると上のログの場所で止まることはありませんでした。

## 三度目の失敗
三度目の正直……はもちろんうまくいかず、以下のエラーが出てビルドが終了しました。
```
dockerfile:34
--------------------
  32 |     RUN mkdir /root/download
  33 |     RUN mkdir /root/download/OpenSiv3D
  34 | >>> RUN git clone https://github.com/Siv3D/OpenSiv3D.git /root/download/OpenSiv3D
  35 |
  36 |     RUN mkdir /root/download/OpenSiv3D/Linux/build
--------------------
ERROR: failed to solve: process "/bin/sh -c git clone https://github.com/Siv3D/OpenSiv3D.git /root/download/OpenSiv3D" did not complete successfully: exit code: 127
```
`git clone`がうまくいってませんね。
う～ん理由は明白！！！　gitパッケージ入ってないやんけ！！！
ということで追加しましょう。
よく考えたら今後使うコマンドであるcmakeや、なんとなく必要そうなg++なども入ってませんね。
ということで完全に独断と偏見で必要そうなパッケージを追加します。
追加したパッケージは以下の通りです。
- git
- cmake
- gcc
- g++

## ついに成功！
これらを追加してビルドすると成功しました！！
最終的はDockerfileは以下の通りです。
```dockerfile
FROM ubuntu:22.04

RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

RUN apt update && apt upgrade -y
RUN apt install -y \
    git \
    cmake \
    gcc \
    g++ \
    ninja-build \
    libasound2-dev \
    libavcodec-dev \
    libavformat-dev \
    libavutil-dev \
    libboost-dev \
    libcurl4-openssl-dev \
    libgtk-3-dev \
    libgif-dev \
    libglu1-mesa-dev \
    libharfbuzz-dev \
    libmpg123-dev \
    libopencv-dev \
    libopus-dev \
    libopusfile-dev \
    libsoundtouch-dev \
    libswresample-dev \
    libtiff-dev \
    libturbojpeg0-dev \
    libvorbis-dev \
    libwebp-dev \
    libxft-dev \
    uuid-dev \
    xorg-dev

RUN mkdir /root/OpenSiv3D
RUN git clone https://github.com/Siv3D/OpenSiv3D.git /root/OpenSiv3D

RUN mkdir /root/OpenSiv3D/Linux/build
WORKDIR /root/OpenSiv3D/Linux/build
RUN cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
WORKDIR /root/OpenSiv3D/Linux
RUN cmake --build build

WORKDIR /root/OpenSiv3D/Linux
RUN cmake --install build
```
最初のDockerfileからサイレント修正した点がありますので紹介します。

- `RUN cmake --install build`以降の記述
途中から気づいたのですが、この部分以降はSiv3Dアプリのビルドであるため、イメージをビルドする段階では必要ない処理でしたので削除しました。
- downloadディレクトリの削除
最初はダウンロードしたあと別の場所にインストールするのかと思っていたためdownloadディレクトリにOpenSiv3Dをクローンし、ビルドしていましたがそうではないことがわかったのでdownloadディレクトリは削除しました。

これにて、今回やりたいことは達成されました！　わーい！！

# おまけ
せっかくなので、作成したイメージでコンテナを作成し、Siv3Dを動かしてみたいなと思いました。
## GUIなし
まずはGUIが無いSiv3Dアプリを動かしてみます。
まずはdocker imageを基にしてコンテナを起動します。
```bash
$ docker run -it siv3d
root@192f3ae3ecac:~/OpenSiv3D/Linux#
```
`docker run`を実行するとコンテナが立ち上がりました。
Siv3DのサンプルコードにはGUIを必要としないプログラムがありますので、そちらを利用します。

<details><summary>サンプルコード</summary>

```cpp
/////////////////
//
//      Example non-graphical program
//
# include <Siv3D.hpp> // OpenSiv3D v0.6.11
SIV3D_SET(EngineOption::Renderer::Headless) // Force non-graphical mode
void Main()
{
        Console << U"\n----------------";
        Console << U"Hello, Siv3D!";
        Console << U"You are running a non-graphical program.";
        Console << U"You can code a graphical program in Linux/App/Main.cpp";
        Console << U"----------------\n";
}
//
/////////////////


/*
/////////////////
//
//      Example graphical program
//
# include <Siv3D.hpp> // OpenSiv3D v0.6.11

void Main()
{
        // 背景の色を設定する | Set the background color
        Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

        // 画像ファイルからテクスチャを作成する | Create a texture from an image file
        const Texture texture{ U"example/windmill.png" };

        // 絵文字からテクスチャを作成する | Create a texture from an emoji
        const Texture emoji{ U"🦖"_emoji };

        // 太文字のフォントを作成する | Create a bold font with MSDF method
        const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

        // テキストに含まれる絵文字のためのフォントを作成し、font に追加する | Create a font for emojis in text and add it to font as a fallback
        const Font emojiFont{ 48, Typeface::ColorEmoji };
        font.addFallback(emojiFont);

        // ボタンを押した回数 | Number of button presses
        int32 count = 0;

        // チェックボックスの状態 | Checkbox state
        bool checked = false;

        // プレイヤーの移動スピード | Player's movement speed
        double speed = 200.0;

        // プレイヤーの X 座標 | Player's X position
        double playerPosX = 400;

        // プレイヤーが右を向いているか | Whether player is facing right
        bool isPlayerFacingRight = true;

        while (System::Update())
        {
                // テクスチャを描く | Draw the texture
                texture.draw(20, 20);

                // テキストを描く | Draw text
                font(U"Hello, Siv3D!🎮").draw(64, Vec2{ 20, 340 }, ColorF{ 0.2, 0.4, 0.8 });

                // 指定した範囲内にテキストを描く | Draw text within a specified area
                font(U"Siv3D (シブスリーディー) は、ゲームやアプリを楽しく簡単な C++ コードで開発できるフレ ームワークです。")
                        .draw(18, Rect{ 20, 430, 480, 200 }, Palette::Black);

                // 長方形を描く | Draw a rectangle
                Rect{ 540, 20, 80, 80 }.draw();

                // 角丸長方形を描く | Draw a rounded rectangle
                RoundRect{ 680, 20, 80, 200, 20 }.draw(ColorF{ 0.0, 0.4, 0.6 });

                // 円を描く | Draw a circle
                Circle{ 580, 180, 40 }.draw(Palette::Seagreen);

                // 矢印を描く | Draw an arrow
                Line{ 540, 330, 760, 260 }.drawArrow(8, SizeF{ 20, 20 }, ColorF{ 0.4 });

                // 半透明の円を描く | Draw a semi-transparent circle
                Circle{ Cursor::Pos(), 40 }.draw(ColorF{ 1.0, 0.0, 0.0, 0.5 });

                // ボタン | Button
                if (SimpleGUI::Button(U"count: {}"_fmt(count), Vec2{ 520, 370 }, 120, (checked == false)))
                {
                        // カウントを増やす | Increase the count
                        ++count;
                }

                // チェックボックス | Checkbox
                SimpleGUI::CheckBox(checked, U"Lock \U000F033E", Vec2{ 660, 370 }, 120);

                // スライダー | Slider
                SimpleGUI::Slider(U"speed: {:.1f}"_fmt(speed), speed, 100, 400, Vec2{ 520, 420 }, 140, 120);

                // 左キーが押されていたら | If left key is pressed
                if (KeyLeft.pressed())
                {
                        // プレイヤーが左に移動する | Player moves left
                        playerPosX = Max((playerPosX - speed * Scene::DeltaTime()), 60.0);
                        isPlayerFacingRight = false;
                }

                // 右キーが押されていたら | If right key is pressed
                if (KeyRight.pressed())
                {
                        // プレイヤーが右に移動する | Player moves right
                        playerPosX = Min((playerPosX + speed * Scene::DeltaTime()), 740.0);
                        isPlayerFacingRight = true;
                }

                // プレイヤーを描く | Draw the player
                emoji.scaled(0.75).mirrored(isPlayerFacingRight).drawAt(playerPosX, 540);
        }
}
//
/////////////////
*/
```
</details>

ではこのサンプルコードからSiv3Dアプリをビルドします。
docker内ではroot権限のため、ドルマークではなくシャープマークの方が正しいですが、コメントアウトと見分けがつかないためここではドルマークで記述致します。
```bash
$ pwd
/root/OpenSiv3D/Linux
$ mkdir App/build
$ cd App/build
$ cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
$ cd ..
$ cmake --build build
```
ここまででSiv3Dアプリのビルドは完了です。
最後に実行してみます。
```bash
$ ./Siv3dTest
# 省略
----------------
Hello, Siv3D!
You are running a non-graphical program.
You can code a graphical program in Linux/App/Main.cpp
----------------
```
これにてGUIを必要としないSiv3Dアプリの実行はできました！　やったね！！

## GUIあり
さて、最後にGUIを必要とするSiv3Dアプリをコンテナ上で動かしてみようと思います。
今回のTCPServerとして稼働させるという目的には関係ないですが、ここまで来たらせっかくなのでやってみたい！！ということでやってみました。

コンテナにはGUI出力する機能はありません。(正確にはコンテナ内のubuntuがGUI出力するための設定を知らない)
そのため、起動時にそれらを教えてあげる必要があります。

### `compose.yml`の記述
dockerコンテナを起動する方法として先程は`docker run`を使用しましたが、GUI出力を設定するために様々な設定をするので今回は`compose.yml`に設定を記述し、`docker compose up`コマンドで起動することにします。
[こちら](https://www.idnet.co.jp/column/page_229.html)のサイトを参考にし、私は以下のように記述しました。
```yml
version: "3.9"
services:
  app:
    image: siv3d
    command: /bin/bash -c "/root/src/build.sh"
    volumes:
      - ./src:/root/src
      - /tmp/.X11-unix:/tmp/.X11-unix
      - /mnt/wslg:/mnt/wslg
    environment:
      - DISPLAY=$DISPLAY
      - WAYLAND_DISPLAY=$WAYLAND_DISPLAY
      - XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR
      - PULSE_SERVER=$PULSE_SERVER
```
`./src`にはGUIを出力するようにした`Main.cpp`と、ビルドの手順が書いてあるシェルスクリプトを置いています。
その後、このcompose.ymlが置いてあるディレクトリに移動し、以下のコマンドを実行しました。
```bash
$ docker compose up
```
実行するとコンテナが起動し……。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558342/a45b93d8-d1ce-7ff8-1f74-3b86c4552ca9.png)
無事コンテナ内からGUI出力ができました！！

# 終わりに
Linux版Siv3Dをインストールしたdocker imageの作成は意外と難しいものではありませんでしたが、docker初心者であるが故に色々詰まってしまいました。
今回の記事の内容がどなたかの役に立てばうれしいです。