mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
vphone-cli: Add variant, udid, and device to vm info
`vm info` now reports the CFW variant the VM was last restored to, its predicted UDID, and the product type the guest identifies as. - udid: read from the bundle's udid-prediction.txt (VPhoneRestoreOps.resolveUDID) - variant + device: recorded into restore-info.json at CFW-install time, in both the standalone `cfw install` and the `vm create` orchestrator. device is iPhone99,11 for every variant except exp, whose DeviceTree rewrite -> iPhone17,3. - both new restore-info.json fields are optional, so pre-existing bundles decode unchanged and pick up variant/device on their next install. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
committed by
zqxwce
co-authored by
Claude Fable 5
parent
c6fa19efcb
commit
aeba01f13c
@@ -6,6 +6,7 @@ public struct VPhoneBundleReport: Codable, Equatable, Sendable {
|
||||
public let memoryMB: Int
|
||||
public let diskSizeBytes: Int64
|
||||
public let restoreInfo: VPhoneRestoreInfo?
|
||||
public let udid: String?
|
||||
|
||||
public init(bundle: VPhoneBundle) {
|
||||
self.name = bundle.name
|
||||
@@ -13,5 +14,6 @@ public struct VPhoneBundleReport: Codable, Equatable, Sendable {
|
||||
self.memoryMB = Int(bundle.manifest.memorySize / (1024 * 1024))
|
||||
self.diskSizeBytes = bundle.diskSizeBytes
|
||||
self.restoreInfo = VPhoneRestoreInfo.load(fromBundle: bundle)
|
||||
self.udid = VPhoneRestoreOps.resolveUDID(bundle: bundle)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,26 @@ public struct VPhoneRestoreInfo: Codable, Equatable, Sendable {
|
||||
|
||||
public let ios: OSVersion
|
||||
public let cloudOS: OSVersion
|
||||
public let variant: String?
|
||||
public let device: String?
|
||||
|
||||
public init(ios: OSVersion, cloudOS: OSVersion) {
|
||||
public init(ios: OSVersion, cloudOS: OSVersion, variant: String? = nil, device: String? = nil) {
|
||||
self.ios = ios
|
||||
self.cloudOS = cloudOS
|
||||
self.variant = variant
|
||||
self.device = device
|
||||
}
|
||||
|
||||
static let fileName = "restore-info.json"
|
||||
|
||||
public static let baseDevice = "iPhone99,11"
|
||||
public static let experimentalDevice = "iPhone17,3"
|
||||
|
||||
/// Only `exp` rewrites the DeviceTree identity; others keep the base type.
|
||||
public static func device(forVariant variant: String) -> String {
|
||||
variant == "exp" ? experimentalDevice : baseDevice
|
||||
}
|
||||
|
||||
public static func url(forBundle bundle: VPhoneBundle) -> URL {
|
||||
bundle.url.appendingPathComponent(fileName)
|
||||
}
|
||||
@@ -57,6 +69,20 @@ public struct VPhoneRestoreInfo: Codable, Equatable, Sendable {
|
||||
try encoder.encode(self).write(to: Self.url(forBundle: bundle))
|
||||
}
|
||||
|
||||
/// Set `variant` (and its device) on the bundle's restore-info.json, keeping
|
||||
/// the recorded versions. nil if no versions exist yet to preserve.
|
||||
@discardableResult
|
||||
public static func recordVariant(_ variant: String, toBundle bundle: VPhoneBundle) throws
|
||||
-> VPhoneRestoreInfo?
|
||||
{
|
||||
guard let base = load(fromBundle: bundle) else { return nil }
|
||||
let merged = VPhoneRestoreInfo(
|
||||
ios: base.ios, cloudOS: base.cloudOS,
|
||||
variant: variant, device: device(forVariant: variant))
|
||||
try merged.write(toBundle: bundle)
|
||||
return merged
|
||||
}
|
||||
|
||||
/// Remove the `iPhone*_Restore/` tree from the bundle; returns its name, or
|
||||
/// nil if absent. Record versions (`derive`) first — it reads this directory.
|
||||
@discardableResult
|
||||
|
||||
@@ -23,6 +23,19 @@ public enum VPhoneRestoreOps {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - UDID
|
||||
|
||||
/// UDID from the `UDID=` line of the bundle's udid-prediction.txt, or nil.
|
||||
public static func resolveUDID(bundle: VPhoneBundle) -> String? {
|
||||
let pred = bundle.url.appendingPathComponent("udid-prediction.txt")
|
||||
guard let text = try? String(contentsOf: pred, encoding: .utf8) else { return nil }
|
||||
for line in text.split(whereSeparator: \.isNewline) where line.hasPrefix("UDID=") {
|
||||
let value = line.dropFirst("UDID=".count).trimmingCharacters(in: .whitespaces)
|
||||
return value.isEmpty ? nil : value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - AEA
|
||||
|
||||
/// True if the file begins with the AEA1 magic (`41 45 41 31`).
|
||||
|
||||
@@ -521,6 +521,10 @@ public struct VPhoneCreateOrchestrator {
|
||||
}
|
||||
guard code == 0 else { throw VPhoneCreateError.cfwInstallFailed(code) }
|
||||
print("[+] CFW installed (\(options.variant)).")
|
||||
if let bundle = try? VPhoneBundle.load(at: bundleURL),
|
||||
let info = try? VPhoneRestoreInfo.recordVariant(options.variant, toBundle: bundle), info.variant != nil {
|
||||
print("[+] Recorded variant \(options.variant), device \(info.device ?? "?")")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - first boot
|
||||
|
||||
@@ -151,8 +151,13 @@ struct VPhoneCFWInstallCommand: ParsableCommand {
|
||||
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)")
|
||||
if code == 0 {
|
||||
if let info = try? VPhoneRestoreInfo.recordVariant(variant, toBundle: bundle), info.variant != nil {
|
||||
print("[cfw] recorded variant \(variant), device \(info.device ?? "?")")
|
||||
}
|
||||
if !keepArtifacts, let removed = try? VPhoneRestoreInfo.removeBuiltFirmware(fromBundle: bundle) {
|
||||
print("[cfw] removed built firmware \(removed)/ to save space (--keep-artifacts to keep)")
|
||||
}
|
||||
}
|
||||
throw ExitCode(code)
|
||||
}
|
||||
|
||||
@@ -91,9 +91,12 @@ struct VPhoneVMInfoCommand: ParsableCommand {
|
||||
print("mem: \(report.memoryMB) MB")
|
||||
print("disk: \(report.diskSizeBytes) bytes")
|
||||
print("net: \(describeNetwork(bundle.manifest.networkConfig))")
|
||||
if let udid = report.udid { print("udid: \(udid)") }
|
||||
if let info = report.restoreInfo {
|
||||
print("iOS: \(info.ios.version) (\(info.ios.build))")
|
||||
print("cloudOS: \(info.cloudOS.version) (\(info.cloudOS.build))")
|
||||
if let variant = info.variant { print("variant: \(variant)") }
|
||||
if let device = info.device { print("device: \(device)") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,44 @@ struct RestoreInfoTests {
|
||||
#expect(report.restoreInfo?.cloudOS.build == "23E5207q")
|
||||
}
|
||||
|
||||
@Test func deviceForVariant() {
|
||||
#expect(VPhoneRestoreInfo.device(forVariant: "exp") == "iPhone17,3")
|
||||
for v in ["regular", "dev", "jb"] {
|
||||
#expect(VPhoneRestoreInfo.device(forVariant: v) == "iPhone99,11")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func recordVariantMergesIntoVersions() throws {
|
||||
let b = try makeBundle()
|
||||
defer { try? FileManager.default.removeItem(at: b.url) }
|
||||
try VPhoneRestoreInfo(
|
||||
ios: .init(version: "18.6.2", build: "22G100"),
|
||||
cloudOS: .init(version: "26.1", build: "23B85")).write(toBundle: b)
|
||||
|
||||
let merged = try VPhoneRestoreInfo.recordVariant("exp", toBundle: b)
|
||||
#expect(merged?.variant == "exp")
|
||||
#expect(merged?.device == "iPhone17,3")
|
||||
|
||||
let loaded = VPhoneRestoreInfo.load(fromBundle: b)
|
||||
#expect(loaded?.ios.build == "22G100")
|
||||
#expect(loaded?.variant == "exp")
|
||||
#expect(loaded?.device == "iPhone17,3")
|
||||
}
|
||||
|
||||
@Test func recordVariantNilWithoutVersions() throws {
|
||||
let b = try makeBundle()
|
||||
defer { try? FileManager.default.removeItem(at: b.url) }
|
||||
#expect(try VPhoneRestoreInfo.recordVariant("jb", toBundle: b) == nil)
|
||||
}
|
||||
|
||||
@Test func bundleReportCarriesUDID() throws {
|
||||
let b = try makeBundle()
|
||||
defer { try? FileManager.default.removeItem(at: b.url) }
|
||||
try "UDID=AAAABBBB-1122334455667788\n"
|
||||
.write(to: b.url.appendingPathComponent("udid-prediction.txt"), atomically: true, encoding: .utf8)
|
||||
#expect(VPhoneBundleReport(bundle: b).udid == "AAAABBBB-1122334455667788")
|
||||
}
|
||||
|
||||
/// The snapshot lives at the bundle root, so `vm export` must not strip it:
|
||||
/// it is matched by neither the `*_Restore*` exclude nor any regenerable-
|
||||
/// artifact pattern. Guards against a future exclude edit dropping it.
|
||||
|
||||
@@ -33,6 +33,22 @@ struct RestoreOpsTests {
|
||||
#expect(VPhoneRestoreOps.resolveECID(explicit: nil, bundle: b) == nil)
|
||||
}
|
||||
|
||||
@Test func resolveUDIDFromPredictionFile() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let b = try bundle(in: root)
|
||||
try "UDID=AAAABBBB-1122334455667788\nECID=1122334455667788\n"
|
||||
.write(to: root.appendingPathComponent("udid-prediction.txt"), atomically: true, encoding: .utf8)
|
||||
#expect(VPhoneRestoreOps.resolveUDID(bundle: b) == "AAAABBBB-1122334455667788")
|
||||
}
|
||||
|
||||
@Test func resolveUDIDNilWhenMissing() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let b = try bundle(in: root)
|
||||
#expect(VPhoneRestoreOps.resolveUDID(bundle: b) == nil)
|
||||
}
|
||||
|
||||
@Test func isAEAEncryptedDetectsMagic() throws {
|
||||
let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
|
||||
Reference in New Issue
Block a user