【Golang】ラズパイ+ go-sdl2 で /usr/bin/ld: -liconv が見つかりませんエラー【静的リンクのビルド時】

# `/usr/bin/ld: cannot find -liconv` on go-sdl2 static build

> デスクトップ環境なし（X11 なし）の RaspberryPi OS Lite 入りラズパイで、スクリーン描画したい。
>
> [SDL](https://ja.wikipedia.org/wiki/SDL) という X11 の代替となるマルチメディア用のライブラリにスクリーンの描画機能があるらしい。ところが Go 言語（以下 Golang）で SDL を利用するために [go-sdl2](https://github.com/veandco/go-sdl2) を使うも、静的リンクでビルドすると「lib-iconv が見つかりません」とエラーが出るのです。
> でも `static` タグなし（`go build .`）だと、ビルドできるし、ターミナルに描画はできるのです。どうしよう。

```shell-session:静的リンクでビルドするとエラーが出る
$ go build -v -tags static -ldflags "-s -w" .
github.com/veandco/go-sdl2/sdl
# github.com/veandco/go-sdl2/sdl
/usr/bin/ld: -liconv が見つかりません
collect2: error: ld returned 1 exit status

$ # 静的リンクしないなら大丈夫
$ go build -v .
$ echo $?
0
```

## TL; DR <sub><sup><sub><sup>（今北産業）</sup></sub></sup></sub>

1. ビルド時に `rpi` タグ(`-tags rpi` を付けます）

    ```diff
    - go build -v -tags static -ldflags "-s -w" .
    + go build -v -tags static -tags rpi -ldflags "-s -w" .
    ```
2. RPi OS (`buster`) は <ruby>`libc6`<rt>C の標準ライブラリ No.6</rt></ruby> に `lib-iconv` をすでに持っています。しかし `go-sdl2` は静的リンク時に Gentoo Linux を想定して [`.a` ファイルが構成されている](https://github.com/veandco/go-sdl2/tree/master/.go-sdl2-libs)ため、別途ラズパイ用のリンクファイルが用意されています。Golang のビルド時に `rpi` タグを指定すると `gcc` はラズパイ用のライブラリをリンクします。
3. 関連 Issue
    - Issue #394 [static build in raspberry](https://github.com/veandco/go-sdl2/issues/394) | go-sdl2 @ github.com

---

## TS; DR <sub><sup><sub><sup>（Raspberry Pi Zero + Raspberry Pi OS Lite + Go + go-sdl2 を使う細かい備忘録）</sup></sub></sup></sub>

- 1024x600@60Hz の 7 inch タッチパネル式 Portable Display
  - CX070PI_70R_50P_CTP
  - V5.0 D190824
  - Amazon で 9 千円弱で購入した Swehoo という怪しい俺様ブランドのメーカー不明品ですが、どうやら Waveshare 社の商品のコピー品のようです。
      - [7inch HDMI LCD](https://www.waveshare.com/wiki/7inch_HDMI_LCD) | Wiki @ WaveShare

上記 7 インチのタッチパネル式ディスプレイに、ラズパイ Zero W をつなげて、Raspberry Pi OS Lite（GUIなし・デスクトップなし版）のターミナル画面（CUI 画面）上に画像を描画する際の備忘録です。

操作は mac から SSH 接続していますが、実行すると手元のターミナルでなく接続されているモニタのターミナルに描画されます。

### SDL2 とは

> SDL (Simple DirectMedia Layer) は、C 言語で書かれたクロスプラットフォームのマルチメディアライブラリである。グラフィックの描画やサウンドの再生などのAPIを提供する。
> オーディオ、キーボード、マウス、ジョイスティック、そして OpenGL および Direct3D を経由したグラフィックスハードウェアへのローレベルなアクセスを提供するよう設計されている。Windows、macOS、Linux、iOS、Android を公式にサポートしている。
> <sub><sup>（[SDL](https://ja.wikipedia.org/wiki/SDL) @ Wikipedia より）</sup></sub>

つまり、デスクトップ含む [X11](https://ja.wikipedia.org/wiki/X_Window_System) などの GUI 系ライブラリがない環境でも、画面に描画したりデバイスからの入力を取得することができるライブラリです。SDL2 を使うと**ターミナル Only の CUI 画面にも画像を出力できます**。

### Go-SDL2 とは

[go-sdl2](https://github.com/veandco/go-sdl2) は、C 言語で書かれた SDL2 の Go 言語によるラッパー（[バインディング](https://ja.wikipedia.org/wiki/%E6%9D%9F%E7%B8%9B_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF))）です。

例えば C 言語版 SDL2 の、とある関数を使いたい場合は、該当する Go-SDL2 のラッパー関数を使うと Go 言語でもモジュールとして使うことができる、という塩梅です。

#### `go-sdl2` の使い方のポイント

「○○○ がしたいなぁ〜」と思ったら、[SDL2 の Wiki](https://wiki.libsdl.org/APIByCategory) で C 言語の関数名を探し、該当する `go-sdl2` の Go 関数を [go-sdl2 のドキュメント](https://pkg.go.dev/github.com/veandco/go-sdl2/sdl)から探して使うことで Go に実装できます。

<details><summary>モニターの画面解像度を取得するまでの調べ方と実装例</summary>
<div>
ここでは「モニターの画面解像度を取得したいなぁ」という場合の具体例を紹介したいと思います。デスクトップなし（RPi OS Lite）の環境を前提とします。

1. 本家 SDL2 の Wiki ドキュメントを開きます。
    - https://wiki.libsdl.org/APIByCategory カテゴリ別 @ 公式 Wiki

    <sub><sup>最初から [`go-dsl2` のドキュメント](https://pkg.go.dev/github.com/veandco/go-sdl2/sdl)で探してもいいのですが、[`go-dsl2` はバージョン違いで仕様が結構変わります](https://github.com/veandco/go-sdl2/blob/master/BREAKING.md)。そのため、本家 SDL2 の API の仕様を確認してから、`go-dsl2` の自分が使っているバージョンのドキュメントを探す方が経験的に失敗が少ない気がします。</sup></sub>

2. 使えそうな関数を探します。
    1. 大カテゴリを見ると `Video` の `Display and Window Management` が一番近そうです。
        - [Display and Window Management](https://wiki.libsdl.org/CategoryVideo)（ディスプレイやウィンドウの管理）
    2. `Functions` 項目（関数名一覧）を見ると `SDL_Get*` の `Getter` 関数がいくつかあります。今回は「取得」したいので `Get` 系にマトが絞れそうです。また、ウィンドウのサイズではなく「画面」サイズなので `Display` を含むものに絞ります。すると以下の 3 つの関数が使えそうです。
        - [SDL_GetCurrentDisplayMode](https://wiki.libsdl.org/SDL_GetCurrentDisplayMode) ... Get information about the current display mode.
        - [SDL_GetDesktopDisplayMode](https://wiki.libsdl.org/SDL_GetDesktopDisplayMode) ... Get information about the desktop's display mode.
        - [SDL_GetDisplayMode](https://wiki.libsdl.org/SDL_GetDisplayMode) ... Get information about a specific display mode.
    3. 各々の関数の説明をみると `SDL_GetCurrentDisplayMode` が近いと思われます。デスクトップなし環境でモニタは 1 つだからです。
        - SDL_GetCurrentDisplayMode ... 現在のモニタの状態に関する情報を取得
        - SDL_GetDesktopDisplayMode ... 現在のデスクトップのモニタの状態に関する情報を取得
        - SDL_GetDisplayMode ... 指定したモニタの状態に関する情報を取得
    4. API の仕様（関数の引数と戻り値の構文）を確認します。
        
        ```c:Syntax
        int SDL_GetCurrentDisplayMode(
            int displayIndex,
            SDL_DisplayMode * mode
        );
        // 戻り値は int 型
        // 第 1 引数は int 型。名は displayIndex。
        // 第 2 引数は SDL_DisplayMode 型オブジェクトのポインタ。名は mode。
        ```
        ```c:Return
        Returns 0 on success or a negative error code on failure;
        call SDL_GetError() for more information.
        // 成功すると 0 が返され、失敗すると -1 以下の数値が返えされる。
        // SDL_GetError() を呼び出すと詳細なエラーが得られる。
        ```
        ```c:FunctionParameters（引数の仕様）
        displayIndex ... the index of the display to query
        mode ... an SDL_DisplayMode structure filled in with the current display mode
        // displayIndex ... 問い合わせ先のモニタ番号
        // mode ... 現在のモニタ情報を埋めるための SDL_DisplayMode 構造体（いわゆる参照渡しで情報を取得する）
        ```
    5. 第 2 引数の <code><a href="https://wiki.libsdl.org/SDL_DisplayMode">SDL_DisplayMode</a></code> 型の構造を確認する。この構造を持ったオブジェクトに、各々の値が代入される。
        
        ```c:DataFields
        Uint32 ... format ... one of the SDL_PixelFormatEnum values; see Remarks for details
        int ... w ... width, in screen coordinates
        int ... h ... height, in screen coordinates
        int ... refresh_rate ... refresh rate (in Hz), or 0 for unspecified
        void* ... driverdata ... driver-specific data, initialize to 0
        // format フィールド ... Uint32 型の SDL_PixelFormatEnum で定義された enum 値
        // w フィールド ... int 型の画面の横の座標 = 画面幅
        // h フィールド ... int 型の画面の縦の座標 = 画面の高さ
        // refresh_rate フィールド ... int 型のリフレッシュレート（単位:Hz）、未定義の場合は 0
        // driverdata フィールド ... void 型のポインタ。モニタドライバ特有のデータで、0 で初期化されていること。
        ```
    6. いよいよ該当する `go-sdl2` の関数を確認
        - [GetCurrentDisplayMode](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.10/sdl#GetCurrentDisplayMode) | v0.4.10 | go-sdl2 @ pkg.go.dev
        - `go-dsl2` が v0.4.10 なので該当するバージョンのドキュメントを確認します。
        
        ```go:GetCurrentDisplayMode
        func GetCurrentDisplayMode(displayIndex int) (mode DisplayMode, err error)
        // GetCurrentDisplayMode returns information about the current display mode. 
        // (https://wiki.libsdl.org/SDL_GetCurrentDisplayMode)
        // GetCurrentDisplayMode は現在のモニタの状態の情報を返します。
        ```
        
        本家と違い、参照渡しではなく `DisplayMode` 型のオブジェクトが返されるようです。

    7. 戻り値の `DisplayMode` 型の構造を確認する。
        - [DisplayMode](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.10/sdl#DisplayMode) | v0.4.10 | go-sdl2 @ pkg.go.dev
        
        ```go
        type DisplayMode struct {
        	Format      uint32         // one of the PixelFormatEnum values (https://wiki.libsdl.org/SDL_PixelFormatEnum)
        	W           int32          // width, in screen coordinates
        	H           int32          // height, in screen coordinates
        	RefreshRate int32          // refresh rate (in Hz), or 0 for unspecified
        	DriverData  unsafe.Pointer // driver-specific data, initialize to 0
        }
        ```
        
        以上により、戻り値の `w` と `H` フィールドを参照すれば画面サイズが取得できそうです。
    8. Go のサンプル
        
        ```go
        // import "github.com/veandco/go-sdl2/sdl"
        func main(){
        	mode, err := sdl.GetCurrentDisplayMode(0) // 最初のモニタ情報を取得
        	if err != nil {
        		log.Fatal(err)
                }
                
                fmt.Println("Width:", mode.W)
                fmt.Println("Height:", mode.H)
        }
        ```

</div></details>

### 検証環境

```shell-session
$ # モデル（Raspberry Pi Zero W H）
$ more /proc/device-tree/model
Raspberry Pi Zero W Rev 1.1

$ # OS 情報(Raspberry Pi OS Lite)
$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Raspbian GNU/Linux 10 (buster)"

$ # カーネル情報
$ uname -a
Linux rpi-keinos 5.10.76+ #1474 Thu Oct 28 13:37:16 BST 2021 armv6l GNU/Linux

$ # Go 情報
$ go version
go version go1.17.2 linux/arm
```

#### 依存 `apt` パッケージのインストール

```bash
sudo apt-get install --yes \
    build-essential \
    libfreeimage-dev \
    libopenal-dev \
    libpango1.0-dev \
    libsndfile1-dev \
    libudev-dev \
    libasound2-dev \
    libjpeg-dev \
    libtiff5-dev \
    libwebp-dev \
    automake
```

#### SDL2 の C ライブラリ・ビルド（X11 なし）

`apt-get install` 経由の SDL2 パッケージは古く、ラズパイ Zero + X11 なしにも最適化されていないのでソースからビルドしました。

```bash
# SDL2 ライブラリ本体のビルド
mkdir ~/SDL2
cd ~/SDL2
wget https://www.libsdl.org/release/SDL2-2.0.16.tar.gz
tar xzf SDL2-2.0.16.tar.gz
cd SDL2-2.0.16
mkdir build && cd build
../configure --host=arm-raspberry-linux-gnueabihf --disable-pulseaudio \
  --disable-esd --disable-video-wayland \
  --disable-video-x11
make install
```
```bash
# IMAGE（SDL2 関連ライブラリのビルド）
cd ~/SDL2
wget https://www.libsdl.org/projects/SDL_image/release/SDL2_image-2.0.5.tar.gz
tar xzf SDL2_image-2.0.5.tar.gz
cd SDL2_image-2.0.5 && mkdir build && cd build
../configure
sudo make install
```
```bash
# Mpeg（SDL2 関連ライブラリのビルド。Mixer のビルド前にビルドする必要がある）
cd ~/SDL2
wget https://www.libsdl.org/projects/smpeg/release/smpeg2-2.0.0.tar.gz
tar xzf smpeg2-2.0.0.tar.gz
cd smpeg2-2.0.0 && mkdir build && cd build
../configure
sudo make install
```
```bash
# Mixer（SDL2 関連ライブラリのビルド）
cd ~/SDL2
wget https://www.libsdl.org/projects/SDL_mixer/release/SDL2_mixer-2.0.4.tar.gz
tar xzf SDL2_mixer-2.0.4.tar.gz
cd SDL2_mixer-2.0.4 && mkdir build && cd build
../configure
sudo make install
```
```bash
# TTF（SDL2 関連ライブラリのビルド）
cd ~/SDL2
wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-2.0.15.tar.gz
tar xzf SDL2_ttf-2.0.15.tar.gz
cd SDL2_ttf-2.0.15 && mkdir build && cd build
sed -i -e 's/have_opengl=yes/have_opengl=no/' ../configure
../configure
sudo make install
```

#### Go で `go-sdl2` 使う際のポイント

1. `go get` でモジュールを `require` する際は `@master` と指定する。

    ```diff
    - go get github.com/veandco/go-sdl2/sdl
    + go get github.com/veandco/go-sdl2/sdl@master
    ```

#### Golang サンプル・コード


```go:go.mod
module github.com/KEINOS/sample

go 1.17

require github.com/veandco/go-sdl2 v0.5.0-alpha.1.0.20211011035448-93229da6a705
```

```go:main.go
package main

import (
	"os"

	"github.com/veandco/go-sdl2/img"
	"github.com/veandco/go-sdl2/sdl"
)

const (
	bmpImagePath = "./sample.bmp" // 適当な ビットマップ画像
	pngImagePath = "./sample.png" // 適当な PNG 画像
)

func run() (err error) {
	var window *sdl.Window
	var surface *sdl.Surface
	var bmpImage *sdl.Surface
	var pngImage *sdl.Surface

	if err = sdl.Init(sdl.INIT_VIDEO); err != nil {
		return
	}
	defer sdl.Quit()

	// Create a window for us to draw the images on
	if window, err = sdl.CreateWindow("Loading images", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, 800, 600, sdl.WINDOW_SHOWN); err != nil {
		return
	}
	defer window.Destroy()

	if surface, err = window.GetSurface(); err != nil {
		return
	}

	// Load a BMP image
	if bmpImage, err = sdl.LoadBMP(bmpImagePath); err != nil {
		return err
	}
	defer bmpImage.Free()

	// Load a PNG image
	if pngImage, err = img.Load(pngImagePath); err != nil {
		return err
	}
	defer pngImage.Free()

	// Hide Cursor
	_,_ = sdl.ShowCursor(sdl.DISABLE)

	// Draw the BMP image on the first half of the window
	bmpImage.BlitScaled(nil, surface, &sdl.Rect{X: 0, Y: 0, W: 400, H: 400})

	// Draw the PNG image on the first half of the window
	pngImage.BlitScaled(nil, surface, &sdl.Rect{X: 400, Y: 0, W: 400, H: 400})

	// Update the window surface with what we have drawn
	window.UpdateSurface()

	// Run infinite loop until user closes the window
	running := true
	for running {
		for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
			switch event.(type) {
			case *sdl.QuitEvent:
				running = false
			}
		}

		sdl.Delay(16)
	}

	return
}

func main() {
	if err := run(); err != nil {
		os.Exit(1)
	}
}
```

#### 静的ビルド

```shell-session
$ ls
sample.bmp  sample.png  go.mod  main.go

$ go mod tidy && go mod download
...

$ go build -v -tags static -tags rpi -ldflags "-s -w" .
...

$ ls
sample.bmp  sample.png  go.mod  main.go  sample

$ ./sample
// ctrl+c するまで画面に表示される
```