LinuxとかOSSのこととか

Linux (主にNixOS) や OSS に関する取り組みを記述するブログ。

NixOS上で、Linux From Scratch を構築した

はじめに

Linux From Scratch(以下 LFS)は, 自前の Linux システムを構築するプロジェクトです. このプロジェクトでは, 必要なパッケージを 1 つ 1 つ, ソースコードからビルドし, Linux システムを構成します. 本記事は, NixOS 上で LFS を構築し, 一般的なディストリビューションと違った点を紹介する記事です.

次のように構築していきました. はじめに, ハードディスクイメージを作成し, パーティションファイルシステムの作成します. 次に, ソースコードをダウンロードし, LFS システムの内外でビルド, インストールを行い, 段階的にシステムを構築していきます. 最後に, QEMUを用いて, LFS を起動します. 最終結果は, 下の図です. 起動時は GRUB を使用せず, UEFI Shell から Linux 本体を実行しています.

f:id:ykonomi:20210515081758p:plain
最終画面

ドキュメントは, 有志の日本語訳を使用しました. 英語のドキュメントだと途中でやめてしまう可能性があるためです. また, この記事を作成するにあたり, LFS は合計 3 回構築しています. はじめは, コマンドをタイプして入力していたのですが, 2 回目以降はスクリプトにしています. 作成したスクリプト群は, 以下にまとめています. 以下では, このリポジトリを単にリポジトリとして言及することがあります.

github.com

構築するLFSのバージョンとホスト情報

  • LFS Version: 10.1-systemd
  • NixOS Version : 20.09
  • CPU モデル名: AMD Ryzen 7 3700U

インストールしたパッケージ

LFS の構築にあたって, インストールしたパッケージを紹介します. LFS のドキュメントには, 一般ユーザーで作業する部分があります. 本記事では, 作業のしやすさを考慮して, インストールするパッケージを分けました.

システムにインストールしたパッケージは以下です. configuration.nix に記述し, ビルドしました.

  environment.systemPackages = with pkgs; [
    ...
    parted 
    wget
    file
    bison
  ];

また, qemu と OVMF ファームウェアを使うため, 以下のオプションを追加しました.

virtualisation.libvirtd.enable = true;
virtualisation.libvirtd.qemuOvmf = true;

一般ユーザーにインストールしたパッケージは以下です. NixOS では一般ユーザーが使うパッケージは nix-env コマンドを使い, インストールします.

[lfs@nixos:/root]$ nix-env --query
binutils-2.31.1
gnum4-1.4.18
gnumake-4.3
python3-3.9.2

記事の構成

LFS のドキュメントでは, 3 部構成で LFS の構築手順を説明していますが, 本記事では以下の 5 部構成で説明します.

  • part1: ディスクの作成, ソースダウンロード, ユーザー lfs の作成
  • part2: ユーザー lfs による一時ツールの作成
  • part3: chroot 環境の準備と chroot 環境での一時ツールの追加ビルド
  • part4: chroot 環境での LFS システム構築
  • part5: Linux カーネルビルドと起動

一時ツールとは, LFS システムを構築するための一時的なツールです. 例えば, GCC(the GNU Compiler Collection)です. LFS システムには(C で書かれた)カーネルをビルドするため, GCC が必要ですが, その GCC 自体をビルドする GCC が必要になります. この例のように, LFS システムを構築するために必要なツールを先に作成します.

各パートごとの作業の詳細は省きます. ドキュメントの二番煎じになってしまうためです. 記事のメインは, 様々な理由でドキュメントの手順通りにいかなかった内容です. 特に, part2 ではホスト環境のツールを使うため, NixOS 特有の作業があります.

ちなみに, 作業マシンでビルドにかかった時間は以下です. ビルドは, ソースファイルの解凍, インストール時間も含まれます. また, part4 ではビルド物のテストが手順に含まれていたので, そちらの時間も載せています. なお, リポジトリの sbu.txt には詳細な測定結果があります.

part 時間(分)
2 36
3 8
4 build 56
4 check 149
5 7

part1の特記事項

ディスクの作成

ホストのストレージにハードディスクイメージを作成しました. 手持ちに物理的なメディアがなく, 購入するのも億劫であったためです. ディスクの容量ははじめ, 20G で作成したのですが, 途中で足らなくなり 60G で作り直しました.

qemu-img create -f raw lfs.img 60G

パーティションUEFI 対応のもので, parted を用いて作成しました. コマンドはNixOS Manualを参考にしています. また, ファイルシステムを作成する際は, ループバックデバイスを使用しています.

parted lfs.img -- mklabel gpt
parted lfs.img -- mkpart primary 512MiB -8GiB
parted lfs.img -- mkpart primary linux-swap -8GiB 100%
parted lfs.img -- mkpart ESP fat32 1MiB 512MiB
parted lfs.img -- set 3 esp on

losetup -P /dev/loop0 lfs.img 
mkfs.ext4 -L lfs /dev/loop0p1
mkswap -L swap /dev/loop0p2
mkfs.fat -F 32 -n boot /dev/loop0p3

一時ツールの作成用のインストールディレクトリの作成

LFS のドキュメントでは, ダウンロードファイルを sources ディレクトリに保存する方針です. しかし, LFS システムを構築するための一時ツールの構築と LFS システム構築を同じディレクトリで行うことで, ビルド作業に混乱がありました. そのため, 一時ツールのソースは tools-sources, LFS システムのソースは sources に保存しました. 具体的なソースリストは, リポジトリの part1/tools-list にあります.

作業用ユーザー lfs の作成

configuration.nix に以下を追加してビルドします. 作業中にパスワードを入力する煩わしさがあるため, パスワードは設定しないことにしました.

users.groups.lfs = {}; # group: lfs
users.users.lfs = {
  isNormalUser = true;
  home = "/home/lfs";
  extraGroups = [ "wheel" "lfs" ];
};

ユーザーに配布する .bashrc は以下です. 追加行は 9 行と 10 行です. NixOS は一般的なディストリビューションとは違い, /bin, /usr/bin に, ls 等の身近なシェルコマンドがありません. 実際に確かめてみると /bin には sh, /usr/bin に env しかないことがわかります. また, NixOS はパッケージを全て /nix/store 以下の Nix Store で管理しているため, 仮にコマンドがあったとしても, それらは全て, Nix Store へのシンボリックリンクになっています. このため, ls 等のコマンドを lfs ユーザーでも使えるようにするため, 9 行目と 10 行目を追加しています. 9 行目は, configuration.nix 経由でインストールした実行ファイルがあるパスで, 10 行目は, nix-env で lfs ユーザーにインストールした実行ファイルがあるパスです. 注意点は, 一時ツールは tools 以下に保存されるため, そちらを優先して探すようにパスを追加してます.

cat > ~/.bashrc << "EOF"
set +h
umask 022
LFS=/mnt/lfs
LC_ALL=POSIX
LFS_TGT=$(uname -m)-lfs-linux-gnu
PATH=/usr/bin
if [ ! -L /bin ]; then PATH=/bin:$PATH; fi
PATH=/run/current-system/sw/bin:$PATH
PATH=$HOME/.nix-profile/bin:$PATH
PATH=$LFS/tools/bin:$PATH
CONFIG_SITE=$LFS/usr/share/config.site/run/current-system/sw/bin
export LFS LC_ALL LFS_TGT PATH CONFIG_SITE
EOF

part2の特記事項

version_check.sh の修正

LFS のドキュメントには, ホストシステム要件としてスクリプトが用意されています. それを実行することで, 必要なパッケージがホストにインストールされているかを確認できます. NixOS の特徴的な仕組みから, スクリプトを修正する必要がありました. それらを次で紹介します.

Shebangsの変更

NixOS には /bin/bash はないため, /bin/sh か /usr/bin/env bash で Shebangs を設定します. 以下は env の例です.

#! /usr/bin/env bash

シンボリックリンクの作成

一部のコマンドは, 特定のパスにあることが重要になります. そのため, 以下のように, それらのコマンドのシンボリックリンクを作成しました. env は元々あったものです. また, ホストシステム要件にはなかったのですが, file パッケージが必要になったため追加しています.

[root@nixos:~/lfs-in-nixos]# ls -l /usr/bin/
total 4
lrwxrwxrwx 1 root root 30 Mar 19 11:16 awk -> /run/current-system/sw/bin/awk
lrwxrwxrwx 1 root root 66 May 21 02:59 env -> /nix/store/vr96j3cxj75xsczl8pzrgsv1k57hcxyp-coreutils-8.31/bin/env
lrwxrwxrwx 1 root root 31 May  9 19:24 file -> /run/current-system/sw/bin/file
lrwxrwxrwx 1 root root 32 Mar 19 11:16 yacc -> /run/current-system/sw/bin/bison

nix-shell 上でgcc, g++ を実行する

NixOS の GCC パッケージは更に特殊で, nix-env でインストールした GCC では, コンパイルに失敗します. これは, /usr/include, /usr/lib がなく, crt1.o 等のファイルがないためです. そのため, NixOS では, gcc や g++は nix-shell による環境で実行する必要があります. これに合わせて, version_check.sh の GCC に関する部分を以下のように書き換えました. -p がパッケージの指定, --run が非インタラクティブモードでの指定されたコマンドの実行を意味します.

nix-shell -p gcc --run "gcc --version | head -n1"
nix-shell -p gcc --run "g++ --version | head -n1"

echo 'int main(){}' > dummy.c && nix-shell -p gcc --run "g++ -o dummy dummy.c"
if [ -x dummy ]
  then echo "g++ compilation OK";
  else echo "g++ compilation faield"; fi
rm -f dummy.c dummy

SBU値と並列ビルド数

LFS では, 各パッケージのビルド時間とインストール時間の合計を見積もるため, 最初のパッケージである Binutils のビルド時間とインストール時間の合計を標準ビルド単位(Standard Build Unit; SBU)として使います. 以下は, MAKEFLAGS の値に対する SBU です. ホストシステムの論理プロセッサ数は 8 であるため, 本番では -j8 を指定します.

フラグ 時間(xmys)
-j1 2m43.576s
-j2 1m37.105s
-j4 1m3.539s
-j8 0m58.666s
-j10 1m0.586s

コンパイルエラー解消のため, コンパイルオプションの追加

一時ツールの中には, デフォルトの gcc をそのまま使うとコンパイルエラーになってしまうパッケージがあります. それは BinutilsGCC です. これは, セキュリティ強化のための設定ですが, このままだとビルドできません. そのため, -Wformat -Wformat-security -Werror=format-security をコンパイル時に追加する必要があります. NixOS では, 以下のファイルを作成し, nix-shell で指定することで, それらのフラグを追加できます. hardeningDisable にある"format"がそのためのオプションです. なお, その他の設定はこちらにあります.

cat > gcc_with_options.nix << "EOF"
  with import <nixpkgs> {};
  pkgs.mkShell {
  buildInputs = with pkgs; [ gcc ];
  hardeningDisable = [ "format" ];
}
nix-shell gcc_with_option.nix --run "./configure --hoge && make && make install"

ネイティブコンパイラとクロスコンパイラの両方を必要とするパッケージをビルドする

例えば, ncurses です. パッケージ gcc を指定した nix-shell 環境では, 環境変数 CC や CXX に gcc, g++ が設定されます. これにより, ./configure で gccx86_64-lfs-linux-gnu-gcc の代わりにクロスコンパイラに設定されてしまいます. そのため, CC や CXX を unset しています. また, nix-shell ではパッケージの gcc への PATH が先頭に追加されていました. そのため, PATH の先頭に一時ツールが先に見つかるように新たに PATH を追加してます.

cat > gcc.nix << "EOF"
  with import <nixpkgs> {};
  pkgs.mkShell {
  buildInputs = with pkgs; [ gcc ];
  shellHook = '' 
    export PATH="$LFS/tools/bin:$PATH"
    unset CC
    unset CXX
  '';
}
EOF

part3, part4の特記事項

特記事項はないです. ここからは, Chroot 環境での作業であり, NixOS のパッケージにはアクセスしないため, 他のディストリビューションと同様の構築になります.

part5の特記事項

カーネルの設定

make defconfig による設定のままですが, UEFI 上で起動するため, EFI stub support にチェックが入っていることを確認しました.

QEMUによる実行

起動のため, 以下のコマンドを実行しました. UEFI ファームウェアを使うため OVMF.fd を /nix/store 以下からコピーしました.

qemu-system-x86_64 -bios OVMF.fd -hda lfs.img

起動後 30 秒程度待つと, UEFI Shell が起動します. FS0: でディスクをマウントし, 以下のコマンドを実行すると, Linux が起動します. その後コンソール画面に移行するので, パスワードを入力すれば, 記事冒頭の図の画面になります.

vmlinuz_5.10.17-lfs-10.1-systemd root=/dev/sda1

おわりに

NixOS での LFS を構築したときの違った点を紹介しました. NixOS では, パッケージは全て Nix Store で管理されるため, いくつかの点でやり方を変える必要がありました. NixOS での作業にずいぶん慣れた気がします. 次にやることですが, part5 は駆け足でやってしまったので, そのあたりの理解を深めるために GRUB の設定をしたいです. また, Beyond Linux From Scratchと呼ばれる LFS 構築後のプロジェクトがあります. それは, NixOS とは関係ないですが, せっかく最小限の Linux が手に入ったので, Linux の理解を深めるため, 手を付けたいです.