add: VM backup, restore, and switch support (#206)

* fix: prefer project venv Python for patchers

* add: VM backup, restore, and switch support

Named backups via rsync --sparse for efficient sparse disk handling.
- vm_backup.sh: save current VM as a named backup to vm.backups/
- vm_restore.sh: restore a named backup into vm/
- vm_switch.sh: save current + restore target in one step
- Makefile targets: vm_backup, vm_restore, vm_switch, vm_list
- Documentation added to all READMEs (EN, ZH, KO, JA)

Closes #204

Made-with: Cursor
This commit is contained in:
matteo zappia
2026-03-14 17:39:10 +01:00
committed by GitHub
parent 23cf4eadbc
commit 624ed4de31
9 changed files with 420 additions and 1 deletions

1
.gitignore vendored
View File

@@ -330,3 +330,4 @@ setup_logs/
/research/artifacts
/research/xnu
/vm
/vm.backups

View File

@@ -7,6 +7,10 @@ VM_DIR ?= vm
CPU ?= 8 # CPU cores (only used during vm_new)
MEMORY ?= 8192 # Memory in MB (only used during vm_new)
DISK_SIZE ?= 64 # Disk size in GB (only used during vm_new)
BACKUPS_DIR ?= vm.backups
NAME ?=
BACKUP_INCLUDE_IPSW ?= 0
FORCE ?= 0
RESTORE_UDID ?= # UDID for restore operations
RESTORE_ECID ?= # ECID for restore operations
IRECOVERY_ECID ?= # ECID for irecovery operations
@@ -62,6 +66,12 @@ help:
@echo " CPU=8 CPU cores (stored in manifest)"
@echo " MEMORY=8192 Memory in MB (stored in manifest)"
@echo " DISK_SIZE=64 Disk size in GB (stored in manifest)"
@echo " make vm_backup NAME=<name> Save current VM as a named backup"
@echo " make vm_restore NAME=<name> Restore a named backup into vm/"
@echo " make vm_switch NAME=<name> Save current + restore target (one step)"
@echo " make vm_list List available backups"
@echo " Options: BACKUP_INCLUDE_IPSW=1 Include *_Restore* IPSW dirs in backup"
@echo " FORCE=1 Skip overwrite prompt on restore"
@echo " make amfidont_allow_vphone Start amfidont for the signed vphone-cli binary"
@echo " make boot_host_preflight Diagnose whether host can launch signed PV=3 binary"
@echo " make boot Boot VM (reads from config.plist)"
@@ -183,12 +193,45 @@ vphoned:
# VM management
# ═══════════════════════════════════════════════════════════════════
.PHONY: vm_new amfidont_allow_vphone boot_host_preflight boot boot_dfu boot_binary_check
.PHONY: vm_new vm_backup vm_restore vm_switch vm_list amfidont_allow_vphone boot_host_preflight boot boot_dfu boot_binary_check
vm_new:
CPU="$(CPU)" MEMORY="$(MEMORY)" \
zsh $(SCRIPTS)/vm_create.sh --dir $(VM_DIR) --disk-size $(DISK_SIZE)
vm_backup:
VM_DIR="$(VM_DIR)" BACKUPS_DIR="$(BACKUPS_DIR)" NAME="$(NAME)" BACKUP_INCLUDE_IPSW="$(BACKUP_INCLUDE_IPSW)" \
zsh $(SCRIPTS)/vm_backup.sh
vm_restore:
VM_DIR="$(VM_DIR)" BACKUPS_DIR="$(BACKUPS_DIR)" NAME="$(NAME)" FORCE="$(FORCE)" \
zsh $(SCRIPTS)/vm_restore.sh
vm_switch:
VM_DIR="$(VM_DIR)" BACKUPS_DIR="$(BACKUPS_DIR)" NAME="$(NAME)" BACKUP_INCLUDE_IPSW="$(BACKUP_INCLUDE_IPSW)" \
zsh $(SCRIPTS)/vm_switch.sh
vm_list:
@if [ -d "$(BACKUPS_DIR)" ]; then \
current=""; \
[ -f "$(VM_DIR)/.vm_name" ] && current="$$(cat "$(VM_DIR)/.vm_name")"; \
found=0; \
for d in "$(BACKUPS_DIR)"/*/; do \
[ -f "$${d}config.plist" ] || continue; \
name="$$(basename "$$d")"; \
size="$$(du -sh "$$d" 2>/dev/null | cut -f1)"; \
if [ "$$name" = "$$current" ]; then \
echo " * $$name ($$size) [active]"; \
else \
echo " $$name ($$size)"; \
fi; \
found=1; \
done; \
[ "$$found" = "0" ] && echo " (no backups yet — run: make vm_backup NAME=<name>)"; \
else \
echo " (no backups yet — run: make vm_backup NAME=<name>)"; \
fi
amfidont_allow_vphone: bundle
zsh $(SCRIPTS)/start_amfidont_for_vphone.sh

View File

@@ -218,6 +218,21 @@ Connect via:
- **VNC:** `vnc://127.0.0.1:5901`
- [**RPC:**](http://github.com/doronz88/rpc-project) `rpcclient -p 5910 127.0.0.1`
## VM Backup & Switch
Save and switch between multiple VM environments (e.g. different iOS builds or firmware variants). Backups are stored in `vm.backups/` using `rsync --sparse` for efficient sparse disk handling.
```bash
make vm_backup NAME=26.1-clean # save current VM
rm -rf vm && make vm_new # start fresh for a different build
# ... fw_prepare, fw_patch, restore, cfw_install, boot
make vm_backup NAME=26.3-jb # save the new one too
make vm_list # list all saved backups
make vm_switch NAME=26.1-clean # swap between them
```
> **Note:** Always stop the VM before backup/switch/restore.
## FAQ
> **Before anything else — run `git pull` to make sure you have the latest version.**

View File

@@ -209,6 +209,21 @@ iproxy 5910 5910 # RPC
- **VNC:** `vnc://127.0.0.1:5901`
- [**RPC:**](http://github.com/doronz88/rpc-project) `rpcclient -p 5910 127.0.0.1`
## VM バックアップと切り替え
複数の VM 環境(異なる iOS ビルドやファームウェアバリアントなど)を保存して切り替えることができます。バックアップは `vm.backups/` に保存され、`rsync --sparse` でスパースディスクイメージを効率的に処理します。
```bash
make vm_backup NAME=26.1-clean # 現在の VM を保存
rm -rf vm && make vm_new # 新しいビルド用に初期化
# ... fw_prepare, fw_patch, restore, cfw_install, boot
make vm_backup NAME=26.3-jb # 新しい VM も保存
make vm_list # すべてのバックアップを一覧表示
make vm_switch NAME=26.1-clean # バックアップ間を切り替え
```
> **注意:** バックアップ/切り替え/復元の前に必ず VM を停止してください。
## よくある質問 (FAQ)
> **何よりもまず — `git pull` を実行して最新バージョンであることを確認してください**

View File

@@ -209,6 +209,21 @@ iproxy 5910 5910 # RPC
- **VNC:** `vnc://127.0.0.1:5901`
- [**RPC:**](http://github.com/doronz88/rpc-project) `rpcclient -p 5910 127.0.0.1`
## VM 백업 및 전환
여러 VM 환경(예: 다른 iOS 빌드 또는 펌웨어 변형)을 저장하고 전환할 수 있습니다. 백업은 `vm.backups/`에 저장되며 `rsync --sparse`를 사용하여 희소 디스크 이미지를 효율적으로 처리합니다.
```bash
make vm_backup NAME=26.1-clean # 현재 VM 저장
rm -rf vm && make vm_new # 새로운 빌드를 위해 초기화
# ... fw_prepare, fw_patch, restore, cfw_install, boot
make vm_backup NAME=26.3-jb # 새 VM도 저장
make vm_list # 모든 백업 목록 보기
make vm_switch NAME=26.1-clean # 백업 간 전환
```
> **참고:** 백업/전환/복원 전에 반드시 VM을 중지하세요.
## FAQ
> **무엇보다 먼저 — `git pull`을 실행하여 최신 버전인지 확인하세요.**

View File

@@ -209,6 +209,21 @@ iproxy 5910 5910 # RPC
- **VNC** `vnc://127.0.0.1:5901`
- [**RPC**](http://github.com/doronz88/rpc-project) `rpcclient -p 5910 127.0.0.1`
## VM 备份与切换
保存并切换多个 VM 环境(例如不同的 iOS 构建版本或固件变体)。备份存储在 `vm.backups/` 下,使用 `rsync --sparse` 高效处理稀疏磁盘镜像。
```bash
make vm_backup NAME=26.1-clean # 保存当前 VM
rm -rf vm && make vm_new # 清空后从新构建开始
# ... fw_prepare, fw_patch, restore, cfw_install, boot
make vm_backup NAME=26.3-jb # 保存新的 VM
make vm_list # 列出所有备份
make vm_switch NAME=26.1-clean # 在不同备份之间切换
```
> **注意:** 备份/切换/恢复前请先停止 VM。
## 常见问题FAQ
> **在做其他任何事情之前——先运行 `git pull` 确保你有最新版。**

95
scripts/vm_backup.sh Executable file
View File

@@ -0,0 +1,95 @@
#!/bin/zsh
# vm_backup.sh — Save the current VM as a named backup.
#
# Backups are stored under vm.backups/<name>/ using rsync --sparse.
# The active VM remembers its name in vm/.vm_name for use by vm_switch.
#
# Usage:
# make vm_backup NAME=ios17
# make vm_backup NAME=ios18-jb BACKUP_INCLUDE_IPSW=1
set -euo pipefail
VM_DIR="${VM_DIR:-vm}"
BACKUPS_DIR="${BACKUPS_DIR:-vm.backups}"
NAME="${NAME:-}"
BACKUP_INCLUDE_IPSW="${BACKUP_INCLUDE_IPSW:-0}"
# --- Parse args ---
while [[ $# -gt 0 ]]; do
case "$1" in
--name) NAME="$2"; shift 2 ;;
--include-ipsw) BACKUP_INCLUDE_IPSW=1; shift ;;
-h|--help)
echo "Usage: $0 --name <name> [--include-ipsw]"
exit 0
;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
if [[ -z "${NAME}" ]]; then
echo "ERROR: NAME is required."
echo " Usage: make vm_backup NAME=ios17"
exit 1
fi
# Reject names with slashes or dots to keep the backups dir clean
if [[ "${NAME}" == */* || "${NAME}" == .* ]]; then
echo "ERROR: NAME must be a simple identifier (no slashes or leading dots)."
exit 1
fi
# --- Validate source ---
if [[ ! -d "${VM_DIR}" ]]; then
echo "ERROR: VM directory not found: ${VM_DIR}"
exit 1
fi
if [[ ! -f "${VM_DIR}/config.plist" ]]; then
echo "ERROR: ${VM_DIR}/config.plist not found — is this a valid VM directory?"
exit 1
fi
# --- Check for running VM ---
if pgrep -f "vphone-cli.*--config.*${VM_DIR}" >/dev/null 2>&1; then
echo "WARNING: vphone-cli appears to be running against ${VM_DIR}."
echo " Backing up a live VM may produce an inconsistent snapshot."
printf "Continue anyway? [y/N] "
read -r answer
[[ "${answer}" =~ ^[Yy]$ ]] || exit 1
fi
DEST="${BACKUPS_DIR}/${NAME}"
echo "=== vphone vm_backup ==="
echo "Name : ${NAME}"
echo "Source : ${VM_DIR}/"
echo "Dest : ${DEST}/"
src_size="$(du -sh "${VM_DIR}" 2>/dev/null | cut -f1)"
echo "Size : ${src_size} (on disk)"
RSYNC_EXCLUDES=()
if [[ "${BACKUP_INCLUDE_IPSW}" != "1" ]]; then
RSYNC_EXCLUDES+=(--exclude '*_Restore*/')
echo "IPSW : excluded (use BACKUP_INCLUDE_IPSW=1 to include)"
fi
echo ""
# --- Sync ---
mkdir -p "${DEST}"
rsync -aH --sparse --progress --delete \
"${RSYNC_EXCLUDES[@]}" \
"${VM_DIR}/" "${DEST}/"
# Tag the active VM with this name
echo "${NAME}" > "${VM_DIR}/.vm_name"
echo ""
echo "=== Saved as '${NAME}' ==="
backup_size="$(du -sh "${DEST}" 2>/dev/null | cut -f1)"
echo "Backup size : ${backup_size}"
echo ""
echo "To restore : make vm_restore NAME=${NAME}"
echo "To switch : make vm_switch NAME=${NAME}"
echo "List all : make vm_list"

102
scripts/vm_restore.sh Executable file
View File

@@ -0,0 +1,102 @@
#!/bin/zsh
# vm_restore.sh — Restore a named backup into the active VM directory.
#
# Usage:
# make vm_restore NAME=ios17
# make vm_restore NAME=ios17 FORCE=1
set -euo pipefail
VM_DIR="${VM_DIR:-vm}"
BACKUPS_DIR="${BACKUPS_DIR:-vm.backups}"
NAME="${NAME:-}"
FORCE="${FORCE:-0}"
# --- Parse args ---
while [[ $# -gt 0 ]]; do
case "$1" in
--name) NAME="$2"; shift 2 ;;
--force) FORCE=1; shift ;;
-h|--help)
echo "Usage: $0 --name <name> [--force]"
exit 0
;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
if [[ -z "${NAME}" ]]; then
echo "ERROR: NAME is required."
echo " Usage: make vm_restore NAME=ios17"
echo ""
echo "Available backups:"
if [[ -d "${BACKUPS_DIR}" ]]; then
for d in "${BACKUPS_DIR}"/*/; do
[[ -f "${d}config.plist" ]] && echo " - $(basename "${d}")"
done
else
echo " (none)"
fi
exit 1
fi
SRC="${BACKUPS_DIR}/${NAME}"
# --- Validate backup ---
if [[ ! -d "${SRC}" ]]; then
echo "ERROR: Backup '${NAME}' not found at ${SRC}/"
echo ""
echo "Available backups:"
if [[ -d "${BACKUPS_DIR}" ]]; then
for d in "${BACKUPS_DIR}"/*/; do
[[ -f "${d}config.plist" ]] && echo " - $(basename "${d}")"
done
else
echo " (none)"
fi
exit 1
fi
if [[ ! -f "${SRC}/config.plist" ]]; then
echo "ERROR: ${SRC}/config.plist not found — backup appears invalid."
exit 1
fi
# --- Check for running VM ---
if pgrep -f "vphone-cli.*--config.*${VM_DIR}" >/dev/null 2>&1; then
echo "ERROR: vphone-cli appears to be running against ${VM_DIR}."
echo " Stop the VM before restoring."
exit 1
fi
# --- Confirm overwrite ---
if [[ -d "${VM_DIR}" && -f "${VM_DIR}/Disk.img" && "${FORCE}" != "1" ]]; then
current=""
[[ -f "${VM_DIR}/.vm_name" ]] && current="$(< "${VM_DIR}/.vm_name")"
echo "WARNING: ${VM_DIR}/ already exists${current:+ (current: '${current}')}."
echo " This will overwrite it with backup '${NAME}'."
echo " Back up first with: make vm_backup NAME=<name>"
printf "Continue? [y/N] "
read -r answer
[[ "${answer}" =~ ^[Yy]$ ]] || exit 1
fi
echo "=== vphone vm_restore ==="
echo "Name : ${NAME}"
echo "Source : ${SRC}/"
echo "Dest : ${VM_DIR}/"
backup_size="$(du -sh "${SRC}" 2>/dev/null | cut -f1)"
echo "Size : ${backup_size} (on disk)"
echo ""
# --- Sync ---
mkdir -p "${VM_DIR}"
rsync -aH --sparse --progress --delete \
"${SRC}/" "${VM_DIR}/"
# Tag the active VM
echo "${NAME}" > "${VM_DIR}/.vm_name"
echo ""
echo "=== Restored '${NAME}' ==="
echo "Next: make boot"

118
scripts/vm_switch.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/bin/zsh
# vm_switch.sh — Switch the active VM to a different named backup.
#
# Saves the current VM under its name (from vm/.vm_name), then restores
# the target backup. If the current VM has no name yet, prompts for one.
#
# Usage:
# make vm_switch NAME=ios18
set -euo pipefail
VM_DIR="${VM_DIR:-vm}"
BACKUPS_DIR="${BACKUPS_DIR:-vm.backups}"
NAME="${NAME:-}"
BACKUP_INCLUDE_IPSW="${BACKUP_INCLUDE_IPSW:-0}"
# --- Parse args ---
while [[ $# -gt 0 ]]; do
case "$1" in
--name) NAME="$2"; shift 2 ;;
--include-ipsw) BACKUP_INCLUDE_IPSW=1; shift ;;
-h|--help)
echo "Usage: $0 --name <target> [--include-ipsw]"
exit 0
;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
if [[ -z "${NAME}" ]]; then
echo "ERROR: NAME is required (the backup to switch to)."
echo " Usage: make vm_switch NAME=ios18"
echo ""
echo "Available backups:"
if [[ -d "${BACKUPS_DIR}" ]]; then
for d in "${BACKUPS_DIR}"/*/; do
[[ -f "${d}config.plist" ]] && echo " - $(basename "${d}")"
done
else
echo " (none)"
fi
exit 1
fi
TARGET="${BACKUPS_DIR}/${NAME}"
if [[ ! -d "${TARGET}" || ! -f "${TARGET}/config.plist" ]]; then
echo "ERROR: Backup '${NAME}' not found."
echo ""
echo "Available backups:"
if [[ -d "${BACKUPS_DIR}" ]]; then
for d in "${BACKUPS_DIR}"/*/; do
[[ -f "${d}config.plist" ]] && echo " - $(basename "${d}")"
done
else
echo " (none)"
fi
exit 1
fi
# --- Check for running VM ---
if pgrep -f "vphone-cli.*--config.*${VM_DIR}" >/dev/null 2>&1; then
echo "ERROR: vphone-cli appears to be running against ${VM_DIR}."
echo " Stop the VM before switching."
exit 1
fi
# --- Determine current VM name ---
CURRENT=""
if [[ -d "${VM_DIR}" && -f "${VM_DIR}/config.plist" ]]; then
if [[ -f "${VM_DIR}/.vm_name" ]]; then
CURRENT="$(< "${VM_DIR}/.vm_name")"
fi
if [[ -z "${CURRENT}" ]]; then
echo "Current VM has no name. Give it one to save before switching."
printf "Name for current VM: "
read -r CURRENT
if [[ -z "${CURRENT}" ]]; then
echo "ERROR: Cannot switch without saving the current VM."
exit 1
fi
fi
if [[ "${CURRENT}" == "${NAME}" ]]; then
echo "'${NAME}' is already the active VM."
exit 0
fi
# --- Save current ---
echo "=== Saving current VM as '${CURRENT}' ==="
CURRENT_DEST="${BACKUPS_DIR}/${CURRENT}"
mkdir -p "${CURRENT_DEST}"
RSYNC_EXCLUDES=()
if [[ "${BACKUP_INCLUDE_IPSW}" != "1" ]]; then
RSYNC_EXCLUDES+=(--exclude '*_Restore*/')
fi
rsync -aH --sparse --progress --delete \
"${RSYNC_EXCLUDES[@]}" \
"${VM_DIR}/" "${CURRENT_DEST}/"
echo ""
fi
# --- Restore target ---
echo "=== Restoring '${NAME}' ==="
mkdir -p "${VM_DIR}"
rsync -aH --sparse --progress --delete \
"${TARGET}/" "${VM_DIR}/"
echo "${NAME}" > "${VM_DIR}/.vm_name"
echo ""
echo "=== Switched: ${CURRENT:+${CURRENT}}${NAME} ==="
echo "Next: make boot"