pong.ursm.jp

Gentoo はどんどん簡単になっている

Gentoo Linux はインストールや運用のハードルが高いディストリビューションとして有名ですが、近年ではビルド済みカーネルamd64/arm64 向けバイナリパッケージが提供されて格段に楽になっています。これにより非力な VPS で Gentoo を運用することも現実的になりました(このブログもメモリ 1GB の Gentoo VM でホストしています)。

実録: Gentoo のインストール

実際に Gentoo のインストールがどれほど楽になっているか見てみましょう。GNOME Boxes で適当な VM を作成して、Gentoo source mirrors からダウンロードした Minimal installation CD を使ってブートします。

Screenshot From 2025-04-19 21-09-36.png

パーティションを作成します。今回作成した VM は legacy BIOS システムのため、BIOS boot partition を 2MB 取って残りを rootfs にします。

gdisk /dev/vda

Screenshot From 2025-04-20 07-10-42.png

ファイルシステムを作成してマウントします。

mkfs.xfs -L root /dev/vda2
mount /dev/vda2 /mnt/gentoo

スワップファイルを作成して有効にしておきます。

cd /mnt/gentoo
fallocate -l 2GiB swapfile
chmod 600 swapfile
mkswap swapfile
swapon swapfile

時計を合わせましょう。

chronyd -q

Gentoo source mirrors から stage3-amd64-systemd-*.tar.xz をダウンロードして展開します。

tar xpvf stage3-amd64-systemd-*.tar.xz --xattrs-include='*.*' --numeric-owner
rm stage3-amd64-systemd-*.tar.xz

/mnt/gentoo/etc/portage/make.conf を編集してバイナリパッケージを利用するための設定をします。binpkg-request-signature はパッケージの署名を検証するオプションです。

FEATURES="getbinpkg binpkg-request-signature"

/mnt/gentoo/etc/portage/binrepos.conf/gentoobinhost.conf を編集して、より最適化されたバイナリを使うようにします (Gentoo x86-64-v3 binary packages available – Gentoo Linux)。

- sync-uri = https://distfiles.gentoo.org/releases/amd64/binpackages/17.1/x86-64
+ sync-uri = https://distfiles.gentoo.org/releases/amd64/binpackages/17.1/x86-64-v3

/etc/resolv.conf をコピーして Gentoo 環境に chroot します。

cp -L /etc/resolv.conf /mnt/gentoo/etc/
arch-chroot /mnt/gentoo

Portage tree のスナップショットを取得します。ついでにニュースを流し読みしておきましょう。

emerge-webrsync
eselect news read new

rsync は遅いので Portage tree の取得元を GitHub にしましょう。まず Git を入れます。

emerge -av dev-vcs/git

Screenshot From 2025-04-19 22-59-16.png

ここで行が紫色、行頭が binary となっているパッケージはバイナリパッケージからインストールされ、緑色で ebuild となっている行のパッケージは通常通りローカルでビルドされます。Gentoo にはパッケージごとにビルドオプションなどを調整する USE フラグという仕組みがあり、これが一致するバイナリパッケージがある場合はそれが使われ、ない場合はビルドにフォールバックされます。今回は nghttp2 だけマッチするバイナリパッケージがなかったのでビルドされています。当然ですが、バイナリパッケージのインストールはアーカイブをダウンロードして展開するだけなので非常に高速です。

Screenshot From 2025-04-19 23-05-11.png

続いて eselect-repository を入れます。

emerge -av eselect-repository

既存の Porage tree を一旦削除し、GitHub から取得し直します。

rm -rf /var/db/repos/gentoo
eselect repository enable gentoo
emerge --sync

Screenshot From 2025-04-19 23-09-33.png

全パッケージを更新しておきましょう。ここでもほとんどのパッケージはバイナリインストールになるので大した時間は掛かりません。

emerge -avuDN @world

タイムゾーンを設定します。

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

ロケールを設定します。

echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen
locale-gen
eselect locale set en_US.utf8
. /etc/profile

ファームウェアをインストールします。ファームウェアにはオープンソース互換ライセンスでないものも含まれているので、/etc/portage/make.conf で受け入れ可能なライセンスを設定します。* は任意のライセンスを受け入れるの意です。

ACCEPT_LICENSE="*"

続いて linux-firmware パッケージをインストールします。

emerge -av linux-firmware

カーネルのインストールに移ります。USE フラグの設定を簡単にするため、flaggie をインストールしましょう。

emerge -av flaggie

今回はブートローダとして grub を使うため、適切な USE フラグを設定して installkernel をインストールします。

flaggie installkernel +grub
emerge -av installkernel

カーネルをインストールします。

emerge -av gentoo-kernel-bin

Screenshot From 2025-04-20 05-47-41.png

USE フラグの変更が必要だと言われるので dispatch-conf しましょう。u で新しい設定を適用します。

dispatch-conf

Screenshot From 2025-04-20 05-49-31.png

あらためてカーネルをインストールします。

emerge -av gentoo-kernel-bin

既存の gentoo-sources はカーネルソースだけをインストールして後のコンフィグとビルドは自分で頑張らないといけなかったのですが、gentoo-kernel-bin はカーネルイメージそのものをインストールします。つまり、これだけでカーネルのインストールはおしまいです。

この新しい方式のカーネルパッケージを Distribution Kernel と呼びます。Distribution Kernel には現状 vanilla-kernel(Gentoo のパッチセットが当たっていない素のカーネル、ソースコードからビルド)、gentoo-kernel(パッチセットを適用したカーネル、ソースコードからビルド)、gentoo-kernel-bin(パッチセットを適用したカーネル、バイナリインストール)の3種類があります。gentoo-kernel-bin 以外の2つはインストール前にコンフィグをカスタマイズすることもできます。プリセットのコンフィグは Fedora のものが使われているそうです。

Distribution Kernel 環境用の USE フラグがあるので設定しておきましょう。これによってカーネル更新時にカーネルモジュールが自動でリビルドされるようになります。

flaggie +dist-kernel

USE フラグを変更したので影響のあるパッケージをリビルドしましょう。

emerge -avuDN @world

/etc/fstab を設定します。

LABEL=root	/	xfs	defaults,noatime	0 1
/swapfile	none	swap	sw	0 0

ルートパスワードを設定して作業用アカウントを作成します。

passwd
useradd -m -G wheel,portage ursm
passwd ursm

sudo をインストールして設定します。

emerge -av sudo
visudo

wheel グループに対して sudo を許可するようにしましょう。NOPASSWD: ALL はお好みで。

%wheel ALL=(ALL:ALL) NOPASSWD: ALL

Systemd の初期設定を行います。

systemd-machine-id-setup
systemd-firstboot --prompt

ブートローダをインストールします。

grub-install /dev/vda

systemd-networkd の設定ファイル /etc/systemd/network/50-dhcp.network を作成します。

[Match]
Name=en*

[Network]
DHCP=yes

chroot 環境を抜けて reboot します。

^D
reboot

Screenshot From 2025-04-20 07-38-54.png

作業用アカウントでログインして、ネットワークを使える状態にします。

sudo systemctl enable --now systemd-networkd

これでインストールは完了です。ね、簡単でしょう?


「ビルドせずして何が Gentoo か」という声もあろうかと思うのですが、別に Gentoo はバイナリベースのディストリビューションになろうとしているわけではありません。基本はビルドで、オプションとしてバイナリパッケージという選択肢が提供されただけです。冒頭に挙げた非力な VPS のように、これまで Gentoo の運用が難しかった環境への導入の道が開けますし、初回インストールはバイナリインストールで手早くセットアップして後からシステム全体をビルドし直すようなやり方も取れるようになります。まさに "Choice is another Gentoo design principle" を体現する動きと言えるのではないでしょうか。

zellij-foreman のご紹介

Screenshot From 2025-04-18 23-50-39.png

最近の Rails では rails server 以外に複数のプロセスを起動して開発を行うスタイルが一般的になっています。jsbundling-rails や cssbundling-rails, dartsass-rails などですね。この場合 bin/dev を叩くと foreman でまとめてプロセスを起動してくれるのですが、ここで困るのが foreman だと binding.irb や debugger がまともに使えないことです。

この問題を解決するために zellij-foreman というものを作りました。これはターミナルマルチプレクサ Zellij のプラグインで、Procfile を読み込んでプロセスを起動する機能を提供します。起動したプロセスはそれぞれ独立したペインに割り当てられるため、debugger も問題なく使えます。

使い方は Rails.root で zellij を起動した状態で以下のコマンドを実行するだけです。終了は Ctrl+q です。

$ zellij plugin --configuration procfile=Procfile.dev -- https://github.com/ursm/zellij-foreman/releases/download/v0.1.2/zellij-foreman.wasm

是非お試しください。

Hotwire 探訪: reform controller

ユーザの入力に応じて動的に表示を切り替えたい場面はよくあります。例えば「その他」を選択したときだけ自由入力欄を表示したい、とかですね。これ簡単そうに見えて意外と難しいというか、考えることが多いのです。動的にということで Stimulus を使うまではよいとしても、コントローラをどの粒度で作るのか、処理をクライアントサイドで閉じるのか Turbo Frame などと組み合わせるかなど常日頃悩ましく思っていました。色々試してみて、こういう状況で使えるシンプルで汎用的なテクニックを見つけたのでご紹介します。

まずこんな感じの Stimulus コントローラを作ります。

// app/javascript/controllers/reform_controller.js
import { Controller } from '@hotwired/stimulus';

export default class ReformController extends Controller {
  static values = {
    visitOptions: Object
  };

  visit() {
    const data = new FormData(this.element);
    const url  = new URL(location.href);

    url.search = new URLSearchParams(data).toString();

    Turbo.visit(url.toString(), this.visitOptionsValue);
  }
}

このコントローラは form 要素に挿して使います。

<%# app/views/wishes/new.html.erb %>
<%= form_with(
  model: @wish,

  data: {
    controller:                 'reform',
    reform_visit_options_value: { frame: 'additional_input' }.to_json
  }
) do |f| %>
  <%= f.select :kind, [
    ['',     'money']
    ['',     'power'],
    ['名声',   'fame'],
    ['その他', 'other']
  ], **{
    data: { action: 'reform#visit' }
  } %>

  <%= turbo_frame_tag 'additional_input' do %>
    <%= if @wish.kind == 'other' %>
      <%= f.text_field :other_text %>
    <% end %>
  <% end %>

  <%= f.submit %>
<% end %>

Rails 側のコントローラはこんな感じです。

# app/controllers/wishes_controller.rb
class WishesController < ApplicationController
  def new
    @wish = Wish.new(wish_params)
  end

  private

  def wish_params
    params.fetch(:wish, {}).permit(:kind, :other_text)
  end
end

これですべてです。どういう風に動くかを見てみましょう。まず、ビューの select を選択したときに reform#visit が呼ばれます。

  <%= f.select :kind, [
    ['',     'money']
    ['',     'power'],
    ['名声',   'fame'],
    ['その他', 'other']
  ], **{
    data: { action: 'reform#visit' }
  } %>

visit は現在の form の内容をクエリ文字列にシリアライズして、今いるページの URL にくっ付けて Turbo.visit() で送信します。今回 turbo_visit_options_value{ frame: 'additional_input' } なので、返ってきた HTML は additional_input という名前の Turbo Frame に挿入されます。

  visit() {
    const data = new FormData(this.element);
    const url  = new URL(location.href);

    url.search = new URLSearchParams(data).toString();

    Turbo.visit(url.toString(), this.visitOptionsValue);
  }
<%= form_with(
  model: @wish,

  data: {
    controller:                 'reform',
    reform_visit_options_value: { frame: 'additional_input' }.to_json
  }
) do |f| %>

「今いるページ」というのはつまり wishes#new です。ここで wish_params を使ってモデルを初期化することで、ユーザの入力内容を反映した状態でフォームが描画されます。つまり、現在の入力内容を元に wishes#new を丸ごと再描画してその一部を Turbo Frame にはめ込んでいるわけです。

  def new
    @wish = Wish.new(wish_params)
  end
  <%= turbo_frame_tag 'additional_input' do %>
    <%= if @wish.kind == 'other' %>
      <%= f.text_field :other_text %>
    <% end %>
  <% end %>

注意点として、wishes#new を直接表示したときは params が空っぽなので params.requireparams.expect だとエラーになります。params.fetch で params が空のときのケアをしてやる必要があります。

  def wish_params
    params.fetch(:wish, {}).permit(:kind, :other_text)
  end

別の例を見てみましょう。一覧の各行にチェックボックスがあって、チェックを付けると一括削除ボタンが active になるような画面を考えます。まず先の reform controller に少し手を入れて、form 要素を target でも指定できるようにします。

// app/javascript/controllers/reform_controller.js
import { Controller } from '@hotwired/stimulus';

export default class ReformController extends Controller {
  static values = {
    visitOptions: Object
  };

  static targets = [
    'form'
  ];

  visit() {
    const form = this.hasFormTarget ? this.formTarget : this.element;
    const data = new FormData(form);
    const url  = new URL(location.href);

    url.search = new URLSearchParams(data).toString();

    Turbo.visit(url.toString(), this.visitOptionsValue);
  }
}

これで form 要素と入力項目が離れていても使えるようになりました。これを使って一覧画面を作ります。

<%# app/views/wishes/index.html.erb %>
<div
  data-controller="reform"
  data-reform-visit-options-value="<%= { frame: 'bulk_destroy' }.to_json %>"
>
  <%= form_with(
    url:    bulk_destroy_wishes_path,
    method: :post,
    id:     'bulk_destroy',
    data:   { reform_target: 'form' }
  ) do |f| %>
    <%= turbo_frame_tag :bulk_destroy do %>
      <%= f.submit '一括削除', disabled: params[:ids].blank? %>
    <% end %>
  <% end %>

  <table>
    <% @wishes.each do |wish| %>
      <tr>
        <td>
          <%= check_box_tag 'ids[]', wish.id, **{
            form: 'bulk_destroy',
            data: { action: 'reform#visit' }
          } %></td>
        </td>

        <td><%= wish.kind %></td>
      </tr>
    <% end %>
  </table>
</div>

チェックボックスをトグルすると reform#visit が呼ばれ、form の内容を元に今の画面を再描画し、最終的に Turbo Frame が更新されて全部辻褄が合う…という一連の流れがおわかりいただけるでしょうか。画面ごとに固有の JavaScript を書く必要もなく、すべてのロジックがサーバサイドに集約されています。

リクエストを GET で送る仕組みの制約として、あまりに巨大なデータやファイルのアップロードは扱えません。そのような場合は万葉の大場さんが紹介されている「Ghost Form パターン」が参考になるかもしれません。

皆さまの快適な Hotwire ライフの一助となれば幸いです。