はじめに
Steam で Linux 向けアプリをビルド・配布する場合、GLIBC の互換性問題を回避するため、できるだけ古い(※ただし古すぎない)Linux 環境でビルドすることが望ましいとされています。
以前、以下の記事にて LXD コンテナを用いたビルド環境の構築方法を解説しました。
しかし、最近になってこのコンテナ環境が何故か正常に動作しなくなってしまったため、Docker ベースの環境へ移行することにしました。
また、Linux 版アプリをリリースした際、Valve のエンジニアの方から
「SDL2 をスタティックビルドした方がよい」
というアドバイスをいただいたため、本記事ではその対応も含めています。
本記事では、Ubuntu 18.04 相当の環境を Docker 上に構築し、SDL2 を含むライブラリをスタティックビルドするための手順をまとめます。
Docker で SDL2 をスタティックビルドするのはやや手間がかかるため、その手順を共有します。(Docker 関連の基本的な操作自体は ChatGPT 等で容易に調べられますが、それでも数回のトライ&エラーが必要でした)
なぜ Ubuntu 18.04 なのか
Ubuntu 18.04(glibc 2.27)は、Steam 向け Linux バイナリの互換性を確保するうえで、現在でも事実上の下限ラインとして扱われることが多い環境です。
これより新しいディストリビューションでビルドすると、生成されたバイナリが新しい glibc に依存してしまい、古い環境(特に Steam Deck や一部の Linux デスクトップ環境)で実行できない問題が発生する可能性があります。
一方で、18.04 は依然として十分に新しく、ビルドツールや依存ライブラリの入手性も良好であるため、「最大限の互換性と現実的な開発効率のバランスが取れた選択肢」と言えます。
環境構築
ホスト環境は Ubuntu Linux (20.04 / 22.04 OK) の前提で記します。
macOSでも構築可能ですが一部読み替えが必要です。
ディレクトリ構成
~/steam-docker/
├─ docker/
│ └─ ubuntu18-static/
│ ├─ Dockerfile
│ └─ build.sh
└─ out/
上記構成を作成する手順:
mkdir ~/steam-docker
cd ~/steam-docker
mkdir docker
mkdir docker/ubuntu18-static
touch docker/ubuntu18-static/Dockerfile
touch docker/ubuntu18-static/build.sh
mkdir out
Docker をインストール
sudo apt install docker.io
sudo usermod -aG docker $USER
上記手順を実行後、一度再ログイン(sshの切断、再接続)が必要です。
インストール確認:
docker ps
Dockerfile 作成
Dockerfile を開き、
vi docker/ubuntu18-static/Dockerfile
以下の内容をコピペ:
# syntax=docker/dockerfile:1.6
FROM ubuntu:18.04
ENV DEBIAN_FRONTEND=noninteractive
ENV PREFIX=/opt/sdl-static
# ビルド前提モジュールがあればここで追加:
RUN apt-get update && apt-get install -y \
build-essential \
gcc g++ make cmake pkg-config \
git \
curl \
zip \
autoconf automake libtool \
nasm yasm \
libasound2-dev libpulse-dev libaudio-dev \
libx11-dev libxext-dev libxrandr-dev libxcursor-dev \
libxinerama-dev libxi-dev \
libgl1-mesa-dev libdrm-dev libgbm-dev \
libfreetype6-dev libharfbuzz-dev \
libpng-dev libjpeg-dev libtiff-dev libwebp-dev \
libogg-dev libvorbis-dev libflac-dev libmpg123-dev \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
# ---- SDL2 ----
RUN git clone --branch release-2.30.10 --depth 1 https://github.com/libsdl-org/SDL.git SDL2 && \
cd SDL2 && \
./configure \
--prefix=$PREFIX \
--enable-static \
--disable-shared \
--disable-joystick \
--disable-haptic \
--disable-sensor && \
make -j$(nproc) && make install
# ---- SDL2_image ----
RUN git clone --branch release-2.8.2 --depth 1 https://github.com/libsdl-org/SDL_image.git SDL2_image && \
cd SDL2_image && \
./autogen.sh && \
./configure \
--prefix=$PREFIX \
--enable-static \
--disable-shared \
--disable-imageio \
--disable-avif && \
make -j$(nproc) && make install
# ---- SDL2_mixer ----
RUN git clone --branch release-2.8.0 --depth 1 https://github.com/libsdl-org/SDL_mixer.git SDL2_mixer && \
cd SDL2_mixer && \
./autogen.sh && \
./configure \
--prefix=$PREFIX \
--enable-static \
--disable-shared \
--disable-music-mod \
--disable-music-opus && \
make -j$(nproc) && make install
# ---- SDL2_ttf (tarball, NOT git) ----
RUN curl -L https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.20.2/SDL2_ttf-2.20.2.tar.gz \
-o SDL2_ttf.tar.gz && \
tar xf SDL2_ttf.tar.gz && \
cd SDL2_ttf-2.20.2 && \
./configure \
--prefix=$PREFIX \
--enable-static \
--disable-shared && \
make -j$(nproc) && make install
ENV PKG_CONFIG_PATH=$PREFIX/lib/pkgconfig
ENV CFLAGS="-I$PREFIX/include"
ENV CXXFLAGS="-I$PREFIX/include"
ENV LDFLAGS="-L$PREFIX/lib"
WORKDIR /work
CMD ["/bin/bash"]
build.sh 作成
build.sh の内容はビルド対象のアプリによってカスタマイズする前提で記しています。
suzukiplan/my-private-repo というプライベートリポジトリでビルドを実行すると game.zip という zip ファイルが生成される前提で、そのビルド成果物をホストコンピュータの out ディレクトリへコピーする例を示します。
#!/bin/bash
set -e
# SSH agent forwarding 経由で プライベートリポジトリを clone
echo "[Clone] my-private-repo"
rm -rf my-private-repo
git clone git@github.com:suzukiplan/my-private-repo.git
cd my-private-repo
echo "[Build] my-private-repo"
export PKG_CONFIG_PATH=/opt/sdl-static/lib/pkgconfig
make
# ビルド生成されたものを out ディレクトリへコピー
mkdir -p /out
cp -v game.zip /out/
プライベートの Makefile では、環境変数 PKG_CONFIG_PATH を用いて、Dockerコンテナ上でスタティックビルドされた SDL2 を用いるように指定してビルドします。
以下、Makefile で指定するコンパイルオプション設定部分の抜粋です。
SDL_PREFIX = /opt/sdl-static
PKG_CONFIG = pkg-config
PKG_CONFIG_PATH := $(SDL_PREFIX)/lib/pkgconfig
CPP_FLAGS += -O2
CPP_FLAGS += -pie
CPP_FLAGS += -fno-rtti
CPP_FLAGS += -fexceptions
CPP_FLAGS += -std=gnu++17
CPP_FLAGS += -ftls-model=global-dynamic
# スタティックSDLを用いるコンパイルオプションの設定
CPP_FLAGS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) $(PKG_CONFIG) --cflags sdl2 SDL2_image SDL2_mixer SDL2_ttf)
CPP_FLAGS += -I$(SDL_PREFIX)/include
CPP_FLAGS += -I./src
CPP_FLAGS += -DLINUX
CPP_FLAGS += -c
以下、Makefile で指定するリンケージオプションの抜粋です。
${EXEC_FILE}: ${OBJECTS}
g++ -o $@ ${OBJECTS} \
-Wl,--as-needed \
-Wl,--strip-all \
-Wl,-rpath,'$$ORIGIN' \
$(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) $(PKG_CONFIG) --libs --static sdl2 SDL2_image SDL2_mixer SDL2_ttf) \
-static-libgcc \
-static-libstdc++ \
-L. -lsteam_api
余分な依存関係を削除するため
-Wl,--as-neededと-Wl,--strip-allを行いつつ、環境変数PKG_CONFIG_PATHを用いてスタティック版 SDL2 の利用に必要なコンパイル・リンクオプションを設定しています。また、
-static-libgccと-static-libstdc++を指定してスタティック版 CRT を利用しておくことも将来の互換性維持のためには重要です。
ビルド実行
SSH エージェントを設定
プライベートリポジトリをビルドしたいのですが、Dockerイメージ内に秘密鍵を置くのはセキュリティ的に危険なので、SSH エージェントを準備します。(public リポジトリなら不要)
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa
以下のコマンドを実行して SSH キーが表示されれば OK です。
ssh-add -l
この手順はマシンリブートの都度実行する必要があります。
面倒な場合、.bashrc / .zshrc に以下を追加してください:
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" >/dev/null
ssh-add ~/.ssh/id_rsa </dev/null
fi
build.sh を実行
docker run --rm \
-e SSH_AUTH_SOCK=/ssh-agent \
-v $SSH_AUTH_SOCK:/ssh-agent \
-v "$PWD":/work \
-v "$PWD/out":/out \
baf-ubuntu18-static \
bash -c 'mkdir -p /root/.ssh && ssh-keyscan github.com >> /root/.ssh/known_hosts && ./docker/ubuntu18-static/build.sh'
正常に実行できれば out/game.zip が生成されているはずです。
~/steam-docker/
├─ docker/
│ └─ ubuntu18-static/
│ ├─ Dockerfile
│ └─ build.sh
└─ out/game.zip 👈 コレ
運用についての補足
新規アプリ追加時の対応
私は次のように運用しています:
-
build.shをアプリ毎に作成(ex:build_app1.sh,build_app2.sh,...) - アプリ毎の zip ファイル名を変える(ex:
app1.zip,app2.zip,...) - Docker イメージは全アプリ共通
最低 GLIBC バージョンが上がった場合
もしも最新の SteamOS が最小 GLIBC のバージョンを引き上げた場合、Dockerfile の FROM を引き上げてリビルドしたものをアップデート配信することで最新の SteamOS への追従が可能になります。