リビングPC向けにSteamOSをフォークした過程
(iliana.fyi)- SteamOS 3 “Holo” は Steam Deck 向けの Arch ベースのディストリビューションだが、リビングPCの suspend 復帰問題 を直すにはカーネルコミットを巻き戻し、rootfs イメージまで自前でフォークする必要があった
- 更新構造は、非アクティブなパーティションに新しい 読み取り専用 rootfs をインストールしてから再起動する A/B 方式で、
/etcは overlayfs により変更分を保持する - Valve のカーネルパッチは、pacman ソースミラーにある
linux-neptune-61-6.1.52.valve9-1.src.tar.gzのような ソース tarball から bare Git リポジトリを複製し、独自タグと PKGBUILD でパッケージをビルドする流れになっている - rootfs の再パッキングは、SteamOS の RAUC バンドルから
rootfs.img.caibxを抽出してイメージ化し、Btrfs UUID の変更・パッケージ差し替え・buildid変更・更新 URL と RAUC 証明書の差し替えを経て、再び RAUC bundle としてまとめる作業である - 独自 Web サーバーが
live.jsonを提供し、steamos-atomupdのQueryUrl、ImagesUrl、MetaUrlを変更すれば、既存の SteamOS インストールも独自イメージへ更新できる
SteamOS をリビングPC向けにフォークした理由
- SteamOS 3 “Holo” は、Valve Software の携帯型 PC ゲーム機 Steam Deck 向けの Arch ベース Linux ディストリビューション である
- 更新方式は、新しい読み取り専用 rootfs を非アクティブなパーティションへダウンロードし、そのパーティションから再起動する A/B アトミックアップデート 構造である
- ユーザーは
steamos-devmodeを実行して rootfs のロックを解除し、pacman データベースを正常化すれば、通常の Linux ディストリビューションのように扱える - 目標は、
steamos-devmodeで簡単に回避するのではなく、rootfs イメージ自体を修正できる きちんとしたフォーク を作ることだった - リビングPCでは SteamOS はほぼ動作したが、suspend 復帰 だけが失敗した
- 同じコンピューターで mainline または stable カーネルを使う他のディストリビューションでは suspend 復帰が動作する
- Valve のカーネルソースを見つけて
git bisectを実行した結果、Steam Deck ハードウェアの suspend 復帰を直すためと思われるコミットが、この PC では問題を引き起こしていた - そのコミットを巻き戻して自前でカーネルをビルドする必要があったことが、この一連の作業の直接の理由だった
- Arch などをそのまま使う選択肢もあったが、ゲーム実行用 Linux ディストリビューションを手直しするなら、Valve がテストしたパッケージ群に依存するほうを好んだ
SteamOS のパーティションと更新構造
- SteamOS システムは 8 個のパーティション を使う
- EFI system partition には stage 1 bootloader と A/B パーティションセット選択用メタデータがある
- 各 A/B セットには stage 2 bootloader である GRUB、root filesystem、
/varパーティションがある - 残りのディスク領域は単一の
homeパーティションが占める
- 起動時には複数の pseudo-filesystem も追加でマウントされる
/var/log、/root、/nixなど、ほぼ 12 個のディレクトリが/home/.steamos/offloadから bind mount され、データを永続化する
/etcは overlayfs で処理される- 変更分は
/var/lib/overlays/etc/upperに保存される machine-idや NetworkManager の接続設定のように、通常/etcに残るべき項目は保持される- 手を加えていない設定ファイルは更新されうる
- この方式により、パッケージマネージャのロジックなしでも A/B パーティション構造で設定保持と更新を両立している
- 変更分は
- システム更新は、Steam クライアントまたはターミナルユーザーが
steamos-updateを実行すると開始される- このコマンドは Python プログラム
steamos-atomupd-clientを実行する - クライアントは現在の OS 情報とユーザーの更新チャネル設定を、
/etc/steamos-atomupd/client.confの URL に送って新しい更新があるか確認する
- このコマンドは Python プログラム
- 新しい更新がある場合、サーバーは RAUC bundle のパスを返す
- クライアントはバンドルをダウンロードして
rauc installを実行する - RAUC はバンドル署名を検証し、
rootfs.img.caibxを探す casync extractで新しいイメージ断片をダウンロードし、非アクティブな rootfs パーティションに書き込む- post-install スクリプトは、アクティブな
/varから非アクティブな/varへデータを選択的に同期し、EFI system partition の stage 1 bootloader 設定を変更して新しいパーティションセットから起動させる
- クライアントはバンドルをダウンロードして
Valve のカーネルソースからパッケージを作る
- Valve は SteamOS で大きく修正した Linux カーネルを使っており、ソースはダウンロード可能である
- 現在の SteamOS イメージのソースは、Valve の pacman ミラーにある
sources/holo-3.5とsources/jupiter-3.5で見つかる - 執筆時点の stable イメージのカーネルは
6.1.52-valve9-1-neptune-61で、そのソース tarball は 2.9GiB ある - tarball が大きい理由は、Linux の Git ツリー全体が含まれているためである
- tarball の中には
PKGBUILD、config、config-neptune、archlinux-linux-neptune/などが含まれる archlinux-linux-neptune/は、そのまま作業できる通常の working tree ではなく bare repository である
- tarball の中には
- PKGBUILD は
git+ssh://git@gitlab.steamos.cloud/jupiter/linux-integration.git#tag=$_tagという形式の非公開 GitLab リポジトリをソースとして指している- 直接 clone したりコミットリンクをたどったりはできない
makepkgのソースから、各タグに対応する完全なコミット履歴入りスナップショットを取得できる- この構造のおかげで、リビングPCの suspend を壊しているコミットを bisect できた
- 作業方法としては、bare repository を通常の working tree として clone し、独自ブランチとタグを維持する流れになる
- 例としては、
6.1.52-valve9タグからmy-branchを作るやり方である - 独自変更は別の Git ホストに上げ、PKGBUILD の source をそのリポジトリと独自タグに変更する
- 例のリポジトリとして linux がある
- 例としては、
makepkgでカーネルパッケージを作成できるmakepkg MAKEFLAGS=-j$(nproc)や/etc/makepkg.confの更新は、小さな VM でない場合に有用である- 見た範囲では、SteamOS 特化パッケージも最初の source に Git リポジトリを使う同様の構造だった
- 後続の手順を簡単にするため、独自 pacman repo も構成した
- パッケージをディレクトリに置き、
repo-add $REPO_NAME.db.tar.zst [PACKAGES...]を実行してから Web ホストにアップロードする - この repo は、後で
steamos-devmodeを実行した場合でもツールが正常に動作する助けになる
- パッケージをディレクトリに置き、
root filesystem の取得とマウント
- release engineering スクリプトは見つからなかったため、既存 root filesystem を必要に応じて 再パッキング する方式を選んだ
- 説明もコメントもないスクリプトは fauxlo にある
- SteamOS rootfs イメージを得る一般的な方法は、Steam Deck を購入するか Steam Deck 復旧イメージをダウンロードすることだが、どちらも Steam End User License Agreement への同意が必要である
- 現在のリリース版は、更新システムの fallback URL と見られる snapshot JSON で確認できる
- 執筆時点の stable バージョンは
20231122.1だった - preview チャネル用の別 snapshot JSON もある
- 執筆時点の stable バージョンは
- rootfs のダウンロードは
steamos-atomupd-clientと同じ手順に従う- RAUC bundle である
.raucbファイルをダウンロードする - SquashFS ファイルシステムのバンドルから
rootfs.img.caibxを抽出する casync extractで.castrstore から断片を取得し、rootfs.imgを生成する.castrstore の URL は RAUC bundle URL の.raucbを.castrに置き換えたものである- この動作は
steamos-atomupdにハードコードされている - 自動化スクリプトは fetch-current.sh にある
- RAUC bundle である
- 隣接する
.img.zipと.img.zstファイルは rootfs ではなく、別の起動可能な 復旧イメージ である- 復旧イメージから rootfs パーティションを抽出して次の手順に使うこともできる
- ただし、RAUC と casync で取得したイメージとは bit-for-bit で同一ではなく、更新バンドルを再作成するには結局それらのツールが必要になる
- rootfs を変更する前に filesystem UUID を変更する必要がある
- 既存の SteamOS イメージからカスタムイメージへ更新する際、UUID を変えないと異なる 2 つの filesystem が同じ UUID を持つことになる
- この状態は問題を引き起こしうる
- 例は
btrfstune -fu rootfs.imgである
- Valve は zstd 圧縮を使う Btrfs イメージ を使っている
- 変更中も圧縮を維持するには
mount -o compress=zstd rootfs.img rootfsでマウントする - SteamOS は Btrfs の
readonlysubvolume 属性を使うため、btrfs property set -ts rootfs ro falseで解除する
- 変更中も圧縮を維持するには
- Linux カーネルのようなパッケージ変更は、
/devや/procを必要とするスクリプトを起動する場合があるdevtmpfsとprocを rootfs 配下にマウントする- 起動済みシステムでマウントされるディレクトリへの書き込みが発生しないよう、
/tmp、/run、/var、/homeには tmpfs をマウントする - chroot 内で名前解決ができるよう、ホストの
/etc/resolv.confを bind mount する
パッケージ差し替えとイメージメタデータの変更
- 独自 repository は
/etc/pacman.confの最初の repo 項目として追加する- こうすると Valve repo により新しいバージョンのパッケージがあっても独自パッケージが優先される
- 後で
steamos-devmodeを実行しても独自パッケージを再インストールできる
- 例の repo stanza では
[fauxlo]、Server = https://fauxlo.ili.fyi/pacman/$arch、SigLevel = Neverを使うSigLevel = Neverはパッケージ署名がなくても許可する- GPG 署名済みパッケージをインストールするには pacman keyring を埋める必要がある
/etc/pacman.d/gnupgの空 keyring をいじる代わりに、tmpfs 上の新しい keyring を作って埋める方法が使われている
- パッケージのインストールは
pacman --sysroot rootfs --noconfirm -Sy linux-neptune-61の形で行う- 実際のスクリプトでは
-yを避け、独自 repo データベースだけを pacman の後段で同期している - これにより他の repository の状態を元イメージがビルドされた時点に固定できる
- イメージ diff に現れる変更を減らすための選択である
- 実際のスクリプトでは
steamos-atomupdは現在のイメージバージョンと build ID を/lib/steamos-atomupd/manifest.jsonから読み取り、なければ/etc/os-releaseを使う- サーバーが提供する更新の build ID が現在のイメージと同じなら更新を拒否する
- どのイメージを実行中かを識別するのにも役立つ
- build ID は必ず
YYYYMMDD.N形式 でなければならない- 形式が合わないと
steamos-atomupdは Python traceback とともに終了する - 手動で増分管理を避けるため、
NにHHMMSSや Unix timestamp を入れる方法が使える manifest.jsonのbuildidとos-releaseのBUILD_IDを一緒に変更する- この作業用の Bash スクリプト断片は repack.sh にある
- 形式が合わないと
- RAUC は信頼設定に X.509 証明書 を使う
- 信頼する証明書は
/etc/rauc/keyring.pemにある - 単純な self-signed certificate で十分である
- 新しい証明書を
rootfs/etc/rauc/keyring.pemにインストールする
- 信頼する証明書は
rootfs/etc/steamos-atomupd/client.confの URL も独自サーバー向けに変更するQueryUrlImagesUrlMetaUrl
- 5GiB の Btrfs イメージ容量を超えない限り、ほかの変更も可能である
- 例えば、ネットワーク上で
hostname.localによって SteamOS デバイスを見つけたいなら、rootfs/usr/lib/systemd/resolved.conf.d/00-disable-mdns.confを削除できる /etcoverlay 設定で override することもできるが、面倒だと判断した
- 例えば、ネットワーク上で
- イメージなしで簡単にできる変更は、原則として rootfs に入れないほうがよい
- Firefox を rootfs にインストールすることはできる
- ただし Firefox のセキュリティ更新のたびにイメージを再パックし直す必要がある
rootfs のアンマウントと RAUC bundle の作成
- 変更が終わったら filesystem を再び read-only としてマークする
btrfs property set -ts rootfs ro true
- 未使用ブロックは
fstrim -v rootfsで discard する - アンマウントには
umount --recursive rootfsが便利である- 先にマウントした pseudo-filesystem もまとめて処理できる
- RAUC bundle を作る前に casync store と blob index を生成する
- 例は
casync make --store=rootfs.img.castr bundle/rootfs.img.caibx rootfs.imgである
- 例は
- RAUC bundle には 3 つのファイルが必要である
manifest.raucmrootfs.img.caibx- filesystem UUID を含む
UUID
manifest.raucmには更新情報と rootfs イメージ情報を記述するcompatible=steamos-amd64version=$versionsha256sizefilename=rootfs.img.caibx
UUIDファイルはblkid -s UUID -o value rootfs.img >bundle/UUIDで作成する- 3 つのファイルを準備したら
rauc bundleを実行する--signing-keyring、--cert、--keyに証明書と鍵を指定する- 生成物は
rootfs.img.raucbである
rootfs.img.raucbとrootfs.img.caibxは、client.confのImagesUrlが指す Web サーバーへアップロードする- 2 つのファイルは同じディレクトリに置く必要がある
独自更新サーバーと適用
QueryUrlとMetaUrlに使う Web サーバーは JSON ファイルを提供する必要がある- 単純な構成では
live.json1 つで十分である.minor.candidates[0].imageオブジェクトは、イメージ内の/lib/steamos-atomupd/manifest.jsonと一致している必要があるupdate_pathは、更新クライアントがImagesUrlの後ろに付けて bundle をダウンロードするパスである
- 例の Caddy 設定では、
steamos-atomupdがQueryUrlとMetaUrlに送るリクエストをlive.jsonへ rewrite する/updatesを/live.jsonに rewrite する/meta/*/*/*/*.jsonと/meta/*/*/*/*/*.jsonを/live.jsonに rewrite するfile_server browseを使う
- 実際の SteamOS の
QueryUrlとMetaUrlには、より多くのロジックがあるようだが、この構成だけでもsteamos-atomupdは新しい更新を見つけられる - すでに告知済みのイメージが現在実行中なら更新を避けるロジックがある
- 既存の SteamOS インストールを独自イメージへ更新するには、
/etc/rauc/keyring.pemと/etc/steamos-atomupd/client.confを修正すればよいsteamos-readonly disableは不要である- 変更は
/etcoverlay に入る steamos-update実行後は、/var/lib/overlays/etc/upperからそれらの変更を整理することを検討してよい
- Valve の復旧イメージの 1 つを修正し、rootfs を独自イメージに差し替えれば、改変版 SteamOS をインストールすることもできそうだが、この方法は未検証である
1件のコメント
Hacker News のコメント
自分が持っているデバイスのソフトウェア/OSを深くカスタマイズするこういう記事は好き。Steam Deckでは Tivoization を心配しなくていい点もうれしい
記事でいちばん興味深かったのは
/nixパーティションだった。Steam Deckが nixpkgs をサポートしているとは知らなかったが、さらに調べてみると、デフォルトインストールではないものの、OS全体をフォークしなくてもデバイスに載せることはできるようだNixストアは書き込み可能な任意の場所に置けるし、
$PATHをシンボリックリンクのディレクトリに向けるよう変更すればいい本当に丁寧で興味深い記事。個人的には絶対ここまではやらないと思う
Linuxを触った経験もRaspberry Piを使っていた頃がすべてで、それもせいぜい1%くらいなので、筆者はすごいと思う
https://github.com/kiler129/relax-intel-rmrr に似ているが、自分のリポジトリではない
根本原因はメーカーのROMアップデートでしか解決できないが、自分が使っている古いDL360はHPEのサポートが終了している
パッチ自体は1行の変更だが、カーネル更新が面倒。SRPMを取得する必要があり、GitリポジトリがないのでSRPMを展開してパッチを適用し、再ビルドしてインストールしなければならない
すでに SteamOS の要素をベースに、PCとコントローラー利用向けに調整されたディストリビューションがある。ChimeraOSはSteam Deck向けの追加ツールであるEmuDeckまで含んでいて、自分の環境ではかなり問題なく動いている
unRaid NASサーバー上でSteam HeadlessをよくできたDockerイメージとして動かし、WindowsノートPCからMoonlightのようなクライアントで接続するためにGPUを注文した
うまくいけば、ほとんどアイドル状態のNASがあるのにゲーミングデスクトップ用ハードウェアをまた買うよりずっとよい。ただし使っていないときはNvidiaカードの電力設定をアイドル状態に保つ必要がある。おそらく
nvidia-persistencedの呼び出しで可能だと期待している1: https://github.com/Steam-Headless/docker-steam-headless
1: https://github.com/games-on-whales/gow
https://kubevirt.io/user-guide/virtual_machines/host-devices...
宣言的なクラウドネイティブのゲーム起動だなんて!
kubectl apply -f crysis.yaml今日 RAUC(https://rauc.io/) を知った。ValveがA/Bアップデート方式をどう実装しているのか気になっていた
Netscapeの 流れ星favicon がちょっと恋しくない?
興味深い記事。A/Bアップグレードは少しやりすぎに見える。問題が起きたらライブディストリビューションを起動するか、古いバージョンのリカバリーシステムを別パーティションにインストールすればいいのだから
ここ数年NixOSを使ってからArchに戻ったが、その前も長くArchを使っていたし、筆者の懸念は外れているように思う
Archは確かに非常に真面目で成熟したディストリビューションであり、Valveより信頼できる
Archへ移った理由は パッケージ品質 だ。メインリポジトリは本当に素早く更新され、AURには有用なパッケージが多い
アップグレード失敗後にシステムが自動で復旧することは、現時点の 低管理OS ではほぼ必須に近い
自分は概して NixOSパッケージ の品質は高いと思っている
最近、ゲーム用携帯機の Legion Go を手に入れて、Linux にもう少し触れるようになっている。以前は終わりのないいじり回しによる時間の無駄に見えたし、実際に使いたいものとの互換性も限られていたので避けていた
不変ファイルシステムや、従来の Linux があらゆる任意のソフトウェアに root 権限を簡単に渡してしまうという話を見て、興味が湧いた
今は NixOS を使っているが、確かにいじり回す時間の無駄になり得る一方で、探索用としてはよい。さまざまな構成要素を簡単に試せて、維持しないと決めたら
~/.configの汚染程度を除けば完全に削除できる。インストール前にパッチを当てるのも些細なことなので、ゲーム用携帯機のような特殊なハードウェアで Linux を使えるようにするカーネルパッチも簡単に追加できるJovian という NixOS コミュニティが、Valve の任意の SteamOS tarball 群を GitHub のタグ付きコミットとして再構成しているので、Valve 社員のようにソースを見て回れる。Nix 設定に数行追加するだけで、NixOS の上に自分用の SteamOS コピーをインストールできるようにしている
彼らは明らかに Linux の専門家で、ソースを見ると、電源ボタンの位置をハードコードせずに検査する、といった単純な調整を除けば、Valve のパッケージを手つかずの状態で受け取っていることが確認できる
純粋な SteamOS 体験が欲しいが Valve の更新システムのミラーを自前で運用したくない、あるいは 3GB の tarball を受け取らずに Valve のソースを探索したいなら、Jovian を試す価値がある
インストール手順: https://jovian-experiments.github.io/Jovian-NixOS/getting-st...
Valve ソースミラー: https://github.com/orgs/Jovian-Experiments/repositories?type...
bazzite.gg もこれをとてもうまくやっている。AMD ハードウェアでは 120Hz VRR がそのまま動作し、HDR 対応もアルファテストできる
「Bazzite は Steam Deck の代替 OS として使える OCI イメージであり、デスクトップ PC、リビングのホームシアター PC、複数の携帯型 PC 向けの、すぐゲームできる SteamOS 風環境だ」
https://github.com/ublue-os/bazzite/
興味がなくても README は読む価値がある。含まれているものの一覧が非常に長く、特にゲーマーやストリーマーにとってかなり格好よく便利そうな項目が多い
HN コメントひとつで簡潔かつ完璧に説明できるほど深く掘り下げてはいないが、要点は、root に読み取り専用の検証済み Linux ディストリビューションを置き、その上にパッケージをレイヤーとして載せる方式だ。サーバー側のコンテナから多くの着想を得た構造になっている
従来の Linux よりも安全で、信頼でき、再現可能で、カスタマイズしやすいものを目指している。欲しいパッケージをコンテナマニフェストに書いておけば、アップグレードが出たときにアップグレードを実行し、その上にパッケージを再インストールする
https://universal-blue.org/guide/fork-your-own/
そして不変 OS なので、問題が起きたら以前のイメージにロールバックすることもできる
ほとんどは Steam Deck か、GPU パススルーを適用した仮想マシンで Bazzite を動かしてプレイしたが、本当によくできていた
リビング PC ということは、もう HTPC や「メディアセンター」という表現はあまり使わないのかな?