From 1d7ae7fe55c6af8467c458600669cc3c7aad7059 Mon Sep 17 00:00:00 2001 From: Lakr <25259084+Lakr233@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:24:30 +0900 Subject: [PATCH] Merge pull request #42 from zqxwce/main --- Makefile | 6 +- README.md | 11 + scripts/cfw_install.sh | 2 +- scripts/cfw_install_jb.sh | 2 +- scripts/ramdisk_build.py | 3 +- scripts/setup_machine.sh | 321 ++++++++++++++++++++++++++ sources/vphone-cli/VPhoneVMView.swift | 26 ++- 7 files changed, 354 insertions(+), 17 deletions(-) create mode 100755 scripts/setup_machine.sh diff --git a/Makefile b/Makefile index fcdf9dc..d694d2e 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,7 @@ help: @echo "vphone-cli — Virtual iPhone boot tool" @echo "" @echo "Setup (one-time):" + @echo " make setup_machine Full setup through README First Boot" @echo " make setup_venv Create Python .venv" @echo " make setup_libimobiledevice Build libimobiledevice toolchain" @echo "" @@ -65,7 +66,10 @@ help: # Setup # ═══════════════════════════════════════════════════════════════════ -.PHONY: setup_venv setup_libimobiledevice +.PHONY: setup_machine setup_venv setup_libimobiledevice + +setup_machine: + zsh $(SCRIPTS)/setup_machine.sh setup_venv: zsh $(SCRIPTS)/setup_venv.sh diff --git a/README.md b/README.md index aab8e28..96f966d 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,22 @@ Restart once more. **Install dependencies:** ```bash +brew install gnu-tar sshpass keystone autoconf automake pkg-config libtool +``` + +## First setup + +```bash +make setup_machine # full automation through "First Boot" (includes restore/ramdisk/CFW) + +# equivalent manual steps: make setup_libimobiledevice # build libimobiledevice toolchain make setup_venv # create Python venv source .venv/bin/activate ``` +`make setup_machine` still requires manual **Recovery-mode SIP/research-guest configuration** and an interactive VM console for the First Boot commands it prints. The script does not validate those security settings. + ## Quick Start ```bash diff --git a/scripts/cfw_install.sh b/scripts/cfw_install.sh index ac4c0bb..7399e64 100755 --- a/scripts/cfw_install.sh +++ b/scripts/cfw_install.sh @@ -45,7 +45,7 @@ SSH_OPTS=( die() { echo "[-] $*" >&2; exit 1; } _sshpass() { - "$VM_DIR/$CFW_INPUT/tools/sshpass" -p "$SSH_PASS" "$@" + "sshpass" -p "$SSH_PASS" "$@" } ssh_cmd() { diff --git a/scripts/cfw_install_jb.sh b/scripts/cfw_install_jb.sh index 616fcbc..9ab7eb6 100755 --- a/scripts/cfw_install_jb.sh +++ b/scripts/cfw_install_jb.sh @@ -51,7 +51,7 @@ SSH_OPTS=( die() { echo "[-] $*" >&2; exit 1; } _sshpass() { - "$VM_DIR/$CFW_INPUT/tools/sshpass" -p "$SSH_PASS" "$@" + "sshpass" -p "$SSH_PASS" "$@" } ssh_cmd() { diff --git a/scripts/ramdisk_build.py b/scripts/ramdisk_build.py index 932a15a..3e603ac 100755 --- a/scripts/ramdisk_build.py +++ b/scripts/ramdisk_build.py @@ -251,9 +251,8 @@ def build_ramdisk(restore_dir, im4m_path, vm_dir, input_dir, output_dir, temp_di ramdisk_custom, "-owners", "off"]) print(" Injecting SSH tools...") - gtar = os.path.join(input_dir, "tools/gtar") ssh_tar = os.path.join(input_dir, "ssh.tar.gz") - run(["sudo", gtar, "-x", "--no-overwrite-dir", + run(["sudo", "gtar", "-x", "--no-overwrite-dir", "-f", ssh_tar, "-C", mountpoint]) # Remove unnecessary files diff --git a/scripts/setup_machine.sh b/scripts/setup_machine.sh new file mode 100755 index 0000000..9723b67 --- /dev/null +++ b/scripts/setup_machine.sh @@ -0,0 +1,321 @@ +#!/bin/zsh +# setup_machine.sh — Full vphone machine bootstrap through "First Boot". +# +# Runs README flow up to (but not including) "Subsequent Boots": +# 1) Host deps + project setup/build +# 2) vm_new + fw_prepare + fw_patch +# 3) DFU restore (boot_dfu + restore_get_shsh + restore) +# 4) Ramdisk + CFW (boot_dfu + ramdisk_build + ramdisk_send + iproxy + cfw_install) +# 5) First boot launch (`make boot`) with printed in-guest commands + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +cd "$PROJECT_ROOT" + +LOG_DIR="${PROJECT_ROOT}/setup_logs" +DFU_LOG="${LOG_DIR}/boot_dfu.log" +IPROXY_LOG="${LOG_DIR}/iproxy_2222.log" + +DFU_PID="" +IPROXY_PID="" +BOOT_PID="" +BOOT_FIFO="" +BOOT_FIFO_FD="" + +VM_DIR="${VM_DIR:-vm}" + +die() { + echo "[-] $*" >&2 + exit 1 +} + +require_cmd() { + local cmd="$1" + command -v "$cmd" >/dev/null 2>&1 || die "Missing required command: $cmd" +} + +list_descendants() { + local pid + local -a children + + children=("${(@f)$(pgrep -P "$1" 2>/dev/null || true)}") + for pid in "${children[@]}"; do + [[ -z "$pid" ]] && continue + list_descendants "$pid" + print -r -- "$pid" + done +} + +kill_descendants() { + local -a descendants + descendants=("${(@f)$(list_descendants "$1")}") + [[ ${#descendants[@]} -gt 0 ]] && kill -9 "${descendants[@]}" >/dev/null 2>&1 || true +} + +cleanup() { + if [[ -n "$BOOT_FIFO_FD" ]]; then + exec {BOOT_FIFO_FD}>&- || true + BOOT_FIFO_FD="" + fi + + if [[ -n "$BOOT_PID" ]] && kill -0 "$BOOT_PID" 2>/dev/null; then + kill_descendants "$BOOT_PID" + kill -9 "$BOOT_PID" >/dev/null 2>&1 || true + wait "$BOOT_PID" 2>/dev/null || true + BOOT_PID="" + fi + + if [[ -n "$BOOT_FIFO" && -p "$BOOT_FIFO" ]]; then + rm -f "$BOOT_FIFO" || true + BOOT_FIFO="" + fi + + if [[ -n "$IPROXY_PID" ]]; then + kill -9 "$IPROXY_PID" >/dev/null 2>&1 || true + wait "$IPROXY_PID" 2>/dev/null || true + IPROXY_PID="" + fi + + if [[ -n "$DFU_PID" ]]; then + kill_descendants "$DFU_PID" + kill -9 "$DFU_PID" >/dev/null 2>&1 || true + wait "$DFU_PID" 2>/dev/null || true + DFU_PID="" + fi +} + +start_first_boot() { + BOOT_FIFO="$(mktemp -u "${TMPDIR:-/tmp}/vphone-first-boot.XXXXXX")" + mkfifo "$BOOT_FIFO" + + make boot <"$BOOT_FIFO" & + BOOT_PID=$! + + exec {BOOT_FIFO_FD}>"$BOOT_FIFO" + + sleep 2 + if ! kill -0 "$BOOT_PID" 2>/dev/null; then + die "make boot exited early during first boot stage" + fi +} + +send_first_boot_commands() { + [[ -n "$BOOT_FIFO_FD" ]] || die "First boot command channel is not open" + + local commands=( + "export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11:/usr/games:/iosbinpack64/usr/local/sbin:/iosbinpack64/usr/local/bin:/iosbinpack64/usr/sbin:/iosbinpack64/usr/bin:/iosbinpack64/sbin:/iosbinpack64/bin'" + "mkdir -p /var/dropbear" + "cp /iosbinpack64/etc/profile /var/profile" + "cp /iosbinpack64/etc/motd /var/motd" + "dropbearkey -t rsa -f /var/dropbear/dropbear_rsa_host_key" + "dropbearkey -t ecdsa -f /var/dropbear/dropbear_ecdsa_host_key" + "shutdown -h now" + ) + + local cmd + for cmd in "${commands[@]}"; do + print -r -- "$cmd" >&${BOOT_FIFO_FD} + done +} + +trap cleanup EXIT INT TERM + +check_platform() { + [[ "$(uname -s)" == "Darwin" ]] || die "This script supports macOS only" + + local major + major="$(sw_vers -productVersion | cut -d. -f1)" + if [[ -z "$major" || "$major" -lt 14 ]]; then + die "macOS 14+ required (detected: $(sw_vers -productVersion))" + fi +} + +install_brew_deps() { + require_cmd brew + + local deps=( + autoconf + automake + cmake + git + keystone + libtool + pkg-config + python@3.13 + ) + + echo "=== Installing Homebrew dependencies ===" + for pkg in "${deps[@]}"; do + if brew list --formula "$pkg" >/dev/null 2>&1; then + echo " $pkg: already installed" + else + echo " $pkg: installing" + brew install "$pkg" + fi + done + echo "" +} + +ensure_python_linked() { + if ! command -v python3.13 >/dev/null 2>&1; then + local pybin + pybin="$(brew --prefix python@3.13)/bin" + export PATH="$pybin:$PATH" + fi + + require_cmd python3.13 +} + +run_make() { + local label="$1" + shift + + echo "" + echo "=== ${label} ===" + make "$@" +} + +start_boot_dfu() { + mkdir -p "$LOG_DIR" + + if [[ -n "$DFU_PID" ]] && kill -0 "$DFU_PID" 2>/dev/null; then + return + fi + + : > "$DFU_LOG" + echo "[*] Starting DFU boot in background..." + (make boot_dfu >"$DFU_LOG" 2>&1) & + DFU_PID=$! + + sleep 2 + if ! kill -0 "$DFU_PID" 2>/dev/null; then + echo "[-] make boot_dfu exited early. Last log lines:" + tail -n 40 "$DFU_LOG" || true + exit 1 + fi + + echo "[+] boot_dfu running (pid=$DFU_PID, log=$DFU_LOG)" +} + +stop_boot_dfu() { + if [[ -n "$DFU_PID" ]] && kill -0 "$DFU_PID" 2>/dev/null; then + echo "[*] Stopping background DFU boot (pid=$DFU_PID)..." + kill_descendants "$DFU_PID" + kill -9 "$DFU_PID" >/dev/null 2>&1 || true + wait "$DFU_PID" 2>/dev/null || true + fi + DFU_PID="" +} + +wait_for_recovery() { + local irecovery="${PROJECT_ROOT}/.limd/bin/irecovery" + [[ -x "$irecovery" ]] || die "irecovery not found at $irecovery" + + echo "[*] Waiting for recovery/DFU endpoint..." + local i + for i in {1..90}; do + if "$irecovery" -q >/dev/null 2>&1; then + echo "[+] Device endpoint is reachable" + return + fi + sleep 2 + done + + echo "[-] Timed out waiting for device endpoint. Last DFU log lines:" + tail -n 60 "$DFU_LOG" || true + exit 1 +} + +start_iproxy_2222() { + local iproxy_bin + iproxy_bin="${PROJECT_ROOT}/.limd/bin/iproxy" + [[ -n "$iproxy_bin" ]] || die "iproxy not found in PATH" + + mkdir -p "$LOG_DIR" + : > "$IPROXY_LOG" + + echo "[*] Starting iproxy 2222 -> 22..." + ("$iproxy_bin" 2222 22 >"$IPROXY_LOG" 2>&1) & + IPROXY_PID=$! + + sleep 1 + if ! kill -0 "$IPROXY_PID" 2>/dev/null; then + echo "[-] iproxy exited early. Log:" + tail -n 40 "$IPROXY_LOG" || true + exit 1 + fi + + echo "[+] iproxy running (pid=$IPROXY_PID, log=$IPROXY_LOG)" +} + +stop_iproxy_2222() { + if [[ -n "$IPROXY_PID" ]] && kill -0 "$IPROXY_PID" 2>/dev/null; then + echo "[*] Stopping iproxy (pid=$IPROXY_PID)..." + kill_descendants "$IPROXY_PID" + kill -9 "$IPROXY_PID" >/dev/null 2>&1 || true + wait "$IPROXY_PID" 2>/dev/null || true + fi + IPROXY_PID="" +} + +main() { + run_make "Project setup" setup_libimobiledevice + run_make "Project setup" setup_venv + run_make "Project setup" build + + run_make "Firmware prep" vm_new + run_make "Firmware prep" fw_prepare + run_make "Firmware patch" fw_patch + + echo "" + echo "=== Restore phase ===" + start_boot_dfu + wait_for_recovery + run_make "Restore" restore_get_shsh + run_make "Restore" restore + stop_boot_dfu + + echo "" + echo "=== Ramdisk + CFW phase ===" + start_boot_dfu + wait_for_recovery + run_make "Ramdisk" ramdisk_build + run_make "Ramdisk" ramdisk_send + start_iproxy_2222 + + sleep 10 # for some reason there is a statistical faiure here if not enough time is given to initialization + + run_make "CFW install" cfw_install + stop_iproxy_2222 + stop_boot_dfu + + echo "" + echo "=== First boot ===" + read -r "?[*] press Enter to start VM, after the VM has finished booting, press Enter again to finish last stage" + + start_first_boot + + read -r "?[*] Press Enter once the VM is fully booted" + send_first_boot_commands + + echo "[*] Commands sent. Waiting for VM shutdown..." + wait "$BOOT_PID" + BOOT_PID="" + + exec {BOOT_FIFO_FD}>&- || true + BOOT_FIFO_FD="" + rm -f "$BOOT_FIFO" || true + BOOT_FIFO="" + + echo "" + echo "=== Done ===" + echo "Setup completed." + + echo "=== Booting VM ===" + run_make "Booting VM" boot +} + +main "$@" diff --git a/sources/vphone-cli/VPhoneVMView.swift b/sources/vphone-cli/VPhoneVMView.swift index 1629f8c..d7a37ee 100644 --- a/sources/vphone-cli/VPhoneVMView.swift +++ b/sources/vphone-cli/VPhoneVMView.swift @@ -24,32 +24,35 @@ class VPhoneVMView: VZVirtualMachineView { // MARK: - Event Handling override func mouseDown(with event: NSEvent) { - let location = self.convert(event.locationInWindow, from: nil) + let localPoint = self.convert(event.locationInWindow, from: nil) - self.currentTouchSwipeAim = hitTestEdge(at: location) + self.currentTouchSwipeAim = hitTestEdge(at: localPoint) sendTouchEvent( phase: 0, // Began - locationInWindow: event.locationInWindow, + localPoint: localPoint, timestamp: event.timestamp ) } override func mouseDragged(with event: NSEvent) { + let localPoint = self.convert(event.locationInWindow, from: nil) sendTouchEvent( phase: 1, // Moved - locationInWindow: event.locationInWindow, + localPoint: localPoint, timestamp: event.timestamp ) super.mouseDragged(with: event) } override func mouseUp(with event: NSEvent) { + let localPoint = self.convert(event.locationInWindow, from: nil) sendTouchEvent( phase: 3, // Ended - locationInWindow: event.locationInWindow, + localPoint: localPoint, timestamp: event.timestamp ) + self.currentTouchSwipeAim = 0 super.mouseUp(with: event) } @@ -63,15 +66,15 @@ class VPhoneVMView: VZVirtualMachineView { // MARK: - Touch Injection Logic - private func sendTouchEvent(phase: Int, locationInWindow: NSPoint, timestamp: TimeInterval) { + private func sendTouchEvent(phase: Int, localPoint: NSPoint, timestamp: TimeInterval) { guard let device = multiTouchDevice, - let vm = self.virtualMachine + self.virtualMachine != nil else { return } - let normalizedPoint = normalizeCoordinate(locationInWindow) + let normalizedPoint = normalizeCoordinate(localPoint) let touch = Dynamic._VZTouch( - view: vm, + view: self, index: 0, phase: phase, location: normalizedPoint, @@ -92,8 +95,7 @@ class VPhoneVMView: VZVirtualMachineView { // MARK: - Coordinate Helpers - private func normalizeCoordinate(_ locationInWindow: NSPoint) -> CGPoint { - let localPoint = self.convert(locationInWindow, from: nil) + private func normalizeCoordinate(_ localPoint: NSPoint) -> CGPoint { let w = self.bounds.width let h = self.bounds.height @@ -144,4 +146,4 @@ class VPhoneVMView: VZVirtualMachineView { return minDist < edgeThreshold ? edgeCode : 0 } -} \ No newline at end of file +}