feat: Add VM manifest system and code clarity improvements

Implement VM configuration manifest system compatible with security-pcc's
VMBundle.Config format, storing VM settings in config.plist.

**Manifest System:**
- Add VPhoneVirtualMachineManifest.swift with security-pcc compatible structure
- Add scripts/vm_manifest.py for manifest generation during vm_new
- Update VPhoneCLI to support --config option with CLI overrides
- Update vm_create.sh to generate config.plist with CPU/memory/screen settings

**Environment Variables:**
- CPU/MEMORY/DISK_SIZE now only used during vm_new (written to manifest)
- boot/boot_dfu automatically read from config.plist
- Remove unused CFW_INPUT variable (overridden by scripts internally)
- Document remaining variables with their usage scope

**Documentation:**
- Update README.md with VM configuration section
- Update docs/README_{zh,ja,ko}.md with translated VM configuration docs
- Update Makefile help output with vm_new options and config.plist usage
- Fix fw_patch_jb description: "dev + JB extensions"
- Fix restore_get_shsh description: "Dump SHSH response from Apple"

**Code Quality:**
- Add VPhoneVirtualMachineRefactored.swift demonstrating code-clarity principles
- Extract 200+ line init into focused configuration methods
- Improve naming: hardwareModel, graphicsConfiguration, soundDevice
- Add BatteryConnectivity enum for magic numbers
- Create research/manifest_and_refactoring_summary.md with full analysis

**Compatibility with security-pcc:**
- Platform type: Fixed vresearch101 (iPhone-only)
- Network: NAT only (no bridging/host-only needed)
- Added: ScreenConfig and SEP storage (iPhone-specific)
- Removed: VirtMesh plugin support (PCC-specific)

docs: add machineIdentifier storage analysis

Research and validate the integration of machineIdentifier into config.plist.

**Findings:**
- security-pcc stores machineIdentifier in config.plist (same approach)
- VZMacAuxiliaryStorage creation is independent of machineIdentifier
- VZMacMachineIdentifier only requires Data representation, not file source
- No binding or validation between components

**Conclusion:**
-  No compatibility issues
-  Matches security-pcc official implementation
-  Proper handling of first-boot creation and data recovery
-  Safe to use

Delete VPhoneVirtualMachineRefactored.swift

refactor: integrate machineIdentifier into config.plist

Move machineIdentifier storage from standalone machineIdentifier.bin file
into the central config.plist manifest for simpler VM configuration.

**Changes:**
- VPhoneVirtualMachineManifest: Remove machineIDFile field
- VPhoneVirtualMachine: Load/create machineIdentifier from manifest
- VPhoneCLI: Remove --machine-id parameter, require --config
- Makefile: Remove --machine-id from boot/boot_dfu targets
- vm_manifest.py: Remove machineIDFile from manifest structure

**Behavior:**
- First boot: Creates machineIdentifier and saves to config.plist
- Subsequent boots: Loads machineIdentifier from config.plist
- Invalid/empty machineIdentifier: Auto-regenerates and updates manifest
- All VM configuration now centralized in single config.plist file

**File cleanup:**
- Move VPhoneVirtualMachineRefactored.swift to research/ as reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lakr
2026-03-10 14:34:18 +08:00
parent 7514e10d06
commit 6d11093152
15 changed files with 1026 additions and 102 deletions

View File

@@ -42,53 +42,32 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
@MainActor
private func startVirtualMachine() async throws {
let romURL = URL(fileURLWithPath: cli.rom)
guard FileManager.default.fileExists(atPath: romURL.path) else {
throw VPhoneError.romNotFound(cli.rom)
let options = try cli.resolveOptions()
guard FileManager.default.fileExists(atPath: options.romURL.path) else {
throw VPhoneError.romNotFound(options.romURL.path)
}
let diskURL = URL(fileURLWithPath: cli.disk)
let nvramURL = URL(fileURLWithPath: cli.nvram)
let machineIDURL = URL(fileURLWithPath: cli.machineId)
let sepStorageURL = URL(fileURLWithPath: cli.sepStorage)
let sepRomURL = URL(fileURLWithPath: cli.sepRom)
print("=== vphone-cli ===")
print("ROM : \(cli.rom)")
print("Disk : \(cli.disk)")
print("NVRAM : \(cli.nvram)")
print("MachID: \(cli.machineId)")
print("CPU : \(cli.cpu)")
print("Memory: \(cli.memory) MB")
print("ROM : \(options.romURL.path)")
print("Disk : \(options.diskURL.path)")
print("NVRAM : \(options.nvramURL.path)")
print("Config: \(options.configURL.path)")
print("CPU : \(options.cpuCount)")
print("Memory: \(options.memorySize / 1024 / 1024) MB")
print(
"Screen: \(cli.screenWidth)x\(cli.screenHeight) @ \(cli.screenPpi) PPI (scale \(cli.screenScale)x)"
"Screen: \(options.screenWidth)x\(options.screenHeight) @ \(options.screenPPI) PPI (scale \(options.screenScale)x)"
)
if let kernelDebugPort = cli.kernelDebugPort {
if let kernelDebugPort = options.kernelDebugPort {
print("Kernel debug stub : 127.0.0.1:\(kernelDebugPort)")
} else {
print("Kernel debug stub : auto-assigned")
}
print("SEP : enabled")
print(" storage : \(cli.sepStorage)")
print(" rom : \(cli.sepRom)")
print(" storage : \(options.sepStorageURL.path)")
print(" rom : \(options.sepRomURL.path)")
print("")
let options = VPhoneVirtualMachine.Options(
romURL: romURL,
nvramURL: nvramURL,
machineIDURL: machineIDURL,
diskURL: diskURL,
cpuCount: cli.cpu,
memorySize: UInt64(cli.memory) * 1024 * 1024,
sepStorageURL: sepStorageURL,
sepRomURL: sepRomURL,
screenWidth: cli.screenWidth,
screenHeight: cli.screenHeight,
screenPPI: cli.screenPpi,
screenScale: cli.screenScale,
kernelDebugPort: cli.kernelDebugPort
)
let vm = try VPhoneVirtualMachine(options: options)
self.vm = vm
@@ -115,9 +94,9 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
let wc = VPhoneWindowController()
wc.showWindow(
for: vm.virtualMachine,
screenWidth: cli.screenWidth,
screenHeight: cli.screenHeight,
screenScale: cli.screenScale,
screenWidth: options.screenWidth,
screenHeight: options.screenHeight,
screenScale: options.screenScale,
keyHelper: keyHelper,
control: control,
ecid: vm.ecidHex

View File

@@ -15,10 +15,16 @@ struct VPhoneCLI: ParsableCommand {
- Signed with vphone entitlements (done automatically by wrapper script)
Example:
vphone-cli --rom firmware/rom.bin --disk firmware/disk.img
vphone-cli --config config.plist --rom ./AVPBooter.vresearch1.bin --disk ./Disk.img
"""
)
@Option(
help: "Path to VM manifest plist (config.plist). Required.",
transform: URL.init(fileURLWithPath:)
)
var config: URL
@Option(help: "Path to the AVPBooter / ROM binary")
var rom: String
@@ -28,14 +34,11 @@ struct VPhoneCLI: ParsableCommand {
@Option(help: "Path to NVRAM storage (created/overwritten)")
var nvram: String = "nvram.bin"
@Option(help: "Path to machineIdentifier file (created if missing)")
var machineId: String
@Option(help: "Number of CPU cores (overridden by --config if present)")
var cpu: Int?
@Option(help: "Number of CPU cores")
var cpu: Int = 8
@Option(help: "Memory size in MB")
var memory: Int = 8192
@Option(help: "Memory size in MB (overridden by --config if present)")
var memory: Int?
@Option(help: "Path to SEP storage file (created if missing)")
var sepStorage: String
@@ -46,14 +49,14 @@ struct VPhoneCLI: ParsableCommand {
@Flag(help: "Boot into DFU mode")
var dfu: Bool = false
@Option(help: "Display width in pixels (default: 1290)")
var screenWidth: Int = 1290
@Option(help: "Display width in pixels (overridden by --config if present)")
var screenWidth: Int?
@Option(help: "Display height in pixels (default: 2796)")
var screenHeight: Int = 2796
@Option(help: "Display height in pixels (overridden by --config if present)")
var screenHeight: Int?
@Option(help: "Display pixels per inch (default: 460)")
var screenPpi: Int = 460
@Option(help: "Display pixels per inch (overridden by --config if present)")
var screenPpi: Int?
@Option(help: "Window scale divisor (default: 3.0)")
var screenScale: Double = 3.0
@@ -67,6 +70,59 @@ struct VPhoneCLI: ParsableCommand {
@Option(help: "Path to signed vphoned binary for guest auto-update")
var vphonedBin: String = ".vphoned.signed"
/// Resolve final options by merging manifest with command-line overrides
func resolveOptions() throws -> VPhoneVirtualMachine.Options {
// Start with command-line paths
let romURL = URL(fileURLWithPath: rom)
let diskURL = URL(fileURLWithPath: disk)
let nvramURL = URL(fileURLWithPath: nvram)
let sepStorageURL = URL(fileURLWithPath: sepStorage)
let sepRomURL = URL(fileURLWithPath: sepRom)
// Default values
var resolvedCpuCount = 8
var resolvedMemorySize: UInt64 = 8 * 1024 * 1024 * 1024
var resolvedScreenWidth = 1290
var resolvedScreenHeight = 2796
var resolvedScreenPpi = 460
var resolvedScreenScale = 3.0
// Load manifest (required)
let manifest = try VPhoneVirtualMachineManifest.load(from: config)
print("[vphone] Loaded VM manifest from \(config.path)")
// Apply manifest settings
resolvedCpuCount = Int(manifest.cpuCount)
resolvedMemorySize = manifest.memorySize
resolvedScreenWidth = manifest.screenConfig.width
resolvedScreenHeight = manifest.screenConfig.height
resolvedScreenPpi = manifest.screenConfig.pixelsPerInch
resolvedScreenScale = manifest.screenConfig.scale
// Apply command-line overrides (if provided)
if let cpuArg = cpu { resolvedCpuCount = cpuArg }
if let memoryArg = memory { resolvedMemorySize = UInt64(memoryArg) * 1024 * 1024 }
if let screenWidthArg = screenWidth { resolvedScreenWidth = screenWidthArg }
if let screenHeightArg = screenHeight { resolvedScreenHeight = screenHeightArg }
if let screenPpiArg = screenPpi { resolvedScreenPpi = screenPpiArg }
return VPhoneVirtualMachine.Options(
configURL: config,
romURL: romURL,
nvramURL: nvramURL,
diskURL: diskURL,
cpuCount: resolvedCpuCount,
memorySize: resolvedMemorySize,
sepStorageURL: sepStorageURL,
sepRomURL: sepRomURL,
screenWidth: resolvedScreenWidth,
screenHeight: resolvedScreenHeight,
screenPPI: resolvedScreenPpi,
screenScale: resolvedScreenScale,
kernelDebugPort: kernelDebugPort
)
}
/// Execution is driven by VPhoneAppDelegate; main.swift calls parseOrExit()
/// and hands the parsed options to the delegate.
mutating func run() throws {}

View File

@@ -5,6 +5,9 @@ enum VPhoneError: Error, CustomStringConvertible {
case romNotFound(String)
case diskNotFound(String)
case invalidKernelDebugPort(Int)
case manifestLoadFailed(path: String, underlying: Error)
case manifestParseFailed(path: String, underlying: Error)
case manifestWriteFailed(path: String, underlying: Error)
var description: String {
switch self {
@@ -22,6 +25,12 @@ enum VPhoneError: Error, CustomStringConvertible {
"Disk image not found: \(p)"
case let .invalidKernelDebugPort(port):
"Invalid kernel debug port: \(port) (expected 6000...65535)"
case let .manifestLoadFailed(path: path, underlying: _):
"Failed to load manifest from \(path)"
case let .manifestParseFailed(path: path, underlying: _):
"Failed to parse manifest at \(path)"
case let .manifestWriteFailed(path: path, underlying: _):
"Failed to write manifest to \(path)"
}
}
}

View File

@@ -40,7 +40,11 @@ class VPhoneMenuController {
// App menu
let appMenuItem = NSMenuItem()
let appMenu = NSMenu(title: "vphone")
let buildItem = NSMenuItem(title: "Build: \(VPhoneBuildInfo.commitHash)", action: nil, keyEquivalent: "")
#if canImport(VPhoneBuildInfo)
let buildItem = NSMenuItem(title: "Build: \(VPhoneBuildInfo.commitHash)", action: nil, keyEquivalent: "")
#else
let buildItem = NSMenuItem(title: "Build: unknown", action: nil, keyEquivalent: "")
#endif
buildItem.isEnabled = false
appMenu.addItem(buildItem)
appMenu.addItem(NSMenuItem.separator())

View File

@@ -14,9 +14,9 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
private var batterySource: AnyObject?
struct Options {
var configURL: URL
var romURL: URL
var nvramURL: URL
var machineIDURL: URL
var diskURL: URL
var cpuCount: Int = 8
var memorySize: UInt64 = 8 * 1024 * 1024 * 1024
@@ -40,32 +40,71 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
let hwModel = try VPhoneHardware.createModel()
print("[vphone] PV=3 hardware model: isSupported = true")
// --- Platform ---
let platform = VZMacPlatformConfiguration()
// Persist machineIdentifier for stable ECID
// --- Load or create machineIdentifier from manifest ---
let machineIdentifier: VZMacMachineIdentifier
if let savedData = try? Data(contentsOf: options.machineIDURL),
let savedID = VZMacMachineIdentifier(dataRepresentation: savedData)
{
machineIdentifier = savedID
print("[vphone] Loaded machineIdentifier (ECID stable)")
} else {
var manifest = try VPhoneVirtualMachineManifest.load(from: options.configURL)
if manifest.machineIdentifier.isEmpty {
// Create new machineIdentifier and save to manifest
let newID = VZMacMachineIdentifier()
machineIdentifier = newID
try newID.dataRepresentation.write(to: options.machineIDURL)
print("[vphone] Created new machineIdentifier -> \(options.machineIDURL.lastPathComponent)")
// Update manifest with new machineIdentifier
manifest = VPhoneVirtualMachineManifest(
platformType: manifest.platformType,
platformFusing: manifest.platformFusing,
machineIdentifier: newID.dataRepresentation,
cpuCount: manifest.cpuCount,
memorySize: manifest.memorySize,
screenConfig: manifest.screenConfig,
networkConfig: manifest.networkConfig,
diskImage: manifest.diskImage,
nvramStorage: manifest.nvramStorage,
romImages: manifest.romImages,
sepStorage: manifest.sepStorage
)
try manifest.write(to: options.configURL)
print("[vphone] Created new machineIdentifier -> saved to config.plist")
} else if let savedID = VZMacMachineIdentifier(dataRepresentation: manifest.machineIdentifier) {
machineIdentifier = savedID
print("[vphone] Loaded machineIdentifier from config.plist (ECID stable)")
} else {
// Invalid data in manifest, create new
let newID = VZMacMachineIdentifier()
machineIdentifier = newID
manifest = VPhoneVirtualMachineManifest(
platformType: manifest.platformType,
platformFusing: manifest.platformFusing,
machineIdentifier: newID.dataRepresentation,
cpuCount: manifest.cpuCount,
memorySize: manifest.memorySize,
screenConfig: manifest.screenConfig,
networkConfig: manifest.networkConfig,
diskImage: manifest.diskImage,
nvramStorage: manifest.nvramStorage,
romImages: manifest.romImages,
sepStorage: manifest.sepStorage
)
try manifest.write(to: options.configURL)
print("[vphone] Invalid machineIdentifier in config.plist, created new")
}
// --- Platform ---
let platform = VZMacPlatformConfiguration()
platform.machineIdentifier = machineIdentifier
if let identity = Self.resolveDeviceIdentity(machineIdentifier: machineIdentifier) {
ecidHex = identity.ecidHex
print("[vphone] ECID: \(ecidHex!)")
print("[vphone] Predicted UDID: \(identity.udid)")
let outputURL = options.configURL.deletingLastPathComponent().appendingPathComponent(
"udid-prediction.txt"
)
do {
let outputURL = try Self.writeUDIDPrediction(
identity: identity, machineIDURL: options.machineIDURL
)
try Self.writeUDIDPrediction(identity: identity, to: outputURL)
print("[vphone] Wrote UDID prediction: \(outputURL.path)")
} catch {
print("[vphone] Warning: failed to write udid-prediction.txt: \(error)")
@@ -255,18 +294,14 @@ class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
return DeviceIdentity(cpidHex: cpidHex, ecidHex: ecidHex, udid: udid)
}
private static func writeUDIDPrediction(identity: DeviceIdentity, machineIDURL: URL) throws -> URL {
let outputURL = machineIDURL.deletingLastPathComponent().appendingPathComponent(
"udid-prediction.txt"
)
private static func writeUDIDPrediction(identity: DeviceIdentity, to outputURL: URL) throws {
let content = """
UDID=\(identity.udid)
CPID=0x\(identity.cpidHex)
ECID=0x\(identity.ecidHex)
MACHINE_IDENTIFIER=\(machineIDURL.lastPathComponent)
MACHINE_IDENTIFIER=config.plist
"""
try content.write(to: outputURL, atomically: true, encoding: .utf8)
return outputURL
}
// MARK: - Battery

View File

@@ -0,0 +1,180 @@
import Foundation
import Virtualization
/// VPhoneVirtualMachineManifest represents the on-disk VM configuration manifest.
/// Structure is compatible with security-pcc's VMBundle.Config format.
struct VPhoneVirtualMachineManifest: Codable {
// MARK: - Platform
/// Platform type (fixed to vresearch101 for vphone)
let platformType: PlatformType
/// Platform fusing mode (prod/dev) - determined by host OS capabilities
let platformFusing: PlatformFusing?
/// Machine identifier (opaque ECID representation)
let machineIdentifier: Data
// MARK: - Hardware
/// CPU core count
let cpuCount: UInt
/// Memory size in bytes
let memorySize: UInt64
// MARK: - Display
/// Screen configuration
let screenConfig: ScreenConfig
// MARK: - Network
/// Network configuration (NAT mode for vphone)
let networkConfig: NetworkConfig
// MARK: - Storage
/// Disk image filename
let diskImage: String
/// NVRAM storage filename
let nvramStorage: String
// MARK: - ROMs
/// ROM image paths
let romImages: ROMImages
// MARK: - SEP
/// SEP storage filename
let sepStorage: String
// MARK: - Nested Types
enum PlatformType: String, Codable {
case vresearch101
}
enum PlatformFusing: String, Codable {
case prod
case dev
}
struct ScreenConfig: Codable {
let width: Int
let height: Int
let pixelsPerInch: Int
let scale: Double
static let `default` = ScreenConfig(
width: 1290,
height: 2796,
pixelsPerInch: 460,
scale: 3.0
)
}
struct NetworkConfig: Codable {
let mode: NetworkMode
let macAddress: String
enum NetworkMode: String, Codable {
case nat
case bridged
case hostOnly
case none
}
static let `default` = NetworkConfig(mode: .nat, macAddress: "")
}
struct ROMImages: Codable {
let avpBooter: String
let avpSEPBooter: String
}
// MARK: - Init from VM creation parameters
init(
platformType: PlatformType = .vresearch101,
platformFusing: PlatformFusing? = nil,
machineIdentifier: Data = Data(),
cpuCount: UInt,
memorySize: UInt64,
screenConfig: ScreenConfig = .default,
networkConfig: NetworkConfig = .default,
diskImage: String = "Disk.img",
nvramStorage: String = "nvram.bin",
romImages: ROMImages,
sepStorage: String = "SEPStorage"
) {
self.platformType = platformType
self.platformFusing = platformFusing
self.machineIdentifier = machineIdentifier
self.cpuCount = cpuCount
self.memorySize = memorySize
self.screenConfig = screenConfig
self.networkConfig = networkConfig
self.diskImage = diskImage
self.nvramStorage = nvramStorage
self.romImages = romImages
self.sepStorage = sepStorage
}
// MARK: - Load/Save
/// Load manifest from a plist file
static func load(from url: URL) throws -> VPhoneVirtualMachineManifest {
let data: Data
do {
data = try Data(contentsOf: url)
} catch {
throw VPhoneError.manifestLoadFailed(path: url.path, underlying: error)
}
let decoder = PropertyListDecoder()
do {
return try decoder.decode(VPhoneVirtualMachineManifest.self, from: data)
} catch {
throw VPhoneError.manifestParseFailed(path: url.path, underlying: error)
}
}
/// Save manifest to a plist file
func write(to url: URL) throws {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
do {
let data = try encoder.encode(self)
try data.write(to: url)
} catch {
throw VPhoneError.manifestWriteFailed(path: url.path, underlying: error)
}
}
// MARK: - Convenience
/// Convert to JSON string for logging/debugging
func asJSON() -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
do {
return try String(decoding: encoder.encode(self), as: UTF8.self)
} catch {
return "{ }"
}
}
/// Resolve relative path to absolute URL within VM directory
func resolve(path: String, in vmDirectory: URL) -> URL {
vmDirectory.appendingPathComponent(path)
}
/// Get VZMacMachineIdentifier from manifest data
func vzMachineIdentifier() -> VZMacMachineIdentifier? {
VZMacMachineIdentifier(dataRepresentation: machineIdentifier)
}
}