Gentoo Linux はインストールや運用のハードルが高いディストリビューションとして有名ですが、近年ではビルド済みカーネル や amd64/arm64 向けバイナリパッケージ が提供されて格段に楽になっています。これにより非力な VPS で Gentoo を運用することも現実的になりました(このブログもメモリ 1GB の Gentoo VM でホストしています)。
実録: Gentoo のインストール
実際に Gentoo のインストールがどれほど楽になっているか見てみましょう。GNOME Boxes で適当な VM を作成して、Gentoo source mirrors からダウンロードした Minimal installation CD を使ってブートします。
パーティションを作成します。今回作成した VM は legacy BIOS システムのため、BIOS boot partition を 2MB 取って残りを rootfs にします。
gdisk /dev/vda
ファイルシステムを作成してマウントします。
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
全パッケージを更新しておきましょう。ほとんどのパッケージはバイナリインストールになるので大した時間は掛かりません。
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
USE フラグの変更が必要だと言われるので dispatch-conf しましょう。u で新しい設定を適用します。
dispatch-conf
あらためてカーネルをインストールします。
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
作業用アカウントでログインして、ネットワークを使える状態にします。
sudo systemctl enable --now systemd-networkd
これでインストールは完了です。ね、簡単でしょう?
「ビルドせずして何が Gentoo か」という声もあろうかと思うのですが、別に Gentoo はバイナリベースのディストリビューションになろうとしているわけではありません。基本はビルドで、オプションとしてバイナリパッケージという選択肢が提供されただけです。冒頭に挙げた非力な VPS のように、これまで Gentoo の運用が難しかった環境への導入の道が開けますし、初回インストールはバイナリインストールで手早くセットアップして後からシステム全体をビルドし直すようなやり方も取れるようになります。まさに "Choice is another Gentoo design principle" を体現する動きと言えるのではないでしょうか。
2025-04-20
最近の 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
是非お試しください。
2025-04-19
OmniAuth .config.request_validation_phase = nil
2025-04-10
ユーザの入力に応じて動的に表示を切り替えたい場面はよくあります。例えば「その他」を選択したときだけ自由入力欄を表示したい、とかですね。これ簡単そうに見えて意外と難しいというか、考えることが多いのです。動的にということで 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.require や params.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 ライフの一助となれば幸いです。
2025-01-20