BtrfsでUbuntuをセットアップしてTimeshiftを活用

UbuntuFedoraCachyOSなど、さまざまなディストリビューションがありますが、どれも完成度は高くなってきていますね。

Ubuntu、Fedora、CachyOSを触った感想

触った感じで個人的な印象ですが、Windowsなどに例えるとそれぞれ次のようなイメージです。

Ubuntu・・・MacOS
Fedora・・・Windows
CachyOS・・Linux

とにかく完成度が高く、インストール直後からすぐ使えるのはUbuntuですね。デザインも統一されておりシンプルで分かりやすくなっています。ただ、カスタマイズ要素は低めになっており、安定度重視で、道具として使うイメージです。
CachyOSはキビキビ動くんですが、ある程度は設定が必要になります。ただ自由度は高いですね。またゲーム用途ならCachyOSが一番でしょう。
Fedoraはそれらの中間といった雰囲気です。
普段Windowsを使っている人がLinuxを試してみるなら、Ubuntuが一番良いと思います。

ファイルシステムについて

Ubuntuは安定度重視でファイルシステムはext4になっていますが、FedoraやCachyOSのデフォルトはBtrfsを採用しています。Btrfsの最大の魅力はスナップショット機能。デスクトップ用途で使う場合は、不具合時でもすぐに戻せるのは安心感があります。

こちらでもTimeshiftについて触れましたが、Timeshiftの魅力を最大限に活かすならBtrfsパーティションにしたいところです。しかし、Canonicalのエンジニアはセキュリティ強化を目的として、Ubuntu 26.10において、署名付きGRUBブートローダーからBtrfs、ZFS、XFSなどのファイルシステムやLVM、LUKS暗号化のサポートを削除する計画を提案しているそうですし、あまりBtrfsパーティションに積極的ではなさそうです。
そこで、この変更が実施された場合でも問題なく動かすように、/bootを別パーティションにしてBtrfsを導入してみます。

手動でパーティションを分割

Ubuntuインストール時に、パーティションを手動作成します。

先頭はブートローダーを格納するパーティションとして1GB程度のFAT32パーティションを作成。
2つ目はカーネルを格納するExt4パーティションを作成。容量は1GB〜2GB程度で/bootにマウントします。
そして残りをBtrfsパーティションにして/にマウントします。
なお、一番最後にUbuntuのISOイメージを置いておくといざという時に便利なので、必要な場合はその部分も確保しておきます。ここではExt4パーティションで/isoにマウントしています。

ISOパーティションを用意した場合はGRUBメニューに追加

ISOパーティションを作成した場合は、Ubuntuのインストール後に、GRUBメニューに項目を追加します。
まず/isoの所有者変更と書き込み権限を付与します。

sudo chown $USER:$USER /iso
chmod u+rwx /iso

続いてスクリプトを保存して実行します。スクリプトの保存先を/isoにしておけば、このISOブートを利用してクリーンインストールした時など、再度GRUBメニューに項目を追加したい場合に便利です。

cd /iso
nano isoboot-grub.sh
sudo bash isoboot-grub.sh
#!/usr/bin/env bash
# =============================================================================
# isoboot-grub.sh
# ISOブート用パーティション確認 → GRUB エントリ追加スクリプト
# 前提:ISOファイル入りのext4パーティションが既に存在すること
# =============================================================================

set -euo pipefail

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'

info()    { echo -e "${CYAN}[INFO]${RESET}  $*"; }
success() { echo -e "${GREEN}[OK]${RESET}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET}  $*"; }
error()   { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
die()     { error "$*"; exit 1; }
hr()      { echo -e "${CYAN}$(printf '=%.0s' {1..70})${RESET}"; }

confirm() {
    local ans
    while true; do
        echo -en "${YELLOW}[確認]${RESET} $1 [y/N]: "
        read -r ans
        case "$ans" in
            [yY]*) return 0 ;;
            [nN]*|"") return 1 ;;
            *) echo "  y または n を入力してください。" ;;
        esac
    done
}

# -----------------------------------------------------------------------------
check_root() {
    [[ $EUID -eq 0 ]] || die "sudo で実行してください。\n  例: sudo bash $0"
}

check_deps() {
    local missing=()
    for cmd in lsblk blkid mount umount file grub-mkconfig update-grub; do
        command -v "$cmd" &>/dev/null || missing+=("$cmd")
    done
    [[ ${#missing[@]} -eq 0 ]] || die "コマンドが見つかりません: ${missing[*]}"
}

# -----------------------------------------------------------------------------
# ステップ1: ISOが入ったパーティションを選択
# -----------------------------------------------------------------------------
select_partition() {
    hr
    echo -e "${BOLD}【ステップ 1/3】ISO パーティションの選択${RESET}"
    hr
    echo ""
    info "現在のパーティション一覧:"
    echo ""
    lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINTS | grep -v loop
    echo ""

    while true; do
        echo -en "${BOLD}ISOが入ったパーティションを入力${RESET} (例: vda3, sda4, nvme0n1p3): /dev/"
        read -r part_name
        PART_DEV="/dev/${part_name}"

        if [[ ! -b "$PART_DEV" ]]; then
            error "${PART_DEV} はブロックデバイスではありません。再入力してください。"
            continue
        fi

        local fstype
        fstype=$(lsblk -no FSTYPE "$PART_DEV" 2>/dev/null || true)
        if [[ "$fstype" != "ext4" ]]; then
            warn "${PART_DEV} のファイルシステムは ${fstype:-不明} です(ext4 を想定)。"
            confirm "このまま続けますか?" || continue
        fi

        echo ""
        info "${PART_DEV} の情報:"
        lsblk -o NAME,SIZE,FSTYPE,LABEL,UUID,MOUNTPOINTS "$PART_DEV" 2>/dev/null || true
        echo ""
        confirm "${PART_DEV} を使用しますか?" && break
    done

    success "対象パーティション: ${PART_DEV}"

    # UUID取得(デバイス名非依存・VM/実機共通)
    PART_UUID=$(blkid -s UUID -o value "$PART_DEV")
    if [[ -z "$PART_UUID" ]]; then
        die "UUIDを取得できませんでした: ${PART_DEV}"
    fi
    success "UUID: ${PART_UUID}"
}

# -----------------------------------------------------------------------------
# ステップ2: ISOファイルの確認と起動パス検出
# -----------------------------------------------------------------------------
inspect_isos() {
    hr
    echo -e "${BOLD}【ステップ 2/3】ISO ファイルの確認${RESET}"
    hr
    echo ""

    MOUNT_POINT="/mnt/isoboot_grub_$$"
    mkdir -p "$MOUNT_POINT"
    mount "$PART_DEV" "$MOUNT_POINT"
    info "${PART_DEV} を ${MOUNT_POINT} にマウントしました"
    echo ""

    # ISOファイルを列挙
    mapfile -t iso_candidates < <(find "$MOUNT_POINT" -maxdepth 2 -name "*.iso" 2>/dev/null)

    if [[ ${#iso_candidates[@]} -eq 0 ]]; then
        umount "$MOUNT_POINT"; rmdir "$MOUNT_POINT"
        die "ISOファイルが見つかりませんでした。パーティションにISOを配置してから再実行してください。"
    fi

    info "見つかったISOファイル:"
    for i in "${!iso_candidates[@]}"; do
        local size
        size=$(du -sh "${iso_candidates[$i]}" 2>/dev/null | cut -f1)
        echo "  [$((i+1))] ${iso_candidates[$i]##"$MOUNT_POINT"}  (${size})"
    done
    echo ""

    ISO_ENTRIES=()  # "iso_path:vmlinuz:initrd:boot_type" の配列

    for iso_full in "${iso_candidates[@]}"; do
        local iso_rel="${iso_full##"$MOUNT_POINT"}"
        local iso_name
        iso_name=$(basename "$iso_full")
        echo ""
        info "--- ${iso_name} を検査中 ---"

        if confirm "${iso_name} をGRUBメニューに追加しますか?"; then
            local paths
            paths=$(detect_boot_paths "$iso_full")
            local vmlinuz="${paths%%:*}"
            local rest="${paths#*:}"
            local initrd="${rest%%:*}"
            local boot_type="${rest##*:}"

            if [[ "$vmlinuz" == "UNKNOWN" ]]; then
                warn "カーネルパスを自動検出できませんでした。手動入力してください。"
                echo -en "  vmlinuz のパス (例: /casper/vmlinuz): "
                read -r vmlinuz
                echo -en "  initrd のパス  (例: /casper/initrd): "
                read -r initrd
                boot_type="custom"
            else
                success "カーネル : ${vmlinuz}"
                success "initrd   : ${initrd}"
                success "起動方式 : ${boot_type}"
            fi

            ISO_ENTRIES+=("${iso_rel}:${vmlinuz}:${initrd}:${boot_type}")
        else
            info "スキップ: ${iso_name}"
        fi
    done

    umount "$MOUNT_POINT"
    rmdir "$MOUNT_POINT"
    success "アンマウント完了"

    if [[ ${#ISO_ENTRIES[@]} -eq 0 ]]; then
        die "追加するエントリがありません。終了します。"
    fi
}

# ISOをループマウントして起動パスを自動検出
detect_boot_paths() {
    local iso_path="$1"
    local tmp="/mnt/iso_inspect_$$"
    mkdir -p "$tmp"

    if ! mount -o loop,ro "$iso_path" "$tmp" 2>/dev/null; then
        echo "UNKNOWN:UNKNOWN:custom"
        return
    fi

    local vmlinuz="" initrd="" boot_type=""

    if   [[ -f "$tmp/casper/vmlinuz" ]];     then vmlinuz="/casper/vmlinuz";   initrd="/casper/initrd";     boot_type="casper"
    elif [[ -f "$tmp/casper/vmlinuz.efi" ]]; then vmlinuz="/casper/vmlinuz.efi"; initrd="/casper/initrd.lz"; boot_type="casper"
    elif [[ -f "$tmp/live/vmlinuz" ]];        then vmlinuz="/live/vmlinuz";     initrd="/live/initrd.img";   boot_type="live"
    elif [[ -f "$tmp/live/vmlinuz.efi" ]];    then vmlinuz="/live/vmlinuz.efi"; initrd="/live/initrd.img";   boot_type="live"
    else
        # フォールバック:vmlinuz を再帰検索
        local found
        found=$(find "$tmp" -name "vmlinuz*" | head -1 || true)
        if [[ -n "$found" ]]; then
            vmlinuz="${found##"$tmp"}"
            local dir
            dir=$(dirname "$found")
            initrd=$(find "$dir" -name "initrd*" | head -1 || true)
            initrd="${initrd##"$tmp"}"
            boot_type="custom"
        else
            vmlinuz="UNKNOWN"; initrd="UNKNOWN"; boot_type="custom"
        fi
    fi

    umount "$tmp"; rmdir "$tmp"
    echo "${vmlinuz}:${initrd}:${boot_type}"
}

# -----------------------------------------------------------------------------
# ステップ3: GRUBエントリ追加
# -----------------------------------------------------------------------------
add_grub_entries() {
    hr
    echo -e "${BOLD}【ステップ 3/3】GRUB エントリの追加${RESET}"
    hr
    echo ""

    local custom_file="/etc/grub.d/40_custom"
    cp "$custom_file" "${custom_file}.bak.$(date +%Y%m%d_%H%M%S)"
    info "40_custom をバックアップしました"
    echo ""

    for entry in "${ISO_ENTRIES[@]}"; do
        local iso_rel vmlinuz initrd boot_type menu_label
        IFS=':' read -r iso_rel vmlinuz initrd boot_type <<< "$entry"
        menu_label=$(basename "$iso_rel" .iso)

        local params=""
        case "$boot_type" in
            casper) params="boot=casper iso-scan/filename=\$isofile quiet splash ---" ;;
            live)   params="boot=live iso-scan/filename=\$isofile quiet splash" ;;
            *)      params="iso-scan/filename=\$isofile quiet splash" ;;
        esac

        local grub_entry
        grub_entry=$(cat <<EOF

menuentry "${menu_label} (ISO Loop Boot)" {
    insmod part_gpt
    insmod ext2
    insmod loopback
    insmod iso9660
    search --no-floppy --fs-uuid --set=isodev ${PART_UUID}
    set isofile="${iso_rel}"
    loopback loop (\$isodev)\$isofile
    linux  (loop)${vmlinuz} ${params}
    initrd (loop)${initrd}
}
EOF
)
        echo -e "${CYAN}追加予定エントリ:${RESET}"
        echo "$grub_entry"
        echo ""

        if confirm "このエントリを追加しますか?"; then
            echo "$grub_entry" >> "$custom_file"
            success "追加しました: ${menu_label}"
        else
            warn "スキップ: ${menu_label}"
        fi
    done

    echo ""
    info "GRUB を更新中..."
    if update-grub 2>&1; then
        success "update-grub 完了"
    else
        warn "update-grub でエラーが発生しました。手動で確認してください:"
        warn "  sudo update-grub"
    fi
}

# -----------------------------------------------------------------------------
# 完了サマリー
# -----------------------------------------------------------------------------
print_summary() {
    hr
    echo -e "${BOLD}${GREEN}【完了】セットアップサマリー${RESET}"
    hr
    echo ""
    echo -e "  対象パーティション : ${BOLD}${PART_DEV}${RESET}"
    echo -e "  パーティションUUID : ${BOLD}${PART_UUID}${RESET}"
    echo -e "  追加エントリ数     : ${BOLD}${#ISO_ENTRIES[@]}${RESET}"
    echo ""
    echo -e "${YELLOW}【次のステップ】${RESET}"
    echo "  1. 再起動して GRUB メニューに新しいエントリが表示されることを確認"
    echo "  2. 起動に失敗した場合は /etc/grub.d/40_custom を確認し"
    echo "     カーネルパスを修正後 sudo update-grub を再実行"
    echo ""
    warn "バックアップ: /etc/grub.d/40_custom.bak.* に保存済み"
    hr
}

# -----------------------------------------------------------------------------
main() {
    clear
    hr
    echo -e "${BOLD}  isoboot-grub.sh — ISO ブート GRUB エントリ 追加スクリプト${RESET}"
    echo -e "  前提: ISOファイル入りのext4パーティションが既に存在すること"
    hr
    echo ""
    check_root
    check_deps
    select_partition
    inspect_isos
    add_grub_entries
    print_summary
}

main "$@"

サブボリューム移行用スクリプト作成

Ubuntu24.04以降では、インストール時にBtrfsを選択しても自動で@、@homeを作成しないようになっています。ひとまずマウント状況を確認します。

mount | grep btrfs

下記のように、/や/homeのままとなっているはずです。

/dev/vda3 on / type btrfs (rw,relatime,discard=async,space_cache=v2,subvolid=5,subvol=/)  # subvol=/@のようになっていない。

サブボリュームへ移行するスクリプトをホームフォルダ内に保存しておきます。
もし、/isoを作成しているなら、このスクリプトも/isoフォルダに保存しておいたほうが便利です。

cd /iso
nano btrfs_migrate.sh
#!/bin/bash
# ============================================================
#  Btrfs サブボリューム移行スクリプト(統合版)
#  実行環境: ライブUSB環境のターミナル
#  構成A: EFI + btrfs の2パーティション構成
#  構成B: EFI + ext4(/boot) + btrfs の3パーティション構成
#  EFI(UEFI)/ BIOS(Legacy)両対応
#  使い方  : sudo bash btrfs_migrate.sh
# ============================================================

set -euo pipefail

RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m'
info()  { echo -e "${GREEN}[INFO]${NC}  $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
step()  { echo -e "\n${CYAN}=== $* ===${NC}"; }

[[ $EUID -ne 0 ]] && error "sudo で実行してください"

# ============================================================
# 環境検出
# ============================================================
step "環境を自動検出"

if [[ -d /sys/firmware/efi ]]; then
    BOOT_MODE="EFI"
else
    BOOT_MODE="BIOS"
fi
info "起動モード: $BOOT_MODE"

# Btrfsパーティション検出
BTRFS_PART=$(lsblk -lnpo NAME,FSTYPE | awk '$2=="btrfs"{print $1}' | head -1)
[[ -n "$BTRFS_PART" ]] || error "Btrfsパーティションが見つかりませんでした"

# ディスク本体
DISK=$(lsblk -lnpo PKNAME "$BTRFS_PART" | head -1)
[[ -n "$DISK" ]] || DISK=$(echo "$BTRFS_PART" | sed 's/p\?[0-9]*$//')
DISK="/dev/${DISK##*/}"

# EFI / BIOS ブートパーティション検出
EFI_PART=""
BIOS_BOOT_PART=""

if [[ "$BOOT_MODE" == "EFI" ]]; then
    EFI_PART=$(lsblk -lnpo NAME,PARTTYPE | \
        awk 'tolower($2) == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" {print $1}' | head -1)
    if [[ -z "$EFI_PART" ]]; then
        EFI_PART=$(lsblk -lnpo NAME,FSTYPE | awk '$2=="vfat"{print $1}' | head -1)
    fi
    [[ -n "$EFI_PART" ]] || error "EFIパーティションが見つかりませんでした"
else
    BIOS_BOOT_PART=$(lsblk -lnpo NAME,PARTTYPE | \
        awk 'tolower($2) == "21686148-6449-6e6f-744e-656564454649" {print $1}' | head -1)
    if [[ -z "$BIOS_BOOT_PART" ]]; then
        warn "BIOS Bootパーティションが見つかりません(MBRディスクでは正常)"
    else
        info "BIOS Bootパーティション: $BIOS_BOOT_PART"
    fi
fi

# ============================================================
# /boot 独立パーティション検出
# EFI・btrfs 以外の ext2/3/4 → /boot 独立パーティションと判断
# ============================================================
BOOT_PART=""
BOOT_FS=""

for part in $(lsblk -lnpo NAME,FSTYPE "$DISK" | awk '$2 ~ /^ext[234]$/ {print $1}'); do
    [[ "$part" == "$EFI_PART" ]] && continue
    [[ "$part" == "$BTRFS_PART" ]] && continue
    BOOT_PART="$part"
    BOOT_FS=$(lsblk -lnpo FSTYPE "$BOOT_PART" | head -1)
    break
done

if [[ -n "$BOOT_PART" ]]; then
    LAYOUT="3partition"
    info "構成B: 3パーティション構成(EFI + /boot独立 + btrfs)"
    info "独立 /boot パーティション: $BOOT_PART ($BOOT_FS)"
else
    LAYOUT="2partition"
    info "構成A: 2パーティション構成(EFI + btrfs)"
fi

# 結果表示
echo ""
echo "  検出結果:"
echo "  ┌──────────────────────────────────────────┐"
printf "  │  起動モード          : %-21s│\n" "$BOOT_MODE"
printf "  │  構成                : %-21s│\n" "$LAYOUT"
printf "  │  Btrfsパーティション : %-21s│\n" "$BTRFS_PART"
if [[ "$BOOT_MODE" == "EFI" ]]; then
    printf "  │  EFIパーティション   : %-21s│\n" "$EFI_PART"
else
    printf "  │  BIOS Boot           : %-21s│\n" "${BIOS_BOOT_PART:-(なし / MBR)}"
fi
printf "  │  /boot パーティション: %-21s│\n" "${BOOT_PART:-(なし / btrfs内)}"
printf "  │  ディスク            : %-21s│\n" "$DISK"
echo "  └──────────────────────────────────────────┘"
echo ""

info "ディスク構成(参考):"
lsblk -o NAME,SIZE,FSTYPE,PARTTYPE,MOUNTPOINT "$DISK"
echo ""

warn "上記の検出結果で続行しますか? [yes/N]"
read -r answer
[[ "$answer" == "yes" ]] || { info "中止しました。"; exit 0; }

# ============================================================
# Step 1: マウント
# ============================================================
step "Step 1: Btrfsパーティションをマウント"

info "既存マウントを解除"
for mp in $(findmnt -rno TARGET --source "$BTRFS_PART" 2>/dev/null | sort -r); do
    [[ "$mp" == "/" ]] && continue
    info "  解除: $mp"
    umount "$mp" 2>/dev/null || umount -l "$mp" 2>/dev/null || true
done
if [[ -n "${EFI_PART:-}" ]]; then
    for mp in $(findmnt -rno TARGET --source "$EFI_PART" 2>/dev/null | sort -r); do
        [[ "$mp" == "/" ]] && continue
        info "  解除 (EFI): $mp"
        umount "$mp" 2>/dev/null || umount -l "$mp" 2>/dev/null || true
    done
fi
if [[ -n "$BOOT_PART" ]]; then
    for mp in $(findmnt -rno TARGET --source "$BOOT_PART" 2>/dev/null | sort -r); do
        [[ "$mp" == "/" ]] && continue
        info "  解除 (/boot): $mp"
        umount "$mp" 2>/dev/null || umount -l "$mp" 2>/dev/null || true
    done
fi

mkdir -p /mnt
mount "$BTRFS_PART" /mnt

# 二重実行チェック
if btrfs subvolume list /mnt 2>/dev/null | grep -qE '\s@$|\s@home$'; then
    error "サブボリューム @ または @home がすでに存在します。移行済みの可能性があります。"
fi

# ============================================================
# Step 2: サブボリューム作成・データ移動
# ============================================================
step "Step 2: サブボリューム作成・データ移動"
cd /mnt

info "スナップショット @ を作成"
btrfs subvolume snapshot . @

# 3パーティション構成: @/boot/ は空のマウントポイントにする
# 2パーティション構成: @/boot/ はそのまま残す(btrfs内にカーネルが必要)
if [[ "$LAYOUT" == "3partition" ]]; then
    if [[ -d /mnt/@/boot ]]; then
        info "/boot 独立構成のため @/boot/ を空のマウントポイントとして準備"
        find /mnt/@/boot -mindepth 1 -delete
    fi
fi

info "サブボリューム @home を作成"
btrfs subvolume create @home

if [[ -d /mnt/home ]] && compgen -G "/mnt/home/*" > /dev/null 2>&1; then
    info "home/* を @home/ へコピー"
    cp -a /mnt/home/. /mnt/@home/
else
    warn "home/ が空またはありません。スキップします。"
fi

info "フラットなルートデータを削除"
for entry in /mnt/*; do
    name=$(basename "$entry")
    [[ "$name" == "@" || "$name" == "@home" ]] && continue
    if mountpoint -q "$entry" 2>/dev/null; then
        info "  アンマウント: $entry"
        umount -R "$entry" 2>/dev/null || umount -l "$entry" 2>/dev/null || true
    fi
    rm -rf "$entry"
    info "  削除: $entry"
done

# ============================================================
# Step 3: fstab 書き換え
# ============================================================
step "Step 3: fstab を更新"
FSTAB="/mnt/@/etc/fstab"
[[ -f "$FSTAB" ]] || error "fstab が見つかりません: $FSTAB"

UUID=$(blkid -s UUID -o value "$BTRFS_PART")
[[ -n "$UUID" ]] || error "UUIDの取得に失敗しました"
info "Btrfs UUID: $UUID"

BOOT_UUID=""
if [[ "$LAYOUT" == "3partition" ]]; then
    BOOT_UUID=$(blkid -s UUID -o value "$BOOT_PART")
    [[ -n "$BOOT_UUID" ]] || error "/boot UUID の取得に失敗しました"
    info "/boot UUID: $BOOT_UUID"
fi

cp "$FSTAB" "${FSTAB}.bak"
info "バックアップ: ${FSTAB}.bak"

python3 - "$FSTAB" "$UUID" "$BOOT_UUID" "${BOOT_FS:-}" << 'PYEOF'
import sys, re

fstab_path = sys.argv[1]
uuid        = sys.argv[2]
boot_uuid   = sys.argv[3]
boot_fs     = sys.argv[4]

with open(fstab_path) as f:
    lines = f.readlines()

new_lines = []
has_home  = False
has_boot  = False

for line in lines:
    stripped = line.strip()
    if stripped.startswith("#") or stripped == "":
        new_lines.append(line)
        continue
    cols = stripped.split()
    if len(cols) < 4:
        new_lines.append(line)
        continue

    mountpoint = cols[1]
    fstype     = cols[2]

    if fstype == "btrfs" and mountpoint == "/":
        opts = re.sub(r'subvol=[^,\s]+,?', '', cols[3]).strip(',')
        opts = (opts + ',subvol=@') if opts else 'defaults,subvol=@'
        new_lines.append(f"UUID={uuid} / btrfs {opts} 0 0\n")
    elif fstype == "btrfs" and mountpoint == "/home":
        opts = re.sub(r'subvol=[^,\s]+,?', '', cols[3]).strip(',')
        opts = (opts + ',subvol=@home') if opts else 'defaults,subvol=@home'
        new_lines.append(f"UUID={uuid} /home btrfs {opts} 0 0\n")
        has_home = True
    elif mountpoint == "/boot" and boot_uuid:
        new_lines.append(f"UUID={boot_uuid} /boot {boot_fs} defaults 0 2\n")
        has_boot = True
    else:
        new_lines.append(line)

if not has_home:
    new_lines.append(f"UUID={uuid} /home btrfs defaults,subvol=@home 0 0\n")
if boot_uuid and not has_boot:
    new_lines.append(f"UUID={boot_uuid} /boot {boot_fs} defaults 0 2\n")

with open(fstab_path, 'w') as f:
    f.writelines(new_lines)
PYEOF

info "更新後の fstab:"
cat "$FSTAB"

# ============================================================
# Step 4: chroot して GRUB 更新
# ============================================================
step "Step 4: chroot して GRUB を更新"

mkdir -p /mnt2
mount -t btrfs -o subvol=@ "$BTRFS_PART" /mnt2
info "subvol=@ で /mnt2 に再マウント完了"

mkdir -p /mnt2/boot/grub

# 3パーティション構成: chroot前に /boot をマウント
if [[ "$LAYOUT" == "3partition" ]]; then
    info "/boot ($BOOT_PART) を /mnt2/boot にマウント"
    mkdir -p /mnt2/boot
    mount "$BOOT_PART" /mnt2/boot
    info "  /mnt2/boot の内容:"
    ls /mnt2/boot/ || true
fi

mount --rbind /dev  /mnt2/dev  && mount --make-rslave /mnt2/dev
mount --rbind /proc /mnt2/proc && mount --make-rslave /mnt2/proc
mount --rbind /sys  /mnt2/sys  && mount --make-rslave /mnt2/sys
mount --rbind /run  /mnt2/run  && mount --make-rslave /mnt2/run

if [[ "$BOOT_MODE" == "EFI" ]]; then
    mount --bind /sys/firmware/efi/efivars /mnt2/sys/firmware/efi/efivars 2>/dev/null || \
        warn "efivars のバインドに失敗しました"
    mkdir -p /mnt2/boot/efi
    mount "$EFI_PART" /mnt2/boot/efi
    chroot /mnt2 /bin/bash -e << CHROOT
echo '[chroot] /boot の内容:'
ls /boot/ || true
echo '[chroot] grub-install (EFI) ...'
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu --recheck
echo '[chroot] grub-mkconfig ...'
grub-mkconfig -o /boot/grub/grub.cfg
echo '[chroot] subvol 指定確認:'
grep -i subvol /boot/grub/grub.cfg | head -5 || echo '(subvol 指定なし)'
echo '[chroot] 完了'
CHROOT
else
    chroot /mnt2 /bin/bash -e << CHROOT
echo '[chroot] /boot の内容:'
ls /boot/ || true
echo '[chroot] grub-install (BIOS) ...'
grub-install --target=i386-pc --recheck $DISK
echo '[chroot] grub-mkconfig ...'
grub-mkconfig -o /boot/grub/grub.cfg
echo '[chroot] subvol 指定確認:'
grep -i subvol /boot/grub/grub.cfg | head -5 || echo '(subvol 指定なし)'
echo '[chroot] 完了'
CHROOT
fi

# ============================================================
# Step 5: アンマウント
# ============================================================
step "Step 5: アンマウント"
umount -R /mnt2 2>/dev/null || true
umount -R /mnt  2>/dev/null || true

echo ""
echo -e "${GREEN}======================================================"
echo "  移行が完了しました!"
echo "  構成: $LAYOUT"
echo "  再起動後に以下で確認してください:"
echo "    sudo btrfs subvolume list /"
if [[ "$LAYOUT" == "3partition" ]]; then
echo "    findmnt -T /boot"
fi
echo -e "======================================================${NC}"
echo ""
warn "再起動しますか? [yes/N]"
read -r reboot_answer
[[ "$reboot_answer" == "yes" ]] && reboot || info "手動で再起動してください: sudo reboot"

ライブUSBやISOブートで起動するために再起動します。

sudo reboot

ライブUSBやISOブートで起動してスクリプトを実行

次に、インストール時に使用したUbuntuのライブUSB、もしくはISOブートで起動します。
今度は「Ubuntuを試す」を選択し、マウントしたドライブでhomeフォルダ、もしくは/isoフォルダまで辿り、右クリックしてターミナルを開き、保存しておいたスクリプトを実行します。
失敗すれば起動しなくなります。それでも構わないような、インストール直後に実施を。あくまでも自己責任で!

sudo bash btrfs_migrate.sh

無事終了したら指示に従い再起動します。

変換後に再度状況を確認

再起動で無事起動したら、先ほどのコマンドを再度実行します。

mount | grep btrfs

下記のような内容なら成功です。

/dev/vda1 on / type btrfs (rw,relatime,discard=async,space_cache=v2,subvolid=256,subvol=/@)
/dev/vda1 on /home type btrfs (rw,relatime,discard=async,space_cache=v2,subvolid=257,subvol=/@home)

管理者権限でBtrfsのサブボリュームの一覧も確認してみましょう。

sudo btrfs subvolume list /

成功していればこのような表示になります。

ID 256 gen 123 top level 5 path @
ID 257 gen 111 top level 5 path @home

Timeshiftインストール

無事サブボリュームが作成されたらTimeshiftをインストールします。

sudo apt update
sudo apt install -y timeshift

インストールが完了したらスナップショットを取りましょう。rsyncの時とは異なり、一瞬で完了します。

復元する時は、Ubutuが起動するならTimeshiftから。起動しない場合はISOブートで起動してTimeshiftをインストールして復元します。GRUBメニューへ追加する方法もありますが、ISOブートやライブUSBで起動して行うほうが確実でしょう。

タイトルとURLをコピーしました