asif: use DiskImageKit on macOS 27

ASIF drives for Apple Virtualization are created, inspected and resized
through the private DiskImages2 framework, loaded by path and called via
NSInvocation. macOS 27 ships DiskImageKit, a public Swift-only framework
that covers the same operations and is what Virtualization.framework now
consumes directly.

Add UTMAppleDiskImage, a Swift wrapper that uses DiskImageKit on macOS 27
and later and falls back to the existing UTMASIFImage helper on macOS 13
through 26. The legacy path is confined to one private extension so it
can be deleted together with UTMASIFImage once the minimum host is
macOS 27. DiskImageKit is weak linked because the deployment target is
below macOS 27.

Callers switch to the wrapper; the resize path no longer force-unwraps
the helper.

Assisted-by: Claude:claude-fable-5-1
This commit is contained in:
osy
2026-09-19 10:49:50 -05:00
parent d67f4ba35c
commit fa92ca48ed
6 changed files with 165 additions and 27 deletions
+1 -5
View File
@@ -132,12 +132,8 @@ extension UTMConfigurationDrive {
#if os(macOS)
@available(macOS 13, *)
private func createAsifImage(at newURL: URL, size sizeMib: Int) async throws {
let numBlocks = sizeMib * Int(bytesInMib) / 512
guard let asif = UTMASIFImage.sharedInstance() else {
throw UTMConfigurationError.cannotCreateDiskImage
}
try await Task.detached {
try asif.createBlank(with: newURL, numBlocks: numBlocks)
try UTMAppleDiskImage.createASIF(at: newURL, sizeMib: sizeMib)
}.value
}
+1 -1
View File
@@ -350,7 +350,7 @@ struct AlertMessage: Identifiable {
if #available(macOS 14, *), useNvmeAsDiskInterface {
newDisk.isNvme = true
}
if #available(macOS 26, *), UTMASIFImage.sharedInstance() != nil {
if UTMAppleDiskImage.isASIFSupported {
newDisk.isASIF = true
}
config.drives.append(newDisk)
+4 -16
View File
@@ -942,27 +942,15 @@ enum AlertItem: Identifiable {
@available(macOS 14, *)
func appleDriveInfo(for driveUrl: URL) -> (format: String?, size: Int64?) {
var format: String? = nil
var size: Int64? = nil
guard let info = try? UTMASIFImage.sharedInstance()?.retrieveInfo(driveUrl) else {
return (format, size)
guard let info = try? UTMAppleDiskImage.info(for: driveUrl) else {
return (nil, nil)
}
if let _format = info["Image Format"] as? String {
format = _format
}
if let _sizeInfo = info["Size Info"] as? [String: Any] {
if let _totalBytes = _sizeInfo["Total Bytes"] as? Int64 {
size = _totalBytes
}
}
return (format, size)
return (info.format.localizedDescription, info.size)
}
@available(macOS 14, *)
func resizeAppleDrive(for driveUrl: URL, sizeInMib: Int) throws {
let bytesinMib = 1048576
let size = Int(sizeInMib * bytesinMib)
try UTMASIFImage.sharedInstance()!.resize(with: driveUrl, size: size)
try UTMAppleDiskImage.resize(driveUrl, toSizeMib: sizeInMib)
}
#endif
@@ -24,11 +24,7 @@ struct VMConfigAppleDriveCreateView: View {
@State private var isGiB: Bool = true
private var isASIFSupported: Bool {
if #available(macOS 26, *) {
return UTMASIFImage.sharedInstance() != nil
} else {
return false
}
UTMAppleDiskImage.isASIFSupported
}
var body: some View {
+150
View File
@@ -0,0 +1,150 @@
//
// Copyright © 2026 Turing Software, LLC. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import DiskImageKit
/// Creates, inspects and resizes disk images for Apple Virtualization drives (ASIF and RAW).
///
/// On macOS 27 and later this uses DiskImageKit. On older hosts it falls back to the private
/// DiskImages2 framework through `UTMASIFImage`. Once the minimum supported host is macOS 27,
/// delete the legacy extension at the bottom of this file along with `UTMASIFImage.h/.m` and
/// its entry in `Swift-Bridging-Header.h`.
enum UTMAppleDiskImage {
enum Format: Equatable {
case asif
case raw
case layered
case other(String)
var localizedDescription: String {
switch self {
case .asif: return NSLocalizedString("ASIF", comment: "UTMAppleDiskImage")
case .raw: return NSLocalizedString("RAW", comment: "UTMAppleDiskImage")
case .layered: return NSLocalizedString("Layered", comment: "UTMAppleDiskImage")
case .other(let name): return name
}
}
}
struct Info {
let format: Format
/// Size of the virtual disk in bytes
let size: Int64
}
private static let bytesInMib = 1048576
/// Block size used when creating images, matches the legacy DiskImages2 path
private static let blockSize = 512
/// True if ASIF images can be created on this host
static var isASIFSupported: Bool {
if #available(macOS 27, *) {
return true
} else if #available(macOS 26, *) {
return UTMASIFImage.sharedInstance() != nil
} else {
return false
}
}
/// Create a new blank ASIF image
/// - Parameters:
/// - url: Location of the new image, must not exist
/// - sizeMib: Size of the virtual disk in MiB
@available(macOS 13, *)
static func createASIF(at url: URL, sizeMib: Int) throws {
let blockCount = sizeMib * bytesInMib / blockSize
if #available(macOS 27, *) {
_ = try DiskImageKit.DiskImage(creating: .asif(url: url, blockCount: blockCount, blockSize: .bytes512))
} else {
try legacyCreateASIF(at: url, blockCount: blockCount)
}
}
/// Read the format and size of an existing image
/// - Parameter url: Location of the image
@available(macOS 14, *)
static func info(for url: URL) throws -> Info {
if #available(macOS 27, *) {
let image = try DiskImageKit.DiskImage(opening: .open(url: url, mode: .readOnly))
let format: Format
switch image.format {
case .asif: format = .asif
case .raw: format = .raw
case .stack: format = .layered
@unknown default: format = .other(String(describing: image.format))
}
return Info(format: format, size: Int64(image.size))
} else {
return try legacyInfo(for: url)
}
}
/// Change the size of the virtual disk of an existing image
/// - Parameters:
/// - url: Location of the image
/// - sizeMib: New size of the virtual disk in MiB
@available(macOS 14, *)
static func resize(_ url: URL, toSizeMib sizeMib: Int) throws {
if #available(macOS 27, *) {
let image = try DiskImageKit.DiskImage(opening: .open(url: url, mode: .readWrite))
try image.truncate(blockCount: sizeMib * bytesInMib / image.blockSize.rawValue)
} else {
try legacyResize(url, toSizeMib: sizeMib)
}
}
}
// MARK: - Legacy DiskImages2 (macOS 13 to 26)
/// Remove this extension together with `UTMASIFImage` when the minimum host is macOS 27.
private extension UTMAppleDiskImage {
@available(macOS 13, *)
static func legacyCreateASIF(at url: URL, blockCount: Int) throws {
guard let asif = UTMASIFImage.sharedInstance() else {
throw UTMAppleConfigurationError.featureNotSupported
}
try asif.createBlank(with: url, numBlocks: blockCount)
}
@available(macOS 14, *)
static func legacyInfo(for url: URL) throws -> Info {
guard let asif = UTMASIFImage.sharedInstance() else {
throw UTMAppleConfigurationError.featureNotSupported
}
let info = try asif.retrieveInfo(url)
guard let sizeInfo = info["Size Info"] as? [String: Any], let totalBytes = sizeInfo["Total Bytes"] as? Int64 else {
throw UTMAppleConfigurationError.featureNotSupported
}
let format: Format
switch info["Image Format"] as? String {
case "ASIF": format = .asif
case "RAW", "RAW*": format = .raw
case .some(let name): format = .other(name)
case .none: format = .other(NSLocalizedString("Unknown", comment: "UTMAppleDiskImage"))
}
return Info(format: format, size: totalBytes)
}
@available(macOS 14, *)
static func legacyResize(_ url: URL, toSizeMib sizeMib: Int) throws {
guard let asif = UTMASIFImage.sharedInstance() else {
throw UTMAppleConfigurationError.featureNotSupported
}
try asif.resize(with: url, size: sizeMib * bytesInMib)
}
}
+8
View File
@@ -672,6 +672,8 @@
CE7A1C022EFA0A01001671E9 /* UTMAppleUSBDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7A1C012EFA0A01001671E9 /* UTMAppleUSBDevice.swift */; };
CE7A1C042EFA0A01001671E9 /* UTMAppleUSBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7A1C032EFA0A01001671E9 /* UTMAppleUSBManager.swift */; };
CE7A1C062EFA0A01001671E9 /* AccessoryAccess.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE7A1C052EFA0A01001671E9 /* AccessoryAccess.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
CE1D15AB2F6B100100D15C01 /* UTMAppleDiskImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1D15AA2F6B100100D15C01 /* UTMAppleDiskImage.swift */; };
CE1D15AD2F6B100100D15C01 /* DiskImageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE1D15AC2F6B100100D15C01 /* DiskImageKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
CE6804852E4E5D84001671E9 /* UTMScriptingInputImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6804842E4E5D84001671E9 /* UTMScriptingInputImpl.swift */; };
CE68E5442E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */; };
CE68E5452E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */; };
@@ -2040,6 +2042,8 @@
CE7A1C012EFA0A01001671E9 /* UTMAppleUSBDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMAppleUSBDevice.swift; sourceTree = "<group>"; };
CE7A1C032EFA0A01001671E9 /* UTMAppleUSBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMAppleUSBManager.swift; sourceTree = "<group>"; };
CE7A1C052EFA0A01001671E9 /* AccessoryAccess.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessoryAccess.framework; path = System/Library/Frameworks/AccessoryAccess.framework; sourceTree = SDKROOT; };
CE1D15AA2F6B100100D15C01 /* UTMAppleDiskImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMAppleDiskImage.swift; sourceTree = "<group>"; };
CE1D15AC2F6B100100D15C01 /* DiskImageKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DiskImageKit.framework; path = System/Library/Frameworks/DiskImageKit.framework; sourceTree = SDKROOT; };
CE6804842E4E5D84001671E9 /* UTMScriptingInputImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingInputImpl.swift; sourceTree = "<group>"; };
CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMKeyboardShortcutsView.swift; sourceTree = "<group>"; };
CE68E5472E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardOSClassicMacView.swift; sourceTree = "<group>"; };
@@ -2362,6 +2366,7 @@
CE0B6F0124AD677200FE012D /* libgsttypefindfunctions.a in Frameworks */,
848308D5278A1F2200E3E474 /* Virtualization.framework in Frameworks */,
CE7A1C062EFA0A01001671E9 /* AccessoryAccess.framework in Frameworks */,
CE1D15AD2F6B100100D15C01 /* DiskImageKit.framework in Frameworks */,
CE0B6EE124AD677200FE012D /* gstrtsp-1.0.0.framework in Frameworks */,
848F71E6277A2466006A0240 /* SwiftTerm in Frameworks */,
CE03D08824D90F0700F76B84 /* gio-2.0.0.framework in Frameworks */,
@@ -2676,6 +2681,7 @@
84937EFE28960789003148F4 /* zstd.1.framework */,
848308D4278A1F2200E3E474 /* Virtualization.framework */,
CE7A1C052EFA0A01001671E9 /* AccessoryAccess.framework */,
CE1D15AC2F6B100100D15C01 /* DiskImageKit.framework */,
8453DCB0278CE33E0037A0DA /* qemu-img.framework */,
CE5451A426AF5F0F008594E5 /* EGL.framework */,
CE5451A226AF5F0F008594E5 /* epoxy.0.framework */,
@@ -3000,6 +3006,7 @@
CE68047D2E493D71001671E9 /* UTMUSBManager.swift */,
CE7A1C012EFA0A01001671E9 /* UTMAppleUSBDevice.swift */,
CE7A1C032EFA0A01001671E9 /* UTMAppleUSBManager.swift */,
CE1D15AA2F6B100100D15C01 /* UTMAppleDiskImage.swift */,
CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */,
CE928C2926ABE6690099F293 /* UTMAppleVirtualMachine.swift */,
4F9F544E438D4BE8A5CA9360 /* UTMAppleVmnetNetworkManager.swift */,
@@ -4175,6 +4182,7 @@
CE6804802E493D71001671E9 /* UTMUSBManager.swift in Sources */,
CE7A1C022EFA0A01001671E9 /* UTMAppleUSBDevice.swift in Sources */,
CE7A1C042EFA0A01001671E9 /* UTMAppleUSBManager.swift in Sources */,
CE1D15AB2F6B100100D15C01 /* UTMAppleDiskImage.swift in Sources */,
CEF0306F26A2AFDF00667B63 /* VMWizardOSView.swift in Sources */,
83034C0926AB630F006B4BAF /* UTMPendingVMView.swift in Sources */,
84909A8F27CACD5C005605F1 /* UTMPlaceholderVMView.swift in Sources */,