Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b409e40
feat(storage): add VBDiskResizer core functionality
balcsida Aug 22, 2025
4894b38
feat(storage): add resize support to VBManagedDiskImage
balcsida Aug 22, 2025
25c760b
feat(ui): add resize support to ManagedDiskImageEditor
balcsida Aug 22, 2025
2431fb6
feat(ui): add visual resize indicators and context menu
balcsida Aug 22, 2025
874433f
fix(compilation): resolve build errors for disk resize feature
balcsida Aug 22, 2025
c35e0ae
fix(storage): use proper pattern matching in canBeResized property
balcsida Aug 22, 2025
bf1ffeb
feat(resize): integrate disk resize at VM startup
balcsida Aug 22, 2025
962a500
fix(build): move disk resize methods to existing extension file
balcsida Sep 16, 2025
25e7672
fix(build): resolve remaining compilation errors
balcsida Aug 22, 2025
d98556f
feat(resize): enable VBDiskResizer functionality
balcsida Sep 16, 2025
eb2550f
fix(resize): correct VBDiskResizer file handling for RAW images
balcsida Aug 22, 2025
d1aaba4
feat(resize): add automatic partition expansion and UI progress indic…
balcsida Aug 22, 2025
04fbbf6
fix(build): add resizingDisk case to exhaustive switch statements
balcsida Aug 22, 2025
486ac92
fix(resize): prioritize main APFS container over ISC
balcsida Aug 22, 2025
53d3dff
fix(resize): handle Apple_APFS_Recovery partitions blocking expansion
balcsida Aug 22, 2025
7507d2b
feat(resize): graceful handling of recovery partition constraints
balcsida Aug 22, 2025
7e511f2
feat(resize): aggressive recovery partition handling strategy
balcsida Aug 22, 2025
058d9d7
fix(resize): proper recovery container detection for deletion
balcsida Aug 22, 2025
0978f89
feat(resize): handle SIP-protected recovery partitions gracefully
balcsida Aug 22, 2025
8037564
fix(resize): properly detect and resize APFS containers using diskuti…
balcsida Aug 23, 2025
c92d9c9
fix(resize): optimize disk space usage for raw image resizing
balcsida Aug 23, 2025
6876bc9
fix(resize): correct device name parsing for APFS container detection
balcsida Aug 23, 2025
f1ff9d6
feat(resize): add fallback strategies for VM disk APFS containers
balcsida Aug 23, 2025
4a0c580
fix: resolve compiler warnings for unused variables and unreachable code
balcsida Aug 23, 2025
551953d
fix(ui): remove redundant disk resize alert
balcsida Sep 17, 2025
a907b03
feat(resize): surface detailed disk resize progress
balcsida Sep 17, 2025
f3e4e77
chore: update BuddyKit to latest
balcsida Sep 17, 2025
b38f02f
feat(resize): teach disk resizer about locked apfs volumes
balcsida Sep 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 63 additions & 38 deletions VirtualBuddy.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion VirtualBuddy/Config/Signing.xcconfig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
CODE_SIGN_IDENTITY = Apple Development
VB_BUNDLE_ID_PREFIX =
VB_BUNDLE_ID_PREFIX = com.yourname.

GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID = $(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddyGuestHelper
GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID_STR=@"$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID)"
Expand Down
27 changes: 27 additions & 0 deletions VirtualCore/Source/Models/Configuration/ConfigurationModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ public struct VBManagedDiskImage: Identifiable, Hashable, Codable {
}
}
}

public var displayName: String {
switch self {
case .raw:
return "Raw Image"
case .dmg:
return "Disk Image (DMG)"
case .sparse:
return "Sparse Image"
case .asif:
return "Apple Silicon Image"
}
}
}

public var id: String = UUID().uuidString
Expand All @@ -135,6 +148,15 @@ public struct VBManagedDiskImage: Identifiable, Hashable, Codable {
format: .raw
)
}

public var canBeResized: Bool {
switch format {
case .raw, .dmg, .sparse:
return true
case .asif:
return false
}
}
}

/// Configures a storage device.
Expand Down Expand Up @@ -202,6 +224,11 @@ public struct VBStorageDevice: Identifiable, Hashable, Codable {
)
}

public var canBeResized: Bool {
guard case .managedImage(let image) = backing else { return false }
return image.canBeResized
}

public var displayName: String {
guard !isBootVolume else { return "Boot" }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// VBManagedDiskImage+Resize.swift
// VirtualCore
//
// Created by VirtualBuddy on 22/08/25.
//

import Foundation

extension VBManagedDiskImage {

public var canBeResized: Bool {
VBDiskResizer.canResizeFormat(format)
}

public var displayName: String {
format.displayName
}

public func resized(to newSize: UInt64) -> VBManagedDiskImage {
var copy = self
copy.size = newSize
return copy
}

public mutating func resize(to newSize: UInt64, at container: any VBStorageDeviceContainer) async throws {
guard canBeResized else {
throw VBDiskResizeError.unsupportedImageFormat(format)
}

guard newSize > size else {
throw VBDiskResizeError.cannotShrinkDisk
}

guard newSize <= Self.maximumExtraDiskImageSize else {
throw VBDiskResizeError.invalidSize(newSize)
}

let imageURL = container.diskImageURL(for: self)

try await VBDiskResizer.resizeDiskImage(
at: imageURL,
format: format,
newSize: newSize
)

self.size = newSize
}

}

extension VBManagedDiskImage.Format {

public var displayName: String {
switch self {
case .raw:
return "Raw Image"
case .dmg:
return "Disk Image (DMG)"
case .sparse:
return "Sparse Image"
case .asif:
return "Apple Silicon Image"
}
}

public var supportsResize: Bool {
VBDiskResizer.canResizeFormat(self)
}

}

extension VBStorageDevice {

public func canBeResized(in container: any VBStorageDeviceContainer) -> Bool {
guard let managedImage = managedImage else { return false }
guard managedImage.canBeResized else { return false }

let imageURL = container.diskImageURL(for: self)
return FileManager.default.fileExists(atPath: imageURL.path)
}

public func resizeDisk(to newSize: UInt64, in container: any VBStorageDeviceContainer) async throws {
guard var managedImage = managedImage else {
throw VBDiskResizeError.unsupportedImageFormat(.raw)
}

try await managedImage.resize(to: newSize, at: container)
backing = .managedImage(managedImage)
}

}
107 changes: 107 additions & 0 deletions VirtualCore/Source/Models/VBVirtualMachine+Metadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,110 @@ extension URL {
return current
}
}

// MARK: - Disk Resize Support

public extension VBVirtualMachine {

typealias DiskResizeProgressHandler = @MainActor (_ message: String) -> Void

/// Checks if any disk images need resizing based on configuration vs actual size
func checkAndResizeDiskImages(progressHandler: DiskResizeProgressHandler? = nil) async throws {
let config = configuration

func report(_ message: String) async {
guard let progressHandler else { return }
await MainActor.run {
progressHandler(message)
}
}

let resizableDevices = config.hardware.storageDevices.compactMap { device -> (VBStorageDevice, VBManagedDiskImage)? in
guard case .managedImage(let image) = device.backing else { return nil }
guard image.canBeResized else { return nil }
return (device, image)
}

guard !resizableDevices.isEmpty else {
await report("Disk images already match their configured sizes.")
return
}

let formatter: ByteCountFormatter = {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useGB, .useMB, .useTB]
formatter.countStyle = .binary
formatter.includesUnit = true
return formatter
}()

for (index, entry) in resizableDevices.enumerated() {
let (device, image) = entry
let position = index + 1
let total = resizableDevices.count
let deviceName = device.displayName

await report("Checking \(deviceName) (\(position)/\(total))...")

let imageURL = diskImageURL(for: image)

guard FileManager.default.fileExists(atPath: imageURL.path) else {
await report("Skipping \(deviceName): disk image not found.")
continue
}

let attributes = try FileManager.default.attributesOfItem(atPath: imageURL.path)
let actualSize = attributes[.size] as? UInt64 ?? 0

if image.size > actualSize {
let targetDescription = formatter.string(fromByteCount: Int64(image.size))
await report("Expanding \(deviceName) to \(targetDescription) (\(position)/\(total))...")

try await resizeDiskImage(image, to: image.size)

await report("\(deviceName) expanded successfully.")
} else if image.size < actualSize {
let actualDescription = formatter.string(fromByteCount: Int64(actualSize))
await report("\(deviceName) exceeds the configured size (\(actualDescription)); no changes made.")
} else {
let currentDescription = formatter.string(fromByteCount: Int64(actualSize))
await report("\(deviceName) already uses \(currentDescription).")
}
}

await report("Disk image checks complete.")
}

/// Resizes a managed disk image to the specified size
private func resizeDiskImage(_ image: VBManagedDiskImage, to newSize: UInt64) async throws {
let imageURL = diskImageURL(for: image)
NSLog("Resizing disk image at \(imageURL.path) from current size to \(newSize) bytes")

try await VBDiskResizer.resizeDiskImage(
at: imageURL,
format: image.format,
newSize: newSize
)

NSLog("Successfully resized disk image at \(imageURL.path) to \(newSize) bytes")
}

/// Validates that all disk images can be resized if needed
func validateDiskResizeCapability() -> [(device: VBStorageDevice, canResize: Bool)] {
let config = configuration

return config.hardware.storageDevices.compactMap { device in
guard case .managedImage(let image) = device.backing else { return nil }

let imageURL = diskImageURL(for: image)
let exists = FileManager.default.fileExists(atPath: imageURL.path)

if !exists {
// New image, no resize needed
return nil
}

return (device: device, canResize: image.canBeResized)
}
}
}
Loading