Skip to content

Commit

Permalink
Linux support (#190)
Browse files Browse the repository at this point in the history
* Linux support

* Support macOS 12

* Improve readability a bit

* Remove useless variable
  • Loading branch information
edigaryev authored Aug 23, 2022
1 parent 732c824 commit 59393df
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 82 deletions.
27 changes: 20 additions & 7 deletions Sources/tart/Commands/Create.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,39 @@ struct Create: AsyncParsableCommand {
@Argument(help: "VM name")
var name: String

@Option(help: ArgumentHelp("Path to the IPSW file (or \"latest\") to fetch the latest appropriate IPSW", valueName: "path"))
@Option(help: ArgumentHelp("create a macOS VM using path to the IPSW file (or \"latest\") to fetch the latest appropriate IPSW", valueName: "path"))
var fromIPSW: String?

@Flag(help: "create a Linux VM")
var linux: Bool = false

@Option(help: ArgumentHelp("Disk size in Gb"))
var diskSize: UInt16 = 50

func validate() throws {
if fromIPSW == nil {
throw ValidationError("Please specify a --from-ipsw option!")
if fromIPSW == nil && !linux {
throw ValidationError("Please specify either a --from-ipsw or --linux option!")
}
}

func run() async throws {
do {
let tmpVMDir = try VMDirectory.temporary()
try await withTaskCancellationHandler(operation: {
if fromIPSW! == "latest" {
_ = try await VM(vmDir: tmpVMDir, ipswURL: nil, diskSizeGB: diskSize)
} else {
_ = try await VM(vmDir: tmpVMDir, ipswURL: URL(fileURLWithPath: fromIPSW!), diskSizeGB: diskSize)
if let fromIPSW = fromIPSW {
if fromIPSW == "latest" {
_ = try await VM(vmDir: tmpVMDir, ipswURL: nil, diskSizeGB: diskSize)
} else {
_ = try await VM(vmDir: tmpVMDir, ipswURL: URL(fileURLWithPath: fromIPSW), diskSizeGB: diskSize)
}
}

if linux {
if #available(macOS 13, *) {
_ = try await VM.linux(vmDir: tmpVMDir, diskSizeGB: diskSize)
} else {
throw UnsupportedOSError()
}
}

try VMStorageLocal().move(name, from: tmpVMDir)
Expand Down
4 changes: 2 additions & 2 deletions Sources/tart/OCI/Manifest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ struct OCIManifest: Codable, Equatable {
}

struct OCIConfig: Codable {
var architecture: String = "arm64"
var os: String = "darwin"
var architecture: Architecture = .arm64
var os: OS = .darwin

func toJSON() throws -> Data {
try Config.jsonEncoder().encode(self)
Expand Down
14 changes: 14 additions & 0 deletions Sources/tart/Platform/Architecture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

enum Architecture: String, Codable {
case arm64
case amd64
}

func CurrentArchitecture() -> Architecture {
#if arch(arm64)
return .arm64
#elseif arch(x86_64)
return .amd64
#endif
}
87 changes: 87 additions & 0 deletions Sources/tart/Platform/Darwin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Virtualization

struct Darwin: Platform {
var ecid: VZMacMachineIdentifier
var hardwareModel: VZMacHardwareModel

init(ecid: VZMacMachineIdentifier, hardwareModel: VZMacHardwareModel) {
self.ecid = ecid
self.hardwareModel = hardwareModel
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let encodedECID = try container.decode(String.self, forKey: .ecid)
guard let data = Data.init(base64Encoded: encodedECID) else {
throw DecodingError.dataCorruptedError(forKey: .ecid,
in: container,
debugDescription: "failed to initialize Data using the provided value")
}
guard let ecid = VZMacMachineIdentifier.init(dataRepresentation: data) else {
throw DecodingError.dataCorruptedError(forKey: .ecid,
in: container,
debugDescription: "failed to initialize VZMacMachineIdentifier using the provided value")
}
self.ecid = ecid

let encodedHardwareModel = try container.decode(String.self, forKey: .hardwareModel)
guard let data = Data.init(base64Encoded: encodedHardwareModel) else {
throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "")
}
guard let hardwareModel = VZMacHardwareModel.init(dataRepresentation: data) else {
throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "")
}
self.hardwareModel = hardwareModel
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(ecid.dataRepresentation.base64EncodedString(), forKey: .ecid)
try container.encode(hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel)
}

func os() -> OS {
.darwin
}

func bootLoader(nvramURL: URL) throws -> VZBootLoader {
VZMacOSBootLoader()
}

func platform(nvramURL: URL) -> VZPlatformConfiguration {
let result = VZMacPlatformConfiguration()

result.machineIdentifier = ecid
result.auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: nvramURL)
result.hardwareModel = hardwareModel

return result
}

func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration {
let result = VZMacGraphicsDeviceConfiguration()

if let hostMainScreen = NSScreen.main {
let vmScreenSize = NSSize(width: vmConfig.display.width, height: vmConfig.display.height)
result.displays = [
VZMacGraphicsDisplayConfiguration(for: hostMainScreen, sizeInPoints: vmScreenSize)
]

return result
}

result.displays = [
VZMacGraphicsDisplayConfiguration(
widthInPixels: vmConfig.display.width,
heightInPixels: vmConfig.display.height,
// A reasonable guess according to Apple's documentation[1]
// [1]: https://developer.apple.com/documentation/coregraphics/1456599-cgdisplayscreensize
pixelsPerInch: 72
)
]

return result
}
}
33 changes: 33 additions & 0 deletions Sources/tart/Platform/Linux.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Virtualization

@available(macOS 13, *)
struct Linux: Platform {
func os() -> OS {
.linux
}

func bootLoader(nvramURL: URL) throws -> VZBootLoader {
let result = VZEFIBootLoader()

result.variableStore = VZEFIVariableStore(url: nvramURL)

return result
}

func platform(nvramURL: URL) -> VZPlatformConfiguration {
VZGenericPlatformConfiguration()
}

func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration {
let result = VZVirtioGraphicsDeviceConfiguration()

result.scanouts = [
VZVirtioGraphicsScanoutConfiguration(
widthInPixels: vmConfig.display.width,
heightInPixels: vmConfig.display.height
)
]

return result
}
}
6 changes: 6 additions & 0 deletions Sources/tart/Platform/OS.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Virtualization

enum OS: String, Codable {
case darwin
case linux
}
8 changes: 8 additions & 0 deletions Sources/tart/Platform/Platform.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Virtualization

protocol Platform: Codable {
func os() -> OS
func bootLoader(nvramURL: URL) throws -> VZBootLoader
func platform(nvramURL: URL) -> VZPlatformConfiguration
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration
}
72 changes: 36 additions & 36 deletions Sources/tart/VM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ struct NoMainScreenFoundError: Error {
struct DownloadFailed: Error {
}

struct UnsupportedOSError: Error {
}

struct UnsupportedArchitectureError: Error {
}

class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Virtualization.Framework's virtual machine
@Published var virtualMachine: VZVirtualMachine
Expand All @@ -29,17 +35,20 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
withSoftnet: Bool = false,
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = []
) throws {
let auxStorage = VZMacAuxiliaryStorage(contentsOf: vmDir.nvramURL)

name = vmDir.name
config = try VMConfig.init(fromURL: vmDir.configURL)

if config.arch != CurrentArchitecture() {
throw UnsupportedArchitectureError()
}

// Initialize the virtual machine and its configuration
if withSoftnet {
softnet = try Softnet(vmMACAddress: config.macAddress.string)
}

let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config,
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL,
nvramURL: vmDir.nvramURL, vmConfig: config,
softnet: softnet, additionalDiskAttachments: additionalDiskAttachments)
virtualMachine = VZVirtualMachine(configuration: configuration)

Expand Down Expand Up @@ -116,15 +125,15 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}

// Create NVRAM
let auxStorage = try VZMacAuxiliaryStorage(creatingStorageAt: vmDir.nvramURL, hardwareModel: requirements.hardwareModel)
_ = try VZMacAuxiliaryStorage(creatingStorageAt: vmDir.nvramURL, hardwareModel: requirements.hardwareModel)

// Create disk
try vmDir.resizeDisk(diskSizeGB)

name = vmDir.name
// Create config
config = VMConfig(
hardwareModel: requirements.hardwareModel,
platform: Darwin(ecid: VZMacMachineIdentifier(), hardwareModel: requirements.hardwareModel),
cpuCountMin: requirements.minimumSupportedCPUCount,
memorySizeMin: requirements.minimumSupportedMemorySize
)
Expand All @@ -137,8 +146,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
softnet = try Softnet(vmMACAddress: config.macAddress.string)
}

let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config,
softnet: softnet, additionalDiskAttachments: additionalDiskAttachments)
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL,
vmConfig: config, softnet: softnet,
additionalDiskAttachments: additionalDiskAttachments)
virtualMachine = VZVirtualMachine(configuration: configuration)

super.init()
Expand All @@ -159,6 +169,21 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
}

@available(macOS 13, *)
static func linux(vmDir: VMDirectory, diskSizeGB: UInt16) async throws -> VM {
// Create NVRAM
_ = try VZEFIVariableStore(creatingVariableStoreAt: vmDir.nvramURL)

// Create disk
try vmDir.resizeDisk(diskSizeGB)

// Create config
let config = VMConfig(platform: Linux(), cpuCountMin: 4, memorySizeMin: 4096 * 1024 * 1024)
try config.save(toURL: vmDir.configURL)

return try VM(vmDir: vmDir)
}

func run(_ recovery: Bool) async throws {
if let softnet = softnet {
try softnet.run()
Expand Down Expand Up @@ -199,50 +224,25 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {

static func craftConfiguration(
diskURL: URL,
auxStorage: VZMacAuxiliaryStorage,
nvramURL: URL,
vmConfig: VMConfig,
softnet: Softnet? = nil,
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment]
) throws -> VZVirtualMachineConfiguration {
let configuration = VZVirtualMachineConfiguration()

// Boot loader
configuration.bootLoader = VZMacOSBootLoader()
configuration.bootLoader = try vmConfig.platform.bootLoader(nvramURL: nvramURL)

// CPU and memory
configuration.cpuCount = vmConfig.cpuCount
configuration.memorySize = vmConfig.memorySize

// Platform
let platform = VZMacPlatformConfiguration()

platform.machineIdentifier = vmConfig.ecid
platform.auxiliaryStorage = auxStorage
platform.hardwareModel = vmConfig.hardwareModel

configuration.platform = platform
configuration.platform = vmConfig.platform.platform(nvramURL: nvramURL)

// Display
let graphicsDeviceConfiguration = VZMacGraphicsDeviceConfiguration()
if let hostMainScreen = NSScreen.main {
let vmScreenSize = NSSize(
width: vmConfig.display.width,
height: vmConfig.display.height
)
graphicsDeviceConfiguration.displays = [
VZMacGraphicsDisplayConfiguration(for: hostMainScreen, sizeInPoints: vmScreenSize)
]
} else {
graphicsDeviceConfiguration.displays = [
VZMacGraphicsDisplayConfiguration(
widthInPixels: vmConfig.display.width,
heightInPixels: vmConfig.display.height,
// Reasonable guess like https://developer.apple.com/documentation/coregraphics/1456599-cgdisplayscreensize
pixelsPerInch: 72
)
]
}
configuration.graphicsDevices = [graphicsDeviceConfiguration]
configuration.graphicsDevices = [vmConfig.platform.graphicsDevice(vmConfig: vmConfig)]

// Audio
let soundDeviceConfiguration = VZVirtioSoundDeviceConfiguration()
Expand Down
Loading

0 comments on commit 59393df

Please sign in to comment.