diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..9c71277
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,6 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+insert_final_newline = true
diff --git a/.run/tart run.run.xml b/.run/tart run.run.xml
new file mode 100644
index 0000000..cf7968e
--- /dev/null
+++ b/.run/tart run.run.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Package.resolved b/Package.resolved
index 01ca2af..5ca09fb 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
- "revision" : "e394bf350e38cb100b6bc4172834770ede1b7232",
- "version" : "1.0.3"
+ "revision" : "82905286cc3f0fa8adc4674bf49437cab65a8373",
+ "version" : "1.1.1"
}
}
],
diff --git a/Package.swift b/Package.swift
index f60c327..77df68e 100644
--- a/Package.swift
+++ b/Package.swift
@@ -3,17 +3,17 @@
import PackageDescription
let package = Package(
- name: "Tart",
- platforms: [
- .macOS(.v12)
- ],
- dependencies: [
- .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.3"),
- ],
- targets: [
- .executableTarget(name: "tart",
- dependencies: [
- .product(name: "ArgumentParser", package: "swift-argument-parser"),
- ]),
- ]
+ name: "Tart",
+ platforms: [
+ .macOS(.v12)
+ ],
+ dependencies: [
+ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.1"),
+ ],
+ targets: [
+ .executableTarget(name: "tart",
+ dependencies: [
+ .product(name: "ArgumentParser", package: "swift-argument-parser"),
+ ]),
+ ]
)
diff --git a/Sources/tart/ARP/ARPCache.swift b/Sources/tart/ARP/ARPCache.swift
index fbe764e..4d960cb 100644
--- a/Sources/tart/ARP/ARPCache.swift
+++ b/Sources/tart/ARP/ARPCache.swift
@@ -3,114 +3,114 @@ import Network
import Virtualization
struct ARPCommandFailedError: Error, CustomStringConvertible {
- var terminationReason: Process.TerminationReason
- var terminationStatus: Int32
-
- var description: String {
- var reason: String
-
- switch terminationReason {
- case .exit:
- reason = "exit code \(terminationStatus)"
- case .uncaughtSignal:
- reason = "uncaught signal"
- default:
- reason = "unknown reason"
- }
-
- return "arp command failed: \(reason)"
+ var terminationReason: Process.TerminationReason
+ var terminationStatus: Int32
+
+ var description: String {
+ var reason: String
+
+ switch terminationReason {
+ case .exit:
+ reason = "exit code \(terminationStatus)"
+ case .uncaughtSignal:
+ reason = "uncaught signal"
+ default:
+ reason = "unknown reason"
}
+
+ return "arp command failed: \(reason)"
+ }
}
struct ARPCommandYieldedInvalidOutputError: Error, CustomStringConvertible {
- var explanation: String
-
- var description: String {
- "arp command yielded invalid output: \(explanation)"
- }
+ var explanation: String
+
+ var description: String {
+ "arp command yielded invalid output: \(explanation)"
+ }
}
struct ARPCacheInternalError: Error, CustomStringConvertible {
- var explanation: String
-
- var description: String {
- "ARPCache internal error: \(explanation)"
- }
+ var explanation: String
+
+ var description: String {
+ "ARPCache internal error: \(explanation)"
+ }
}
struct ARPCache {
- static func ResolveMACAddress(macAddress: MACAddress, bridgeOnly: Bool = true) throws -> IPv4Address? {
- let process = Process.init()
- process.executableURL = URL.init(fileURLWithPath: "/usr/sbin/arp")
- process.arguments = ["-an"]
-
- let pipe = Pipe()
- process.standardOutput = pipe
- process.standardError = pipe
- process.standardInput = FileHandle.nullDevice
-
- try process.run()
- process.waitUntilExit()
-
- if !(process.terminationReason == .exit && process.terminationStatus == 0) {
- throw ARPCommandFailedError(
- terminationReason: process.terminationReason,
- terminationStatus: process.terminationStatus)
- }
-
- guard let rawLines = try pipe.fileHandleForReading.readToEnd() else {
- throw ARPCommandYieldedInvalidOutputError(explanation: "empty output")
- }
- let lines = String(decoding: rawLines, as: UTF8.self)
+ static func ResolveMACAddress(macAddress: MACAddress, bridgeOnly: Bool = true) throws -> IPv4Address? {
+ let process = Process.init()
+ process.executableURL = URL.init(fileURLWithPath: "/usr/sbin/arp")
+ process.arguments = ["-an"]
+
+ let pipe = Pipe()
+ process.standardOutput = pipe
+ process.standardError = pipe
+ process.standardInput = FileHandle.nullDevice
+
+ try process.run()
+ process.waitUntilExit()
+
+ if !(process.terminationReason == .exit && process.terminationStatus == 0) {
+ throw ARPCommandFailedError(
+ terminationReason: process.terminationReason,
+ terminationStatus: process.terminationStatus)
+ }
+
+ guard let rawLines = try pipe.fileHandleForReading.readToEnd() else {
+ throw ARPCommandYieldedInvalidOutputError(explanation: "empty output")
+ }
+ let lines = String(decoding: rawLines, as: UTF8.self)
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: "\n")
-
- // Based on https://opensource.apple.com/source/network_cmds/network_cmds-606.40.2/arp.tproj/arp.c.auto.html
- let regex = try NSRegularExpression(pattern: #"^.* \((?.*)\) at (?.*) on (?.*) .*$"#)
-
- for line in lines {
- let nsLineRange = NSRange(line.startIndex...*)\) at (?.*) on (?.*) .*$"#)
- let rawMAC = try match.getCaptureGroup(name: "mac", for: line)
- guard let mac = MACAddress(fromString: rawMAC) else {
- throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse MAC address \(rawMAC)")
- }
+ for line in lines {
+ let nsLineRange = NSRange(line.startIndex.. String {
- let nsRange = self.range(withName: name)
-
- if nsRange.location == NSNotFound {
- throw ARPCacheInternalError(explanation: "attempted to retrieve non-existent named capture group \(name)")
- }
-
- guard let range = Range.init(nsRange, in: string) else {
- throw ARPCacheInternalError(explanation: "failed to convert NSRange to Range")
- }
-
- return String(string[range])
+ func getCaptureGroup(name: String, for string: String) throws -> String {
+ let nsRange = self.range(withName: name)
+
+ if nsRange.location == NSNotFound {
+ throw ARPCacheInternalError(explanation: "attempted to retrieve non-existent named capture group \(name)")
}
+
+ guard let range = Range.init(nsRange, in: string) else {
+ throw ARPCacheInternalError(explanation: "failed to convert NSRange to Range")
+ }
+
+ return String(string[range])
+ }
}
diff --git a/Sources/tart/ARP/MACAddress.swift b/Sources/tart/ARP/MACAddress.swift
index c5c507a..e41017c 100644
--- a/Sources/tart/ARP/MACAddress.swift
+++ b/Sources/tart/ARP/MACAddress.swift
@@ -1,21 +1,21 @@
import Foundation
struct MACAddress: Equatable, CustomStringConvertible {
- var mac: [UInt8] = Array(repeating: 0, count: 6)
-
- init?(fromString: String) {
- let components = fromString.components(separatedBy: ":")
-
- if components.count != 6 {
- return nil
- }
-
- for (index, component) in components.enumerated() {
- mac[index] = UInt8(component, radix: 16)!
- }
+ var mac: [UInt8] = Array(repeating: 0, count: 6)
+
+ init?(fromString: String) {
+ let components = fromString.components(separatedBy: ":")
+
+ if components.count != 6 {
+ return nil
}
-
- var description: String {
- return String(format: "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
+
+ for (index, component) in components.enumerated() {
+ mac[index] = UInt8(component, radix: 16)!
}
+ }
+
+ var description: String {
+ return String(format: "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
+ }
}
diff --git a/Sources/tart/Commands/Clone.swift b/Sources/tart/Commands/Clone.swift
index 6872ad2..7642919 100644
--- a/Sources/tart/Commands/Clone.swift
+++ b/Sources/tart/Commands/Clone.swift
@@ -3,38 +3,34 @@ import Foundation
import SystemConfiguration
import Virtualization
-struct Clone: ParsableCommand {
- static var configuration = CommandConfiguration(abstract: "Clone a VM")
-
- @Argument(help: "source VM name")
- var sourceName: String
-
- @Argument(help: "new VM name")
- var newName: String
-
- func run() throws {
- Task {
- do {
- let vmStorage = VMStorage()
- let sourceVMDir = try vmStorage.read(sourceName)
- let newVMDir = try vmStorage.create(newName)
-
- try FileManager.default.copyItem(at: sourceVMDir.configURL, to: newVMDir.configURL)
- try FileManager.default.copyItem(at: sourceVMDir.nvramURL, to: newVMDir.nvramURL)
- try FileManager.default.copyItem(at: sourceVMDir.diskURL, to: newVMDir.diskURL)
-
- var newVMConfig = try VMConfig(fromURL: newVMDir.configURL)
- newVMConfig.macAddress = VZMACAddress.randomLocallyAdministered()
- try newVMConfig.save(toURL: newVMDir.configURL)
-
- Foundation.exit(0)
- } catch {
- print(error)
-
- Foundation.exit(1)
- }
- }
-
- dispatchMain()
+struct Clone: AsyncParsableCommand {
+ static var configuration = CommandConfiguration(abstract: "Clone a VM")
+
+ @Argument(help: "source VM name")
+ var sourceName: String
+
+ @Argument(help: "new VM name")
+ var newName: String
+
+ func run() async throws {
+ do {
+ let vmStorage = VMStorage()
+ let sourceVMDir = try vmStorage.read(sourceName)
+ let newVMDir = try vmStorage.create(newName)
+
+ try FileManager.default.copyItem(at: sourceVMDir.configURL, to: newVMDir.configURL)
+ try FileManager.default.copyItem(at: sourceVMDir.nvramURL, to: newVMDir.nvramURL)
+ try FileManager.default.copyItem(at: sourceVMDir.diskURL, to: newVMDir.diskURL)
+
+ var newVMConfig = try VMConfig(fromURL: newVMDir.configURL)
+ newVMConfig.macAddress = VZMACAddress.randomLocallyAdministered()
+ try newVMConfig.save(toURL: newVMDir.configURL)
+
+ Foundation.exit(0)
+ } catch {
+ print(error)
+
+ Foundation.exit(1)
}
+ }
}
diff --git a/Sources/tart/Commands/Create.swift b/Sources/tart/Commands/Create.swift
index d179af4..7509430 100644
--- a/Sources/tart/Commands/Create.swift
+++ b/Sources/tart/Commands/Create.swift
@@ -3,39 +3,35 @@ import Dispatch
import SwiftUI
import Foundation
-struct Create: ParsableCommand {
- static var configuration = CommandConfiguration(abstract: "Create a VM")
-
- @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")) var fromIPSW: String?
-
- func validate() throws {
- if fromIPSW == nil {
- throw ValidationError("Please specify a --from-ipsw option!")
- }
+struct Create: AsyncParsableCommand {
+ static var configuration = CommandConfiguration(abstract: "Create a VM")
+
+ @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")) var fromIPSW: String?
+
+ func validate() throws {
+ if fromIPSW == nil {
+ throw ValidationError("Please specify a --from-ipsw option!")
}
-
- func run() throws {
- Task {
- do {
- let vmDir = try VMStorage().create(name)
-
- if fromIPSW! == "latest" {
- _ = try await VM(vmDir: vmDir, ipswURL: nil)
- } else {
- _ = try await VM(vmDir: vmDir, ipswURL: URL(fileURLWithPath: fromIPSW!))
- }
-
- Foundation.exit(0)
- } catch {
- print(error)
-
- Foundation.exit(1)
- }
- }
-
- dispatchMain()
+ }
+
+ func run() async throws {
+ do {
+ let vmDir = try VMStorage().create(name)
+
+ if fromIPSW! == "latest" {
+ _ = try await VM(vmDir: vmDir, ipswURL: nil)
+ } else {
+ _ = try await VM(vmDir: vmDir, ipswURL: URL(fileURLWithPath: fromIPSW!))
+ }
+
+ Foundation.exit(0)
+ } catch {
+ print(error)
+
+ Foundation.exit(1)
}
+ }
}
diff --git a/Sources/tart/Commands/Delete.swift b/Sources/tart/Commands/Delete.swift
index 7b6fe6d..3964742 100644
--- a/Sources/tart/Commands/Delete.swift
+++ b/Sources/tart/Commands/Delete.swift
@@ -2,25 +2,21 @@ import ArgumentParser
import Dispatch
import SwiftUI
-struct Delete: ParsableCommand {
- static var configuration = CommandConfiguration(abstract: "Delete a VM")
-
- @Argument(help: "VM name")
- var name: String
-
- func run() throws {
- Task {
- do {
- try VMStorage().delete(name)
-
- Foundation.exit(0)
- } catch {
- print(error)
-
- Foundation.exit(1)
- }
- }
-
- dispatchMain()
+struct Delete: AsyncParsableCommand {
+ static var configuration = CommandConfiguration(abstract: "Delete a VM")
+
+ @Argument(help: "VM name")
+ var name: String
+
+ func run() async throws {
+ do {
+ try VMStorage().delete(name)
+
+ Foundation.exit(0)
+ } catch {
+ print(error)
+
+ Foundation.exit(1)
}
+ }
}
diff --git a/Sources/tart/Commands/IP.swift b/Sources/tart/Commands/IP.swift
index 31fa364..c17bc58 100644
--- a/Sources/tart/Commands/IP.swift
+++ b/Sources/tart/Commands/IP.swift
@@ -2,35 +2,31 @@ import ArgumentParser
import Foundation
import SystemConfiguration
-struct IP: ParsableCommand {
- static var configuration = CommandConfiguration(abstract: "Get VM's IP address")
-
- @Argument(help: "VM name")
- var name: String
-
- func run() throws {
- Task {
- do {
- let vmDir = try VMStorage().read(name)
- let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
- let vmMacAddress = MACAddress(fromString: vmConfig.macAddress.string)!
-
- guard let ip = try ARPCache.ResolveMACAddress(macAddress: vmMacAddress) else {
- print("no IP address found, is your VM running?")
-
- Foundation.exit(1)
- }
-
- print(ip)
+struct IP: AsyncParsableCommand {
+ static var configuration = CommandConfiguration(abstract: "Get VM's IP address")
- Foundation.exit(0)
- } catch {
- print(error)
-
- Foundation.exit(1)
- }
- }
-
- dispatchMain()
+ @Argument(help: "VM name")
+ var name: String
+
+ func run() async throws {
+ do {
+ let vmDir = try VMStorage().read(name)
+ let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
+ let vmMacAddress = MACAddress(fromString: vmConfig.macAddress.string)!
+
+ guard let ip = try ARPCache.ResolveMACAddress(macAddress: vmMacAddress) else {
+ print("no IP address found, is your VM running?")
+
+ Foundation.exit(1)
+ }
+
+ print(ip)
+
+ Foundation.exit(0)
+ } catch {
+ print(error)
+
+ Foundation.exit(1)
}
+ }
}
diff --git a/Sources/tart/Commands/List.swift b/Sources/tart/Commands/List.swift
index ab7c634..fb35701 100644
--- a/Sources/tart/Commands/List.swift
+++ b/Sources/tart/Commands/List.swift
@@ -2,24 +2,20 @@ import ArgumentParser
import Dispatch
import SwiftUI
-struct List: ParsableCommand {
- static var configuration = CommandConfiguration(abstract: "List created VMs")
-
- func run() throws {
- Task {
- do {
- for vmURL in try VMStorage().list() {
- print(vmURL)
- }
-
- Foundation.exit(0)
- } catch {
- print(error)
-
- Foundation.exit(1)
- }
- }
-
- dispatchMain()
+struct List: AsyncParsableCommand {
+ static var configuration = CommandConfiguration(abstract: "List created VMs")
+
+ func run() async throws {
+ do {
+ for vmURL in try VMStorage().list() {
+ print(vmURL)
+ }
+
+ Foundation.exit(0)
+ } catch {
+ print(error)
+
+ Foundation.exit(1)
}
+ }
}
diff --git a/Sources/tart/Commands/Root.swift b/Sources/tart/Commands/Root.swift
deleted file mode 100644
index cac6230..0000000
--- a/Sources/tart/Commands/Root.swift
+++ /dev/null
@@ -1,7 +0,0 @@
-import ArgumentParser
-
-struct Root: ParsableCommand {
- static var configuration = CommandConfiguration(
- commandName: "tart",
- subcommands: [Create.self, Clone.self, Run.self, List.self, IP.self, Delete.self])
-}
diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift
index 7340384..70aa462 100644
--- a/Sources/tart/Commands/Run.swift
+++ b/Sources/tart/Commands/Run.swift
@@ -5,61 +5,61 @@ import Virtualization
var vm: VM?
-struct Run: ParsableCommand {
- static var configuration = CommandConfiguration(abstract: "Run a VM")
-
- @Argument(help: "VM name")
- var name: String
-
- @Flag var noGraphics: Bool = false
-
- func run() throws {
- let vmDir = try VMStorage().read(name)
- vm = try VM(vmDir: vmDir)
-
- Task {
- do {
- try await vm!.run()
-
- Foundation.exit(0)
- } catch {
- print(error)
-
- Foundation.exit(1)
- }
- }
-
- if noGraphics {
- dispatchMain()
- } else {
- // UI mumbo-jumbo
- let nsApp = NSApplication.shared
- nsApp.setActivationPolicy(.regular)
- nsApp.activate(ignoringOtherApps: true)
-
- struct MainApp : App {
- var body: some Scene {
- WindowGroup {
- VMView(vm: vm!)
- }
- }
- }
-
- MainApp.main()
- }
+struct Run: AsyncParsableCommand {
+ static var configuration = CommandConfiguration(abstract: "Run a VM")
+
+ @Argument(help: "VM name")
+ var name: String
+
+ @Flag var noGraphics: Bool = false
+
+ func run() async throws {
+ let vmDir = try VMStorage().read(name)
+ vm = try VM(vmDir: vmDir)
+
+ Task {
+ do {
+ try await vm!.run()
+
+ Foundation.exit(0)
+ } catch {
+ print(error)
+
+ Foundation.exit(1)
+ }
}
+
+ if noGraphics {
+ dispatchMain()
+ } else {
+ // UI mumbo-jumbo
+ let nsApp = await NSApplication.shared
+ await nsApp.setActivationPolicy(.regular)
+ await nsApp.activate(ignoringOtherApps: true)
+
+ struct MainApp: App {
+ var body: some Scene {
+ WindowGroup {
+ VMView(vm: vm!)
+ }
+ }
+ }
+
+ await MainApp.main()
+ }
+ }
}
struct VMView: NSViewRepresentable {
- typealias NSViewType = VZVirtualMachineView
-
- @ObservedObject var vm: VM
-
- func makeNSView(context: Context) -> NSViewType {
- VZVirtualMachineView()
- }
-
- func updateNSView(_ nsView: NSViewType, context: Context) {
- nsView.virtualMachine = vm.virtualMachine
- }
+ typealias NSViewType = VZVirtualMachineView
+
+ @ObservedObject var vm: VM
+
+ func makeNSView(context: Context) -> NSViewType {
+ VZVirtualMachineView()
+ }
+
+ func updateNSView(_ nsView: NSViewType, context: Context) {
+ nsView.virtualMachine = vm.virtualMachine
+ }
}
diff --git a/Sources/tart/Logging/Logger.swift b/Sources/tart/Logging/Logger.swift
index a14a2ee..5ac3138 100644
--- a/Sources/tart/Logging/Logger.swift
+++ b/Sources/tart/Logging/Logger.swift
@@ -1,46 +1,46 @@
import Foundation
public protocol Logger {
- func appendNewLine(_ line: String) -> Void
- func updateLastLine(_ line: String) -> Void
+ func appendNewLine(_ line: String) -> Void
+ func updateLastLine(_ line: String) -> Void
}
var defaultLogger: Logger = {
- if ProcessInfo.processInfo.environment["CI"] != nil {
- return SimpleConsoleLogger()
- } else {
- return InteractiveConsoleLogger()
- }
+ if ProcessInfo.processInfo.environment["CI"] != nil {
+ return SimpleConsoleLogger()
+ } else {
+ return InteractiveConsoleLogger()
+ }
}()
public class InteractiveConsoleLogger: Logger {
- private let eraseCursorDown = "\u{001B}[J" // clear entire line
- private let moveUp = "\u{001B}[1A" // move one line up
- private let moveBeginningOfLine = "\r" //
+ private let eraseCursorDown = "\u{001B}[J" // clear entire line
+ private let moveUp = "\u{001B}[1A" // move one line up
+ private let moveBeginningOfLine = "\r" //
- public init() {
+ public init() {
- }
+ }
- public func appendNewLine(_ line: String) {
- print(line, terminator: "\n")
- }
+ public func appendNewLine(_ line: String) {
+ print(line, terminator: "\n")
+ }
- public func updateLastLine(_ line: String) {
- print(moveUp, moveBeginningOfLine, eraseCursorDown, line, separator: "", terminator: "\n")
- }
+ public func updateLastLine(_ line: String) {
+ print(moveUp, moveBeginningOfLine, eraseCursorDown, line, separator: "", terminator: "\n")
+ }
}
public class SimpleConsoleLogger: Logger {
- public init() {
+ public init() {
- }
+ }
- public func appendNewLine(_ line: String) {
- print(line, terminator: "\n")
- }
+ public func appendNewLine(_ line: String) {
+ print(line, terminator: "\n")
+ }
- public func updateLastLine(_ line: String) {
- print(line, terminator: "\n")
- }
+ public func updateLastLine(_ line: String) {
+ print(line, terminator: "\n")
+ }
}
diff --git a/Sources/tart/Logging/ProgressObserver.swift b/Sources/tart/Logging/ProgressObserver.swift
index 729bf70..79e6e23 100644
--- a/Sources/tart/Logging/ProgressObserver.swift
+++ b/Sources/tart/Logging/ProgressObserver.swift
@@ -1,21 +1,21 @@
import Foundation
public class ProgressObserver: NSObject {
- @objc var progressToObserve: Progress
- var observation: NSKeyValueObservation?
+ @objc var progressToObserve: Progress
+ var observation: NSKeyValueObservation?
- public init(_ progress: Progress) {
- progressToObserve = progress
- }
-
- func log(_ renderer: Logger) {
- renderer.appendNewLine(ProgressObserver.lineToRender(progressToObserve))
- observation = observe(\.progressToObserve.fractionCompleted) { progress, _ in
- renderer.updateLastLine(ProgressObserver.lineToRender(self.progressToObserve))
- }
- }
+ public init(_ progress: Progress) {
+ progressToObserve = progress
+ }
- private static func lineToRender(_ progress: Progress) -> String {
- String(Int(100 * progress.fractionCompleted)) + "%"
+ func log(_ renderer: Logger) {
+ renderer.appendNewLine(ProgressObserver.lineToRender(progressToObserve))
+ observation = observe(\.progressToObserve.fractionCompleted) { progress, _ in
+ renderer.updateLastLine(ProgressObserver.lineToRender(self.progressToObserve))
}
+ }
+
+ private static func lineToRender(_ progress: Progress) -> String {
+ String(Int(100 * progress.fractionCompleted)) + "%"
+ }
}
diff --git a/Sources/tart/Logging/URLSessionLogger.swift b/Sources/tart/Logging/URLSessionLogger.swift
index 240d316..afd94b2 100644
--- a/Sources/tart/Logging/URLSessionLogger.swift
+++ b/Sources/tart/Logging/URLSessionLogger.swift
@@ -1,17 +1,17 @@
import Foundation
public class URLSessionLogger: NSObject, URLSessionTaskDelegate {
- let renderer: Logger
+ let renderer: Logger
- public init(_ renderer: Logger) {
- self.renderer = renderer
- }
+ public init(_ renderer: Logger) {
+ self.renderer = renderer
+ }
- public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
- renderer.updateLastLine(URLSessionLogger.lineToRender(task.progress))
- }
+ public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
+ renderer.updateLastLine(URLSessionLogger.lineToRender(task.progress))
+ }
- private static func lineToRender(_ progress: Progress) -> String {
- String(100 * progress.completedUnitCount / progress.totalUnitCount) + "%"
- }
+ private static func lineToRender(_ progress: Progress) -> String {
+ String(100 * progress.completedUnitCount / progress.totalUnitCount) + "%"
+ }
}
diff --git a/Sources/tart/Root.swift b/Sources/tart/Root.swift
new file mode 100644
index 0000000..a813b05
--- /dev/null
+++ b/Sources/tart/Root.swift
@@ -0,0 +1,8 @@
+import ArgumentParser
+
+@main
+struct Root: AsyncParsableCommand {
+ static var configuration = CommandConfiguration(
+ commandName: "tart",
+ subcommands: [Create.self, Clone.self, Run.self, List.self, IP.self, Delete.self])
+}
diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift
index 81b68aa..c2e20c1 100644
--- a/Sources/tart/VM.swift
+++ b/Sources/tart/VM.swift
@@ -1,222 +1,235 @@
import Foundation
import Virtualization
-struct UnsupportedRestoreImageError: Error {}
-struct NoMainScreenFoundError: Error {}
-struct DownloadFailed: Error {}
+struct UnsupportedRestoreImageError: Error {
+}
+
+struct NoMainScreenFoundError: Error {
+}
+
+struct DownloadFailed: Error {
+}
class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
- // Virtualization.Framework's virtual machine
- @Published var virtualMachine: VZVirtualMachine
-
- // Semaphore used to communicate with the VZVirtualMachineDelegate
- var sema = DispatchSemaphore(value: 0)
-
- // VM's config
- var vmConfig: VMConfig
-
- init(vmDir: VMDirectory) throws {
- let auxStorage = VZMacAuxiliaryStorage(contentsOf: vmDir.nvramURL)
-
- self.vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
-
- let configuration = try VM.craftConfiguration(
- diskURL: vmDir.diskURL,
- ecid: vmConfig.ecid,
- auxStorage: auxStorage,
- hardwareModel: vmConfig.hardwareModel,
- cpuCount: vmConfig.cpuCount,
- memorySize: vmConfig.memorySize,
- macAddress: vmConfig.macAddress
- )
-
- self.virtualMachine = VZVirtualMachine(configuration: configuration)
-
- super.init()
-
- self.virtualMachine.delegate = self
+ // Virtualization.Framework's virtual machine
+ @Published var virtualMachine: VZVirtualMachine
+
+ // Semaphore used to communicate with the VZVirtualMachineDelegate
+ var sema = DispatchSemaphore(value: 0)
+
+ // VM's config
+ var vmConfig: VMConfig
+
+ init(vmDir: VMDirectory) throws {
+ let auxStorage = VZMacAuxiliaryStorage(contentsOf: vmDir.nvramURL)
+
+ self.vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
+
+ let configuration = try VM.craftConfiguration(
+ diskURL: vmDir.diskURL,
+ ecid: vmConfig.ecid,
+ auxStorage: auxStorage,
+ hardwareModel: vmConfig.hardwareModel,
+ cpuCount: vmConfig.cpuCount,
+ memorySize: vmConfig.memorySize,
+ macAddress: vmConfig.macAddress
+ )
+
+ self.virtualMachine = VZVirtualMachine(configuration: configuration)
+
+ super.init()
+
+ self.virtualMachine.delegate = self
+ }
+
+ static func retrieveLatestIPSW() async throws -> URL {
+ defaultLogger.appendNewLine("Looking up the latest supported IPSW...")
+ let image = try await withCheckedThrowingContinuation { continuation in
+ VZMacOSRestoreImage.fetchLatestSupported() { result in
+ continuation.resume(with: result)
+ }
}
-
- static func retrieveLatestIPSW() async throws -> URL {
- defaultLogger.appendNewLine("Looking up the latest supported IPSW...")
- let image = try await withCheckedThrowingContinuation { continuation in
- VZMacOSRestoreImage.fetchLatestSupported() { result in continuation.resume(with: result) }
- }
-
- let ipswCacheFolder = VMStorage.tartCacheDir.appendingPathComponent("IPSWs", isDirectory: true)
- try FileManager.default.createDirectory(at: ipswCacheFolder, withIntermediateDirectories: true)
-
- let expectedIPSWLocation = ipswCacheFolder.appendingPathComponent("\(image.buildVersion).ipsw", isDirectory: false)
- if FileManager.default.fileExists(atPath: expectedIPSWLocation.path) {
- defaultLogger.appendNewLine("Using cached *.ipsw file...")
- return expectedIPSWLocation
- }
+ let ipswCacheFolder = VMStorage.tartCacheDir.appendingPathComponent("IPSWs", isDirectory: true)
+ try FileManager.default.createDirectory(at: ipswCacheFolder, withIntermediateDirectories: true)
- defaultLogger.appendNewLine("Fetching \(expectedIPSWLocation.lastPathComponent)...")
+ let expectedIPSWLocation = ipswCacheFolder.appendingPathComponent("\(image.buildVersion).ipsw", isDirectory: false)
- let data: Data = try await withCheckedThrowingContinuation { continuation in
- let downloadedTask = URLSession.shared.dataTask(with: image.url) { data, response, error in
+ if FileManager.default.fileExists(atPath: expectedIPSWLocation.path) {
+ defaultLogger.appendNewLine("Using cached *.ipsw file...")
+ return expectedIPSWLocation
+ }
+
+ defaultLogger.appendNewLine("Fetching \(expectedIPSWLocation.lastPathComponent)...")
+
+ let data: Data = try await withCheckedThrowingContinuation { continuation in
+ let downloadedTask = URLSession.shared.dataTask(with: image.url) { data, response, error in
if error != nil {
- continuation.resume(throwing: error!)
- return
- }
+ continuation.resume(throwing: error!)
+ return
+ }
if (data == nil) {
- continuation.resume(throwing: DownloadFailed())
- return
+ continuation.resume(throwing: DownloadFailed())
+ return
}
continuation.resume(returning: data!)
- }
- ProgressObserver(downloadedTask.progress).log(defaultLogger)
- downloadedTask.resume()
- }
-
- try data.write(to: expectedIPSWLocation, options: [.atomic])
- return expectedIPSWLocation
+ }
+ ProgressObserver(downloadedTask.progress).log(defaultLogger)
+ downloadedTask.resume()
}
-
- init(vmDir: VMDirectory, ipswURL: URL?, diskSize: UInt64 = 32 * 1024 * 1024 * 1024) async throws {
- let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW();
-
- // Load the restore image and try to get the requirements
- // that match both the image and our platform
- let image = try await withCheckedThrowingContinuation { continuation in
- VZMacOSRestoreImage.load(from: ipswURL) { result in continuation.resume(with: result) }
- }
-
- guard let requirements = image.mostFeaturefulSupportedConfiguration else { throw UnsupportedRestoreImageError() }
-
- // Create NVRAM
- let auxStorage = try VZMacAuxiliaryStorage(creatingStorageAt: vmDir.nvramURL, hardwareModel: requirements.hardwareModel)
-
- // Create disk
- FileManager.default.createFile(atPath: vmDir.diskURL.path, contents: nil, attributes: nil)
- let diskFileHandle = try FileHandle.init(forWritingTo: vmDir.diskURL)
- try diskFileHandle.truncate(atOffset: diskSize)
- try diskFileHandle.close()
-
- // Create config
- self.vmConfig = VMConfig(
- hardwareModel: requirements.hardwareModel,
- cpuCount: requirements.minimumSupportedCPUCount,
- memorySize: requirements.minimumSupportedMemorySize
- )
- try self.vmConfig.save(toURL: vmDir.configURL)
-
- // Initialize the virtual machine and its configuration
- let configuration = try VM.craftConfiguration(
- diskURL: vmDir.diskURL,
- ecid: self.vmConfig.ecid,
- auxStorage: auxStorage,
- hardwareModel: requirements.hardwareModel,
- cpuCount: self.vmConfig.cpuCount,
- memorySize: self.vmConfig.memorySize,
- macAddress: self.vmConfig.macAddress
- )
- self.virtualMachine = VZVirtualMachine(configuration: configuration)
-
- super.init()
-
- self.virtualMachine.delegate = self
-
- // Run automated installation
- try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
- DispatchQueue.main.async {
+
+ try data.write(to: expectedIPSWLocation, options: [.atomic])
+ return expectedIPSWLocation
+ }
+
+ init(vmDir: VMDirectory, ipswURL: URL?, diskSize: UInt64 = 32 * 1024 * 1024 * 1024) async throws {
+ let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW();
+
+ // Load the restore image and try to get the requirements
+ // that match both the image and our platform
+ let image = try await withCheckedThrowingContinuation { continuation in
+ VZMacOSRestoreImage.load(from: ipswURL) { result in
+ continuation.resume(with: result)
+ }
+ }
+
+ guard let requirements = image.mostFeaturefulSupportedConfiguration else {
+ throw UnsupportedRestoreImageError()
+ }
+
+ // Create NVRAM
+ let auxStorage = try VZMacAuxiliaryStorage(creatingStorageAt: vmDir.nvramURL, hardwareModel: requirements.hardwareModel)
+
+ // Create disk
+ FileManager.default.createFile(atPath: vmDir.diskURL.path, contents: nil, attributes: nil)
+ let diskFileHandle = try FileHandle.init(forWritingTo: vmDir.diskURL)
+ try diskFileHandle.truncate(atOffset: diskSize)
+ try diskFileHandle.close()
+
+ // Create config
+ self.vmConfig = VMConfig(
+ hardwareModel: requirements.hardwareModel,
+ cpuCount: requirements.minimumSupportedCPUCount,
+ memorySize: requirements.minimumSupportedMemorySize
+ )
+ try self.vmConfig.save(toURL: vmDir.configURL)
+
+ // Initialize the virtual machine and its configuration
+ let configuration = try VM.craftConfiguration(
+ diskURL: vmDir.diskURL,
+ ecid: self.vmConfig.ecid,
+ auxStorage: auxStorage,
+ hardwareModel: requirements.hardwareModel,
+ cpuCount: self.vmConfig.cpuCount,
+ memorySize: self.vmConfig.memorySize,
+ macAddress: self.vmConfig.macAddress
+ )
+ self.virtualMachine = VZVirtualMachine(configuration: configuration)
+
+ super.init()
+
+ self.virtualMachine.delegate = self
+
+ // Run automated installation
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+ DispatchQueue.main.async {
let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL)
defaultLogger.appendNewLine("Installing OS...")
- ProgressObserver(installer.progress).log(defaultLogger)
-
- installer.install { result in continuation.resume(with: result) }
- }
- }
+ ProgressObserver(installer.progress).log(defaultLogger)
+
+ installer.install { result in
+ continuation.resume(with: result)
+ }
+ }
}
-
- func run() async throws {
- try await withCheckedThrowingContinuation { continuation in
- DispatchQueue.main.async {
+ }
+
+ func run() async throws {
+ try await withCheckedThrowingContinuation { continuation in
+ DispatchQueue.main.async {
self.virtualMachine.start(completionHandler: { result in
- continuation.resume(with: result)
- })
- }
- }
-
- sema.wait()
+ continuation.resume(with: result)
+ })
+ }
}
-
- static func craftConfiguration(
- diskURL: URL,
- ecid: VZMacMachineIdentifier,
- auxStorage: VZMacAuxiliaryStorage,
- hardwareModel: VZMacHardwareModel,
- cpuCount: Int,
- memorySize: UInt64,
- macAddress: VZMACAddress
- ) throws -> VZVirtualMachineConfiguration {
- let configuration = VZVirtualMachineConfiguration()
-
- // Boot loader
- configuration.bootLoader = VZMacOSBootLoader()
-
- // CPU and memory
- configuration.cpuCount = cpuCount
- configuration.memorySize = memorySize
-
- // Platform
- let platform = VZMacPlatformConfiguration()
-
- platform.machineIdentifier = ecid
- platform.auxiliaryStorage = auxStorage
- platform.hardwareModel = hardwareModel
-
- configuration.platform = platform
-
- // Display
- let graphicsDeviceConfiguration = VZMacGraphicsDeviceConfiguration()
- guard let mainScreen = NSScreen.main else {
- throw NoMainScreenFoundError()
- }
- graphicsDeviceConfiguration.displays = [
- VZMacGraphicsDisplayConfiguration(for: mainScreen, sizeInPoints: mainScreen.frame.size)
- ]
- configuration.graphicsDevices = [graphicsDeviceConfiguration]
-
- // Keyboard and mouse
- configuration.keyboards = [VZUSBKeyboardConfiguration()]
- configuration.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]
-
- // Networking
- let vio = VZVirtioNetworkDeviceConfiguration()
- vio.attachment = VZNATNetworkDeviceAttachment()
- vio.macAddress = macAddress
- configuration.networkDevices = [vio]
-
- // Storage
- let attachment = try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)
- let storage = VZVirtioBlockDeviceConfiguration(attachment: attachment)
- configuration.storageDevices = [storage]
-
- // Entropy
- configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
-
- try configuration.validate()
-
- return configuration
- }
-
- func guestDidStop(_ virtualMachine: VZVirtualMachine) {
- print("guest has stopped the virtual machine")
- sema.signal()
- }
-
- func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
- print("guest has stopped the virtual machine due to error")
- sema.signal()
- }
-
- func virtualMachine(_ virtualMachine: VZVirtualMachine, networkDevice: VZNetworkDevice, attachmentWasDisconnectedWithError error: Error) {
- print("virtual machine's network attachment has been disconnected")
- sema.signal()
+
+ sema.wait()
+ }
+
+ static func craftConfiguration(
+ diskURL: URL,
+ ecid: VZMacMachineIdentifier,
+ auxStorage: VZMacAuxiliaryStorage,
+ hardwareModel: VZMacHardwareModel,
+ cpuCount: Int,
+ memorySize: UInt64,
+ macAddress: VZMACAddress
+ ) throws -> VZVirtualMachineConfiguration {
+ let configuration = VZVirtualMachineConfiguration()
+
+ // Boot loader
+ configuration.bootLoader = VZMacOSBootLoader()
+
+ // CPU and memory
+ configuration.cpuCount = cpuCount
+ configuration.memorySize = memorySize
+
+ // Platform
+ let platform = VZMacPlatformConfiguration()
+
+ platform.machineIdentifier = ecid
+ platform.auxiliaryStorage = auxStorage
+ platform.hardwareModel = hardwareModel
+
+ configuration.platform = platform
+
+ // Display
+ let graphicsDeviceConfiguration = VZMacGraphicsDeviceConfiguration()
+ guard let mainScreen = NSScreen.main else {
+ throw NoMainScreenFoundError()
}
+ graphicsDeviceConfiguration.displays = [
+ VZMacGraphicsDisplayConfiguration(for: mainScreen, sizeInPoints: mainScreen.frame.size)
+ ]
+ configuration.graphicsDevices = [graphicsDeviceConfiguration]
+
+ // Keyboard and mouse
+ configuration.keyboards = [VZUSBKeyboardConfiguration()]
+ configuration.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]
+
+ // Networking
+ let vio = VZVirtioNetworkDeviceConfiguration()
+ vio.attachment = VZNATNetworkDeviceAttachment()
+ vio.macAddress = macAddress
+ configuration.networkDevices = [vio]
+
+ // Storage
+ let attachment = try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)
+ let storage = VZVirtioBlockDeviceConfiguration(attachment: attachment)
+ configuration.storageDevices = [storage]
+
+ // Entropy
+ configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
+
+ try configuration.validate()
+
+ return configuration
+ }
+
+ func guestDidStop(_ virtualMachine: VZVirtualMachine) {
+ print("guest has stopped the virtual machine")
+ sema.signal()
+ }
+
+ func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
+ print("guest has stopped the virtual machine due to error")
+ sema.signal()
+ }
+
+ func virtualMachine(_ virtualMachine: VZVirtualMachine, networkDevice: VZNetworkDevice, attachmentWasDisconnectedWithError error: Error) {
+ print("virtual machine's network attachment has been disconnected")
+ sema.signal()
+ }
}
diff --git a/Sources/tart/VMConfig.swift b/Sources/tart/VMConfig.swift
index cc7f08d..4ff0cdf 100644
--- a/Sources/tart/VMConfig.swift
+++ b/Sources/tart/VMConfig.swift
@@ -1,96 +1,96 @@
import Virtualization
enum CodingKeys: String, CodingKey {
- case version
- case ecid
- case hardwareModel
- case cpuCount
- case memorySize
- case macAddress
+ case version
+ case ecid
+ case hardwareModel
+ case cpuCount
+ case memorySize
+ case macAddress
}
struct VMConfig: Encodable, Decodable {
- var version: Int = 0
- var ecid: VZMacMachineIdentifier
- var hardwareModel: VZMacHardwareModel
- var cpuCount: Int
- var memorySize: UInt64
- var macAddress: VZMACAddress
-
- init(
- ecid: VZMacMachineIdentifier = VZMacMachineIdentifier(),
- hardwareModel: VZMacHardwareModel,
- cpuCount: Int,
- memorySize: UInt64,
- macAddress: VZMACAddress = VZMACAddress.randomLocallyAdministered()
- ) {
- self.ecid = ecid
- self.hardwareModel = hardwareModel
- self.cpuCount = cpuCount
- self.memorySize = memorySize
- self.macAddress = macAddress
+ var version: Int = 0
+ var ecid: VZMacMachineIdentifier
+ var hardwareModel: VZMacHardwareModel
+ var cpuCount: Int
+ var memorySize: UInt64
+ var macAddress: VZMACAddress
+
+ init(
+ ecid: VZMacMachineIdentifier = VZMacMachineIdentifier(),
+ hardwareModel: VZMacHardwareModel,
+ cpuCount: Int,
+ memorySize: UInt64,
+ macAddress: VZMACAddress = VZMACAddress.randomLocallyAdministered()
+ ) {
+ self.ecid = ecid
+ self.hardwareModel = hardwareModel
+ self.cpuCount = cpuCount
+ self.memorySize = memorySize
+ self.macAddress = macAddress
+ }
+
+ init(fromURL: URL) throws {
+ let jsonConfigData = try FileHandle.init(forReadingFrom: fromURL).readToEnd()!
+ self = try JSONDecoder().decode(VMConfig.self, from: jsonConfigData)
+ }
+
+ func save(toURL: URL) throws {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ try encoder.encode(self).write(to: toURL)
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ self.version = try container.decode(Int.self, forKey: .version)
+
+ 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")
}
-
- init(fromURL: URL) throws {
- let jsonConfigData = try FileHandle.init(forReadingFrom: fromURL).readToEnd()!
- self = try JSONDecoder().decode(VMConfig.self, from: jsonConfigData)
+ guard let ecid = VZMacMachineIdentifier.init(dataRepresentation: data) else {
+ throw DecodingError.dataCorruptedError(forKey: .ecid,
+ in: container,
+ debugDescription: "failed to initialize VZMacMachineIdentifier using the provided value")
}
-
- func save(toURL: URL) throws {
- let encoder = JSONEncoder()
- encoder.outputFormatting = .prettyPrinted
- try encoder.encode(self).write(to: toURL)
+ 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: "")
}
-
- init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
-
- self.version = try container.decode(Int.self, forKey: .version)
-
- 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
-
- self.cpuCount = try container.decode(Int.self, forKey: .cpuCount)
-
- self.memorySize = try container.decode(UInt64.self, forKey: .memorySize)
-
- let encodedMacAddress = try container.decode(String.self, forKey: .macAddress)
- guard let macAddress = VZMACAddress.init(string: encodedMacAddress) else {
- throw DecodingError.dataCorruptedError(
+ guard let hardwareModel = VZMacHardwareModel.init(dataRepresentation: data) else {
+ throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "")
+ }
+ self.hardwareModel = hardwareModel
+
+ self.cpuCount = try container.decode(Int.self, forKey: .cpuCount)
+
+ self.memorySize = try container.decode(UInt64.self, forKey: .memorySize)
+
+ let encodedMacAddress = try container.decode(String.self, forKey: .macAddress)
+ guard let macAddress = VZMACAddress.init(string: encodedMacAddress) else {
+ throw DecodingError.dataCorruptedError(
forKey: .hardwareModel,
in: container,
debugDescription: "failed to initialize VZMacAddress using the provided value")
- }
- self.macAddress = macAddress
- }
-
- func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
-
- try container.encode(self.version, forKey: .version)
- try container.encode(self.ecid.dataRepresentation.base64EncodedString(), forKey: .ecid)
- try container.encode(self.hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel)
- try container.encode(self.cpuCount, forKey: .cpuCount)
- try container.encode(self.memorySize, forKey: .memorySize)
- try container.encode(self.macAddress.string, forKey: .macAddress)
}
+ self.macAddress = macAddress
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+
+ try container.encode(self.version, forKey: .version)
+ try container.encode(self.ecid.dataRepresentation.base64EncodedString(), forKey: .ecid)
+ try container.encode(self.hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel)
+ try container.encode(self.cpuCount, forKey: .cpuCount)
+ try container.encode(self.memorySize, forKey: .memorySize)
+ try container.encode(self.macAddress.string, forKey: .macAddress)
+ }
}
diff --git a/Sources/tart/VMDirectory.swift b/Sources/tart/VMDirectory.swift
index ff9de0f..acfc4c4 100644
--- a/Sources/tart/VMDirectory.swift
+++ b/Sources/tart/VMDirectory.swift
@@ -1,32 +1,41 @@
import Foundation
-struct UninitializedVMDirectoryError: Error {}
-struct AlreadyInitializedVMDirectoryError: Error {}
+struct UninitializedVMDirectoryError: Error {
+}
+
+struct AlreadyInitializedVMDirectoryError: Error {
+}
struct VMDirectory {
- var baseURL: URL
-
- var configURL: URL { self.baseURL.appendingPathComponent("config.json") }
- var diskURL: URL { self.baseURL.appendingPathComponent("disk.bin") }
- var nvramURL: URL { self.baseURL.appendingPathComponent("nvram.bin") }
-
- var initialized: Bool {
- FileManager.default.fileExists(atPath: configURL.path) &&
- FileManager.default.fileExists(atPath: diskURL.path) &&
- FileManager.default.fileExists(atPath: nvramURL.path)
+ var baseURL: URL
+
+ var configURL: URL {
+ self.baseURL.appendingPathComponent("config.json")
+ }
+ var diskURL: URL {
+ self.baseURL.appendingPathComponent("disk.bin")
+ }
+ var nvramURL: URL {
+ self.baseURL.appendingPathComponent("nvram.bin")
+ }
+
+ var initialized: Bool {
+ FileManager.default.fileExists(atPath: configURL.path) &&
+ FileManager.default.fileExists(atPath: diskURL.path) &&
+ FileManager.default.fileExists(atPath: nvramURL.path)
+ }
+
+ func initialize() throws {
+ if initialized {
+ throw AlreadyInitializedVMDirectoryError()
}
-
- func initialize() throws {
- if initialized {
- throw AlreadyInitializedVMDirectoryError()
- }
-
- try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true, attributes: nil)
- }
-
- func validate() throws {
- if !initialized {
- throw UninitializedVMDirectoryError()
- }
+
+ try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true, attributes: nil)
+ }
+
+ func validate() throws {
+ if !initialized {
+ throw UninitializedVMDirectoryError()
}
+ }
}
diff --git a/Sources/tart/VMStorage.swift b/Sources/tart/VMStorage.swift
index 2e3b658..546ddf7 100644
--- a/Sources/tart/VMStorage.swift
+++ b/Sources/tart/VMStorage.swift
@@ -1,58 +1,58 @@
import Foundation
struct VMStorage {
- public static let tartHomeDir: URL = FileManager.default
- .homeDirectoryForCurrentUser
- .appendingPathComponent(".tart", isDirectory: true)
-
- public static let tartVMsDir: URL = tartHomeDir.appendingPathComponent("vms", isDirectory: true)
- public static let tartCacheDir: URL = tartHomeDir.appendingPathComponent("cache", isDirectory: true)
-
- func create(_ name: String) throws -> VMDirectory {
- let vmDir = VMDirectory(baseURL: vmURL(name))
-
- try vmDir.initialize()
-
- return vmDir
- }
-
- func read(_ name: String) throws -> VMDirectory {
- let vmDir = VMDirectory(baseURL: vmURL(name))
-
- try vmDir.validate()
-
- return vmDir
- }
-
- func delete(_ name: String) throws {
- try FileManager.default.removeItem(at: vmURL(name))
- }
-
- func list() throws -> [URL] {
- do {
- return try FileManager.default.contentsOfDirectory(
+ public static let tartHomeDir: URL = FileManager.default
+ .homeDirectoryForCurrentUser
+ .appendingPathComponent(".tart", isDirectory: true)
+
+ public static let tartVMsDir: URL = tartHomeDir.appendingPathComponent("vms", isDirectory: true)
+ public static let tartCacheDir: URL = tartHomeDir.appendingPathComponent("cache", isDirectory: true)
+
+ func create(_ name: String) throws -> VMDirectory {
+ let vmDir = VMDirectory(baseURL: vmURL(name))
+
+ try vmDir.initialize()
+
+ return vmDir
+ }
+
+ func read(_ name: String) throws -> VMDirectory {
+ let vmDir = VMDirectory(baseURL: vmURL(name))
+
+ try vmDir.validate()
+
+ return vmDir
+ }
+
+ func delete(_ name: String) throws {
+ try FileManager.default.removeItem(at: vmURL(name))
+ }
+
+ func list() throws -> [URL] {
+ do {
+ return try FileManager.default.contentsOfDirectory(
at: VMStorage.tartVMsDir,
includingPropertiesForKeys: [.isDirectoryKey],
options: .skipsSubdirectoryDescendants)
- } catch {
- if error.isFileNotFound() {
- return []
- }
-
- throw error
- }
- }
-
- private func vmURL(_ name: String) -> URL {
- return URL.init(
- fileURLWithPath: name,
- isDirectory: true,
- relativeTo: VMStorage.tartVMsDir)
+ } catch {
+ if error.isFileNotFound() {
+ return []
+ }
+
+ throw error
}
+ }
+
+ private func vmURL(_ name: String) -> URL {
+ return URL.init(
+ fileURLWithPath: name,
+ isDirectory: true,
+ relativeTo: VMStorage.tartVMsDir)
+ }
}
extension Error {
- func isFileNotFound() -> Bool {
- return (self as NSError).code == NSFileReadNoSuchFileError
- }
+ func isFileNotFound() -> Bool {
+ return (self as NSError).code == NSFileReadNoSuchFileError
+ }
}
diff --git a/Sources/tart/main.swift b/Sources/tart/main.swift
deleted file mode 100644
index 480b247..0000000
--- a/Sources/tart/main.swift
+++ /dev/null
@@ -1,3 +0,0 @@
-import SwiftUI
-
-Root.main()