From 7a2c20ba30e8e2e25a5b2ad035e2685cb9b54954 Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Tue, 27 Sep 2022 23:36:18 +0400 Subject: [PATCH] tart create: support fetching URLs specified in the --from-ipsw option (#256) * tart create: support fetching URLs specified in the --from-ipsw option * Use x-amz-meta-digest-sha256 header to cache IPSWs --- Sources/tart/Commands/Create.swift | 12 ++++-- Sources/tart/IPSWCache.swift | 4 +- Sources/tart/VM.swift | 61 ++++++++++++++++++++---------- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/Sources/tart/Commands/Create.swift b/Sources/tart/Commands/Create.swift index 2034771..0511627 100644 --- a/Sources/tart/Commands/Create.swift +++ b/Sources/tart/Commands/Create.swift @@ -9,7 +9,7 @@ struct Create: AsyncParsableCommand { @Argument(help: "VM name") var name: String - @Option(help: ArgumentHelp("create a macOS VM using path to the IPSW file (or \"latest\") to fetch the latest appropriate IPSW", valueName: "path")) + @Option(help: ArgumentHelp("create a macOS VM using path to the IPSW file or URL (or \"latest\", to fetch the latest supported IPSW automatically)", valueName: "path")) var fromIPSW: String? @Flag(help: "create a Linux VM") @@ -34,11 +34,17 @@ struct Create: AsyncParsableCommand { try await withTaskCancellationHandler(operation: { if let fromIPSW = fromIPSW { + let ipswURL: URL + if fromIPSW == "latest" { - _ = try await VM(vmDir: tmpVMDir, ipswURL: nil, diskSizeGB: diskSize) + ipswURL = try await VM.latestIPSWURL() + } else if fromIPSW.starts(with: "http://") || fromIPSW.starts(with: "https://") { + ipswURL = URL(string: fromIPSW)! } else { - _ = try await VM(vmDir: tmpVMDir, ipswURL: URL(fileURLWithPath: fromIPSW), diskSizeGB: diskSize) + ipswURL = URL(fileURLWithPath: fromIPSW) } + + _ = try await VM(vmDir: tmpVMDir, ipswURL: ipswURL, diskSizeGB: diskSize) } if linux { diff --git a/Sources/tart/IPSWCache.swift b/Sources/tart/IPSWCache.swift index 1e9d9d4..ab667d0 100644 --- a/Sources/tart/IPSWCache.swift +++ b/Sources/tart/IPSWCache.swift @@ -9,8 +9,8 @@ class IPSWCache: PrunableStorage { try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) } - func locationFor(image: VZMacOSRestoreImage) -> URL { - baseURL.appendingPathComponent("\(image.buildVersion).ipsw", isDirectory: false) + func locationFor(fileName: String) -> URL { + baseURL.appendingPathComponent(fileName, isDirectory: false) } func prunables() throws -> [Prunable] { diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift index 5607ca2..75b054c 100644 --- a/Sources/tart/VM.swift +++ b/Sources/tart/VM.swift @@ -60,26 +60,29 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { 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 retrieveIPSW(remoteURL: URL) async throws -> URL { + // Check if we already have this IPSW in cache + var request = URLRequest(url: remoteURL) + request.httpMethod = "HEAD" + let (_, response) = try await URLSession.shared.data(for: request) + let httpURLResponse = response as! HTTPURLResponse + + if let hash = httpURLResponse.value(forHTTPHeaderField: "x-amz-meta-digest-sha256") { + let ipswLocation = try IPSWCache().locationFor(fileName: "sha256:\(hash).ipsw") + + if FileManager.default.fileExists(atPath: ipswLocation.path) { + defaultLogger.appendNewLine("Using cached *.ipsw file...") + try ipswLocation.updateAccessDate() + + return ipswLocation } } - let expectedIPSWLocation = try IPSWCache().locationFor(image: image) - - if FileManager.default.fileExists(atPath: expectedIPSWLocation.path) { - defaultLogger.appendNewLine("Using cached *.ipsw file...") - try expectedIPSWLocation.updateAccessDate() - return expectedIPSWLocation - } - - defaultLogger.appendNewLine("Fetching \(expectedIPSWLocation.lastPathComponent)...") + // Download the IPSW + defaultLogger.appendNewLine("Fetching \(remoteURL.lastPathComponent)...") let data: Data = try await withCheckedThrowingContinuation { continuation in - let downloadedTask = URLSession.shared.dataTask(with: image.url) { data, response, error in + let downloadedTask = URLSession.shared.dataTask(with: remoteURL) { data, response, error in if error != nil { continuation.resume(throwing: error!) return @@ -94,10 +97,24 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { downloadedTask.resume() } - try data.write(to: expectedIPSWLocation, options: [.atomic]) - return expectedIPSWLocation + let ipswLocation = try IPSWCache().locationFor(fileName: Digest.hash(data) + ".ipsw") + try data.write(to: ipswLocation, options: [.atomic]) + + return ipswLocation } - + + static func latestIPSWURL() 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) + } + } + + return image.url + } + var inFinalState: Bool { get { virtualMachine.state == VZVirtualMachine.State.stopped || @@ -109,12 +126,16 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { init( vmDir: VMDirectory, - ipswURL: URL?, + ipswURL: URL, diskSizeGB: UInt16, network: Network = NetworkShared(), additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [] ) async throws { - let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW(); + var ipswURL = ipswURL + + if !ipswURL.isFileURL { + ipswURL = try await VM.retrieveIPSW(remoteURL: ipswURL) + } // Load the restore image and try to get the requirements // that match both the image and our platform