mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
vphone-cli: Add --root-popup to elevate CFW host-mount via macOS auth dialog
* feat: add --root-popup to elevate CFW host-mount via macOS auth dialog Adds --root-popup to `cfw install` and `vm create`, elevating the CFW host-mount through macOS's native authentication dialog (osascript -> do shell script with administrator privileges) instead of the script's sudo re-exec. do shell script runs under a bare env, so the vars the bundled scripts read are forwarded inline, plus SUDO_USER so the script's chown-back still returns artifacts to the invoking user. On `vm create`, --sudo-password takes precedence. Co-Authored-By: Claude Opus 4.8 <[email protected]> * cfw: remove entire .cfw_temp on install cleanup Replaces the selective `rm -f` of individual temp binaries with `rm -rf "$TEMP_DIR"`, dropping the cached Cryptex DMGs along with the temp files. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
744090f694
commit
6aef60bd9a
@@ -579,14 +579,8 @@ echo "[*] Unmounting image volumes..."
|
||||
/sbin/umount $MNT1 2>/dev/null || true
|
||||
/sbin/umount $MNT3 2>/dev/null || true
|
||||
|
||||
# Keep .cfw_temp/Cryptex*.dmg cached (slow to re-create)
|
||||
# Only remove temp binaries
|
||||
echo "[*] Cleaning up temp binaries..."
|
||||
rm -f "$TEMP_DIR/seputil" \
|
||||
"$TEMP_DIR/launchd_cache_loader" \
|
||||
"$TEMP_DIR/mobileactivationd" \
|
||||
"$TEMP_DIR/vphoned" \
|
||||
"$TEMP_DIR/launchd.plist"
|
||||
echo "[*] Cleaning up temp..."
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo ""
|
||||
echo "[+] CFW installation complete!"
|
||||
|
||||
@@ -508,14 +508,8 @@ echo "[*] Unmounting image volumes..."
|
||||
/sbin/umount $MNT1 2>/dev/null || true
|
||||
/sbin/umount $MNT3 2>/dev/null || true
|
||||
|
||||
# Keep .cfw_temp/Cryptex*.dmg cached (slow to re-create)
|
||||
# Only remove temp binaries
|
||||
echo "[*] Cleaning up temp binaries..."
|
||||
rm -f "$TEMP_DIR/seputil" \
|
||||
"$TEMP_DIR/launchd_cache_loader" \
|
||||
"$TEMP_DIR/mobileactivationd" \
|
||||
"$TEMP_DIR/vphoned" \
|
||||
"$TEMP_DIR/launchd.plist"
|
||||
echo "[*] Cleaning up temp..."
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo ""
|
||||
echo "[+] CFW installation complete!"
|
||||
|
||||
@@ -818,9 +818,8 @@ echo "[*] Unmounting image volumes..."
|
||||
/sbin/umount $MNT3 2>/dev/null || true
|
||||
/sbin/umount $MNT5 2>/dev/null || true
|
||||
|
||||
echo "[*] Cleaning up temp binaries..."
|
||||
rm -f "$TEMP_DIR/launchd" \
|
||||
"$TEMP_DIR/bootstrap-iphoneos-arm64.tar"
|
||||
echo "[*] Cleaning up temp..."
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo ""
|
||||
echo "[+] CFW + JB + EXP installation complete!"
|
||||
|
||||
@@ -480,9 +480,8 @@ echo "[*] Unmounting image volumes..."
|
||||
/sbin/umount $MNT3 2>/dev/null || true
|
||||
/sbin/umount $MNT5 2>/dev/null || true
|
||||
|
||||
echo "[*] Cleaning up temp binaries..."
|
||||
rm -f "$TEMP_DIR/launchd" \
|
||||
"$TEMP_DIR/bootstrap-iphoneos-arm64.tar"
|
||||
echo "[*] Cleaning up temp..."
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo ""
|
||||
echo "[+] CFW + JB installation complete!"
|
||||
|
||||
@@ -167,4 +167,23 @@ public enum VPhoneProcessRunner {
|
||||
if savedFg > 0 { _ = tcsetpgrp(ttyFD, savedFg) } // take it back
|
||||
return process.terminationStatus
|
||||
}
|
||||
|
||||
/// Run `executable args` as root via macOS's native auth dialog (`osascript` →
|
||||
/// `do shell script … with administrator privileges`). `do shell script` runs
|
||||
/// under a bare env, so `env` is passed inline as `KEY=value` — nothing else
|
||||
/// reaches the command. Returns the command's exit status.
|
||||
public static func runWithAdminPrivileges(
|
||||
_ executable: URL, _ args: [String], env: [String: String] = [:], echo: Bool = true
|
||||
) throws -> Int32 {
|
||||
func shQuote(_ s: String) -> String { "'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'" }
|
||||
var tokens = env.sorted { $0.key < $1.key }.map { "\($0.key)=\(shQuote($0.value))" }
|
||||
tokens.append(shQuote(executable.path))
|
||||
tokens += args.map(shQuote)
|
||||
// Escape the /bin/sh command for the AppleScript string literal (\ then ").
|
||||
let appleEscaped = tokens.joined(separator: " ")
|
||||
.replacingOccurrences(of: "\\", with: "\\\\")
|
||||
.replacingOccurrences(of: "\"", with: "\\\"")
|
||||
let source = "do shell script \"\(appleEscaped)\" with administrator privileges"
|
||||
return try runStreaming(URL(fileURLWithPath: "/usr/bin/osascript"), ["-e", source], echo: echo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ public struct VPhoneCreateOrchestrator {
|
||||
public var sudoPassword: String?
|
||||
public var spoofBuild: String?
|
||||
public var forceDSCMaxSlide: Bool
|
||||
public var rootPopup: Bool
|
||||
public var interactive: Bool
|
||||
public var cpuCount: UInt
|
||||
public var memoryMB: UInt64
|
||||
@@ -114,6 +115,7 @@ public struct VPhoneCreateOrchestrator {
|
||||
sudoPassword: String? = nil,
|
||||
spoofBuild: String? = nil,
|
||||
forceDSCMaxSlide: Bool = false,
|
||||
rootPopup: Bool = false,
|
||||
interactive: Bool = false,
|
||||
cpuCount: UInt = 8,
|
||||
memoryMB: UInt64 = 8192,
|
||||
@@ -128,6 +130,7 @@ public struct VPhoneCreateOrchestrator {
|
||||
self.sudoPassword = sudoPassword
|
||||
self.spoofBuild = spoofBuild
|
||||
self.forceDSCMaxSlide = forceDSCMaxSlide
|
||||
self.rootPopup = rootPopup
|
||||
self.interactive = interactive
|
||||
self.cpuCount = cpuCount
|
||||
self.memoryMB = memoryMB
|
||||
@@ -187,9 +190,9 @@ public struct VPhoneCreateOrchestrator {
|
||||
} else {
|
||||
print("[!] --sudo-password failed validation; will still try at CFW-install time")
|
||||
}
|
||||
} else if isatty(FileHandle.standardInput.fileDescriptor) == 0 {
|
||||
// No password and no terminal for sudo to prompt on — fail before
|
||||
// the long download/restore, not at the eventual sudo prompt.
|
||||
} else if !options.rootPopup && isatty(FileHandle.standardInput.fileDescriptor) == 0 {
|
||||
// No password, no popup, no terminal for sudo to prompt on — fail
|
||||
// before the long download/restore, not at the eventual sudo prompt.
|
||||
throw VPhoneCreateError.sudoPasswordRequired
|
||||
}
|
||||
}
|
||||
@@ -475,35 +478,46 @@ public struct VPhoneCreateOrchestrator {
|
||||
|
||||
private func runCFWInstall(options: Options, bundleURL: URL, sudoEnvExtras: [String: String]) throws {
|
||||
let v = options.verbosity
|
||||
var env = ProcessInfo.processInfo.environment
|
||||
if let spoofBuild = options.spoofBuild { env["SPOOF_BUILD"] = spoofBuild }
|
||||
if options.forceDSCMaxSlide { env["FORCE_DSC_MAXSLIDE"] = "1" }
|
||||
try FileManager.default.createDirectory(at: resources.ipswCacheDir, withIntermediateDirectories: true)
|
||||
try FileManager.default.createDirectory(at: resources.sealVolumeCacheDir, withIntermediateDirectories: true)
|
||||
try FileManager.default.createDirectory(at: resources.debsCacheDir, withIntermediateDirectories: true)
|
||||
env["VPHONE_PYTHON"] = try resources.pythonExecutable().path
|
||||
env["IPSW_DIR"] = resources.ipswCacheDir.path
|
||||
env["VPHONE_SEAL_DIR"] = resources.sealVolumeCacheDir.path
|
||||
env["VPHONE_DEBS_DIR"] = resources.debsCacheDir.path
|
||||
if options.keepArtifacts { env["VPHONE_KEEP_ARTIFACTS"] = "1" }
|
||||
for (key, value) in sudoEnvExtras { env[key] = value }
|
||||
var scriptEnv: [String: String] = [
|
||||
"VPHONE_PYTHON": try resources.pythonExecutable().path,
|
||||
"IPSW_DIR": resources.ipswCacheDir.path,
|
||||
"VPHONE_SEAL_DIR": resources.sealVolumeCacheDir.path,
|
||||
"VPHONE_DEBS_DIR": resources.debsCacheDir.path,
|
||||
]
|
||||
if let spoofBuild = options.spoofBuild { scriptEnv["SPOOF_BUILD"] = spoofBuild }
|
||||
if options.forceDSCMaxSlide { scriptEnv["FORCE_DSC_MAXSLIDE"] = "1" }
|
||||
if options.keepArtifacts { scriptEnv["VPHONE_KEEP_ARTIFACTS"] = "1" }
|
||||
|
||||
let args = [resources.cfwInstallHostScript.path, "--variant", options.variant, bundleURL.path]
|
||||
let envKeys = (["VPHONE_PYTHON", "IPSW_DIR", "VPHONE_SEAL_DIR"] + sudoEnvExtras.keys.sorted()).joined(separator: ", ")
|
||||
trace("spawn /bin/zsh \(args.joined(separator: " ")) (env keys: \(envKeys))", v)
|
||||
// With an askpass credential sudo is non-interactive → honor verbosity.
|
||||
// Without one, sudo must prompt on the terminal → run as a foreground job
|
||||
// so its process group owns the tty (see runForeground).
|
||||
// --sudo-password (askpass) wins over --root-popup.
|
||||
let usePopup = options.rootPopup && sudoEnvExtras["SUDO_ASKPASS"] == nil
|
||||
let code: Int32
|
||||
if sudoEnvExtras["SUDO_ASKPASS"] != nil {
|
||||
code = try VPhoneProcessRunner.runStreaming(
|
||||
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
|
||||
if usePopup {
|
||||
// Forward SUDO_USER (sudo would set it) so the script's chown-back runs.
|
||||
scriptEnv["SUDO_USER"] = NSUserName()
|
||||
trace("osascript admin-privileges /bin/zsh \(args.joined(separator: " "))", v)
|
||||
code = try VPhoneProcessRunner.runWithAdminPrivileges(
|
||||
URL(fileURLWithPath: "/bin/zsh"), args, env: scriptEnv, echo: v.showsToolDetail)
|
||||
} else {
|
||||
print("[*] CFW install needs root — sudo will prompt for your macOS password.")
|
||||
// Foreground so sudo can own the tty; echo honors verbosity (quiet
|
||||
// suppresses the install's own output, sudo's /dev/tty prompt stays).
|
||||
code = try VPhoneProcessRunner.runForeground(
|
||||
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
|
||||
var env = ProcessInfo.processInfo.environment
|
||||
for (key, value) in scriptEnv { env[key] = value }
|
||||
for (key, value) in sudoEnvExtras { env[key] = value }
|
||||
let envKeys = (["VPHONE_PYTHON", "IPSW_DIR", "VPHONE_SEAL_DIR"] + sudoEnvExtras.keys.sorted()).joined(separator: ", ")
|
||||
trace("spawn /bin/zsh \(args.joined(separator: " ")) (env keys: \(envKeys))", v)
|
||||
// With an askpass credential sudo is non-interactive → honor verbosity.
|
||||
// Without one, sudo must prompt on the terminal → run as a foreground
|
||||
// job so its process group owns the tty (see runForeground).
|
||||
if sudoEnvExtras["SUDO_ASKPASS"] != nil {
|
||||
code = try VPhoneProcessRunner.runStreaming(
|
||||
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
|
||||
} else {
|
||||
print("[*] CFW install needs root — sudo will prompt for your macOS password.")
|
||||
code = try VPhoneProcessRunner.runForeground(
|
||||
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
|
||||
}
|
||||
}
|
||||
guard code == 0 else { throw VPhoneCreateError.cfwInstallFailed(code) }
|
||||
print("[+] CFW installed (\(options.variant)).")
|
||||
|
||||
@@ -104,6 +104,7 @@ struct VPhoneCFWInstallCommand: ParsableCommand {
|
||||
@Option(name: [.customShort("V"), .long], help: "variant: regular | dev | jb | exp") var variant: String = "exp"
|
||||
@Option(name: [.customShort("b"), .long], help: "(exp only) rewrite ProductBuildVersion to this build id") var spoofBuild: String?
|
||||
@Flag(name: .customLong("force-dsc-maxslide"), help: "Zero the dyld cache maxSlide on non-27 bases (opt-in DSC-map fit)") var forceDSCMaxSlide = false
|
||||
@Flag(name: .customLong("root-popup"), help: "Elevate via macOS's native authentication dialog (osascript) instead of the sudo re-exec") var rootPopup = false
|
||||
@Flag(name: .customLong("keep-artifacts"), help: "Keep the extracted CFW input dirs (cfw_input/, cfw_jb_input/) after install (default: removed to save space)") var keepArtifacts = false
|
||||
@Option(name: .shortAndLong, help: "Resource base override (default: inferred from the running binary path)")
|
||||
var projectRoot: String?
|
||||
@@ -119,29 +120,37 @@ struct VPhoneCFWInstallCommand: ParsableCommand {
|
||||
let bundle = try lib.library.bundle(named: name)
|
||||
let resources = projectRoot.map { VPhoneResources(base: URL(fileURLWithPath: $0)) } ?? .resolve()
|
||||
|
||||
var env = ProcessInfo.processInfo.environment
|
||||
if let spoofBuild { env["SPOOF_BUILD"] = spoofBuild }
|
||||
if forceDSCMaxSlide { env["FORCE_DSC_MAXSLIDE"] = "1" }
|
||||
if keepArtifacts { env["VPHONE_KEEP_ARTIFACTS"] = "1" }
|
||||
|
||||
// Same redirect as `fw prepare`: VPHONE_PYTHON/IPSW_DIR/VPHONE_SEAL_DIR are
|
||||
// exported for the bundled scripts (cfw_install_host.sh's PY/P lines honor
|
||||
// VPHONE_PYTHON); the apfs_sealvolume read itself only happens on the
|
||||
// `fw patch` path (CryptexFilesystemPatcher).
|
||||
// Env the bundled scripts read: rides along via `sudo -E` by default;
|
||||
// --root-popup forwards it inline (do shell script's bare env).
|
||||
try FileManager.default.createDirectory(at: resources.ipswCacheDir, withIntermediateDirectories: true)
|
||||
try FileManager.default.createDirectory(at: resources.sealVolumeCacheDir, withIntermediateDirectories: true)
|
||||
try FileManager.default.createDirectory(at: resources.debsCacheDir, withIntermediateDirectories: true)
|
||||
env["VPHONE_PYTHON"] = try resources.pythonExecutable().path
|
||||
env["IPSW_DIR"] = resources.ipswCacheDir.path
|
||||
env["VPHONE_SEAL_DIR"] = resources.sealVolumeCacheDir.path
|
||||
env["VPHONE_DEBS_DIR"] = resources.debsCacheDir.path
|
||||
var scriptEnv: [String: String] = [
|
||||
"VPHONE_PYTHON": try resources.pythonExecutable().path,
|
||||
"IPSW_DIR": resources.ipswCacheDir.path,
|
||||
"VPHONE_SEAL_DIR": resources.sealVolumeCacheDir.path,
|
||||
"VPHONE_DEBS_DIR": resources.debsCacheDir.path,
|
||||
]
|
||||
if let spoofBuild { scriptEnv["SPOOF_BUILD"] = spoofBuild }
|
||||
if forceDSCMaxSlide { scriptEnv["FORCE_DSC_MAXSLIDE"] = "1" }
|
||||
if keepArtifacts { scriptEnv["VPHONE_KEEP_ARTIFACTS"] = "1" }
|
||||
|
||||
let args = [resources.cfwInstallHostScript.path, "--variant", variant, bundle.url.path]
|
||||
if v.tracesInternals {
|
||||
print("[trace] spawning: /bin/zsh \(args.joined(separator: " ")) (env keys: VPHONE_PYTHON, IPSW_DIR, VPHONE_SEAL_DIR)")
|
||||
let code: Int32
|
||||
if rootPopup {
|
||||
// Forward SUDO_USER (sudo would set it) so the script's chown-back runs.
|
||||
scriptEnv["SUDO_USER"] = NSUserName()
|
||||
code = try VPhoneProcessRunner.runWithAdminPrivileges(
|
||||
URL(fileURLWithPath: "/bin/zsh"), args, env: scriptEnv, echo: v.showsToolDetail)
|
||||
} else {
|
||||
var env = ProcessInfo.processInfo.environment
|
||||
for (key, value) in scriptEnv { env[key] = value }
|
||||
if v.tracesInternals {
|
||||
print("[trace] spawning: /bin/zsh \(args.joined(separator: " ")) (env keys: VPHONE_PYTHON, IPSW_DIR, VPHONE_SEAL_DIR)")
|
||||
}
|
||||
code = try VPhoneProcessRunner.runStreaming(
|
||||
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
|
||||
}
|
||||
let code = try VPhoneProcessRunner.runStreaming(
|
||||
URL(fileURLWithPath: "/bin/zsh"), args, env: env, echo: v.showsToolDetail)
|
||||
if code == 0, !keepArtifacts, let removed = try? VPhoneRestoreInfo.removeBuiltFirmware(fromBundle: bundle) {
|
||||
print("[cfw] removed built firmware \(removed)/ to save space (--keep-artifacts to keep)")
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ struct VPhoneVMCreateCommand: ParsableCommand {
|
||||
var sudoPassword: String?
|
||||
@Option(name: [.customShort("b"), .long], help: "(exp only) rewrite ProductBuildVersion to this build id") var spoofBuild: String?
|
||||
@Flag(name: .customLong("force-dsc-maxslide"), help: "Zero the dyld cache maxSlide on non-27 bases (opt-in DSC-map fit)") var forceDSCMaxSlide = false
|
||||
@Flag(name: .customLong("root-popup"), help: "Elevate the CFW host-mount via macOS's native authentication dialog (osascript) instead of a sudo prompt") var rootPopup = false
|
||||
@Flag(help: "Prompt at first-boot stages instead of running non-interactively") var interactive = false
|
||||
@Flag(name: .customLong("keep-artifacts"), help: "Keep intermediate build artifacts (built restore firmware, extracted base-IPSW caches, extracted CFW input dirs) instead of removing them after use. Source archives (.ipsw / .tar.zst) are always kept.")
|
||||
var keepArtifacts = false
|
||||
@@ -39,6 +40,7 @@ struct VPhoneVMCreateCommand: ParsableCommand {
|
||||
name: name, variant: variant,
|
||||
iphoneSource: sources.iphoneSource, cloudosSource: sources.cloudosSource,
|
||||
sudoPassword: sudoPassword, spoofBuild: spoofBuild, forceDSCMaxSlide: forceDSCMaxSlide,
|
||||
rootPopup: rootPopup,
|
||||
interactive: interactive, verbosity: VPhoneVerbosity(count: verboseCount),
|
||||
keepArtifacts: keepArtifacts))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user