diff --git a/.gitignore b/.gitignore index 06e7890..3d74e12 100644 --- a/.gitignore +++ b/.gitignore @@ -330,3 +330,4 @@ setup_logs/ /research/artifacts /research/xnu /vm +/vm.backups diff --git a/Makefile b/Makefile index a4e62c8..fe48596 100644 --- a/Makefile +++ b/Makefile @@ -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= Save current VM as a named backup" + @echo " make vm_restore NAME= Restore a named backup into vm/" + @echo " make vm_switch 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=)"; \ + else \ + echo " (no backups yet — run: make vm_backup NAME=)"; \ + fi + amfidont_allow_vphone: bundle zsh $(SCRIPTS)/start_amfidont_for_vphone.sh diff --git a/README.md b/README.md index 535f4b9..e301d22 100644 --- a/README.md +++ b/README.md @@ -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.** diff --git a/docs/README_ja.md b/docs/README_ja.md index 900f2cb..884e7de 100644 --- a/docs/README_ja.md +++ b/docs/README_ja.md @@ -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` を実行して最新バージョンであることを確認してください** diff --git a/docs/README_ko.md b/docs/README_ko.md index 862e745..528cef2 100644 --- a/docs/README_ko.md +++ b/docs/README_ko.md @@ -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`을 실행하여 최신 버전인지 확인하세요.** diff --git a/docs/README_zh.md b/docs/README_zh.md index 4508600..4618890 100644 --- a/docs/README_zh.md +++ b/docs/README_zh.md @@ -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` 确保你有最新版。** diff --git a/scripts/vm_backup.sh b/scripts/vm_backup.sh new file mode 100755 index 0000000..c0f8c69 --- /dev/null +++ b/scripts/vm_backup.sh @@ -0,0 +1,95 @@ +#!/bin/zsh +# vm_backup.sh — Save the current VM as a named backup. +# +# Backups are stored under vm.backups// 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 [--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" diff --git a/scripts/vm_restore.sh b/scripts/vm_restore.sh new file mode 100755 index 0000000..f2241d6 --- /dev/null +++ b/scripts/vm_restore.sh @@ -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 [--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=" + 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" diff --git a/scripts/vm_switch.sh b/scripts/vm_switch.sh new file mode 100755 index 0000000..7322832 --- /dev/null +++ b/scripts/vm_switch.sh @@ -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 [--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"