Skip to content

Commit ff07e74

Browse files
committed
vm(qemu): load QEMU resources from Caches
For reasons still unknown, in the App Store build, we can no longer read the BIOS from /Applications/UTM.app/Contents/Resources/qemu as a bookmark even though this had worked fine in the past. As a workaround, we will now copy all the data files to the App Sandbox's Cache and then share a bookmark to that. Resolves #6861
1 parent 5bf27b8 commit ff07e74

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

Configuration/UTMQemuConfiguration+Arguments.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ import Virtualization // for getting network interfaces
510510
}
511511

512512
private var resourceURL: URL {
513-
Bundle.main.url(forResource: "qemu", withExtension: nil)!
513+
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("qemu", isDirectory: true)
514514
}
515515

516516
private var soundBackend: UTMQEMUSoundBackend {

Services/UTMQemuVirtualMachine.swift

+87
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ final class UTMQemuVirtualMachine: UTMSpiceVirtualMachine {
155155

156156
private var changeCursorRequestInProgress: Bool = false
157157

158+
private static var resourceCacheOperationQueue = DispatchQueue(label: "Resource Cache Operation")
159+
158160
#if WITH_SERVER
159161
@Setting("ServerPort") private var serverPort: Int = 0
160162
private var spicePort: SwiftPortmap.Port?
@@ -275,6 +277,10 @@ extension UTMQemuVirtualMachine {
275277
guard await isSupported else {
276278
throw UTMQemuVirtualMachineError.emulationNotSupported
277279
}
280+
281+
// create QEMU resource cache if needed
282+
try await ensureQemuResourceCacheUpToDate()
283+
278284
let hasDebugLog = await config.qemu.hasDebugLog
279285
// start logging
280286
if hasDebugLog, let debugLogURL = await config.qemu.debugLogURL {
@@ -885,6 +891,87 @@ extension UTMQemuVirtualMachine {
885891
}
886892
}
887893

894+
// MARK: - Caching QEMU resources
895+
extension UTMQemuVirtualMachine {
896+
private func _ensureQemuResourceCacheUpToDate() throws {
897+
let fm = FileManager.default
898+
let qemuResourceUrl = Bundle.main.url(forResource: "qemu", withExtension: nil)!
899+
let cacheUrl = try fm.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
900+
let qemuCacheUrl = cacheUrl.appendingPathComponent("qemu", isDirectory: true)
901+
902+
guard fm.fileExists(atPath: qemuCacheUrl.path) else {
903+
try fm.copyItem(at: qemuResourceUrl, to: qemuCacheUrl)
904+
return
905+
}
906+
907+
logger.info("Updating QEMU resource cache...")
908+
// first visit all the subdirectories and create them if needed
909+
let subdirectoryEnumerator = fm.enumerator(at: qemuResourceUrl, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .producesRelativePathURLs, .includesDirectoriesPostOrder])!
910+
for case let directoryURL as URL in subdirectoryEnumerator {
911+
guard subdirectoryEnumerator.isEnumeratingDirectoryPostOrder else {
912+
continue
913+
}
914+
let relativePath = directoryURL.relativePath
915+
let destUrl = qemuCacheUrl.appendingPathComponent(relativePath)
916+
var isDirectory: ObjCBool = false
917+
if fm.fileExists(atPath: destUrl.path, isDirectory: &isDirectory) {
918+
// old file is now a directory
919+
if !isDirectory.boolValue {
920+
logger.info("Removing file \(destUrl.path)")
921+
try fm.removeItem(at: destUrl)
922+
} else {
923+
continue
924+
}
925+
}
926+
logger.info("Creating directory \(destUrl.path)")
927+
try fm.createDirectory(at: destUrl, withIntermediateDirectories: true)
928+
}
929+
// next check all the files
930+
let fileEnumerator = fm.enumerator(at: qemuResourceUrl, includingPropertiesForKeys: [.contentModificationDateKey, .fileSizeKey, .isDirectoryKey], options: [.skipsHiddenFiles, .producesRelativePathURLs])!
931+
for case let sourceUrl as URL in fileEnumerator {
932+
let relativePath = sourceUrl.relativePath
933+
let sourceResourceValues = try sourceUrl.resourceValues(forKeys: [.contentModificationDateKey, .fileSizeKey, .isDirectoryKey])
934+
guard !sourceResourceValues.isDirectory! else {
935+
continue
936+
}
937+
let destUrl = qemuCacheUrl.appendingPathComponent(relativePath)
938+
if fm.fileExists(atPath: destUrl.path) {
939+
// first do a quick comparsion with resource keys
940+
let destResourceValues = try destUrl.resourceValues(forKeys: [.contentModificationDateKey, .fileSizeKey, .isDirectoryKey])
941+
// old directory is now a file
942+
if destResourceValues.isDirectory! {
943+
logger.info("Removing directory \(destUrl.path)")
944+
try fm.removeItem(at: destUrl)
945+
} else if destResourceValues.contentModificationDate == sourceResourceValues.contentModificationDate && destResourceValues.fileSize == sourceResourceValues.fileSize {
946+
// assume the file is the same
947+
continue
948+
} else {
949+
logger.info("Removing file \(destUrl.path)")
950+
try fm.removeItem(at: destUrl)
951+
}
952+
}
953+
// if we are here, the file has changed
954+
logger.info("Copying file \(sourceUrl.path) to \(destUrl.path)")
955+
try fm.copyItem(at: sourceUrl, to: destUrl)
956+
}
957+
}
958+
959+
func ensureQemuResourceCacheUpToDate() async throws {
960+
try await withCheckedThrowingContinuation { continuation in
961+
Self.resourceCacheOperationQueue.async { [weak self] in
962+
do {
963+
try self?._ensureQemuResourceCacheUpToDate()
964+
continuation.resume()
965+
} catch {
966+
continuation.resume(throwing: error)
967+
}
968+
}
969+
}
970+
}
971+
}
972+
973+
// MARK: - Errors
974+
888975
enum UTMQemuVirtualMachineError: Error {
889976
case failedToAccessShortcut
890977
case emulationNotSupported

0 commit comments

Comments
 (0)