mirror of https://github.com/cirruslabs/tart.git
Merge 01d9359fbd into f87b57bbc5
This commit is contained in:
commit
6169502404
|
|
@ -66,10 +66,19 @@ struct Clone: AsyncParsableCommand {
|
|||
let lock = try FileLock(lockURL: Config().tartHomeDir)
|
||||
try lock.lock()
|
||||
|
||||
let sourceState = try sourceVM.state()
|
||||
let sourceConfig = try VMConfig(fromURL: sourceVM.configURL)
|
||||
|
||||
let generateMAC = try localStorage.hasVMsWithMACAddress(macAddress: sourceVM.macAddress())
|
||||
&& sourceVM.state() != .Suspended
|
||||
&& sourceState != .Suspended
|
||||
try sourceVM.clone(to: tmpVMDir, generateMAC: generateMAC)
|
||||
|
||||
if sourceState != .Suspended,
|
||||
let linux = sourceConfig.platform as? Linux,
|
||||
linux.machineIdentifier == nil {
|
||||
try tmpVMDir.initializeLinuxMachineIdentifier()
|
||||
}
|
||||
|
||||
try localStorage.move(newName, from: tmpVMDir)
|
||||
|
||||
try lock.unlock()
|
||||
|
|
|
|||
|
|
@ -271,7 +271,9 @@ struct Run: AsyncParsableCommand {
|
|||
var rootDiskOpts: String = ""
|
||||
|
||||
#if arch(arm64)
|
||||
@Flag(help: ArgumentHelp("Disables audio and entropy devices and switches to only Mac-specific input devices.", discussion: "Useful for running a VM that can be suspended via \"tart suspend\"."))
|
||||
@Flag(
|
||||
help: ArgumentHelp("Disables or replaces devices that do not support VM suspension, such as audio, entropy and some input devices.",
|
||||
discussion: "Useful for running a VM that can be suspended via \"tart suspend\"."))
|
||||
#endif
|
||||
var suspendable: Bool = false
|
||||
|
||||
|
|
@ -359,7 +361,11 @@ struct Run: AsyncParsableCommand {
|
|||
if suspendable {
|
||||
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||
if !(config.platform is PlatformSuspendable) {
|
||||
throw ValidationError("You can only suspend macOS VMs")
|
||||
throw ValidationError("This platform is not suspendable")
|
||||
}
|
||||
|
||||
if let linux = config.platform as? Linux, linux.machineIdentifier == nil {
|
||||
throw ValidationError("Linux VMs without a machine identifier cannot be suspended or resumed")
|
||||
}
|
||||
|
||||
if noTrackpad {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,42 @@
|
|||
import Virtualization
|
||||
|
||||
@available(macOS 13, *)
|
||||
struct Linux: Platform {
|
||||
struct Linux: PlatformSuspendable {
|
||||
|
||||
var machineIdentifier: VZGenericMachineIdentifier?
|
||||
|
||||
init(machineIdentifier: VZGenericMachineIdentifier = VZGenericMachineIdentifier()) {
|
||||
self.machineIdentifier = machineIdentifier
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
guard let encodedMachineIdentifier = try container.decodeIfPresent(
|
||||
String.self, forKey: .machineIdentifier
|
||||
) else {
|
||||
self.machineIdentifier = nil
|
||||
return
|
||||
}
|
||||
guard let data = Data.init(base64Encoded: encodedMachineIdentifier) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .machineIdentifier,
|
||||
in: container,
|
||||
debugDescription: "failed to initialize Data using the provided value")
|
||||
}
|
||||
guard let machineIdentifier = VZGenericMachineIdentifier.init(dataRepresentation: data) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .machineIdentifier,
|
||||
in: container,
|
||||
debugDescription: "failed to initialize VZGenericMachineIdentifier using the provided value")
|
||||
}
|
||||
self.machineIdentifier = machineIdentifier
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encodeIfPresent(machineIdentifier?.dataRepresentation.base64EncodedString(), forKey: .machineIdentifier)
|
||||
}
|
||||
|
||||
func os() -> OS {
|
||||
.linux
|
||||
}
|
||||
|
|
@ -16,6 +51,11 @@ struct Linux: Platform {
|
|||
|
||||
func platform(nvramURL: URL, needsNestedVirtualization: Bool) throws -> VZPlatformConfiguration {
|
||||
let config = VZGenericPlatformConfiguration()
|
||||
|
||||
if let machineIdentifier {
|
||||
config.machineIdentifier = machineIdentifier
|
||||
}
|
||||
|
||||
if #available(macOS 15, *) {
|
||||
config.isNestedVirtualizationEnabled = needsNestedVirtualization
|
||||
}
|
||||
|
|
@ -47,4 +87,14 @@ struct Linux: Platform {
|
|||
// Linux doesn't support trackpad, so just return the regular pointing devices
|
||||
return pointingDevices()
|
||||
}
|
||||
|
||||
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] {
|
||||
// VZUSBScreenCoordinatePointingDeviceConfiguration passes save/restore
|
||||
// validation, but causes restoring a Linux VM to fail with "invalid argument".
|
||||
[]
|
||||
}
|
||||
|
||||
func keyboardsSuspendable() -> [VZKeyboardConfiguration] {
|
||||
keyboards()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ enum CodingKeys: String, CodingKey {
|
|||
// macOS-specific keys
|
||||
case ecid
|
||||
case hardwareModel
|
||||
|
||||
// Linux-specific keys
|
||||
case machineIdentifier
|
||||
}
|
||||
|
||||
struct VMDisplayConfig: Codable, Equatable {
|
||||
|
|
|
|||
|
|
@ -198,6 +198,23 @@ struct VMDirectory: Prunable {
|
|||
try vmConfig.save(toURL: configURL)
|
||||
}
|
||||
|
||||
func initializeLinuxMachineIdentifier() throws {
|
||||
var vmConfig = try VMConfig(fromURL: configURL)
|
||||
guard var vmLinux = vmConfig.platform as? Linux else {
|
||||
throw RuntimeError.VMConfigurationError("cannot initialize a Linux machine identifier on a non-Linux VM")
|
||||
}
|
||||
guard vmLinux.machineIdentifier == nil else {
|
||||
throw RuntimeError.VMConfigurationError(
|
||||
"cannot initialize a Linux machine identifier when one already exists"
|
||||
)
|
||||
}
|
||||
|
||||
vmLinux.machineIdentifier = VZGenericMachineIdentifier()
|
||||
vmConfig.platform = vmLinux
|
||||
|
||||
try vmConfig.save(toURL: configURL)
|
||||
}
|
||||
|
||||
func resizeDisk(_ sizeGB: UInt16, format: DiskImageFormat = .raw) throws {
|
||||
let diskExists = FileManager.default.fileExists(atPath: diskURL.path)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import XCTest
|
||||
@testable import tart
|
||||
|
||||
import Foundation
|
||||
import Virtualization
|
||||
|
||||
final class VMConfigTests: XCTestCase {
|
||||
func testVMDisplayConfig() throws {
|
||||
// Defaults units (points)
|
||||
|
|
@ -15,4 +18,53 @@ final class VMConfigTests: XCTestCase {
|
|||
vmDisplayConfig = VMDisplayConfig.init(argument: "1234x5678px")
|
||||
XCTAssertEqual(VMDisplayConfig(width: 1234, height: 5678, unit: .pixel), vmDisplayConfig)
|
||||
}
|
||||
|
||||
func testLinuxMachineIdentifierSerialization() throws {
|
||||
let originalIdentifier = VZGenericMachineIdentifier()
|
||||
let originalConfig = VMConfig(
|
||||
platform: Linux(machineIdentifier: originalIdentifier), cpuCountMin: 2, memorySizeMin: 1024 * 1024 * 1024
|
||||
)
|
||||
let encodedConfigData = try originalConfig.toJSON()
|
||||
|
||||
let decodedConfig = try VMConfig(fromJSON: encodedConfigData)
|
||||
let decodedLinux = try XCTUnwrap(decodedConfig.platform as? Linux)
|
||||
let decodedMachineIdentifier = try XCTUnwrap(decodedLinux.machineIdentifier)
|
||||
|
||||
XCTAssertEqual(
|
||||
decodedMachineIdentifier.dataRepresentation,
|
||||
originalIdentifier.dataRepresentation,
|
||||
"decoded machine identifier should match original identifier"
|
||||
)
|
||||
|
||||
let platformConfiguration = try decodedLinux.platform(
|
||||
nvramURL: URL(fileURLWithPath: "/dev/null"),
|
||||
needsNestedVirtualization: false
|
||||
)
|
||||
let decodedPlatformConfiguration = try XCTUnwrap(
|
||||
platformConfiguration as? VZGenericPlatformConfiguration
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
decodedPlatformConfiguration.machineIdentifier.dataRepresentation,
|
||||
originalIdentifier.dataRepresentation,
|
||||
"platform configuration should reuse decoded machine identifier"
|
||||
)
|
||||
}
|
||||
|
||||
func testLegacyLinuxConfigWithoutMachineIdentifier() throws {
|
||||
let originalIdentifier = VZGenericMachineIdentifier()
|
||||
let originalConfig = VMConfig(
|
||||
platform: Linux(machineIdentifier: originalIdentifier), cpuCountMin: 2, memorySizeMin: 1024 * 1024 * 1024
|
||||
)
|
||||
let encodedConfigData = try originalConfig.toJSON()
|
||||
|
||||
var configJSONObject = try XCTUnwrap(JSONSerialization.jsonObject(with: encodedConfigData) as? [String: Any])
|
||||
configJSONObject.removeValue(forKey: "machineIdentifier")
|
||||
let legacyConfigData = try JSONSerialization.data(withJSONObject: configJSONObject)
|
||||
|
||||
let decodedConfig = try VMConfig(fromJSON: legacyConfigData)
|
||||
let decodedLinux = try XCTUnwrap(decodedConfig.platform as? Linux)
|
||||
|
||||
XCTAssertNil(decodedLinux.machineIdentifier, "missing machineIdentifier should be decoded as nil")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue