- 注意:以下の記事を参考にされ、問題が生じた場合、責任は負えません。
- 基本的には私の忘備録です。
- 使用環境は manjaro-linuxです。
- link先に了解なく引用している部分がありますので、問題があればご連絡ください。
- 260605付近で間違い箇所を修正。最後のオマケを修正。
背景
SSDをxfsでformatして、長年利用してきたが、manjaroがbtrfsを採用しており、昔よりは安全性が上がったようだ。そこで、新しいSSDをbtrfsでformatしたのち、xfsのシステムをコピーして移行させた。その時の作業記録。(2026/05/12)
- deviceについては、各自の環境に沿って読み替えてください。
- SSDの容量は1TBで作業してます。これも各自の環境に合わせて修正ください。
- install-USBを作成し、そのUSBから作業を行う前提です。
この方法を採用した理由
- reinstall不要。
- 現行環境をそのまま移行可能。
- 外付けSSDで安全に起動テストできる。
- Btrfs snapshot運用へ移行できる。
- rollbackしやすい。
- Timeshiftの使用可。
作業前のチェックポイント
コピー元: /dev/sdb xfs環境 (内蔵SSD)
コピー先: /dev/sdc btrfs環境 (外付けSSD)
1. /dev/sdc が本当にコピー先か確認。
2. /dev/sdc をformatしてよいか確認。
3. /mnt/etc/fstab を新UUIDに変更。
4. 古い /dev/sdb のUUIDを残さない。
5. fallback用GRUBは --removable を使う。
6. /boot/efi/EFI/BOOT/BOOTX64.EFI が作成されたか確認。
7. 外付けSSDで起動確認後に、内蔵SSDと入れ替える。
作業一覧
- 確認
lsblk -f
sudo fdisk -l
- create partition: 1TB SSD.
sudo parted /dev/sdc --script mklabel gpt
sudo parted /dev/sdc --script mkpart ESP fat32 1MiB 1025MiB
sudo parted /dev/sdc --script set 1 esp on
sudo parted /dev/sdc --script mkpart root 1025MiB 251GiB
sudo parted /dev/sdc --script mkpart home 251GiB 100%
- format: btrfs
sudo mkfs.vfat -F32 -n EFI_WD_1T_B /dev/sdc1
sudo mkfs.btrfs -f -L root_wd_1t_b /dev/sdc2
sudo mkfs.btrfs -f -L home_wd_1t_b /dev/sdc3
- if you use kernelbelow 6.1, use following command.
kenel below 6.1 can not handle block-group-tree.
sudo mkfs.btrfs -f -L root_wd_1t_b -O '^block-group-tree' /dev/sdc2
sudo mkfs.btrfs -f -L home_wd_1t_b -O '^block-group-tree' /dev/sdc3
- subvolume root
sudo mount /dev/sdc2 /mnt
sudo btrfs subvolume create /mnt/@
sudo btrfs subvolume create /mnt/@snapshots
sudo btrfs subvolume create /mnt/@var_log
sudo btrfs subvolume create /mnt/@pkg
sudo umount /mnt
- subvolume home
sudo mount /dev/sdc3 /mnt
sudo btrfs subvolume create /mnt/@home
sudo btrfs subvolume create /mnt/@home_snapshots
sudo umount /mnt
- コピー先(外付けSSD: /dev/sdc)をmount
sudo mount -o subvol=@,compress=zstd:3,noatime,ssd,discard=async /dev/sdc2 /mnt
sudo mkdir -p /mnt/boot/efi /mnt/.snapshots /mnt/var/log /mnt/var/cache/pacman/pkg /mnt/home /mnt/home/.snapshots
sudo mount /dev/sdc1 /mnt/boot/efi
sudo mount -o subvol=@snapshots,compress=zstd:3,noatime,ssd,discard=async /dev/sdc2 /mnt/.snapshots
sudo mount -o subvol=@var_log,compress=zstd:3,noatime,ssd,discard=async /dev/sdc2 /mnt/var/log
sudo mount -o subvol=@pkg,compress=zstd:3,noatime,ssd,discard=async /dev/sdc2 /mnt/var/cache/pacman/pkg
sudo mount -o subvol=@home,compress=zstd:3,noatime,ssd,discard=async /dev/sdc3 /mnt/home
sudo mount -o subvol=@home_snapshots,compress=zstd:3,noatime,ssd,discard=async /dev/sdc3 /mnt/home/.snapshots
- コピー元(内蔵SSD: /dev/sdb)をmount
sudo mkdir -p /oldroot
sudo mount /dev/sdb2 /oldroot
#sudo mkdir -p /oldroot/home
sudo mount /dev/sdb3 /oldroot/home
#sudo mkdir -p /oldroot/boot/efi
sudo mount /dev/sdb1 /oldroot/boot/efi
- copy system ; rsync
sudo rsync -aAXHv --numeric-ids --info=progress2 \
--exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","/lost+found","/.snapshots/*","/home/.snapshots/*","/swapfile"} \
/oldroot/ /mnt/
- コピー先SSD(/dev/sdc)のfstab編集
lsblk -f /dev/sdc
#sudo blkid /dev/sdc1 /dev/sdc2 /dev/sdc3
sudo vim /mnt/etc/fstab
- chroot & install grub (/dev/sdc)
sudo manjaro-chroot /mnt
mkinitcpio -P
# fallback用GRUB: removable mode
grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck
update-grub
exit
- unmount & reboot
sudo umount -R /mnt
sync
sudo reboot
==========================================================
1. 準備と確認事項
外付けSSD(dev/sdc)に内臓のSSD (/dev/sda)のシステムをコピーする。
外付けSSDから起動させ、問題がないことを確認後、外付けSSDを内蔵SSDと入れ替える。
install用のUSBを作成して、usbから起動。
コピー元: /dev/sdb xfs環境
コピー先: /dev/sdc btrfs環境
/dev/sdcの最終的なpartition構成
device LABEL mount point
--------------------------------
/dev/sdc1 EFI /boot/efi
/dev/sdc2 root /
/dev/sdc3 home /home
format:
/ = btrfs
/home = btrfs
GRUB:
fallback用として --removable を使用
bootloader-id は使わない.
Timeshift:
/ の週1 snapshot 用
/home は基本含めない
/home:
設定変更前だけ手動snapshot
2. 事前確認
install用のUSBを作成して、usbから起動させた後、確認。
lsblk -f
sudo fdisk -l
確認:
/dev/sdb = コピー元
/dev/sdc = コピー先・外付けSSD:消してよい方
重要:
/dev/sdc は全消去される。
/dev/sdb と /dev/sdc を絶対に取り違えないこと。
3. /dev/sdc にpartition作成
構成:1TB SSDを使用した場合。
device LABEL Volume
---------------------------
/dev/sdc1 EFI 1G
/dev/sdc2 root 250G
/dev/sdc3 home 残り全部
partitionの作成:
Attention!! delete all data of /dev/sdc
partitionの容量は各自で修正のこと.
sudo parted /dev/sdc --script mklabel gpt
sudo parted /dev/sdc --script mkpart ESP fat32 1MiB 1025MiB
sudo parted /dev/sdc --script set 1 esp on
sudo parted /dev/sdc --script mkpart root 1025MiB 251GiB
sudo parted /dev/sdc --script mkpart home 251GiB 100%
確認:
lsblk -f /dev/sdc
#sudo parted /dev/sdc print
4. format
partitionをbtrfsでformat.
sudo mkfs.vfat -F32 -n BOOT_WD_1T_B /dev/sdc1
sudo mkfs.btrfs -f -L root_wd_1t_b /dev/sdc2
sudo mkfs.btrfs -f -L home_wd_1t_b /dev/sdc3
確認:
lsblk -f /dev/sdc
5. btrfs subvolume作成
5-1. root側
sudo mount /dev/sdc2 /mnt
sudo btrfs subvolume create /mnt/@
sudo btrfs subvolume create /mnt/@snapshots
sudo btrfs subvolume create /mnt/@var_log
sudo btrfs subvolume create /mnt/@pkg
sudo umount /mnt
5-2. home側
sudo mount /dev/sdc3 /mnt
sudo btrfs subvolume create /mnt/@home
sudo btrfs subvolume create /mnt/@home_snapshots
sudo umount /mnt
5-3. 確認
/root
sudo mount /dev/sdc2 /mnt
sudo btrfs subvolume list /mnt
sudo umount /mnt
/home
sudo mount /dev/sdc3 /mnt
sudo btrfs subvolume list /mnt
sudo umount /mnt
6. コピー先(外付けSSD)を /mnt にmount
sudo mount -o subvol=@,compress=zstd:3,noatime,ssd,discard=async /dev/sdc2 /mnt
sudo mkdir -p /mnt/boot/efi
sudo mkdir -p /mnt/.snapshots
sudo mkdir -p /mnt/var/log
sudo mkdir -p /mnt/var/cache/pacman/pkg
sudo mkdir -p /mnt/home
sudo mkdir -p /mnt/home/.snapshots
sudo mount /dev/sdc1 /mnt/boot/efi
sudo mount -o subvol=@snapshots,compress=zstd:3,noatime,ssd,discard=async /dev/sdc2 /mnt/.snapshots
sudo mount -o subvol=@var_log,compress=zstd:3,noatime,ssd,discard=async /dev/sdc2 /mnt/var/log
sudo mount -o subvol=@pkg,compress=zstd:3,noatime,ssd,discard=async /dev/sdc2 /mnt/var/cache/pacman/pkg
sudo mount -o subvol=@home,compress=zstd:3,noatime,ssd,discard=async /dev/sdc3 /mnt/home
sudo mount -o subvol=@home_snapshots,compress=zstd:3,noatime,ssd,discard=async /dev/sdc3 /mnt/home/.snapshots
注意:
- SSDやUSBケースによっては discard=async が不安定な場合がある。
- 問題がある場合は discard=async を削除し、fstrim.timer を利用する。
確認:
lsblk -f
##もしくは以下。
findmnt /mnt
findmnt /mnt/home
findmnt /mnt/boot/efi
findmnt /mnt/.snapshots
findmnt /mnt/var/log
findmnt /mnt/var/cache/pacman/pkg
findmnt /mnt/home/.snapshots
7. コピー元(内蔵SSD)をmount
sudo mkdir -p /oldroot
sudo mount /dev/sdb2 /oldroot
#sudo mkdir -p /oldroot/home
sudo mount /dev/sdb3 /oldroot/home
#sudo mkdir -p /oldroot/boot/efi
sudo mount /dev/sdb1 /oldroot/boot/efi
8. rsyncで system + home をコピー
Manjaro Live USBから起動してコピーする。
稼働中systemからコピーすると、browser DB、cache、log、設定ファイルなどがコピー中に変化する可能性がある。
sudo rsync -aAXHv --numeric-ids --info=progress2 \
--exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","/lost+found","/.snapshots/*","/home/.snapshots/*","/swapfile"} \
/oldroot/ /mnt/
コピー後確認:
ls /mnt
ls /mnt/home
ls /mnt/etc
8. コピー先のfstab を作成・修正
UUID確認:
lsblk -f
# sudo blkid /dev/sdc1 /dev/sdc2 /dev/sdc3
編集:
sudo vim /mnt/etc/fstab
例:
UUID=EFI_UUID /boot/efi vfat umask=0077 0 2
UUID=ROOT_UUID / btrfs subvol=@,compress=zstd:3,noatime,ssd,discard=async 0 0
UUID=ROOT_UUID /.snapshots btrfs subvol=@snapshots,compress=zstd:3,noatime,ssd,discard=async 0 0
UUID=ROOT_UUID /var/log btrfs subvol=@var_log,compress=zstd:3,noatime,ssd,discard=async 0 0
UUID=ROOT_UUID /var/cache/pacman/pkg btrfs subvol=@pkg,compress=zstd:3,noatime,ssd,discard=async 0 0
UUID=HOME_UUID /home btrfs subvol=@home,compress=zstd:3,noatime,ssd,discard=async 0 0
#UUID=HOME_UUID /home/.snapshots btrfs subvol=@home_snapshots,compress=zstd:3,noatime,ssd,discard=async 0 0
EFI_UUID = /dev/sdc1 のUUID
ROOT_UUID = /dev/sdc2 のUUID
HOME_UUID = /dev/sdc3 のUUID
注意:
UUIDを間違えると起動しなくなるので、慎重に。
古い /dev/sdb のUUIDを残さない。
9. コピー先のSSDにchrootで入りgrubのinstallなどを実施
Manjaro Live USBの場合:
sudo manjaro-chroot /mnt
chroot内で確認:
lsblk -f
cat /etc/fstab
findmnt /
findmnt /home
mkinitcpio.conf確認:
古いxfs環境から移行した場合、HOOKS に btrfs が入っているか確認。
cat /etc/mkinitcpio.conf
initramfs更新:
mkinitcpio -P
fallback用GRUBを --removable で作成
今回は通常のNVRAM boot entryを作る目的ではなく、外付けSSD fallback用。 そのため --bootloader-id は使わず、--removable を使う。
grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck
update-grub
--removable:
EFI fallback path にGRUBを配置する。
通常は以下へ配置される。
利点:
UEFIのNVRAM entryに依存しにくい。
外付けSSDとして起動しやすい。
他の既存Manjaro boot entryを上書きしにくい。
課題:
複数OSを運用している場合、EFI/BOOT/BOOTX64.EFI が競合する可能性がある。
確認:
ls -lh /boot/efi/EFI/BOOT/
期待:
BOOTX64.EFI が存在する。
/boot/efi/EFI/BOOT/BOOTX64.EFI
chroot終了:
exit
10. umount 外付けSSD
sudo umount -R /mnt
sync
11. 外付けSSDから起動テスト
再起動。
BIOS/UEFI boot menuから、外付けSSDを選択。
起動後に確認:
lsblk -f
findmnt /
findmnt /home
findmnt /.snapshots
findmnt /home/.snapshots
findmnt /boot/efi
期待:
/ -> /dev/sdc2 subvol=@
/home -> /dev/sdc3 subvol=@home
/.snapshots -> /dev/sdc2 subvol=@snapshots
/home/.snapshots -> /dev/sdc3 subvol=@home_snapshots
/boot/efi -> /dev/sdc1
btrfs確認:
sudo btrfs subvolume list /
sudo btrfs subvolume list /home
sudo btrfs filesystem usage /
sudo btrfs filesystem usage /home
12. 起動後のsystem確認
lsblk -f
df -h
cat /etc/fstab
uname -a
sudo pacman -Q linux
失敗service確認:
systemctl --failed
重要error確認:
journalctl -p 3 -xb
EFI fallback確認:
ls -lh /boot/efi/EFI/BOOT/
14. 内蔵SSDと入れ替える前の確認
外付けSSDとして正常起動したら確認:
読み書き確認
network確認
systemctl --failed確認
journalctl -p 3 -xb確認
確認コマンド例:
systemctl --failed
journalctl -p 3 -xb
findmnt /
findmnt /home
lsblk -f
15. 内蔵SSDと入れ替え後の注意
入れ替え後、デバイス名は変わる可能性あり。
外付け時: /dev/sdc
内蔵後: /dev/sda or /dev/sdb
ただし /etc/fstab をUUIDで書いていれば基本問題なし。
入れ替え後に確認:
lsblk -f
findmnt /
findmnt /home
findmnt /boot/efi
cat /etc/fstab
16. 最終構成イメージ
/dev/sdc1 EFI vfat
/dev/sdc2 root btrfs
@
@snapshots
@var_log
@pkg
/dev/sdc3 home btrfs
@home
@home_snapshots
17. Timeshiftの設定
Timeshiftは rootだけ 対象。
/ を対象
/home は基本含めない
mode:
BTRFS
運用
root:
Timeshift weekly 2〜4個
大きな更新前だけ手動snapshot
snapshot頻度:
Monthly 0〜1
Weekly 2〜4
Daily 1
Hourly 0
Boot 0
home:
Timeshiftには含めない
設定変更前だけ手動snapshot
重要データは別SSDへrsync backup
sample setting
sudo vi /etc/timeshift/timeshift.json
{
"backup_device_uuid" : "UUID", # /root: UUID
"parent_device_uuid" : "",
"do_first_run" : "false",
"btrfs_mode" : "true", # false >> true
"include_btrfs_home" : "false", # false : not backup /home
"stop_cron_emails" : "true",
"schedule_monthly" : "true", # false >> true
"schedule_weekly" : "true", # false >> true
"schedule_daily" : "false",
"schedule_hourly" : "false",
"schedule_boot" : "false",
"count_monthly" : "0",
"count_weekly" : "2",
"count_daily" : "2",
"count_hourly" : "0",
"count_boot" : "1",
"snapshot_size" : "0",
"snapshot_count" : "0",
"exclude" : [
],
"exclude-apps" : [
]
}
snapshot list up
sudo timeshift --list
sudo btrfs subvolume list /
sudo btrfs subvolume list /home
初回snapshot:
sudo timeshift --create --comments "initial btrfs system snapshot"
system更新前だけ手動追加:
sudo timeshift --create --comments "before pacman update"
## system update
#sudo pacman -Syu
restore system from snapshot. You should start system from install-USB.
## GUI
sudo timeshift --restore
## CUI
sudo timeshift --restore --snapshot '2026-05-11_10-30-00'
18. /home の手動snapshot
/homeはTimeshiftに含めず、設定変更前だけ手動snapshot。
sudo btrfs subvolume snapshot -r /home /home/.snapshots/home-before-dotfiles-260505
確認
sudo btrfs subvolume list /home
ID 256 gen 77 top level 5 path @home
ID 257 gen 70 top level 5 path @home_snapshots
ID 258 gen 70 top level 257 path @home_snapshots/home-before-dotfiles-260505
snapshotの使用量
sudo btrfs filesystem du -s /home/.snapshots/home-before-dotfiles-260605
Total Exclusive Set shared Filename
73.08GiB 6.52MiB 51.82GiB /home/.snapshots/home-before-dotfiles-260605
項目 意味
------------------------------------------------------------
Total 73.08GiB : snapshotから見えるファイル総量
Exclusive 6.52MiB << このsnapshotだけが専有している容量
Set shared 51.82GiB : 他のsubvolume/snapshotと共有している容量
戻す時は、まずファイル単位で戻す。
cp -a /home/.snapshots/home-before-zsh-260505/someone/.zshrc /home/someone/.zshrc
.config の一部を戻す. 例:
cp -a /home/.snapshots/home-before-kde-260505/someone/.config/kwinrc /home/someone/.config/kwinrc
snapshot削除:
sudo btrfs subvolume delete /home/.snapshots/home-before-zsh-260505
保持目安:
/home snapshotは3〜5個程度。
古いものは削除。
大量に残さない。
snapshotの実用的な運用例
通常運用:
snapshotは作業前だけ取る
保持:
直近1〜3個だけ残す
削除:
作業が安定したら古いsnapshotは削除
変更前に snapshotをとる。
sudo btrfs subvolume snapshot -r /home /home/.snapshots/home-before-dotfiles-260605
snapshotを確認。
sudo btrfs subvolume list /home
更新に問題がなければ、不要になった古いsnapshotを削除。
sudo btrfs subvolume delete /home/.snapshots/古いsnapshot名
容量確認。
sudo btrfs filesystem du -s /home/.snapshots/*
df -h /home
注意:snapshot削除直後に df -h の空き容量がすぐ大きく戻らないことがあります。
その場合はまず同期します。
sudo btrfs filesystem sync /home
df -h /home
du と df は見ているものが少し違うため、数字が完全には一致しません。
よくある失敗
- コピー元(/dev/sdb)とコピー先(/dev/sdc)のdeviceを取り違える。(深刻:systemの消失)
- fstab中のUUIDを間違える。(起動できない。)
- EFI mount を忘れる。(起動できない。)
- mkinitcpio -P を忘れる。(起動できない?)
- fallback EFI が作成されていない。(起動できない)
- rsync中に稼働systemが更新される。
- snapshot を大量に残して容量を圧迫する。(運用時に注意)
Reference
2022年の少し古い記事。
RasberryPiでbtrfsを利用した例。よく理解せずに実行すると、危険かもしれない。
補足:
上記は少し古い記事のため、現状で利用できるか未確認。
オマケ
1. btrfsとext4,xfsの比較
項目 Btrfs ext4 XFS
------------------------------------------------------------------------------------------------
安定性 ○ 実用十分 ◎ 枯れている ◎ 大容量で強い
初心者向け △ 概念が多い ◎ 簡単 ○ 比較的簡単
snapshot ◎ 標準機能 × 標準なし × 標準なし
rollback ◎ Timeshiftと相性良い △ 別ツール依存 △ 別ツール依存
圧縮 ◎ zstd圧縮が使いやすい × 標準なし × 標準なし
容量節約 ◎ text/log/sourceで有効 × なし × なし
checksum ◎ data/metadata検出可 △ metadata中心 △ metadata中心
scrub ◎ 破損検出に強い × なし × あり/限定的
send/receive ◎ 差分backupに強い × なし × なし
容量拡張 ◎ device add可能 △ LVM等が必要 △ LVM等が必要
縮小 ○ 可能 ◎ 可能 × 原則不可
大容量file性能 ○ 良い ○ 良い ◎ 非常に良い
小file大量 ○〜△ 設定次第 ○ 安定 ○ 安定
DB/VM image △ CoWが邪魔な場合あり ○ 安定 ◎ 強い
HDD倉庫 △〜○ 用途次第 ◎ 無難 ◎ 大容量倉庫向き
SSD system ◎ とても相性良い ◎ 無難 ○ 良い
復旧容易性 △ 専門知識が要る ◎ 情報多い ○ 情報多い
運用ミス耐性 △ snapshot肥大化等に注意 ◎ 単純で強い ○ 単純で強い
2. btrfsのsystemを別のdiskにcloning
同じ容量のDsikが必要です。
危険なので、以下をよく理解してから、自己責任でご利用ください。
万一、障害が発生しても、責任は負えません。
#!/usr/bin/env bash
#////////////////////////////////////////////////////
#
# v03:260608
# copy system if target SSD has different size, but enough vol.
# eg) copy 1TB system but root+ home =512GB >> 512GB SSD
# - copy /home/.snapshots by creating @home_snapshots on TARGET3.
# - write fstab by UUID, not LABEL.
#
# copy_btrfs_system_to_disk_rsync.sh
#
# usage: copy from /dev/nvme0n1 to /dev/sda
# sudo ./copy_btrfs_system_to_disk_rsync_v06_3p_match_revised_v03.sh --source /dev/nvme0n1 --target /dev/sda
#
# ## 続行するには、手入力で次のように打つ必要があります。
# ## start format & copy
# FORMAT /dev/sda
#
# Purpose:
# Copy a Manjaro/Arch Btrfs system disk to another disk by rsync only.
# This script prepares the target disk up to the point just before manjaro-chroot.
#
# Expected source layout:
# SOURCEp1 = EFI /boot/efi
# SOURCEp2 = Btrfs root partition with subvol=@
# SOURCEp3 = Btrfs home partition with subvol=@home and optional @home_snapshots
# SOURCEp4 = optional data partition, ignored
#
# Target layout created:
# TARGET1 = EFI FAT32, LABEL=BOOT
# TARGET2 = Btrfs root, LABEL=ROOT
# @
# @snapshots
# @var_log
# @pkg
# TARGET3 = Btrfs home, LABEL=HOME
# @home
# @home_snapshots
#
# After this script:
# Target root is mounted at:
# /mnt/target
# Target home is mounted at:
# /mnt/target/home
# Target home snapshots are mounted at:
# /mnt/target/home/.snapshots
# Target EFI is mounted at:
# /mnt/target/boot/efi
# Target extra subvolumes are mounted at:
# /mnt/target/.snapshots
# /mnt/target/var/log
# /mnt/target/var/cache/pacman/pkg
#
# Then run manually:
# sudo manjaro-chroot /mnt/target /bin/bash
# mkinitcpio -P
# grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck
# update-grub
# exit
# sudo vi /mnt/target/etc/fstab
# sudo umount -R /mnt/target
# sync
#
# WARNING:
# The TARGET disk will be completely erased.
#
# memo:
# - v06 creates only first 3 partitions on TARGET.
# - TARGET1/TARGET2/TARGET3 sizes are based on SOURCEp1/SOURCEp2/SOURCEp3.
# - SOURCEp4, for example /data0, is NOT created and NOT copied.
# - Any remaining target disk space is left unallocated.
# - rsync only; btrfs send/receive is NOT used.
# - Running from Manjaro Live USB is strongly recommended.
# - Root snapshot history under /.snapshots is not copied.
# - Home snapshot history under /home/.snapshots is copied into TARGET3:@home_snapshots.
# - fstab is generated using UUID= entries.
# - Previous versions are kept separately.
#
#/////////////////////////////////////////////////
set -euo pipefail
VERSION="v03"
SOURCE_DISK=""
TARGET_DISK=""
YES=0
SRC_ROOT_MNT="/mnt/src_root"
SRC_ROOT_TOP="/mnt/src_root_top"
SRC_HOME_MNT="/mnt/src_home"
SRC_HOME_TOP="/mnt/src_home_top"
SRC_HOME_SNAP_MNT="/mnt/src_home_snapshots"
SRC_EFI_MNT="/mnt/src_efi"
TARGET_ROOT="/mnt/target"
TARGET_TOP_ROOT="/mnt/tgt_root_top"
TARGET_TOP_HOME="/mnt/tgt_home_top"
usage() {
cat <<'EOF'
Usage:
sudo ./copy_btrfs_system_to_disk_rsync_v06_3p_match_revised_v03.sh --source /dev/nvme0n1 --target /dev/sda
memo:
- TARGET disk is formatted completely.
- Only first 3 partitions are created:
TARGET1 = EFI, size approx SOURCEp1, LABEL=BOOT
TARGET2 = root, size approx SOURCEp2, LABEL=ROOT
TARGET3 = home, size approx SOURCEp3, LABEL=HOME
- SOURCEp4, for example /data0, is ignored.
- Any remaining target disk space is left unallocated.
- v03 creates @snapshots, @var_log, and @pkg on TARGET2.
- v03 creates @home and @home_snapshots on TARGET3.
- v03 copies /home/.snapshots from source @home_snapshots if it exists.
- v03 writes fstab using UUID= entries, not LABEL=.
- rsync only; btrfs send/receive is NOT used.
- Root snapshot history under /.snapshots is not copied.
Dangerous, no final confirmation:
sudo ./copy_btrfs_system_to_disk_rsync_v06_3p_match_revised_v03.sh --source /dev/nvme0n1 --target /dev/sda --yes
Confirmation:
The script asks you to type exactly:
FORMAT /dev/sda
Options:
--source DEV Source disk, e.g. /dev/nvme0n1
--target DEV Target disk, e.g. /dev/sda
--yes Skip final interactive confirmation
-h, --help Show this help
Example:
sudo ./copy_btrfs_system_to_disk_rsync_v06_3p_match_revised_v03.sh --source /dev/nvme0n1 --target /dev/sda
option: add LABEL to partition manually if needed
sudo dosfslabel /dev/sda1 BOOT
sudo btrfs filesystem label /dev/sda2 ROOT
sudo btrfs filesystem label /dev/sda3 HOME
After script:
sudo manjaro-chroot /mnt/target /bin/bash
mkinitcpio -P
grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck
update-grub
exit
sudo vi /mnt/target/etc/fstab
## and modify UUID or LABEL name if needed
reboot
EOF
echo -e "\n-----------------------------------------------------"
lsblk -f || true
}
log() {
printf '\n\033[1;32m==>\033[0m %s\n' "$*"
}
warn() {
printf '\n\033[1;33mWARN:\033[0m %s\n' "$*" >&2
}
die() {
printf '\n\033[1;31mERROR:\033[0m %s\n' "$*" >&2
exit 1
}
part_path() {
local disk="$1"
local num="$2"
case "$disk" in
*nvme*n*|*mmcblk*) printf '%sp%s' "$disk" "$num" ;;
*) printf '%s%s' "$disk" "$num" ;;
esac
}
require_root() {
[[ "${EUID}" -eq 0 ]] || die "Run as root: sudo $0 ..."
}
require_cmds() {
local missing=()
for c in lsblk findmnt blkid blockdev parted partprobe wipefs mkfs.vfat mkfs.btrfs btrfs rsync awk grep sort; do
command -v "$c" >/dev/null 2>&1 || missing+=("$c")
done
if ((${#missing[@]})); then
die "Missing commands: ${missing[*]}"
fi
}
parse_args() {
if (($# == 0)); then
usage
exit 0
fi
while (($#)); do
case "$1" in
--source)
SOURCE_DISK="${2:-}"
shift 2
;;
--target)
TARGET_DISK="${2:-}"
shift 2
;;
--yes)
YES=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown argument: $1"
;;
esac
done
[[ -n "$SOURCE_DISK" ]] || die "--source is required"
[[ -n "$TARGET_DISK" ]] || die "--target is required"
[[ -b "$SOURCE_DISK" ]] || die "Source disk not found: $SOURCE_DISK"
[[ -b "$TARGET_DISK" ]] || die "Target disk not found: $TARGET_DISK"
[[ "$SOURCE_DISK" != "$TARGET_DISK" ]] || die "Source and target must be different"
}
ensure_not_mounted() {
local dev="$1"
if findmnt -rn -S "$dev" >/dev/null 2>&1; then
die "$dev or its partition is mounted. Unmount it first."
fi
while read -r child; do
[[ -n "$child" ]] || continue
if findmnt -rn -S "$child" >/dev/null 2>&1; then
die "$child is mounted. Unmount it first."
fi
done < <(lsblk -lnpo NAME "$dev" | tail -n +2)
}
cleanup_mounts() {
umount -R "$TARGET_ROOT" 2>/dev/null || true
umount -R "$TARGET_TOP_ROOT" 2>/dev/null || true
umount -R "$TARGET_TOP_HOME" 2>/dev/null || true
umount -R "$SRC_ROOT_MNT" 2>/dev/null || true
umount -R "$SRC_ROOT_TOP" 2>/dev/null || true
umount -R "$SRC_HOME_MNT" 2>/dev/null || true
umount -R "$SRC_HOME_TOP" 2>/dev/null || true
umount -R "$SRC_HOME_SNAP_MNT" 2>/dev/null || true
umount -R "$SRC_EFI_MNT" 2>/dev/null || true
}
bytes_to_mib_ceil() {
local bytes="$1"
awk -v b="$bytes" 'BEGIN { printf "%d", int((b + 1048575) / 1048576) }'
}
show_current_state() {
log "Current disk state"
lsblk -f "$SOURCE_DISK" "$TARGET_DISK" || true
echo
echo "Source partitions:"
lsblk -b -o NAME,SIZE,FSTYPE,LABEL,UUID,MOUNTPOINTS "$SOURCE_DISK"
echo
echo "Target partitions:"
lsblk -b -o NAME,SIZE,FSTYPE,LABEL,UUID,MOUNTPOINTS "$TARGET_DISK"
echo
echo "Source usage:"
df -hT / /home /boot/efi 2>/dev/null || true
}
confirm_plan() {
local sp1 sp2 sp3 tp1 tp2 tp3
sp1="$(part_path "$SOURCE_DISK" 1)"
sp2="$(part_path "$SOURCE_DISK" 2)"
sp3="$(part_path "$SOURCE_DISK" 3)"
tp1="$(part_path "$TARGET_DISK" 1)"
tp2="$(part_path "$TARGET_DISK" 2)"
tp3="$(part_path "$TARGET_DISK" 3)"
[[ -b "$sp1" ]] || die "Source EFI partition missing: $sp1"
[[ -b "$sp2" ]] || die "Source root partition missing: $sp2"
[[ -b "$sp3" ]] || die "Source home partition missing: $sp3"
local p1_mib p2_mib p3_mib target_mib required_mib remaining_mib
p1_mib="$(bytes_to_mib_ceil "$(blockdev --getsize64 "$sp1")")"
p2_mib="$(bytes_to_mib_ceil "$(blockdev --getsize64 "$sp2")")"
p3_mib="$(bytes_to_mib_ceil "$(blockdev --getsize64 "$sp3")")"
target_mib="$(bytes_to_mib_ceil "$(blockdev --getsize64 "$TARGET_DISK")")"
required_mib=$((1 + p1_mib + p2_mib + p3_mib + 4))
remaining_mib=$((target_mib - required_mib))
# (( target_mib > required_mib )) || die "Target disk is too small. Need more than ${required_mib}MiB."
log "Planned operation"
cat <<EOF
Version:
$VERSION
Copy source:
Disk: $SOURCE_DISK
EFI: $sp1
root: $sp2 subvol=@
home: $sp3 subvol=@home
ignored: ${SOURCE_DISK}p4 or ${SOURCE_DISK}4 if present
Copy target:
Disk: $TARGET_DISK
EFI: $tp1, LABEL=BOOT
root: $tp2, LABEL=ROOT
home: $tp3, LABEL=HOME
New target partition plan:
${tp1}: FAT32 EFI, approx source p1 size = ${p1_mib}MiB
${tp2}: Btrfs root, approx source p2 size = ${p2_mib}MiB
${tp3}: Btrfs home, approx source p3 size = ${p3_mib}MiB
Remaining target disk space:
approx ${remaining_mib}MiB left unallocated
New target Btrfs subvolumes:
${tp2}: @
${tp2}: @snapshots
${tp2}: @var_log
${tp2}: @pkg
${tp3}: @home
${tp3}: @home_snapshots
Mount after copy:
${tp2} subvol=@ -> /mnt/target
${tp2} subvol=@snapshots -> /mnt/target/.snapshots
${tp2} subvol=@var_log -> /mnt/target/var/log
${tp2} subvol=@pkg -> /mnt/target/var/cache/pacman/pkg
${tp3} subvol=@home -> /mnt/target/home
${tp3} subvol=@home_snapshots -> /mnt/target/home/.snapshots
${tp1} -> /mnt/target/boot/efi
DANGER:
ALL DATA ON $TARGET_DISK WILL BE ERASED.
EOF
show_current_state
if (( YES == 0 )); then
echo
echo "To continue, type exactly:"
echo " FORMAT $TARGET_DISK"
read -r -p "> " ans
[[ "$ans" == "FORMAT $TARGET_DISK" ]] || die "Confirmation failed. Aborted."
else
warn "--yes specified. Skipping interactive confirmation."
fi
}
create_partitions() {
local sp1 sp2 sp3 tp1 tp2 tp3
sp1="$(part_path "$SOURCE_DISK" 1)"
sp2="$(part_path "$SOURCE_DISK" 2)"
sp3="$(part_path "$SOURCE_DISK" 3)"
tp1="$(part_path "$TARGET_DISK" 1)"
tp2="$(part_path "$TARGET_DISK" 2)"
tp3="$(part_path "$TARGET_DISK" 3)"
local p1_mib p2_mib p3_mib end1 end2 end3
p1_mib="$(bytes_to_mib_ceil "$(blockdev --getsize64 "$sp1")")"
p2_mib="$(bytes_to_mib_ceil "$(blockdev --getsize64 "$sp2")")"
p3_mib="$(bytes_to_mib_ceil "$(blockdev --getsize64 "$sp3")")"
end1=$((1 + p1_mib))
end2=$((end1 + p2_mib))
end3=$((end2 + p3_mib))
log "Wiping and partitioning target disk: $TARGET_DISK"
wipefs -a "$TARGET_DISK" || true
parted "$TARGET_DISK" --script mklabel gpt
parted "$TARGET_DISK" --script mkpart ESP fat32 1MiB "${end1}MiB"
parted "$TARGET_DISK" --script set 1 esp on
parted "$TARGET_DISK" --script mkpart root btrfs "${end1}MiB" "${end2}MiB"
parted "$TARGET_DISK" --script mkpart home btrfs "${end2}MiB" "${end3}MiB"
partprobe "$TARGET_DISK" || true
sleep 2
[[ -b "$tp1" && -b "$tp2" && -b "$tp3" ]] || die "Target partitions not found after partitioning."
log "Formatting target partitions"
mkfs.vfat -F32 -n BOOT "$tp1"
mkfs.btrfs -f -L ROOT "$tp2"
mkfs.btrfs -f -L HOME "$tp3"
}
create_target_subvolumes() {
local tp2 tp3
tp2="$(part_path "$TARGET_DISK" 2)"
tp3="$(part_path "$TARGET_DISK" 3)"
log "Creating target root subvolumes: @ @snapshots @var_log @pkg"
mkdir -p "$TARGET_TOP_ROOT"
mount "$tp2" "$TARGET_TOP_ROOT"
btrfs subvolume create "$TARGET_TOP_ROOT/@"
btrfs subvolume create "$TARGET_TOP_ROOT/@snapshots"
btrfs subvolume create "$TARGET_TOP_ROOT/@var_log"
btrfs subvolume create "$TARGET_TOP_ROOT/@pkg"
umount "$TARGET_TOP_ROOT"
log "Creating target home subvolumes: @home @home_snapshots"
mkdir -p "$TARGET_TOP_HOME"
mount "$tp3" "$TARGET_TOP_HOME"
btrfs subvolume create "$TARGET_TOP_HOME/@home"
btrfs subvolume create "$TARGET_TOP_HOME/@home_snapshots"
umount "$TARGET_TOP_HOME"
}
mount_sources() {
local sp2 sp3
sp2="$(part_path "$SOURCE_DISK" 2)"
sp3="$(part_path "$SOURCE_DISK" 3)"
log "Mounting source root and home read-only"
mkdir -p "$SRC_ROOT_MNT" "$SRC_HOME_MNT"
mount -o ro,subvol=@ "$sp2" "$SRC_ROOT_MNT"
mount -o ro,subvol=@home "$sp3" "$SRC_HOME_MNT"
# If source has @var_log / @pkg, mount them so rsync copies their current contents.
mkdir -p "$SRC_ROOT_MNT/var/log" "$SRC_ROOT_MNT/var/cache/pacman/pkg" "$SRC_ROOT_TOP"
mount -o ro "$sp2" "$SRC_ROOT_TOP"
if [[ -d "$SRC_ROOT_TOP/@var_log" ]]; then
mount -o ro,subvol=@var_log "$sp2" "$SRC_ROOT_MNT/var/log"
fi
if [[ -d "$SRC_ROOT_TOP/@pkg" ]]; then
mount -o ro,subvol=@pkg "$sp2" "$SRC_ROOT_MNT/var/cache/pacman/pkg"
fi
umount "$SRC_ROOT_TOP"
# If source has a separate home snapshots subvolume, mount it separately.
# It will be rsynced into TARGET /home/.snapshots later.
mkdir -p "$SRC_HOME_TOP" "$SRC_HOME_SNAP_MNT"
mount -o ro "$sp3" "$SRC_HOME_TOP"
if [[ -d "$SRC_HOME_TOP/@home_snapshots" ]]; then
mount -o ro,subvol=@home_snapshots "$sp3" "$SRC_HOME_SNAP_MNT"
elif [[ -d "$SRC_HOME_TOP/@home/.snapshots" ]]; then
warn "Source /home/.snapshots appears to be a normal directory inside @home. It will be copied separately by rsync."
else
warn "Source @home_snapshots was not found. /home/.snapshots may be empty on target unless it exists inside @home."
fi
umount "$SRC_HOME_TOP"
findmnt "$SRC_ROOT_MNT" || true
findmnt "$SRC_ROOT_MNT/var/log" || true
findmnt "$SRC_ROOT_MNT/var/cache/pacman/pkg" || true
findmnt "$SRC_HOME_MNT" || true
findmnt "$SRC_HOME_SNAP_MNT" || true
}
mount_target_for_copy() {
local tp1 tp2 tp3
tp1="$(part_path "$TARGET_DISK" 1)"
tp2="$(part_path "$TARGET_DISK" 2)"
tp3="$(part_path "$TARGET_DISK" 3)"
log "Mounting target for copy"
mkdir -p "$TARGET_ROOT"
mount -o noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=@ "$tp2" "$TARGET_ROOT"
mkdir -p "$TARGET_ROOT/home" "$TARGET_ROOT/boot/efi" "$TARGET_ROOT/.snapshots" "$TARGET_ROOT/var/log" "$TARGET_ROOT/var/cache/pacman/pkg"
mount -o noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=@snapshots "$tp2" "$TARGET_ROOT/.snapshots"
mount -o noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=@var_log "$tp2" "$TARGET_ROOT/var/log"
mount -o noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=@pkg "$tp2" "$TARGET_ROOT/var/cache/pacman/pkg"
mount -o noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=@home "$tp3" "$TARGET_ROOT/home"
mkdir -p "$TARGET_ROOT/home/.snapshots"
mount -o noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=@home_snapshots "$tp3" "$TARGET_ROOT/home/.snapshots"
mount "$tp1" "$TARGET_ROOT/boot/efi"
findmnt "$TARGET_ROOT" || true
findmnt "$TARGET_ROOT/.snapshots" || true
findmnt "$TARGET_ROOT/var/log" || true
findmnt "$TARGET_ROOT/var/cache/pacman/pkg" || true
findmnt "$TARGET_ROOT/home" || true
findmnt "$TARGET_ROOT/home/.snapshots" || true
findmnt "$TARGET_ROOT/boot/efi" || true
}
copy_efi() {
local sp1
sp1="$(part_path "$SOURCE_DISK" 1)"
log "Copying EFI partition by rsync"
mkdir -p "$SRC_EFI_MNT"
mount "$sp1" "$SRC_EFI_MNT"
rsync -aHAXv --delete "$SRC_EFI_MNT"/ "$TARGET_ROOT/boot/efi"/
umount "$SRC_EFI_MNT"
}
copy_root_home() {
log "Copying root by rsync"
rsync -aAXHv --numeric-ids --delete \
--exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","/lost+found","/.snapshots/*","/home/*","/data0/*","/swapfile"} \
"$SRC_ROOT_MNT"/ "$TARGET_ROOT"/
log "Copying home by rsync"
rsync -aAXHv --numeric-ids --delete \
--exclude={".snapshots/*",".cache/*",".local/share/Trash/*"} \
"$SRC_HOME_MNT"/ "$TARGET_ROOT/home"/
if findmnt -rn "$SRC_HOME_SNAP_MNT" >/dev/null 2>&1; then
log "Copying /home/.snapshots from source @home_snapshots by rsync"
rsync -aAXHv --numeric-ids --delete \
"$SRC_HOME_SNAP_MNT"/ "$TARGET_ROOT/home/.snapshots"/
elif [[ -d "$SRC_HOME_MNT/.snapshots" ]]; then
log "Copying /home/.snapshots normal directory by rsync"
rsync -aAXHv --numeric-ids --delete \
"$SRC_HOME_MNT/.snapshots"/ "$TARGET_ROOT/home/.snapshots"/
else
warn "No source /home/.snapshots content found to copy."
fi
}
fix_fstab() {
local tp1 tp2 tp3 boot_uuid root_uuid home_uuid
tp1="$(part_path "$TARGET_DISK" 1)"
tp2="$(part_path "$TARGET_DISK" 2)"
tp3="$(part_path "$TARGET_DISK" 3)"
boot_uuid="$(blkid -s UUID -o value "$tp1")"
root_uuid="$(blkid -s UUID -o value "$tp2")"
home_uuid="$(blkid -s UUID -o value "$tp3")"
[[ -n "$boot_uuid" ]] || die "Could not get UUID for $tp1"
[[ -n "$root_uuid" ]] || die "Could not get UUID for $tp2"
[[ -n "$home_uuid" ]] || die "Could not get UUID for $tp3"
log "Creating target fstab backup and replacing with UUID based fstab"
cp -a "$TARGET_ROOT/etc/fstab" "$TARGET_ROOT/etc/fstab.bak.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
cat > "$TARGET_ROOT/etc/fstab" <<EOF
# /etc/fstab generated by copy_btrfs_system_to_disk_rsync_v06_3p_match_revised_v03.sh
# TARGET: first 3 partitions only
# UUIDs:
# BOOT: $boot_uuid
# ROOT: $root_uuid
# HOME: $home_uuid
UUID=$boot_uuid /boot/efi vfat umask=0077 0 2
UUID=$root_uuid / btrfs subvol=@,compress=zstd:3,noatime,ssd,discard=async 0 0
UUID=$root_uuid /.snapshots btrfs subvol=@snapshots,compress=zstd:3,noatime,ssd,discard=async 0 0
UUID=$root_uuid /var/log btrfs subvol=@var_log,compress=zstd:3,noatime,ssd,discard=async 0 0
UUID=$root_uuid /var/cache/pacman/pkg btrfs subvol=@pkg,compress=zstd:3,noatime,ssd,discard=async 0 0
UUID=$home_uuid /home btrfs subvol=@home,compress=zstd:3,noatime,ssd,discard=async 0 0
UUID=$home_uuid /home/.snapshots btrfs subvol=@home_snapshots,compress=zstd:3,noatime,ssd,discard=async 0 0
tmpfs /tmp tmpfs defaults,noatime,mode=1777,size=8G 0 0
# NOTE:
# If discard=async is unstable on your SSD/USB enclosure,
# remove discard=async and use:
#
# sudo systemctl enable --now fstrim.timer
EOF
log "New fstab"
cat "$TARGET_ROOT/etc/fstab"
}
final_check() {
log "Final mount check"
findmnt "$TARGET_ROOT" || true
findmnt "$TARGET_ROOT/.snapshots" || true
findmnt "$TARGET_ROOT/var/log" || true
findmnt "$TARGET_ROOT/var/cache/pacman/pkg" || true
findmnt "$TARGET_ROOT/home" || true
findmnt "$TARGET_ROOT/home/.snapshots" || true
findmnt "$TARGET_ROOT/boot/efi" || true
log "Target lsblk"
lsblk -f "$TARGET_DISK"
cat <<EOF
Next manual steps:
sudo manjaro-chroot $TARGET_ROOT /bin/bash
Inside chroot:
mkinitcpio -P
grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck
update-grub
If update-grub is not available:
grub-mkconfig -o /boot/grub/grub.cfg
After exit:
exit
sudo vi $TARGET_ROOT/etc/fstab
sudo umount -R $TARGET_ROOT
EOF
}
main() {
require_root
require_cmds
parse_args "$@"
trap 'warn "Interrupted or failed. Some mount points may remain mounted. Check with: findmnt | grep /mnt";' ERR
cleanup_mounts
ensure_not_mounted "$TARGET_DISK"
confirm_plan
create_partitions
create_target_subvolumes
mount_sources
mount_target_for_copy
copy_efi
copy_root_home
fix_fstab
final_check
log "Done. Target is ready for manjaro-chroot."
}
main "$@"