diff --git a/.cirrus.yml b/.cirrus.yml index 98c046f..3602118 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -22,6 +22,18 @@ task: path: "integration-tests/pytest-junit.xml" format: junit +task: + name: Lint + alias: lint + macos_instance: + image: ghcr.io/cirruslabs/macos-ventura-xcode:latest + lint_script: + - swift package plugin --allow-writing-to-package-directory swiftformat --cache ignore --lint --report swiftformat.json . + always: + swiftformat_report_artifacts: + path: swiftformat.json + format: swiftformat + task: name: Build alias: build @@ -37,6 +49,7 @@ task: name: Release only_if: $CIRRUS_TAG != '' depends_on: + - lint - test - build macos_instance: diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..743e500 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,5 @@ +--disable all +--enable indent +--indent 2 +--exclude Sources/tart/OCI/Reference/Generated +--swiftversion 5.7 diff --git a/Package.resolved b/Package.resolved index 8ff9b80..2961e1b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -98,6 +98,15 @@ "revision" : "6190d0cefff3013e77ed567e6b074f324e5c5bf5", "version" : "6.3.1" } + }, + { + "identity" : "swiftformat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/SwiftFormat", + "state" : { + "revision" : "7c7dd06554a5fc3f452f1d16a780a81a3be04bce", + "version" : "0.50.4" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index f199157..5cf714b 100644 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,7 @@ let package = Package( .package(url: "https://github.com/sushichop/Puppy", from: "0.5.1"), .package(url: "https://github.com/antlr/antlr4", branch: "dev"), .package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.4"), ], targets: [ .executableTarget(name: "tart", dependencies: [ diff --git a/Sources/tart/Commands/Login.swift b/Sources/tart/Commands/Login.swift index 0490dbd..0bd4992 100644 --- a/Sources/tart/Commands/Login.swift +++ b/Sources/tart/Commands/Login.swift @@ -45,7 +45,7 @@ struct Login: AsyncParsableCommand { do { let registry = try Registry(host: host, namespace: "", insecure: insecure, - credentialsProviders: [credentialsProvider]) + credentialsProviders: [credentialsProvider]) try await registry.ping() } catch { print("invalid credentials: \(error)") diff --git a/Sources/tart/Commands/Prune.swift b/Sources/tart/Commands/Prune.swift index 8b978e9..feab283 100644 --- a/Sources/tart/Commands/Prune.swift +++ b/Sources/tart/Commands/Prune.swift @@ -7,13 +7,13 @@ struct Prune: AsyncParsableCommand { static var configuration = CommandConfiguration(abstract: "Prune OCI and IPSW caches") @Option(help: ArgumentHelp("Remove cache entries last accessed more than n days ago", - discussion: "For example, --older-than=7 will remove entries that weren't accessed by Tart in the last 7 days.", - valueName: "n")) + discussion: "For example, --older-than=7 will remove entries that weren't accessed by Tart in the last 7 days.", + valueName: "n")) var olderThan: UInt? @Option(help: ArgumentHelp("Remove least recently used cache entries that do not fit the specified cache size budget n, expressed in gigabytes", - discussion: "For example, --cache-budget=50 will effectively shrink all caches to a total size of 50 gigabytes.", - valueName: "n")) + discussion: "For example, --cache-budget=50 will effectively shrink all caches to a total size of 50 gigabytes.", + valueName: "n")) var cacheBudget: UInt? @Flag(help: .hidden) @@ -62,8 +62,8 @@ struct Prune: AsyncParsableCommand { static func pruneCacheBudget(cacheBudgetBytes: UInt64) throws { let prunableStorages: [PrunableStorage] = [VMStorageOCI(), try IPSWCache()] let prunables: [Prunable] = try prunableStorages - .flatMap { try $0.prunables() } - .sorted { try $0.accessDate() > $1.accessDate() } + .flatMap { try $0.prunables() } + .sorted { try $0.accessDate() > $1.accessDate() } var cacheBudgetBytes = cacheBudgetBytes var prunablesToDelete: [Prunable] = [] @@ -87,8 +87,8 @@ struct Prune: AsyncParsableCommand { static func pruneReclaim(reclaimBytes: UInt64) throws { let prunableStorages: [PrunableStorage] = [VMStorageOCI(), try IPSWCache()] let prunables: [Prunable] = try prunableStorages - .flatMap { try $0.prunables() } - .sorted { try $0.accessDate() < $1.accessDate() } + .flatMap { try $0.prunables() } + .sorted { try $0.accessDate() < $1.accessDate() } // Does it even make sense to start? let cacheUsedBytes = try prunables.map { try $0.sizeBytes() }.reduce(0, +) diff --git a/Sources/tart/Commands/Push.swift b/Sources/tart/Commands/Push.swift index 0b1f721..b7ef60c 100644 --- a/Sources/tart/Commands/Push.swift +++ b/Sources/tart/Commands/Push.swift @@ -16,15 +16,15 @@ struct Push: AsyncParsableCommand { var insecure: Bool = false @Option(help: ArgumentHelp("chunk size in MB if registry supports chunked uploads", - discussion: """ - By default monolithic method is used for uploading blobs to the registry but some registries support a more efficient chunked method. - For example, AWS Elastic Container Registry supports only chunks larger than 5MB but GitHub Container Registry supports only chunks smaller than 4MB. Google Container Registry on the other hand doesn't support chunked uploads at all. - Please refer to the documentation of your particular registry in order to see if this option is suitable for you and what's the recommended chunk size. - """)) + discussion: """ + By default monolithic method is used for uploading blobs to the registry but some registries support a more efficient chunked method. + For example, AWS Elastic Container Registry supports only chunks larger than 5MB but GitHub Container Registry supports only chunks smaller than 4MB. Google Container Registry on the other hand doesn't support chunked uploads at all. + Please refer to the documentation of your particular registry in order to see if this option is suitable for you and what's the recommended chunk size. + """)) var chunkSize: Int = 0 @Flag(help: ArgumentHelp("cache pushed images locally", - discussion: "Increases disk usage, but saves time if you're going to pull the pushed images later.")) + discussion: "Increases disk usage, but saves time if you're going to pull the pushed images later.")) var populateCache: Bool = false func run() async throws { @@ -49,7 +49,7 @@ struct Push: AsyncParsableCommand { // Push VM for (registryIdentifier, remoteNamesForRegistry) in registryGroups { let registry = try Registry(host: registryIdentifier.host, namespace: registryIdentifier.namespace, - insecure: insecure) + insecure: insecure) defaultLogger.appendNewLine("pushing \(localName) to " + "\(registryIdentifier.host)/\(registryIdentifier.namespace)\(remoteNamesForRegistry.referenceNames())...") diff --git a/Sources/tart/Commands/Rename.swift b/Sources/tart/Commands/Rename.swift index 8caccfe..bc9be3d 100644 --- a/Sources/tart/Commands/Rename.swift +++ b/Sources/tart/Commands/Rename.swift @@ -2,39 +2,39 @@ import ArgumentParser import Foundation struct Rename: AsyncParsableCommand { - static var configuration = CommandConfiguration(abstract: "Rename a VM") + static var configuration = CommandConfiguration(abstract: "Rename a VM") - @Argument(help: "VM name") - var name: String + @Argument(help: "VM name") + var name: String - @Argument(help: "new VM name") - var newName: String + @Argument(help: "new VM name") + var newName: String - func validate() throws { - if newName.contains("/") { - throw ValidationError(" should be a local name") - } + func validate() throws { + if newName.contains("/") { + throw ValidationError(" should be a local name") } + } - func run() async throws { - do { - let localStorage = VMStorageLocal() + func run() async throws { + do { + let localStorage = VMStorageLocal() - if !localStorage.exists(name) { - throw ValidationError("failed to rename a non-existent VM: \(name)") - } + if !localStorage.exists(name) { + throw ValidationError("failed to rename a non-existent VM: \(name)") + } - if localStorage.exists(newName) { - throw ValidationError("failed to rename VM \(name), target VM \(name) already exists, delete it first!") - } + if localStorage.exists(newName) { + throw ValidationError("failed to rename VM \(name), target VM \(name) already exists, delete it first!") + } - try localStorage.rename(name, newName) + try localStorage.rename(name, newName) - Foundation.exit(0) - } catch { - print(error) + Foundation.exit(0) + } catch { + print(error) - Foundation.exit(1) - } + Foundation.exit(1) } + } } diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index 7f421ee..b222e92 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -15,8 +15,8 @@ struct Run: AsyncParsableCommand { var name: String @Flag(help: ArgumentHelp( - "Don't open a UI window.", - discussion: "Useful for integrating Tart VMs into other tools.\nUse `tart ip` in order to get an IP for SSHing or VNCing into the VM.")) + "Don't open a UI window.", + discussion: "Useful for integrating Tart VMs into other tools.\nUse `tart ip` in order to get an IP for SSHing or VNCing into the VM.")) var noGraphics: Bool = false @Flag(help: "Force open a UI window, even when VNC is enabled.") @@ -24,11 +24,11 @@ struct Run: AsyncParsableCommand { @Flag(help: "Boot into recovery mode") var recovery: Bool = false - + @Flag(help: ArgumentHelp( - "Use screen sharing instead of the built-in UI.", - discussion: "Useful since Screen Sharing supports copy/paste, drag and drop, etc.\n" - + "Note that Remote Login option should be enabled inside the VM.")) + "Use screen sharing instead of the built-in UI.", + discussion: "Useful since Screen Sharing supports copy/paste, drag and drop, etc.\n" + + "Note that Remote Login option should be enabled inside the VM.")) var vnc: Bool = false @Flag(help: ArgumentHelp( @@ -41,46 +41,46 @@ struct Run: AsyncParsableCommand { var withSoftnet: Bool = false @Option(help: ArgumentHelp(""" - Additional disk attachments with an optional read-only specifier\n(e.g. --disk=\"disk.bin\" --disk=\"ubuntu.iso:ro\") - """, discussion: """ - Learn how to create a disk image using Disk Utility here: - https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac - """, valueName: "path[:ro]")) + Additional disk attachments with an optional read-only specifier\n(e.g. --disk=\"disk.bin\" --disk=\"ubuntu.iso:ro\") + """, discussion: """ + Learn how to create a disk image using Disk Utility here: + https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac + """, valueName: "path[:ro]")) var disk: [String] = [] @Option(name: [.customLong("rosetta")], help: ArgumentHelp( "Attaches a Rosetta share to the guest Linux VM with a specific tag (e.g. --rosetta=\"rosetta\")", discussion: """ - Requires host to be macOS 13.0 (Ventura) with Rosetta installed. The latter can be done - by running "softwareupdate --install-rosetta" (without quotes) in the Terminal.app. + Requires host to be macOS 13.0 (Ventura) with Rosetta installed. The latter can be done + by running "softwareupdate --install-rosetta" (without quotes) in the Terminal.app. - Note that you also have to configure Rosetta in the guest Linux VM by following the - steps from "Mount the Shared Directory and Register Rosetta" section here: - https://developer.apple.com/documentation/virtualization/running_intel_binaries_in_linux_vms_with_rosetta#3978496 - """, + Note that you also have to configure Rosetta in the guest Linux VM by following the + steps from "Mount the Shared Directory and Register Rosetta" section here: + https://developer.apple.com/documentation/virtualization/running_intel_binaries_in_linux_vms_with_rosetta#3978496 + """, valueName: "tag" )) var rosettaTag: String? @Option(help: ArgumentHelp(""" - Additional directory shares with an optional read-only specifier\n(e.g. --dir=\"build:~/src/build\" --dir=\"sources:~/src/sources:ro\") - """, discussion: """ - Requires host to be macOS 13.0 (Ventura) or newer. - All shared directories are automatically mounted to "/Volumes/My Shared Files" directory on macOS, - while on Linux you have to do it manually: "mount -t virtiofs com.apple.virtio-fs.automount /mount/point". - For macOS guests, they must be running macOS 13.0 (Ventura) or newer. - """, valueName: "name:path[:ro]")) + Additional directory shares with an optional read-only specifier\n(e.g. --dir=\"build:~/src/build\" --dir=\"sources:~/src/sources:ro\") + """, discussion: """ + Requires host to be macOS 13.0 (Ventura) or newer. + All shared directories are automatically mounted to "/Volumes/My Shared Files" directory on macOS, + while on Linux you have to do it manually: "mount -t virtiofs com.apple.virtio-fs.automount /mount/point". + For macOS guests, they must be running macOS 13.0 (Ventura) or newer. + """, valueName: "name:path[:ro]")) var dir: [String] = [] @Option(help: ArgumentHelp(""" - Use bridged networking instead of the default shared (NAT) networking \n(e.g. --net-bridged=en0 or --net-bridged=\"Wi-Fi\") - """, discussion: """ - Specify "list" as an interface name (--net-bridged=list) to list the available bridged interfaces. - """, valueName: "interface name")) + Use bridged networking instead of the default shared (NAT) networking \n(e.g. --net-bridged=en0 or --net-bridged=\"Wi-Fi\") + """, discussion: """ + Specify "list" as an interface name (--net-bridged=list) to list the available bridged interfaces. + """, valueName: "interface name")) var netBridged: String? @Flag(help: ArgumentHelp("Use software networking instead of the default shared (NAT) networking", - discussion: "Learn how to configure Softnet for use with Tart here: https://github.com/cirruslabs/softnet")) + discussion: "Learn how to configure Softnet for use with Tart here: https://github.com/cirruslabs/softnet")) var netSoftnet: Bool = false func validate() throws { @@ -357,16 +357,16 @@ struct Run: AsyncParsableCommand { } }.frame(width: CGFloat(vm!.config.display.width), height: CGFloat(vm!.config.display.height)) }.commands { - // Remove some standard menu options - CommandGroup(replacing: .help, addition: {}) - CommandGroup(replacing: .newItem, addition: {}) - CommandGroup(replacing: .pasteboard, addition: {}) - CommandGroup(replacing: .textEditing, addition: {}) - CommandGroup(replacing: .undoRedo, addition: {}) - CommandGroup(replacing: .windowSize, addition: {}) - // Replace some standard menu options - CommandGroup(replacing: .appInfo) { AboutTart() } - } + // Remove some standard menu options + CommandGroup(replacing: .help, addition: {}) + CommandGroup(replacing: .newItem, addition: {}) + CommandGroup(replacing: .pasteboard, addition: {}) + CommandGroup(replacing: .textEditing, addition: {}) + CommandGroup(replacing: .undoRedo, addition: {}) + CommandGroup(replacing: .windowSize, addition: {}) + // Replace some standard menu options + CommandGroup(replacing: .appInfo) { AboutTart() } + } } } diff --git a/Sources/tart/Config.swift b/Sources/tart/Config.swift index 34799c2..05b295f 100644 --- a/Sources/tart/Config.swift +++ b/Sources/tart/Config.swift @@ -12,8 +12,8 @@ struct Config { tartHomeDir = URL(fileURLWithPath: customTartHome) } else { tartHomeDir = FileManager.default - .homeDirectoryForCurrentUser - .appendingPathComponent(".tart", isDirectory: true) + .homeDirectoryForCurrentUser + .appendingPathComponent(".tart", isDirectory: true) } self.tartHomeDir = tartHomeDir @@ -26,7 +26,7 @@ struct Config { func gc() throws { for entry in try FileManager.default.contentsOfDirectory(at: tartTmpDir, - includingPropertiesForKeys: [], options: []) { + includingPropertiesForKeys: [], options: []) { let lock = try FileLock(lockURL: entry) if try !lock.trylock() { continue diff --git a/Sources/tart/Credentials/CredentialsProvider.swift b/Sources/tart/Credentials/CredentialsProvider.swift index 8f12fe0..5da19bc 100644 --- a/Sources/tart/Credentials/CredentialsProvider.swift +++ b/Sources/tart/Credentials/CredentialsProvider.swift @@ -1,10 +1,10 @@ import Foundation enum CredentialsProviderError: Error { - case Failed(message: String) + case Failed(message: String) } protocol CredentialsProvider { - func retrieve(host: String) throws -> (String, String)? - func store(host: String, user: String, password: String) throws + func retrieve(host: String) throws -> (String, String)? + func store(host: String, user: String, password: String) throws } diff --git a/Sources/tart/Credentials/DockerConfigCredentialsProvider.swift b/Sources/tart/Credentials/DockerConfigCredentialsProvider.swift index e32ed34..1e46d03 100644 --- a/Sources/tart/Credentials/DockerConfigCredentialsProvider.swift +++ b/Sources/tart/Credentials/DockerConfigCredentialsProvider.swift @@ -22,14 +22,14 @@ class DockerConfigCredentialsProvider: CredentialsProvider { guard let executableURL = resolveBinaryPath(binaryName) else { throw CredentialsProviderError.Failed(message: "\(binaryName) not found in PATH") } - + let process = Process.init() process.executableURL = executableURL process.arguments = ["get"] let outPipe = Pipe() let inPipe = Pipe() - + process.standardOutput = outPipe process.standardError = outPipe process.standardInput = inPipe @@ -38,7 +38,7 @@ class DockerConfigCredentialsProvider: CredentialsProvider { inPipe.fileHandleForWriting.write("\(host)\n".data(using: .utf8)!) inPipe.fileHandleForWriting.closeFile() - + process.waitUntilExit() if !(process.terminationReason == .exit && process.terminationStatus == 0) { diff --git a/Sources/tart/Credentials/KeychainCredentialsProvider.swift b/Sources/tart/Credentials/KeychainCredentialsProvider.swift index 3b64441..fc76634 100644 --- a/Sources/tart/Credentials/KeychainCredentialsProvider.swift +++ b/Sources/tart/Credentials/KeychainCredentialsProvider.swift @@ -1,66 +1,66 @@ import Foundation class KeychainCredentialsProvider: CredentialsProvider { - func retrieve(host: String) throws -> (String, String)? { - let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, - kSecAttrProtocol as String: kSecAttrProtocolHTTPS, - kSecAttrServer as String: host, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecReturnAttributes as String: true, - kSecReturnData as String: true, - kSecAttrLabel as String: "Tart Credentials", - ] + func retrieve(host: String) throws -> (String, String)? { + let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + kSecAttrProtocol as String: kSecAttrProtocolHTTPS, + kSecAttrServer as String: host, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecReturnAttributes as String: true, + kSecReturnData as String: true, + kSecAttrLabel as String: "Tart Credentials", + ] - var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) - if status != errSecSuccess { - if status == errSecItemNotFound { - return nil - } + if status != errSecSuccess { + if status == errSecItemNotFound { + return nil + } - throw CredentialsProviderError.Failed(message: "Keychain returned unsuccessful status \(status)") - } - - guard let item = item as? [String: Any], - let user = item[kSecAttrAccount as String] as? String, - let passwordData = item[kSecValueData as String] as? Data, - let password = String(data: passwordData, encoding: .utf8) - else { - throw CredentialsProviderError.Failed(message: "Keychain item has unexpected format") - } - - return (user, password) + throw CredentialsProviderError.Failed(message: "Keychain returned unsuccessful status \(status)") } - func store(host: String, user: String, password: String) throws { - let passwordData = password.data(using: .utf8) - let key: [String: Any] = [kSecClass as String: kSecClassInternetPassword, - kSecAttrProtocol as String: kSecAttrProtocolHTTPS, - kSecAttrServer as String: host, - kSecAttrLabel as String: "Tart Credentials", - ] - let value: [String: Any] = [kSecAttrAccount as String: user, - kSecValueData as String: passwordData, - ] - - let status = SecItemCopyMatching(key as CFDictionary, nil) - - switch status { - case errSecItemNotFound: - let status = SecItemAdd(key.merging(value) { (current, _) in current } as CFDictionary, nil) - if status != errSecSuccess { - throw CredentialsProviderError.Failed(message: "Keychain failed to add item: \(status.explanation())") - } - case errSecSuccess: - let status = SecItemUpdate(key as CFDictionary, value as CFDictionary) - if status != errSecSuccess { - throw CredentialsProviderError.Failed(message: "Keychain failed to update item: \(status.explanation())") - } - default: - throw CredentialsProviderError.Failed(message: "Keychain failed to find item: \(status.explanation())") - } + guard let item = item as? [String: Any], + let user = item[kSecAttrAccount as String] as? String, + let passwordData = item[kSecValueData as String] as? Data, + let password = String(data: passwordData, encoding: .utf8) + else { + throw CredentialsProviderError.Failed(message: "Keychain item has unexpected format") } + + return (user, password) + } + + func store(host: String, user: String, password: String) throws { + let passwordData = password.data(using: .utf8) + let key: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + kSecAttrProtocol as String: kSecAttrProtocolHTTPS, + kSecAttrServer as String: host, + kSecAttrLabel as String: "Tart Credentials", + ] + let value: [String: Any] = [kSecAttrAccount as String: user, + kSecValueData as String: passwordData, + ] + + let status = SecItemCopyMatching(key as CFDictionary, nil) + + switch status { + case errSecItemNotFound: + let status = SecItemAdd(key.merging(value) { (current, _) in current } as CFDictionary, nil) + if status != errSecSuccess { + throw CredentialsProviderError.Failed(message: "Keychain failed to add item: \(status.explanation())") + } + case errSecSuccess: + let status = SecItemUpdate(key as CFDictionary, value as CFDictionary) + if status != errSecSuccess { + throw CredentialsProviderError.Failed(message: "Keychain failed to update item: \(status.explanation())") + } + default: + throw CredentialsProviderError.Failed(message: "Keychain failed to find item: \(status.explanation())") + } + } } extension OSStatus { diff --git a/Sources/tart/FileLock.swift b/Sources/tart/FileLock.swift index aa288e9..1d90f8d 100644 --- a/Sources/tart/FileLock.swift +++ b/Sources/tart/FileLock.swift @@ -2,47 +2,47 @@ import Foundation import System enum FileLockError: Error, Equatable { - case Failed(_ message: String) - case AlreadyLocked + case Failed(_ message: String) + case AlreadyLocked } class FileLock { - let url: URL - let fd: Int32 + let url: URL + let fd: Int32 - init(lockURL: URL) throws { - url = lockURL - fd = open(lockURL.path, 0) + init(lockURL: URL) throws { + url = lockURL + fd = open(lockURL.path, 0) + } + + deinit { + close(fd) + } + + func trylock() throws -> Bool { + try flockWrapper(LOCK_EX | LOCK_NB) + } + + func lock() throws { + _ = try flockWrapper(LOCK_EX) + } + + func unlock() throws { + _ = try flockWrapper(LOCK_UN) + } + + func flockWrapper(_ operation: Int32) throws -> Bool { + let ret = flock(fd, operation) + if ret != 0 { + let details = Errno(rawValue: CInt(errno)) + + if (operation & LOCK_NB) != 0 && details == .wouldBlock { + return false + } + + throw FileLockError.Failed("failed to lock \(url): \(details)") } - deinit { - close(fd) - } - - func trylock() throws -> Bool { - try flockWrapper(LOCK_EX | LOCK_NB) - } - - func lock() throws { - _ = try flockWrapper(LOCK_EX) - } - - func unlock() throws { - _ = try flockWrapper(LOCK_UN) - } - - func flockWrapper(_ operation: Int32) throws -> Bool { - let ret = flock(fd, operation) - if ret != 0 { - let details = Errno(rawValue: CInt(errno)) - - if (operation & LOCK_NB) != 0 && details == .wouldBlock { - return false - } - - throw FileLockError.Failed("failed to lock \(url): \(details)") - } - - return true - } + return true + } } diff --git a/Sources/tart/IPSWCache.swift b/Sources/tart/IPSWCache.swift index ab667d0..82ff451 100644 --- a/Sources/tart/IPSWCache.swift +++ b/Sources/tart/IPSWCache.swift @@ -15,6 +15,6 @@ class IPSWCache: PrunableStorage { func prunables() throws -> [Prunable] { try FileManager.default.contentsOfDirectory(at: baseURL, includingPropertiesForKeys: nil) - .filter { $0.lastPathComponent.hasSuffix(".ipsw")} + .filter { $0.lastPathComponent.hasSuffix(".ipsw")} } } diff --git a/Sources/tart/MACAddressResolver/ARPCache.swift b/Sources/tart/MACAddressResolver/ARPCache.swift index f8e02d9..e96e402 100644 --- a/Sources/tart/MACAddressResolver/ARPCache.swift +++ b/Sources/tart/MACAddressResolver/ARPCache.swift @@ -3,117 +3,117 @@ import Network import Virtualization struct ARPCommandFailedError: Error, CustomStringConvertible { - var terminationReason: Process.TerminationReason - var terminationStatus: Int32 + var terminationReason: Process.TerminationReason + var terminationStatus: Int32 - var description: String { - var reason: String + 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)" + 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 explanation: String - var description: String { - "arp command yielded invalid output: \(explanation)" - } + var description: String { + "arp command yielded invalid output: \(explanation)" + } } struct ARPCacheInternalError: Error, CustomStringConvertible { - var explanation: String + var explanation: String - var description: String { - "ARPCache internal error: \(explanation)" - } + 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"] + 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 + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + process.standardInput = FileHandle.nullDevice - try process.run() - process.waitUntilExit() + 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 (?.*) .*$"#) + + for line in lines { + let nsLineRange = NSRange(line.startIndex.. String { - let nsRange = self.range(withName: name) + 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]) + 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/Network/NetworkBridged.swift b/Sources/tart/Network/NetworkBridged.swift index eb1e379..af6ed25 100644 --- a/Sources/tart/Network/NetworkBridged.swift +++ b/Sources/tart/Network/NetworkBridged.swift @@ -2,21 +2,21 @@ import Foundation import Virtualization class NetworkBridged: Network { - let interface: VZBridgedNetworkInterface + let interface: VZBridgedNetworkInterface - init(interface: VZBridgedNetworkInterface) { - self.interface = interface - } + init(interface: VZBridgedNetworkInterface) { + self.interface = interface + } - func attachment() -> VZNetworkDeviceAttachment { - VZBridgedNetworkDeviceAttachment(interface: interface) - } + func attachment() -> VZNetworkDeviceAttachment { + VZBridgedNetworkDeviceAttachment(interface: interface) + } - func run(_ sema: DispatchSemaphore) throws { - // no-op, only used for Softnet - } + func run(_ sema: DispatchSemaphore) throws { + // no-op, only used for Softnet + } - func stop() async throws { - // no-op, only used for Softnet - } + func stop() async throws { + // no-op, only used for Softnet + } } diff --git a/Sources/tart/Network/NetworkShared.swift b/Sources/tart/Network/NetworkShared.swift index 44f01d5..203f92d 100644 --- a/Sources/tart/Network/NetworkShared.swift +++ b/Sources/tart/Network/NetworkShared.swift @@ -2,15 +2,15 @@ import Foundation import Virtualization class NetworkShared: Network { - func attachment() -> VZNetworkDeviceAttachment { - VZNATNetworkDeviceAttachment() - } + func attachment() -> VZNetworkDeviceAttachment { + VZNATNetworkDeviceAttachment() + } - func run(_ sema: DispatchSemaphore) throws { - // no-op, only used for Softnet - } + func run(_ sema: DispatchSemaphore) throws { + // no-op, only used for Softnet + } - func stop() async throws { - // no-op, only used for Softnet - } + func stop() async throws { + // no-op, only used for Softnet + } } diff --git a/Sources/tart/OCI/Digest.swift b/Sources/tart/OCI/Digest.swift index 703c340..3f2c864 100644 --- a/Sources/tart/OCI/Digest.swift +++ b/Sources/tart/OCI/Digest.swift @@ -20,8 +20,8 @@ class Digest { extension SHA256.Digest { func hexdigest() -> String { "sha256:" + self.map { - String(format: "%02x", $0) - } - .joined() + String(format: "%02x", $0) + } + .joined() } } diff --git a/Sources/tart/OCI/Registry.swift b/Sources/tart/OCI/Registry.swift index 30c0a06..0b787aa 100644 --- a/Sources/tart/OCI/Registry.swift +++ b/Sources/tart/OCI/Registry.swift @@ -130,11 +130,11 @@ class Registry { let manifestJSON = try manifest.toJSON() let (data, response) = try await dataRequest(.PUT, endpointURL("\(namespace)/manifests/\(reference)"), - headers: ["Content-Type": manifest.mediaType], - body: manifestJSON) + headers: ["Content-Type": manifest.mediaType], + body: manifestJSON) if response.statusCode != HTTPCode.Created.rawValue { throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing manifest", code: response.statusCode, - details: data.asText()) + details: data.asText()) } return Digest.hash(manifestJSON) @@ -142,10 +142,10 @@ class Registry { public func pullManifest(reference: String) async throws -> (OCIManifest, Data) { let (data, response) = try await dataRequest(.GET, endpointURL("\(namespace)/manifests/\(reference)"), - headers: ["Accept": ociManifestMediaType]) + headers: ["Accept": ociManifestMediaType]) if response.statusCode != HTTPCode.Ok.rawValue { throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling manifest", code: response.statusCode, - details: data.asText()) + details: data.asText()) } let manifest = try OCIManifest(fromJSON: data) @@ -168,17 +168,17 @@ class Registry { public func pushBlob(fromData: Data, chunkSizeMb: Int = 0) async throws -> String { // Initiate a blob upload let (data, postResponse) = try await dataRequest(.POST, endpointURL("\(namespace)/blobs/uploads/"), - headers: ["Content-Length": "0"]) + headers: ["Content-Length": "0"]) if postResponse.statusCode != HTTPCode.Accepted.rawValue { throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (POST)", code: postResponse.statusCode, - details: data.asText()) + details: data.asText()) } // Figure out where to upload the blob var uploadLocation = try uploadLocationFromResponse(postResponse) let digest = Digest.hash(fromData) - + if chunkSizeMb == 0 { // monolithic upload let (data, response) = try await dataRequest( @@ -192,7 +192,7 @@ class Registry { ) if response.statusCode != HTTPCode.Created.rawValue { throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (PUT) to \(uploadLocation)", - code: response.statusCode, details: data.asText()) + code: response.statusCode, details: data.asText()) } return digest } @@ -215,13 +215,13 @@ class Registry { // always accept both statuses since AWS ECR is not following specification if response.statusCode != HTTPCode.Created.rawValue && response.statusCode != HTTPCode.Accepted.rawValue { throw RegistryError.UnexpectedHTTPStatusCode(when: "streaming blob to \(uploadLocation)", - code: response.statusCode, details: data.asText()) + code: response.statusCode, details: data.asText()) } uploadedBytes += chunk.count // Update location for the next chunk uploadLocation = try uploadLocationFromResponse(response) } - + return digest } @@ -230,7 +230,7 @@ class Registry { if response.statusCode != HTTPCode.Ok.rawValue { let body = try await channel.asData().asText() throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling blob", code: response.statusCode, - details: body) + details: body) } for try await part in channel { @@ -247,15 +247,15 @@ class Registry { } private func dataRequest( - _ method: HTTPMethod, - _ urlComponents: URLComponents, - headers: Dictionary = Dictionary(), - parameters: Dictionary = Dictionary(), - body: Data? = nil, - doAuth: Bool = true + _ method: HTTPMethod, + _ urlComponents: URLComponents, + headers: Dictionary = Dictionary(), + parameters: Dictionary = Dictionary(), + body: Data? = nil, + doAuth: Bool = true ) async throws -> (Data, HTTPURLResponse) { let (channel, response) = try await channelRequest(method, urlComponents, - headers: headers, parameters: parameters, body: body, doAuth: doAuth) + headers: headers, parameters: parameters, body: body, doAuth: doAuth) return (try await channel.asData(), response) } diff --git a/Sources/tart/Passphrase/PassphraseGenerator.swift b/Sources/tart/Passphrase/PassphraseGenerator.swift index a1ebf51..5642f6a 100644 --- a/Sources/tart/Passphrase/PassphraseGenerator.swift +++ b/Sources/tart/Passphrase/PassphraseGenerator.swift @@ -1,13 +1,13 @@ import Foundation struct PassphraseGenerator: Sequence { - func makeIterator() -> PassphraseIterator { - PassphraseIterator() - } + func makeIterator() -> PassphraseIterator { + PassphraseIterator() + } } struct PassphraseIterator: IteratorProtocol { - mutating func next() -> String? { - passphrases[Int(arc4random_uniform(UInt32(passphrases.count)))] - } + mutating func next() -> String? { + passphrases[Int(arc4random_uniform(UInt32(passphrases.count)))] + } } diff --git a/Sources/tart/Platform/Architecture.swift b/Sources/tart/Platform/Architecture.swift index f74b7ca..7dffca4 100644 --- a/Sources/tart/Platform/Architecture.swift +++ b/Sources/tart/Platform/Architecture.swift @@ -1,14 +1,14 @@ import Foundation enum Architecture: String, Codable { - case arm64 - case amd64 + case arm64 + case amd64 } func CurrentArchitecture() -> Architecture { - #if arch(arm64) - return .arm64 - #elseif arch(x86_64) - return .amd64 - #endif + #if arch(arm64) + return .arm64 + #elseif arch(x86_64) + return .amd64 + #endif } diff --git a/Sources/tart/Platform/Darwin.swift b/Sources/tart/Platform/Darwin.swift index eb08d94..5e3c6b0 100644 --- a/Sources/tart/Platform/Darwin.swift +++ b/Sources/tart/Platform/Darwin.swift @@ -1,97 +1,97 @@ import Virtualization struct Darwin: Platform { - var ecid: VZMacMachineIdentifier - var hardwareModel: VZMacHardwareModel + var ecid: VZMacMachineIdentifier + var hardwareModel: VZMacHardwareModel - init(ecid: VZMacMachineIdentifier, hardwareModel: VZMacHardwareModel) { - self.ecid = ecid - self.hardwareModel = hardwareModel + init(ecid: VZMacMachineIdentifier, hardwareModel: VZMacHardwareModel) { + self.ecid = ecid + self.hardwareModel = hardwareModel + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let encodedECID = try container.decode(String.self, forKey: .ecid) + guard let data = Data.init(base64Encoded: encodedECID) else { + throw DecodingError.dataCorruptedError(forKey: .ecid, + in: container, + debugDescription: "failed to initialize Data using the provided value") + } + guard let ecid = VZMacMachineIdentifier.init(dataRepresentation: data) else { + throw DecodingError.dataCorruptedError(forKey: .ecid, + in: container, + debugDescription: "failed to initialize VZMacMachineIdentifier using the provided value") + } + self.ecid = ecid + + let encodedHardwareModel = try container.decode(String.self, forKey: .hardwareModel) + guard let data = Data.init(base64Encoded: encodedHardwareModel) else { + throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "") + } + guard let hardwareModel = VZMacHardwareModel.init(dataRepresentation: data) else { + throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "") + } + self.hardwareModel = hardwareModel + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(ecid.dataRepresentation.base64EncodedString(), forKey: .ecid) + try container.encode(hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel) + } + + func os() -> OS { + .darwin + } + + func bootLoader(nvramURL: URL) throws -> VZBootLoader { + VZMacOSBootLoader() + } + + func platform(nvramURL: URL) -> VZPlatformConfiguration { + let result = VZMacPlatformConfiguration() + + result.machineIdentifier = ecid + result.auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: nvramURL) + result.hardwareModel = hardwareModel + + return result + } + + func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration { + let result = VZMacGraphicsDeviceConfiguration() + + if let hostMainScreen = NSScreen.main { + let vmScreenSize = NSSize(width: vmConfig.display.width, height: vmConfig.display.height) + result.displays = [ + VZMacGraphicsDisplayConfiguration(for: hostMainScreen, sizeInPoints: vmScreenSize) + ] + + return result } - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) + result.displays = [ + VZMacGraphicsDisplayConfiguration( + widthInPixels: vmConfig.display.width, + heightInPixels: vmConfig.display.height, + // A reasonable guess according to Apple's documentation[1] + // [1]: https://developer.apple.com/documentation/coregraphics/1456599-cgdisplayscreensize + pixelsPerInch: 72 + ) + ] - 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 + return result + } - let encodedHardwareModel = try container.decode(String.self, forKey: .hardwareModel) - guard let data = Data.init(base64Encoded: encodedHardwareModel) else { - throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "") - } - guard let hardwareModel = VZMacHardwareModel.init(dataRepresentation: data) else { - throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "") - } - self.hardwareModel = hardwareModel - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(ecid.dataRepresentation.base64EncodedString(), forKey: .ecid) - try container.encode(hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel) - } - - func os() -> OS { - .darwin - } - - func bootLoader(nvramURL: URL) throws -> VZBootLoader { - VZMacOSBootLoader() - } - - func platform(nvramURL: URL) -> VZPlatformConfiguration { - let result = VZMacPlatformConfiguration() - - result.machineIdentifier = ecid - result.auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: nvramURL) - result.hardwareModel = hardwareModel - - return result - } - - func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration { - let result = VZMacGraphicsDeviceConfiguration() - - if let hostMainScreen = NSScreen.main { - let vmScreenSize = NSSize(width: vmConfig.display.width, height: vmConfig.display.height) - result.displays = [ - VZMacGraphicsDisplayConfiguration(for: hostMainScreen, sizeInPoints: vmScreenSize) - ] - - return result - } - - result.displays = [ - VZMacGraphicsDisplayConfiguration( - widthInPixels: vmConfig.display.width, - heightInPixels: vmConfig.display.height, - // A reasonable guess according to Apple's documentation[1] - // [1]: https://developer.apple.com/documentation/coregraphics/1456599-cgdisplayscreensize - pixelsPerInch: 72 - ) - ] - - return result - } - - func pointingDevices() -> [VZPointingDeviceConfiguration] { - if #available(macOS 13, *) { - // Trackpad is only supported starting with macOS Ventura - // macOS Monterey will continue using a USB device == .darwin - return [VZMacTrackpadConfiguration(), VZUSBScreenCoordinatePointingDeviceConfiguration()] - } else { - return [VZUSBScreenCoordinatePointingDeviceConfiguration()] - } + func pointingDevices() -> [VZPointingDeviceConfiguration] { + if #available(macOS 13, *) { + // Trackpad is only supported starting with macOS Ventura + // macOS Monterey will continue using a USB device == .darwin + return [VZMacTrackpadConfiguration(), VZUSBScreenCoordinatePointingDeviceConfiguration()] + } else { + return [VZUSBScreenCoordinatePointingDeviceConfiguration()] } + } } diff --git a/Sources/tart/Platform/Linux.swift b/Sources/tart/Platform/Linux.swift index 10ed0c8..02ec60a 100644 --- a/Sources/tart/Platform/Linux.swift +++ b/Sources/tart/Platform/Linux.swift @@ -2,36 +2,36 @@ import Virtualization @available(macOS 13, *) struct Linux: Platform { - func os() -> OS { - .linux - } + func os() -> OS { + .linux + } - func bootLoader(nvramURL: URL) throws -> VZBootLoader { - let result = VZEFIBootLoader() + func bootLoader(nvramURL: URL) throws -> VZBootLoader { + let result = VZEFIBootLoader() - result.variableStore = VZEFIVariableStore(url: nvramURL) + result.variableStore = VZEFIVariableStore(url: nvramURL) - return result - } + return result + } - func platform(nvramURL: URL) -> VZPlatformConfiguration { - VZGenericPlatformConfiguration() - } + func platform(nvramURL: URL) -> VZPlatformConfiguration { + VZGenericPlatformConfiguration() + } - func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration { - let result = VZVirtioGraphicsDeviceConfiguration() + func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration { + let result = VZVirtioGraphicsDeviceConfiguration() - result.scanouts = [ - VZVirtioGraphicsScanoutConfiguration( - widthInPixels: vmConfig.display.width, - heightInPixels: vmConfig.display.height - ) - ] + result.scanouts = [ + VZVirtioGraphicsScanoutConfiguration( + widthInPixels: vmConfig.display.width, + heightInPixels: vmConfig.display.height + ) + ] - return result - } + return result + } - func pointingDevices() -> [VZPointingDeviceConfiguration] { - [VZUSBScreenCoordinatePointingDeviceConfiguration()] - } + func pointingDevices() -> [VZPointingDeviceConfiguration] { + [VZUSBScreenCoordinatePointingDeviceConfiguration()] + } } diff --git a/Sources/tart/Platform/OS.swift b/Sources/tart/Platform/OS.swift index 13b9440..696824c 100644 --- a/Sources/tart/Platform/OS.swift +++ b/Sources/tart/Platform/OS.swift @@ -1,6 +1,6 @@ import Virtualization enum OS: String, Codable { - case darwin - case linux + case darwin + case linux } diff --git a/Sources/tart/Platform/Platform.swift b/Sources/tart/Platform/Platform.swift index 84abd37..89eda6e 100644 --- a/Sources/tart/Platform/Platform.swift +++ b/Sources/tart/Platform/Platform.swift @@ -1,9 +1,9 @@ import Virtualization protocol Platform: Codable { - func os() -> OS - func bootLoader(nvramURL: URL) throws -> VZBootLoader - func platform(nvramURL: URL) -> VZPlatformConfiguration - func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration - func pointingDevices() -> [VZPointingDeviceConfiguration] + func os() -> OS + func bootLoader(nvramURL: URL) throws -> VZBootLoader + func platform(nvramURL: URL) -> VZPlatformConfiguration + func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration + func pointingDevices() -> [VZPointingDeviceConfiguration] } diff --git a/Sources/tart/Root.swift b/Sources/tart/Root.swift index 8d5c372..9df041b 100644 --- a/Sources/tart/Root.swift +++ b/Sources/tart/Root.swift @@ -5,9 +5,9 @@ import Puppy var puppy = Puppy.default class LogFormatter: LogFormattable { - func formatMessage(_ level: LogLevel, message: String, tag: String, function: String, file: String, line: UInt, swiftLogInfo: [String: String], label: String, date: Date, threadID: UInt64) -> String { - "\(date) \(level) \(message)" - } + func formatMessage(_ level: LogLevel, message: String, tag: String, function: String, file: String, line: UInt, swiftLogInfo: [String: String], label: String, date: Date, threadID: UInt64) -> String { + "\(date) \(level) \(message)" + } } @main diff --git a/Sources/tart/Utils.swift b/Sources/tart/Utils.swift index b8f963c..ddb54bb 100644 --- a/Sources/tart/Utils.swift +++ b/Sources/tart/Utils.swift @@ -13,7 +13,7 @@ func resolveBinaryPath(_ name: String) -> URL? { for pathComponent in path.split(separator: ":") { let url = URL(fileURLWithPath: String(pathComponent)) - .appendingPathComponent(name, isDirectory: false) + .appendingPathComponent(name, isDirectory: false) if FileManager.default.fileExists(atPath: url.path) { return url diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift index 08300a7..0438cd3 100644 --- a/Sources/tart/VM.swift +++ b/Sources/tart/VM.swift @@ -31,7 +31,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { // VM's config var name: String - + // VM's config var config: VMConfig @@ -52,9 +52,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { // Initialize the virtual machine and its configuration self.network = network let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, - nvramURL: vmDir.nvramURL, vmConfig: config, - network: network, additionalDiskAttachments: additionalDiskAttachments, - directorySharingDevices: directorySharingDevices + nvramURL: vmDir.nvramURL, vmConfig: config, + network: network, additionalDiskAttachments: additionalDiskAttachments, + directorySharingDevices: directorySharingDevices ) virtualMachine = VZVirtualMachine(configuration: configuration) @@ -122,7 +122,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { virtualMachine.state == VZVirtualMachine.State.stopped || virtualMachine.state == VZVirtualMachine.State.paused || virtualMachine.state == VZVirtualMachine.State.error - + } } @@ -177,9 +177,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { // Initialize the virtual machine and its configuration self.network = network let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL, - vmConfig: config, network: network, - additionalDiskAttachments: additionalDiskAttachments, - directorySharingDevices: directorySharingDevices + vmConfig: config, network: network, + additionalDiskAttachments: additionalDiskAttachments, + directorySharingDevices: directorySharingDevices ) virtualMachine = VZVirtualMachine(configuration: configuration) diff --git a/Sources/tart/VMConfig.swift b/Sources/tart/VMConfig.swift index 4327849..8f52736 100644 --- a/Sources/tart/VMConfig.swift +++ b/Sources/tart/VMConfig.swift @@ -48,10 +48,10 @@ struct VMConfig: Codable { var display: VMDisplayConfig = VMDisplayConfig() init( - platform: Platform, - cpuCountMin: Int, - memorySizeMin: UInt64, - macAddress: VZMACAddress = VZMACAddress.randomLocallyAdministered() + platform: Platform, + cpuCountMin: Int, + memorySizeMin: UInt64, + macAddress: VZMACAddress = VZMACAddress.randomLocallyAdministered() ) { self.os = platform.os() self.arch = CurrentArchitecture() @@ -105,12 +105,12 @@ struct VMConfig: Codable { 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") + forKey: .hardwareModel, + in: container, + debugDescription: "failed to initialize VZMacAddress using the provided value") } self.macAddress = macAddress - + display = try container.decodeIfPresent(VMDisplayConfig.self, forKey: .display) ?? VMDisplayConfig() } @@ -132,7 +132,7 @@ struct VMConfig: Codable { mutating func setCPU(cpuCount: Int) throws { if cpuCount < cpuCountMin { throw LessThanMinimalResourcesError("VM should have \(cpuCountMin) CPU cores" - + " at minimum (requested \(cpuCount))") + + " at minimum (requested \(cpuCount))") } self.cpuCount = cpuCount @@ -141,7 +141,7 @@ struct VMConfig: Codable { mutating func setMemory(memorySize: UInt64) throws { if memorySize < memorySizeMin { throw LessThanMinimalResourcesError("VM should have \(memorySizeMin) bytes" - + " of memory at minimum (requested \(memorySizeMin))") + + " of memory at minimum (requested \(memorySizeMin))") } self.memorySize = memorySize diff --git a/Sources/tart/VMDirectory+OCI.swift b/Sources/tart/VMDirectory+OCI.swift index 69042a3..a29fc45 100644 --- a/Sources/tart/VMDirectory+OCI.swift +++ b/Sources/tart/VMDirectory+OCI.swift @@ -59,11 +59,11 @@ extension VMDirectory { // Progress let diskCompressedSize: Int64 = Int64(diskLayers.map { - $0.size - } - .reduce(0) { - $0 + $1 - }) + $0.size + } + .reduce(0) { + $0 + $1 + }) let prettyDiskSize = String(format: "%.1f", Double(diskCompressedSize) / 1_000_000_000.0) defaultLogger.appendNewLine("pulling disk (\(prettyDiskSize) GB compressed)...") let progress = Progress(totalUnitCount: diskCompressedSize) @@ -144,9 +144,9 @@ extension VMDirectory { let ociConfigJSON = try OCIConfig(architecture: config.arch, os: config.os).toJSON() let ociConfigDigest = try await registry.pushBlob(fromData: ociConfigJSON, chunkSizeMb: chunkSizeMb) let manifest = OCIManifest( - config: OCIManifestConfig(size: ociConfigJSON.count, digest: ociConfigDigest), - layers: layers, - uncompressedDiskSize: UInt64(mappedDiskReadOffset) + config: OCIManifestConfig(size: ociConfigJSON.count, digest: ociConfigDigest), + layers: layers, + uncompressedDiskSize: UInt64(mappedDiskReadOffset) ) // Manifest diff --git a/Sources/tart/VMStorageOCI.swift b/Sources/tart/VMStorageOCI.swift index e44fd08..afbfd09 100644 --- a/Sources/tart/VMStorageOCI.swift +++ b/Sources/tart/VMStorageOCI.swift @@ -39,7 +39,7 @@ class VMStorageOCI: PrunableStorage { // Pre-create intermediate directories (e.g. creates ~/.tart/cache/OCIs/github.com/org/repo/ // for github.com/org/repo:latest) try FileManager.default.createDirectory(at: targetURL.deletingLastPathComponent(), - withIntermediateDirectories: true) + withIntermediateDirectories: true) _ = try FileManager.default.replaceItemAt(targetURL, withItemAt: from.baseURL) } @@ -53,7 +53,7 @@ class VMStorageOCI: PrunableStorage { var refCounts = Dictionary() guard let enumerator = FileManager.default.enumerator(at: baseURL, - includingPropertiesForKeys: [.isSymbolicLinkKey]) else { + includingPropertiesForKeys: [.isSymbolicLinkKey]) else { return } @@ -90,7 +90,7 @@ class VMStorageOCI: PrunableStorage { var result: [(String, VMDirectory, Bool)] = Array() guard let enumerator = FileManager.default.enumerator(at: baseURL, - includingPropertiesForKeys: [.isSymbolicLinkKey], options: [.producesRelativePathURLs]) else { + includingPropertiesForKeys: [.isSymbolicLinkKey], options: [.producesRelativePathURLs]) else { return [] } @@ -127,7 +127,7 @@ class VMStorageOCI: PrunableStorage { let (manifest, manifestData) = try await registry.pullManifest(reference: name.reference.value) let digestName = RemoteName(host: name.host, namespace: name.namespace, - reference: Reference(digest: Digest.hash(manifestData))) + reference: Reference(digest: Digest.hash(manifestData))) // Ensure that host directory for given RemoteName exists in OCI storage let hostDirectoryURL = hostDirectoryURL(digestName) @@ -165,15 +165,15 @@ class VMStorageOCI: PrunableStorage { if capacityImportant == 0 || capacityAvailable == 0 { puppy.warning("important capacity \(capacityImportant) bytes, " - + "available capacity is \(capacityAvailable) bytes") + + "available capacity is \(capacityAvailable) bytes") } // There is a suspicious that occasionally capacity is returned as zero which can't be true. // Let's validate to avoid unnecessary pruning. if 0 < availableCapacityBytes && availableCapacityBytes < requiredCapacityBytes { puppy.info("pruning cache to accommodate \(name) with a disk of size \(uncompressedDiskSize) bytes (" - + "available capacity is \(availableCapacityBytes) bytes, required capacity " - + "is \(requiredCapacityBytes) bytes)") + + "available capacity is \(availableCapacityBytes) bytes, required capacity " + + "is \(requiredCapacityBytes) bytes)") try Prune.pruneReclaim(reclaimBytes: requiredCapacityBytes - availableCapacityBytes) } diff --git a/Sources/tart/VNC/FullFledgedVNC.swift b/Sources/tart/VNC/FullFledgedVNC.swift index 62e94b8..bbcca4b 100644 --- a/Sources/tart/VNC/FullFledgedVNC.swift +++ b/Sources/tart/VNC/FullFledgedVNC.swift @@ -3,36 +3,36 @@ import Dynamic import Virtualization class FullFledgedVNC: VNC { - let password: String - private let vnc: Dynamic + let password: String + private let vnc: Dynamic - init(virtualMachine: VZVirtualMachine) { - password = Array(PassphraseGenerator().prefix(4)).joined(separator: "-") - let securityConfiguration = Dynamic._VZVNCAuthenticationSecurityConfiguration(password: password) - vnc = Dynamic._VZVNCServer(port: 0, queue: DispatchQueue.global(), - securityConfiguration: securityConfiguration) - vnc.virtualMachine = virtualMachine - vnc.start() - } + init(virtualMachine: VZVirtualMachine) { + password = Array(PassphraseGenerator().prefix(4)).joined(separator: "-") + let securityConfiguration = Dynamic._VZVNCAuthenticationSecurityConfiguration(password: password) + vnc = Dynamic._VZVNCServer(port: 0, queue: DispatchQueue.global(), + securityConfiguration: securityConfiguration) + vnc.virtualMachine = virtualMachine + vnc.start() + } - func waitForURL() async throws -> URL { - while true { - // Port is 0 shortly after start(), - // but will be initialized later - if let port = vnc.port.asUInt16, port != 0 { - return URL(string: "vnc://:\(password)@127.0.0.1:\(port)")! - } - - // Wait 50 ms. - try await Task.sleep(nanoseconds: 50_000_000) + func waitForURL() async throws -> URL { + while true { + // Port is 0 shortly after start(), + // but will be initialized later + if let port = vnc.port.asUInt16, port != 0 { + return URL(string: "vnc://:\(password)@127.0.0.1:\(port)")! } - } - func stop() throws { - vnc.stop() + // Wait 50 ms. + try await Task.sleep(nanoseconds: 50_000_000) } + } - deinit { - try? stop() - } + func stop() throws { + vnc.stop() + } + + deinit { + try? stop() + } } diff --git a/Tests/TartTests/FileLockTests.swift b/Tests/TartTests/FileLockTests.swift index 4147bda..accf828 100644 --- a/Tests/TartTests/FileLockTests.swift +++ b/Tests/TartTests/FileLockTests.swift @@ -2,33 +2,33 @@ import XCTest @testable import tart final class FileLockTests: XCTestCase { - func testSimple() throws { - // Create a temporary file that will be used as a lock - let url = temporaryFile() + func testSimple() throws { + // Create a temporary file that will be used as a lock + let url = temporaryFile() - // Make sure this file can be locked and unlocked - let lock = try FileLock(lockURL: url) - try lock.lock() - try lock.unlock() - } + // Make sure this file can be locked and unlocked + let lock = try FileLock(lockURL: url) + try lock.lock() + try lock.unlock() + } - func testDoubleLockResultsInError() throws { - // Create a temporary file that will be used as a lock - let url = temporaryFile() + func testDoubleLockResultsInError() throws { + // Create a temporary file that will be used as a lock + let url = temporaryFile() - // Create two locks on a same file and ensure one of them fails - let firstLock = try FileLock(lockURL: url) - try firstLock.lock() + // Create two locks on a same file and ensure one of them fails + let firstLock = try FileLock(lockURL: url) + try firstLock.lock() - let secondLock = try! FileLock(lockURL: url) - XCTAssertFalse(try secondLock.trylock()) - } + let secondLock = try! FileLock(lockURL: url) + XCTAssertFalse(try secondLock.trylock()) + } - private func temporaryFile() -> URL { - let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + private func temporaryFile() -> URL { + let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) - FileManager.default.createFile(atPath: url.path, contents: nil) + FileManager.default.createFile(atPath: url.path, contents: nil) - return url - } + return url + } } diff --git a/Tests/TartTests/MACAddressResolverTests.swift b/Tests/TartTests/MACAddressResolverTests.swift index 9fab7b0..8e7b0cd 100644 --- a/Tests/TartTests/MACAddressResolverTests.swift +++ b/Tests/TartTests/MACAddressResolverTests.swift @@ -5,31 +5,31 @@ import Network final class MACAddressResolverTests: XCTestCase { func testSingleEntry() throws { let leases = try Leases(""" - { - ip_address=1.2.3.4 - hw_address=1,00:11:22:33:44:55 - } - """) + { + ip_address=1.2.3.4 + hw_address=1,00:11:22:33:44:55 + } + """) XCTAssertEqual(IPv4Address("1.2.3.4"), - try leases.resolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!)) + try leases.resolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!)) } func testMultipleEntries() throws { let leases = try Leases(""" - { - ip_address=1.2.3.4 - hw_address=1,00:11:22:33:44:55 - } - { - ip_address=5.6.7.8 - hw_address=1,AA:BB:CC:DD:EE:FF - } - """) + { + ip_address=1.2.3.4 + hw_address=1,00:11:22:33:44:55 + } + { + ip_address=5.6.7.8 + hw_address=1,AA:BB:CC:DD:EE:FF + } + """) XCTAssertEqual(IPv4Address("1.2.3.4"), - try leases.resolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!)) + try leases.resolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!)) XCTAssertEqual(IPv4Address("5.6.7.8"), - try leases.resolveMACAddress(macAddress: MACAddress(fromString: "AA:BB:CC:DD:EE:FF")!)) + try leases.resolveMACAddress(macAddress: MACAddress(fromString: "AA:BB:CC:DD:EE:FF")!)) } } diff --git a/Tests/TartTests/RegistryTests.swift b/Tests/TartTests/RegistryTests.swift index 864cf17..dad02ed 100644 --- a/Tests/TartTests/RegistryTests.swift +++ b/Tests/TartTests/RegistryTests.swift @@ -2,88 +2,88 @@ import XCTest @testable import tart final class RegistryTests: XCTestCase { - var registryRunner: RegistryRunner? + var registryRunner: RegistryRunner? - override func setUp() async throws { - try await super.setUp() + override func setUp() async throws { + try await super.setUp() - do { - registryRunner = try await RegistryRunner() - } catch { - try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] == nil) - } + do { + registryRunner = try await RegistryRunner() + } catch { + try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] == nil) + } + } + + override func tearDown() async throws { + try await super.tearDown() + + registryRunner = nil + } + + var registry: Registry { + registryRunner!.registry + } + + func testPushPullBlobSmall() async throws { + // Generate a simple blob + let pushedBlob = Data("The quick brown fox jumps over the lazy dog".utf8) + + // Push it + let pushedBlobDigest = try await registry.pushBlob(fromData: pushedBlob) + XCTAssertEqual("sha256:d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", pushedBlobDigest) + + // Pull it + var pulledBlob = Data() + try await registry.pullBlob(pushedBlobDigest) { data in + pulledBlob.append(data) } - override func tearDown() async throws { - try await super.tearDown() + // Ensure that both blobs are identical + XCTAssertEqual(pushedBlob, pulledBlob) + } - registryRunner = nil + func testPushPullBlobHugeInChunks() async throws { + // Generate a large enough blob + let fh = FileHandle(forReadingAtPath: "/dev/urandom")! + let largeBlobToPush = try fh.read(upToCount: 768 * 1024 * 1024)! + + // Push it + let largeBlobDigest = try await registry.pushBlob(fromData: largeBlobToPush, chunkSizeMb: 10) + + // Pull it + var pulledLargeBlob = Data() + try await registry.pullBlob(largeBlobDigest) { data in + pulledLargeBlob.append(data) } - var registry: Registry { - registryRunner!.registry - } + // Ensure that both blobs are identical + XCTAssertEqual(largeBlobToPush, pulledLargeBlob) + } - func testPushPullBlobSmall() async throws { - // Generate a simple blob - let pushedBlob = Data("The quick brown fox jumps over the lazy dog".utf8) + func testPushPullManifest() async throws { + // Craft a basic config + let configData = try OCIConfig().toJSON() + let configDigest = try await registry.pushBlob(fromData: configData) - // Push it - let pushedBlobDigest = try await registry.pushBlob(fromData: pushedBlob) - XCTAssertEqual("sha256:d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", pushedBlobDigest) + // Craft a basic layer + let layerData = Data("doesn't matter".utf8) + let layerDigest = try await registry.pushBlob(fromData: layerData) - // Pull it - var pulledBlob = Data() - try await registry.pullBlob(pushedBlobDigest) { data in - pulledBlob.append(data) - } + // Craft a basic manifest and push it + let manifest = OCIManifest( + config: OCIManifestConfig(size: configData.count, digest: configDigest), + layers: [ + OCIManifestLayer(mediaType: "application/octet-stream", size: layerData.count, digest: layerDigest) + ] + ) + let pushedManifestDigest = try await registry.pushManifest(reference: "latest", manifest: manifest) - // Ensure that both blobs are identical - XCTAssertEqual(pushedBlob, pulledBlob) - } + // Ensure that the manifest pulled by tag matches with the one pushed above + let (pulledByTagManifest, _) = try await registry.pullManifest(reference: "latest") + XCTAssertEqual(manifest, pulledByTagManifest) - func testPushPullBlobHugeInChunks() async throws { - // Generate a large enough blob - let fh = FileHandle(forReadingAtPath: "/dev/urandom")! - let largeBlobToPush = try fh.read(upToCount: 768 * 1024 * 1024)! - - // Push it - let largeBlobDigest = try await registry.pushBlob(fromData: largeBlobToPush, chunkSizeMb: 10) - - // Pull it - var pulledLargeBlob = Data() - try await registry.pullBlob(largeBlobDigest) { data in - pulledLargeBlob.append(data) - } - - // Ensure that both blobs are identical - XCTAssertEqual(largeBlobToPush, pulledLargeBlob) - } - - func testPushPullManifest() async throws { - // Craft a basic config - let configData = try OCIConfig().toJSON() - let configDigest = try await registry.pushBlob(fromData: configData) - - // Craft a basic layer - let layerData = Data("doesn't matter".utf8) - let layerDigest = try await registry.pushBlob(fromData: layerData) - - // Craft a basic manifest and push it - let manifest = OCIManifest( - config: OCIManifestConfig(size: configData.count, digest: configDigest), - layers: [ - OCIManifestLayer(mediaType: "application/octet-stream", size: layerData.count, digest: layerDigest) - ] - ) - let pushedManifestDigest = try await registry.pushManifest(reference: "latest", manifest: manifest) - - // Ensure that the manifest pulled by tag matches with the one pushed above - let (pulledByTagManifest, _) = try await registry.pullManifest(reference: "latest") - XCTAssertEqual(manifest, pulledByTagManifest) - - // Ensure that the manifest pulled by digest matches with the one pushed above - let (pulledByDigestManifest, _) = try await registry.pullManifest(reference: "\(pushedManifestDigest)") - XCTAssertEqual(manifest, pulledByDigestManifest) - } + // Ensure that the manifest pulled by digest matches with the one pushed above + let (pulledByDigestManifest, _) = try await registry.pullManifest(reference: "\(pushedManifestDigest)") + XCTAssertEqual(manifest, pulledByDigestManifest) + } } diff --git a/Tests/TartTests/RemoteNameTests.swift b/Tests/TartTests/RemoteNameTests.swift index 577d97f..c1ea796 100644 --- a/Tests/TartTests/RemoteNameTests.swift +++ b/Tests/TartTests/RemoteNameTests.swift @@ -7,7 +7,7 @@ final class RemoteNameTests: XCTestCase { XCTAssertEqual(expectedRemoteName, try RemoteName("ghcr.io/a/b:latest")) } - + func testComplexTag() throws { let expectedRemoteName = RemoteName(host: "ghcr.io", namespace: "a/b", reference: Reference(tag: "1.2.3-RC-1")) @@ -22,7 +22,7 @@ final class RemoteNameTests: XCTestCase { ) XCTAssertEqual(expectedRemoteName, - try RemoteName("ghcr.io/a/b@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")) + try RemoteName("ghcr.io/a/b@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")) } func testASCIIOnly() throws { diff --git a/Tests/TartTests/URLAbsolutizationTests.swift b/Tests/TartTests/URLAbsolutizationTests.swift index 446d855..6445540 100644 --- a/Tests/TartTests/URLAbsolutizationTests.swift +++ b/Tests/TartTests/URLAbsolutizationTests.swift @@ -4,14 +4,14 @@ import XCTest final class URLAbsolutizationTets: XCTestCase { func testNeedsAbsolutization() throws { let url = URL(string: "/v2/some/path?some=query")! - .absolutize(URL(string: "https://example.com/v2/")!) + .absolutize(URL(string: "https://example.com/v2/")!) XCTAssertEqual(url.absoluteString, "https://example.com/v2/some/path?some=query") } func testDoesntNeedAbsolutization() throws { let url = URL(string: "https://example.org/v2/some/path?some=query")! - .absolutize(URL(string: "https://example.com/v2/")!) + .absolutize(URL(string: "https://example.com/v2/")!) XCTAssertEqual(url.absoluteString, "https://example.org/v2/some/path?some=query") } diff --git a/Tests/TartTests/Util/RegistryRunner.swift b/Tests/TartTests/Util/RegistryRunner.swift index 1002201..efb6ab5 100644 --- a/Tests/TartTests/Util/RegistryRunner.swift +++ b/Tests/TartTests/Util/RegistryRunner.swift @@ -2,53 +2,53 @@ import Foundation @testable import tart enum RegistryRunnerError: Error { - case DockerFailed(exitCode: Int32) + case DockerFailed(exitCode: Int32) } class RegistryRunner { - let containerID: String - let registry: Registry + let containerID: String + let registry: Registry - static func dockerCmd(_ arguments: String...) throws -> String { - let stdoutPipe = Pipe() + static func dockerCmd(_ arguments: String...) throws -> String { + let stdoutPipe = Pipe() - let proc = Process() - proc.executableURL = URL(fileURLWithPath: "/usr/local/bin/docker") - proc.arguments = arguments - proc.standardOutput = stdoutPipe - try proc.run() + let proc = Process() + proc.executableURL = URL(fileURLWithPath: "/usr/local/bin/docker") + proc.arguments = arguments + proc.standardOutput = stdoutPipe + try proc.run() - let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() + let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() - proc.waitUntilExit() + proc.waitUntilExit() - if proc.terminationStatus != 0 { - throw RegistryRunnerError.DockerFailed(exitCode: proc.terminationStatus) - } - - return String(data: stdoutData, encoding: .utf8) ?? "" + if proc.terminationStatus != 0 { + throw RegistryRunnerError.DockerFailed(exitCode: proc.terminationStatus) } - init() async throws { - // Start container - let container = try Self.dockerCmd("run", "-d", "--rm", "-p", "5000", "registry:2") - .trimmingCharacters(in: CharacterSet.newlines) - containerID = container + return String(data: stdoutData, encoding: .utf8) ?? "" + } - // Get forwarded port - let port = try Self.dockerCmd("inspect", containerID, "--format", "{{(index (index .NetworkSettings.Ports \"5000/tcp\") 0).HostPort}}") - .trimmingCharacters(in: CharacterSet.newlines) + init() async throws { + // Start container + let container = try Self.dockerCmd("run", "-d", "--rm", "-p", "5000", "registry:2") + .trimmingCharacters(in: CharacterSet.newlines) + containerID = container - registry = try Registry(urlComponents: URLComponents(string: "http://127.0.0.1:\(port)/v2/")!, - namespace: "vm-image") + // Get forwarded port + let port = try Self.dockerCmd("inspect", containerID, "--format", "{{(index (index .NetworkSettings.Ports \"5000/tcp\") 0).HostPort}}") + .trimmingCharacters(in: CharacterSet.newlines) - // Wait for the Docker Registry to start - while ((try? await registry.ping()) == nil) { - try await Task.sleep(nanoseconds: 100_000_000) - } + registry = try Registry(urlComponents: URLComponents(string: "http://127.0.0.1:\(port)/v2/")!, + namespace: "vm-image") + + // Wait for the Docker Registry to start + while ((try? await registry.ping()) == nil) { + try await Task.sleep(nanoseconds: 100_000_000) } + } - deinit { - _ = try! Self.dockerCmd("kill", containerID) - } + deinit { + _ = try! Self.dockerCmd("kill", containerID) + } }