mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 04:59:05 +08:00
Add JB finalizer script; remove IPA signing UI
Add scripts/cfw_install_jb_post.sh — an idempotent SSH-based finalizer to complete JB bootstrap on a normally-booted vphone (creates /var/jb symlink, fixes ownership, runs prep_bootstrap, creates markers, installs Sileo, and runs apt; requires sshpass). Add Makefile help, .PHONY and target cfw_install_jb_finalize to invoke the script. Remove host-side IPA signing/installing and related UI: delete VPhoneSigner, VPhoneIPAInstaller, VPhoneMenuInstall and remove signer/ipaInstaller fields and menu items/callbacks from the vphone-cli UI (also removed the DevMode enable WIP flow). Misc: minor table/formatting tweaks in AGENTS.md and research docs.
This commit is contained in:
@@ -29,10 +29,10 @@ For any changes applying new patches, also update research/0_binary_patch_compar
|
||||
|
||||
## Firmware Variants
|
||||
|
||||
| Variant | Boot Chain | CFW | Make Targets |
|
||||
| ------------------- | :--------: | :-------: | ---------------------------------- |
|
||||
| **Regular** | 51 patches | 10 phases | `fw_patch` + `cfw_install` |
|
||||
| **Development** | 64 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` |
|
||||
| Variant | Boot Chain | CFW | Make Targets |
|
||||
| ------------------- | :---------: | :-------: | ---------------------------------- |
|
||||
| **Regular** | 51 patches | 10 phases | `fw_patch` + `cfw_install` |
|
||||
| **Development** | 64 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` |
|
||||
| **Jailbreak (WIP)** | 126 patches | 14 phases | `fw_patch_jb` + `cfw_install_jb` |
|
||||
|
||||
See `research/` for detailed firmware pipeline, component origins, patch breakdowns, and boot flow documentation.
|
||||
|
||||
6
Makefile
6
Makefile
@@ -81,6 +81,7 @@ help:
|
||||
@echo " make cfw_install Install CFW mods via SSH"
|
||||
@echo " make cfw_install_dev Install CFW mods via SSH (dev mode)"
|
||||
@echo " make cfw_install_jb Install CFW + JB extensions (jetsam/procursus/basebin)"
|
||||
@echo " make cfw_install_jb_finalize Finalize JB bootstrap on live device (symlinks/sileo/apt)"
|
||||
@echo ""
|
||||
@echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE)"
|
||||
|
||||
@@ -243,7 +244,7 @@ ramdisk_send:
|
||||
# CFW
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
.PHONY: cfw_install cfw_install_dev cfw_install_jb
|
||||
.PHONY: cfw_install cfw_install_dev cfw_install_jb cfw_install_jb_finalize
|
||||
|
||||
cfw_install:
|
||||
cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" .
|
||||
@@ -253,3 +254,6 @@ cfw_install_dev:
|
||||
|
||||
cfw_install_jb:
|
||||
cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" .
|
||||
|
||||
cfw_install_jb_finalize:
|
||||
$(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") $(if $(SSH_PASS),SSH_PASS="$(SSH_PASS)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb_post.sh"
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
| # | Group | Method | Function | Purpose | JB Enabled |
|
||||
| ----- | ----- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------: |
|
||||
| JB-01 | A | `patch_amfi_cdhash_in_trustcache` | `AMFIIsCDHashInTrustCache` | Always return true + store hash | Y |
|
||||
| JB-02 | A | `patch_amfi_execve_kill_path` | AMFI execve kill return site | Convert shared kill return from deny to allow (superseded by C21; standalone only) | N |
|
||||
| JB-02 | A | `patch_amfi_execve_kill_path` | AMFI execve kill return site | Convert shared kill return from deny to allow (superseded by C21; standalone only) | N |
|
||||
| JB-03 | C | `patch_cred_label_update_execve` | `_cred_label_update_execve` | Reworked C21-v3: C21-v1 already boots; v3 keeps split late exits and additionally ORs success-only helper bits `0xC` after clearing `0x3F00`; still disabled pending boot validation | N |
|
||||
| JB-04 | C | `patch_hook_cred_label_update_execve` | sandbox `mpo_cred_label_update_execve` wrapper (`ops[18]` -> `sub_FFFFFE00093BDB64`) | Faithful upstream C23 trampoline: copy `VSUID`/`VSGID` owner state into pending cred, set `P_SUGID`, then branch back to wrapper | Y |
|
||||
| JB-05 | C | `patch_kcall10` | `sysent[439]` (`SYS_kas_info` replacement) | Rebuilt ABI-correct kcall cave: `target + 7 args -> uint64 x0`; re-enabled after focused dry-run validation | Y |
|
||||
@@ -130,19 +130,19 @@
|
||||
|
||||
### CFW Installer Flow Matrix (Script-Level)
|
||||
|
||||
| Flow Item | Regular (`cfw_install.sh`) | Dev (`cfw_install_dev.sh`) | JB (`cfw_install_jb.sh`) |
|
||||
| ---------------------------------------------------- | ------------------------------- | ----------------------------------------------- | --------------------------------------------- |
|
||||
| Base CFW phases (1/7 -> 7/7) | Runs directly | Runs directly | Runs via `CFW_SKIP_HALT=1 zsh cfw_install.sh` |
|
||||
| Dev overlay (`rpcserver_ios` replacement) | - | Y (`apply_dev_overlay`) | - |
|
||||
| SSH readiness wait before install | Y (`wait_for_device_ssh_ready`) | - | Y (inherited from base run) |
|
||||
| launchd jetsam patch (`patch-launchd-jetsam`) | - | Y (base-flow injection) | Y (JB-1) |
|
||||
| launchd dylib injection (`inject-dylib /b`) | - | - | Y (JB-1) |
|
||||
| Flow Item | Regular (`cfw_install.sh`) | Dev (`cfw_install_dev.sh`) | JB (`cfw_install_jb.sh`) |
|
||||
| --------------------------------------------- | ------------------------------- | -------------------------- | --------------------------------------------- |
|
||||
| Base CFW phases (1/7 -> 7/7) | Runs directly | Runs directly | Runs via `CFW_SKIP_HALT=1 zsh cfw_install.sh` |
|
||||
| Dev overlay (`rpcserver_ios` replacement) | - | Y (`apply_dev_overlay`) | - |
|
||||
| SSH readiness wait before install | Y (`wait_for_device_ssh_ready`) | - | Y (inherited from base run) |
|
||||
| launchd jetsam patch (`patch-launchd-jetsam`) | - | Y (base-flow injection) | Y (JB-1) |
|
||||
| launchd dylib injection (`inject-dylib /b`) | - | - | Y (JB-1) |
|
||||
|
||||
| Procursus bootstrap deployment | - | - | Y (JB-2) |
|
||||
| BaseBin hook deployment (`*.dylib` -> `/mnt1/cores`) | - | - | Y (JB-3) |
|
||||
| Additional input resources | `cfw_input` | `cfw_input` + `resources/cfw_dev/rpcserver_ios` | `cfw_input` + `cfw_jb_input` |
|
||||
| Extra tool requirement beyond base | - | - | `zstd` |
|
||||
| Halt behavior | Halts unless `CFW_SKIP_HALT=1` | Halts unless `CFW_SKIP_HALT=1` | Always halts after JB phases |
|
||||
| Procursus bootstrap deployment | - | - | Y (JB-2) |
|
||||
| BaseBin hook deployment (`*.dylib` -> `/mnt1/cores`) | - | - | Y (JB-3) |
|
||||
| Additional input resources | `cfw_input` | `cfw_input` + `resources/cfw_dev/rpcserver_ios` | `cfw_input` + `cfw_jb_input` |
|
||||
| Extra tool requirement beyond base | - | - | `zstd` |
|
||||
| Halt behavior | Halts unless `CFW_SKIP_HALT=1` | Halts unless `CFW_SKIP_HALT=1` | Always halts after JB phases |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -163,11 +163,11 @@
|
||||
|
||||
## Ramdisk Variant Matrix
|
||||
|
||||
| Variant | Pre-step | `Ramdisk/txm.img4` | `Ramdisk/krnl.ramdisk.img4` | `Ramdisk/krnl.img4` | Effective kernel used by `ramdisk_send.sh` |
|
||||
| ------------- | ------------------- | -------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------- |
|
||||
| `RAMDISK` | `make fw_patch` | release TXM + base TXM patch (1) | base kernel (28), legacy `*.ramdisk` preferred else derive from pristine CloudOS | restore kernel from `fw_patch` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` |
|
||||
| `DEV+RAMDISK` | `make fw_patch_dev` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_dev` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` |
|
||||
| `JB+RAMDISK` | `make fw_patch_jb` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_jb` (28+59) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` |
|
||||
| Variant | Pre-step | `Ramdisk/txm.img4` | `Ramdisk/krnl.ramdisk.img4` | `Ramdisk/krnl.img4` | Effective kernel used by `ramdisk_send.sh` |
|
||||
| ------------- | ------------------- | -------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------- |
|
||||
| `RAMDISK` | `make fw_patch` | release TXM + base TXM patch (1) | base kernel (28), legacy `*.ramdisk` preferred else derive from pristine CloudOS | restore kernel from `fw_patch` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` |
|
||||
| `DEV+RAMDISK` | `make fw_patch_dev` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_dev` (28) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` |
|
||||
| `JB+RAMDISK` | `make fw_patch_jb` | release TXM + base TXM patch (1) | base kernel (28), same derivation rule | restore kernel from `fw_patch_jb` (28+59) | `krnl.ramdisk.img4` preferred, fallback `krnl.img4` |
|
||||
|
||||
## Cross-Version Dynamic Snapshot
|
||||
|
||||
@@ -177,4 +177,3 @@
|
||||
| PCC 26.3 (`23D128`) | 14 | 59 |
|
||||
| iOS 26.1 (`23B85`) | 14 | 59 |
|
||||
| iOS 26.3 (`23D127`) | 14 | 59 |
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ Two methods added since initial document: `patch_shared_region_map`, `patch_io_s
|
||||
Three previously failing patches (`patch_nvram_verify_permission`, `patch_thid_should_crash`, `patch_hook_cred_label_update_execve`) have been implemented — see details below.
|
||||
|
||||
On 2026-03-06, three patches were retargeted after IDA-MCP re-analysis revealed their matchers were hitting wrong sites:
|
||||
|
||||
- `patch_bsd_init_auth` — was hitting `exec_handle_sugid` instead of the real `bsd_init` rootauth gate
|
||||
- `patch_io_secure_bsd_root` — was patching the `"SecureRoot"` dispatch branch instead of the `"SecureRootName"` deny-return
|
||||
- `patch_vm_fault_enter_prepare` — was NOPing a `pmap_lock_phys_page()` call instead of the upstream `cs_bypass` gate
|
||||
|
||||
@@ -60,34 +60,34 @@ Result: **byte-identical** output between hardcoded and dynamic patching.
|
||||
|
||||
Offsets and 32-bit patch values, taken from `patch_fw.py`:
|
||||
|
||||
| # | Offset (hex) | Patch value | Purpose |
|
||||
| --- | -----------: | ----------: | ------------------------------------------ |
|
||||
| 1 | 0x2476964 | 0xD503201F | \_apfs_vfsop_mount root snapshot NOP |
|
||||
| 2 | 0x23CFDE4 | 0xD503201F | \_authapfs_seal_is_broken NOP |
|
||||
| 3 | 0x00F6D960 | 0xD503201F | \_bsd_init rootvp NOP |
|
||||
| 4 | 0x163863C | 0x52800000 | \_proc_check_launch_constraints mov w0,#0 |
|
||||
| 5 | 0x1638640 | 0xD65F03C0 | \_proc_check_launch_constraints ret |
|
||||
| 6 | 0x12C8138 | 0xD2800020 | \_PE_i_can_has_debugger mov x0,#1 |
|
||||
| 7 | 0x12C813C | 0xD65F03C0 | \_PE_i_can_has_debugger ret |
|
||||
| 8 | 0x00FFAB98 | 0xD503201F | TXM post-validation NOP (tbnz) |
|
||||
| 9 | 0x16405AC | 0x6B00001F | postValidation cmp w0,w0 |
|
||||
| 10 | 0x16410BC | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (1) |
|
||||
| 11 | 0x16410C8 | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (2) |
|
||||
| 12 | 0x242011C | 0x52800000 | \_apfs_graft mov w0,#0 |
|
||||
| 13 | 0x2475044 | 0xEB00001F | \_apfs_vfsop_mount cmp x0,x0 |
|
||||
| 14 | 0x2476C00 | 0x52800000 | \_apfs_mount_upgrade_checks mov w0,#0 |
|
||||
| 15 | 0x248C800 | 0x52800000 | \_handle_fsioc_graft mov w0,#0 |
|
||||
| # | Offset (hex) | Patch value | Purpose |
|
||||
| --- | -----------: | ----------: | ------------------------------------------- |
|
||||
| 1 | 0x2476964 | 0xD503201F | \_apfs_vfsop_mount root snapshot NOP |
|
||||
| 2 | 0x23CFDE4 | 0xD503201F | \_authapfs_seal_is_broken NOP |
|
||||
| 3 | 0x00F6D960 | 0xD503201F | \_bsd_init rootvp NOP |
|
||||
| 4 | 0x163863C | 0x52800000 | \_proc_check_launch_constraints mov w0,#0 |
|
||||
| 5 | 0x1638640 | 0xD65F03C0 | \_proc_check_launch_constraints ret |
|
||||
| 6 | 0x12C8138 | 0xD2800020 | \_PE_i_can_has_debugger mov x0,#1 |
|
||||
| 7 | 0x12C813C | 0xD65F03C0 | \_PE_i_can_has_debugger ret |
|
||||
| 8 | 0x00FFAB98 | 0xD503201F | TXM post-validation NOP (tbnz) |
|
||||
| 9 | 0x16405AC | 0x6B00001F | postValidation cmp w0,w0 |
|
||||
| 10 | 0x16410BC | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (1) |
|
||||
| 11 | 0x16410C8 | 0x52800020 | \_check_dyld_policy_internal mov w0,#1 (2) |
|
||||
| 12 | 0x242011C | 0x52800000 | \_apfs_graft mov w0,#0 |
|
||||
| 13 | 0x2475044 | 0xEB00001F | \_apfs_vfsop_mount cmp x0,x0 |
|
||||
| 14 | 0x2476C00 | 0x52800000 | \_apfs_mount_upgrade_checks mov w0,#0 |
|
||||
| 15 | 0x248C800 | 0x52800000 | \_handle_fsioc_graft mov w0,#0 |
|
||||
| 16 | | | \_handle_get_dev_by_role entitlement bypass |
|
||||
| 17 | 0x23AC528 | 0xD2800000 | \_hook_file_check_mmap mov x0,#0 |
|
||||
| 18 | 0x23AC52C | 0xD65F03C0 | \_hook_file_check_mmap ret |
|
||||
| 19 | 0x23AAB58 | 0xD2800000 | \_hook_mount_check_mount mov x0,#0 |
|
||||
| 20 | 0x23AAB5C | 0xD65F03C0 | \_hook_mount_check_mount ret |
|
||||
| 21 | 0x23AA9A0 | 0xD2800000 | \_hook_mount_check_remount mov x0,#0 |
|
||||
| 22 | 0x23AA9A4 | 0xD65F03C0 | \_hook_mount_check_remount ret |
|
||||
| 23 | 0x23AA80C | 0xD2800000 | \_hook_mount_check_umount mov x0,#0 |
|
||||
| 24 | 0x23AA810 | 0xD65F03C0 | \_hook_mount_check_umount ret |
|
||||
| 25 | 0x23A5514 | 0xD2800000 | \_hook_vnode_check_rename mov x0,#0 |
|
||||
| 26 | 0x23A5518 | 0xD65F03C0 | \_hook_vnode_check_rename ret |
|
||||
| 17 | 0x23AC528 | 0xD2800000 | \_hook_file_check_mmap mov x0,#0 |
|
||||
| 18 | 0x23AC52C | 0xD65F03C0 | \_hook_file_check_mmap ret |
|
||||
| 19 | 0x23AAB58 | 0xD2800000 | \_hook_mount_check_mount mov x0,#0 |
|
||||
| 20 | 0x23AAB5C | 0xD65F03C0 | \_hook_mount_check_mount ret |
|
||||
| 21 | 0x23AA9A0 | 0xD2800000 | \_hook_mount_check_remount mov x0,#0 |
|
||||
| 22 | 0x23AA9A4 | 0xD65F03C0 | \_hook_mount_check_remount ret |
|
||||
| 23 | 0x23AA80C | 0xD2800000 | \_hook_mount_check_umount mov x0,#0 |
|
||||
| 24 | 0x23AA810 | 0xD65F03C0 | \_hook_mount_check_umount ret |
|
||||
| 25 | 0x23A5514 | 0xD2800000 | \_hook_vnode_check_rename mov x0,#0 |
|
||||
| 26 | 0x23A5518 | 0xD65F03C0 | \_hook_vnode_check_rename ret |
|
||||
|
||||
## TXM Patch Details
|
||||
|
||||
|
||||
191
scripts/cfw_install_jb_post.sh
Executable file
191
scripts/cfw_install_jb_post.sh
Executable file
@@ -0,0 +1,191 @@
|
||||
#!/bin/zsh
|
||||
# cfw_install_jb_post.sh — Finalize JB bootstrap on a normally-booted vphone.
|
||||
#
|
||||
# Runs after `cfw_install_jb` + first normal boot. Connects to the live device
|
||||
# via SSH and sets up procursus symlinks, markers, Sileo, and apt packages.
|
||||
#
|
||||
# Every step is idempotent — safe to re-run at any point.
|
||||
# All binary paths are discovered dynamically (no hardcoded /bin, /sbin, etc.).
|
||||
#
|
||||
# Usage: make cfw_install_jb_finalize [SSH_PORT=22222] [SSH_PASS=alpine]
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="${0:a:h}"
|
||||
|
||||
# ── Configuration ───────────────────────────────────────────────
|
||||
SSH_PORT="${SSH_PORT:-22222}"
|
||||
SSH_PASS="${SSH_PASS:-alpine}"
|
||||
SSH_USER="root"
|
||||
SSH_HOST="localhost"
|
||||
SSH_RETRY="${SSH_RETRY:-3}"
|
||||
SSHPASS_BIN=""
|
||||
SSH_OPTS=(
|
||||
-o StrictHostKeyChecking=no
|
||||
-o UserKnownHostsFile=/dev/null
|
||||
-o PreferredAuthentications=password
|
||||
-o ConnectTimeout=30
|
||||
-q
|
||||
)
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────────
|
||||
die() {
|
||||
echo "[-] $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
_sshpass() {
|
||||
"$SSHPASS_BIN" -p "$SSH_PASS" "$@"
|
||||
}
|
||||
|
||||
_ssh_retry() {
|
||||
local attempt rc label
|
||||
label=${2:-cmd}
|
||||
for ((attempt = 1; attempt <= SSH_RETRY; attempt++)); do
|
||||
"$@" && return 0
|
||||
rc=$?
|
||||
[[ $rc -ne 255 ]] && return $rc
|
||||
echo " [${label}] connection lost (attempt $attempt/$SSH_RETRY), retrying in 3s..." >&2
|
||||
sleep 3
|
||||
done
|
||||
return 255
|
||||
}
|
||||
|
||||
# Raw ssh — no PATH prefix
|
||||
ssh_raw() {
|
||||
_ssh_retry _sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@"
|
||||
}
|
||||
|
||||
# ssh with discovered PATH prepended
|
||||
ssh_cmd() {
|
||||
ssh_raw "$RENV $*"
|
||||
}
|
||||
|
||||
# ── Prerequisites ──────────────────────────────────────────────
|
||||
command -v sshpass &>/dev/null || die "Missing sshpass. Run: make setup_tools"
|
||||
SSHPASS_BIN="$(command -v sshpass)"
|
||||
|
||||
echo "[*] cfw_install_jb_post.sh — Finalizing JB bootstrap..."
|
||||
echo " Target: ${SSH_USER}@${SSH_HOST}:${SSH_PORT}"
|
||||
echo ""
|
||||
|
||||
# ── Verify SSH connectivity ────────────────────────────────────
|
||||
echo "[*] Checking SSH connectivity..."
|
||||
ssh_raw "echo ready" >/dev/null 2>&1 || die "Cannot reach device on ${SSH_HOST}:${SSH_PORT}. Is the VM booted normally?"
|
||||
echo "[+] Device reachable"
|
||||
|
||||
# ── Discover remote PATH ──────────────────────────────────────
|
||||
# Uses only shell builtins (test -d, echo) — works with empty PATH.
|
||||
echo "[*] Discovering remote binary directories..."
|
||||
DISCOVERED_PATH=$(ssh_raw 'P=""; \
|
||||
for d in \
|
||||
/var/jb/usr/bin /var/jb/bin /var/jb/sbin /var/jb/usr/sbin \
|
||||
/iosbinpack64/bin /iosbinpack64/usr/bin /iosbinpack64/sbin /iosbinpack64/usr/sbin \
|
||||
/usr/bin /usr/sbin /bin /sbin; do \
|
||||
[ -d "$d" ] && P="$P:$d"; \
|
||||
done; \
|
||||
echo "${P#:}"')
|
||||
|
||||
[[ -n "$DISCOVERED_PATH" ]] || die "Could not discover any binary directories on device"
|
||||
echo " PATH=$DISCOVERED_PATH"
|
||||
|
||||
# This gets prepended to every ssh_cmd call
|
||||
RENV="export PATH='$DISCOVERED_PATH' TERM='xterm-256color';"
|
||||
|
||||
# Quick sanity: verify we can run ls now
|
||||
ssh_cmd "ls / >/dev/null" || die "PATH discovery succeeded but 'ls' still not found"
|
||||
echo "[+] Remote environment ready"
|
||||
|
||||
# ═══════════ 1/6 SYMLINK /var/jb ══════════════════════════════
|
||||
echo ""
|
||||
echo "[1/6] Creating /private/var/jb symlink..."
|
||||
|
||||
# Find 96-char boot manifest hash — use shell glob (no ls dependency)
|
||||
BOOT_HASH=$(ssh_cmd 'for d in /private/preboot/*/; do \
|
||||
b="${d%/}"; b="${b##*/}"; \
|
||||
[ "${#b}" = 96 ] && echo "$b" && break; \
|
||||
done')
|
||||
[[ -n "$BOOT_HASH" ]] || die "Could not find 96-char boot manifest hash in /private/preboot"
|
||||
echo " Boot manifest hash: $BOOT_HASH"
|
||||
|
||||
JB_TARGET="/private/preboot/$BOOT_HASH/jb-vphone/procursus"
|
||||
ssh_cmd "test -d '$JB_TARGET'" || die "Procursus directory not found at $JB_TARGET. Run cfw_install_jb first."
|
||||
|
||||
CURRENT_LINK=$(ssh_cmd "readlink /private/var/jb 2>/dev/null || true")
|
||||
if [[ "$CURRENT_LINK" == "$JB_TARGET" ]]; then
|
||||
echo " [*] Symlink already correct, skipping"
|
||||
else
|
||||
ssh_cmd "ln -sf '$JB_TARGET' /private/var/jb"
|
||||
echo " [+] /private/var/jb -> $JB_TARGET"
|
||||
fi
|
||||
|
||||
# ═══════════ 2/6 FIX OWNERSHIP / PERMISSIONS ═════════════════
|
||||
echo ""
|
||||
echo "[2/6] Fixing mobile Library ownership..."
|
||||
|
||||
ssh_cmd "mkdir -p /var/jb/var/mobile/Library/Preferences"
|
||||
ssh_cmd "chown -R 501:501 /var/jb/var/mobile/Library"
|
||||
ssh_cmd "chmod 0755 /var/jb/var/mobile/Library"
|
||||
ssh_cmd "chown -R 501:501 /var/jb/var/mobile/Library/Preferences"
|
||||
ssh_cmd "chmod 0755 /var/jb/var/mobile/Library/Preferences"
|
||||
|
||||
echo " [+] Ownership set"
|
||||
|
||||
# ═══════════ 3/6 RUN prep_bootstrap.sh ════════════════════════
|
||||
echo ""
|
||||
echo "[3/6] Running prep_bootstrap.sh..."
|
||||
|
||||
if ssh_cmd "test -f /var/jb/prep_bootstrap.sh"; then
|
||||
# Skip interactive password prompt (uses uialert GUI — not usable over SSH)
|
||||
ssh_cmd "NO_PASSWORD_PROMPT=1 /var/jb/prep_bootstrap.sh"
|
||||
echo " [+] prep_bootstrap.sh completed"
|
||||
echo " [!] Terminal password was NOT set (automated mode)."
|
||||
echo " To set it manually: ssh in and run: passwd"
|
||||
else
|
||||
echo " [*] prep_bootstrap.sh already ran (deleted itself), skipping"
|
||||
fi
|
||||
|
||||
# ═══════════ 4/6 CREATE MARKER FILES ═════════════════════════
|
||||
echo ""
|
||||
echo "[4/6] Creating marker files..."
|
||||
|
||||
for marker in .procursus_strapped .installed_dopamine; do
|
||||
if ssh_cmd "test -f /var/jb/$marker"; then
|
||||
echo " [*] $marker already exists, skipping"
|
||||
else
|
||||
ssh_cmd "touch /var/jb/$marker && chown 0:0 /var/jb/$marker && chmod 0644 /var/jb/$marker"
|
||||
echo " [+] $marker created"
|
||||
fi
|
||||
done
|
||||
|
||||
# ═══════════ 5/6 INSTALL SILEO ══════════════════════════════
|
||||
echo ""
|
||||
echo "[5/6] Installing Sileo..."
|
||||
|
||||
SILEO_DEB_PATH="/private/preboot/$BOOT_HASH/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb"
|
||||
|
||||
if ssh_cmd "dpkg -s org.coolstar.sileo >/dev/null 2>&1"; then
|
||||
echo " [*] Sileo already installed, skipping"
|
||||
else
|
||||
ssh_cmd "test -f '$SILEO_DEB_PATH'" || die "Sileo deb not found at $SILEO_DEB_PATH. Was it uploaded by cfw_install_jb?"
|
||||
ssh_cmd "dpkg -i '$SILEO_DEB_PATH'"
|
||||
echo " [+] Sileo installed"
|
||||
fi
|
||||
|
||||
ssh_cmd "uicache -a 2>/dev/null || true"
|
||||
echo " [+] uicache refreshed"
|
||||
|
||||
# ═══════════ 6/6 APT SETUP ═════════════════════════════════
|
||||
echo ""
|
||||
echo "[6/6] Running apt setup..."
|
||||
|
||||
ssh_cmd "apt-get update -qq && apt-get install -y -qq libkrw0-tfp0 2>/dev/null || true"
|
||||
echo " [+] apt update + libkrw0-tfp0 done"
|
||||
|
||||
ssh_cmd "apt-get upgrade -y -qq 2>/dev/null || true"
|
||||
echo " [+] apt upgrade done"
|
||||
|
||||
# ═══════════ DONE ═══════════════════════════════════════════
|
||||
echo ""
|
||||
echo "[+] JB finalization complete!"
|
||||
echo " Next: open Sileo on device, add source https://ellekit.space, install ElleKit"
|
||||
echo " Then reboot the device for full JB environment."
|
||||
@@ -135,10 +135,6 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
|
||||
mc.locationProvider = provider
|
||||
}
|
||||
mc.screenRecorder = VPhoneScreenRecorder()
|
||||
if let signer = VPhoneSigner() {
|
||||
mc.signer = signer
|
||||
mc.ipaInstaller = VPhoneIPAInstaller(signer: signer)
|
||||
}
|
||||
menuController = mc
|
||||
|
||||
// Wire location toggle through onConnect/onDisconnect
|
||||
|
||||
@@ -261,24 +261,12 @@ class VPhoneControl {
|
||||
let enabled: Bool
|
||||
}
|
||||
|
||||
struct DevModeEnableResult {
|
||||
let alreadyEnabled: Bool
|
||||
let message: String
|
||||
}
|
||||
|
||||
func sendDevModeStatus() async throws -> DevModeStatus {
|
||||
let (resp, _) = try await sendRequest(["t": "devmode", "action": "status"])
|
||||
let enabled = resp["enabled"] as? Bool ?? false
|
||||
return DevModeStatus(enabled: enabled)
|
||||
}
|
||||
|
||||
func sendDevModeEnable() async throws -> DevModeEnableResult {
|
||||
let (resp, _) = try await sendRequest(["t": "devmode", "action": "enable"])
|
||||
let alreadyEnabled = resp["already_enabled"] as? Bool ?? false
|
||||
let message = resp["msg"] as? String ?? ""
|
||||
return DevModeEnableResult(alreadyEnabled: alreadyEnabled, message: message)
|
||||
}
|
||||
|
||||
func sendPing() async throws {
|
||||
_ = try await sendRequest(["t": "ping"])
|
||||
}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - IPA Installer
|
||||
|
||||
/// Host-side IPA installer. Uses VPhoneSigner for re-signing,
|
||||
/// ideviceinstaller for USB installation via usbmuxd.
|
||||
@MainActor
|
||||
class VPhoneIPAInstaller {
|
||||
let signer: VPhoneSigner
|
||||
private let ideviceInstallerURL: URL
|
||||
private let ideviceIdURL: URL
|
||||
|
||||
init?(signer: VPhoneSigner, bundle: Bundle = .main) {
|
||||
guard let execURL = bundle.executableURL else { return nil }
|
||||
let macosDir = execURL.deletingLastPathComponent()
|
||||
|
||||
ideviceInstallerURL = macosDir.appendingPathComponent("ideviceinstaller")
|
||||
ideviceIdURL = macosDir.appendingPathComponent("idevice_id")
|
||||
|
||||
let fm = FileManager.default
|
||||
guard fm.fileExists(atPath: ideviceInstallerURL.path),
|
||||
fm.fileExists(atPath: ideviceIdURL.path)
|
||||
else { return nil }
|
||||
|
||||
self.signer = signer
|
||||
}
|
||||
|
||||
// MARK: - Install
|
||||
|
||||
/// Install an IPA. If `resign` is true, re-sign all Mach-O binaries
|
||||
/// preserving their original entitlements before installing.
|
||||
func install(ipaURL: URL, resign: Bool) async throws {
|
||||
let udid = try await getUDID()
|
||||
print("[ipa] device UDID: \(udid)")
|
||||
|
||||
var installURL = ipaURL
|
||||
var tempDir: URL?
|
||||
|
||||
if resign {
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("vphone-cli-resign-\(UUID().uuidString)")
|
||||
tempDir = dir
|
||||
installURL = try await resignIPA(ipaURL: ipaURL, tempDir: dir)
|
||||
}
|
||||
|
||||
defer {
|
||||
if let tempDir {
|
||||
try? FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
print("[ipa] installing \(installURL.lastPathComponent) to \(udid)...")
|
||||
let result = try await signer.run(
|
||||
ideviceInstallerURL,
|
||||
arguments: ["-u", udid, "install", installURL.path]
|
||||
)
|
||||
guard result.status == 0 else {
|
||||
let msg = result.stderr.isEmpty ? result.stdout : result.stderr
|
||||
throw IPAError.installFailed(msg.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
}
|
||||
print("[ipa] installed successfully")
|
||||
}
|
||||
|
||||
// MARK: - UDID Discovery
|
||||
|
||||
private func getUDID() async throws -> String {
|
||||
let result = try await signer.run(ideviceIdURL, arguments: ["-l"])
|
||||
guard result.status == 0 else {
|
||||
throw IPAError.noDevice
|
||||
}
|
||||
let udids = result.stdout
|
||||
.components(separatedBy: .newlines)
|
||||
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
.filter { !$0.isEmpty }
|
||||
guard let first = udids.first else {
|
||||
throw IPAError.noDevice
|
||||
}
|
||||
return first
|
||||
}
|
||||
|
||||
// MARK: - Re-sign IPA
|
||||
|
||||
private func resignIPA(ipaURL: URL, tempDir: URL) async throws -> URL {
|
||||
let fm = FileManager.default
|
||||
try fm.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
||||
|
||||
// Unzip
|
||||
print("[ipa] extracting \(ipaURL.lastPathComponent)...")
|
||||
let unzip = try await signer.run(
|
||||
URL(fileURLWithPath: "/usr/bin/unzip"),
|
||||
arguments: ["-o", ipaURL.path, "-d", tempDir.path]
|
||||
)
|
||||
guard unzip.status == 0 else {
|
||||
throw IPAError.extractFailed(unzip.stderr)
|
||||
}
|
||||
|
||||
// Remove macOS resource fork files that break iOS installd
|
||||
_ = try? await signer.run(
|
||||
URL(fileURLWithPath: "/usr/bin/find"),
|
||||
arguments: [tempDir.path, "-name", "._*", "-delete"]
|
||||
)
|
||||
_ = try? await signer.run(
|
||||
URL(fileURLWithPath: "/usr/bin/find"),
|
||||
arguments: [tempDir.path, "-name", ".DS_Store", "-delete"]
|
||||
)
|
||||
|
||||
// Find Payload/*.app
|
||||
let payloadDir = tempDir.appendingPathComponent("Payload")
|
||||
guard fm.fileExists(atPath: payloadDir.path) else {
|
||||
throw IPAError.invalidIPA("no Payload directory")
|
||||
}
|
||||
let contents = try fm.contentsOfDirectory(atPath: payloadDir.path)
|
||||
guard let appName = contents.first(where: { $0.hasSuffix(".app") }) else {
|
||||
throw IPAError.invalidIPA("no .app bundle in Payload")
|
||||
}
|
||||
let appDir = payloadDir.appendingPathComponent(appName)
|
||||
|
||||
// Walk and re-sign all Mach-O files
|
||||
let machoFiles = signer.findMachOFiles(in: appDir)
|
||||
print("[ipa] re-signing \(machoFiles.count) Mach-O binaries...")
|
||||
|
||||
for file in machoFiles {
|
||||
do {
|
||||
try await signer.signFile(at: file, tempDir: tempDir)
|
||||
} catch {
|
||||
print("[ipa] warning: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Re-zip (use zip from the temp dir so Payload/ is at the root)
|
||||
let outputIPA = tempDir.appendingPathComponent("resigned.ipa")
|
||||
print("[ipa] re-packaging...")
|
||||
let zip = try await signer.run(
|
||||
URL(fileURLWithPath: "/usr/bin/zip"),
|
||||
arguments: ["-r", "-y", outputIPA.path, "Payload"],
|
||||
currentDirectory: tempDir
|
||||
)
|
||||
guard zip.status == 0 else {
|
||||
throw IPAError.repackFailed(zip.stderr)
|
||||
}
|
||||
|
||||
return outputIPA
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
enum IPAError: Error, CustomStringConvertible {
|
||||
case noDevice
|
||||
case extractFailed(String)
|
||||
case invalidIPA(String)
|
||||
case repackFailed(String)
|
||||
case installFailed(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .noDevice: "no device found (is the VM running?)"
|
||||
case let .extractFailed(msg): "failed to extract IPA: \(msg)"
|
||||
case let .invalidIPA(msg): "invalid IPA: \(msg)"
|
||||
case let .repackFailed(msg): "failed to repackage IPA: \(msg)"
|
||||
case let .installFailed(msg): "install failed: \(msg)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ extension VPhoneMenuController {
|
||||
menu.addItem(makeItem("File Browser", action: #selector(openFiles)))
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
menu.addItem(makeItem("Developer Mode Status", action: #selector(devModeStatus)))
|
||||
menu.addItem(makeItem("Enable Developer Mode [WIP]", action: #selector(devModeEnable)))
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
menu.addItem(makeItem("Ping", action: #selector(sendPing)))
|
||||
menu.addItem(makeItem("Guest Version", action: #selector(queryGuestVersion)))
|
||||
@@ -36,23 +35,6 @@ extension VPhoneMenuController {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func devModeEnable() {
|
||||
Task {
|
||||
do {
|
||||
let result = try await control.sendDevModeEnable()
|
||||
showAlert(
|
||||
title: "Developer Mode",
|
||||
message: result.message.isEmpty
|
||||
? (result.alreadyEnabled ? "Developer Mode already enabled." : "Developer Mode enabled.")
|
||||
: result.message,
|
||||
style: .informational
|
||||
)
|
||||
} catch {
|
||||
showAlert(title: "Developer Mode", message: "\(error)", style: .warning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func sendPing() {
|
||||
Task {
|
||||
do {
|
||||
|
||||
@@ -17,8 +17,6 @@ class VPhoneMenuController {
|
||||
var locationReplayStopItem: NSMenuItem?
|
||||
var screenRecorder: VPhoneScreenRecorder?
|
||||
var recordingItem: NSMenuItem?
|
||||
var signer: VPhoneSigner?
|
||||
var ipaInstaller: VPhoneIPAInstaller?
|
||||
|
||||
init(keyHelper: VPhoneKeyHelper, control: VPhoneControl) {
|
||||
self.keyHelper = keyHelper
|
||||
@@ -47,7 +45,6 @@ class VPhoneMenuController {
|
||||
mainMenu.addItem(buildKeysMenu())
|
||||
mainMenu.addItem(buildTypeMenu())
|
||||
mainMenu.addItem(buildConnectMenu())
|
||||
mainMenu.addItem(buildInstallMenu())
|
||||
mainMenu.addItem(buildLocationMenu())
|
||||
mainMenu.addItem(buildRecordMenu())
|
||||
mainMenu.addItem(buildBatteryMenu())
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import AppKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
// MARK: - Install Menu
|
||||
|
||||
extension VPhoneMenuController {
|
||||
func buildInstallMenu() -> NSMenuItem {
|
||||
let item = NSMenuItem()
|
||||
let menu = NSMenu(title: "Install")
|
||||
menu.addItem(makeItem("Install Package (.ipa) [WIP]", action: #selector(installPackage)))
|
||||
menu.addItem(makeItem("Install Package with Resign (.ipa) [WIP]", action: #selector(installPackageResign)))
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
menu.addItem(makeItem("Upload Binary to Guest", action: #selector(uploadBinary)))
|
||||
menu.addItem(makeItem("Upload Binary with Resign to Guest", action: #selector(uploadBinaryResign)))
|
||||
item.submenu = menu
|
||||
return item
|
||||
}
|
||||
|
||||
// MARK: - IPA Install
|
||||
|
||||
@objc func installPackage() {
|
||||
pickAndInstall(resign: false)
|
||||
}
|
||||
|
||||
@objc func installPackageResign() {
|
||||
pickAndInstall(resign: true)
|
||||
}
|
||||
|
||||
private func pickAndInstall(resign: Bool) {
|
||||
let panel = NSOpenPanel()
|
||||
panel.title = "Select IPA"
|
||||
panel.allowedContentTypes = [.init(filenameExtension: "ipa")!]
|
||||
panel.allowsMultipleSelection = false
|
||||
panel.canChooseDirectories = false
|
||||
|
||||
guard panel.runModal() == .OK, let url = panel.url else { return }
|
||||
|
||||
guard let installer = ipaInstaller else {
|
||||
showAlert(
|
||||
title: "Install Package",
|
||||
message: "IPA installer not available (bundled tools missing).",
|
||||
style: .warning
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await installer.install(ipaURL: url, resign: resign)
|
||||
showAlert(
|
||||
title: "Install Package",
|
||||
message: "Successfully installed \(url.lastPathComponent).",
|
||||
style: .informational
|
||||
)
|
||||
} catch {
|
||||
showAlert(
|
||||
title: "Install Package",
|
||||
message: "\(error)",
|
||||
style: .warning
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Upload Binary
|
||||
|
||||
@objc func uploadBinary() {
|
||||
pickAndUploadBinary(resign: false)
|
||||
}
|
||||
|
||||
@objc func uploadBinaryResign() {
|
||||
pickAndUploadBinary(resign: true)
|
||||
}
|
||||
|
||||
private func pickAndUploadBinary(resign: Bool) {
|
||||
let panel = NSOpenPanel()
|
||||
panel.title = "Select Binary to Upload"
|
||||
panel.allowsMultipleSelection = false
|
||||
panel.canChooseDirectories = false
|
||||
|
||||
guard panel.runModal() == .OK, let url = panel.url else { return }
|
||||
|
||||
Task {
|
||||
do {
|
||||
var data = try Data(contentsOf: url)
|
||||
|
||||
if resign {
|
||||
guard let signer else {
|
||||
showAlert(
|
||||
title: "Upload Binary",
|
||||
message: "Signing tools not available (bundled tools missing).",
|
||||
style: .warning
|
||||
)
|
||||
return
|
||||
}
|
||||
data = try await signer.resign(data: data, filename: url.lastPathComponent)
|
||||
}
|
||||
|
||||
let filename = url.lastPathComponent
|
||||
let remotePath = "/var/root/Library/Caches/\(filename)"
|
||||
try await control.uploadFile(path: remotePath, data: data, permissions: "755")
|
||||
showAlert(
|
||||
title: "Upload Binary",
|
||||
message: "Uploaded \(filename) to \(remotePath) (\(data.count) bytes)\(resign ? " [resigned]" : "").",
|
||||
style: .informational
|
||||
)
|
||||
} catch {
|
||||
showAlert(
|
||||
title: "Upload Binary",
|
||||
message: "\(error)",
|
||||
style: .warning
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - Code Signer
|
||||
|
||||
/// Host-side code signing using bundled ldid + signcert.p12.
|
||||
/// Preserves existing entitlements when re-signing.
|
||||
@MainActor
|
||||
class VPhoneSigner {
|
||||
private let ldidURL: URL
|
||||
private let signcertURL: URL
|
||||
|
||||
init?(bundle: Bundle = .main) {
|
||||
guard let execURL = bundle.executableURL else { return nil }
|
||||
let macosDir = execURL.deletingLastPathComponent()
|
||||
let resourcesDir = macosDir
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Resources")
|
||||
|
||||
ldidURL = macosDir.appendingPathComponent("ldid")
|
||||
signcertURL = resourcesDir.appendingPathComponent("signcert.p12")
|
||||
|
||||
let fm = FileManager.default
|
||||
guard fm.fileExists(atPath: ldidURL.path),
|
||||
fm.fileExists(atPath: signcertURL.path)
|
||||
else { return nil }
|
||||
}
|
||||
|
||||
// MARK: - Sign Binary
|
||||
|
||||
/// Re-sign a single Mach-O binary in-memory. Preserves existing entitlements.
|
||||
func resign(data: Data, filename: String) async throws -> Data {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("vphone-cli-sign-\(UUID().uuidString)")
|
||||
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: tempDir) }
|
||||
|
||||
let binaryURL = tempDir.appendingPathComponent(filename)
|
||||
try data.write(to: binaryURL)
|
||||
|
||||
try await signFile(at: binaryURL, tempDir: tempDir)
|
||||
|
||||
return try Data(contentsOf: binaryURL)
|
||||
}
|
||||
|
||||
/// Re-sign a Mach-O binary on disk in-place. Preserves existing entitlements.
|
||||
func signFile(at url: URL, tempDir: URL) async throws {
|
||||
let entsResult = try await run(ldidURL, arguments: ["-e", url.path])
|
||||
let entsXML = entsResult.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let cert = signcertURL.path
|
||||
|
||||
var args: [String]
|
||||
if !entsXML.isEmpty, entsXML.hasPrefix("<?xml") || entsXML.hasPrefix("<!DOCTYPE") {
|
||||
let entsFile = tempDir.appendingPathComponent("ents-\(UUID().uuidString).plist")
|
||||
try entsXML.write(to: entsFile, atomically: true, encoding: .utf8)
|
||||
args = ["-S\(entsFile.path)", "-M", "-K\(cert)", url.path]
|
||||
} else {
|
||||
args = ["-S", "-M", "-K\(cert)", url.path]
|
||||
}
|
||||
|
||||
let result = try await run(ldidURL, arguments: args)
|
||||
guard result.status == 0 else {
|
||||
throw SignError.ldidFailed(url.lastPathComponent, result.stderr)
|
||||
}
|
||||
print("[sign] signed \(url.lastPathComponent)")
|
||||
}
|
||||
|
||||
// MARK: - Mach-O Detection
|
||||
|
||||
/// Recursively find all Mach-O files in a directory.
|
||||
func findMachOFiles(in directory: URL) -> [URL] {
|
||||
let fm = FileManager.default
|
||||
guard let enumerator = fm.enumerator(
|
||||
at: directory,
|
||||
includingPropertiesForKeys: [.isRegularFileKey],
|
||||
options: [.skipsHiddenFiles]
|
||||
) else { return [] }
|
||||
|
||||
var results: [URL] = []
|
||||
for case let url as URL in enumerator {
|
||||
guard let values = try? url.resourceValues(forKeys: [.isRegularFileKey]),
|
||||
values.isRegularFile == true,
|
||||
Self.isMachO(at: url)
|
||||
else { continue }
|
||||
results.append(url)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
static func isMachO(at url: URL) -> Bool {
|
||||
guard let fh = try? FileHandle(forReadingFrom: url) else { return false }
|
||||
defer { try? fh.close() }
|
||||
guard let data = try? fh.read(upToCount: 4), data.count == 4 else { return false }
|
||||
let magic = data.withUnsafeBytes { $0.load(as: UInt32.self) }
|
||||
return magic == 0xFEED_FACF // MH_MAGIC_64
|
||||
|| magic == 0xCFFA_EDFE // MH_CIGAM_64
|
||||
|| magic == 0xFEED_FACE // MH_MAGIC
|
||||
|| magic == 0xCEFA_EDFE // MH_CIGAM
|
||||
|| magic == 0xCAFE_BABE // FAT_MAGIC
|
||||
|| magic == 0xBEBA_FECA // FAT_CIGAM
|
||||
}
|
||||
|
||||
// MARK: - Process Runner
|
||||
|
||||
struct ProcessResult: Sendable {
|
||||
let stdout: String
|
||||
let stderr: String
|
||||
let status: Int32
|
||||
}
|
||||
|
||||
func run(
|
||||
_ executable: URL,
|
||||
arguments: [String],
|
||||
currentDirectory: URL? = nil
|
||||
) async throws -> ProcessResult {
|
||||
let execPath = executable.path
|
||||
let args = arguments
|
||||
let dirPath = currentDirectory?.path
|
||||
|
||||
return try await Task.detached {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: execPath)
|
||||
process.arguments = args
|
||||
if let dirPath {
|
||||
process.currentDirectoryURL = URL(fileURLWithPath: dirPath)
|
||||
}
|
||||
|
||||
let stdoutPipe = Pipe()
|
||||
let stderrPipe = Pipe()
|
||||
process.standardOutput = stdoutPipe
|
||||
process.standardError = stderrPipe
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
let outData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let errData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
|
||||
return ProcessResult(
|
||||
stdout: String(data: outData, encoding: .utf8) ?? "",
|
||||
stderr: String(data: errData, encoding: .utf8) ?? "",
|
||||
status: process.terminationStatus
|
||||
)
|
||||
}.value
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
enum SignError: Error, CustomStringConvertible {
|
||||
case ldidFailed(String, String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .ldidFailed(file, msg): "failed to sign \(file): \(msg)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user