mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
feat: standalone .app bundling + versioned resource resolution
- scripts/build.sh builds+signs the binary, bundles a self-contained .app (mirrors scripts/patchers/resources/tools/vphoned + requirements.txt into Contents/Resources) and re-signs; the signed guest daemon stages under .build (not the repo root) - CryptexFilesystemPatcher resolves assets via VPhoneResources instead of CWD-relative paths - Makefile space-safety; .gitignore updates Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
This commit is contained in:
committed by
zqxwce
co-authored by
Claude Opus 4.8
parent
cce7898494
commit
c5300d0377
@@ -218,6 +218,7 @@ ipython_config.py
|
|||||||
|
|
||||||
# Local scratch planning
|
# Local scratch planning
|
||||||
TODO.md
|
TODO.md
|
||||||
|
.superpowers/
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ VM_DIR ?= vm
|
|||||||
# (e.g. external SSD) VM_DIR values. `abspath` leaves absolute paths intact
|
# (e.g. external SSD) VM_DIR values. `abspath` leaves absolute paths intact
|
||||||
# and joins relative ones against CURDIR — use this for the VM directory arg.
|
# and joins relative ones against CURDIR — use this for the VM directory arg.
|
||||||
VM_DIR_ABS := $(abspath $(VM_DIR))
|
VM_DIR_ABS := $(abspath $(VM_DIR))
|
||||||
CPU ?= 8 # CPU cores (only used during vm_new)
|
# CPU cores, memory (MB), disk size (GB) — used only during vm_new.
|
||||||
MEMORY ?= 8192 # Memory in MB (only used during vm_new)
|
# NB: no inline comments on these `?=` lines — make would fold the trailing
|
||||||
DISK_SIZE ?= 64 # Disk size in GB (only used during vm_new)
|
# whitespace into the value (e.g. CPU="8 ") and break numeric consumers.
|
||||||
|
CPU ?= 8
|
||||||
|
MEMORY ?= 8192
|
||||||
|
DISK_SIZE ?= 64
|
||||||
BACKUPS_DIR ?= vm.backups
|
BACKUPS_DIR ?= vm.backups
|
||||||
NAME ?=
|
NAME ?=
|
||||||
BACKUP_INCLUDE_IPSW ?= 0
|
BACKUP_INCLUDE_IPSW ?= 0
|
||||||
@@ -252,7 +255,7 @@ vphoned:
|
|||||||
|
|
||||||
vm_new:
|
vm_new:
|
||||||
CPU="$(CPU)" MEMORY="$(MEMORY)" \
|
CPU="$(CPU)" MEMORY="$(MEMORY)" \
|
||||||
zsh $(SCRIPTS)/vm_create.sh --dir $(VM_DIR) --disk-size $(DISK_SIZE)
|
zsh $(SCRIPTS)/vm_create.sh --dir "$(VM_DIR)" --disk-size $(DISK_SIZE)
|
||||||
|
|
||||||
vm_backup:
|
vm_backup:
|
||||||
VM_DIR="$(VM_DIR)" BACKUPS_DIR="$(BACKUPS_DIR)" NAME="$(NAME)" BACKUP_INCLUDE_IPSW="$(BACKUP_INCLUDE_IPSW)" \
|
VM_DIR="$(VM_DIR)" BACKUPS_DIR="$(BACKUPS_DIR)" NAME="$(NAME)" BACKUP_INCLUDE_IPSW="$(BACKUP_INCLUDE_IPSW)" \
|
||||||
@@ -321,17 +324,17 @@ boot_binary_check: $(BINARY)
|
|||||||
$(call BOOT_BINARY_CHECK,--assert-bootable)
|
$(call BOOT_BINARY_CHECK,--assert-bootable)
|
||||||
|
|
||||||
boot: bundle vphoned boot_binary_check
|
boot: bundle vphoned boot_binary_check
|
||||||
cd $(VM_DIR) && "$(CURDIR)/$(BUNDLE_BIN)" \
|
cd "$(VM_DIR)" && "$(CURDIR)/$(BUNDLE_BIN)" \
|
||||||
--config ./config.plist
|
--config ./config.plist
|
||||||
|
|
||||||
boot_less: bundle boot_binary_check_less
|
boot_less: bundle boot_binary_check_less
|
||||||
cd $(VM_DIR) && "$(CURDIR)/$(BUNDLE_BIN)" \
|
cd "$(VM_DIR)" && "$(CURDIR)/$(BUNDLE_BIN)" \
|
||||||
--config ./config.plist \
|
--config ./config.plist \
|
||||||
--variant less \
|
--variant less \
|
||||||
$(if $(filter 1 true yes YES TRUE,$(NO_VPHONED)),--no-vphoned,)
|
$(if $(filter 1 true yes YES TRUE,$(NO_VPHONED)),--no-vphoned,)
|
||||||
|
|
||||||
boot_dfu: build boot_binary_check
|
boot_dfu: build boot_binary_check
|
||||||
cd $(VM_DIR) && "$(CURDIR)/$(BINARY)" \
|
cd "$(VM_DIR)" && "$(CURDIR)/$(BINARY)" \
|
||||||
--config ./config.plist \
|
--config ./config.plist \
|
||||||
--dfu
|
--dfu
|
||||||
|
|
||||||
@@ -342,7 +345,7 @@ boot_dfu: build boot_binary_check
|
|||||||
.PHONY: fw_prepare fw_patch fw_patch_less fw_patch_dev fw_patch_jb
|
.PHONY: fw_prepare fw_patch fw_patch_less fw_patch_dev fw_patch_jb
|
||||||
|
|
||||||
fw_prepare:
|
fw_prepare:
|
||||||
cd $(VM_DIR) && bash "$(CURDIR)/$(SCRIPTS)/fw_prepare.sh"
|
cd "$(VM_DIR)" && bash "$(CURDIR)/$(SCRIPTS)/fw_prepare.sh"
|
||||||
|
|
||||||
fw_patch: patcher_build
|
fw_patch: patcher_build
|
||||||
"$(CURDIR)/$(PATCHER_BINARY)" patch-firmware --vm-directory "$(VM_DIR_ABS)" --variant regular \
|
"$(CURDIR)/$(PATCHER_BINARY)" patch-firmware --vm-directory "$(VM_DIR_ABS)" --variant regular \
|
||||||
|
|||||||
Executable
+113
@@ -0,0 +1,113 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
# build.sh — Build, sign, and bundle vphone-cli (+ cross-compile vphoned).
|
||||||
|
#
|
||||||
|
# This is the bootstrap step that a running binary cannot do for itself:
|
||||||
|
# it compiles the vphone-cli binary, signs it with the PV=3 entitlements,
|
||||||
|
# wraps it in the .app bundle used for GUI boot, and cross-compiles + signs
|
||||||
|
# the vphoned guest daemon. Everything else in the project is driven by the
|
||||||
|
# resulting `vphone-cli` binary — this script is the only build entrypoint.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/build.sh # build + sign + bundle + vphoned
|
||||||
|
# ./scripts/build.sh --no-vphoned # skip the vphoned cross-compile
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="${0:A:h}"
|
||||||
|
PROJECT_ROOT="${SCRIPT_DIR:h}"
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
BINARY=".build/release/vphone-cli"
|
||||||
|
BUNDLE=".build/vphone-cli.app"
|
||||||
|
BUNDLE_BIN="${BUNDLE}/Contents/MacOS/vphone-cli"
|
||||||
|
INFO_PLIST="sources/Info.plist"
|
||||||
|
ENTITLEMENTS="sources/vphone.entitlements"
|
||||||
|
BUILD_INFO="sources/vphone-cli/VPhoneBuildInfo.swift"
|
||||||
|
GIT_HASH="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||||
|
|
||||||
|
BUILD_VPHONED=1
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--no-vphoned) BUILD_VPHONED=0 ;;
|
||||||
|
-h|--help) echo "Usage: $0 [--no-vphoned]"; exit 0 ;;
|
||||||
|
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Build + sign the binary ---
|
||||||
|
echo "=== Building vphone-cli (${GIT_HASH}) ==="
|
||||||
|
echo '// Auto-generated — do not edit' > "$BUILD_INFO"
|
||||||
|
echo "enum VPhoneBuildInfo { static let commitHash = \"${GIT_HASH}\" }" >> "$BUILD_INFO"
|
||||||
|
swift build -c release
|
||||||
|
|
||||||
|
echo "=== Signing with entitlements ==="
|
||||||
|
codesign --force --sign - --entitlements "$ENTITLEMENTS" "$BINARY"
|
||||||
|
echo " signed OK → ${BINARY}"
|
||||||
|
|
||||||
|
# --- Bundle (.app used for GUI boot) ---
|
||||||
|
echo "=== Bundling ${BUNDLE} ==="
|
||||||
|
mkdir -p "${BUNDLE}/Contents/MacOS" "${BUNDLE}/Contents/Resources"
|
||||||
|
cp -f "$BINARY" "$BUNDLE_BIN"
|
||||||
|
cp -f "$INFO_PLIST" "${BUNDLE}/Contents/Info.plist"
|
||||||
|
cp -f "sources/AppIcon.icns" "${BUNDLE}/Contents/Resources/AppIcon.icns"
|
||||||
|
cp -f "scripts/vphoned/signcert.p12" "${BUNDLE}/Contents/Resources/signcert.p12"
|
||||||
|
cp -f "$(command -v ldid)" "${BUNDLE}/Contents/MacOS/ldid"
|
||||||
|
codesign --force --sign - "${BUNDLE}/Contents/MacOS/ldid"
|
||||||
|
codesign --force --sign - --entitlements "$ENTITLEMENTS" "$BUNDLE_BIN"
|
||||||
|
echo " bundled → ${BUNDLE}"
|
||||||
|
|
||||||
|
# --- vphoned guest daemon (cross-compiled + signed for iOS arm64) ---
|
||||||
|
if [[ "$BUILD_VPHONED" -eq 1 ]]; then
|
||||||
|
command -v ldid >/dev/null 2>&1 \
|
||||||
|
|| { echo "Error: ldid not found. Run: brew install ldid-procursus" >&2; exit 1; }
|
||||||
|
echo "=== Building vphoned ==="
|
||||||
|
make -C scripts/vphoned GIT_HASH="$GIT_HASH"
|
||||||
|
echo "=== Signing vphoned ==="
|
||||||
|
mkdir -p .build
|
||||||
|
cp scripts/vphoned/vphoned .build/vphoned.signed
|
||||||
|
ldid \
|
||||||
|
-Sscripts/vphoned/entitlements.plist \
|
||||||
|
-M "-Kscripts/vphoned/signcert.p12" \
|
||||||
|
.build/vphoned.signed
|
||||||
|
echo " signed → .build/vphoned.signed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Bundle the standalone runtime mini-repo into Contents/Resources ---
|
||||||
|
RES="${BUNDLE}/Contents/Resources"
|
||||||
|
echo "=== Bundling runtime assets → ${RES} ==="
|
||||||
|
rm -rf "${RES}/scripts" "${RES}/tools" "${RES}/.tools" "${RES}/vphoned.signed"
|
||||||
|
mkdir -p "${RES}/scripts" "${RES}/tools" "${RES}/.tools/bin"
|
||||||
|
# Mirror scripts/ EXCEPT the make-coupled orchestrator, toolchain source, caches.
|
||||||
|
rsync -a \
|
||||||
|
--exclude 'setup_machine.sh' \
|
||||||
|
--exclude 'repos' \
|
||||||
|
--exclude '__pycache__' \
|
||||||
|
--exclude '.git' \
|
||||||
|
--exclude '.build' \
|
||||||
|
scripts/ "${RES}/scripts/"
|
||||||
|
cp -f tools/apfs_snap_rename.py "${RES}/tools/apfs_snap_rename.py"
|
||||||
|
# Custom-built tools (bundled; not brew/pip). apfs_sealvolume is NOT bundled
|
||||||
|
# (it is extracted from the target IPSW at `fw prepare` time — Task 5).
|
||||||
|
for t in trustcache insert_dylib; do
|
||||||
|
if [[ -x ".tools/bin/$t" ]]; then cp -f ".tools/bin/$t" "${RES}/.tools/bin/$t"
|
||||||
|
else echo "Error: .tools/bin/$t missing — run ./scripts/setup_tools.sh first" >&2; exit 1; fi
|
||||||
|
done
|
||||||
|
[[ -f .build/vphoned.signed ]] && cp -f .build/vphoned.signed "${RES}/vphoned.signed" || true
|
||||||
|
# requirements.txt lets the app provision its own ~/.vphone/venv on first run
|
||||||
|
# (see VPhoneResources.pythonExecutable) — the app carries no venv itself.
|
||||||
|
cp -f requirements.txt "${RES}/requirements.txt"
|
||||||
|
echo " bundled: scripts/ (patchers+resources), tools/, .tools/bin/{trustcache,insert_dylib}, vphoned.signed, requirements.txt"
|
||||||
|
|
||||||
|
# Re-sign: codesign seals Contents/Resources at sign time, so the earlier
|
||||||
|
# bundle-step signature (made before these assets existed) is now stale —
|
||||||
|
# re-signing here reseals against the final Resources tree.
|
||||||
|
echo "=== Re-signing ${BUNDLE_BIN} (resealing Resources) ==="
|
||||||
|
codesign --force --sign - --entitlements "$ENTITLEMENTS" "$BUNDLE_BIN"
|
||||||
|
echo " resealed OK"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Build complete ==="
|
||||||
|
echo " binary : ${BINARY}"
|
||||||
|
echo " bundle : ${BUNDLE}"
|
||||||
|
[[ "$BUILD_VPHONED" -eq 1 ]] && echo " vphoned: .build/vphoned.signed"
|
||||||
|
echo ""
|
||||||
|
echo "Run: ${BINARY} --help"
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import Img4tool
|
import Img4tool
|
||||||
|
import VPhoneCore
|
||||||
|
|
||||||
enum ProcessError: Error {
|
enum ProcessError: Error {
|
||||||
case failed(Int32, String)
|
case failed(Int32, String)
|
||||||
@@ -46,6 +47,7 @@ public final class CryptexFilesystemPatcher: Patcher {
|
|||||||
public let noBinpack: Bool
|
public let noBinpack: Bool
|
||||||
public let noVphoned: Bool
|
public let noVphoned: Bool
|
||||||
let vphoneCliDirectory = URL(filePath: "./")
|
let vphoneCliDirectory = URL(filePath: "./")
|
||||||
|
let resources = VPhoneResources.resolve()
|
||||||
|
|
||||||
var buildManiest: Data
|
var buildManiest: Data
|
||||||
var rebuiltData: Data?
|
var rebuiltData: Data?
|
||||||
@@ -134,7 +136,7 @@ public final class CryptexFilesystemPatcher: Patcher {
|
|||||||
print("- Fix Dyld Cache")
|
print("- Fix Dyld Cache")
|
||||||
try addDyldSymlinks(targetMount: targetMount)
|
try addDyldSymlinks(targetMount: targetMount)
|
||||||
|
|
||||||
let cfwInputOgPath = vphoneCliDirectory.appending(path: "scripts/resources/cfw_input.tar.zst")
|
let cfwInputOgPath = resources.resourceArchivesDir.appendingPathComponent("cfw_input.tar.zst")
|
||||||
let cfwInputPath = try createTmpDir()
|
let cfwInputPath = try createTmpDir()
|
||||||
_ = try runProcess("/usr/bin/tar", [
|
_ = try runProcess("/usr/bin/tar", [
|
||||||
"--zstd", "-xf", cfwInputOgPath.path, "-C", cfwInputPath.path
|
"--zstd", "-xf", cfwInputOgPath.path, "-C", cfwInputPath.path
|
||||||
@@ -178,8 +180,8 @@ public final class CryptexFilesystemPatcher: Patcher {
|
|||||||
func patchLaunchdCacheLoader(targetMount: String, cfwInput: URL) throws {
|
func patchLaunchdCacheLoader(targetMount: String, cfwInput: URL) throws {
|
||||||
let target = URL.init(filePath: targetMount)
|
let target = URL.init(filePath: targetMount)
|
||||||
let launchdCacheLoaderPath = target.appending(path: "/usr/libexec/launchd_cache_loader")
|
let launchdCacheLoaderPath = target.appending(path: "/usr/libexec/launchd_cache_loader")
|
||||||
let pythonPath = vphoneCliDirectory.appending(path: ".venv/bin/python3")
|
let pythonPath = try resources.pythonExecutable()
|
||||||
let patcherPath = vphoneCliDirectory.appending(path: "scripts/patchers/cfw.py")
|
let patcherPath = resources.cfwPy
|
||||||
_ = try runProcess(pythonPath.path, [
|
_ = try runProcess(pythonPath.path, [
|
||||||
patcherPath.path, "patch-launchd-cache-loader",
|
patcherPath.path, "patch-launchd-cache-loader",
|
||||||
launchdCacheLoaderPath.path
|
launchdCacheLoaderPath.path
|
||||||
@@ -196,7 +198,7 @@ public final class CryptexFilesystemPatcher: Patcher {
|
|||||||
|
|
||||||
func injectLaunchDaemons(targetMount: String, cfwInput: URL, vphoned: Bool = true, cfw: Bool = true) throws {
|
func injectLaunchDaemons(targetMount: String, cfwInput: URL, vphoned: Bool = true, cfw: Bool = true) throws {
|
||||||
let target = URL.init(filePath: targetMount)
|
let target = URL.init(filePath: targetMount)
|
||||||
let scriptDir = vphoneCliDirectory.appending(path: "scripts")
|
let scriptDir = resources.scriptsDir
|
||||||
|
|
||||||
let tmpDir = try createTmpDir()
|
let tmpDir = try createTmpDir()
|
||||||
let launchdPath = tmpDir.appending(path: "launchd.plist")
|
let launchdPath = tmpDir.appending(path: "launchd.plist")
|
||||||
@@ -224,8 +226,9 @@ public final class CryptexFilesystemPatcher: Patcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = try runProcess(vphoneCliDirectory.appending(path: ".venv/bin/python3").path, [
|
let pythonPath = try resources.pythonExecutable()
|
||||||
vphoneCliDirectory.appending(path: "scripts/patchers/cfw.py").path, "inject-daemons",
|
_ = try runProcess(pythonPath.path, [
|
||||||
|
resources.cfwPy.path, "inject-daemons",
|
||||||
launchdPath.path, launchDaemonsPath.path
|
launchdPath.path, launchDaemonsPath.path
|
||||||
])
|
])
|
||||||
try FileManager.default.moveItem(at: launchdPath, to: launchdOgPath)
|
try FileManager.default.moveItem(at: launchdPath, to: launchdOgPath)
|
||||||
@@ -242,9 +245,12 @@ public final class CryptexFilesystemPatcher: Patcher {
|
|||||||
|
|
||||||
func addVphoned(targetMount: String, cfwInput: URL) throws {
|
func addVphoned(targetMount: String, cfwInput: URL) throws {
|
||||||
let target = URL.init(filePath: targetMount)
|
let target = URL.init(filePath: targetMount)
|
||||||
let scriptDir = vphoneCliDirectory.appending(path: "scripts")
|
let scriptDir = resources.scriptsDir
|
||||||
let vphonedSrc = scriptDir.appendingPathComponent("vphoned")
|
let vphonedSrc = scriptDir.appendingPathComponent("vphoned")
|
||||||
let vphonedBin = vphonedSrc.appendingPathComponent("vphoned")
|
// vphonedSrc (bundled source) is read-only inside a packaged .app, so the
|
||||||
|
// compiled binary must land in a writable temp dir, not next to the source.
|
||||||
|
let buildDir = try createTmpDir()
|
||||||
|
let vphonedBin = buildDir.appendingPathComponent("vphoned")
|
||||||
|
|
||||||
try buildVphoned(vphonedSrc: vphonedSrc, vphonedBin: vphonedBin)
|
try buildVphoned(vphonedSrc: vphonedSrc, vphonedBin: vphonedBin)
|
||||||
defer { try? FileManager.default.removeItem(at: vphonedBin) }
|
defer { try? FileManager.default.removeItem(at: vphonedBin) }
|
||||||
@@ -323,8 +329,9 @@ public final class CryptexFilesystemPatcher: Patcher {
|
|||||||
func patchMobileActivation(targetMount: String, cfwInput: URL) throws {
|
func patchMobileActivation(targetMount: String, cfwInput: URL) throws {
|
||||||
let target = URL.init(filePath: targetMount)
|
let target = URL.init(filePath: targetMount)
|
||||||
let mobileActivationdPath = target.appending(path: "/usr/libexec/mobileactivationd")
|
let mobileActivationdPath = target.appending(path: "/usr/libexec/mobileactivationd")
|
||||||
_ = try runProcess("./.venv/bin/python3", [
|
let pythonPath = try resources.pythonExecutable()
|
||||||
"./scripts/patchers/cfw.py", "patch-mobileactivationd",
|
_ = try runProcess(pythonPath.path, [
|
||||||
|
resources.cfwPy.path, "patch-mobileactivationd",
|
||||||
mobileActivationdPath.path
|
mobileActivationdPath.path
|
||||||
])
|
])
|
||||||
_ = try runProcess("/bin/chmod", ["0755", mobileActivationdPath.path])
|
_ = try runProcess("/bin/chmod", ["0755", mobileActivationdPath.path])
|
||||||
@@ -398,7 +405,12 @@ public final class CryptexFilesystemPatcher: Patcher {
|
|||||||
|
|
||||||
private func identifyApfsSealvolume() throws -> URL {
|
private func identifyApfsSealvolume() throws -> URL {
|
||||||
let iosVersion = try getProductVersion()
|
let iosVersion = try getProductVersion()
|
||||||
let path = self.vphoneCliDirectory.appending(path: ".tools/apfs_sealvolume_\(iosVersion)")
|
// VPHONE_SEAL_DIR (set by the CLI's `fw prepare`/`fw patch`) must agree with
|
||||||
|
// wherever fw_prepare.sh's download_apfs_sealvolume() wrote the file; unset
|
||||||
|
// (dev Makefile flow) falls back to the historical repo-relative `.tools/`.
|
||||||
|
let sealDir = ProcessInfo.processInfo.environment["VPHONE_SEAL_DIR"].map { URL(fileURLWithPath: $0) }
|
||||||
|
?? self.vphoneCliDirectory.appending(path: ".tools")
|
||||||
|
let path = sealDir.appendingPathComponent("apfs_sealvolume_\(iosVersion)")
|
||||||
guard FileManager.default.fileExists(atPath: path.path) else {
|
guard FileManager.default.fileExists(atPath: path.path) else {
|
||||||
throw FirmwareManifest.ManifestError.fileNotFound(path.path)
|
throw FirmwareManifest.ManifestError.fileNotFound(path.path)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user