From 1e3d6d75eeec064b9f3c6212bd12bd5729b6d88e Mon Sep 17 00:00:00 2001 From: James Jackson <13633271+jsj@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:49:18 -0400 Subject: [PATCH] boot: add --install-ipa auto-install option (#236) --- sources/vphone-cli/VPhoneAppDelegate.swift | 43 ++++++++++++++++++++++ sources/vphone-cli/VPhoneCLI.swift | 10 +++++ 2 files changed, 53 insertions(+) diff --git a/sources/vphone-cli/VPhoneAppDelegate.swift b/sources/vphone-cli/VPhoneAppDelegate.swift index 9f663b6..5dfd309 100644 --- a/sources/vphone-cli/VPhoneAppDelegate.swift +++ b/sources/vphone-cli/VPhoneAppDelegate.swift @@ -13,6 +13,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate { private var appWindowController: VPhoneAppWindowController? private var locationProvider: VPhoneLocationProvider? private var sigintSource: DispatchSourceSignal? + private var didAttemptAutoInstall = false init(cli: VPhoneBootCLI) { self.cli = cli @@ -154,6 +155,9 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate { } mc?.syncBatteryFromHost() mc?.syncLowPowerModeFromHost() + Task { @MainActor [weak self] in + await self?.installPackageIfRequested(caps: caps) + } } control.onDisconnect = { [weak mc, weak provider = locationProvider] in mc?.updateConnectAvailability(available: false) @@ -174,6 +178,9 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate { } else { print("[location] guest does not support location simulation") } + Task { @MainActor [weak self] in + await self?.installPackageIfRequested(caps: caps) + } } control.onDisconnect = { [weak provider = locationProvider] in provider?.stopReplay() @@ -182,6 +189,42 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate { } } + @MainActor + private func installPackageIfRequested(caps: [String]) async { + guard !didAttemptAutoInstall else { return } + guard let packageURL = cli.installPackageURL else { return } + + guard FileManager.default.fileExists(atPath: packageURL.path) else { + didAttemptAutoInstall = true + print("[install] requested package not found: \(packageURL.path)") + return + } + guard VPhoneInstallPackage.isSupportedFile(packageURL) else { + didAttemptAutoInstall = true + print("[install] unsupported package type: \(packageURL.path)") + return + } + guard caps.contains("ipa_install") else { + print( + "[install] guest does not advertise ipa_install; reconnect or reboot the guest so the updated daemon can take over" + ) + return + } + guard let control else { + print("[install] control channel is not ready") + return + } + + didAttemptAutoInstall = true + print("[install] auto-installing \(packageURL.lastPathComponent)") + do { + let result = try await control.installIPA(localURL: packageURL) + print("[install] \(result)") + } catch { + print("[install] failed: \(error)") + } + } + func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { !cli.noGraphics } diff --git a/sources/vphone-cli/VPhoneCLI.swift b/sources/vphone-cli/VPhoneCLI.swift index f76826a..7c29932 100644 --- a/sources/vphone-cli/VPhoneCLI.swift +++ b/sources/vphone-cli/VPhoneCLI.swift @@ -44,11 +44,21 @@ struct VPhoneBootCLI: ParsableCommand { @Option(help: "Path to signed vphoned binary for guest auto-update") var vphonedBin: String = ".vphoned.signed" + @Option( + help: "Automatically install the given IPA/TIPA after the guest control channel connects.", + transform: URL.init(fileURLWithPath:) + ) + var installIPA: URL? + /// DFU mode runs headless (no GUI). var noGraphics: Bool { dfu } + var installPackageURL: URL? { + installIPA?.standardizedFileURL + } + /// Resolve final options by merging manifest values. func resolveOptions() throws -> VPhoneVirtualMachine.Options { let manifest = try VPhoneVirtualMachineManifest.load(from: config)