From 49d430b3c4c15d660f5511615f74044b3bb26e54 Mon Sep 17 00:00:00 2001 From: Fedor Korotkov Date: Thu, 24 Mar 2022 11:54:49 -0400 Subject: [PATCH] Update dependencies and styles (#12) * Update deps and .editorconfig * run config --- .editorconfig | 6 + .run/tart run.run.xml | 8 + Package.resolved | 4 +- Package.swift | 26 +- Sources/tart/ARP/ARPCache.swift | 180 ++++----- Sources/tart/ARP/MACAddress.swift | 30 +- Sources/tart/Commands/Clone.swift | 62 ++- Sources/tart/Commands/Create.swift | 62 ++- Sources/tart/Commands/Delete.swift | 36 +- Sources/tart/Commands/IP.swift | 54 ++- Sources/tart/Commands/List.swift | 34 +- Sources/tart/Commands/Root.swift | 7 - Sources/tart/Commands/Run.swift | 106 ++--- Sources/tart/Logging/Logger.swift | 52 +-- Sources/tart/Logging/ProgressObserver.swift | 28 +- Sources/tart/Logging/URLSessionLogger.swift | 20 +- Sources/tart/Root.swift | 8 + Sources/tart/VM.swift | 411 ++++++++++---------- Sources/tart/VMConfig.swift | 164 ++++---- Sources/tart/VMDirectory.swift | 59 +-- Sources/tart/VMStorage.swift | 94 ++--- Sources/tart/main.swift | 3 - 22 files changed, 734 insertions(+), 720 deletions(-) create mode 100644 .editorconfig create mode 100644 .run/tart run.run.xml delete mode 100644 Sources/tart/Commands/Root.swift create mode 100644 Sources/tart/Root.swift delete mode 100644 Sources/tart/main.swift 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()