Repository: insidegui/VirtualBuddy Branch: main Commit: 6a331eb4ef8c Files: 410 Total size: 1.5 MB Directory structure: gitextract_fi8xynku/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── DeepLinkSecurity/ │ ├── DeepLinkSecurity.h │ └── Source/ │ ├── Base/ │ │ └── DeepLinkSecurityDefines.swift │ ├── DeepLinkSentinel.swift │ ├── Models/ │ │ ├── DeepLinkClient.swift │ │ ├── DeepLinkClientDescriptor.swift │ │ ├── Extensions/ │ │ │ ├── DeepLinkClient+Crypto.swift │ │ │ └── DeepLinkClientDescriptor+.swift │ │ └── OpenDeepLinkRequest.swift │ ├── Storage/ │ │ ├── DeepLinkAuthStore.swift │ │ ├── DeepLinkManagementStore.swift │ │ ├── KeychainDeepLinkAuthStore.swift │ │ ├── MemoryDeepLinkAuthStore.swift │ │ └── UserDefaultsDeepLinkManagementStore.swift │ └── UI/ │ └── DeepLinkAuthUI.swift ├── LICENSE ├── README.md ├── ReleaseNotes/ │ ├── VirtualBuddy 1.2 Release Nodes.md │ ├── VirtualBuddy 1.2.1 Release Notes.md │ └── VirtualBuddy 1.2.2 Release Notes.md ├── VirtualBuddy/ │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon-Default.appiconset/ │ │ │ └── Contents.json │ │ ├── AppIcon-Dev.appiconset/ │ │ │ └── Contents.json │ │ ├── AppIcon-zBeta.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Automation/ │ │ ├── DeepLinkHandler.swift │ │ ├── Support/ │ │ │ └── DeepLinkAuthDialog.swift │ │ └── VirtualBuddyDeepLinks.swift │ ├── Bootstrap/ │ │ ├── SoftwareUpdateController.swift │ │ ├── VirtualBuddyApp.swift │ │ ├── VirtualBuddyAppDelegate.swift │ │ └── VirtualBuddyEntryPoint.swift │ ├── CommandLine/ │ │ ├── VirtualBuddyCLI.swift │ │ └── vctool/ │ │ ├── BlurHashCommand.swift │ │ ├── CatalogCommand.swift │ │ ├── Core/ │ │ │ ├── BuildManifest+Fetch.swift │ │ │ ├── BuildManifest.swift │ │ │ ├── Helpers.swift │ │ │ ├── TreeStringConvertible.swift │ │ │ └── URL+ContentLength.swift │ │ ├── GroupCommand.swift │ │ ├── IPSWCommand.swift │ │ ├── ImageCommand.swift │ │ ├── MigrateCommand.swift │ │ ├── MobileDeviceCommand.swift │ │ ├── ResolveCommand.swift │ │ └── VCTool.swift │ ├── Config/ │ │ ├── AppTarget.xcconfig │ │ ├── Entitlements/ │ │ │ ├── VirtualBuddy.entitlements │ │ │ └── VirtualBuddy_Managed.entitlements │ │ ├── Features.xcconfig │ │ ├── InfoPlist.xcconfig │ │ ├── Main.xcconfig │ │ ├── Paths.xcconfig │ │ ├── Signing.xcconfig │ │ └── Versions.xcconfig │ ├── Info.plist │ └── Preview Content/ │ └── Preview Assets.xcassets/ │ └── Contents.json ├── VirtualBuddy.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ ├── xcdebugger/ │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes/ │ ├── DeepLinkSecurity.xcscheme │ ├── VirtualBuddy (Dev Release).xcscheme │ ├── VirtualBuddy (Managed - Beta).xcscheme │ ├── VirtualBuddy (Managed).xcscheme │ ├── VirtualBuddy.xcscheme │ ├── VirtualBuddyGuest.xcscheme │ ├── VirtualCore.xcscheme │ ├── VirtualUI.xcscheme │ ├── VirtualWormhole.xcscheme │ └── vctool.xcscheme ├── VirtualBuddyGuest/ │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── StatusItem.imageset/ │ │ └── Contents.json │ ├── Dashboard/ │ │ ├── GuestDashboard.swift │ │ ├── GuestDefaultsImportView.swift │ │ ├── GuestSharedFoldersManager.swift │ │ ├── HostConnectionStateProvider.swift │ │ └── Support/ │ │ ├── GuestLaunchAtLoginManager.swift │ │ └── VirtualBuddyGuest-Bridging-Header.h │ ├── GuestAppDelegate.swift │ ├── GuestAppInstaller.swift │ ├── Main.storyboard │ ├── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ └── Contents.json │ └── VirtualBuddyGuest.entitlements ├── VirtualBuddyGuestHelper/ │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── Main.storyboard │ ├── GuestHelperAppDelegate.swift │ └── VirtualBuddyGuestHelper.entitlements ├── VirtualCore/ │ ├── Source/ │ │ ├── Definitions/ │ │ │ ├── Logging.swift │ │ │ ├── PreviewSupport.swift │ │ │ └── VirtualCoreConstants.swift │ │ ├── GuestSupport/ │ │ │ ├── CreateGuestImage.sh │ │ │ └── GuestAdditionsDiskImage.swift │ │ ├── Headers/ │ │ │ └── VirtualizationPrivate.h │ │ ├── Import/ │ │ │ ├── UTM/ │ │ │ │ ├── UTMAppleConfiguration.swift │ │ │ │ └── UTMImporter.swift │ │ │ ├── VMImporter+Helpers.swift │ │ │ ├── VMImporter.swift │ │ │ └── VMImporterRegistry.swift │ │ ├── Models/ │ │ │ ├── BlurHashToken.swift │ │ │ ├── Configuration/ │ │ │ │ ├── ConfigurationModels+Summary.swift │ │ │ │ ├── ConfigurationModels+Validation.swift │ │ │ │ ├── ConfigurationModels.swift │ │ │ │ ├── DecodableDefault.swift │ │ │ │ └── VBMacDevice+Storage.swift │ │ │ ├── SavedState/ │ │ │ │ ├── VBSavedStateMetadata+Clone.swift │ │ │ │ ├── VBSavedStateMetadata.swift │ │ │ │ ├── VBSavedStatePackage+VM.swift │ │ │ │ ├── VBSavedStatePackage.swift │ │ │ │ └── VMLibraryController+SavedState.swift │ │ │ ├── VBError.swift │ │ │ ├── VBNVRAMVariable.swift │ │ │ ├── VBStorageDeviceContainer.swift │ │ │ ├── VBVirtualMachine+Metadata.swift │ │ │ ├── VBVirtualMachine+Screenshot.swift │ │ │ └── VBVirtualMachine.swift │ │ ├── ReleaseTrains/ │ │ │ ├── AppUpdateChannel.swift │ │ │ └── VBBuildType.swift │ │ ├── Resources/ │ │ │ ├── Preview/ │ │ │ │ ├── FakeRestoreImage.ipsw │ │ │ │ ├── PreviewLinux.vbvm/ │ │ │ │ │ ├── .vbdata/ │ │ │ │ │ │ ├── Config.plist │ │ │ │ │ │ ├── Metadata.plist │ │ │ │ │ │ ├── Screenshot.heic │ │ │ │ │ │ └── Thumbnail.heic │ │ │ │ │ └── Disk.img │ │ │ │ ├── PreviewLinuxBlurHash.vbvm/ │ │ │ │ │ ├── .vbdata/ │ │ │ │ │ │ ├── Config.plist │ │ │ │ │ │ └── Metadata.plist │ │ │ │ │ └── Disk.img │ │ │ │ ├── PreviewLinuxNoArtwork.vbvm/ │ │ │ │ │ ├── .vbdata/ │ │ │ │ │ │ ├── Config.plist │ │ │ │ │ │ └── Metadata.plist │ │ │ │ │ └── Disk.img │ │ │ │ ├── PreviewMac.vbvm/ │ │ │ │ │ ├── .vbdata/ │ │ │ │ │ │ ├── Config.plist │ │ │ │ │ │ ├── Metadata.plist │ │ │ │ │ │ └── Thumbnail.heic │ │ │ │ │ └── Disk.img │ │ │ │ ├── PreviewMacBlurHash.vbvm/ │ │ │ │ │ ├── .vbdata/ │ │ │ │ │ │ ├── Config.plist │ │ │ │ │ │ └── Metadata.plist │ │ │ │ │ └── Disk.img │ │ │ │ ├── PreviewMacNoArtwork.vbvm/ │ │ │ │ │ ├── .vbdata/ │ │ │ │ │ │ ├── Config.plist │ │ │ │ │ │ └── Metadata.plist │ │ │ │ │ └── Disk.img │ │ │ │ ├── PreviewSavedStates/ │ │ │ │ │ ├── Save-2024-03-27_16;06;24.vbst/ │ │ │ │ │ │ ├── Info.plist │ │ │ │ │ │ ├── Screenshot.heic │ │ │ │ │ │ └── Thumbnail.heic │ │ │ │ │ ├── Save-2024-03-27_16;07;04.vbst/ │ │ │ │ │ │ ├── Info.plist │ │ │ │ │ │ ├── Screenshot.heic │ │ │ │ │ │ └── Thumbnail.heic │ │ │ │ │ ├── Save-2024-03-27_16;08;06.vbst/ │ │ │ │ │ │ ├── Info.plist │ │ │ │ │ │ ├── Screenshot.heic │ │ │ │ │ │ └── Thumbnail.heic │ │ │ │ │ ├── Save-2024-03-27_16;08;28.vbst/ │ │ │ │ │ │ ├── Info.plist │ │ │ │ │ │ ├── Screenshot.heic │ │ │ │ │ │ └── Thumbnail.heic │ │ │ │ │ └── Save-2024-03-27_16;08;51.vbst/ │ │ │ │ │ ├── Info.plist │ │ │ │ │ ├── Screenshot.heic │ │ │ │ │ └── Thumbnail.heic │ │ │ │ └── _Downloads/ │ │ │ │ ├── UniversalMac_13.3.1_22E261_Restore.ipsw │ │ │ │ ├── UniversalMac_14.0_23A344_Restore.ipsw │ │ │ │ ├── UniversalMac_14.5_23F79_Restore.ipsw │ │ │ │ ├── UniversalMac_15.3_24D60_Restore.ipsw │ │ │ │ └── UniversalMac_15.5_24F74_Restore.ipsw │ │ │ └── VirtualCore.xcassets/ │ │ │ ├── Adjectives.dataset/ │ │ │ │ ├── Adjectives.txt │ │ │ │ └── Contents.json │ │ │ ├── Animals.dataset/ │ │ │ │ ├── Animals.txt │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Restore/ │ │ │ ├── Download/ │ │ │ │ ├── DownloadBackend.swift │ │ │ │ ├── SimulatedDownloadBackend.swift │ │ │ │ └── URLSessionDownloadBackend.swift │ │ │ └── Installation/ │ │ │ ├── RestoreBackend.swift │ │ │ ├── SimulatedRestoreBackend.swift │ │ │ └── VirtualizationRestoreBackend.swift │ │ ├── Restore Images/ │ │ │ ├── Models/ │ │ │ │ ├── VBRestoreImageInfo.swift │ │ │ │ └── VBRestoreImagesResponse.swift │ │ │ └── VBAPIClient.swift │ │ ├── Settings/ │ │ │ ├── VBSettings+CatalogDownload.swift │ │ │ ├── VBSettings.swift │ │ │ └── VBSettingsContainer.swift │ │ ├── Utilities/ │ │ │ ├── Bundle+Version.swift │ │ │ ├── LogStreamer.swift │ │ │ ├── PreventTerminationAssertion.swift │ │ │ ├── ProcessInfo+ECID.swift │ │ │ ├── VBMemoryLeakDebugAssertions.swift │ │ │ ├── VolumeUtils.swift │ │ │ └── WeakReference.swift │ │ ├── VirtualCatalog/ │ │ │ ├── LegacyCatalog.swift │ │ │ ├── MobileDeviceFramework.swift │ │ │ ├── README.md │ │ │ ├── ResolvedCatalog.swift │ │ │ ├── SoftwareCatalog+DownloadMatching.swift │ │ │ ├── SoftwareCatalog.swift │ │ │ └── Utilities/ │ │ │ ├── BlurHashEncode.swift │ │ │ ├── String+AppleOSBuild.swift │ │ │ └── URL+ExtendedAttributes.swift │ │ └── Virtualization/ │ │ ├── Helpers/ │ │ │ ├── CatalogExtensions.swift │ │ │ ├── DirectoryObserver.swift │ │ │ ├── DiskImageGenerator.swift │ │ │ ├── LinuxVirtualMachineConfigurationHelper.swift │ │ │ ├── MacOSVirtualMachineConfigurationHelper.swift │ │ │ ├── RandomNameGenerator.swift │ │ │ ├── VBDebugUtil.h │ │ │ ├── VBDebugUtil.m │ │ │ ├── VZVirtualMachineConfiguration+NVRAM.swift │ │ │ └── VirtualMachineConfigurationHelper.swift │ │ ├── Screenshot/ │ │ │ ├── NSImage+DRMProtected.swift │ │ │ └── NSImage+HEIC.swift │ │ ├── VBVirtualMachine+Virtualization.swift │ │ ├── VMController.swift │ │ ├── VMInstance.swift │ │ ├── VMLibraryController.swift │ │ └── VMSavedStatesController.swift │ └── VirtualCore.h ├── VirtualUI/ │ ├── Resources/ │ │ ├── CatalogGroupPlaceholder.heic │ │ └── VirtualUI.xcassets/ │ │ ├── Contents.json │ │ ├── FirstLaunchExperience.dataset/ │ │ │ ├── Contents.json │ │ │ └── FirstLaunchExperience.caar │ │ ├── FullBleedBlurHash.dataset/ │ │ │ ├── Contents.json │ │ │ └── FullBleedBlurHash.caar │ │ ├── GuestSymbol.imageset/ │ │ │ └── Contents.json │ │ ├── StatusItemPanelChromeBorder.colorset/ │ │ │ └── Contents.json │ │ ├── ThumbnailPlaceholder.imageset/ │ │ │ └── Contents.json │ │ ├── VBGuestType/ │ │ │ ├── Contents.json │ │ │ ├── linux.imageset/ │ │ │ │ └── Contents.json │ │ │ └── mac.imageset/ │ │ │ └── Contents.json │ │ ├── VirtualBuddyMono.imageset/ │ │ │ └── Contents.json │ │ ├── VirtualBuddyMonoHappy.imageset/ │ │ │ └── Contents.json │ │ └── VirtualBuddyMonoSad.imageset/ │ │ └── Contents.json │ ├── Source/ │ │ ├── Building Blocks/ │ │ │ ├── CGFloat+OnePixel.swift │ │ │ ├── ChromeBorderModifier.swift │ │ │ ├── Configuration Controls/ │ │ │ │ ├── EphemeralTextField.swift │ │ │ │ ├── NumericPropertyControl.swift │ │ │ │ ├── NumericValueField.swift │ │ │ │ ├── PropertyControl.swift │ │ │ │ └── SharedFocusEnvironment.swift │ │ │ ├── ControlGroupChrome.swift │ │ │ └── SliderConversion.swift │ │ ├── Components/ │ │ │ ├── Array+Navigation.swift │ │ │ ├── BackportedContentUnavailableView.swift │ │ │ ├── BlurHash/ │ │ │ │ └── BlurHashDecoding.swift │ │ │ ├── CALayer+Asset.swift │ │ │ ├── DecentFormView.swift │ │ │ ├── EqualWidthHStack.swift │ │ │ ├── HostingWindowController/ │ │ │ │ ├── FB18383725Window.swift │ │ │ │ ├── HostingWindowController.swift │ │ │ │ ├── OpenCocoaWindowAction.swift │ │ │ │ ├── VBRestorableWindow+Resizing.swift │ │ │ │ ├── VBRestorableWindow.swift │ │ │ │ └── WindowEnvironment.swift │ │ │ ├── KeyboardNavigationModifier.swift │ │ │ ├── LogConsole.swift │ │ │ ├── MaterialView.swift │ │ │ ├── NSAlert+Confirmation.swift │ │ │ ├── OnAppearOnce.swift │ │ │ ├── OpenSavePanelUtils.swift │ │ │ ├── RemoteImage/ │ │ │ │ └── RemoteImage.swift │ │ │ ├── SelfSizingGroupedForm.swift │ │ │ ├── SwiftUI Status Item/ │ │ │ │ ├── Components/ │ │ │ │ │ ├── Cocoa/ │ │ │ │ │ │ ├── StatusBarContentPanel.swift │ │ │ │ │ │ ├── StatusBarHighlightView.swift │ │ │ │ │ │ ├── StatusItemMenuBarExtraView.swift │ │ │ │ │ │ ├── StatusItemPanelContentController.swift │ │ │ │ │ │ └── VUIAppKitViewControllerHost.swift │ │ │ │ │ ├── ObjC/ │ │ │ │ │ │ ├── NSApplication+MenuBar.h │ │ │ │ │ │ ├── NSApplication+MenuBar.m │ │ │ │ │ │ ├── NSStatusBarPrivate.h │ │ │ │ │ │ ├── NSStatusItem+.h │ │ │ │ │ │ └── NSStatusItem+.m │ │ │ │ │ ├── ScreenChangeModifier.swift │ │ │ │ │ ├── StatusBarPanelChrome.swift │ │ │ │ │ ├── StatusItemButton.swift │ │ │ │ │ └── StatusItemProviderProtocol.swift │ │ │ │ └── StatusItemManager.swift │ │ │ └── VMArtworkView.swift │ │ ├── Definitions/ │ │ │ ├── PreviewSupport-VirtualUI.swift │ │ │ └── VirtualUIConstants.swift │ │ ├── Installer/ │ │ │ ├── Components/ │ │ │ │ ├── AuthenticatingWebView.swift │ │ │ │ ├── InstallationConsole.swift │ │ │ │ ├── InstallationWizardTitle.swift │ │ │ │ ├── RestoreImageURLInputView.swift │ │ │ │ ├── VirtualBuddyInstallerInputView.swift │ │ │ │ └── VirtualMachineNameInputView.swift │ │ │ ├── Steps/ │ │ │ │ ├── GuestTypePicker.swift │ │ │ │ ├── InstallConfigurationStepView.swift │ │ │ │ ├── InstallMethod.swift │ │ │ │ ├── InstallMethodPicker.swift │ │ │ │ ├── InstallProgressStepView.swift │ │ │ │ ├── Restore Image Selection/ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ ├── BlurHashFullBleedBackground.swift │ │ │ │ │ │ ├── CatalogGroupPicker.swift │ │ │ │ │ │ ├── CatalogGroupView.swift │ │ │ │ │ │ ├── InstallProgressDisplayView.swift │ │ │ │ │ │ ├── RestoreImageBrowser.swift │ │ │ │ │ │ ├── SoftwareCatalog+Placeholder.swift │ │ │ │ │ │ ├── VirtualBuddyMonoIcon.swift │ │ │ │ │ │ ├── VirtualBuddyMonoProgressView.swift │ │ │ │ │ │ └── VirtualDisplayView.swift │ │ │ │ │ ├── RestoreImageSelectionController.swift │ │ │ │ │ └── RestoreImageSelectionStep.swift │ │ │ │ └── RestoreImageDownloadView.swift │ │ │ ├── VMInstallData.swift │ │ │ ├── VMInstallationViewModel.swift │ │ │ └── VMInstallationWizard.swift │ │ ├── Library/ │ │ │ ├── Components/ │ │ │ │ ├── FirstLaunchExperienceView.swift │ │ │ │ └── LibraryItemView.swift │ │ │ └── LibraryView.swift │ │ ├── Session/ │ │ │ ├── Components/ │ │ │ │ ├── ContinuousProgressIndicator.swift │ │ │ │ ├── MaskProgressView.swift │ │ │ │ ├── NumberDisplayMode.swift │ │ │ │ ├── SavedStatePicker.swift │ │ │ │ ├── SwiftUIVMView.swift │ │ │ │ ├── VMProgressOverlay.swift │ │ │ │ └── VirtualMachineControls.swift │ │ │ ├── Configuration/ │ │ │ │ └── VMSessionConfigurationView.swift │ │ │ ├── VirtualMachineSessionUI.swift │ │ │ ├── VirtualMachineSessionUIManager.swift │ │ │ └── VirtualMachineSessionView.swift │ │ ├── Settings/ │ │ │ ├── AutomationSettingsView.swift │ │ │ ├── Components/ │ │ │ │ ├── BackwardsCompatibility.swift │ │ │ │ ├── FileSystemPathFormControl.swift │ │ │ │ ├── OpenVirtualBuddySettingsAction.swift │ │ │ │ ├── SettingsFooter.swift │ │ │ │ └── VerticalLabeledContentStyle.swift │ │ │ ├── GeneralSettingsView.swift │ │ │ ├── SettingsScreen.swift │ │ │ └── VirtualizationSettingsView.swift │ │ └── VM Configuration/ │ │ ├── Components/ │ │ │ ├── ConfigurationSection.swift │ │ │ └── GroupedList.swift │ │ ├── Sections/ │ │ │ ├── DisplayConfigurationView.swift │ │ │ ├── GuestAppConfigurationView.swift │ │ │ ├── HardwareConfigurationView.swift │ │ │ ├── KeyboardDeviceConfigurationView.swift │ │ │ ├── NetworkConfigurationView.swift │ │ │ ├── PointingDeviceConfigurationView.swift │ │ │ ├── Sharing/ │ │ │ │ ├── SharedFolderListItem.swift │ │ │ │ ├── SharedFoldersManagementView.swift │ │ │ │ └── SharingConfigurationView.swift │ │ │ ├── SoundConfigurationView.swift │ │ │ └── Storage/ │ │ │ ├── ManagedDiskImageEditor.swift │ │ │ ├── StorageConfigurationView.swift │ │ │ └── StorageDeviceDetailView.swift │ │ ├── VMConfigurationSheet.swift │ │ ├── VMConfigurationView.swift │ │ └── VMConfigurationViewModel.swift │ └── VirtualUI.h ├── VirtualWormhole/ │ ├── Source/ │ │ ├── Definitions/ │ │ │ └── VirtualWormholeConstants.swift │ │ ├── Services/ │ │ │ ├── Base/ │ │ │ │ ├── WormholeServiceClient.swift │ │ │ │ └── WormholeServiceProtocol.swift │ │ │ ├── DarwinNotifications/ │ │ │ │ ├── SystemNotification.swift │ │ │ │ └── WHDarwinNotificationsService.swift │ │ │ ├── DefaultsImport/ │ │ │ │ ├── Implementation/ │ │ │ │ │ ├── DefaultsDomain+ExportImport.swift │ │ │ │ │ ├── DefaultsDomainDescriptor.swift │ │ │ │ │ └── DefaultsImportController.swift │ │ │ │ ├── Resources/ │ │ │ │ │ └── DefaultsDomains.plist │ │ │ │ ├── WHDefaultsImportClient.swift │ │ │ │ └── WHDefaultsImportService.swift │ │ │ ├── DesktopPicture/ │ │ │ │ ├── CGImage+FullyTransparent.swift │ │ │ │ ├── NSImage+DesktopPicture.h │ │ │ │ ├── NSImage+DesktopPicture.m │ │ │ │ └── WHDesktopPictureService.swift │ │ │ └── WHSharedClipboardService.swift │ │ ├── WireProtocol/ │ │ │ ├── WHPayload.swift │ │ │ ├── WHPing.swift │ │ │ └── WormholePacket.swift │ │ └── WormholeManager.swift │ └── VirtualWormhole.h ├── VirtualWormholeTests/ │ └── WormholePacketTests.swift └── data/ ├── images/ │ ├── debian-dark-thumbnail.heic │ ├── debian-dark.heic │ ├── debian-thumbnail.heic │ ├── debian.heic │ ├── fedora-dark-thumbnail.heic │ ├── fedora-dark.heic │ ├── fedora-thumbnail.heic │ ├── fedora.heic │ ├── jammyjellyfish-dark-thumbnail.heic │ ├── jammyjellyfish-dark.heic │ ├── jammyjellyfish-thumbnail.heic │ ├── jammyjellyfish.heic │ ├── kali-dark-thumbnail.heic │ ├── kali-dark.heic │ ├── kali-thumbnail.heic │ ├── kali.heic │ ├── monterey-dark-thumbnail.heic │ ├── monterey-dark.heic │ ├── monterey-thumbnail.heic │ ├── monterey.heic │ ├── sequoia-dark-thumbnail.heic │ ├── sequoia-dark.heic │ ├── sequoia-thumbnail.heic │ ├── sequoia.heic │ ├── sonoma-dark-thumbnail.heic │ ├── sonoma-dark.heic │ ├── sonoma-thumbnail.heic │ ├── sonoma.heic │ ├── tahoe-dark-thumbnail.heic │ ├── tahoe-dark.heic │ ├── tahoe-thumbnail.heic │ ├── tahoe.heic │ ├── ventura-dark-thumbnail.heic │ ├── ventura-dark.heic │ ├── ventura-thumbnail.heic │ └── ventura.heic ├── ipsws_v1.json ├── ipsws_v2.json ├── linux_v1.json └── linux_v2.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: insidegui ================================================ FILE: .gitignore ================================================ .DS_Store __MACOSX *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata profile *.moved-aside DerivedData .idea/ Crashlytics.sh generatechangelog.sh Pods/ Carthage Provisioning Crashlytics.sh Sharing.h Tests/Private Design/Icon Build/ MockServer/ ================================================ FILE: DeepLinkSecurity/DeepLinkSecurity.h ================================================ // // DeepLinkSecurity.h // DeepLinkSecurity // // Created by Guilherme Rambo on 12/10/23. // #import //! Project version number for DeepLinkSecurity. FOUNDATION_EXPORT double DeepLinkSecurityVersionNumber; //! Project version string for DeepLinkSecurity. FOUNDATION_EXPORT const unsigned char DeepLinkSecurityVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import ================================================ FILE: DeepLinkSecurity/Source/Base/DeepLinkSecurityDefines.swift ================================================ import Foundation import OSLog struct DeepLinkSecurityDefines { static let subsystem = "codes.rambo.DeepLinkSecurity" } struct DeepLinkError: LocalizedError { var errorDescription: String? init(_ errorDescription: String) { self.errorDescription = errorDescription } } extension Logger { static func deepLinkLogger(for type: T.Type) -> Logger { Logger(subsystem: DeepLinkSecurityDefines.subsystem, category: String(describing: type)) } } ================================================ FILE: DeepLinkSecurity/Source/DeepLinkSentinel.swift ================================================ import Cocoa import OSLog public final class DeepLinkSentinel: ObservableObject { private lazy var logger = Logger.deepLinkLogger(for: Self.self) public let authUI: DeepLinkAuthUI public let authStore: DeepLinkAuthStore public let managementStore: DeepLinkManagementStore public init(authUI: DeepLinkAuthUI, authStore: DeepLinkAuthStore, managementStore: DeepLinkManagementStore) { self.authUI = authUI self.authStore = authStore self.managementStore = managementStore } /// Sets the sentinel as the handler for URL scheme Apple Events. public func installAppleEventHandler() { NSAppleEventManager.shared().setEventHandler( self, andSelector: #selector(DeepLinkSentinel.handleURLEvent(_:replyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL) ) } /// Delivers open URL events once they've been authenticated. /// Open URL requests that fail authentication are not delivered. public private(set) lazy var openURL: AsyncStream = { AsyncStream { [weak self] continuation in self?.onOpenURL = { url in continuation.yield(url) } } }() private var onOpenURL: (URL) -> Void = { _ in } @objc func handleURLEvent(_ event: NSAppleEventDescriptor, replyEvent: NSAppleEventDescriptor) { logger.debug(#function) guard let urlStr = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue else { logger.fault("Failed to get string from URL open event") return } logger.debug("Handling URL \(urlStr)") guard let tokenDescriptor = event.attributeDescriptor(forKeyword: AEKeyword(keySenderAuditTokenAttr))?.copy() as? NSAppleEventDescriptor else { logger.error("Missing Apple Event sender audit token in URL event") return } guard let url = URL(string: urlStr) else { logger.error("Invalid deep link URL: \"\(urlStr)\"") return } Task { await authenticate(descriptor: tokenDescriptor, url: url) } } func storeDescriptor(_ descriptor: DeepLinkClientDescriptor, with authorization: DeepLinkClientAuthorization = .undetermined) async { do { var updatedDescriptor = descriptor updatedDescriptor.authorization = authorization try await managementStore.insert(updatedDescriptor) } catch { logger.warning("Failed to store management descriptor: \(error, privacy: .public)") } } func setAuthorization(_ result: DeepLinkClientAuthorization, for client: DeepLinkClient, descriptor: DeepLinkClientDescriptor) async { do { try await authStore.setAuthorization(result, for: client) /// Update the client management descriptor with the new authorization. await storeDescriptor(descriptor, with: result) } catch { logger.warning("Failed to store authorization decision: \(error, privacy: .public)") } } public func setAuthorization(_ result: DeepLinkClientAuthorization, for descriptor: DeepLinkClientDescriptor) async throws { let client = try DeepLinkClient(url: descriptor.url) try await authStore.setAuthorization(result, for: client) /// Update the client management descriptor with the new authorization. await storeDescriptor(descriptor, with: result) } private func authenticate(descriptor: NSAppleEventDescriptor, url: URL) async { do { let client = try DeepLinkClient(auditTokenDescriptor: descriptor) let clientDescriptor = DeepLinkClientDescriptor(client: client) do { /// Store an initial entry for the client descriptor if it doesn't exist yet. let descriptorExists = await managementStore.hasDescriptor(with: clientDescriptor.id) if !descriptorExists { try await managementStore.insert(clientDescriptor) } } catch { logger.warning("Failed to store management descriptor before auth: \(error, privacy: .public)") } let request = OpenDeepLinkRequest(url: url, client: clientDescriptor) do { var authorization = await existingAuthorization(for: client) switch authorization { case .undetermined: logger.debug("No existing authorization for \(client.designatedRequirement), prompting") authorization = try await authUI.presentDeepLinkAuth(for: request) guard authorization != .undetermined else { logger.warning("Auth UI ended with undetermined authorization, denying current request without modifying auth store") return } await setAuthorization(authorization, for: client, descriptor: clientDescriptor) case .authorized: logger.debug("Got existing client authorization for \(client.designatedRequirement)") case .denied: logger.warning("Denying open URL: client authorization denied for \(client.designatedRequirement)") return } /// Just being extra paranoid here. guard authorization != .denied else { return } logger.notice("Successfully authenticated deep link request for opening \(url)") await MainActor.run { onOpenURL(url) } } catch { logger.error("Deep link authentication failed: \(error, privacy: .public)") } } catch { logger.error("Failed to get client information: \(error, privacy: .public)") } } private func existingAuthorization(for client: DeepLinkClient) async -> DeepLinkClientAuthorization { let auth = await authStore.authorization(for: client) guard auth == .authorized else { return auth } do { try await client.validate() return .authorized } catch { logger.warning("Client signature validated for \(client.designatedRequirement), prompting again. Error: \(error, privacy: .public)") return .undetermined } } } extension NSAppleEventDescriptor: @retroactive @unchecked Sendable { } ================================================ FILE: DeepLinkSecurity/Source/Models/DeepLinkClient.swift ================================================ import Foundation import CryptoKit /// Represents an app/process that's attempting to open a deep link in the app. public struct DeepLinkClient: Identifiable { /// The client ID is a SHA256 hash of its designated CS requirement. public var id: String /// The bundle URL for the app or executable URL for the process. public var url: URL /// The designated code signing requirement for the client. /// This is hashed and used as a key to store the user's decision, /// and it's also used in order to verify that the client's code signature is valid. public var designatedRequirement: String public init(url: URL, designatedRequirement: String) { self.url = url self.designatedRequirement = designatedRequirement self.id = SHA256.hash(data: Data(designatedRequirement.utf8)) .map { String(format: "%02X", $0) } .joined() } } ================================================ FILE: DeepLinkSecurity/Source/Models/DeepLinkClientDescriptor.swift ================================================ import Cocoa /// Describes metadata for a client that's previously requested deep link authorization. /// See ``DeepLinkManagementStore``. public struct DeepLinkClientDescriptor: Identifiable, Hashable, Codable { public struct Icon: Hashable, Codable { public var image: NSImage } /// Unique identifier for the client. public var id: String /// The client's main bundle or executable URL. public var url: URL /// The client's bundle identifier, if available. public var bundleIdentifier: String? /// A user-friendly name for the client. public var displayName: String /// Icon image representing the client app or executable. public var icon: Icon /// The current authorization state for the client. public var authorization: DeepLinkClientAuthorization /// Will be `false` if the descriptor's client could no longer be found on the filesystem. public var isValid: Bool } ================================================ FILE: DeepLinkSecurity/Source/Models/Extensions/DeepLinkClient+Crypto.swift ================================================ import Cocoa public extension DeepLinkClient { init(auditTokenDescriptor: NSAppleEventDescriptor) throws { let attrs = [kSecGuestAttributeAudit: auditTokenDescriptor.data] var client: SecCode! try checkSecError(SecCodeCopyGuestWithAttributes(nil, attrs as CFDictionary, .default, &client), task: "copy client cs attributes") var staticCode: SecStaticCode! try checkSecError(SecCodeCopyStaticCode(client, .default, &staticCode), task: "copy client static code") var url: CFURL! try checkSecError(SecCodeCopyPath(staticCode, .default, &url), task: "copy client path") var requirement: SecRequirement! try checkSecError(SecCodeCopyDesignatedRequirement(staticCode, .default, &requirement), task: "copy client designated requirement") var requirementText: CFString! try checkSecError(SecRequirementCopyString(requirement, .default, &requirementText), task: "copy client designated requirement text") self.init(url: url as URL, designatedRequirement: requirementText as String) } init(url: URL) throws { var staticCode: SecStaticCode! try checkSecError(SecStaticCodeCreateWithPath(url as CFURL, .default, &staticCode), task: "load client static code") var requirement: SecRequirement! try checkSecError(SecCodeCopyDesignatedRequirement(staticCode, .default, &requirement), task: "copy client designated requirement") var requirementText: CFString! try checkSecError(SecRequirementCopyString(requirement, .default, &requirementText), task: "copy client designated requirement text") self.init(url: url, designatedRequirement: requirementText as String) } /// Validates the client's static code against the designated requirement, /// throwing an error if the signature doesn't match. func validate() async throws { var requirement: SecRequirement! try checkSecError(SecRequirementCreateWithString(designatedRequirement as CFString, .default, &requirement), task: "create client designated requirement") var staticCode: SecStaticCode! try checkSecError(SecStaticCodeCreateWithPath(url as CFURL, .default, &staticCode), task: "load client static code") var errors: Unmanaged? guard SecStaticCodeCheckValidityWithErrors(staticCode, .default, requirement, &errors) == noErr else { let errorMessage = errors?.takeUnretainedValue().localizedDescription ?? "Unknown error" throw DeepLinkError("Client code signature validation failed with: \(errorMessage)") } } } private func checkSecError(_ closure: @autoclosure () -> OSStatus, task: String) throws { let err = closure() guard err != noErr else { return } let msg = SecCopyErrorMessageString(err, nil) as String? throw DeepLinkError("Failed to \(task). Error code \(err): \(msg ?? "")") } extension SecCSFlags { static let `default` = SecCSFlags([]) } ================================================ FILE: DeepLinkSecurity/Source/Models/Extensions/DeepLinkClientDescriptor+.swift ================================================ import Cocoa public extension DeepLinkClientDescriptor { init(client: DeepLinkClient, authorization: DeepLinkClientAuthorization = .undetermined) { self.init(clientID: client.id, clientURL: client.url, authorization: authorization) } init(clientID: DeepLinkClient.ID, clientURL: URL, authorization: DeepLinkClientAuthorization = .undetermined) { let bundle = Bundle(url: clientURL) self.init( id: clientID, url: clientURL, bundleIdentifier: bundle.flatMap(\.bundleIdentifier), displayName: bundle.flatMap(\.bestEffortAppName) ?? clientURL.fileNameWithoutExtension, icon: Icon(clientURL: clientURL), authorization: authorization, isValid: FileManager.default.fileExists(atPath: clientURL.path) ) } /// Checks if the descriptor's client is still present at its original filesystem location, /// attempting to update the client if it's been moved or deleted. func resolved() -> DeepLinkClientDescriptor { guard let bundleIdentifier else { return self } guard !FileManager.default.fileExists(atPath: url.path) else { return self } guard let updatedURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) else { return self.invalidated() } return DeepLinkClientDescriptor(clientID: id, clientURL: updatedURL, authorization: authorization) } func invalidated() -> DeepLinkClientDescriptor { var mSelf = self mSelf.isValid = false return mSelf } func withAuthorization(_ authorization: DeepLinkClientAuthorization) -> DeepLinkClientDescriptor { var mSelf = self mSelf.authorization = authorization return mSelf } } public extension DeepLinkClientDescriptor.Icon { init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let data = try container.decode(Data.self) let image = NSImage(data: data) ?? NSWorkspace.shared.icon(for: .application) self.init(image: image) } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() guard let png = image.pngData() else { throw EncodingError.invalidValue(0, .init(codingPath: [], debugDescription: "Failed to get icon PNG data")) } try container.encode(png) } } private extension DeepLinkClientDescriptor.Icon { init(clientURL: URL) { let image: NSImage if FileManager.default.fileExists(atPath: clientURL.path) { image = NSWorkspace.shared.icon(forFile: clientURL.path) } else { image = NSWorkspace.shared.icon(for: .application) } image.size = NSSize(width: 64, height: 64) self.init(image: image) } } private extension NSImage { func pngData() -> Data? { guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } let rep = NSBitmapImageRep(cgImage: cgImage) guard let png = rep.representation(using: .png, properties: [:]) else { return nil } return png } } private extension Bundle { var bestEffortAppName: String? { guard let info = infoDictionary else { return bundleURL.fileNameWithoutExtension } return info["CFBundleDisplayName"] as? String ?? info["CFBundleName"] as? String ?? bundleURL.fileNameWithoutExtension } } private extension URL { var fileNameWithoutExtension: String { deletingPathExtension().lastPathComponent } } ================================================ FILE: DeepLinkSecurity/Source/Models/OpenDeepLinkRequest.swift ================================================ import Foundation /// Represents a client's request for opening a deep link in the app. public struct OpenDeepLinkRequest { /// The URL for the deep link the client is trying to open. public var url: URL /// The client model used for authentication. public var client: DeepLinkClientDescriptor public init(url: URL, client: DeepLinkClientDescriptor) { self.url = url self.client = client } } ================================================ FILE: DeepLinkSecurity/Source/Storage/DeepLinkAuthStore.swift ================================================ import Cocoa /// Describes a user's decision about a client opening deep links in the app. public enum DeepLinkClientAuthorization: Int, Codable, CustomStringConvertible { /// The user has not granted/rejected the client yet. /// Also used as a fallback when something goes wrong in the process of /// authenticating a previously authorized/denied client. case undetermined /// The user has granted authorization to the client. case authorized /// The user has denied authorization for the client. case denied } public extension DeepLinkClientAuthorization { var description: String { switch self { case .undetermined: return "undetermined" case .authorized: return "authorized" case .denied: return "denied" } } } /// Implemented by types that can provide persistence for user's decision regarding the opening of deep links from other apps. /// See ``MemoryDeepLinkAuthStore`` and ``KeychainDeepLinkAuthStore``. public protocol DeepLinkAuthStore { func authorization(for client: DeepLinkClient) async -> DeepLinkClientAuthorization func setAuthorization(_ authorization: DeepLinkClientAuthorization, for client: DeepLinkClient) async throws } ================================================ FILE: DeepLinkSecurity/Source/Storage/DeepLinkManagementStore.swift ================================================ import Foundation /// Implemented by types that can provide persistence for a list of authorized/denied deep link clients, /// so that a UI can be assembled showing the user their previous decisions and allowing users to change their mind. public protocol DeepLinkManagementStore { /// Returns deep link client descriptors previously added using ``insert(_:)``. var currentClientDescriptors: [DeepLinkClientDescriptor] { get } /// Streams deep link client descriptors previously added using ``insert(_:)`` as they're updated. func clientDescriptors() -> AsyncStream<[DeepLinkClientDescriptor]> /// Whether the store currently has a descriptor with the specified identifier. func hasDescriptor(with id: DeepLinkClientDescriptor.ID) async -> Bool /// Upserts a client descriptor. func insert(_ descriptor: DeepLinkClientDescriptor) async throws /// Deletes an existing descriptor. func delete(_ descriptor: DeepLinkClientDescriptor) async throws } ================================================ FILE: DeepLinkSecurity/Source/Storage/KeychainDeepLinkAuthStore.swift ================================================ import Foundation import Security import OSLog import CryptoKit /// A robust auth store that uses signed keychain items for storing the user's authorization decisions. public final actor KeychainDeepLinkAuthStore: DeepLinkAuthStore { private lazy var logger = Logger.deepLinkLogger(for: Self.self) private let namespace: String private let keyID: String public init(namespace: String, keyID: String) { self.namespace = namespace self.keyID = keyID } private let encoder = PropertyListEncoder() private let decoder = PropertyListDecoder() public func authorization(for client: DeepLinkClient) async -> DeepLinkClientAuthorization { do { guard let authData = try readKeychainItem(id: client.id) else { return .undetermined } do { /// Fetch the existing response item from the keychain. let response = try decoder.decode(KeychainAuthorizationResponse.self, from: authData) do { /// The mere existence of the response in the keychain doesn't automatically mean the decision should be honored. /// When written to the keychain, responses are signed with a private key. /// When read from the keychain, the app verifies the stored signature against the corresponding public key, /// ensuring that this response was written by an app that has permission to access this app's private key item from the keychain. /// If another app attempts to write fake permissions to the keychain, they'll only be valid if the app can also sign the responses using our private key, /// but in order to do that, the app would need the user's permission to access our keychain item, which triggers a permission prompt on macOS. try validate(response: response) logger.debug("Validated authorization response") return response.authorization } catch { logger.error("Failed to validate authorization response: \(error, privacy: .public)") throw error } } catch { logger.fault("Error decoding authorization from Keychain entry: \(error, privacy: .public)") throw error } } catch { return .undetermined } } public func setAuthorization(_ authorization: DeepLinkClientAuthorization, for client: DeepLinkClient) async throws { let clientID = client.id let signingKey = try fetchSigningKey() let payload = KeychainAuthorizationResponse.signingPayload(clientID: clientID, authorization: authorization) let signature = try signingKey.sign(payload) let response = KeychainAuthorizationResponse( authorization: authorization, clientID: clientID, signingKeyID: keyID, signature: signature ) guard let authData = try? encoder.encode(response) else { throw DeepLinkError("Failed to encode authorization") } try writeKeychainItem(id: clientID, data: authData) logger.debug("Stored authorization \(authorization) for \(client.designatedRequirement) (id = \(clientID, privacy: .public))") } private func validate(response: KeychainAuthorizationResponse) throws { let key = try fetchSigningKey() guard try key.verify(response.signature, for: response.signingPayload) else { throw DeepLinkError("Invalid authorization signature for client \(response.clientID)") } } private func fetchSigningKey() throws -> KeychainAuthorizationSigningKey { func createNewKey() throws -> KeychainAuthorizationSigningKey { logger.debug("Creating new authorization signing key") let newKey = KeychainAuthorizationSigningKey(id: keyID) let keyData = try encoder.encode(newKey) try writeKeychainItem(id: keyID, data: keyData) return newKey } if let data = try readKeychainItem(id: keyID) { logger.debug("Found existing authorization signing key") do { let key = try decoder.decode(KeychainAuthorizationSigningKey.self, from: data) return key } catch { logger.fault("Error reading existing authorization signing key, will generate new one. \(error, privacy: .public)") return try createNewKey() } } else { return try createNewKey() } } /// Simple helper for reading an item's data from the keychain. private func readKeychainItem(id: String) throws -> Data? { let query = [ kSecClass: kSecClassGenericPassword, kSecReturnData: true, kSecMatchLimit: kSecMatchLimitOne, kSecAttrService: namespace as CFString, kSecAttrAccount: id as CFString ] as [CFString: Any] as CFDictionary var result: CFTypeRef? let res = SecItemCopyMatching(query, &result) guard res != errSecItemNotFound else { return nil } try check(res) guard let data = result as? Data else { throw DeepLinkError("Failed to cast security query result to data") } return data } /// Simple helper for writing an item to the keychain. private func writeKeychainItem(id: String, data: Data) throws { let query = [ kSecClass: kSecClassGenericPassword, kSecAttrService: namespace as CFString, kSecAttrAccount: id as CFString ] as [CFString: Any] var attrs = query attrs[kSecValueData] = data as CFData var result: CFTypeRef? var res = SecItemAdd(attrs as CFDictionary, &result) if res == errSecDuplicateItem { logger.debug("Keychain item \(id) already exists, updating") res = SecItemUpdate(query as CFDictionary, [kSecValueData: data as CFData] as CFDictionary) } try check(res) } /// Simple helper for removing an item from the keychain. private func deleteKeychainItem(id: String) throws { let query = [ kSecClass: kSecClassGenericPassword, kSecMatchLimit: kSecMatchLimitOne, kSecAttrService: namespace as CFString, kSecAttrAccount: id as CFString ] as [CFString: Any] as CFDictionary let res = SecItemDelete(query) guard res != errSecItemNotFound else { return } try check(res) } } /// This is the payload that's stored as the value for a keychain item representing the user's decision /// regarding an app's permission to open deep links within this app. private struct KeychainAuthorizationResponse: Codable { /// The user's decision (allow/deny). var authorization: DeepLinkClientAuthorization /// The SHA256 hash of the designated CS requirement for the app the decision is for. var clientID: String /// The identifier for the key used to generate the ECDSA signature. var signingKeyID: String /// The ECDSA signature for the payload, which is composed of the client ID and the user's decision. var signature: Data } private extension KeychainAuthorizationResponse { /// The payload that gets signed when storing the response on the keychain. var signingPayload: Data { Self.signingPayload(clientID: clientID, authorization: authorization) } static func signingPayload(clientID: String, authorization: DeepLinkClientAuthorization) -> Data { Data("\(clientID)-\(authorization.rawValue)".utf8) } } // MARK: - Crypto private struct KeychainAuthorizationSigningKey: Codable { var id: String var keyData: Data } private extension KeychainAuthorizationSigningKey { init(id: String = UUID().uuidString) { let key = P521.Signing.PrivateKey() self.init(id: id, keyData: key.rawRepresentation) } } private extension KeychainAuthorizationSigningKey { var privateKey: P521.Signing.PrivateKey { get throws { try P521.Signing.PrivateKey(rawRepresentation: keyData) } } var publicKey: P521.Signing.PublicKey { get throws { try privateKey.publicKey } } } private extension KeychainAuthorizationSigningKey { func sign(_ digest: some DataProtocol) throws -> Data { try privateKey.signature(for: digest).rawRepresentation } func verify(_ signature: Data, for digest: some DataProtocol) throws -> Bool { let signature = try P521.Signing.ECDSASignature(rawRepresentation: signature) return try publicKey.isValidSignature(signature, for: digest) } } // MARK: - Helpers private func check(_ res: OSStatus) throws { guard res != errSecSuccess else { return } if let str = SecCopyErrorMessageString(res, nil) { throw DeepLinkError("Security error code \(res): \"\(str)\"") } else { throw DeepLinkError("Security error code \(res)") } } ================================================ FILE: DeepLinkSecurity/Source/Storage/MemoryDeepLinkAuthStore.swift ================================================ import Foundation import OSLog /// A very basic store that uses an in-memory dictionary and is destroyed when the app terminates. Useful for testing. public final actor MemoryDeepLinkAuthStore: DeepLinkAuthStore { private lazy var logger = Logger.deepLinkLogger(for: Self.self) private var authorizationByClientRequirement = [String: DeepLinkClientAuthorization]() public init() { } public func authorization(for client: DeepLinkClient) async -> DeepLinkClientAuthorization { if let result = authorizationByClientRequirement[client.designatedRequirement] { logger.debug("Found existing authorization \(result) for \(client.designatedRequirement)") return result } else { logger.debug("No authorization in store for \(client.designatedRequirement), returning undetermined") return .undetermined } } public func setAuthorization(_ authorization: DeepLinkClientAuthorization, for client: DeepLinkClient) async throws { logger.debug("Setting authorization \(authorization) for \(client.designatedRequirement)") authorizationByClientRequirement[client.designatedRequirement] = authorization } } ================================================ FILE: DeepLinkSecurity/Source/Storage/UserDefaultsDeepLinkManagementStore.swift ================================================ import Foundation import OSLog /// A management store that persists client descriptors in `UserDefaults`. public final actor UserDefaultsDeepLinkManagementStore: DeepLinkManagementStore { private let logger = Logger.deepLinkLogger(for: UserDefaultsDeepLinkManagementStore.self) private let defaults: UserDefaults private let storageKey: String public init(namespace: String = "DeepLinkSecurity", suiteName: String? = nil, inMemory: Bool = false) { self.storageKey = "\(namespace)-Management" if let suiteName { if let instance = UserDefaults(suiteName: suiteName) { self.defaults = instance } else { assertionFailure("Failed to initialize user defaults with suite name \"\(suiteName)\"") self.defaults = .standard } } else if inMemory { self.defaults = UserDefaults() } else { self.defaults = .standard } Task { let existingDescriptors = readDescriptors() await cacheDescriptors(existingDescriptors) } } public func hasDescriptor(with id: DeepLinkClientDescriptor.ID) -> Bool { cachedDescriptors[id] != nil } public nonisolated var currentClientDescriptors: [DeepLinkClientDescriptor] { Self.descriptorsArray(from: readDescriptors()) } public nonisolated func clientDescriptors() -> AsyncStream<[DeepLinkClientDescriptor]> { let stream = AsyncStream { [weak self] continuation in guard let self = self else { return } Task { await self.onStoreChanged { descriptorsByID in let descriptors = Self.descriptorsArray(from: descriptorsByID) continuation.yield(descriptors) } } } Task { let snapshot = readDescriptors() await storeChangeHandler(snapshot) } return stream } private static func descriptorsArray(from descriptorsByID: [DeepLinkClientDescriptor.ID: DeepLinkClientDescriptor]) -> [DeepLinkClientDescriptor] { descriptorsByID .values .map { $0.resolved() } .sorted(by: { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending }) } public func insert(_ descriptor: DeepLinkClientDescriptor) async throws { await update(id: descriptor.id, with: descriptor) cacheDescriptors(readDescriptors()) } public func delete(_ descriptor: DeepLinkClientDescriptor) async throws { await update(id: descriptor.id, with: nil) cacheDescriptors(readDescriptors()) } private var cachedDescriptors = DescriptorStorage() private var storeChangeHandler: (DescriptorStorage) async -> Void = { _ in } private func onStoreChanged(perform block: @escaping (DescriptorStorage) async -> Void) { storeChangeHandler = block } private func cacheDescriptors(_ descriptors: DescriptorStorage) { self.cachedDescriptors = descriptors } private func update(id: DeepLinkClientDescriptor.ID, with descriptor: DeepLinkClientDescriptor?) async { do { var snapshot = readDescriptors() snapshot[id] = descriptor let data = try encoder.encode(snapshot) defaults.set(data, forKey: storageKey) defaults.synchronize() if let descriptor { logger.debug("Inserted descriptor \(descriptor.id, privacy: .public)") } else { logger.debug("Deleted descriptor \(id, privacy: .public)") } await storeChangeHandler(snapshot) } catch { logger.error("Failed to insert descriptor: \(error, privacy: .public)") } } private typealias DescriptorStorage = [DeepLinkClientDescriptor.ID: DeepLinkClientDescriptor] nonisolated private func readDescriptors() -> DescriptorStorage { logger.debug(#function) guard let data = defaults.data(forKey: storageKey) else { logger.debug("No data for \(self.storageKey)") return [:] } do { let descriptorsByID = try decoder.decode(DescriptorStorage.self, from: data) logger.debug("Fetched \(descriptorsByID.count, privacy: .public) client descriptor(s)") return descriptorsByID } catch { logger.error("Failed to decode management store data: \(error, privacy: .public)") return [:] } } private let encoder = PropertyListEncoder() private let decoder = PropertyListDecoder() } extension UserDefaults: @retroactive @unchecked Sendable { } ================================================ FILE: DeepLinkSecurity/Source/UI/DeepLinkAuthUI.swift ================================================ import SwiftUI public protocol DeepLinkAuthUI: AnyObject { /// Return ``DeepLinkClientAuthorization/authorized`` if the user has allowed the client to open deep links in the app. /// /// If this method throws, then the auth store is not modified and the user will be prompted again the next time the same client /// attempts to open a deep link in the app. /// /// If this method returns ``DeepLinkClientAuthorization/denied``, then the auth store will be modified and future /// requests from the same client will be rejected without a prompt. @MainActor func presentDeepLinkAuth(for request: OpenDeepLinkRequest) async throws -> DeepLinkClientAuthorization } ================================================ FILE: LICENSE ================================================ Copyright (c) 2022 Guilherme Rambo Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # VirtualBuddy VirtualBuddy can virtualize macOS 12 and later on Apple Silicon, with the goal of offering features that are useful to developers who need to test their apps on multiple versions of macOS, especially betas. ![](./assets/Showcase.jpg) ## System Requirements - **Apple Silicon Mac** - macOS 13 or later ### Installing macOS To install a macOS virtual machine, you can select from a list of macOS versions provided by VirtualBuddy, which will automatically download and install the selected version. You may also provide your own IPSW link or select from an IPSW you have already downloaded. ### Installing macOS Betas If you’d like to run a virtual machine with a macOS beta that’s more recent than the one you’re currently running, such as a macOS 15 virtual machine running in a macOS 14 host, then you’ll need the latest device support package from Apple. Device support packages are sometimes made available by Apple directly, but they’re always included and installed automatically with the latest Xcode beta. These can be obtained from the [Apple Developer portal](https://developer.apple.com/download). ![Screenshot of the Apple Developer device support download](./assets/DeviceSupport.png) ## Download VirtualBuddy is free and open-source. If you’d like to support its development, [you may purchase it on Gumroad](https://insidegui.gumroad.com/l/virtualbuddy) or [sponsor my work on GitHub](https://github.com/sponsors/insidegui). If you’d just like to download the latest version, [go to GitHub releases](https://github.com/insidegui/VirtualBuddy/releases/latest). ### Feature Checklist - [x] Ability to boot any version of macOS 12 or macOS 13, including betas - [x] Ability to boot some ARM-based Linux distros (tested with Ubuntu Server and Ubuntu Desktop) - [x] Built-in installation wizard - [x] Select from a collection of restore images available on Apple’s servers - [x] Install the latest stable version of macOS - [x] Local restore image IPSW file - [x] Custom restore image URL - [x] Install a Linux distro from a local .iso file - [x] Select from a collection of Linux distros - [x] Install Linux from URL - [x] Boot into recovery mode (in order to disable SIP, for example) - [x] Networking and file sharing support - [x] Clipboard sharing - [x] Customize virtual machine hardware configuration - [x] Save and restore macOS virtual machine state ### Tips and Tricks #### VirtualBuddyGuest app VirtualBuddy automatically mounts a disk image with the VirtualBuddyGuest app when you boot up a virtual machine running macOS. To install the VirtualBuddyGuest app, just select the “Guest” disk on Finder’s side bar then double-click the “VirtualBuddyGuest” app icon. VirtualBuddyGuest enables clipboard sharing between host and guest and automatic mounting of the shared folders configured for the virtual machine. ![](./assets/GuestApp.jpg) #### Taking Advantage of APFS Sometimes when trying things out in the OS installed in one of the virtual machines, things might break, requiring a full install of the guest operating system again, which is a pain. Thanks to APFS cloning though, you can just duplicate a virtual machine within your library folder (using Command + D in Finder), and the copy will take almost no additional disk space. This way you can have a “clean” copy of your VM, do whatever you want with a duplicate of it, and then throw the copy away and re-duplicate the clean version if things break. #### Sharing Folders Between Host and Virtual Machine You can share folders from your Mac to the Virtual Machine and vice-versa using regular macOS file sharing that can be configured in System Preferences/Settings. When both the Virtual Machine and the host are running macOS 13 or later, it’s possible to share folders directly by configuring them in the VM settings within VirtualBuddy before booting up the VM. To mount shared folders in the VM, run the following command in the VM’s Terminal: ```bash mkdir -p ~/Desktop/VirtualBuddyShared && mount -t virtiofs VirtualBuddyShared ~/Desktop/VirtualBuddyShared ``` ## Building **Xcode 16** is required for building on `main`. - Open the `VirtualBuddy/Config/Signing.xcconfig` file - Set the `VB_BUNDLE_ID_PREFIX` variable to something unique like `com.yourname.` - Select the VirtualBuddy project in the Xcode sidebar - Under "Targets", select "VirtualBuddy" - Go to the Signing & Capabilities tab and select your development team under Signing > Team - Repeat the same process for the "VirtualBuddyGuest" target - Build the `VirtualBuddy` scheme (the one that **doesn't** have `(Managed)` in its name) ================================================ FILE: ReleaseNotes/VirtualBuddy 1.2 Release Nodes.md ================================================ # What's new in VirtualBuddy 1.2 - Managing virtual machines can now be done entirely within the library view, the contextual menu offers options for renaming, deleting, duplicating, and showing the VM in Finder - The library now sorts virtual machines by creation date, in reverse chronological order - Virtual machines can now be configured with custom CPU, RAM, storage devices, network devices, displays, and many other options - The option to capture system keyboard shortcuts is now persisted for each virtual machine in the library - Adds support for shared folders to share specific directories from your Mac with the virtual machine - Adds support for bridged networking, allowing a physical network interface from your Mac to be exposed to the virtual machine - Additional storage can now be added to virtual machines by creating new disk images from within VirtualBuddy - A new debug console showing logs related to the installation process is now available while installing macOS in a new virtual machine - The default library directory for new installs is now ~/Library/Application Support/VirtualBuddy (this is where VirtualBuddy stores virtual machines and downloads) - Clicking a virtual machine that's already open in the library will now correctly focus the existing window for that virtual machine ================================================ FILE: ReleaseNotes/VirtualBuddy 1.2.1 Release Notes.md ================================================ # New in VirtualBuddy 1.2.1 ### General improvements to the installer user interface: - Addresses an issue that caused the installer to clip the configuration user interface, hiding the "continue" button - It is now possible to navigate using the arrow keys when selecting the installation method - Text fields now use the same consistent style - The virtual machine name button is automatically focused as expected - Command + R can be used to generate a new random name while editing the virtual machine's name during installation ================================================ FILE: ReleaseNotes/VirtualBuddy 1.2.2 Release Notes.md ================================================ # Fixed in VirtualBuddy 1.2.2 - Makes custom IPSW URL validation less strict, allowing downloads from plain HTTP URLs and URLs that don't end in .ipsw - Addresses an issue that caused audio input to not work in virtual machines; VirtualBuddy will ask for microphone access the first time audio input is used within a virtual machine ================================================ FILE: VirtualBuddy/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "platform" : "universal", "reference" : "systemOrangeColor" }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddy/Assets.xcassets/AppIcon-Default.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "icon_16x16@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "icon_32x32@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "icon_128x128@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "icon_256x256@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "icon_512x512@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddy/Assets.xcassets/AppIcon-Dev.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "icon_16x16@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "icon_32x32@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "icon_128x128@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "icon_256x256@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "icon_512x512@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddy/Assets.xcassets/AppIcon-zBeta.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "icon_16x16@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "icon_32x32@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "icon_128x128@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "icon_256x256@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "icon_512x512@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddy/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddy/Automation/DeepLinkHandler.swift ================================================ import SwiftUI import VirtualCore import VirtualUI import OSLog import DeepLinkSecurity typealias WindowUpdatingClosure = (_ perform: () -> Void) -> Void final class DeepLinkHandler { private let settingsContainer: VBSettingsContainer private let updateController: SoftwareUpdateController private let library: VMLibraryController private let sessionManager: VirtualMachineSessionUIManager let runner: ActionRunner static var shared: DeepLinkHandler { guard let _shared else { fatalError("Attempting to access DeepLinkHandler instance before calling bootstrap()") } return _shared } private static var _shared: DeepLinkHandler! @MainActor static func bootstrap(library: VMLibraryController) { DeepLinkHandler._shared = DeepLinkHandler(library: library) DeepLinkHandler.shared.install() } @MainActor private init(library: VMLibraryController) { self.settingsContainer = VBSettingsContainer.current self.updateController = SoftwareUpdateController.shared self.library = library self.sessionManager = VirtualMachineSessionUIManager.shared self.runner = ActionRunner( settingsContainer: settingsContainer, updateController: updateController, library: library, sessionManager: sessionManager ) } private lazy var logger = Logger(subsystem: kShellAppSubsystem, category: String(describing: Self.self)) private let namespace = "VirtualBuddy" private let keyID = "c3bfea24ee1ca95700a4e56d73097aac" private(set) lazy var sentinel = DeepLinkSentinel( authUI: DeepLinkAuthUIPresenter(), authStore: KeychainDeepLinkAuthStore(namespace: namespace, keyID: keyID), managementStore: UserDefaultsDeepLinkManagementStore() ) func actions() -> AsyncCompactMapSequence, DeepLinkAction> { sentinel.openURL.compactMap { url in do { let action = try DeepLinkAction(url) self.logger.debug("Action: \(String(describing: action))") return action } catch { self.logger.error("Error processing deep link URL \"\(url)\": \(error, privacy: .public)") return nil } } } func install() { sentinel.installAppleEventHandler() Task { for await action in DeepLinkHandler.shared.actions() { await runner.run(action) } } } @MainActor final class ActionRunner { private let settingsContainer: VBSettingsContainer private let updateController: SoftwareUpdateController private let library: VMLibraryController private let sessionManager: VirtualMachineSessionUIManager private let openWindow = OpenCocoaWindowAction.default init(settingsContainer: VBSettingsContainer, updateController: SoftwareUpdateController, library: VMLibraryController, sessionManager: VirtualMachineSessionUIManager) { self.settingsContainer = settingsContainer self.updateController = updateController self.library = library self.sessionManager = sessionManager } func run(_ action: DeepLinkAction) async { do { switch action { case .open(let params): try openVM(named: params.name, options: nil) case .boot(let params): var effectiveOptions = params.options ?? VMSessionOptions.default effectiveOptions.autoBoot = true try openVM(named: params.name, options: effectiveOptions) case .stop(let params): try await stopVM(named: params.name) } } catch { let alert = NSAlert(error: error) alert.runModal() } } func openVM(named name: String, options: VMSessionOptions?) throws { let vm = try getVM(named: name) sessionManager.launch(vm, library: library, options: options) } func stopVM(named name: String) async throws { let controller = try getController(forVMNamed: name) switch controller.state { case .idle, .stopped: throw Failure("Can't stop virtual machine \(name.wrappedInSmartQuotes) because it's not running.") default: try await controller.stop() } } func getVM(named name: String) throws -> VBVirtualMachine { guard let vm = library.virtualMachine(named: name) else { throw Failure("Couldn't find a virtual machine with the name \(name.wrappedInSmartQuotes).") } return vm } func getController(forVMNamed name: String) throws -> VMController { let vm = try getVM(named: name) guard let controller = library.activeController(for: vm.id) else { throw Failure("Couldn't find active instance of virtual machine with the name \(name.wrappedInSmartQuotes).") } return controller } } } private final class DeepLinkAuthUIPresenter: DeepLinkAuthUI { func presentDeepLinkAuth(for request: OpenDeepLinkRequest) async throws -> DeepLinkClientAuthorization { try await DeepLinkAuthPanel.run(for: request) } } extension String { var wrappedInSmartQuotes: String { "“\(self)”" } } ================================================ FILE: VirtualBuddy/Automation/Support/DeepLinkAuthDialog.swift ================================================ import SwiftUI import DeepLinkSecurity final class DeepLinkAuthPanel: NSPanel { private static var panelInstances = NSHashTable(options: [.strongMemory, .objectPointerPersonality]) @MainActor static func run(for request: OpenDeepLinkRequest) async throws -> DeepLinkClientAuthorization { try await withCheckedThrowingContinuation { continuation in let panel = DeepLinkAuthPanel(request: request) { panelInstance, decision in defer { panelInstances.remove(panelInstance) } continuation.resume(returning: decision) panelInstance.close() } panelInstances.add(panel) panel.makeKeyAndOrderFront(nil) panel.center() } } private init(request: OpenDeepLinkRequest, completion: @escaping (DeepLinkAuthPanel, DeepLinkClientAuthorization) -> Void) { super.init(contentRect: .zero, styleMask: [.borderless, .titled, .fullSizeContentView], backing: .buffered, defer: false) titleVisibility = .hidden titlebarAppearsTransparent = true animationBehavior = .alertPanel let dialog = DeepLinkAuthDialog(request: request) { [weak self] granted in guard let self = self else { return } completion(self, granted ? .authorized : .denied) } contentViewController = NSHostingController(rootView: dialog) } override var canBecomeKey: Bool { true } override var canBecomeMain: Bool { true } } struct DeepLinkAuthDialog: View { var request: OpenDeepLinkRequest var response: (Bool) -> Void private var appName: String private var appIcon: Image init(request: OpenDeepLinkRequest, response: @escaping (Bool) -> Void) { self.request = request self.response = response self.appName = request.client.displayName self.appIcon = Image(nsImage: request.client.icon.image) } private var iconContainerSize: CGFloat { 58 } private var handIconSizeMultiplier: CGFloat { 0.5 } private var appIconSizeMultiplier: CGFloat { 0.432 } var body: some View { VStack(spacing: 22) { ZStack { RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(LinearGradient(colors: [.cyan, .blue], startPoint: .top, endPoint: .bottom)) .frame(width: iconContainerSize, height: iconContainerSize) Image(systemName: "hand.raised.fill") .foregroundStyle(Color.white) .imageScale(.large) .font(.system(size: iconContainerSize * handIconSizeMultiplier, weight: .medium, design: .rounded)) .shadow(color: .black.opacity(0.3), radius: 1, x: 0.5, y: 0.5) } .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1) .overlay(alignment: .bottomTrailing) { appIcon .resizable() .aspectRatio(contentMode: .fit) .frame(width: iconContainerSize * appIconSizeMultiplier, height: iconContainerSize * appIconSizeMultiplier) .offset(x: 8, y: 8) } Text("\"\(appName)\" would like to access and control your virtual machines in VirtualBuddy.") .lineLimit(nil) .font(.headline) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) Spacer() HStack { Button(role: .cancel) { response(false) } label: { Text("Don't Allow") .frame(maxWidth: .infinity) } .keyboardShortcut(.cancelAction) Button { response(true) } label: { Text("Allow") .frame(maxWidth: .infinity) } .keyboardShortcut(.defaultAction) } .controlSize(.large) .frame(maxWidth: .infinity) } .padding() .frame(minWidth: 260, minHeight: 230) } } #if DEBUG extension DeepLinkClient { static let preview = DeepLinkClient( url: URL(fileURLWithPath: "/System/Applications/Notes.app"), designatedRequirement: "identifier \"com.apple.Notes\" and anchor apple" ) } extension DeepLinkClientDescriptor { static let preview = DeepLinkClientDescriptor(client: .preview) } extension OpenDeepLinkRequest { static let preview = OpenDeepLinkRequest(url: URL(string: "x-test-link-auth://test1")!, client: .preview) } #Preview { DeepLinkAuthDialog(request: .preview) { response in print("Response: \(response)") } .frame(width: 320) } #endif ================================================ FILE: VirtualBuddy/Automation/VirtualBuddyDeepLinks.swift ================================================ import Foundation import URLQueryItemCoder import DeepLinkSecurity import OSLog import VirtualCore struct OpenVMParameters: Codable { var name: String } struct BootVMParameters: Codable { var name: String var options: VMSessionOptions? } struct StopVMParameters: Codable { var name: String } enum DeepLinkAction { case open(OpenVMParameters) case boot(BootVMParameters) case stop(StopVMParameters) } extension DeepLinkAction { init(_ url: URL) throws { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { throw Failure("Invalid URL: failed to construct URLComponents") } guard let host = components.host else { throw Failure("Invalid URL: missing host") } switch host { case "open": let params = try Self.decodeParameters(OpenVMParameters.self, from: components) self = .open(params) case "boot": let params = try Self.decodeParameters(BootVMParameters.self, from: components) self = .boot(params) case "stop": let params = try Self.decodeParameters(StopVMParameters.self, from: components) self = .stop(params) default: throw Failure("Unrecognized URL action \"\(host)\"") } } private static func decodeParameters(_ type: T.Type, from components: URLComponents) throws -> T where T: Decodable { let items = components.queryItems ?? [] return try URLQueryItemDecoder.deepLink.decode(type, from: items) } } private extension URLQueryItemDecoder { static let deepLink = URLQueryItemDecoder() } ================================================ FILE: VirtualBuddy/Bootstrap/SoftwareUpdateController.swift ================================================ // // SoftwareUpdateController.swift // VirtualBuddy // // Created by Guilherme Rambo on 25/06/22. // import SwiftUI import VirtualCore import OSLog #if ENABLE_SPARKLE import Sparkle #endif final class SoftwareUpdateController: NSObject, ObservableObject { private let logger = Logger(subsystem: kShellAppSubsystem, category: "SoftwareUpdateController") static let shared = SoftwareUpdateController() private var settings: VBSettings { VBSettingsContainer.current.settings } @Published var automaticUpdatesEnabled = true { didSet { #if ENABLE_SPARKLE guard automaticUpdatesEnabled != oldValue else { return } logger.debug("Setting \(#function, privacy: .public) to \(self.automaticUpdatesEnabled, privacy: .public)") updateController.updater.automaticallyChecksForUpdates = automaticUpdatesEnabled #endif } } var automaticUpdatesBinding: Binding { Binding { [self] in automaticUpdatesEnabled } set: { [self] newValue in automaticUpdatesEnabled = newValue } } #if ENABLE_SPARKLE private lazy var updateController: SPUStandardUpdaterController = { SPUStandardUpdaterController( startingUpdater: false, updaterDelegate: self, userDriverDelegate: self ) }() #endif func activate() { #if ENABLE_SPARKLE logger.debug(#function) updateController.startUpdater() automaticUpdatesEnabled = updateController.updater.automaticallyChecksForUpdates registerForUpdateChannelChanges() #endif } @objc func checkForUpdates(_ sender: Any?) { #if ENABLE_SPARKLE logger.debug(#function) updateController.checkForUpdates(sender) #else let alert = NSAlert() alert.messageText = "Updating Disabled" alert.informativeText = "This build doesn't include Sparkle updates." alert.runModal() #endif } private func registerForUpdateChannelChanges() { NotificationCenter.default.addObserver( self, selector: #selector(handleUpdateChannelChanged), name: VBSettings.updateChannelDidChangeNotification, object: nil ) } /// Check for updates when switching from release to beta channel. @objc private func handleUpdateChannelChanged(_ note: Notification) { guard let channel = note.object as? AppUpdateChannel else { return } guard channel != .release else { return } checkForUpdates(nil) } } #if ENABLE_SPARKLE extension SoftwareUpdateController: SPUUpdaterDelegate, SPUStandardUserDriverDelegate { func feedURLString(for updater: SPUUpdater) -> String? { settings.updateChannel.appCastURL.absoluteString } func allowedChannels(for updater: SPUUpdater) -> Set { [settings.updateChannel.id] } } #endif ================================================ FILE: VirtualBuddy/Bootstrap/VirtualBuddyApp.swift ================================================ // // VirtualBuddyApp.swift // VirtualBuddy // // Created by Guilherme Rambo on 07/04/22. // import SwiftUI import VirtualCore import VirtualUI let kShellAppSubsystem = "codes.rambo.VirtualBuddy" struct VirtualBuddyApp: App { @NSApplicationDelegateAdaptor var appDelegate: VirtualBuddyAppDelegate private var settingsContainer: VBSettingsContainer { appDelegate.settingsContainer } private var updateController: SoftwareUpdateController { appDelegate.updateController } private var library: VMLibraryController { appDelegate.library } private var sessionManager: VirtualMachineSessionUIManager { appDelegate.sessionManager } @Environment(\.openWindow) private var openWindow @StateObject private var updatesController = SoftwareUpdateController.shared private let mainWindowTitle: String = Bundle.main.vbFullVersionDescription var body: some Scene { Window(Text(mainWindowTitle), id: .vb_libraryWindowID) { LibraryView() .onAppearOnce(perform: updateController.activate) .environmentObject(library) .environmentObject(sessionManager) .handlesExternalEvents(preferring: ["*"], allowing: ["*"]) .onOpenURL { url in UILog("OPEN URL \(url.path(percentEncoded: false))") sessionManager.open(fileURL: url, library: library) } .environment(\.openVirtualBuddySettings, appDelegate.openSettingsAction) .background { TransparentWindowTitleBarView() } } .windowToolbarStyle(.unified) .commands { #if ENABLE_SPARKLE CommandGroup(after: .appInfo) { Button("Check for Updates…") { updateController.checkForUpdates(nil) } } #endif CommandGroup(replacing: .appSettings) { Button("Settings…") { appDelegate.openSettingsAction() } .keyboardShortcut(",", modifiers: .command) } CommandGroup(before: .windowSize) { VirtualMachineWindowCommands() .environmentObject(sessionManager) } CommandGroup(after: .windowArrangement) { Button("Library") { openWindow(id: .vb_libraryWindowID) } .keyboardShortcut(KeyEquivalent("0"), modifiers: .command) } } .handlesExternalEvents(matching: ["*"]) } } // TODO: Remove this after moving to AppKit lifecycle private struct TransparentWindowTitleBarView: NSViewRepresentable { typealias NSViewType = _MakeWindowTitleBarTransparentView func makeNSView(context: Context) -> _MakeWindowTitleBarTransparentView { _MakeWindowTitleBarTransparentView(frame: .zero) } func updateNSView(_ nsView: _MakeWindowTitleBarTransparentView, context: Context) { } final class _MakeWindowTitleBarTransparentView: NSView { override func viewDidMoveToWindow() { super.viewDidMoveToWindow() window?.titlebarAppearsTransparent = true } } } ================================================ FILE: VirtualBuddy/Bootstrap/VirtualBuddyAppDelegate.swift ================================================ // // VirtualBuddyNSApp.swift // VirtualBuddy // // Created by Guilherme Rambo on 07/04/22. // import Cocoa @_exported import VirtualCore @_exported import VirtualUI import VirtualWormhole import DeepLinkSecurity import OSLog import Combine import SwiftUI #if BUILDING_NON_MANAGED_RELEASE #error("Trying to build for release without using the managed scheme. This build won't include managed entitlements. This error is here for Rambo, you may safely comment it out and keep going.") #endif @MainActor @objc final class VirtualBuddyAppDelegate: NSObject, NSApplicationDelegate { private let logger = Logger(for: VirtualBuddyAppDelegate.self) let settingsContainer = VBSettingsContainer.current let updateController = SoftwareUpdateController.shared let library = VMLibraryController() let sessionManager = VirtualMachineSessionUIManager.shared func applicationWillFinishLaunching(_ notification: Notification) { DeepLinkHandler.bootstrap(library: library) NSApp?.appearance = NSAppearance(named: .darkAqua) } private var cancellables = Set() func applicationDidFinishLaunching(_ notification: Notification) { GuestAdditionsDiskImage.current.$state.sink { state in switch state { case .ready: self.logger.debug("Guest disk image ready") case .installing: self.logger.debug("Guest disk image installing") case .installFailed(let error): self.logger.debug("Guest disk image installation failed - \(error, privacy: .public)") } } .store(in: &cancellables) Task { try? await GuestAdditionsDiskImage.current.installIfNeeded() } #if DEBUG runLaunchDebugTasks() #endif } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { false } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { if let firstValidAssertion = sender.assertionsPreventingAppTermination.first { logger.debug("Preventing app termination due to active assertions: \(sender.assertionsPreventingAppTermination.map(\.reason).formatted(.list(type: .and)), privacy: .public)") let reply: NSApplication.TerminateReply if let assertionReply = firstValidAssertion.handleShouldTerminate() { logger.debug("Assertion handles should terminate, returning its reply \(assertionReply)") reply = assertionReply } else { logger.debug("Assertion doesn't handle should terminate, performing default handling") let alert = NSAlert() alert.messageText = "Quit VirtualBuddy?" alert.informativeText = "VirtualBuddy is currently \(firstValidAssertion.reason). This will be cancelled if you quit the app." let button = alert.addButton(withTitle: "Quit") button.hasDestructiveAction = true let button2 = alert.addButton(withTitle: "Quit When Done") button2.keyEquivalent = "\r" alert.addButton(withTitle: "Cancel") let response = alert.runModal() reply = switch response { case .alertFirstButtonReturn: .terminateNow case .alertSecondButtonReturn: .terminateLater default: .terminateCancel } } switch reply { case .terminateCancel: logger.info("User cancelled termination request. Good.") case .terminateNow: logger.info("User decided to terminate now despite assertions :(") case .terminateLater: logger.info("User wants app to terminate when assertions preventing termination are invalidated.") /// Note that there's no point in resetting this to `false` in any other case because once `.terminateLater` /// has been returned from this method, any attempt to terminate the app will no longer trigger it. sender.shouldTerminateWhenLastAssertionInvalidated = true @unknown default: logger.fault("Unknown terminate reply \(reply, privacy: .public)") } return reply } else { return .terminateNow } } private var settingsWindow: NSWindow? private(set) lazy var openSettingsAction = OpenVirtualBuddySettingsAction { [weak self] in self?.openSettingsWindow() } private func openSettingsWindow() { if let settingsWindow { logger.debug("Settings window already available, showing") settingsWindow.makeKeyAndOrderFront(self) return } let rootView = SettingsScreen( enableAutomaticUpdates: updateController.automaticUpdatesBinding, deepLinkSentinel: DeepLinkHandler.shared.sentinel ) .environmentObject(settingsContainer) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: SettingsScreen.width, height: SettingsScreen.minHeight), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView, .unifiedTitleAndToolbar], backing: .buffered, defer: false ) window.isReleasedWhenClosed = false window.contentViewController = NSHostingController(rootView: rootView) window.makeKeyAndOrderFront(self) window.center() self.settingsWindow = window } } extension NSWindow { /// At least as of macOS 14.4, a SwiftUI window's `identifier` matches the `id` that's set in SwiftUI. var isVirtualBuddyLibraryWindow: Bool { identifier?.rawValue == .vb_libraryWindowID } } #if DEBUG // MARK: - Debugging Helpers private extension VirtualBuddyAppDelegate { func runLaunchDebugTasks() { RunLoop.main.perform { [self] in MainActor.assumeIsolated { VirtualMachineSessionUIManager.shared.testImportVMIfEnabled(library: library) } } } } #endif ================================================ FILE: VirtualBuddy/Bootstrap/VirtualBuddyEntryPoint.swift ================================================ import SwiftUI import VirtualCore import VirtualUI import BuddyFoundation /** Main entry point for the VirtualBuddy app and all supported command-line tools. Command-line tools are implemented using ArgumentParser and declared in ``VirtualBuddyCLI``. The app target symlinks the VirtualBuddy app executable with the names of each supported command-line tool as part of a run script build phase. This entry point uses ``VirtualBuddyCLI`` to check if the current executable name matches that of a supported command-line tool. If that's the case, ``VirtualBuddyCLI`` will invoke the tool's implementation and skip running the app itself. */ @main struct VirtualBuddyEntryPoint { static func main() async throws { let name: String #if DEBUG /** `VB_TOOL` environment variable can be used to test command-line tools when running from within Xcode, where it is inconvenient to deal with running the symlinked variants. Each tool should have an aggregate target set up in the project that has this environment variable set. */ if let overrideName = ProcessInfo.processInfo.environment["VB_TOOL"] { name = overrideName } else { name = ProcessInfo.processInfo.processName } #else name = ProcessInfo.processInfo.processName #endif /// Only attempt to process commands if the executable name doesn't match the name in the app bundle. guard name != Bundle.main.executableName else { return VirtualBuddyApp.main() } await VirtualBuddyCLI.runCommand(named: name) VirtualBuddyApp.main() } } private extension Bundle { /** For the purposes of checking whether the user is running the VirtualBuddy app or a command-line tool, the actual bundle executable declared in the `Info.plist` must be compared against the process name. The `Bundle.executableURL` property will return the URL of the actual executable symlink instead of the one declared. */ var executableName: String { infoDictionary?["CFBundleExecutable"] as? String ?? "VirtualBuddy" } } ================================================ FILE: VirtualBuddy/CommandLine/VirtualBuddyCLI.swift ================================================ import Foundation import ArgumentParser /** Declares supported `ParsableCommand`s that VirtualBuddy provides. Commands are implemented in the main app binary, but the app's entry point checks the process name in order to determine whether a command-line tool should be run instead of the app itself. */ struct VirtualBuddyCLI { static let supportedEntryPoints: [ParsableCommand.Type] = [ VCTool.self, ] static func runCommand(named name: String) async { guard let command = supportedEntryPoints.first(where: { $0.configuration.commandName == name }) else { return } /// Remove any arguments injected by Xcode (such as `-NSDocumentRevisionsDebugMode`). /// Also remove first argument, which is the name of the command itself. let sanitizedArguments: [String] = CommandLine.arguments .suffix(from: 1) .filter { !$0.hasPrefix("-NS") && $0 != "YES" && $0 != "NO" } if let asyncCommand = command as? AsyncParsableCommand.Type { await asyncCommand.main(sanitizedArguments) } else { command.main(sanitizedArguments) } /** For non-async commands, we have to exit explicitly if the command didn't do it for us, otherwise this function will return and the app could end up running from the command-line. Placed at the end here just in case the behavior changes for async commands in the future. **/ exit(0) } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/BlurHashCommand.swift ================================================ import Foundation import ArgumentParser import VirtualCore import VirtualUI import BuddyFoundation struct BlurHashCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "blurhash", abstract: "Encodes or decodes blur hashes.", subcommands: [ BlurHashEncodeCommand.self, BlurHashDecodeCommand.self, ] ) } private struct BlurHashEncodeCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "encode", abstract: "Encodes an image into a blur hash." ) @Argument(help: "Path to input image.") var input: String @Option(name: .shortAndLong, help: "Number of blur hash components.") var components: Int = 4 @Option(name: .shortAndLong, help: "Size of thumbnail used to generate the blur hash.") var size: Int = 128 @Option(name: .shortAndLong, help: "HEIC compression quality of thumbnail used to generate the blur hash.") var quality: Double = 0.6 func run() async throws { let inputURL = try URL(fileURLWithPath: (input as NSString).expandingTildeInPath) .ensureExistingFile() let blurHashComponents: (Int, Int) = (Int(CGSize.vbBlurHashSize.width), Int(CGSize.vbBlurHashSize.height)) let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID()).heic") try inputURL.vctool_encodeHEIC(to: tempURL, maxSize: size, quality: quality) let image = try NSImage(contentsOf: tempURL) .require("Error loading input image.") let blurHash = try image.blurHash(numberOfComponents: blurHashComponents) .require("Error generating blur hash.") try? FileManager.default.removeItem(at: tempURL) print(blurHash) } } private struct BlurHashDecodeCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "decode", abstract: "Decodes a blur hash into an image." ) @Argument(help: "Blur hash string.") var input: String @Option(name: .shortAndLong, help: "Path to output image file.", completion: .file()) var output: String @Option(name: .shortAndLong, help: "Number of blur hash components.") var components: Int = 4 @Option(name: .shortAndLong, help: "The punch parameter for the blur hash.") var punch: Float = 1.0 func run() async throws { let image = try NSImage(blurHash: input, size: CGSize(width: components, height: components), punch: punch) .require("Error decoding blur hash into image.") try image.vb_encodeHEIC(to: output.resolvedURL, options: [ kCGImageDestinationLossyCompressionQuality: 1, kCGImageDestinationImageMaxPixelSize: components ] as CFDictionary) print("✅ Blur hash image saved to \(output.quoted)\n") } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/CatalogCommand.swift ================================================ import Foundation import ArgumentParser import FragmentZip struct CatalogCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "catalog", abstract: "Manipulates the VirtualBuddy software catalog", subcommands: [ ImageCommand.self, GroupCommand.self, MigrateCommand.self ] ) } ================================================ FILE: VirtualBuddy/CommandLine/vctool/Core/BuildManifest+Fetch.swift ================================================ import Foundation import FragmentZip extension BuildManifest { init(remoteIPSWURL url: URL, build: String) async throws { let manifestFileName = "BuildManifest-\(build).plist" let cachedManifestURL = try URL.vctoolBuildManifestCache.appending(path: manifestFileName) let manifestURL: URL if cachedManifestURL.exists { fputs("Using cached build manifest for \(build)\n", stderr) manifestURL = cachedManifestURL } else { fputs("Retrieving build manifest for \(build)\n", stderr) let ipsw = FragmentZip(url: url) let downloadedManifestURL = try await ipsw.download(filePath: "BuildManifest.plist", as: manifestFileName) do { try FileManager.default.copyItem(at: downloadedManifestURL, to: cachedManifestURL) } catch { fputs("WARN: Failed to cache build manifest: \(error)\n", stderr) } manifestURL = downloadedManifestURL } try self.init(contentsOf: manifestURL) } } extension URL { static var vctoolBuildManifestCache: URL { get throws { try URL.vctoolCache .appending(path: "BuildManifests", directoryHint: .isDirectory) .ensureExistingDirectory(createIfNeeded: true) } } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/Core/BuildManifest.swift ================================================ import Foundation import BuddyFoundation struct BuildManifest: Decodable, TreeStringConvertible { var manifestVersion: Int var productBuildVersion: String var productVersion: SoftwareVersion var supportedProductTypes: [String] var buildIdentities: [BuildIdentity] enum CodingKeys: String, CodingKey { case manifestVersion = "ManifestVersion" case productBuildVersion = "ProductBuildVersion" case productVersion = "ProductVersion" case supportedProductTypes = "SupportedProductTypes" case buildIdentities = "BuildIdentities" } } struct BuildIdentity: Decodable, TreeStringConvertible { var productMarketingVersion: SoftwareVersion var boardID: String var chipID: String var uniqueBuildID: Data var info: BuildInfo enum CodingKeys: String, CodingKey { case productMarketingVersion = "ProductMarketingVersion" case boardID = "ApBoardID" case chipID = "ApChipID" case uniqueBuildID = "UniqueBuildID" case info = "Info" } } struct BuildInfo: Decodable, TreeStringConvertible { var buildNumber: String var buildTrain: String var deviceClass: String var restoreBehavior: String var variant: String var mobileDeviceMinVersion: SoftwareVersion var virtualMachineMinCPUCount: Int? var virtualMachineMinHostOS: SoftwareVersion? var virtualMachineMinMemorySizeMB: Int? enum CodingKeys: String, CodingKey { case buildNumber = "BuildNumber" case buildTrain = "BuildTrain" case deviceClass = "DeviceClass" case restoreBehavior = "RestoreBehavior" case variant = "Variant" case virtualMachineMinCPUCount = "VirtualMachineMinCPUCount" case virtualMachineMinHostOS = "VirtualMachineMinHostOS" case virtualMachineMinMemorySizeMB = "VirtualMachineMinMemorySizeMB" case mobileDeviceMinVersion = "MobileDeviceMinVersion" } } // MARK: - Parsing extension BuildManifest { private static let decoder: PropertyListDecoder = { let d = PropertyListDecoder() return d }() init(contentsOf url: URL) throws { let data = try Data(contentsOf: url, options: .mappedIfSafe) try self.init(data: data) } init(data: Data) throws { self = try Self.decoder.decode(Self.self, from: data) } } // MARK: - Filtering extension BuildManifest { mutating func filterBuildIdentities(using predicate: (BuildIdentity) -> Bool) { buildIdentities.removeAll(where: { !predicate($0) }) } func filteringBuildIdentities(using predicate: (BuildIdentity) -> Bool) -> BuildManifest { var mSelf = self mSelf.filterBuildIdentities(using: predicate) return mSelf } } extension BuildInfo { /// `true` if the information indicates that the associated build identity is for a Mac VM. var hasVMInformation: Bool { virtualMachineMinHostOS != nil || virtualMachineMinCPUCount != nil || virtualMachineMinMemorySizeMB != nil || deviceClass.caseInsensitiveCompare("vma2macosap") == .orderedSame } } extension BuildIdentity { /// `true` if the information indicates that this build identity is for a Mac VM. var hasVMInformation: Bool { info.hasVMInformation } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/Core/Helpers.swift ================================================ import Foundation import ArgumentParser import BuddyFoundation extension URL { init(validating string: String) throws { guard let url = URL(string: string) else { throw "Invalid URL: \"\(string)\"" } self = url } } extension ProcessInfo { nonisolated(unsafe) private static var _sessionID: String? var sessionID: String { if let id = Self._sessionID { return id } let newID = UUID().uuidString Self._sessionID = newID return newID } } extension URL { var exists: Bool { FileManager.default.fileExists(atPath: path) } var isExistingDirectory: Bool { var isDir = ObjCBool(false) guard FileManager.default.fileExists(atPath: path, isDirectory: &isDir) else { return false } return isDir.boolValue } func ensureExistingFile() throws -> URL { try requireExistingFile() return self } func ensureExistingDirectory(createIfNeeded: Bool = false) throws -> URL { try requireExistingDirectory(createIfNeeded: createIfNeeded) return self } func requireExistingFile() throws { guard exists else { throw "File doesn't exist at \(path)" } guard !isExistingDirectory else { throw "Expected a file, but found a directory at \(path)" } } func requireExistingDirectory(createIfNeeded: Bool = false) throws { if exists { guard isExistingDirectory else { throw "Expected a directory, but found a regular file at \(path)" } } else { guard createIfNeeded else { throw "Directory doesn't exist at \(path)" } try FileManager.default.createDirectory(at: self, withIntermediateDirectories: true) } } static var baseTempURL: URL { get throws { let tempBase = FileManager.default.temporaryDirectory let sessionBase = tempBase.appendingPathComponent(ProcessInfo.processInfo.sessionID) if !FileManager.default.fileExists(atPath: sessionBase.path) { try FileManager.default.createDirectory(at: sessionBase, withIntermediateDirectories: true) } return sessionBase } } static func tempFileURL(name: String, create: Bool = false) throws -> URL { let fileURL = try baseTempURL.appendingPathComponent(name) if create, !fileURL.exists { FileManager.default.createFile(atPath: fileURL.path, contents: nil) } return fileURL } static func tempDirURL(name: String, create: Bool = false) throws -> URL { let dirURL = try baseTempURL.appendingPathComponent(name) if create, !dirURL.exists { try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true) } return dirURL } static var vctoolCache: URL { get throws { try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) .appending(path: "vctool", directoryHint: .isDirectory) } } } extension String { var resolvedPath: String { (self as NSString).expandingTildeInPath } var resolvedURL: URL { URL(filePath: resolvedPath) } } extension CatalogGuestPlatform: @retroactive EnumerableFlag { } extension ResolvedFeatureStatus { var cliDescription: String { switch self { case .supported: return "✅ Supported" case .warning(_, let message): return "⚠️ Warning: \(message)" case .unsupported(_, let message): return "🛑 Not Supported: \(message)" } } } extension SoftwareVersion: @retroactive ExpressibleByArgument { public init?(argument: String) { self.init(string: argument) } } extension RequirementSet { func matches(info: BuildInfo) -> Bool { guard let manifestMinVersionHost = info.virtualMachineMinHostOS else { return true } guard manifestMinVersionHost == self.minVersionHost else { return false } guard let manifestMinMemory = info.virtualMachineMinMemorySizeMB else { return true } guard manifestMinMemory == self.minMemorySizeMB else { return false } guard let manifestMinCPU = info.virtualMachineMinCPUCount else { return true } return manifestMinCPU == self.minCPUCount } } extension BuildInfo { var requirementsDescription: String { let minVer = virtualMachineMinHostOS?.description ?? "?" let minMem = virtualMachineMinMemorySizeMB.flatMap({ String($0) }) ?? "?" let minCPU = virtualMachineMinCPUCount.flatMap({ String($0) }) ?? "?" return "minHostVersion: \(minVer) | minMemory: \(minMem) | minCPU: \(minCPU)" } } extension RestoreImage: TreeStringConvertible, @retroactive CustomStringConvertible { public var description: String { description(level: 0) } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/Core/TreeStringConvertible.swift ================================================ import Foundation protocol TreeStringConvertible: CustomStringConvertible { func description(level: Int) -> String } extension TreeStringConvertible { func indentation(for level: Int) -> String { String(repeating: " ", count: level * 2) } func description(level: Int) -> String { let prefix = indentation(for: level) let mirror = Mirror(reflecting: self) var output = [String]() for child in mirror.children { let name = "- " + (child.label ?? "???") let item: String if let convertibleChild = child.value as? TreeStringConvertible { let childDescription = convertibleChild.description(level: level + 1) if childDescription.contains("\n") { output.append(prefix + name) item = childDescription } else { item = prefix + "\(name) = \(childDescription)" } } else { let value = String(describing: child.value) item = prefix + "\(name) = \(value)" } output.append(item) } return output.joined(separator: "\n") } var description: String { description(level: 0) } } extension Array: TreeStringConvertible { func description(level: Int) -> String { let prefix = indentation(for: level) return enumerated().map { let elementPrefix = prefix + "- [\($0.offset)] " if let convertibleElement = $0.element as? TreeStringConvertible { return elementPrefix + "\n" + convertibleElement.description(level: level + 1) } else { return elementPrefix + String(describing: $0.element) } }.joined(separator: "\n") } } extension Optional: @retroactive CustomStringConvertible {} extension Optional: TreeStringConvertible { func description(level: Int) -> String { switch self { case .none: return "" case .some(let value): if let convertibleValue = value as? TreeStringConvertible { return convertibleValue.description(level: level + 1) } else { return String(describing: value) } } } public var description: String { description(level: 0) } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/Core/URL+ContentLength.swift ================================================ import Foundation extension URL { func contentLength() async throws -> Int64 { var request = URLRequest(url: self) request.httpMethod = "HEAD" request.timeoutInterval = 10 let (_, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw "Invalid response" } guard httpResponse.statusCode < 300 else { throw "HTTP \(httpResponse.statusCode)" } guard let stringValue = httpResponse.value(forHTTPHeaderField: "content-length"), let length = Int64(stringValue) else { throw "Missing or invalid Content-Length header" } return length } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/GroupCommand.swift ================================================ import Foundation import ArgumentParser import AppKit import BuddyFoundation import VirtualUI extension CatalogCommand { struct GroupCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "group", abstract: "View or modify groups.", subcommands: [ AddCommand.self ] ) struct AddCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "add", abstract: "Adds a new group to the catalog." ) @Option(help: "Unique identifier for the group (ex: \"sequoia\").") var id: String @Option(help: "The major version for releases in the group (ex: 15).") var version: SoftwareVersion @Option(help: "User-friendly name for the group (ex: \"macOS Sequoia\").") var name: String @Option(help: "Path to an image representing the group (usually that release's default wallpaper).") var image: String @Option(name: [.short, .long], help: "Path to an existing catalog JSON file that will be updated with the new group.") var output: String @Option(help: "Remote base URL where catalog will be served from.") var baseURL: String = "https://api.virtualbuddy.app/v2" func run() async throws { let catalogURL = try output.resolvedURL.ensureExistingFile() var catalog = try SoftwareCatalog(contentsOf: catalogURL) guard let remoteBaseURL = URL(string: baseURL) else { throw "Invalid base URL: \"\(baseURL)\"" } guard !catalog.groups.contains(where: { $0.id.caseInsensitiveCompare(id) == .orderedSame }) else { throw "A group already exists with id \"\(id)\"" } let imageURL = try image.resolvedURL.ensureExistingFile() /// Dark image is expected to be named the same as the image but with the "-dark" suffix. let darkImageURL = try imageURL .deletingLastPathComponent() .appendingPathComponent(imageURL.deletingPathExtension().lastPathComponent + "-dark") .appendingPathExtension(imageURL.pathExtension) .ensureExistingFile() let localImagesBaseURL = try catalogURL .deletingLastPathComponent() .appending(path: "images", directoryHint: .isDirectory) .ensureExistingDirectory(createIfNeeded: true) let localImageURL = localImagesBaseURL .appendingPathComponent(id, conformingTo: .heic) let localThumbnailURL = localImagesBaseURL .appendingPathComponent(id + "-thumbnail", conformingTo: .heic) let localDarkImageURL = localImagesBaseURL .appendingPathComponent(id + "-dark", conformingTo: .heic) let localDarkThumbnailURL = localImagesBaseURL .appendingPathComponent(id + "-dark-thumbnail", conformingTo: .heic) let remoteImageURL = remoteBaseURL.appendingPathComponent("images/" + localImageURL.lastPathComponent) let remoteThumbnailImageURL = remoteBaseURL.appendingPathComponent("images/" + localThumbnailURL.lastPathComponent) let remoteDarkImageURL = remoteBaseURL.appendingPathComponent("images/" + localDarkImageURL.lastPathComponent) let remoteDarkThumbnailImageURL = remoteBaseURL.appendingPathComponent("images/" + localDarkThumbnailURL.lastPathComponent) try imageURL.vctool_encodeHEIC(to: localImageURL, maxSize: 2048, quality: 0.9) try imageURL.vctool_encodeHEIC(to: localThumbnailURL, maxSize: 720, quality: 0.8) try darkImageURL.vctool_encodeHEIC(to: localDarkImageURL, maxSize: 2048, quality: 0.9) try darkImageURL.vctool_encodeHEIC(to: localDarkThumbnailURL, maxSize: 720, quality: 0.8) let blurHashComponents: (Int, Int) = (Int(CGSize.vbBlurHashSize.width), Int(CGSize.vbBlurHashSize.height)) guard let thumbnailImage = NSImage(contentsOf: localThumbnailURL) else { throw "Failed to load generated thumbnail image from \(localThumbnailURL.path)" } guard let blurHash = thumbnailImage.blurHash(numberOfComponents: blurHashComponents) else { throw "Failed to generate blur hash from generated thumbnail image at \(localThumbnailURL.path)" } guard let darkThumbnailImage = NSImage(contentsOf: localDarkThumbnailURL) else { throw "Failed to load generated thumbnail dark image from \(localDarkThumbnailURL.path)" } guard let darkBlurHash = darkThumbnailImage.blurHash(numberOfComponents: blurHashComponents) else { throw "Failed to generate blur hash from generated dark thumbnail image at \(localDarkThumbnailURL.path)" } let image = CatalogGraphic( id: id, url: remoteImageURL, thumbnail: CatalogGraphic.Thumbnail( url: remoteThumbnailImageURL, width: Int(thumbnailImage.size.width), height: Int(thumbnailImage.size.height), blurHash: blurHash ) ) let darkImage = CatalogGraphic( id: id, url: remoteDarkImageURL, thumbnail: CatalogGraphic.Thumbnail( url: remoteDarkThumbnailImageURL, width: Int(darkThumbnailImage.size.width), height: Int(darkThumbnailImage.size.height), blurHash: darkBlurHash ) ) let group = CatalogGroup( id: id, name: name, majorVersion: version, image: image, darkImage: darkImage ) catalog.groups.insert(group, at: 0) try catalog.write(to: catalogURL) } } } } // MARK: - Helpers extension URL { func vctool_encodeHEIC(to outputURL: URL, maxSize: Int, quality: Double) throws { guard let image = NSImage(contentsOf: self) else { throw "Image couldn't be loaded from \(self.path)" } guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { throw "Couldn't get CGImage from input image" } guard let destination = CGImageDestinationCreateWithURL(outputURL as CFURL, "public.heic" as CFString, 1, nil) else { throw "Failed to create image destination" } let imageOptions = [ kCGImageDestinationLossyCompressionQuality: quality, kCGImageDestinationImageMaxPixelSize: maxSize ] as CFDictionary CGImageDestinationAddImage(destination, cgImage, imageOptions) CGImageDestinationFinalize(destination) } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/IPSWCommand.swift ================================================ import Foundation import ArgumentParser import FragmentZip struct IPSWCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "ipsw", abstract: "Tools for exploring IPSW packages.", subcommands: [ InspectCommand.self, ManifestCommand.self ] ) struct ManifestOptions: ParsableArguments { @Flag(help: "List only the build identities containing virtual machine information.") var vm = false } // MARK: - Inspect Command struct InspectCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "inspect", abstract: "Inspects an IPSW file, retrieving information that's relevant to VirtualBuddy." ) @Argument(help: "URL to the IPSW file.") var ipsw: String @OptionGroup var options: ManifestOptions func run() async throws { let url = try URL(validating: ipsw) let zip = FragmentZip(url: url) let fileURL = try await zip.download(filePath: "BuildManifest.plist") let manifestCommand = ManifestCommand(manifestPath: fileURL.path, options: options) try await manifestCommand.run() } } // MARK: - Manifest Command struct ManifestCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "manifest", abstract: "Parses properties from a local BuildManifest.plist file." ) @Argument(help: "Path to a BuildManifest.plist file.") var manifestPath: String @OptionGroup var options: ManifestOptions init() { } // This initializer is needed because this command is run by InspectCommand. init(manifestPath: String, options: ManifestOptions) { self.manifestPath = manifestPath self.options = options } func run() async throws { let url = try manifestPath.resolvedURL.ensureExistingFile() var manifest = try BuildManifest(contentsOf: url) manifest.filterBuildIdentities { identity in options.vm ? identity.hasVMInformation : true } print(manifest) } } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/ImageCommand.swift ================================================ import Foundation import ArgumentParser import FragmentZip import BuddyFoundation import VirtualCore extension CatalogCommand { struct ImageCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "image", abstract: "Manipulates restore images in the VirtualBuddy catalog.", subcommands: [ AddCommand.self ] ) // MARK: - Add Command struct AddCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "add", abstract: "Adds a new macOS release to a VirtualBuddy software catalog based on an IPSW URL." ) @Option(name: [.short, .long], help: "URL to the IPSW file.") var ipsw: String @Option(name: [.short, .long], help: "ID of the release channel (devbeta or regular).") var channel: String @Option(name: [.short, .long], help: "User-facing name for the release (ex: \"macOS 15.0 Developer Beta 4\").") var name: String @Option(name: [.short, .long], help: "Path to the software catalog JSON file that will be updated.") var output: String @Flag(name: .shortAndLong, help: "Replace existing build if it already exists in the catalog.") var force = false func run() async throws { let ipswURL = try URL(validating: ipsw) let catalogURL = try output.resolvedURL.ensureExistingFile() fputs("Detecting download size...\n", stderr) let contentLength = try await ipswURL.contentLength() var catalog = try SoftwareCatalog(contentsOf: catalogURL) fputs("Reading build manifest from remote IPSW...\n", stderr) let zip = FragmentZip(url: ipswURL) let fileURL = try await zip.download(filePath: "BuildManifest.plist") let manifest = try BuildManifest(contentsOf: fileURL) guard let identity = manifest.buildIdentities.first(where: { $0.hasVMInformation }) else { throw "Couldn't find a build identity with VM information in the specified IPSW. Are you sure this IPSW supports macOS VMs?" } let majorVersion = SoftwareVersion(majorVersionFrom: manifest.productVersion) guard let group = catalog.groups.first(where: { $0.majorVersion == majorVersion }) else { throw "Couldn't find a group with majorVersion = \(majorVersion). If this is a new major version of macOS, a new group must be added to the catalog manually before running this command." } guard catalog.channels.contains(where: { $0.id == channel }) else { throw "Couldn't find \"\(channel)\" channel in the catalog. Catalog channels: \(catalog.channels.map(\.id).joined(separator: ", "))" } fputs("Found group \(group.name)\n", stderr) let requirementSet: RequirementSet if let existingSet = catalog.requirementSets.first(where: { $0.matches(info: identity.info) }) { fputs("Found existing requirement set \(existingSet.id)\n", stderr) requirementSet = existingSet } else { fputs("Found no existing requirement set matching properties from manifest, will create a new one: \(identity.info.requirementsDescription)\n", stderr) requirementSet = RequirementSet( id: UUID().uuidString, minCPUCount: identity.info.virtualMachineMinCPUCount ?? 2, minMemorySizeMB: identity.info.virtualMachineMinMemorySizeMB ?? 4096, minVersionHost: identity.info.virtualMachineMinHostOS ?? SoftwareVersion(major: 12, minor: 0, patch: 0) ) catalog.requirementSets.insert(requirementSet, at: 0) } let image = RestoreImage( id: manifest.productBuildVersion, group: group.id, channel: channel, requirements: requirementSet.id, name: name, build: manifest.productBuildVersion, version: manifest.productVersion, mobileDeviceMinVersion: identity.info.mobileDeviceMinVersion, url: ipswURL, downloadSize: UInt64(contentLength) ) let index = catalog.index(forInserting: image) let isUpdate = catalog.restoreImages[index].id == image.id /// Replacing an existing image requires a flag. if isUpdate { guard force else { fputs("\n❌ Build \(image.id) already exists in the catalog. Use --force flag to update it.\n\n", stderr) Darwin.exit(1) } catalog.restoreImages.remove(at: index) } catalog.restoreImages.insert(image, at: index) let successMessage = isUpdate ? "Updated image in catalog" : "Added image to catalog" fputs("✅ \(successMessage):\n\n", stderr) fputs("\(image)\n\n", stderr) try catalog.write(to: catalogURL) fputs("✅ Done!\n\n", stderr) } } } } private extension SoftwareCatalog { func index(forInserting image: RestoreImage) -> Int { if let existingIndex = restoreImages.firstIndex(where: { $0.id == image.id }) { existingIndex /// Replace image at its current index (client must delete existing one before replacing) } else if let placementIndex = restoreImages.firstIndex(where: { $0.group == image.group && $0.version <= image.version }) { placementIndex /// Place image before the first image of the same release group and OS version } else { 0 /// Place image in first slot } } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/MigrateCommand.swift ================================================ import Foundation import ArgumentParser import FragmentZip import BuddyFoundation extension CatalogCommand { struct MigrateCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "migrate", abstract: "Migrates a version 1 restore image catalog to version 2.", discussion: "This command will fetch metadata for each IPSW in the legacy catalog, so it requires an internet connection and may take a while to run." ) @Option(name: [.short, .long], help: "Path to version 1 catalog.") var inputPath: String? @Flag(help: "Migrate from live API catalog instead of a local file.") var live = false @Option(name: [.short, .long], help: "Path where to save the migrated version 2 catalog. If a catalog already exists at this path, all restore images will be removed from it and replaced by the migrated ones from the version 1 catalog, but groups and channels will be retained.") var outputPath: String private func fetchLegacyCatalog() async throws -> LegacyCatalog { if let inputPath { return try LegacyCatalog(contentsOf: inputPath.resolvedURL.ensureExistingFile()) } else { fputs("Downloading live v1 catalog...\n", stderr) let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api.virtualbuddy.app/restore/mac?apiKey=15A25D48-4A34-4EE4-A293-C22B0DE1B54E")!) return try LegacyCatalog(data: data) } } func run() async throws { /// Any channels not listed here will be excluded from the output catalog. let channelIdentifiers: Set = ["devbeta", "regular"] let outputURL = outputPath.resolvedURL let legacyCatalog = try await fetchLegacyCatalog() var catalog: SoftwareCatalog /// If there's an existing version 2 catalog at the output path, then use that catalog as a template, migrating only the restore images from the v1 catalog. if outputURL.exists { fputs("Using existing version 2 catalog for migration\n", stderr) catalog = try SoftwareCatalog(contentsOf: outputURL) catalog.restoreImages.removeAll() } else { fputs("Creating empty version 2 catalog for migration\n", stderr) catalog = SoftwareCatalog(apiVersion: 2, minAppVersion: .init(string: "2.0.0")!, channels: [], groups: [], restoreImages: [], features: [], requirementSets: [], deviceSupportVersions: []) } for legacyChannel in legacyCatalog.channels { guard channelIdentifiers.contains(legacyChannel.id), !catalog.channels.contains(where: { $0.id == legacyChannel.id }) else { continue } let channel = CatalogChannel(id: legacyChannel.id, name: legacyChannel.name, note: legacyChannel.note, icon: legacyChannel.icon) catalog.channels.append(channel) } if catalog.groups.isEmpty { for legacyGroup in legacyCatalog.groups { let group = CatalogGroup(id: legacyGroup.id, name: legacyGroup.name, majorVersion: legacyGroup.majorVersion, image: .placeholder, darkImage: .placeholder) catalog.groups.append(group) } } let requirement_min_host_13 = catalog.requirementSets.first(where: { $0.id == "min_host_13" }) ?? RequirementSet( id: "min_host_13", minCPUCount: 2, minMemorySizeMB: 4096, minVersionHost: SoftwareVersion(string: "13.0")! ) let requirement_min_host_12 = catalog.requirementSets.first(where: { $0.id == "min_host_12" }) ?? RequirementSet( id: "min_host_12", minCPUCount: 2, minMemorySizeMB: 4096, minVersionHost: SoftwareVersion(string: "12.0")! ) if !catalog.requirementSets.contains(where: { $0.id == requirement_min_host_13.id }) { catalog.requirementSets.append(requirement_min_host_13) } if !catalog.requirementSets.contains(where: { $0.id == requirement_min_host_12.id }) { catalog.requirementSets.append(requirement_min_host_12) } for legacyImage in legacyCatalog.images { do { let contentLength = try await legacyImage.url.contentLength() let manifest = try await BuildManifest(remoteIPSWURL: legacyImage.url, build: legacyImage.build) /// Version 13.3 started requiring macOS 13 host, all versions higher than that require macOS 13 host, all versions below that support macOS 12 host. let requirements: RequirementSet = manifest.productVersion >= SoftwareVersion(string: "13.3")! ? requirement_min_host_13 : requirement_min_host_12 guard let vmIdentity = manifest.buildIdentities.first(where: { $0.hasVMInformation }) else { throw "Couldn't find a build identity with VM properties" } let image = RestoreImage( id: legacyImage.id, group: legacyImage.group.id, channel: legacyImage.channel.id, requirements: requirements.id, name: legacyImage.name, build: legacyImage.build, version: manifest.productVersion, mobileDeviceMinVersion: vmIdentity.info.mobileDeviceMinVersion, url: legacyImage.url, downloadSize: UInt64(contentLength) ) catalog.restoreImages.append(image) try catalog.write(to: outputURL) } catch { fputs("Error processing restore image \(legacyImage.id): \(error)\n", stderr) } } print("Migrated catalog written to \(outputURL.path)") print("") } } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/MobileDeviceCommand.swift ================================================ import Foundation import ArgumentParser struct MobileDeviceCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "mobiledevice", abstract: "Interacts with the MobileDevice framework on the host.", subcommands: [ VersionCommand.self ] ) struct VersionCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "version", abstract: "Retrieves the version of the MobileDevice framework that's currently installed." ) func run() async throws { guard let framework = MobileDeviceFramework.current else { throw "Couldn't find MobileDevice.framework" } print(framework.version) } } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/ResolveCommand.swift ================================================ import Foundation import ArgumentParser import BuddyFoundation extension CatalogCommand { struct ResolveCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "resolve", abstract: "Resolves a VirtualBuddy software catalog for a given environment." ) @Option(name: [.short, .long], help: "Path to the catalog JSON file.") var input: String @Option(help: "Custom host version.") var host: SoftwareVersion? @Option(help: "Custom MobileDevice version.") var mobileDevice: SoftwareVersion? @Flag(help: "Specify guest platform.") var guestPlatform: CatalogGuestPlatform = .mac @Option(help: "Show results for a specific build.") var build: String? func run() async throws { let url = try input.resolvedURL.ensureExistingFile() let catalog = try SoftwareCatalog(contentsOf: url) var env = CatalogResolutionEnvironment.current if let host { env.hostVersion = host } if let mobileDevice { env.mobileDeviceVersion = mobileDevice } env.guestPlatform = guestPlatform let resolved = ResolvedCatalog(environment: env, catalog: catalog) if let build { guard let targetImage = resolved.groups.flatMap(\.restoreImages).first(where: { $0.image.build == build }) else { throw "Build not found: \(build)" } printResult(for: targetImage) } else { for group in resolved.groups { print("## \(group.name)") print() for resolvedImage in group.restoreImages { printResult(for: resolvedImage) } } } } func printResult(for resolvedImage: ResolvedRestoreImage) { let image = resolvedImage.image print("### \(image.name) (\(image.build))") print(" - Guest: \(resolvedImage.status.cliDescription)") print(" - Host: \(resolvedImage.requirements.status.cliDescription)") print(" - Features:") for feature in resolvedImage.features { print(" - \(feature.feature.name)") print(" - \(feature.status.cliDescription)") } print() } } } ================================================ FILE: VirtualBuddy/CommandLine/vctool/VCTool.swift ================================================ import Foundation import ArgumentParser import AppKit struct VCTool: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "vctool", abstract: "Tools for updating the VirtualBuddy software catalog.", subcommands: [ CatalogCommand.self, IPSWCommand.self, MobileDeviceCommand.self, BlurHashCommand.self, ] ) } ================================================ FILE: VirtualBuddy/Config/AppTarget.xcconfig ================================================ #include "Paths.xcconfig" // Settings in this file only apply to the main VirtualBuddy.app target // Entitlement Settings // Name of the provisioning profile used for all managed builds (debug, beta, release, dev release). MANAGED_PROFILE = VirtualBuddy Dev Mid 2025 CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=*][arch=*] = $(ENTITLEMENTS_DIR)/VirtualBuddy.entitlements CODE_SIGN_ENTITLEMENTS[config=Release][sdk=*][arch=*] = $(ENTITLEMENTS_DIR)/VirtualBuddy.entitlements CODE_SIGN_ENTITLEMENTS[config=Debug_Managed][sdk=*][arch=*] = $(ENTITLEMENTS_DIR)/VirtualBuddy_Managed.entitlements CODE_SIGN_ENTITLEMENTS[config=Release_Managed][sdk=*][arch=*] = $(ENTITLEMENTS_DIR)/VirtualBuddy_Managed.entitlements CODE_SIGN_ENTITLEMENTS[config=Dev_Release][sdk=*][arch=*] = $(ENTITLEMENTS_DIR)/VirtualBuddy_Managed.entitlements CODE_SIGN_ENTITLEMENTS[config=Beta_Debug][sdk=*][arch=*] = $(ENTITLEMENTS_DIR)/VirtualBuddy_Managed.entitlements CODE_SIGN_ENTITLEMENTS[config=Beta_Release][sdk=*][arch=*] = $(ENTITLEMENTS_DIR)/VirtualBuddy_Managed.entitlements PROVISIONING_PROFILE_SPECIFIER[config=Debug][sdk=*][arch=*] = PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=*][arch=*] = PROVISIONING_PROFILE_SPECIFIER[config=Debug_Managed][sdk=*][arch=*] = $(MANAGED_PROFILE) PROVISIONING_PROFILE_SPECIFIER[config=Release_Managed][sdk=*][arch=*] = $(MANAGED_PROFILE) PROVISIONING_PROFILE_SPECIFIER[config=Dev_Release][sdk=*][arch=*] = $(MANAGED_PROFILE) PROVISIONING_PROFILE_SPECIFIER[config=Beta_Debug][sdk=*][arch=*] = $(MANAGED_PROFILE) PROVISIONING_PROFILE_SPECIFIER[config=Beta_Release][sdk=*][arch=*] = $(MANAGED_PROFILE) CODE_SIGN_STYLE[config=Debug][sdk=*][arch=*] = Automatic CODE_SIGN_STYLE[config=Release][sdk=*][arch=*] = Automatic CODE_SIGN_STYLE[config=Debug_Managed][sdk=*][arch=*] = Manual CODE_SIGN_STYLE[config=Release_Managed][sdk=*][arch=*] = Manual CODE_SIGN_STYLE[config=Dev_Release][sdk=*][arch=*] = Manual CODE_SIGN_STYLE[config=Beta_Debug][sdk=*][arch=*] = Manual CODE_SIGN_STYLE[config=Beta_Release][sdk=*][arch=*] = Manual OTHER_SWIFT_FLAGS[config=Release][sdk=*][arch=*] = -D BUILDING_NON_MANAGED_RELEASE OTHER_SWIFT_FLAGS[config=Dev_Release][sdk=*][arch=*] = -D BUILDING_DEV_RELEASE // Release Train Settings // Special app icon for development releases. Note that Xcode uses the first icon when sorted alphabetically, hence why the default icon has the -Default suffix. ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-Default ASSETCATALOG_COMPILER_APPICON_NAME[config=Dev_Release][sdk=*][arch=*] = AppIcon-Dev ASSETCATALOG_COMPILER_APPICON_NAME[config=Beta_Debug][sdk=*][arch=*] = AppIcon-zBeta ASSETCATALOG_COMPILER_APPICON_NAME[config=Beta_Release][sdk=*][arch=*] = AppIcon-zBeta PRODUCT_NAME = $(TARGET_NAME) // Development releases named VirtualBuddy-Dev.app PRODUCT_NAME[config=Dev_Release][sdk=*][arch=*] = $(TARGET_NAME)-Dev ================================================ FILE: VirtualBuddy/Config/Entitlements/VirtualBuddy.entitlements ================================================ com.apple.security.device.audio-input com.apple.security.virtualization ================================================ FILE: VirtualBuddy/Config/Entitlements/VirtualBuddy_Managed.entitlements ================================================ com.apple.security.device.audio-input com.apple.security.virtualization com.apple.vm.networking ================================================ FILE: VirtualBuddy/Config/Features.xcconfig ================================================ // ENABLE_SPARKLE = Enables building with Sparkle for automatic updates // ENABLE_USERDEFAULTS_SYNC = Enables the user defaults sync feature OTHER_SWIFT_FLAGS = -D ENABLE_SPARKLE $(inherited) // The BETA flag must be present in all targets, hence why these are here instead of in AppTarget.xcconfig OTHER_SWIFT_FLAGS[config=Beta_Debug][sdk=*][arch=*] = -D BETA $(inherited) OTHER_SWIFT_FLAGS[config=Beta_Release][sdk=*][arch=*] = -D BETA $(inherited) VB_SPARKLE_PUBLIC_ED_KEY=dj8ljUPnwoLj/dLs6HyJg5Ayw+t8zWtgjQUfQsH56ww= VB_SPARKLE_CHECK_INTERVAL=86400 ================================================ FILE: VirtualBuddy/Config/InfoPlist.xcconfig ================================================ INFOPLIST_KEY_NSHumanReadableCopyright = © 2024 Buddy Software LTD INFOPLIST_KEY_NSMicrophoneUsageDescription = Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio. ================================================ FILE: VirtualBuddy/Config/Main.xcconfig ================================================ #include "Paths.xcconfig" #include "Versions.xcconfig" #include "Signing.xcconfig" #include "Features.xcconfig" #include "InfoPlist.xcconfig" ARCHS=arm64 ================================================ FILE: VirtualBuddy/Config/Paths.xcconfig ================================================ ENTITLEMENTS_DIR=VirtualBuddy/Config/Entitlements VB_APP_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks VB_FRAMEWORK_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks VB_CLI_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @executable_path/Frameworks @executable_path ================================================ FILE: VirtualBuddy/Config/Signing.xcconfig ================================================ CODE_SIGN_IDENTITY = Apple Development VB_BUNDLE_ID_PREFIX = 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)" GCC_PREPROCESSOR_DEFINITIONS=$(inherited) GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID='$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID_STR)' ================================================ FILE: VirtualBuddy/Config/Versions.xcconfig ================================================ MARKETING_VERSION = 2.1 CURRENT_PROJECT_VERSION = 284 DYLIB_CURRENT_VERSION = $(CURRENT_PROJECT_VERSION) VERSIONING_SYSTEM = apple-generic MACOSX_DEPLOYMENT_TARGET = 13.0 ================================================ FILE: VirtualBuddy/Info.plist ================================================ CFBundleDocumentTypes CFBundleTypeExtensions vbvm CFBundleTypeName VirtualBuddy VM CFBundleTypeRole Editor LSHandlerRank Owner LSItemContentTypes codes.rambo.VirtualBuddy.VM LSTypeIsPackage CFBundleTypeExtensions vbst CFBundleTypeName VirtualBuddy Saved State CFBundleTypeRole Editor LSHandlerRank Owner LSItemContentTypes codes.rambo.VirtualBuddy.SavedState LSTypeIsPackage CFBundleTypeName UTM virtual machine CFBundleTypeRole Viewer LSHandlerRank Alternate LSItemContentTypes com.utmapp.utm LSTypeIsPackageLSTypeIsPackage CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName codes.rambo.VirtualBuddy.Action CFBundleURLSchemes virtualbuddy NSAppTransportSecurity NSAllowsArbitraryLoads SUPublicEDKey $(VB_SPARKLE_PUBLIC_ED_KEY) SUScheduledCheckInterval $(VB_SPARKLE_CHECK_INTERVAL) UTExportedTypeDeclarations UTTypeConformsTo public.composite-content com.apple.package UTTypeDescription VirtualBuddy VM UTTypeIcons UTTypeIdentifier codes.rambo.VirtualBuddy.VM UTTypeTagSpecification public.filename-extension vbvm UTTypeConformsTo public.composite-content com.apple.package UTTypeDescription VirtualBuddy Saved State UTTypeIcons UTTypeIdentifier codes.rambo.VirtualBuddy.SavedState UTTypeTagSpecification public.filename-extension vbst UTImportedTypeDeclarations UTTypeConformsTo com.apple.package UTTypeDescription UTM virtual machine UTTypeIcons UTTypeIdentifier com.utmapp.utm UTTypeTagSpecification public.filename-extension utm ================================================ FILE: VirtualBuddy/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddy.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 55; objects = { /* Begin PBXAggregateTarget section */ F453C44F2DF0BCE3007EAD5F /* vctool */ = { isa = PBXAggregateTarget; buildConfigurationList = F453C4502DF0BCE3007EAD5F /* Build configuration list for PBXAggregateTarget "vctool" */; buildPhases = ( ); dependencies = ( F453C4592DF0BCEB007EAD5F /* PBXTargetDependency */, ); name = vctool; packageProductDependencies = ( ); productName = vctool; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 0196B45329292B2A00614EF1 /* LinuxVirtualMachineConfigurationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0196B45229292B2A00614EF1 /* LinuxVirtualMachineConfigurationHelper.swift */; }; 4BA6BE7D293D22E500F396AE /* VirtualMachineConfigurationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA6BE7C293D22E500F396AE /* VirtualMachineConfigurationHelper.swift */; }; F40A1E9D2C1873CA0033E47D /* VBBuildType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40A1E9C2C1873C60033E47D /* VBBuildType.swift */; }; F413696229916F6E002CE8D3 /* StatusItemButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413695129916F6E002CE8D3 /* StatusItemButton.swift */; }; F413696329916F6E002CE8D3 /* StatusBarPanelChrome.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413695229916F6E002CE8D3 /* StatusBarPanelChrome.swift */; }; F413696429916F6E002CE8D3 /* StatusItemProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413695329916F6E002CE8D3 /* StatusItemProviderProtocol.swift */; }; F413696529916F6E002CE8D3 /* StatusItemPanelContentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413695529916F6E002CE8D3 /* StatusItemPanelContentController.swift */; }; F413696729916F6E002CE8D3 /* VUIAppKitViewControllerHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413695729916F6E002CE8D3 /* VUIAppKitViewControllerHost.swift */; }; F413696829916F6E002CE8D3 /* StatusItemMenuBarExtraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413695829916F6E002CE8D3 /* StatusItemMenuBarExtraView.swift */; }; F413696929916F6E002CE8D3 /* StatusBarContentPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413695929916F6E002CE8D3 /* StatusBarContentPanel.swift */; }; F413696A29916F6E002CE8D3 /* StatusBarHighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413695A29916F6E002CE8D3 /* StatusBarHighlightView.swift */; }; F413696B29916F6E002CE8D3 /* NSStatusItem+.h in Headers */ = {isa = PBXBuildFile; fileRef = F413695C29916F6E002CE8D3 /* NSStatusItem+.h */; settings = {ATTRIBUTES = (Public, ); }; }; F413696C29916F6E002CE8D3 /* NSApplication+MenuBar.h in Headers */ = {isa = PBXBuildFile; fileRef = F413695D29916F6E002CE8D3 /* NSApplication+MenuBar.h */; settings = {ATTRIBUTES = (Public, ); }; }; F413696D29916F6E002CE8D3 /* NSStatusBarPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = F413695E29916F6E002CE8D3 /* NSStatusBarPrivate.h */; }; F413696E29916F6E002CE8D3 /* NSApplication+MenuBar.m in Sources */ = {isa = PBXBuildFile; fileRef = F413695F29916F6E002CE8D3 /* NSApplication+MenuBar.m */; }; F413696F29916F6E002CE8D3 /* NSStatusItem+.m in Sources */ = {isa = PBXBuildFile; fileRef = F413696029916F6E002CE8D3 /* NSStatusItem+.m */; }; F413697029916F6E002CE8D3 /* StatusItemManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413696129916F6E002CE8D3 /* StatusItemManager.swift */; }; F413697929917135002CE8D3 /* CGFloat+OnePixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413697829917135002CE8D3 /* CGFloat+OnePixel.swift */; }; F4136999299179B1002CE8D3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F4136998299179B1002CE8D3 /* Main.storyboard */; }; F413699A299179F8002CE8D3 /* VirtualCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */; }; F413699B299179F8002CE8D3 /* VirtualCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F41369A329917FA0002CE8D3 /* ScreenChangeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41369A229917FA0002CE8D3 /* ScreenChangeModifier.swift */; }; F41369A6299183C8002CE8D3 /* GuestLaunchAtLoginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41369A5299183C8002CE8D3 /* GuestLaunchAtLoginManager.swift */; }; F41369AE29918576002CE8D3 /* GuestHelperAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41369AD29918576002CE8D3 /* GuestHelperAppDelegate.swift */; }; F41369B229918576002CE8D3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F41369B129918576002CE8D3 /* Assets.xcassets */; }; F41369B529918576002CE8D3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F41369B329918576002CE8D3 /* Main.storyboard */; }; F41369BF2991863A002CE8D3 /* VirtualBuddyGuestHelper.app in Embed Login Item */ = {isa = PBXBuildFile; fileRef = F41369AB29918576002CE8D3 /* VirtualBuddyGuestHelper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F41369CA2991A492002CE8D3 /* HostConnectionStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41369C92991A492002CE8D3 /* HostConnectionStateProvider.swift */; }; F41369CC2991A68F002CE8D3 /* GuestSharedFoldersManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41369CB2991A68F002CE8D3 /* GuestSharedFoldersManager.swift */; }; F417255D288604A8004FF8A7 /* SoundConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F417255C288604A8004FF8A7 /* SoundConfigurationView.swift */; }; F417255F28861604004FF8A7 /* DecodableDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = F417255E28861604004FF8A7 /* DecodableDefault.swift */; }; F417256128861A05004FF8A7 /* SharingConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F417256028861A05004FF8A7 /* SharingConfigurationView.swift */; }; F41725632886DD37004FF8A7 /* SharedFolderListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41725622886DD37004FF8A7 /* SharedFolderListItem.swift */; }; F41725662886DF58004FF8A7 /* OpenSavePanelUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41725652886DF58004FF8A7 /* OpenSavePanelUtils.swift */; }; F41725682886E5AD004FF8A7 /* MaterialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41725672886E5AD004FF8A7 /* MaterialView.swift */; }; F417256C2887500F004FF8A7 /* StorageConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F417256B2887500F004FF8A7 /* StorageConfigurationView.swift */; }; F417256F2887544A004FF8A7 /* StorageDeviceDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F417256E2887544A004FF8A7 /* StorageDeviceDetailView.swift */; }; F417257128877121004FF8A7 /* DiskImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F417257028877121004FF8A7 /* DiskImageGenerator.swift */; }; F417257428877478004FF8A7 /* VirtualCore.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F417257328877478004FF8A7 /* VirtualCore.xcassets */; }; F41725762887758A004FF8A7 /* RandomNameGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41725752887758A004FF8A7 /* RandomNameGenerator.swift */; }; F417CB882E0EDECD0065B5D6 /* BackportedContentUnavailableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F417CB872E0EDECD0065B5D6 /* BackportedContentUnavailableView.swift */; }; F417CB8A2E0EDF1A0065B5D6 /* EqualWidthHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = F417CB892E0EDF1A0065B5D6 /* EqualWidthHStack.swift */; }; F417CBB92E0F3D2E0065B5D6 /* FirstLaunchExperienceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F417CBB82E0F3D2E0065B5D6 /* FirstLaunchExperienceView.swift */; }; F422586D2885CC9F009420AE /* SharedFocusEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F422586C2885CC9F009420AE /* SharedFocusEnvironment.swift */; }; F42258702885D537009420AE /* EphemeralTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F422586F2885D537009420AE /* EphemeralTextField.swift */; }; F42258722885E100009420AE /* VMConfigurationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42258712885E100009420AE /* VMConfigurationSheet.swift */; }; F42258742885E10B009420AE /* VMConfigurationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42258732885E10B009420AE /* VMConfigurationViewModel.swift */; }; F42258782885E14A009420AE /* DisplayConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42258772885E14A009420AE /* DisplayConfigurationView.swift */; }; F422587A2885E17D009420AE /* ConfigurationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42258792885E17D009420AE /* ConfigurationSection.swift */; }; F422587C2885E1CE009420AE /* NetworkConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F422587B2885E1CE009420AE /* NetworkConfigurationView.swift */; }; F422587E2885E2ED009420AE /* HardwareConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F422587D2885E2ED009420AE /* HardwareConfigurationView.swift */; }; F42258802885E71D009420AE /* PropertyControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F422587F2885E71D009420AE /* PropertyControl.swift */; }; F428622D2AE8726D0052F029 /* VirtualMachineControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = F428622C2AE8726D0052F029 /* VirtualMachineControls.swift */; }; F428622E2AE87D7E0052F029 /* DeepLinkSecurity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43B01152AD858FE00164CD1 /* DeepLinkSecurity.framework */; }; F428622F2AE87D7E0052F029 /* DeepLinkSecurity.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F43B01152AD858FE00164CD1 /* DeepLinkSecurity.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F42862372AE947C90052F029 /* WHPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42862362AE947C90052F029 /* WHPayload.swift */; }; F42C014A2888C2F800EB15CD /* InstallationConsole.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42C01492888C2F800EB15CD /* InstallationConsole.swift */; }; F42C014C2888C34B00EB15CD /* LogConsole.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42C014B2888C34B00EB15CD /* LogConsole.swift */; }; F42C014E2888CBCB00EB15CD /* InstallProgressStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42C014D2888CBCB00EB15CD /* InstallProgressStepView.swift */; }; F42C015A2888FC0C00EB15CD /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42C01512888FC0C00EB15CD /* LibraryView.swift */; }; F42C015B2888FC0C00EB15CD /* VMSessionConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42C01542888FC0C00EB15CD /* VMSessionConfigurationView.swift */; }; F42C015C2888FC0C00EB15CD /* VirtualMachineSessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42C01552888FC0C00EB15CD /* VirtualMachineSessionView.swift */; }; F42C015D2888FC0C00EB15CD /* SwiftUIVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42C01572888FC0C00EB15CD /* SwiftUIVMView.swift */; }; F42C015E2888FC0C00EB15CD /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42C01592888FC0C00EB15CD /* SettingsScreen.swift */; }; F42C01612888FC3500EB15CD /* LibraryItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42C01602888FC3500EB15CD /* LibraryItemView.swift */; }; F42CF4A82DF5FEC3001DE049 /* BlurHashToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42CF4A72DF5FEC3001DE049 /* BlurHashToken.swift */; }; F43B01182AD858FE00164CD1 /* DeepLinkSecurity.h in Headers */ = {isa = PBXBuildFile; fileRef = F43B01172AD858FE00164CD1 /* DeepLinkSecurity.h */; settings = {ATTRIBUTES = (Public, ); }; }; F43B011B2AD858FE00164CD1 /* DeepLinkSecurity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43B01152AD858FE00164CD1 /* DeepLinkSecurity.framework */; }; F43B011C2AD858FE00164CD1 /* DeepLinkSecurity.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F43B01152AD858FE00164CD1 /* DeepLinkSecurity.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F43B01352AD8590F00164CD1 /* DeepLinkAuthUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01242AD8590F00164CD1 /* DeepLinkAuthUI.swift */; }; F43B01362AD8590F00164CD1 /* DeepLinkSentinel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01252AD8590F00164CD1 /* DeepLinkSentinel.swift */; }; F43B01372AD8590F00164CD1 /* OpenDeepLinkRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01272AD8590F00164CD1 /* OpenDeepLinkRequest.swift */; }; F43B01382AD8590F00164CD1 /* DeepLinkClientDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01282AD8590F00164CD1 /* DeepLinkClientDescriptor.swift */; }; F43B01392AD8590F00164CD1 /* DeepLinkClient+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B012A2AD8590F00164CD1 /* DeepLinkClient+Crypto.swift */; }; F43B013A2AD8590F00164CD1 /* DeepLinkClientDescriptor+.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B012B2AD8590F00164CD1 /* DeepLinkClientDescriptor+.swift */; }; F43B013B2AD8590F00164CD1 /* DeepLinkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B012C2AD8590F00164CD1 /* DeepLinkClient.swift */; }; F43B013C2AD8590F00164CD1 /* KeychainDeepLinkAuthStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B012E2AD8590F00164CD1 /* KeychainDeepLinkAuthStore.swift */; }; F43B013D2AD8590F00164CD1 /* MemoryDeepLinkAuthStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B012F2AD8590F00164CD1 /* MemoryDeepLinkAuthStore.swift */; }; F43B013E2AD8590F00164CD1 /* DeepLinkManagementStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01302AD8590F00164CD1 /* DeepLinkManagementStore.swift */; }; F43B013F2AD8590F00164CD1 /* UserDefaultsDeepLinkManagementStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01312AD8590F00164CD1 /* UserDefaultsDeepLinkManagementStore.swift */; }; F43B01402AD8590F00164CD1 /* DeepLinkAuthStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01322AD8590F00164CD1 /* DeepLinkAuthStore.swift */; }; F43B01412AD8590F00164CD1 /* DeepLinkSecurityDefines.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01342AD8590F00164CD1 /* DeepLinkSecurityDefines.swift */; }; F43B01442AD85A6500164CD1 /* VirtualBuddyDeepLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01432AD85A6500164CD1 /* VirtualBuddyDeepLinks.swift */; }; F43B01472AD85A7D00164CD1 /* URLQueryItemCoder in Frameworks */ = {isa = PBXBuildFile; productRef = F43B01462AD85A7D00164CD1 /* URLQueryItemCoder */; }; F43B014B2AD85ABB00164CD1 /* DeepLinkAuthDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01492AD85ABB00164CD1 /* DeepLinkAuthDialog.swift */; }; F43B014E2AD86BFA00164CD1 /* DeepLinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B014D2AD86BFA00164CD1 /* DeepLinkHandler.swift */; }; F443620A29B7947A00745B43 /* GuestAdditionsDiskImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F443620929B7947A00745B43 /* GuestAdditionsDiskImage.swift */; }; F443620C29B79A6800745B43 /* VirtualBuddyGuest.app in Embed Guest App */ = {isa = PBXBuildFile; fileRef = F4C18A4228491B8500335EC7 /* VirtualBuddyGuest.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F443620F29B7A0C600745B43 /* CreateGuestImage.sh in Resources */ = {isa = PBXBuildFile; fileRef = F443620E29B7A0C600745B43 /* CreateGuestImage.sh */; }; F444D0CA2DF321CD0086537A /* CatalogGroupPlaceholder.heic in Resources */ = {isa = PBXBuildFile; fileRef = F444D0C92DF321CD0086537A /* CatalogGroupPlaceholder.heic */; }; F444D0CC2DF322B90086537A /* SoftwareCatalog+Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F444D0CB2DF322B50086537A /* SoftwareCatalog+Placeholder.swift */; }; F444D0CE2DF32E100086537A /* BlurHashFullBleedBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = F444D0CD2DF32E100086537A /* BlurHashFullBleedBackground.swift */; }; F444D0F42DF34BE80086537A /* CALayer+Asset.swift in Sources */ = {isa = PBXBuildFile; fileRef = F444D0F32DF34BE40086537A /* CALayer+Asset.swift */; }; F444D0F62DF37D170086537A /* VirtualBuddyMonoIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F444D0F52DF37D170086537A /* VirtualBuddyMonoIcon.swift */; }; F444D0F82DF37D410086537A /* VirtualDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F444D0F72DF37D410086537A /* VirtualDisplayView.swift */; }; F444D0FA2DF37DFB0086537A /* InstallProgressDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F444D0F92DF37DFB0086537A /* InstallProgressDisplayView.swift */; }; F444D0FC2DF37EF80086537A /* VirtualBuddyMonoProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F444D0FB2DF37EF80086537A /* VirtualBuddyMonoProgressView.swift */; }; F444D1342BB478AD00AB786F /* VBMemoryLeakDebugAssertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F444D1332BB478AD00AB786F /* VBMemoryLeakDebugAssertions.swift */; }; F4450CCA2ACB0DB500092618 /* KeyboardDeviceConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4450CC92ACB0DB500092618 /* KeyboardDeviceConfigurationView.swift */; }; F44C00FB2889CE1600640BF5 /* VBVirtualMachine+Virtualization.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44C00FA2889CE1600640BF5 /* VBVirtualMachine+Virtualization.swift */; }; F4510A782AE2A16F00E24DD9 /* WeakReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4510A772AE2A16F00E24DD9 /* WeakReference.swift */; }; F4510A7B2AE2B3B300E24DD9 /* DeepLinkSecurity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43B01152AD858FE00164CD1 /* DeepLinkSecurity.framework */; }; F453C4122DF0B1ED007EAD5F /* BuddyKit in Frameworks */ = {isa = PBXBuildFile; productRef = F453C4112DF0B1ED007EAD5F /* BuddyKit */; }; F453C41D2DF0B43D007EAD5F /* ResolvedCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C41A2DF0B43D007EAD5F /* ResolvedCatalog.swift */; }; F453C41E2DF0B43D007EAD5F /* LegacyCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4182DF0B43D007EAD5F /* LegacyCatalog.swift */; }; F453C41F2DF0B43D007EAD5F /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4162DF0B43D007EAD5F /* BlurHashEncode.swift */; }; F453C4202DF0B43D007EAD5F /* SoftwareCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C41B2DF0B43D007EAD5F /* SoftwareCatalog.swift */; }; F453C4212DF0B43D007EAD5F /* MobileDeviceFramework.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4192DF0B43D007EAD5F /* MobileDeviceFramework.swift */; }; F453C4232DF0B5B1007EAD5F /* VirtualBuddyEntryPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4222DF0B5B1007EAD5F /* VirtualBuddyEntryPoint.swift */; }; F453C4242DF0B602007EAD5F /* VirtualUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F498ACFE2884BF13006F1C00 /* VirtualUI.framework */; }; F453C4252DF0B602007EAD5F /* VirtualUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F498ACFE2884BF13006F1C00 /* VirtualUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F453C42B2DF0B792007EAD5F /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = F453C42A2DF0B792007EAD5F /* ArgumentParser */; }; F453C43B2DF0B7A5007EAD5F /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C42E2DF0B7A5007EAD5F /* Helpers.swift */; }; F453C43C2DF0B7A5007EAD5F /* VCTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4392DF0B7A5007EAD5F /* VCTool.swift */; }; F453C43D2DF0B7A5007EAD5F /* URL+ContentLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4302DF0B7A5007EAD5F /* URL+ContentLength.swift */; }; F453C43E2DF0B7A5007EAD5F /* IPSWCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4352DF0B7A5007EAD5F /* IPSWCommand.swift */; }; F453C43F2DF0B7A5007EAD5F /* ImageCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4342DF0B7A5007EAD5F /* ImageCommand.swift */; }; F453C4402DF0B7A5007EAD5F /* BuildManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C42C2DF0B7A5007EAD5F /* BuildManifest.swift */; }; F453C4412DF0B7A5007EAD5F /* GroupCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4332DF0B7A5007EAD5F /* GroupCommand.swift */; }; F453C4422DF0B7A5007EAD5F /* MobileDeviceCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4372DF0B7A5007EAD5F /* MobileDeviceCommand.swift */; }; F453C4432DF0B7A5007EAD5F /* BuildManifest+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C42D2DF0B7A5007EAD5F /* BuildManifest+Fetch.swift */; }; F453C4442DF0B7A5007EAD5F /* TreeStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C42F2DF0B7A5007EAD5F /* TreeStringConvertible.swift */; }; F453C4452DF0B7A5007EAD5F /* MigrateCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4362DF0B7A5007EAD5F /* MigrateCommand.swift */; }; F453C4462DF0B7A5007EAD5F /* ResolveCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4382DF0B7A5007EAD5F /* ResolveCommand.swift */; }; F453C4472DF0B7A5007EAD5F /* CatalogCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4322DF0B7A5007EAD5F /* CatalogCommand.swift */; }; F453C44A2DF0B7F6007EAD5F /* FragmentZip in Frameworks */ = {isa = PBXBuildFile; productRef = F453C4492DF0B7F6007EAD5F /* FragmentZip */; }; F453C44C2DF0B835007EAD5F /* libcurl.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = F453C44B2DF0B835007EAD5F /* libcurl.tbd */; }; F453C44E2DF0B870007EAD5F /* VirtualBuddyCLI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C44D2DF0B869007EAD5F /* VirtualBuddyCLI.swift */; }; F453C45B2DF0C4BE007EAD5F /* BuddyKit in Frameworks */ = {isa = PBXBuildFile; productRef = F453C45A2DF0C4BE007EAD5F /* BuddyKit */; }; F453C45D2DF0D28A007EAD5F /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = F453C45C2DF0D286007EAD5F /* README.md */; }; F453C4632DF0E609007EAD5F /* BuddyKit in Frameworks */ = {isa = PBXBuildFile; productRef = F453C4622DF0E609007EAD5F /* BuddyKit */; }; F453C4682DF10181007EAD5F /* BlurHashCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4672DF10181007EAD5F /* BlurHashCommand.swift */; }; F453C4892DF1CDA0007EAD5F /* DownloadBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4882DF1CDA0007EAD5F /* DownloadBackend.swift */; }; F453C4922DF1D213007EAD5F /* FakeRestoreImage.ipsw in Resources */ = {isa = PBXBuildFile; fileRef = F453C4912DF1D213007EAD5F /* FakeRestoreImage.ipsw */; }; F453C49B2DF1D768007EAD5F /* SimulatedDownloadBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C49A2DF1D768007EAD5F /* SimulatedDownloadBackend.swift */; }; F453C4A02DF1D7C0007EAD5F /* RestoreBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C49F2DF1D7C0007EAD5F /* RestoreBackend.swift */; }; F453C4A22DF1D7F6007EAD5F /* SimulatedRestoreBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4A12DF1D7F6007EAD5F /* SimulatedRestoreBackend.swift */; }; F453C4A42DF1D861007EAD5F /* VirtualizationRestoreBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4A32DF1D85C007EAD5F /* VirtualizationRestoreBackend.swift */; }; F453C4B42DF20301007EAD5F /* RestoreImageURLInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4B32DF20301007EAD5F /* RestoreImageURLInputView.swift */; }; F453C4B92DF21985007EAD5F /* VMInstallData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4B82DF21985007EAD5F /* VMInstallData.swift */; }; F453C4BB2DF231BB007EAD5F /* PreventTerminationAssertion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453C4BA2DF231B7007EAD5F /* PreventTerminationAssertion.swift */; }; F45502142DF394DC005582A4 /* VirtualBuddyInstallerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45502132DF394DC005582A4 /* VirtualBuddyInstallerInputView.swift */; }; F45502162DF45E53005582A4 /* VBSettings+CatalogDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45502152DF45E4D005582A4 /* VBSettings+CatalogDownload.swift */; }; F45502232DF463A1005582A4 /* UniversalMac_13.3.1_22E261_Restore.ipsw in Copy Preview Library Downloads */ = {isa = PBXBuildFile; fileRef = F45502182DF46368005582A4 /* UniversalMac_13.3.1_22E261_Restore.ipsw */; }; F45502242DF463A1005582A4 /* UniversalMac_14.0_23A344_Restore.ipsw in Copy Preview Library Downloads */ = {isa = PBXBuildFile; fileRef = F45502192DF46368005582A4 /* UniversalMac_14.0_23A344_Restore.ipsw */; }; F45502252DF463A1005582A4 /* UniversalMac_14.5_23F79_Restore.ipsw in Copy Preview Library Downloads */ = {isa = PBXBuildFile; fileRef = F455021A2DF46368005582A4 /* UniversalMac_14.5_23F79_Restore.ipsw */; }; F45502262DF463A1005582A4 /* UniversalMac_15.3_24D60_Restore.ipsw in Copy Preview Library Downloads */ = {isa = PBXBuildFile; fileRef = F455021B2DF46368005582A4 /* UniversalMac_15.3_24D60_Restore.ipsw */; }; F45502272DF463A1005582A4 /* UniversalMac_15.5_24F74_Restore.ipsw in Copy Preview Library Downloads */ = {isa = PBXBuildFile; fileRef = F455021C2DF46368005582A4 /* UniversalMac_15.5_24F74_Restore.ipsw */; }; F4561A6828981B4100055289 /* VirtualMachineNameInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4561A6728981B4100055289 /* VirtualMachineNameInputView.swift */; }; F462C9422E0C96D300C172E2 /* FB18383725Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = F462C9412E0C96D300C172E2 /* FB18383725Window.swift */; }; F465C3AE284F93A5006E9ED4 /* VBAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F465C3AD284F93A5006E9ED4 /* VBAPIClient.swift */; }; F465C3B0284F9660006E9ED4 /* VBRestoreImagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F465C3AF284F9660006E9ED4 /* VBRestoreImagesResponse.swift */; }; F465C3B2284F9666006E9ED4 /* VBRestoreImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F465C3B1284F9666006E9ED4 /* VBRestoreImageInfo.swift */; }; F465C3B8284FA252006E9ED4 /* URLSessionDownloadBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = F465C3B7284FA252006E9ED4 /* URLSessionDownloadBackend.swift */; }; F46FFBA82804F07400D61023 /* VBNVRAMVariable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46FFBA72804F07400D61023 /* VBNVRAMVariable.swift */; }; F46FFBAA2804F0A000D61023 /* VZVirtualMachineConfiguration+NVRAM.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46FFBA92804F0A000D61023 /* VZVirtualMachineConfiguration+NVRAM.swift */; }; F46FFBAC28059FF600D61023 /* VMInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46FFBAB28059FF600D61023 /* VMInstance.swift */; }; F47BCDA12C5BE8FF00165191 /* ipsws_v2.json in Copy Software Catalog Feeds */ = {isa = PBXBuildFile; fileRef = F47BCD9F2C5BE8EF00165191 /* ipsws_v2.json */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; F47BCDA22C5BE8FF00165191 /* linux_v2.json in Copy Software Catalog Feeds */ = {isa = PBXBuildFile; fileRef = F47BCDA02C5BE8EF00165191 /* linux_v2.json */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; F47BCDCB2C5C01D100165191 /* BlurHashDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47BCDCA2C5C01CE00165191 /* BlurHashDecoding.swift */; }; F47BCDCD2C5C01EF00165191 /* RemoteImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47BCDCC2C5C01EF00165191 /* RemoteImage.swift */; }; F47BCDCF2C5C023E00165191 /* CatalogGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47BCDCE2C5C023900165191 /* CatalogGroupView.swift */; }; F47BCDD12C5C06CE00165191 /* CatalogGroupPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47BCDD02C5C06C900165191 /* CatalogGroupPicker.swift */; }; F47BCDD32C5C0AB300165191 /* KeyboardNavigationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47BCDD22C5C0AB300165191 /* KeyboardNavigationModifier.swift */; }; F47BCDD52C5C0B8F00165191 /* Array+Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47BCDD42C5C0B8C00165191 /* Array+Navigation.swift */; }; F47BCDD72C5D2B4600165191 /* CatalogExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47BCDD62C5D2B4300165191 /* CatalogExtensions.swift */; }; F47BCDD92C5D2EE300165191 /* RestoreImageBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47BCDD82C5D2EDB00165191 /* RestoreImageBrowser.swift */; }; F485B91B2BB22D2D004B3C2B /* VBSavedStateMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F485B91A2BB22D2D004B3C2B /* VBSavedStateMetadata.swift */; }; F485B91D2BB2F0D9004B3C2B /* ProcessInfo+ECID.swift in Sources */ = {isa = PBXBuildFile; fileRef = F485B91C2BB2F0D9004B3C2B /* ProcessInfo+ECID.swift */; }; F485B91F2BB2F4AC004B3C2B /* Bundle+Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = F485B91E2BB2F4AC004B3C2B /* Bundle+Version.swift */; }; F485B9222BB306AF004B3C2B /* VBDebugUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = F485B9202BB306AF004B3C2B /* VBDebugUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; F485B9232BB306AF004B3C2B /* VBDebugUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = F485B9212BB306AF004B3C2B /* VBDebugUtil.m */; }; F48E0D03288858E00080DDFA /* ManagedDiskImageEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D02288858DF0080DDFA /* ManagedDiskImageEditor.swift */; }; F48E0D0728885E140080DDFA /* PreviewSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D0628885E140080DDFA /* PreviewSupport.swift */; }; F48E0D0C2888760D0080DDFA /* VBMacDevice+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D0B2888760D0080DDFA /* VBMacDevice+Storage.swift */; }; F48E0D1A288882BD0080DDFA /* InstallationWizardTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D14288882BD0080DDFA /* InstallationWizardTitle.swift */; }; F48E0D1B288882BD0080DDFA /* InstallMethodPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D15288882BD0080DDFA /* InstallMethodPicker.swift */; }; F48E0D1C288882BD0080DDFA /* RestoreImageSelectionStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D16288882BD0080DDFA /* RestoreImageSelectionStep.swift */; }; F48E0D1D288882BD0080DDFA /* AuthenticatingWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D17288882BD0080DDFA /* AuthenticatingWebView.swift */; }; F48E0D1E288882BD0080DDFA /* VMInstallationWizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D18288882BD0080DDFA /* VMInstallationWizard.swift */; }; F48E0D1F288882BD0080DDFA /* VMInstallationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D19288882BD0080DDFA /* VMInstallationViewModel.swift */; }; F48E0D23288882E50080DDFA /* DecentFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D20288882E50080DDFA /* DecentFormView.swift */; }; F48E0D24288882E50080DDFA /* NSAlert+Confirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D21288882E50080DDFA /* NSAlert+Confirmation.swift */; }; F48E0D25288882E50080DDFA /* OnAppearOnce.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D22288882E50080DDFA /* OnAppearOnce.swift */; }; F48E0D2A288883150080DDFA /* OpenCocoaWindowAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D27288883150080DDFA /* OpenCocoaWindowAction.swift */; }; F48E0D2B288883150080DDFA /* HostingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D28288883150080DDFA /* HostingWindowController.swift */; }; F48E0D2C288883150080DDFA /* WindowEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D29288883150080DDFA /* WindowEnvironment.swift */; }; F48E0D2F2888835A0080DDFA /* VirtualUIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D2E2888835A0080DDFA /* VirtualUIConstants.swift */; }; F48E0D32288884A10080DDFA /* RestoreImageDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D31288884A10080DDFA /* RestoreImageDownloadView.swift */; }; F48E0D34288889E60080DDFA /* InstallConfigurationStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48E0D33288889E60080DDFA /* InstallConfigurationStepView.swift */; }; F4959F3C2992A284001DF4CB /* GuestAppInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4959F3B2992A284001DF4CB /* GuestAppInstaller.swift */; }; F498AD012884BF13006F1C00 /* VirtualUI.h in Headers */ = {isa = PBXBuildFile; fileRef = F498AD002884BF13006F1C00 /* VirtualUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; F498AD042884BF13006F1C00 /* VirtualUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F498ACFE2884BF13006F1C00 /* VirtualUI.framework */; }; F498AD052884BF13006F1C00 /* VirtualUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F498ACFE2884BF13006F1C00 /* VirtualUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F498AD0C2884BF67006F1C00 /* VirtualCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */; }; F498AD0E2884BF9D006F1C00 /* VMConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F498AD0D2884BF9D006F1C00 /* VMConfigurationView.swift */; }; F498AD142884C36A006F1C00 /* NumericValueField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F498AD112884C36A006F1C00 /* NumericValueField.swift */; }; F498AD162884C36A006F1C00 /* ControlGroupChrome.swift in Sources */ = {isa = PBXBuildFile; fileRef = F498AD132884C36A006F1C00 /* ControlGroupChrome.swift */; }; F498AD182884C593006F1C00 /* NumericPropertyControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F498AD172884C593006F1C00 /* NumericPropertyControl.swift */; }; F498AD1A2884C5FF006F1C00 /* SliderConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F498AD192884C5FF006F1C00 /* SliderConversion.swift */; }; F49A68E12884917E00A17582 /* ConfigurationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49A68E02884917E00A17582 /* ConfigurationModels.swift */; }; F49AA2C329BA22A5009625F7 /* VBRestorableWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49AA2C229BA22A5009625F7 /* VBRestorableWindow.swift */; }; F49AA2C529BA31CC009625F7 /* VirtualMachineSessionUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49AA2C429BA31CC009625F7 /* VirtualMachineSessionUI.swift */; }; F49AA2C729BA3F2B009625F7 /* VBRestorableWindow+Resizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49AA2C629BA3F2B009625F7 /* VBRestorableWindow+Resizing.swift */; }; F49B82982E02F5A900395F87 /* VMArtworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49B82972E02F5A300395F87 /* VMArtworkView.swift */; }; F49B82A82E0304D400395F87 /* PreviewMac.vbvm in Copy Preview Library */ = {isa = PBXBuildFile; fileRef = F49B829F2E02FCB100395F87 /* PreviewMac.vbvm */; }; F49B82A92E0304D400395F87 /* PreviewMacBlurHash.vbvm in Copy Preview Library */ = {isa = PBXBuildFile; fileRef = F49B829D2E02FCB100395F87 /* PreviewMacBlurHash.vbvm */; }; F49B82AA2E0304D400395F87 /* PreviewMacNoArtwork.vbvm in Copy Preview Library */ = {isa = PBXBuildFile; fileRef = F49B82A32E02FCF400395F87 /* PreviewMacNoArtwork.vbvm */; }; F49B82AB2E0304D400395F87 /* PreviewLinux.vbvm in Copy Preview Library */ = {isa = PBXBuildFile; fileRef = F49B829E2E02FCB100395F87 /* PreviewLinux.vbvm */; }; F49B82AC2E0304D400395F87 /* PreviewLinuxBlurHash.vbvm in Copy Preview Library */ = {isa = PBXBuildFile; fileRef = F49B82A52E03049C00395F87 /* PreviewLinuxBlurHash.vbvm */; }; F49B82AD2E0304D400395F87 /* PreviewLinuxNoArtwork.vbvm in Copy Preview Library */ = {isa = PBXBuildFile; fileRef = F49B82A62E03049C00395F87 /* PreviewLinuxNoArtwork.vbvm */; }; F49B82FC2E034EAD00395F87 /* WHDesktopPictureService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49B82FB2E034EAD00395F87 /* WHDesktopPictureService.swift */; }; F49B83002E034F6800395F87 /* NSImage+DesktopPicture.m in Sources */ = {isa = PBXBuildFile; fileRef = F49B82FF2E034F6800395F87 /* NSImage+DesktopPicture.m */; }; F49B83012E034F6800395F87 /* NSImage+DesktopPicture.h in Headers */ = {isa = PBXBuildFile; fileRef = F49B82FE2E034F6800395F87 /* NSImage+DesktopPicture.h */; settings = {ATTRIBUTES = (Public, ); }; }; F49B832B2E04593A00395F87 /* NSImage+DRMProtected.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49B832A2E04593100395F87 /* NSImage+DRMProtected.swift */; }; F49B832D2E046B8D00395F87 /* GuestAppConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49B832C2E046B8D00395F87 /* GuestAppConfigurationView.swift */; }; F49B83712E04837400395F87 /* CGImage+FullyTransparent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49B83702E04837400395F87 /* CGImage+FullyTransparent.swift */; }; F49FD87D2DFB68F50019D638 /* VMImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49FD87C2DFB68F20019D638 /* VMImporter.swift */; }; F49FD87F2DFB6B670019D638 /* VMImporterRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49FD87E2DFB6B630019D638 /* VMImporterRegistry.swift */; }; F49FD8812DFB6CDD0019D638 /* UTMImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49FD8802DFB6CD80019D638 /* UTMImporter.swift */; }; F49FD8842DFB727B0019D638 /* VMImporter+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49FD8832DFB72790019D638 /* VMImporter+Helpers.swift */; }; F49FD8862DFB728A0019D638 /* UTMAppleConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49FD8852DFB728A0019D638 /* UTMAppleConfiguration.swift */; }; F4A21BF228032FD8001072B8 /* VMLibraryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A21BF128032FD8001072B8 /* VMLibraryController.swift */; }; F4A21BF428033102001072B8 /* VBError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A21BF328033102001072B8 /* VBError.swift */; }; F4A7FB3B2BB5E79100E4C12A /* DirectoryObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A7FB3A2BB5E79100E4C12A /* DirectoryObserver.swift */; }; F4A7FB3D2BB5E8A200E4C12A /* VMSavedStatesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A7FB3C2BB5E8A200E4C12A /* VMSavedStatesController.swift */; }; F4A7FB3F2BB5EBEF00E4C12A /* SavedStatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A7FB3E2BB5EBEF00E4C12A /* SavedStatePicker.swift */; }; F4A7FB472BB5ED7E00E4C12A /* Save-2024-03-27_16;06;24.vbst in Copy Preview Saved States */ = {isa = PBXBuildFile; fileRef = F4A7FB402BB5ED4A00E4C12A /* Save-2024-03-27_16;06;24.vbst */; }; F4A7FB482BB5ED7E00E4C12A /* Save-2024-03-27_16;07;04.vbst in Copy Preview Saved States */ = {isa = PBXBuildFile; fileRef = F4A7FB412BB5ED4A00E4C12A /* Save-2024-03-27_16;07;04.vbst */; }; F4A7FB492BB5ED7E00E4C12A /* Save-2024-03-27_16;08;06.vbst in Copy Preview Saved States */ = {isa = PBXBuildFile; fileRef = F4A7FB422BB5ED4A00E4C12A /* Save-2024-03-27_16;08;06.vbst */; }; F4A7FB4A2BB5ED7E00E4C12A /* Save-2024-03-27_16;08;28.vbst in Copy Preview Saved States */ = {isa = PBXBuildFile; fileRef = F4A7FB432BB5ED4A00E4C12A /* Save-2024-03-27_16;08;28.vbst */; }; F4A7FB4B2BB5ED7E00E4C12A /* Save-2024-03-27_16;08;51.vbst in Copy Preview Saved States */ = {isa = PBXBuildFile; fileRef = F4A7FB442BB5ED4A00E4C12A /* Save-2024-03-27_16;08;51.vbst */; }; F4A7FB6E2BB7206C00E4C12A /* SelfSizingGroupedForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A7FB6D2BB7206C00E4C12A /* SelfSizingGroupedForm.swift */; }; F4A7FB732BB7238A00E4C12A /* SwiftUIIntrospect-Static in Frameworks */ = {isa = PBXBuildFile; productRef = F4A7FB722BB7238A00E4C12A /* SwiftUIIntrospect-Static */; }; F4A7FB752BB7252A00E4C12A /* PreviewSupport-VirtualUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A7FB742BB7252A00E4C12A /* PreviewSupport-VirtualUI.swift */; }; F4B5C5D32886FA8D005AA632 /* SharedFoldersManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B5C5D22886FA8D005AA632 /* SharedFoldersManagementView.swift */; }; F4B5C5D52886FFB5005AA632 /* PointingDeviceConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B5C5D42886FFB5005AA632 /* PointingDeviceConfigurationView.swift */; }; F4B5C5D728870619005AA632 /* ConfigurationModels+Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B5C5D628870619005AA632 /* ConfigurationModels+Validation.swift */; }; F4B5C5D928870BBF005AA632 /* ConfigurationModels+Summary.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B5C5D828870BBF005AA632 /* ConfigurationModels+Summary.swift */; }; F4B5C5DB28873628005AA632 /* GroupedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B5C5DA28873628005AA632 /* GroupedList.swift */; }; F4BE580E29BA6C8D00C5525C /* DefaultsDomains.plist in Resources */ = {isa = PBXBuildFile; fileRef = F4BE580D29BA6C8D00C5525C /* DefaultsDomains.plist */; }; F4BE581129BA6DFC00C5525C /* DefaultsImportController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BE581029BA6DFC00C5525C /* DefaultsImportController.swift */; }; F4BE581329BA6E0D00C5525C /* DefaultsDomainDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BE581229BA6E0D00C5525C /* DefaultsDomainDescriptor.swift */; }; F4BE584029BA7B7A00C5525C /* DefaultsDomain+ExportImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BE583F29BA7B7A00C5525C /* DefaultsDomain+ExportImport.swift */; }; F4BE584229BA7BDB00C5525C /* WHDefaultsImportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BE584129BA7BDB00C5525C /* WHDefaultsImportService.swift */; }; F4BE9C5227FF052100B648F8 /* VirtualBuddyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BE9C5127FF052100B648F8 /* VirtualBuddyApp.swift */; }; F4BE9C5627FF052100B648F8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4BE9C5527FF052100B648F8 /* Assets.xcassets */; }; F4BE9C5927FF052100B648F8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4BE9C5827FF052100B648F8 /* Preview Assets.xcassets */; }; F4BE9C6827FF053A00B648F8 /* VirtualCore.h in Headers */ = {isa = PBXBuildFile; fileRef = F4BE9C6727FF053A00B648F8 /* VirtualCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; F4BE9C6B27FF053A00B648F8 /* VirtualCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */; }; F4BE9C6C27FF053A00B648F8 /* VirtualCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F4BE9C7627FF055100B648F8 /* VBVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BE9C7327FF055100B648F8 /* VBVirtualMachine.swift */; }; F4BE9C7827FF055100B648F8 /* MacOSVirtualMachineConfigurationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BE9C7527FF055100B648F8 /* MacOSVirtualMachineConfigurationHelper.swift */; }; F4BE9C7A27FF05B900B648F8 /* VMController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BE9C7927FF05B900B648F8 /* VMController.swift */; }; F4BE9C8127FF111100B648F8 /* VirtualBuddyAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BE9C8027FF111100B648F8 /* VirtualBuddyAppDelegate.swift */; }; F4BE9C8627FF140F00B648F8 /* VirtualizationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = F4BE9C8527FF13FF00B648F8 /* VirtualizationPrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; F4C189E32848F59F00335EC7 /* VirtualWormhole.h in Headers */ = {isa = PBXBuildFile; fileRef = F4C189E22848F59F00335EC7 /* VirtualWormhole.h */; settings = {ATTRIBUTES = (Public, ); }; }; F4C189E62848F59F00335EC7 /* VirtualWormhole.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */; }; F4C189E72848F59F00335EC7 /* VirtualWormhole.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F4C189EE2848F5B500335EC7 /* WormholeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C189ED2848F5B500335EC7 /* WormholeManager.swift */; }; F4C189F22848F5F500335EC7 /* VirtualWormhole.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */; settings = {ATTRIBUTES = (Required, ); }; }; F4C189F72848F6A600335EC7 /* VirtualCoreConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C189F62848F6A600335EC7 /* VirtualCoreConstants.swift */; }; F4C189FA2848F6F700335EC7 /* VirtualWormholeConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C189F92848F6F700335EC7 /* VirtualWormholeConstants.swift */; }; F4C189FD2848F8F600335EC7 /* WHSharedClipboardService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C189FC2848F8F600335EC7 /* WHSharedClipboardService.swift */; }; F4C189FF2848FB3F00335EC7 /* WormholeServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C189FE2848FB3F00335EC7 /* WormholeServiceProtocol.swift */; }; F4C18A4528491B8500335EC7 /* GuestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C18A4428491B8500335EC7 /* GuestAppDelegate.swift */; }; F4C18A4728491B8500335EC7 /* GuestDashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C18A4628491B8500335EC7 /* GuestDashboard.swift */; }; F4C18A4928491B8500335EC7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4C18A4828491B8500335EC7 /* Assets.xcassets */; }; F4C18A4C28491B8500335EC7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4C18A4B28491B8500335EC7 /* Preview Assets.xcassets */; }; F4C18A5228491B9D00335EC7 /* VirtualWormhole.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */; }; F4C18A5328491B9D00335EC7 /* VirtualWormhole.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F4C2374D2888A462001FF286 /* VolumeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C2374C2888A462001FF286 /* VolumeUtils.swift */; }; F4C237502888AF67001FF286 /* LogStreamer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C2374F2888AF67001FF286 /* LogStreamer.swift */; }; F4C947BF2E0B0F71001ACC91 /* URL+ExtendedAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C947BE2E0B0F71001ACC91 /* URL+ExtendedAttributes.swift */; }; F4C947D62E0B12D0001ACC91 /* String+AppleOSBuild.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C947D52E0B12D0001ACC91 /* String+AppleOSBuild.swift */; }; F4C947DA2E0B1E5D001ACC91 /* SoftwareCatalog+DownloadMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C947D92E0B1E5D001ACC91 /* SoftwareCatalog+DownloadMatching.swift */; }; F4CD13202E05A5780067DC75 /* FileSystemPathFormControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CD131F2E05A5780067DC75 /* FileSystemPathFormControl.swift */; }; F4CD133C2E05A9DF0067DC75 /* OpenVirtualBuddySettingsAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CD133B2E05A9DF0067DC75 /* OpenVirtualBuddySettingsAction.swift */; }; F4CD133E2E05AB280067DC75 /* BackwardsCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CD133D2E05AB280067DC75 /* BackwardsCompatibility.swift */; }; F4CD13402E05AB8F0067DC75 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CD133F2E05AB8F0067DC75 /* GeneralSettingsView.swift */; }; F4CD13442E05AD400067DC75 /* VerticalLabeledContentStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CD13432E05AD400067DC75 /* VerticalLabeledContentStyle.swift */; }; F4CD13462E05B4DE0067DC75 /* VirtualizationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CD13452E05B4DE0067DC75 /* VirtualizationSettingsView.swift */; }; F4CD13482E05B67E0067DC75 /* SettingsFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CD13472E05B67E0067DC75 /* SettingsFooter.swift */; }; F4CD134A2E05CB390067DC75 /* AutomationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CD13492E05CB390067DC75 /* AutomationSettingsView.swift */; }; F4D0F71528667984004D5782 /* VBVirtualMachine+Screenshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D0F71428667984004D5782 /* VBVirtualMachine+Screenshot.swift */; }; F4D0F71828674E4B004D5782 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = F4D0F71728674E4B004D5782 /* Sparkle */; }; F4D0F71A28674E76004D5782 /* SoftwareUpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D0F71928674E76004D5782 /* SoftwareUpdateController.swift */; }; F4D0F71F2867517A004D5782 /* AppUpdateChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D0F71E2867517A004D5782 /* AppUpdateChannel.swift */; }; F4D3059729B8D9D30006E748 /* WormholePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D3059629B8D9D30006E748 /* WormholePacket.swift */; }; F4D3059F29B8DB700006E748 /* WormholePacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D3059E29B8DB700006E748 /* WormholePacketTests.swift */; }; F4D305A029B8DB700006E748 /* VirtualWormhole.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */; }; F4D305AA29B8E7120006E748 /* TestStream.bin in Resources */ = {isa = PBXBuildFile; fileRef = F4D305A929B8E7120006E748 /* TestStream.bin */; }; F4D305AC29B8FEE90006E748 /* WHDarwinNotificationsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D305AB29B8FEE90006E748 /* WHDarwinNotificationsService.swift */; }; F4D305B029B900860006E748 /* SystemNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D305AF29B900860006E748 /* SystemNotification.swift */; }; F4D305B229B907A10006E748 /* WHPing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D305B129B907A10006E748 /* WHPing.swift */; }; F4D725FE286677B8001818F7 /* VBVirtualMachine+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D725FD286677B8001818F7 /* VBVirtualMachine+Metadata.swift */; }; F4DE1C0B2D6F54E700603527 /* VBSavedStateMetadata+Clone.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DE1C0A2D6F54E000603527 /* VBSavedStateMetadata+Clone.swift */; }; F4DE1C0F2D6F603300603527 /* VBSavedStatePackage+VM.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DE1C0E2D6F603300603527 /* VBSavedStatePackage+VM.swift */; }; F4DE1C112D6F642E00603527 /* VBStorageDeviceContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DE1C102D6F642E00603527 /* VBStorageDeviceContainer.swift */; }; F4E4F6C52DEF96C200B3B8BA /* ChromeBorderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E4F6C42DEF96C200B3B8BA /* ChromeBorderModifier.swift */; }; F4E4F7202DF080FC00B3B8BA /* RestoreImageSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E4F71F2DF080FC00B3B8BA /* RestoreImageSelectionController.swift */; }; F4E4F7262DF0A1CB00B3B8BA /* BuddyKit in Frameworks */ = {isa = PBXBuildFile; productRef = F4E4F7252DF0A1CB00B3B8BA /* BuddyKit */; }; F4E7680A29B64C590075A897 /* GuestTypePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E7680929B64C590075A897 /* GuestTypePicker.swift */; }; F4E7680D29B651220075A897 /* VirtualUI.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4E7680C29B651220075A897 /* VirtualUI.xcassets */; }; F4E7680F29B655DD0075A897 /* InstallMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E7680E29B655DD0075A897 /* InstallMethod.swift */; }; F4E7DF922BB3338900C459FC /* NSImage+HEIC.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E7DF912BB3338900C459FC /* NSImage+HEIC.swift */; }; F4E7DF952BB336F600C459FC /* VBSavedStatePackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E7DF942BB336F600C459FC /* VBSavedStatePackage.swift */; }; F4E7DF972BB33E1700C459FC /* VMLibraryController+SavedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E7DF962BB33E1700C459FC /* VMLibraryController+SavedState.swift */; }; F4E7DFCF2BB3587D00C459FC /* VirtualMachineSessionUIManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E7DFCE2BB3587D00C459FC /* VirtualMachineSessionUIManager.swift */; }; F4ECC6D52C63BFD5001DAC1D /* NumberDisplayMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ECC6D42C63BFD5001DAC1D /* NumberDisplayMode.swift */; }; F4F9B416284CE0F900F21737 /* VBSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F9B415284CE0F900F21737 /* VBSettings.swift */; }; F4F9B418284CE12000F21737 /* VBSettingsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F9B417284CE12000F21737 /* VBSettingsContainer.swift */; }; F4F9B41A284CE37C00F21737 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F9B419284CE37C00F21737 /* Logging.swift */; }; F4FC276729BBAE350012CB65 /* WormholeServiceClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FC276629BBAE350012CB65 /* WormholeServiceClient.swift */; }; F4FC276A29BBAE590012CB65 /* WHDefaultsImportClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FC276929BBAE590012CB65 /* WHDefaultsImportClient.swift */; }; F4FC276C29BBB3030012CB65 /* GuestDefaultsImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FC276B29BBB3030012CB65 /* GuestDefaultsImportView.swift */; }; F4FC98392BB386A000E511C9 /* ContinuousProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FC98382BB386A000E511C9 /* ContinuousProgressIndicator.swift */; }; F4FC983B2BB386B500E511C9 /* MaskProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FC983A2BB386B500E511C9 /* MaskProgressView.swift */; }; F4FC983D2BB386DD00E511C9 /* VMProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FC983C2BB386DD00E511C9 /* VMProgressOverlay.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ F413699C299179F8002CE8D3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F4BE9C6427FF053A00B648F8; remoteInfo = VirtualCore; }; F41369BC29918617002CE8D3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F41369AA29918576002CE8D3; remoteInfo = VirtualBuddyGuestHelper; }; F42862302AE87D7E0052F029 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F43B01142AD858FE00164CD1; remoteInfo = DeepLinkSecurity; }; F4510A792AE2B3AB00E24DD9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F43B01142AD858FE00164CD1; remoteInfo = DeepLinkSecurity; }; F453C4262DF0B602007EAD5F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F498ACFD2884BF13006F1C00; remoteInfo = VirtualUI; }; F453C4582DF0BCEB007EAD5F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F4BE9C4D27FF052100B648F8; remoteInfo = VirtualBuddy; }; F498AD022884BF13006F1C00 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F498ACFD2884BF13006F1C00; remoteInfo = VirtualUI; }; F498AD0A2884BF60006F1C00 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F4BE9C6427FF053A00B648F8; remoteInfo = VirtualCore; }; F4C189EF2848F5F100335EC7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F4C189DF2848F59F00335EC7; remoteInfo = VirtualWormhole; }; F4C18A5428491B9D00335EC7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F4C189DF2848F59F00335EC7; remoteInfo = VirtualWormhole; }; F4C18A5728491C0D00335EC7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F4C18A4128491B8500335EC7; remoteInfo = VirtualBuddyGuest; }; F4D305A129B8DB700006E748 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4BE9C4627FF052100B648F8 /* Project object */; proxyType = 1; remoteGlobalIDString = F4C189DF2848F59F00335EC7; remoteInfo = VirtualWormhole; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ F41369BE2991861C002CE8D3 /* Embed Login Item */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = Contents/Library/LoginItems; dstSubfolderSpec = 1; files = ( F41369BF2991863A002CE8D3 /* VirtualBuddyGuestHelper.app in Embed Login Item */, ); name = "Embed Login Item"; runOnlyForDeploymentPostprocessing = 0; }; F443620B29B79A5800745B43 /* Embed Guest App */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 12; files = ( F443620C29B79A6800745B43 /* VirtualBuddyGuest.app in Embed Guest App */, ); name = "Embed Guest App"; runOnlyForDeploymentPostprocessing = 0; }; F45502222DF46386005582A4 /* Copy Preview Library Downloads */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = PreviewLibrary/_Downloads; dstSubfolderSpec = 7; files = ( F45502232DF463A1005582A4 /* UniversalMac_13.3.1_22E261_Restore.ipsw in Copy Preview Library Downloads */, F45502242DF463A1005582A4 /* UniversalMac_14.0_23A344_Restore.ipsw in Copy Preview Library Downloads */, F45502252DF463A1005582A4 /* UniversalMac_14.5_23F79_Restore.ipsw in Copy Preview Library Downloads */, F45502262DF463A1005582A4 /* UniversalMac_15.3_24D60_Restore.ipsw in Copy Preview Library Downloads */, F45502272DF463A1005582A4 /* UniversalMac_15.5_24F74_Restore.ipsw in Copy Preview Library Downloads */, ); name = "Copy Preview Library Downloads"; runOnlyForDeploymentPostprocessing = 0; }; F47BCD9C2C5BE89E00165191 /* Copy Software Catalog Feeds */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = SoftwareCatalog; dstSubfolderSpec = 7; files = ( F47BCDA12C5BE8FF00165191 /* ipsws_v2.json in Copy Software Catalog Feeds */, F47BCDA22C5BE8FF00165191 /* linux_v2.json in Copy Software Catalog Feeds */, ); name = "Copy Software Catalog Feeds"; runOnlyForDeploymentPostprocessing = 0; }; F4A7FB462BB5ED6400E4C12A /* Copy Preview Saved States */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = PreviewLibrary/_SavedStates; dstSubfolderSpec = 7; files = ( F4A7FB472BB5ED7E00E4C12A /* Save-2024-03-27_16;06;24.vbst in Copy Preview Saved States */, F4A7FB482BB5ED7E00E4C12A /* Save-2024-03-27_16;07;04.vbst in Copy Preview Saved States */, F4A7FB492BB5ED7E00E4C12A /* Save-2024-03-27_16;08;06.vbst in Copy Preview Saved States */, F4A7FB4A2BB5ED7E00E4C12A /* Save-2024-03-27_16;08;28.vbst in Copy Preview Saved States */, F4A7FB4B2BB5ED7E00E4C12A /* Save-2024-03-27_16;08;51.vbst in Copy Preview Saved States */, ); name = "Copy Preview Saved States"; runOnlyForDeploymentPostprocessing = 0; }; F4A7FB4C2BB5F0B700E4C12A /* Copy Preview Library */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = PreviewLibrary; dstSubfolderSpec = 7; files = ( F49B82A82E0304D400395F87 /* PreviewMac.vbvm in Copy Preview Library */, F49B82A92E0304D400395F87 /* PreviewMacBlurHash.vbvm in Copy Preview Library */, F49B82AA2E0304D400395F87 /* PreviewMacNoArtwork.vbvm in Copy Preview Library */, F49B82AB2E0304D400395F87 /* PreviewLinux.vbvm in Copy Preview Library */, F49B82AC2E0304D400395F87 /* PreviewLinuxBlurHash.vbvm in Copy Preview Library */, F49B82AD2E0304D400395F87 /* PreviewLinuxNoArtwork.vbvm in Copy Preview Library */, ); name = "Copy Preview Library"; runOnlyForDeploymentPostprocessing = 0; }; F4BE9C7027FF053A00B648F8 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( F4BE9C6C27FF053A00B648F8 /* VirtualCore.framework in Embed Frameworks */, F498AD052884BF13006F1C00 /* VirtualUI.framework in Embed Frameworks */, F43B011C2AD858FE00164CD1 /* DeepLinkSecurity.framework in Embed Frameworks */, F4C189E72848F59F00335EC7 /* VirtualWormhole.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; F4C18A5628491B9D00335EC7 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( F453C4252DF0B602007EAD5F /* VirtualUI.framework in Embed Frameworks */, F4C18A5328491B9D00335EC7 /* VirtualWormhole.framework in Embed Frameworks */, F428622F2AE87D7E0052F029 /* DeepLinkSecurity.framework in Embed Frameworks */, F413699B299179F8002CE8D3 /* VirtualCore.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 0196B45229292B2A00614EF1 /* LinuxVirtualMachineConfigurationHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinuxVirtualMachineConfigurationHelper.swift; sourceTree = ""; }; 4BA6BE7C293D22E500F396AE /* VirtualMachineConfigurationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMachineConfigurationHelper.swift; sourceTree = ""; }; F40A1E9C2C1873C60033E47D /* VBBuildType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBBuildType.swift; sourceTree = ""; }; F413695129916F6E002CE8D3 /* StatusItemButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItemButton.swift; sourceTree = ""; }; F413695229916F6E002CE8D3 /* StatusBarPanelChrome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarPanelChrome.swift; sourceTree = ""; }; F413695329916F6E002CE8D3 /* StatusItemProviderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItemProviderProtocol.swift; sourceTree = ""; }; F413695529916F6E002CE8D3 /* StatusItemPanelContentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItemPanelContentController.swift; sourceTree = ""; }; F413695729916F6E002CE8D3 /* VUIAppKitViewControllerHost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VUIAppKitViewControllerHost.swift; sourceTree = ""; }; F413695829916F6E002CE8D3 /* StatusItemMenuBarExtraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItemMenuBarExtraView.swift; sourceTree = ""; }; F413695929916F6E002CE8D3 /* StatusBarContentPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarContentPanel.swift; sourceTree = ""; }; F413695A29916F6E002CE8D3 /* StatusBarHighlightView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarHighlightView.swift; sourceTree = ""; }; F413695C29916F6E002CE8D3 /* NSStatusItem+.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSStatusItem+.h"; sourceTree = ""; }; F413695D29916F6E002CE8D3 /* NSApplication+MenuBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSApplication+MenuBar.h"; sourceTree = ""; }; F413695E29916F6E002CE8D3 /* NSStatusBarPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSStatusBarPrivate.h; sourceTree = ""; }; F413695F29916F6E002CE8D3 /* NSApplication+MenuBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSApplication+MenuBar.m"; sourceTree = ""; }; F413696029916F6E002CE8D3 /* NSStatusItem+.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSStatusItem+.m"; sourceTree = ""; }; F413696129916F6E002CE8D3 /* StatusItemManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItemManager.swift; sourceTree = ""; }; F413697829917135002CE8D3 /* CGFloat+OnePixel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGFloat+OnePixel.swift"; sourceTree = ""; }; F4136998299179B1002CE8D3 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; F41369A229917FA0002CE8D3 /* ScreenChangeModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenChangeModifier.swift; sourceTree = ""; }; F41369A5299183C8002CE8D3 /* GuestLaunchAtLoginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestLaunchAtLoginManager.swift; sourceTree = ""; }; F41369AB29918576002CE8D3 /* VirtualBuddyGuestHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VirtualBuddyGuestHelper.app; sourceTree = BUILT_PRODUCTS_DIR; }; F41369AD29918576002CE8D3 /* GuestHelperAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestHelperAppDelegate.swift; sourceTree = ""; }; F41369B129918576002CE8D3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F41369B429918576002CE8D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; F41369B629918576002CE8D3 /* VirtualBuddyGuestHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VirtualBuddyGuestHelper.entitlements; sourceTree = ""; }; F41369C5299187E1002CE8D3 /* VirtualBuddyGuest-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VirtualBuddyGuest-Bridging-Header.h"; sourceTree = ""; }; F41369C92991A492002CE8D3 /* HostConnectionStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostConnectionStateProvider.swift; sourceTree = ""; }; F41369CB2991A68F002CE8D3 /* GuestSharedFoldersManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestSharedFoldersManager.swift; sourceTree = ""; }; F417255C288604A8004FF8A7 /* SoundConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundConfigurationView.swift; sourceTree = ""; }; F417255E28861604004FF8A7 /* DecodableDefault.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecodableDefault.swift; sourceTree = ""; }; F417256028861A05004FF8A7 /* SharingConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingConfigurationView.swift; sourceTree = ""; }; F41725622886DD37004FF8A7 /* SharedFolderListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFolderListItem.swift; sourceTree = ""; }; F41725652886DF58004FF8A7 /* OpenSavePanelUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSavePanelUtils.swift; sourceTree = ""; }; F41725672886E5AD004FF8A7 /* MaterialView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MaterialView.swift; sourceTree = ""; }; F417256B2887500F004FF8A7 /* StorageConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageConfigurationView.swift; sourceTree = ""; }; F417256E2887544A004FF8A7 /* StorageDeviceDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageDeviceDetailView.swift; sourceTree = ""; }; F417257028877121004FF8A7 /* DiskImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskImageGenerator.swift; sourceTree = ""; }; F417257328877478004FF8A7 /* VirtualCore.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = VirtualCore.xcassets; sourceTree = ""; }; F41725752887758A004FF8A7 /* RandomNameGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomNameGenerator.swift; sourceTree = ""; }; F417CB872E0EDECD0065B5D6 /* BackportedContentUnavailableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackportedContentUnavailableView.swift; sourceTree = ""; }; F417CB892E0EDF1A0065B5D6 /* EqualWidthHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualWidthHStack.swift; sourceTree = ""; }; F417CBB82E0F3D2E0065B5D6 /* FirstLaunchExperienceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstLaunchExperienceView.swift; sourceTree = ""; }; F422586C2885CC9F009420AE /* SharedFocusEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFocusEnvironment.swift; sourceTree = ""; }; F422586F2885D537009420AE /* EphemeralTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralTextField.swift; sourceTree = ""; }; F42258712885E100009420AE /* VMConfigurationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigurationSheet.swift; sourceTree = ""; }; F42258732885E10B009420AE /* VMConfigurationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigurationViewModel.swift; sourceTree = ""; }; F42258772885E14A009420AE /* DisplayConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayConfigurationView.swift; sourceTree = ""; }; F42258792885E17D009420AE /* ConfigurationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationSection.swift; sourceTree = ""; }; F422587B2885E1CE009420AE /* NetworkConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConfigurationView.swift; sourceTree = ""; }; F422587D2885E2ED009420AE /* HardwareConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareConfigurationView.swift; sourceTree = ""; }; F422587F2885E71D009420AE /* PropertyControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyControl.swift; sourceTree = ""; }; F428622C2AE8726D0052F029 /* VirtualMachineControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMachineControls.swift; sourceTree = ""; }; F42862362AE947C90052F029 /* WHPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WHPayload.swift; sourceTree = ""; }; F42C01492888C2F800EB15CD /* InstallationConsole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationConsole.swift; sourceTree = ""; }; F42C014B2888C34B00EB15CD /* LogConsole.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogConsole.swift; sourceTree = ""; }; F42C014D2888CBCB00EB15CD /* InstallProgressStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallProgressStepView.swift; sourceTree = ""; }; F42C01512888FC0C00EB15CD /* LibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; F42C01542888FC0C00EB15CD /* VMSessionConfigurationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VMSessionConfigurationView.swift; sourceTree = ""; }; F42C01552888FC0C00EB15CD /* VirtualMachineSessionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VirtualMachineSessionView.swift; sourceTree = ""; }; F42C01572888FC0C00EB15CD /* SwiftUIVMView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUIVMView.swift; sourceTree = ""; }; F42C01592888FC0C00EB15CD /* SettingsScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; F42C01602888FC3500EB15CD /* LibraryItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryItemView.swift; sourceTree = ""; }; F42CF4A72DF5FEC3001DE049 /* BlurHashToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashToken.swift; sourceTree = ""; }; F43B01152AD858FE00164CD1 /* DeepLinkSecurity.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DeepLinkSecurity.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F43B01172AD858FE00164CD1 /* DeepLinkSecurity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeepLinkSecurity.h; sourceTree = ""; }; F43B01242AD8590F00164CD1 /* DeepLinkAuthUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkAuthUI.swift; sourceTree = ""; }; F43B01252AD8590F00164CD1 /* DeepLinkSentinel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkSentinel.swift; sourceTree = ""; }; F43B01272AD8590F00164CD1 /* OpenDeepLinkRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenDeepLinkRequest.swift; sourceTree = ""; }; F43B01282AD8590F00164CD1 /* DeepLinkClientDescriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkClientDescriptor.swift; sourceTree = ""; }; F43B012A2AD8590F00164CD1 /* DeepLinkClient+Crypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DeepLinkClient+Crypto.swift"; sourceTree = ""; }; F43B012B2AD8590F00164CD1 /* DeepLinkClientDescriptor+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DeepLinkClientDescriptor+.swift"; sourceTree = ""; }; F43B012C2AD8590F00164CD1 /* DeepLinkClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkClient.swift; sourceTree = ""; }; F43B012E2AD8590F00164CD1 /* KeychainDeepLinkAuthStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainDeepLinkAuthStore.swift; sourceTree = ""; }; F43B012F2AD8590F00164CD1 /* MemoryDeepLinkAuthStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryDeepLinkAuthStore.swift; sourceTree = ""; }; F43B01302AD8590F00164CD1 /* DeepLinkManagementStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkManagementStore.swift; sourceTree = ""; }; F43B01312AD8590F00164CD1 /* UserDefaultsDeepLinkManagementStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsDeepLinkManagementStore.swift; sourceTree = ""; }; F43B01322AD8590F00164CD1 /* DeepLinkAuthStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkAuthStore.swift; sourceTree = ""; }; F43B01342AD8590F00164CD1 /* DeepLinkSecurityDefines.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkSecurityDefines.swift; sourceTree = ""; }; F43B01432AD85A6500164CD1 /* VirtualBuddyDeepLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualBuddyDeepLinks.swift; sourceTree = ""; }; F43B01492AD85ABB00164CD1 /* DeepLinkAuthDialog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkAuthDialog.swift; sourceTree = ""; }; F43B014D2AD86BFA00164CD1 /* DeepLinkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkHandler.swift; sourceTree = ""; }; F443620929B7947A00745B43 /* GuestAdditionsDiskImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestAdditionsDiskImage.swift; sourceTree = ""; }; F443620E29B7A0C600745B43 /* CreateGuestImage.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = CreateGuestImage.sh; sourceTree = ""; }; F444D0C92DF321CD0086537A /* CatalogGroupPlaceholder.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = CatalogGroupPlaceholder.heic; sourceTree = ""; }; F444D0CB2DF322B50086537A /* SoftwareCatalog+Placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SoftwareCatalog+Placeholder.swift"; sourceTree = ""; }; F444D0CD2DF32E100086537A /* BlurHashFullBleedBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashFullBleedBackground.swift; sourceTree = ""; }; F444D0F32DF34BE40086537A /* CALayer+Asset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+Asset.swift"; sourceTree = ""; }; F444D0F52DF37D170086537A /* VirtualBuddyMonoIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualBuddyMonoIcon.swift; sourceTree = ""; }; F444D0F72DF37D410086537A /* VirtualDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualDisplayView.swift; sourceTree = ""; }; F444D0F92DF37DFB0086537A /* InstallProgressDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallProgressDisplayView.swift; sourceTree = ""; }; F444D0FB2DF37EF80086537A /* VirtualBuddyMonoProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualBuddyMonoProgressView.swift; sourceTree = ""; }; F444D1332BB478AD00AB786F /* VBMemoryLeakDebugAssertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBMemoryLeakDebugAssertions.swift; sourceTree = ""; }; F4450CC92ACB0DB500092618 /* KeyboardDeviceConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardDeviceConfigurationView.swift; sourceTree = ""; }; F44C00FA2889CE1600640BF5 /* VBVirtualMachine+Virtualization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VBVirtualMachine+Virtualization.swift"; sourceTree = ""; }; F4510A772AE2A16F00E24DD9 /* WeakReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakReference.swift; sourceTree = ""; }; F453C4162DF0B43D007EAD5F /* BlurHashEncode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashEncode.swift; sourceTree = ""; }; F453C4182DF0B43D007EAD5F /* LegacyCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCatalog.swift; sourceTree = ""; }; F453C4192DF0B43D007EAD5F /* MobileDeviceFramework.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileDeviceFramework.swift; sourceTree = ""; }; F453C41A2DF0B43D007EAD5F /* ResolvedCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolvedCatalog.swift; sourceTree = ""; }; F453C41B2DF0B43D007EAD5F /* SoftwareCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareCatalog.swift; sourceTree = ""; }; F453C4222DF0B5B1007EAD5F /* VirtualBuddyEntryPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualBuddyEntryPoint.swift; sourceTree = ""; }; F453C42C2DF0B7A5007EAD5F /* BuildManifest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildManifest.swift; sourceTree = ""; }; F453C42D2DF0B7A5007EAD5F /* BuildManifest+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BuildManifest+Fetch.swift"; sourceTree = ""; }; F453C42E2DF0B7A5007EAD5F /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; F453C42F2DF0B7A5007EAD5F /* TreeStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeStringConvertible.swift; sourceTree = ""; }; F453C4302DF0B7A5007EAD5F /* URL+ContentLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+ContentLength.swift"; sourceTree = ""; }; F453C4322DF0B7A5007EAD5F /* CatalogCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogCommand.swift; sourceTree = ""; }; F453C4332DF0B7A5007EAD5F /* GroupCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCommand.swift; sourceTree = ""; }; F453C4342DF0B7A5007EAD5F /* ImageCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCommand.swift; sourceTree = ""; }; F453C4352DF0B7A5007EAD5F /* IPSWCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPSWCommand.swift; sourceTree = ""; }; F453C4362DF0B7A5007EAD5F /* MigrateCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateCommand.swift; sourceTree = ""; }; F453C4372DF0B7A5007EAD5F /* MobileDeviceCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileDeviceCommand.swift; sourceTree = ""; }; F453C4382DF0B7A5007EAD5F /* ResolveCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveCommand.swift; sourceTree = ""; }; F453C4392DF0B7A5007EAD5F /* VCTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCTool.swift; sourceTree = ""; }; F453C44B2DF0B835007EAD5F /* libcurl.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcurl.tbd; path = usr/lib/libcurl.tbd; sourceTree = SDKROOT; }; F453C44D2DF0B869007EAD5F /* VirtualBuddyCLI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualBuddyCLI.swift; sourceTree = ""; }; F453C45C2DF0D286007EAD5F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; F453C4672DF10181007EAD5F /* BlurHashCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashCommand.swift; sourceTree = ""; }; F453C4882DF1CDA0007EAD5F /* DownloadBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadBackend.swift; sourceTree = ""; }; F453C4912DF1D213007EAD5F /* FakeRestoreImage.ipsw */ = {isa = PBXFileReference; lastKnownFileType = file; path = FakeRestoreImage.ipsw; sourceTree = ""; }; F453C49A2DF1D768007EAD5F /* SimulatedDownloadBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatedDownloadBackend.swift; sourceTree = ""; }; F453C49F2DF1D7C0007EAD5F /* RestoreBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreBackend.swift; sourceTree = ""; }; F453C4A12DF1D7F6007EAD5F /* SimulatedRestoreBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatedRestoreBackend.swift; sourceTree = ""; }; F453C4A32DF1D85C007EAD5F /* VirtualizationRestoreBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualizationRestoreBackend.swift; sourceTree = ""; }; F453C4B32DF20301007EAD5F /* RestoreImageURLInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreImageURLInputView.swift; sourceTree = ""; }; F453C4B82DF21985007EAD5F /* VMInstallData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMInstallData.swift; sourceTree = ""; }; F453C4BA2DF231B7007EAD5F /* PreventTerminationAssertion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreventTerminationAssertion.swift; sourceTree = ""; }; F45502132DF394DC005582A4 /* VirtualBuddyInstallerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualBuddyInstallerInputView.swift; sourceTree = ""; }; F45502152DF45E4D005582A4 /* VBSettings+CatalogDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VBSettings+CatalogDownload.swift"; sourceTree = ""; }; F45502182DF46368005582A4 /* UniversalMac_13.3.1_22E261_Restore.ipsw */ = {isa = PBXFileReference; lastKnownFileType = text; path = UniversalMac_13.3.1_22E261_Restore.ipsw; sourceTree = ""; }; F45502192DF46368005582A4 /* UniversalMac_14.0_23A344_Restore.ipsw */ = {isa = PBXFileReference; lastKnownFileType = text; path = UniversalMac_14.0_23A344_Restore.ipsw; sourceTree = ""; }; F455021A2DF46368005582A4 /* UniversalMac_14.5_23F79_Restore.ipsw */ = {isa = PBXFileReference; lastKnownFileType = text; path = UniversalMac_14.5_23F79_Restore.ipsw; sourceTree = ""; }; F455021B2DF46368005582A4 /* UniversalMac_15.3_24D60_Restore.ipsw */ = {isa = PBXFileReference; lastKnownFileType = text; path = UniversalMac_15.3_24D60_Restore.ipsw; sourceTree = ""; }; F455021C2DF46368005582A4 /* UniversalMac_15.5_24F74_Restore.ipsw */ = {isa = PBXFileReference; lastKnownFileType = text; path = UniversalMac_15.5_24F74_Restore.ipsw; sourceTree = ""; }; F4561A6728981B4100055289 /* VirtualMachineNameInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMachineNameInputView.swift; sourceTree = ""; }; F462C9412E0C96D300C172E2 /* FB18383725Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FB18383725Window.swift; sourceTree = ""; }; F465C3AD284F93A5006E9ED4 /* VBAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBAPIClient.swift; sourceTree = ""; }; F465C3AF284F9660006E9ED4 /* VBRestoreImagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBRestoreImagesResponse.swift; sourceTree = ""; }; F465C3B1284F9666006E9ED4 /* VBRestoreImageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBRestoreImageInfo.swift; sourceTree = ""; }; F465C3B7284FA252006E9ED4 /* URLSessionDownloadBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDownloadBackend.swift; sourceTree = ""; }; F46FFBA72804F07400D61023 /* VBNVRAMVariable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBNVRAMVariable.swift; sourceTree = ""; }; F46FFBA92804F0A000D61023 /* VZVirtualMachineConfiguration+NVRAM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VZVirtualMachineConfiguration+NVRAM.swift"; sourceTree = ""; }; F46FFBAB28059FF600D61023 /* VMInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMInstance.swift; sourceTree = ""; }; F47BCD9F2C5BE8EF00165191 /* ipsws_v2.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = ipsws_v2.json; path = data/ipsws_v2.json; sourceTree = SOURCE_ROOT; }; F47BCDA02C5BE8EF00165191 /* linux_v2.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = linux_v2.json; path = data/linux_v2.json; sourceTree = SOURCE_ROOT; }; F47BCDCA2C5C01CE00165191 /* BlurHashDecoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecoding.swift; sourceTree = ""; }; F47BCDCC2C5C01EF00165191 /* RemoteImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImage.swift; sourceTree = ""; }; F47BCDCE2C5C023900165191 /* CatalogGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogGroupView.swift; sourceTree = ""; }; F47BCDD02C5C06C900165191 /* CatalogGroupPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogGroupPicker.swift; sourceTree = ""; }; F47BCDD22C5C0AB300165191 /* KeyboardNavigationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardNavigationModifier.swift; sourceTree = ""; }; F47BCDD42C5C0B8C00165191 /* Array+Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Navigation.swift"; sourceTree = ""; }; F47BCDD62C5D2B4300165191 /* CatalogExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogExtensions.swift; sourceTree = ""; }; F47BCDD82C5D2EDB00165191 /* RestoreImageBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreImageBrowser.swift; sourceTree = ""; }; F482FC7228CB7A6C00F2BA4F /* InfoPlist.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = InfoPlist.xcconfig; sourceTree = ""; }; F485B91A2BB22D2D004B3C2B /* VBSavedStateMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBSavedStateMetadata.swift; sourceTree = ""; }; F485B91C2BB2F0D9004B3C2B /* ProcessInfo+ECID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+ECID.swift"; sourceTree = ""; }; F485B91E2BB2F4AC004B3C2B /* Bundle+Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Version.swift"; sourceTree = ""; }; F485B9202BB306AF004B3C2B /* VBDebugUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VBDebugUtil.h; sourceTree = ""; }; F485B9212BB306AF004B3C2B /* VBDebugUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VBDebugUtil.m; sourceTree = ""; }; F48E0D02288858DF0080DDFA /* ManagedDiskImageEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedDiskImageEditor.swift; sourceTree = ""; }; F48E0D0628885E140080DDFA /* PreviewSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewSupport.swift; sourceTree = ""; }; F48E0D0B2888760D0080DDFA /* VBMacDevice+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VBMacDevice+Storage.swift"; sourceTree = ""; }; F48E0D14288882BD0080DDFA /* InstallationWizardTitle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstallationWizardTitle.swift; sourceTree = ""; }; F48E0D15288882BD0080DDFA /* InstallMethodPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstallMethodPicker.swift; sourceTree = ""; }; F48E0D16288882BD0080DDFA /* RestoreImageSelectionStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreImageSelectionStep.swift; sourceTree = ""; }; F48E0D17288882BD0080DDFA /* AuthenticatingWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticatingWebView.swift; sourceTree = ""; }; F48E0D18288882BD0080DDFA /* VMInstallationWizard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VMInstallationWizard.swift; sourceTree = ""; }; F48E0D19288882BD0080DDFA /* VMInstallationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VMInstallationViewModel.swift; sourceTree = ""; }; F48E0D20288882E50080DDFA /* DecentFormView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecentFormView.swift; sourceTree = ""; }; F48E0D21288882E50080DDFA /* NSAlert+Confirmation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAlert+Confirmation.swift"; sourceTree = ""; }; F48E0D22288882E50080DDFA /* OnAppearOnce.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnAppearOnce.swift; sourceTree = ""; }; F48E0D27288883150080DDFA /* OpenCocoaWindowAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenCocoaWindowAction.swift; sourceTree = ""; }; F48E0D28288883150080DDFA /* HostingWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostingWindowController.swift; sourceTree = ""; }; F48E0D29288883150080DDFA /* WindowEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowEnvironment.swift; sourceTree = ""; }; F48E0D2E2888835A0080DDFA /* VirtualUIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualUIConstants.swift; sourceTree = ""; }; F48E0D31288884A10080DDFA /* RestoreImageDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreImageDownloadView.swift; sourceTree = ""; }; F48E0D33288889E60080DDFA /* InstallConfigurationStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallConfigurationStepView.swift; sourceTree = ""; }; F4959F3B2992A284001DF4CB /* GuestAppInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestAppInstaller.swift; sourceTree = ""; }; F498ACFE2884BF13006F1C00 /* VirtualUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VirtualUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F498AD002884BF13006F1C00 /* VirtualUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VirtualUI.h; sourceTree = ""; }; F498AD0D2884BF9D006F1C00 /* VMConfigurationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VMConfigurationView.swift; sourceTree = ""; }; F498AD112884C36A006F1C00 /* NumericValueField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumericValueField.swift; sourceTree = ""; }; F498AD132884C36A006F1C00 /* ControlGroupChrome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlGroupChrome.swift; sourceTree = ""; }; F498AD172884C593006F1C00 /* NumericPropertyControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumericPropertyControl.swift; sourceTree = ""; }; F498AD192884C5FF006F1C00 /* SliderConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderConversion.swift; sourceTree = ""; }; F49A68E02884917E00A17582 /* ConfigurationModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationModels.swift; sourceTree = ""; }; F49AA2C229BA22A5009625F7 /* VBRestorableWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBRestorableWindow.swift; sourceTree = ""; }; F49AA2C429BA31CC009625F7 /* VirtualMachineSessionUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMachineSessionUI.swift; sourceTree = ""; }; F49AA2C629BA3F2B009625F7 /* VBRestorableWindow+Resizing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VBRestorableWindow+Resizing.swift"; sourceTree = ""; }; F49B82972E02F5A300395F87 /* VMArtworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMArtworkView.swift; sourceTree = ""; }; F49B829D2E02FCB100395F87 /* PreviewMacBlurHash.vbvm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PreviewMacBlurHash.vbvm; sourceTree = ""; }; F49B829E2E02FCB100395F87 /* PreviewLinux.vbvm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PreviewLinux.vbvm; sourceTree = ""; }; F49B829F2E02FCB100395F87 /* PreviewMac.vbvm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PreviewMac.vbvm; sourceTree = ""; }; F49B82A32E02FCF400395F87 /* PreviewMacNoArtwork.vbvm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PreviewMacNoArtwork.vbvm; sourceTree = ""; }; F49B82A52E03049C00395F87 /* PreviewLinuxBlurHash.vbvm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PreviewLinuxBlurHash.vbvm; sourceTree = ""; }; F49B82A62E03049C00395F87 /* PreviewLinuxNoArtwork.vbvm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PreviewLinuxNoArtwork.vbvm; sourceTree = ""; }; F49B82FB2E034EAD00395F87 /* WHDesktopPictureService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WHDesktopPictureService.swift; sourceTree = ""; }; F49B82FE2E034F6800395F87 /* NSImage+DesktopPicture.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSImage+DesktopPicture.h"; sourceTree = ""; }; F49B82FF2E034F6800395F87 /* NSImage+DesktopPicture.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSImage+DesktopPicture.m"; sourceTree = ""; }; F49B832A2E04593100395F87 /* NSImage+DRMProtected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+DRMProtected.swift"; sourceTree = ""; }; F49B832C2E046B8D00395F87 /* GuestAppConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestAppConfigurationView.swift; sourceTree = ""; }; F49B83702E04837400395F87 /* CGImage+FullyTransparent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImage+FullyTransparent.swift"; sourceTree = ""; }; F49FD87C2DFB68F20019D638 /* VMImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMImporter.swift; sourceTree = ""; }; F49FD87E2DFB6B630019D638 /* VMImporterRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMImporterRegistry.swift; sourceTree = ""; }; F49FD8802DFB6CD80019D638 /* UTMImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMImporter.swift; sourceTree = ""; }; F49FD8832DFB72790019D638 /* VMImporter+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VMImporter+Helpers.swift"; sourceTree = ""; }; F49FD8852DFB728A0019D638 /* UTMAppleConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMAppleConfiguration.swift; sourceTree = ""; }; F4A21BF128032FD8001072B8 /* VMLibraryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMLibraryController.swift; sourceTree = ""; }; F4A21BF328033102001072B8 /* VBError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBError.swift; sourceTree = ""; }; F4A7FB3A2BB5E79100E4C12A /* DirectoryObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryObserver.swift; sourceTree = ""; }; F4A7FB3C2BB5E8A200E4C12A /* VMSavedStatesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMSavedStatesController.swift; sourceTree = ""; }; F4A7FB3E2BB5EBEF00E4C12A /* SavedStatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedStatePicker.swift; sourceTree = ""; }; F4A7FB402BB5ED4A00E4C12A /* Save-2024-03-27_16;06;24.vbst */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = "Save-2024-03-27_16;06;24.vbst"; sourceTree = ""; }; F4A7FB412BB5ED4A00E4C12A /* Save-2024-03-27_16;07;04.vbst */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = "Save-2024-03-27_16;07;04.vbst"; sourceTree = ""; }; F4A7FB422BB5ED4A00E4C12A /* Save-2024-03-27_16;08;06.vbst */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = "Save-2024-03-27_16;08;06.vbst"; sourceTree = ""; }; F4A7FB432BB5ED4A00E4C12A /* Save-2024-03-27_16;08;28.vbst */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = "Save-2024-03-27_16;08;28.vbst"; sourceTree = ""; }; F4A7FB442BB5ED4A00E4C12A /* Save-2024-03-27_16;08;51.vbst */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = "Save-2024-03-27_16;08;51.vbst"; sourceTree = ""; }; F4A7FB6D2BB7206C00E4C12A /* SelfSizingGroupedForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingGroupedForm.swift; sourceTree = ""; }; F4A7FB742BB7252A00E4C12A /* PreviewSupport-VirtualUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreviewSupport-VirtualUI.swift"; sourceTree = ""; }; F4B068B428882EB7003743BF /* VirtualBuddy_Managed.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VirtualBuddy_Managed.entitlements; sourceTree = ""; }; F4B068B528882F5D003743BF /* AppTarget.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppTarget.xcconfig; sourceTree = ""; }; F4B5C5D22886FA8D005AA632 /* SharedFoldersManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFoldersManagementView.swift; sourceTree = ""; }; F4B5C5D42886FFB5005AA632 /* PointingDeviceConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointingDeviceConfigurationView.swift; sourceTree = ""; }; F4B5C5D628870619005AA632 /* ConfigurationModels+Validation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationModels+Validation.swift"; sourceTree = ""; }; F4B5C5D828870BBF005AA632 /* ConfigurationModels+Summary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationModels+Summary.swift"; sourceTree = ""; }; F4B5C5DA28873628005AA632 /* GroupedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedList.swift; sourceTree = ""; }; F4BE580D29BA6C8D00C5525C /* DefaultsDomains.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DefaultsDomains.plist; sourceTree = ""; }; F4BE581029BA6DFC00C5525C /* DefaultsImportController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsImportController.swift; sourceTree = ""; }; F4BE581229BA6E0D00C5525C /* DefaultsDomainDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsDomainDescriptor.swift; sourceTree = ""; }; F4BE583F29BA7B7A00C5525C /* DefaultsDomain+ExportImport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DefaultsDomain+ExportImport.swift"; sourceTree = ""; }; F4BE584129BA7BDB00C5525C /* WHDefaultsImportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WHDefaultsImportService.swift; sourceTree = ""; }; F4BE9C4E27FF052100B648F8 /* VirtualBuddy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VirtualBuddy.app; sourceTree = BUILT_PRODUCTS_DIR; }; F4BE9C5127FF052100B648F8 /* VirtualBuddyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualBuddyApp.swift; sourceTree = ""; }; F4BE9C5527FF052100B648F8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F4BE9C5827FF052100B648F8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; F4BE9C5A27FF052100B648F8 /* VirtualBuddy.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VirtualBuddy.entitlements; sourceTree = ""; }; F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VirtualCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F4BE9C6727FF053A00B648F8 /* VirtualCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VirtualCore.h; sourceTree = ""; }; F4BE9C7327FF055100B648F8 /* VBVirtualMachine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VBVirtualMachine.swift; sourceTree = ""; }; F4BE9C7527FF055100B648F8 /* MacOSVirtualMachineConfigurationHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacOSVirtualMachineConfigurationHelper.swift; sourceTree = ""; }; F4BE9C7927FF05B900B648F8 /* VMController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMController.swift; sourceTree = ""; }; F4BE9C7F27FF10FB00B648F8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; F4BE9C8027FF111100B648F8 /* VirtualBuddyAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualBuddyAppDelegate.swift; sourceTree = ""; }; F4BE9C8527FF13FF00B648F8 /* VirtualizationPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VirtualizationPrivate.h; sourceTree = ""; }; F4C122482807146200D359E2 /* Versions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Versions.xcconfig; sourceTree = ""; }; F4C1224A2807156200D359E2 /* Signing.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Signing.xcconfig; sourceTree = ""; }; F4C1224D280715B500D359E2 /* Paths.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Paths.xcconfig; sourceTree = ""; }; F4C1224E280715F200D359E2 /* Main.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Main.xcconfig; sourceTree = ""; }; F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VirtualWormhole.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F4C189E22848F59F00335EC7 /* VirtualWormhole.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VirtualWormhole.h; sourceTree = ""; }; F4C189ED2848F5B500335EC7 /* WormholeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WormholeManager.swift; sourceTree = ""; }; F4C189F32848F61E00335EC7 /* Virtualization.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Virtualization.framework; path = System/Library/Frameworks/Virtualization.framework; sourceTree = SDKROOT; }; F4C189F62848F6A600335EC7 /* VirtualCoreConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualCoreConstants.swift; sourceTree = ""; }; F4C189F92848F6F700335EC7 /* VirtualWormholeConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VirtualWormholeConstants.swift; sourceTree = ""; }; F4C189FC2848F8F600335EC7 /* WHSharedClipboardService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WHSharedClipboardService.swift; sourceTree = ""; }; F4C189FE2848FB3F00335EC7 /* WormholeServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WormholeServiceProtocol.swift; sourceTree = ""; }; F4C18A4228491B8500335EC7 /* VirtualBuddyGuest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VirtualBuddyGuest.app; sourceTree = BUILT_PRODUCTS_DIR; }; F4C18A4428491B8500335EC7 /* GuestAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestAppDelegate.swift; sourceTree = ""; }; F4C18A4628491B8500335EC7 /* GuestDashboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestDashboard.swift; sourceTree = ""; }; F4C18A4828491B8500335EC7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F4C18A4B28491B8500335EC7 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; F4C18A4D28491B8500335EC7 /* VirtualBuddyGuest.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VirtualBuddyGuest.entitlements; sourceTree = ""; }; F4C2374C2888A462001FF286 /* VolumeUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeUtils.swift; sourceTree = ""; }; F4C2374F2888AF67001FF286 /* LogStreamer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogStreamer.swift; sourceTree = ""; }; F4C947BE2E0B0F71001ACC91 /* URL+ExtendedAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+ExtendedAttributes.swift"; sourceTree = ""; }; F4C947D52E0B12D0001ACC91 /* String+AppleOSBuild.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AppleOSBuild.swift"; sourceTree = ""; }; F4C947D92E0B1E5D001ACC91 /* SoftwareCatalog+DownloadMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SoftwareCatalog+DownloadMatching.swift"; sourceTree = ""; }; F4CD131F2E05A5780067DC75 /* FileSystemPathFormControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemPathFormControl.swift; sourceTree = ""; }; F4CD133B2E05A9DF0067DC75 /* OpenVirtualBuddySettingsAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenVirtualBuddySettingsAction.swift; sourceTree = ""; }; F4CD133D2E05AB280067DC75 /* BackwardsCompatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackwardsCompatibility.swift; sourceTree = ""; }; F4CD133F2E05AB8F0067DC75 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; F4CD13432E05AD400067DC75 /* VerticalLabeledContentStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalLabeledContentStyle.swift; sourceTree = ""; }; F4CD13452E05B4DE0067DC75 /* VirtualizationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualizationSettingsView.swift; sourceTree = ""; }; F4CD13472E05B67E0067DC75 /* SettingsFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFooter.swift; sourceTree = ""; }; F4CD13492E05CB390067DC75 /* AutomationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomationSettingsView.swift; sourceTree = ""; }; F4D0F71428667984004D5782 /* VBVirtualMachine+Screenshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VBVirtualMachine+Screenshot.swift"; sourceTree = ""; }; F4D0F71928674E76004D5782 /* SoftwareUpdateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdateController.swift; sourceTree = ""; }; F4D0F71B28674F24004D5782 /* Features.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Features.xcconfig; sourceTree = ""; }; F4D0F71E2867517A004D5782 /* AppUpdateChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateChannel.swift; sourceTree = ""; }; F4D3059629B8D9D30006E748 /* WormholePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WormholePacket.swift; sourceTree = ""; }; F4D3059C29B8DB700006E748 /* VirtualWormholeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VirtualWormholeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F4D3059E29B8DB700006E748 /* WormholePacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WormholePacketTests.swift; sourceTree = ""; }; F4D305A929B8E7120006E748 /* TestStream.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = TestStream.bin; sourceTree = ""; }; F4D305AB29B8FEE90006E748 /* WHDarwinNotificationsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WHDarwinNotificationsService.swift; sourceTree = ""; }; F4D305AF29B900860006E748 /* SystemNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemNotification.swift; sourceTree = ""; }; F4D305B129B907A10006E748 /* WHPing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WHPing.swift; sourceTree = ""; }; F4D725FD286677B8001818F7 /* VBVirtualMachine+Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VBVirtualMachine+Metadata.swift"; sourceTree = ""; }; F4DE1C0A2D6F54E000603527 /* VBSavedStateMetadata+Clone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VBSavedStateMetadata+Clone.swift"; sourceTree = ""; }; F4DE1C0E2D6F603300603527 /* VBSavedStatePackage+VM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VBSavedStatePackage+VM.swift"; sourceTree = ""; }; F4DE1C102D6F642E00603527 /* VBStorageDeviceContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBStorageDeviceContainer.swift; sourceTree = ""; }; F4E4F6C42DEF96C200B3B8BA /* ChromeBorderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromeBorderModifier.swift; sourceTree = ""; }; F4E4F71F2DF080FC00B3B8BA /* RestoreImageSelectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreImageSelectionController.swift; sourceTree = ""; }; F4E7680929B64C590075A897 /* GuestTypePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestTypePicker.swift; sourceTree = ""; }; F4E7680C29B651220075A897 /* VirtualUI.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = VirtualUI.xcassets; sourceTree = ""; }; F4E7680E29B655DD0075A897 /* InstallMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallMethod.swift; sourceTree = ""; }; F4E7DF912BB3338900C459FC /* NSImage+HEIC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+HEIC.swift"; sourceTree = ""; }; F4E7DF942BB336F600C459FC /* VBSavedStatePackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBSavedStatePackage.swift; sourceTree = ""; }; F4E7DF962BB33E1700C459FC /* VMLibraryController+SavedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VMLibraryController+SavedState.swift"; sourceTree = ""; }; F4E7DFCE2BB3587D00C459FC /* VirtualMachineSessionUIManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMachineSessionUIManager.swift; sourceTree = ""; }; F4ECC6D42C63BFD5001DAC1D /* NumberDisplayMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberDisplayMode.swift; sourceTree = ""; }; F4F9B415284CE0F900F21737 /* VBSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBSettings.swift; sourceTree = ""; }; F4F9B417284CE12000F21737 /* VBSettingsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBSettingsContainer.swift; sourceTree = ""; }; F4F9B419284CE37C00F21737 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; F4FC276629BBAE350012CB65 /* WormholeServiceClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WormholeServiceClient.swift; sourceTree = ""; }; F4FC276929BBAE590012CB65 /* WHDefaultsImportClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WHDefaultsImportClient.swift; sourceTree = ""; }; F4FC276B29BBB3030012CB65 /* GuestDefaultsImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestDefaultsImportView.swift; sourceTree = ""; }; F4FC98382BB386A000E511C9 /* ContinuousProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinuousProgressIndicator.swift; sourceTree = ""; }; F4FC983A2BB386B500E511C9 /* MaskProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskProgressView.swift; sourceTree = ""; }; F4FC983C2BB386DD00E511C9 /* VMProgressOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMProgressOverlay.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F41369A829918576002CE8D3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; F43B01122AD858FE00164CD1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; F498ACFB2884BF13006F1C00 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F453C4632DF0E609007EAD5F /* BuddyKit in Frameworks */, F4510A7B2AE2B3B300E24DD9 /* DeepLinkSecurity.framework in Frameworks */, F498AD0C2884BF67006F1C00 /* VirtualCore.framework in Frameworks */, F4A7FB732BB7238A00E4C12A /* SwiftUIIntrospect-Static in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; F4BE9C4B27FF052100B648F8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F453C44C2DF0B835007EAD5F /* libcurl.tbd in Frameworks */, F453C4122DF0B1ED007EAD5F /* BuddyKit in Frameworks */, F43B01472AD85A7D00164CD1 /* URLQueryItemCoder in Frameworks */, F498AD042884BF13006F1C00 /* VirtualUI.framework in Frameworks */, F4D0F71828674E4B004D5782 /* Sparkle in Frameworks */, F453C44A2DF0B7F6007EAD5F /* FragmentZip in Frameworks */, F4BE9C6B27FF053A00B648F8 /* VirtualCore.framework in Frameworks */, F43B011B2AD858FE00164CD1 /* DeepLinkSecurity.framework in Frameworks */, F453C42B2DF0B792007EAD5F /* ArgumentParser in Frameworks */, F4C189E62848F59F00335EC7 /* VirtualWormhole.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; F4BE9C6227FF053A00B648F8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F4E4F7262DF0A1CB00B3B8BA /* BuddyKit in Frameworks */, F4C189F22848F5F500335EC7 /* VirtualWormhole.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; F4C189DD2848F59F00335EC7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; F4C18A3F28491B8500335EC7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F453C4242DF0B602007EAD5F /* VirtualUI.framework in Frameworks */, F4C18A5228491B9D00335EC7 /* VirtualWormhole.framework in Frameworks */, F453C45B2DF0C4BE007EAD5F /* BuddyKit in Frameworks */, F428622E2AE87D7E0052F029 /* DeepLinkSecurity.framework in Frameworks */, F413699A299179F8002CE8D3 /* VirtualCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; F4D3059929B8DB700006E748 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F4D305A029B8DB700006E748 /* VirtualWormhole.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ F40A1E9B2C1873B90033E47D /* ReleaseTrains */ = { isa = PBXGroup; children = ( F40A1E9C2C1873C60033E47D /* VBBuildType.swift */, F4D0F71E2867517A004D5782 /* AppUpdateChannel.swift */, ); path = ReleaseTrains; sourceTree = ""; }; F413694F29916F6E002CE8D3 /* SwiftUI Status Item */ = { isa = PBXGroup; children = ( F413695029916F6E002CE8D3 /* Components */, F413696129916F6E002CE8D3 /* StatusItemManager.swift */, ); path = "SwiftUI Status Item"; sourceTree = ""; }; F413695029916F6E002CE8D3 /* Components */ = { isa = PBXGroup; children = ( F413695429916F6E002CE8D3 /* Cocoa */, F413695B29916F6E002CE8D3 /* ObjC */, F413695129916F6E002CE8D3 /* StatusItemButton.swift */, F413695229916F6E002CE8D3 /* StatusBarPanelChrome.swift */, F413695329916F6E002CE8D3 /* StatusItemProviderProtocol.swift */, F41369A229917FA0002CE8D3 /* ScreenChangeModifier.swift */, ); path = Components; sourceTree = ""; }; F413695429916F6E002CE8D3 /* Cocoa */ = { isa = PBXGroup; children = ( F413695529916F6E002CE8D3 /* StatusItemPanelContentController.swift */, F413695729916F6E002CE8D3 /* VUIAppKitViewControllerHost.swift */, F413695829916F6E002CE8D3 /* StatusItemMenuBarExtraView.swift */, F413695929916F6E002CE8D3 /* StatusBarContentPanel.swift */, F413695A29916F6E002CE8D3 /* StatusBarHighlightView.swift */, ); path = Cocoa; sourceTree = ""; }; F413695B29916F6E002CE8D3 /* ObjC */ = { isa = PBXGroup; children = ( F413695E29916F6E002CE8D3 /* NSStatusBarPrivate.h */, F413695C29916F6E002CE8D3 /* NSStatusItem+.h */, F413696029916F6E002CE8D3 /* NSStatusItem+.m */, F413695D29916F6E002CE8D3 /* NSApplication+MenuBar.h */, F413695F29916F6E002CE8D3 /* NSApplication+MenuBar.m */, ); path = ObjC; sourceTree = ""; }; F41369A4299183BA002CE8D3 /* Dashboard */ = { isa = PBXGroup; children = ( F41369C4299187D8002CE8D3 /* Support */, F41369C92991A492002CE8D3 /* HostConnectionStateProvider.swift */, F41369CB2991A68F002CE8D3 /* GuestSharedFoldersManager.swift */, F4FC276B29BBB3030012CB65 /* GuestDefaultsImportView.swift */, F4C18A4628491B8500335EC7 /* GuestDashboard.swift */, ); path = Dashboard; sourceTree = ""; }; F41369AC29918576002CE8D3 /* VirtualBuddyGuestHelper */ = { isa = PBXGroup; children = ( F41369AD29918576002CE8D3 /* GuestHelperAppDelegate.swift */, F41369B129918576002CE8D3 /* Assets.xcassets */, F41369B329918576002CE8D3 /* Main.storyboard */, F41369B629918576002CE8D3 /* VirtualBuddyGuestHelper.entitlements */, ); path = VirtualBuddyGuestHelper; sourceTree = ""; }; F41369C4299187D8002CE8D3 /* Support */ = { isa = PBXGroup; children = ( F41369A5299183C8002CE8D3 /* GuestLaunchAtLoginManager.swift */, F41369C5299187E1002CE8D3 /* VirtualBuddyGuest-Bridging-Header.h */, ); path = Support; sourceTree = ""; }; F41725642886DF4A004FF8A7 /* Components */ = { isa = PBXGroup; children = ( F453C4642DF0F7F4007EAD5F /* BlurHash */, F47BCDC92C5C01C600165191 /* RemoteImage */, F413694F29916F6E002CE8D3 /* SwiftUI Status Item */, F48E0D26288883150080DDFA /* HostingWindowController */, F48E0D20288882E50080DDFA /* DecentFormView.swift */, F48E0D21288882E50080DDFA /* NSAlert+Confirmation.swift */, F48E0D22288882E50080DDFA /* OnAppearOnce.swift */, F41725672886E5AD004FF8A7 /* MaterialView.swift */, F41725652886DF58004FF8A7 /* OpenSavePanelUtils.swift */, F42C014B2888C34B00EB15CD /* LogConsole.swift */, F4A7FB6D2BB7206C00E4C12A /* SelfSizingGroupedForm.swift */, F47BCDD22C5C0AB300165191 /* KeyboardNavigationModifier.swift */, F47BCDD42C5C0B8C00165191 /* Array+Navigation.swift */, F444D0F32DF34BE40086537A /* CALayer+Asset.swift */, F49B82972E02F5A300395F87 /* VMArtworkView.swift */, F417CB872E0EDECD0065B5D6 /* BackportedContentUnavailableView.swift */, F417CB892E0EDF1A0065B5D6 /* EqualWidthHStack.swift */, ); path = Components; sourceTree = ""; }; F417256D2887543B004FF8A7 /* Storage */ = { isa = PBXGroup; children = ( F417256B2887500F004FF8A7 /* StorageConfigurationView.swift */, F417256E2887544A004FF8A7 /* StorageDeviceDetailView.swift */, F48E0D02288858DF0080DDFA /* ManagedDiskImageEditor.swift */, ); path = Storage; sourceTree = ""; }; F417257228877453004FF8A7 /* Resources */ = { isa = PBXGroup; children = ( F47BCD9E2C5BE8D300165191 /* SoftwareCatalog */, F48E0D0828885E510080DDFA /* Preview */, F417257328877478004FF8A7 /* VirtualCore.xcassets */, ); path = Resources; sourceTree = ""; }; F422586E2885D518009420AE /* Configuration Controls */ = { isa = PBXGroup; children = ( F422586C2885CC9F009420AE /* SharedFocusEnvironment.swift */, F498AD112884C36A006F1C00 /* NumericValueField.swift */, F498AD172884C593006F1C00 /* NumericPropertyControl.swift */, F422586F2885D537009420AE /* EphemeralTextField.swift */, F422587F2885E71D009420AE /* PropertyControl.swift */, ); path = "Configuration Controls"; sourceTree = ""; }; F42258752885E132009420AE /* Sections */ = { isa = PBXGroup; children = ( F417256D2887543B004FF8A7 /* Storage */, F4B5C5D12886FA77005AA632 /* Sharing */, F422587D2885E2ED009420AE /* HardwareConfigurationView.swift */, F42258772885E14A009420AE /* DisplayConfigurationView.swift */, F422587B2885E1CE009420AE /* NetworkConfigurationView.swift */, F417255C288604A8004FF8A7 /* SoundConfigurationView.swift */, F4B5C5D42886FFB5005AA632 /* PointingDeviceConfigurationView.swift */, F4450CC92ACB0DB500092618 /* KeyboardDeviceConfigurationView.swift */, F49B832C2E046B8D00395F87 /* GuestAppConfigurationView.swift */, ); path = Sections; sourceTree = ""; }; F42258762885E139009420AE /* Components */ = { isa = PBXGroup; children = ( F42258792885E17D009420AE /* ConfigurationSection.swift */, F4B5C5DA28873628005AA632 /* GroupedList.swift */, ); path = Components; sourceTree = ""; }; F42C01502888FC0C00EB15CD /* Library */ = { isa = PBXGroup; children = ( F42C015F2888FC2500EB15CD /* Components */, F42C01512888FC0C00EB15CD /* LibraryView.swift */, ); path = Library; sourceTree = ""; }; F42C01522888FC0C00EB15CD /* Session */ = { isa = PBXGroup; children = ( F42C01562888FC0C00EB15CD /* Components */, F42C01532888FC0C00EB15CD /* Configuration */, F4E7DFCE2BB3587D00C459FC /* VirtualMachineSessionUIManager.swift */, F49AA2C429BA31CC009625F7 /* VirtualMachineSessionUI.swift */, F42C01552888FC0C00EB15CD /* VirtualMachineSessionView.swift */, ); path = Session; sourceTree = ""; }; F42C01532888FC0C00EB15CD /* Configuration */ = { isa = PBXGroup; children = ( F42C01542888FC0C00EB15CD /* VMSessionConfigurationView.swift */, ); path = Configuration; sourceTree = ""; }; F42C01562888FC0C00EB15CD /* Components */ = { isa = PBXGroup; children = ( F42C01572888FC0C00EB15CD /* SwiftUIVMView.swift */, F4ECC6D42C63BFD5001DAC1D /* NumberDisplayMode.swift */, F428622C2AE8726D0052F029 /* VirtualMachineControls.swift */, F4FC98382BB386A000E511C9 /* ContinuousProgressIndicator.swift */, F4FC983A2BB386B500E511C9 /* MaskProgressView.swift */, F4FC983C2BB386DD00E511C9 /* VMProgressOverlay.swift */, F4A7FB3E2BB5EBEF00E4C12A /* SavedStatePicker.swift */, ); path = Components; sourceTree = ""; }; F42C01582888FC0C00EB15CD /* Settings */ = { isa = PBXGroup; children = ( F4CD131E2E05A5760067DC75 /* Components */, F42C01592888FC0C00EB15CD /* SettingsScreen.swift */, F4CD133F2E05AB8F0067DC75 /* GeneralSettingsView.swift */, F4CD13452E05B4DE0067DC75 /* VirtualizationSettingsView.swift */, F4CD13492E05CB390067DC75 /* AutomationSettingsView.swift */, ); path = Settings; sourceTree = ""; }; F42C015F2888FC2500EB15CD /* Components */ = { isa = PBXGroup; children = ( F42C01602888FC3500EB15CD /* LibraryItemView.swift */, F417CBB82E0F3D2E0065B5D6 /* FirstLaunchExperienceView.swift */, ); path = Components; sourceTree = ""; }; F43B01162AD858FE00164CD1 /* DeepLinkSecurity */ = { isa = PBXGroup; children = ( F43B01222AD8590F00164CD1 /* Source */, F43B01172AD858FE00164CD1 /* DeepLinkSecurity.h */, ); path = DeepLinkSecurity; sourceTree = ""; }; F43B01222AD8590F00164CD1 /* Source */ = { isa = PBXGroup; children = ( F43B01332AD8590F00164CD1 /* Base */, F43B01232AD8590F00164CD1 /* UI */, F43B01262AD8590F00164CD1 /* Models */, F43B012D2AD8590F00164CD1 /* Storage */, F43B01252AD8590F00164CD1 /* DeepLinkSentinel.swift */, ); path = Source; sourceTree = ""; }; F43B01232AD8590F00164CD1 /* UI */ = { isa = PBXGroup; children = ( F43B01242AD8590F00164CD1 /* DeepLinkAuthUI.swift */, ); path = UI; sourceTree = ""; }; F43B01262AD8590F00164CD1 /* Models */ = { isa = PBXGroup; children = ( F43B01272AD8590F00164CD1 /* OpenDeepLinkRequest.swift */, F43B01282AD8590F00164CD1 /* DeepLinkClientDescriptor.swift */, F43B01292AD8590F00164CD1 /* Extensions */, F43B012C2AD8590F00164CD1 /* DeepLinkClient.swift */, ); path = Models; sourceTree = ""; }; F43B01292AD8590F00164CD1 /* Extensions */ = { isa = PBXGroup; children = ( F43B012A2AD8590F00164CD1 /* DeepLinkClient+Crypto.swift */, F43B012B2AD8590F00164CD1 /* DeepLinkClientDescriptor+.swift */, ); path = Extensions; sourceTree = ""; }; F43B012D2AD8590F00164CD1 /* Storage */ = { isa = PBXGroup; children = ( F43B012E2AD8590F00164CD1 /* KeychainDeepLinkAuthStore.swift */, F43B012F2AD8590F00164CD1 /* MemoryDeepLinkAuthStore.swift */, F43B01302AD8590F00164CD1 /* DeepLinkManagementStore.swift */, F43B01312AD8590F00164CD1 /* UserDefaultsDeepLinkManagementStore.swift */, F43B01322AD8590F00164CD1 /* DeepLinkAuthStore.swift */, ); path = Storage; sourceTree = ""; }; F43B01332AD8590F00164CD1 /* Base */ = { isa = PBXGroup; children = ( F43B01342AD8590F00164CD1 /* DeepLinkSecurityDefines.swift */, ); path = Base; sourceTree = ""; }; F43B01422AD85A5200164CD1 /* Automation */ = { isa = PBXGroup; children = ( F43B01482AD85AAF00164CD1 /* Support */, F43B01432AD85A6500164CD1 /* VirtualBuddyDeepLinks.swift */, F43B014D2AD86BFA00164CD1 /* DeepLinkHandler.swift */, ); path = Automation; sourceTree = ""; }; F43B01482AD85AAF00164CD1 /* Support */ = { isa = PBXGroup; children = ( F43B01492AD85ABB00164CD1 /* DeepLinkAuthDialog.swift */, ); path = Support; sourceTree = ""; }; F443620D29B79D6800745B43 /* GuestSupport */ = { isa = PBXGroup; children = ( F443620E29B7A0C600745B43 /* CreateGuestImage.sh */, F443620929B7947A00745B43 /* GuestAdditionsDiskImage.swift */, ); path = GuestSupport; sourceTree = ""; }; F453C4172DF0B43D007EAD5F /* Utilities */ = { isa = PBXGroup; children = ( F453C4162DF0B43D007EAD5F /* BlurHashEncode.swift */, F4C947BE2E0B0F71001ACC91 /* URL+ExtendedAttributes.swift */, F4C947D52E0B12D0001ACC91 /* String+AppleOSBuild.swift */, ); path = Utilities; sourceTree = ""; }; F453C41C2DF0B43D007EAD5F /* VirtualCatalog */ = { isa = PBXGroup; children = ( F453C45C2DF0D286007EAD5F /* README.md */, F453C4172DF0B43D007EAD5F /* Utilities */, F453C4182DF0B43D007EAD5F /* LegacyCatalog.swift */, F453C4192DF0B43D007EAD5F /* MobileDeviceFramework.swift */, F453C41B2DF0B43D007EAD5F /* SoftwareCatalog.swift */, F453C41A2DF0B43D007EAD5F /* ResolvedCatalog.swift */, F4C947D92E0B1E5D001ACC91 /* SoftwareCatalog+DownloadMatching.swift */, ); path = VirtualCatalog; sourceTree = ""; }; F453C4292DF0B785007EAD5F /* CommandLine */ = { isa = PBXGroup; children = ( F453C44D2DF0B869007EAD5F /* VirtualBuddyCLI.swift */, F453C43A2DF0B7A5007EAD5F /* vctool */, ); path = CommandLine; sourceTree = ""; }; F453C4312DF0B7A5007EAD5F /* Core */ = { isa = PBXGroup; children = ( F453C42C2DF0B7A5007EAD5F /* BuildManifest.swift */, F453C42D2DF0B7A5007EAD5F /* BuildManifest+Fetch.swift */, F453C42E2DF0B7A5007EAD5F /* Helpers.swift */, F453C42F2DF0B7A5007EAD5F /* TreeStringConvertible.swift */, F453C4302DF0B7A5007EAD5F /* URL+ContentLength.swift */, ); path = Core; sourceTree = ""; }; F453C43A2DF0B7A5007EAD5F /* vctool */ = { isa = PBXGroup; children = ( F453C4312DF0B7A5007EAD5F /* Core */, F453C4322DF0B7A5007EAD5F /* CatalogCommand.swift */, F453C4332DF0B7A5007EAD5F /* GroupCommand.swift */, F453C4342DF0B7A5007EAD5F /* ImageCommand.swift */, F453C4352DF0B7A5007EAD5F /* IPSWCommand.swift */, F453C4362DF0B7A5007EAD5F /* MigrateCommand.swift */, F453C4372DF0B7A5007EAD5F /* MobileDeviceCommand.swift */, F453C4382DF0B7A5007EAD5F /* ResolveCommand.swift */, F453C4672DF10181007EAD5F /* BlurHashCommand.swift */, F453C4392DF0B7A5007EAD5F /* VCTool.swift */, ); path = vctool; sourceTree = ""; }; F453C4642DF0F7F4007EAD5F /* BlurHash */ = { isa = PBXGroup; children = ( F47BCDCA2C5C01CE00165191 /* BlurHashDecoding.swift */, ); path = BlurHash; sourceTree = ""; }; F453C49C2DF1D788007EAD5F /* Restore */ = { isa = PBXGroup; children = ( F453C49D2DF1D79F007EAD5F /* Download */, F453C49E2DF1D7A7007EAD5F /* Installation */, ); path = Restore; sourceTree = ""; }; F453C49D2DF1D79F007EAD5F /* Download */ = { isa = PBXGroup; children = ( F453C4882DF1CDA0007EAD5F /* DownloadBackend.swift */, F453C49A2DF1D768007EAD5F /* SimulatedDownloadBackend.swift */, F465C3B7284FA252006E9ED4 /* URLSessionDownloadBackend.swift */, ); path = Download; sourceTree = ""; }; F453C49E2DF1D7A7007EAD5F /* Installation */ = { isa = PBXGroup; children = ( F453C49F2DF1D7C0007EAD5F /* RestoreBackend.swift */, F453C4A12DF1D7F6007EAD5F /* SimulatedRestoreBackend.swift */, F453C4A32DF1D85C007EAD5F /* VirtualizationRestoreBackend.swift */, ); path = Installation; sourceTree = ""; }; F45502172DF4635C005582A4 /* _Downloads */ = { isa = PBXGroup; children = ( F45502182DF46368005582A4 /* UniversalMac_13.3.1_22E261_Restore.ipsw */, F45502192DF46368005582A4 /* UniversalMac_14.0_23A344_Restore.ipsw */, F455021A2DF46368005582A4 /* UniversalMac_14.5_23F79_Restore.ipsw */, F455021B2DF46368005582A4 /* UniversalMac_15.3_24D60_Restore.ipsw */, F455021C2DF46368005582A4 /* UniversalMac_15.5_24F74_Restore.ipsw */, ); path = _Downloads; sourceTree = ""; }; F465C3AC284F939C006E9ED4 /* Restore Images */ = { isa = PBXGroup; children = ( F465C3B3284F967C006E9ED4 /* Models */, F465C3AD284F93A5006E9ED4 /* VBAPIClient.swift */, ); path = "Restore Images"; sourceTree = ""; }; F465C3B3284F967C006E9ED4 /* Models */ = { isa = PBXGroup; children = ( F465C3AF284F9660006E9ED4 /* VBRestoreImagesResponse.swift */, F465C3B1284F9666006E9ED4 /* VBRestoreImageInfo.swift */, ); path = Models; sourceTree = ""; }; F47BCD9E2C5BE8D300165191 /* SoftwareCatalog */ = { isa = PBXGroup; children = ( F47BCD9F2C5BE8EF00165191 /* ipsws_v2.json */, F47BCDA02C5BE8EF00165191 /* linux_v2.json */, ); name = SoftwareCatalog; sourceTree = ""; }; F47BCDC92C5C01C600165191 /* RemoteImage */ = { isa = PBXGroup; children = ( F47BCDCC2C5C01EF00165191 /* RemoteImage.swift */, ); path = RemoteImage; sourceTree = ""; }; F48E0D0828885E510080DDFA /* Preview */ = { isa = PBXGroup; children = ( F45502172DF4635C005582A4 /* _Downloads */, F4A7FB452BB5ED4A00E4C12A /* PreviewSavedStates */, F453C4912DF1D213007EAD5F /* FakeRestoreImage.ipsw */, F49B829F2E02FCB100395F87 /* PreviewMac.vbvm */, F49B829D2E02FCB100395F87 /* PreviewMacBlurHash.vbvm */, F49B82A32E02FCF400395F87 /* PreviewMacNoArtwork.vbvm */, F49B829E2E02FCB100395F87 /* PreviewLinux.vbvm */, F49B82A52E03049C00395F87 /* PreviewLinuxBlurHash.vbvm */, F49B82A62E03049C00395F87 /* PreviewLinuxNoArtwork.vbvm */, ); path = Preview; sourceTree = ""; }; F48E0D12288882BD0080DDFA /* Installer */ = { isa = PBXGroup; children = ( F48E0D13288882BD0080DDFA /* Components */, F48E0D30288884780080DDFA /* Steps */, F48E0D18288882BD0080DDFA /* VMInstallationWizard.swift */, F453C4B82DF21985007EAD5F /* VMInstallData.swift */, F48E0D19288882BD0080DDFA /* VMInstallationViewModel.swift */, ); path = Installer; sourceTree = ""; }; F48E0D13288882BD0080DDFA /* Components */ = { isa = PBXGroup; children = ( F45502132DF394DC005582A4 /* VirtualBuddyInstallerInputView.swift */, F48E0D14288882BD0080DDFA /* InstallationWizardTitle.swift */, F48E0D17288882BD0080DDFA /* AuthenticatingWebView.swift */, F42C01492888C2F800EB15CD /* InstallationConsole.swift */, F4561A6728981B4100055289 /* VirtualMachineNameInputView.swift */, F453C4B32DF20301007EAD5F /* RestoreImageURLInputView.swift */, ); path = Components; sourceTree = ""; }; F48E0D26288883150080DDFA /* HostingWindowController */ = { isa = PBXGroup; children = ( F48E0D27288883150080DDFA /* OpenCocoaWindowAction.swift */, F48E0D28288883150080DDFA /* HostingWindowController.swift */, F48E0D29288883150080DDFA /* WindowEnvironment.swift */, F462C9412E0C96D300C172E2 /* FB18383725Window.swift */, F49AA2C229BA22A5009625F7 /* VBRestorableWindow.swift */, F49AA2C629BA3F2B009625F7 /* VBRestorableWindow+Resizing.swift */, ); path = HostingWindowController; sourceTree = ""; }; F48E0D2D288883450080DDFA /* Definitions */ = { isa = PBXGroup; children = ( F48E0D2E2888835A0080DDFA /* VirtualUIConstants.swift */, F4A7FB742BB7252A00E4C12A /* PreviewSupport-VirtualUI.swift */, ); path = Definitions; sourceTree = ""; }; F48E0D30288884780080DDFA /* Steps */ = { isa = PBXGroup; children = ( F4E4F71D2DF080D200B3B8BA /* Restore Image Selection */, F4E7680E29B655DD0075A897 /* InstallMethod.swift */, F48E0D15288882BD0080DDFA /* InstallMethodPicker.swift */, F4E7680929B64C590075A897 /* GuestTypePicker.swift */, F48E0D31288884A10080DDFA /* RestoreImageDownloadView.swift */, F48E0D33288889E60080DDFA /* InstallConfigurationStepView.swift */, F42C014D2888CBCB00EB15CD /* InstallProgressStepView.swift */, ); path = Steps; sourceTree = ""; }; F498ACFF2884BF13006F1C00 /* VirtualUI */ = { isa = PBXGroup; children = ( F4E7680B29B651180075A897 /* Resources */, F498AD092884BF20006F1C00 /* Source */, F498AD002884BF13006F1C00 /* VirtualUI.h */, ); path = VirtualUI; sourceTree = ""; }; F498AD092884BF20006F1C00 /* Source */ = { isa = PBXGroup; children = ( F42C01502888FC0C00EB15CD /* Library */, F42C01522888FC0C00EB15CD /* Session */, F42C01582888FC0C00EB15CD /* Settings */, F48E0D2D288883450080DDFA /* Definitions */, F48E0D12288882BD0080DDFA /* Installer */, F41725642886DF4A004FF8A7 /* Components */, F498AD102884C35F006F1C00 /* VM Configuration */, F498AD0F2884C35A006F1C00 /* Building Blocks */, ); path = Source; sourceTree = ""; }; F498AD0F2884C35A006F1C00 /* Building Blocks */ = { isa = PBXGroup; children = ( F422586E2885D518009420AE /* Configuration Controls */, F498AD132884C36A006F1C00 /* ControlGroupChrome.swift */, F498AD192884C5FF006F1C00 /* SliderConversion.swift */, F413697829917135002CE8D3 /* CGFloat+OnePixel.swift */, F4E4F6C42DEF96C200B3B8BA /* ChromeBorderModifier.swift */, ); path = "Building Blocks"; sourceTree = ""; }; F498AD102884C35F006F1C00 /* VM Configuration */ = { isa = PBXGroup; children = ( F42258762885E139009420AE /* Components */, F42258752885E132009420AE /* Sections */, F42258732885E10B009420AE /* VMConfigurationViewModel.swift */, F42258712885E100009420AE /* VMConfigurationSheet.swift */, F498AD0D2884BF9D006F1C00 /* VMConfigurationView.swift */, ); path = "VM Configuration"; sourceTree = ""; }; F49A68DF2884917400A17582 /* Configuration */ = { isa = PBXGroup; children = ( F417255E28861604004FF8A7 /* DecodableDefault.swift */, F49A68E02884917E00A17582 /* ConfigurationModels.swift */, F4B5C5D628870619005AA632 /* ConfigurationModels+Validation.swift */, F4B5C5D828870BBF005AA632 /* ConfigurationModels+Summary.swift */, F48E0D0B2888760D0080DDFA /* VBMacDevice+Storage.swift */, ); path = Configuration; sourceTree = ""; }; F49B82FD2E034F5600395F87 /* DesktopPicture */ = { isa = PBXGroup; children = ( F49B82FE2E034F6800395F87 /* NSImage+DesktopPicture.h */, F49B82FF2E034F6800395F87 /* NSImage+DesktopPicture.m */, F49B83702E04837400395F87 /* CGImage+FullyTransparent.swift */, F49B82FB2E034EAD00395F87 /* WHDesktopPictureService.swift */, ); path = DesktopPicture; sourceTree = ""; }; F49FD87B2DFB68CC0019D638 /* Import */ = { isa = PBXGroup; children = ( F49FD87E2DFB6B630019D638 /* VMImporterRegistry.swift */, F49FD87C2DFB68F20019D638 /* VMImporter.swift */, F49FD8832DFB72790019D638 /* VMImporter+Helpers.swift */, F49FD8822DFB726C0019D638 /* UTM */, ); path = Import; sourceTree = ""; }; F49FD8822DFB726C0019D638 /* UTM */ = { isa = PBXGroup; children = ( F49FD8802DFB6CD80019D638 /* UTMImporter.swift */, F49FD8852DFB728A0019D638 /* UTMAppleConfiguration.swift */, ); path = UTM; sourceTree = ""; }; F4A21BEE28032F97001072B8 /* Models */ = { isa = PBXGroup; children = ( F49A68DF2884917400A17582 /* Configuration */, F4E7DF932BB336E000C459FC /* SavedState */, F4A21BF328033102001072B8 /* VBError.swift */, F42CF4A72DF5FEC3001DE049 /* BlurHashToken.swift */, F4BE9C7327FF055100B648F8 /* VBVirtualMachine.swift */, F4DE1C102D6F642E00603527 /* VBStorageDeviceContainer.swift */, F46FFBA72804F07400D61023 /* VBNVRAMVariable.swift */, F4D725FD286677B8001818F7 /* VBVirtualMachine+Metadata.swift */, F4D0F71428667984004D5782 /* VBVirtualMachine+Screenshot.swift */, ); path = Models; sourceTree = ""; }; F4A21BEF28032FB0001072B8 /* Virtualization */ = { isa = PBXGroup; children = ( F4E7DF832BB30D8200C459FC /* Screenshot */, F4A21BF028032FBD001072B8 /* Helpers */, F4A21BF128032FD8001072B8 /* VMLibraryController.swift */, F4A7FB3C2BB5E8A200E4C12A /* VMSavedStatesController.swift */, F46FFBAB28059FF600D61023 /* VMInstance.swift */, F4BE9C7927FF05B900B648F8 /* VMController.swift */, F44C00FA2889CE1600640BF5 /* VBVirtualMachine+Virtualization.swift */, ); path = Virtualization; sourceTree = ""; }; F4A21BF028032FBD001072B8 /* Helpers */ = { isa = PBXGroup; children = ( F47BCDD62C5D2B4300165191 /* CatalogExtensions.swift */, 4BA6BE7C293D22E500F396AE /* VirtualMachineConfigurationHelper.swift */, F4BE9C7527FF055100B648F8 /* MacOSVirtualMachineConfigurationHelper.swift */, 0196B45229292B2A00614EF1 /* LinuxVirtualMachineConfigurationHelper.swift */, F46FFBA92804F0A000D61023 /* VZVirtualMachineConfiguration+NVRAM.swift */, F417257028877121004FF8A7 /* DiskImageGenerator.swift */, F41725752887758A004FF8A7 /* RandomNameGenerator.swift */, F4A7FB3A2BB5E79100E4C12A /* DirectoryObserver.swift */, F485B9202BB306AF004B3C2B /* VBDebugUtil.h */, F485B9212BB306AF004B3C2B /* VBDebugUtil.m */, ); path = Helpers; sourceTree = ""; }; F4A21BFB28033968001072B8 /* Bootstrap */ = { isa = PBXGroup; children = ( F453C4222DF0B5B1007EAD5F /* VirtualBuddyEntryPoint.swift */, F4BE9C5127FF052100B648F8 /* VirtualBuddyApp.swift */, F4BE9C8027FF111100B648F8 /* VirtualBuddyAppDelegate.swift */, F4D0F71928674E76004D5782 /* SoftwareUpdateController.swift */, ); path = Bootstrap; sourceTree = ""; }; F4A7FB452BB5ED4A00E4C12A /* PreviewSavedStates */ = { isa = PBXGroup; children = ( F4A7FB402BB5ED4A00E4C12A /* Save-2024-03-27_16;06;24.vbst */, F4A7FB412BB5ED4A00E4C12A /* Save-2024-03-27_16;07;04.vbst */, F4A7FB422BB5ED4A00E4C12A /* Save-2024-03-27_16;08;06.vbst */, F4A7FB432BB5ED4A00E4C12A /* Save-2024-03-27_16;08;28.vbst */, F4A7FB442BB5ED4A00E4C12A /* Save-2024-03-27_16;08;51.vbst */, ); path = PreviewSavedStates; sourceTree = ""; }; F4B5C5D12886FA77005AA632 /* Sharing */ = { isa = PBXGroup; children = ( F417256028861A05004FF8A7 /* SharingConfigurationView.swift */, F4B5C5D22886FA8D005AA632 /* SharedFoldersManagementView.swift */, F41725622886DD37004FF8A7 /* SharedFolderListItem.swift */, ); path = Sharing; sourceTree = ""; }; F4BE580B29BA6C6300C5525C /* DefaultsImport */ = { isa = PBXGroup; children = ( F4FC276829BBAE470012CB65 /* Implementation */, F4BE580C29BA6C7A00C5525C /* Resources */, F4BE584129BA7BDB00C5525C /* WHDefaultsImportService.swift */, F4FC276929BBAE590012CB65 /* WHDefaultsImportClient.swift */, ); path = DefaultsImport; sourceTree = ""; }; F4BE580C29BA6C7A00C5525C /* Resources */ = { isa = PBXGroup; children = ( F4BE580D29BA6C8D00C5525C /* DefaultsDomains.plist */, ); path = Resources; sourceTree = ""; }; F4BE9C4527FF052100B648F8 = { isa = PBXGroup; children = ( F4BE9C5027FF052100B648F8 /* VirtualBuddy */, F4BE9C6627FF053A00B648F8 /* VirtualCore */, F498ACFF2884BF13006F1C00 /* VirtualUI */, F4C189E12848F59F00335EC7 /* VirtualWormhole */, F4C18A4328491B8500335EC7 /* VirtualBuddyGuest */, F41369AC29918576002CE8D3 /* VirtualBuddyGuestHelper */, F4D3059D29B8DB700006E748 /* VirtualWormholeTests */, F43B01162AD858FE00164CD1 /* DeepLinkSecurity */, F4BE9C4F27FF052100B648F8 /* Products */, F4C189F12848F5F500335EC7 /* Frameworks */, ); sourceTree = ""; }; F4BE9C4F27FF052100B648F8 /* Products */ = { isa = PBXGroup; children = ( F4BE9C4E27FF052100B648F8 /* VirtualBuddy.app */, F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */, F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */, F4C18A4228491B8500335EC7 /* VirtualBuddyGuest.app */, F498ACFE2884BF13006F1C00 /* VirtualUI.framework */, F41369AB29918576002CE8D3 /* VirtualBuddyGuestHelper.app */, F4D3059C29B8DB700006E748 /* VirtualWormholeTests.xctest */, F43B01152AD858FE00164CD1 /* DeepLinkSecurity.framework */, ); name = Products; sourceTree = ""; }; F4BE9C5027FF052100B648F8 /* VirtualBuddy */ = { isa = PBXGroup; children = ( F453C4292DF0B785007EAD5F /* CommandLine */, F4C122472807145600D359E2 /* Config */, F4A21BFB28033968001072B8 /* Bootstrap */, F43B01422AD85A5200164CD1 /* Automation */, F4BE9C7F27FF10FB00B648F8 /* Info.plist */, F4BE9C5527FF052100B648F8 /* Assets.xcassets */, F4BE9C5727FF052100B648F8 /* Preview Content */, ); path = VirtualBuddy; sourceTree = ""; }; F4BE9C5727FF052100B648F8 /* Preview Content */ = { isa = PBXGroup; children = ( F4BE9C5827FF052100B648F8 /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; F4BE9C6627FF053A00B648F8 /* VirtualCore */ = { isa = PBXGroup; children = ( F4BE9C7127FF053D00B648F8 /* Source */, F4BE9C6727FF053A00B648F8 /* VirtualCore.h */, ); path = VirtualCore; sourceTree = ""; }; F4BE9C7127FF053D00B648F8 /* Source */ = { isa = PBXGroup; children = ( F49FD87B2DFB68CC0019D638 /* Import */, F453C49C2DF1D788007EAD5F /* Restore */, F453C41C2DF0B43D007EAD5F /* VirtualCatalog */, F40A1E9B2C1873B90033E47D /* ReleaseTrains */, F4C2374E2888AF5B001FF286 /* Utilities */, F417257228877453004FF8A7 /* Resources */, F465C3AC284F939C006E9ED4 /* Restore Images */, F4F9B414284CE0DC00F21737 /* Settings */, F4C189F52848F69B00335EC7 /* Definitions */, F4A21BEF28032FB0001072B8 /* Virtualization */, F443620D29B79D6800745B43 /* GuestSupport */, F4A21BEE28032F97001072B8 /* Models */, F4BE9C8427FF13F600B648F8 /* Headers */, ); path = Source; sourceTree = ""; }; F4BE9C8427FF13F600B648F8 /* Headers */ = { isa = PBXGroup; children = ( F4BE9C8527FF13FF00B648F8 /* VirtualizationPrivate.h */, ); path = Headers; sourceTree = ""; }; F4C122472807145600D359E2 /* Config */ = { isa = PBXGroup; children = ( F4C1224C2807158A00D359E2 /* Entitlements */, F4C1224D280715B500D359E2 /* Paths.xcconfig */, F4C122482807146200D359E2 /* Versions.xcconfig */, F4C1224A2807156200D359E2 /* Signing.xcconfig */, F482FC7228CB7A6C00F2BA4F /* InfoPlist.xcconfig */, F4B068B528882F5D003743BF /* AppTarget.xcconfig */, F4D0F71B28674F24004D5782 /* Features.xcconfig */, F4C1224E280715F200D359E2 /* Main.xcconfig */, ); path = Config; sourceTree = ""; }; F4C1224C2807158A00D359E2 /* Entitlements */ = { isa = PBXGroup; children = ( F4BE9C5A27FF052100B648F8 /* VirtualBuddy.entitlements */, F4B068B428882EB7003743BF /* VirtualBuddy_Managed.entitlements */, ); path = Entitlements; sourceTree = ""; }; F4C189E12848F59F00335EC7 /* VirtualWormhole */ = { isa = PBXGroup; children = ( F4C189EC2848F5A300335EC7 /* Source */, F4C189E22848F59F00335EC7 /* VirtualWormhole.h */, ); path = VirtualWormhole; sourceTree = ""; }; F4C189EC2848F5A300335EC7 /* Source */ = { isa = PBXGroup; children = ( F4D3059529B8D9C70006E748 /* WireProtocol */, F4C189F82848F6F700335EC7 /* Definitions */, F4C189FB2848F8E800335EC7 /* Services */, F4C189ED2848F5B500335EC7 /* WormholeManager.swift */, ); path = Source; sourceTree = ""; }; F4C189F12848F5F500335EC7 /* Frameworks */ = { isa = PBXGroup; children = ( F453C44B2DF0B835007EAD5F /* libcurl.tbd */, F4C189F32848F61E00335EC7 /* Virtualization.framework */, ); name = Frameworks; sourceTree = ""; }; F4C189F52848F69B00335EC7 /* Definitions */ = { isa = PBXGroup; children = ( F48E0D0628885E140080DDFA /* PreviewSupport.swift */, F4C189F62848F6A600335EC7 /* VirtualCoreConstants.swift */, F4F9B419284CE37C00F21737 /* Logging.swift */, ); path = Definitions; sourceTree = ""; }; F4C189F82848F6F700335EC7 /* Definitions */ = { isa = PBXGroup; children = ( F4C189F92848F6F700335EC7 /* VirtualWormholeConstants.swift */, ); path = Definitions; sourceTree = ""; }; F4C189FB2848F8E800335EC7 /* Services */ = { isa = PBXGroup; children = ( F4D305AE29B900790006E748 /* Base */, F4BE580B29BA6C6300C5525C /* DefaultsImport */, F4D305AD29B9006E0006E748 /* DarwinNotifications */, F49B82FD2E034F5600395F87 /* DesktopPicture */, F4C189FC2848F8F600335EC7 /* WHSharedClipboardService.swift */, ); path = Services; sourceTree = ""; }; F4C18A4328491B8500335EC7 /* VirtualBuddyGuest */ = { isa = PBXGroup; children = ( F41369A4299183BA002CE8D3 /* Dashboard */, F4C18A4428491B8500335EC7 /* GuestAppDelegate.swift */, F4959F3B2992A284001DF4CB /* GuestAppInstaller.swift */, F4C18A4828491B8500335EC7 /* Assets.xcassets */, F4C18A4D28491B8500335EC7 /* VirtualBuddyGuest.entitlements */, F4136998299179B1002CE8D3 /* Main.storyboard */, F4C18A4A28491B8500335EC7 /* Preview Content */, ); path = VirtualBuddyGuest; sourceTree = ""; }; F4C18A4A28491B8500335EC7 /* Preview Content */ = { isa = PBXGroup; children = ( F4C18A4B28491B8500335EC7 /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; F4C2374E2888AF5B001FF286 /* Utilities */ = { isa = PBXGroup; children = ( F4C2374F2888AF67001FF286 /* LogStreamer.swift */, F4C2374C2888A462001FF286 /* VolumeUtils.swift */, F4510A772AE2A16F00E24DD9 /* WeakReference.swift */, F485B91C2BB2F0D9004B3C2B /* ProcessInfo+ECID.swift */, F485B91E2BB2F4AC004B3C2B /* Bundle+Version.swift */, F444D1332BB478AD00AB786F /* VBMemoryLeakDebugAssertions.swift */, F453C4BA2DF231B7007EAD5F /* PreventTerminationAssertion.swift */, ); path = Utilities; sourceTree = ""; }; F4CD131E2E05A5760067DC75 /* Components */ = { isa = PBXGroup; children = ( F4CD13432E05AD400067DC75 /* VerticalLabeledContentStyle.swift */, F4CD131F2E05A5780067DC75 /* FileSystemPathFormControl.swift */, F4CD133B2E05A9DF0067DC75 /* OpenVirtualBuddySettingsAction.swift */, F4CD133D2E05AB280067DC75 /* BackwardsCompatibility.swift */, F4CD13472E05B67E0067DC75 /* SettingsFooter.swift */, ); path = Components; sourceTree = ""; }; F4D3059529B8D9C70006E748 /* WireProtocol */ = { isa = PBXGroup; children = ( F4D3059629B8D9D30006E748 /* WormholePacket.swift */, F4D305B129B907A10006E748 /* WHPing.swift */, F42862362AE947C90052F029 /* WHPayload.swift */, ); path = WireProtocol; sourceTree = ""; }; F4D3059D29B8DB700006E748 /* VirtualWormholeTests */ = { isa = PBXGroup; children = ( F4D305A829B8E70A0006E748 /* Resources */, F4D3059E29B8DB700006E748 /* WormholePacketTests.swift */, ); path = VirtualWormholeTests; sourceTree = ""; }; F4D305A829B8E70A0006E748 /* Resources */ = { isa = PBXGroup; children = ( F4D305A929B8E7120006E748 /* TestStream.bin */, ); path = Resources; sourceTree = ""; }; F4D305AD29B9006E0006E748 /* DarwinNotifications */ = { isa = PBXGroup; children = ( F4D305AB29B8FEE90006E748 /* WHDarwinNotificationsService.swift */, F4D305AF29B900860006E748 /* SystemNotification.swift */, ); path = DarwinNotifications; sourceTree = ""; }; F4D305AE29B900790006E748 /* Base */ = { isa = PBXGroup; children = ( F4C189FE2848FB3F00335EC7 /* WormholeServiceProtocol.swift */, F4FC276629BBAE350012CB65 /* WormholeServiceClient.swift */, ); path = Base; sourceTree = ""; }; F4E4F71D2DF080D200B3B8BA /* Restore Image Selection */ = { isa = PBXGroup; children = ( F4E4F71E2DF080DF00B3B8BA /* Components */, F4E4F71F2DF080FC00B3B8BA /* RestoreImageSelectionController.swift */, F48E0D16288882BD0080DDFA /* RestoreImageSelectionStep.swift */, ); path = "Restore Image Selection"; sourceTree = ""; }; F4E4F71E2DF080DF00B3B8BA /* Components */ = { isa = PBXGroup; children = ( F444D0CB2DF322B50086537A /* SoftwareCatalog+Placeholder.swift */, F47BCDD02C5C06C900165191 /* CatalogGroupPicker.swift */, F47BCDD82C5D2EDB00165191 /* RestoreImageBrowser.swift */, F47BCDCE2C5C023900165191 /* CatalogGroupView.swift */, F444D0CD2DF32E100086537A /* BlurHashFullBleedBackground.swift */, F444D0F52DF37D170086537A /* VirtualBuddyMonoIcon.swift */, F444D0FB2DF37EF80086537A /* VirtualBuddyMonoProgressView.swift */, F444D0F72DF37D410086537A /* VirtualDisplayView.swift */, F444D0F92DF37DFB0086537A /* InstallProgressDisplayView.swift */, ); path = Components; sourceTree = ""; }; F4E7680B29B651180075A897 /* Resources */ = { isa = PBXGroup; children = ( F444D0C92DF321CD0086537A /* CatalogGroupPlaceholder.heic */, F4E7680C29B651220075A897 /* VirtualUI.xcassets */, ); path = Resources; sourceTree = ""; }; F4E7DF832BB30D8200C459FC /* Screenshot */ = { isa = PBXGroup; children = ( F4E7DF912BB3338900C459FC /* NSImage+HEIC.swift */, F49B832A2E04593100395F87 /* NSImage+DRMProtected.swift */, ); path = Screenshot; sourceTree = ""; }; F4E7DF932BB336E000C459FC /* SavedState */ = { isa = PBXGroup; children = ( F485B91A2BB22D2D004B3C2B /* VBSavedStateMetadata.swift */, F4E7DF942BB336F600C459FC /* VBSavedStatePackage.swift */, F4DE1C0A2D6F54E000603527 /* VBSavedStateMetadata+Clone.swift */, F4DE1C0E2D6F603300603527 /* VBSavedStatePackage+VM.swift */, F4E7DF962BB33E1700C459FC /* VMLibraryController+SavedState.swift */, ); path = SavedState; sourceTree = ""; }; F4F9B414284CE0DC00F21737 /* Settings */ = { isa = PBXGroup; children = ( F4F9B415284CE0F900F21737 /* VBSettings.swift */, F4F9B417284CE12000F21737 /* VBSettingsContainer.swift */, F45502152DF45E4D005582A4 /* VBSettings+CatalogDownload.swift */, ); path = Settings; sourceTree = ""; }; F4FC276829BBAE470012CB65 /* Implementation */ = { isa = PBXGroup; children = ( F4BE581229BA6E0D00C5525C /* DefaultsDomainDescriptor.swift */, F4BE583F29BA7B7A00C5525C /* DefaultsDomain+ExportImport.swift */, F4BE581029BA6DFC00C5525C /* DefaultsImportController.swift */, ); path = Implementation; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ F43B01102AD858FE00164CD1 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( F43B01182AD858FE00164CD1 /* DeepLinkSecurity.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; F498ACF92884BF13006F1C00 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( F413696B29916F6E002CE8D3 /* NSStatusItem+.h in Headers */, F498AD012884BF13006F1C00 /* VirtualUI.h in Headers */, F413696C29916F6E002CE8D3 /* NSApplication+MenuBar.h in Headers */, F413696D29916F6E002CE8D3 /* NSStatusBarPrivate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; F4BE9C6027FF053A00B648F8 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( F4BE9C6827FF053A00B648F8 /* VirtualCore.h in Headers */, F485B9222BB306AF004B3C2B /* VBDebugUtil.h in Headers */, F4BE9C8627FF140F00B648F8 /* VirtualizationPrivate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; F4C189DB2848F59F00335EC7 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( F4C189E32848F59F00335EC7 /* VirtualWormhole.h in Headers */, F49B83012E034F6800395F87 /* NSImage+DesktopPicture.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ F41369AA29918576002CE8D3 /* VirtualBuddyGuestHelper */ = { isa = PBXNativeTarget; buildConfigurationList = F41369B729918576002CE8D3 /* Build configuration list for PBXNativeTarget "VirtualBuddyGuestHelper" */; buildPhases = ( F41369A729918576002CE8D3 /* Sources */, F41369A829918576002CE8D3 /* Frameworks */, F41369A929918576002CE8D3 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = VirtualBuddyGuestHelper; productName = VirtualBuddyGuestHelper; productReference = F41369AB29918576002CE8D3 /* VirtualBuddyGuestHelper.app */; productType = "com.apple.product-type.application"; }; F43B01142AD858FE00164CD1 /* DeepLinkSecurity */ = { isa = PBXNativeTarget; buildConfigurationList = F43B01212AD858FE00164CD1 /* Build configuration list for PBXNativeTarget "DeepLinkSecurity" */; buildPhases = ( F43B01102AD858FE00164CD1 /* Headers */, F43B01112AD858FE00164CD1 /* Sources */, F43B01122AD858FE00164CD1 /* Frameworks */, F43B01132AD858FE00164CD1 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = DeepLinkSecurity; productName = DeepLinkSecurity; productReference = F43B01152AD858FE00164CD1 /* DeepLinkSecurity.framework */; productType = "com.apple.product-type.framework"; }; F498ACFD2884BF13006F1C00 /* VirtualUI */ = { isa = PBXNativeTarget; buildConfigurationList = F498AD082884BF13006F1C00 /* Build configuration list for PBXNativeTarget "VirtualUI" */; buildPhases = ( F498ACF92884BF13006F1C00 /* Headers */, F498ACFA2884BF13006F1C00 /* Sources */, F498ACFB2884BF13006F1C00 /* Frameworks */, F498ACFC2884BF13006F1C00 /* Resources */, ); buildRules = ( ); dependencies = ( F4510A7A2AE2B3AB00E24DD9 /* PBXTargetDependency */, F498AD0B2884BF60006F1C00 /* PBXTargetDependency */, ); name = VirtualUI; packageProductDependencies = ( F4A7FB722BB7238A00E4C12A /* SwiftUIIntrospect-Static */, F453C4622DF0E609007EAD5F /* BuddyKit */, ); productName = VirtualUI; productReference = F498ACFE2884BF13006F1C00 /* VirtualUI.framework */; productType = "com.apple.product-type.framework"; }; F4BE9C4D27FF052100B648F8 /* VirtualBuddy */ = { isa = PBXNativeTarget; buildConfigurationList = F4BE9C5D27FF052100B648F8 /* Build configuration list for PBXNativeTarget "VirtualBuddy" */; buildPhases = ( F4BE9C4A27FF052100B648F8 /* Sources */, F4BE9C4B27FF052100B648F8 /* Frameworks */, F4BE9C4C27FF052100B648F8 /* Resources */, F4BE9C7027FF053A00B648F8 /* Embed Frameworks */, F443620B29B79A5800745B43 /* Embed Guest App */, F453C4282DF0B65B007EAD5F /* Create Command-Line Tool Symlinks */, ); buildRules = ( ); dependencies = ( F498AD032884BF13006F1C00 /* PBXTargetDependency */, F4C18A5828491C0D00335EC7 /* PBXTargetDependency */, ); name = VirtualBuddy; packageProductDependencies = ( F4D0F71728674E4B004D5782 /* Sparkle */, F43B01462AD85A7D00164CD1 /* URLQueryItemCoder */, F453C4112DF0B1ED007EAD5F /* BuddyKit */, F453C42A2DF0B792007EAD5F /* ArgumentParser */, F453C4492DF0B7F6007EAD5F /* FragmentZip */, ); productName = VirtualBuddy; productReference = F4BE9C4E27FF052100B648F8 /* VirtualBuddy.app */; productType = "com.apple.product-type.application"; }; F4BE9C6427FF053A00B648F8 /* VirtualCore */ = { isa = PBXNativeTarget; buildConfigurationList = F4BE9C6D27FF053A00B648F8 /* Build configuration list for PBXNativeTarget "VirtualCore" */; buildPhases = ( F4BE9C6027FF053A00B648F8 /* Headers */, F4BE9C6127FF053A00B648F8 /* Sources */, F4BE9C6227FF053A00B648F8 /* Frameworks */, F4BE9C6327FF053A00B648F8 /* Resources */, F47BCD9C2C5BE89E00165191 /* Copy Software Catalog Feeds */, F4A7FB4C2BB5F0B700E4C12A /* Copy Preview Library */, F45502222DF46386005582A4 /* Copy Preview Library Downloads */, F4A7FB462BB5ED6400E4C12A /* Copy Preview Saved States */, F4A277142BF51C480011B626 /* Strip Preview Content in Release Builds */, ); buildRules = ( ); dependencies = ( F453C3ED2DF0A4D1007EAD5F /* PBXTargetDependency */, F4C189F02848F5F100335EC7 /* PBXTargetDependency */, ); name = VirtualCore; productName = VirtualCore; productReference = F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */; productType = "com.apple.product-type.framework"; }; F4C189DF2848F59F00335EC7 /* VirtualWormhole */ = { isa = PBXNativeTarget; buildConfigurationList = F4C189EB2848F59F00335EC7 /* Build configuration list for PBXNativeTarget "VirtualWormhole" */; buildPhases = ( F4C189DB2848F59F00335EC7 /* Headers */, F4C189DC2848F59F00335EC7 /* Sources */, F4C189DD2848F59F00335EC7 /* Frameworks */, F4C189DE2848F59F00335EC7 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = VirtualWormhole; packageProductDependencies = ( ); productName = VirtualWormhole; productReference = F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */; productType = "com.apple.product-type.framework"; }; F4C18A4128491B8500335EC7 /* VirtualBuddyGuest */ = { isa = PBXNativeTarget; buildConfigurationList = F4C18A4E28491B8500335EC7 /* Build configuration list for PBXNativeTarget "VirtualBuddyGuest" */; buildPhases = ( F4552F4729BA60C7002A21D8 /* Set VBGuestBuildID in Info.plist */, F4C18A3E28491B8500335EC7 /* Sources */, F4C18A3F28491B8500335EC7 /* Frameworks */, F4C18A4028491B8500335EC7 /* Resources */, F4C18A5628491B9D00335EC7 /* Embed Frameworks */, F41369BE2991861C002CE8D3 /* Embed Login Item */, ); buildRules = ( ); dependencies = ( F41369BD29918617002CE8D3 /* PBXTargetDependency */, F4C18A5528491B9D00335EC7 /* PBXTargetDependency */, F413699D299179F8002CE8D3 /* PBXTargetDependency */, F42862312AE87D7E0052F029 /* PBXTargetDependency */, F453C4272DF0B602007EAD5F /* PBXTargetDependency */, ); name = VirtualBuddyGuest; productName = VirtualBuddyGuest; productReference = F4C18A4228491B8500335EC7 /* VirtualBuddyGuest.app */; productType = "com.apple.product-type.application"; }; F4D3059B29B8DB700006E748 /* VirtualWormholeTests */ = { isa = PBXNativeTarget; buildConfigurationList = F4D305A329B8DB700006E748 /* Build configuration list for PBXNativeTarget "VirtualWormholeTests" */; buildPhases = ( F4D3059829B8DB700006E748 /* Sources */, F4D3059929B8DB700006E748 /* Frameworks */, F4D3059A29B8DB700006E748 /* Resources */, ); buildRules = ( ); dependencies = ( F4D305A229B8DB700006E748 /* PBXTargetDependency */, ); name = VirtualWormholeTests; productName = VirtualWormholeTests; productReference = F4D3059C29B8DB700006E748 /* VirtualWormholeTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F4BE9C4627FF052100B648F8 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1640; LastUpgradeCheck = 1640; TargetAttributes = { F41369AA29918576002CE8D3 = { CreatedOnToolsVersion = 14.2; LastSwiftMigration = 1420; }; F43B01142AD858FE00164CD1 = { CreatedOnToolsVersion = 15.1; }; F453C44F2DF0BCE3007EAD5F = { CreatedOnToolsVersion = 16.4; }; F498ACFD2884BF13006F1C00 = { CreatedOnToolsVersion = 14.0; LastSwiftMigration = 1400; }; F4BE9C4D27FF052100B648F8 = { CreatedOnToolsVersion = 13.3; LastSwiftMigration = 1330; }; F4BE9C6427FF053A00B648F8 = { CreatedOnToolsVersion = 13.3; LastSwiftMigration = 1530; }; F4C189DF2848F59F00335EC7 = { CreatedOnToolsVersion = 13.4; LastSwiftMigration = 2600; }; F4C18A4128491B8500335EC7 = { CreatedOnToolsVersion = 13.4; LastSwiftMigration = 1420; }; F4D3059B29B8DB700006E748 = { CreatedOnToolsVersion = 14.2; }; }; }; buildConfigurationList = F4BE9C4927FF052100B648F8 /* Build configuration list for PBXProject "VirtualBuddy" */; compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F4BE9C4527FF052100B648F8; packageReferences = ( F4D0F71628674E4B004D5782 /* XCRemoteSwiftPackageReference "Sparkle" */, F43B01452AD85A7D00164CD1 /* XCRemoteSwiftPackageReference "URLQueryItemCoder" */, F4A7FB712BB7238A00E4C12A /* XCRemoteSwiftPackageReference "swiftui-introspect" */, F453C3D82DF0A426007EAD5F /* XCRemoteSwiftPackageReference "BuddyKit" */, F453C4012DF0AEF5007EAD5F /* XCRemoteSwiftPackageReference "swift-argument-parser" */, F453C4482DF0B7F6007EAD5F /* XCRemoteSwiftPackageReference "libfragmentzip" */, ); productRefGroup = F4BE9C4F27FF052100B648F8 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F4BE9C4D27FF052100B648F8 /* VirtualBuddy */, F4C18A4128491B8500335EC7 /* VirtualBuddyGuest */, F41369AA29918576002CE8D3 /* VirtualBuddyGuestHelper */, F43B01142AD858FE00164CD1 /* DeepLinkSecurity */, F4BE9C6427FF053A00B648F8 /* VirtualCore */, F498ACFD2884BF13006F1C00 /* VirtualUI */, F4D3059B29B8DB700006E748 /* VirtualWormholeTests */, F4C189DF2848F59F00335EC7 /* VirtualWormhole */, F453C44F2DF0BCE3007EAD5F /* vctool */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F41369A929918576002CE8D3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F41369B229918576002CE8D3 /* Assets.xcassets in Resources */, F41369B529918576002CE8D3 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; F43B01132AD858FE00164CD1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; F498ACFC2884BF13006F1C00 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F444D0CA2DF321CD0086537A /* CatalogGroupPlaceholder.heic in Resources */, F4E7680D29B651220075A897 /* VirtualUI.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; F4BE9C4C27FF052100B648F8 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F4BE9C5927FF052100B648F8 /* Preview Assets.xcassets in Resources */, F4BE9C5627FF052100B648F8 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; F4BE9C6327FF053A00B648F8 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F453C4922DF1D213007EAD5F /* FakeRestoreImage.ipsw in Resources */, F453C45D2DF0D28A007EAD5F /* README.md in Resources */, F417257428877478004FF8A7 /* VirtualCore.xcassets in Resources */, F443620F29B7A0C600745B43 /* CreateGuestImage.sh in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; F4C189DE2848F59F00335EC7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F4BE580E29BA6C8D00C5525C /* DefaultsDomains.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; F4C18A4028491B8500335EC7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F4136999299179B1002CE8D3 /* Main.storyboard in Resources */, F4C18A4C28491B8500335EC7 /* Preview Assets.xcassets in Resources */, F4C18A4928491B8500335EC7 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; F4D3059A29B8DB700006E748 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F4D305AA29B8E7120006E748 /* TestStream.bin in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ F453C4282DF0B65B007EAD5F /* Create Command-Line Tool Symlinks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "$(BUILT_PRODUCTS_DIR)/$(EXECUTABLE_PATH)", ); name = "Create Command-Line Tool Symlinks"; outputFileListPaths = ( ); outputPaths = ( "$(BUILT_PRODUCTS_DIR)/$(CONTENTS_FOLDER_PATH)/MacOS/vctool", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "# Go into the VirtualBuddy.app/Contents/MacOS path\ncd \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/MacOS\"\n\n# Symlink VirtualBuddy as vctool for VirtualCatalog command-line tool\nln -fs $EXECUTABLE_NAME vctool\n"; }; F4552F4729BA60C7002A21D8 /* Set VBGuestBuildID in Info.plist */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Set VBGuestBuildID in Info.plist"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/VBGenerated-Info.plist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "# Ensures VirtualBuddyGuest has a VBGuestBuildID entry in its Info.plist file\n# This entry is used by the app itself when running in a VM to determine when it needs to be updated.\nTEMPLATE=\"\"\nPLISTPATH=\"$DERIVED_FILE_DIR/VBGenerated-Info.plist\"\n\necho $TEMPLATE > \"$PLISTPATH\"\n\n/usr/libexec/PlistBuddy -c \"Add :VBGuestBuildID string xxxxxx\" \"$PLISTPATH\" 2>/dev/null || echo \"\"\n\n/usr/libexec/PlistBuddy -c \"Set :VBGuestBuildID `uuidgen`\" \"$PLISTPATH\"\n"; }; F4A277142BF51C480011B626 /* Strip Preview Content in Release Builds */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 8; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Strip Preview Content in Release Builds"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; shellScript = "if [ \"${CONFIGURATION}\" = \"Release_Managed\" ] || [ \"${CONFIGURATION}\" = \"Dev_Release\" ] ]; then\n PREVIEW_CONTENT_PATH=\"$CODESIGNING_FOLDER_PATH/Resources/PreviewLibrary\"\n \n if [ -d \"$PREVIEW_CONTENT_PATH\" ]; then\n echo \"Stripping VirtualCore preview content from release build at $PREVIEW_CONTENT_PATH\"\n rm -r \"$PREVIEW_CONTENT_PATH\"\n fi\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F41369A729918576002CE8D3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F41369AE29918576002CE8D3 /* GuestHelperAppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F43B01112AD858FE00164CD1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F43B013E2AD8590F00164CD1 /* DeepLinkManagementStore.swift in Sources */, F43B01392AD8590F00164CD1 /* DeepLinkClient+Crypto.swift in Sources */, F43B013C2AD8590F00164CD1 /* KeychainDeepLinkAuthStore.swift in Sources */, F43B013A2AD8590F00164CD1 /* DeepLinkClientDescriptor+.swift in Sources */, F43B01352AD8590F00164CD1 /* DeepLinkAuthUI.swift in Sources */, F43B01402AD8590F00164CD1 /* DeepLinkAuthStore.swift in Sources */, F43B01362AD8590F00164CD1 /* DeepLinkSentinel.swift in Sources */, F43B01372AD8590F00164CD1 /* OpenDeepLinkRequest.swift in Sources */, F43B013F2AD8590F00164CD1 /* UserDefaultsDeepLinkManagementStore.swift in Sources */, F43B013D2AD8590F00164CD1 /* MemoryDeepLinkAuthStore.swift in Sources */, F43B01382AD8590F00164CD1 /* DeepLinkClientDescriptor.swift in Sources */, F43B01412AD8590F00164CD1 /* DeepLinkSecurityDefines.swift in Sources */, F43B013B2AD8590F00164CD1 /* DeepLinkClient.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F498ACFA2884BF13006F1C00 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F417256128861A05004FF8A7 /* SharingConfigurationView.swift in Sources */, F413696A29916F6E002CE8D3 /* StatusBarHighlightView.swift in Sources */, F47BCDD92C5D2EE300165191 /* RestoreImageBrowser.swift in Sources */, F498AD0E2884BF9D006F1C00 /* VMConfigurationView.swift in Sources */, F48E0D25288882E50080DDFA /* OnAppearOnce.swift in Sources */, F413696429916F6E002CE8D3 /* StatusItemProviderProtocol.swift in Sources */, F422587E2885E2ED009420AE /* HardwareConfigurationView.swift in Sources */, F444D0CC2DF322B90086537A /* SoftwareCatalog+Placeholder.swift in Sources */, F4CD134A2E05CB390067DC75 /* AutomationSettingsView.swift in Sources */, F4FC98392BB386A000E511C9 /* ContinuousProgressIndicator.swift in Sources */, F4E4F7202DF080FC00B3B8BA /* RestoreImageSelectionController.swift in Sources */, F48E0D1D288882BD0080DDFA /* AuthenticatingWebView.swift in Sources */, F4CD13442E05AD400067DC75 /* VerticalLabeledContentStyle.swift in Sources */, F47BCDCF2C5C023E00165191 /* CatalogGroupView.swift in Sources */, F413696529916F6E002CE8D3 /* StatusItemPanelContentController.swift in Sources */, F4E7DFCF2BB3587D00C459FC /* VirtualMachineSessionUIManager.swift in Sources */, F4CD13462E05B4DE0067DC75 /* VirtualizationSettingsView.swift in Sources */, F4CD13202E05A5780067DC75 /* FileSystemPathFormControl.swift in Sources */, F417CBB92E0F3D2E0065B5D6 /* FirstLaunchExperienceView.swift in Sources */, F4561A6828981B4100055289 /* VirtualMachineNameInputView.swift in Sources */, F413696229916F6E002CE8D3 /* StatusItemButton.swift in Sources */, F49AA2C529BA31CC009625F7 /* VirtualMachineSessionUI.swift in Sources */, F48E0D34288889E60080DDFA /* InstallConfigurationStepView.swift in Sources */, F444D0F82DF37D410086537A /* VirtualDisplayView.swift in Sources */, F42C015B2888FC0C00EB15CD /* VMSessionConfigurationView.swift in Sources */, F42C015D2888FC0C00EB15CD /* SwiftUIVMView.swift in Sources */, F48E0D23288882E50080DDFA /* DecentFormView.swift in Sources */, F417256F2887544A004FF8A7 /* StorageDeviceDetailView.swift in Sources */, F498AD142884C36A006F1C00 /* NumericValueField.swift in Sources */, F417255D288604A8004FF8A7 /* SoundConfigurationView.swift in Sources */, F48E0D1E288882BD0080DDFA /* VMInstallationWizard.swift in Sources */, F462C9422E0C96D300C172E2 /* FB18383725Window.swift in Sources */, F48E0D03288858E00080DDFA /* ManagedDiskImageEditor.swift in Sources */, F413696F29916F6E002CE8D3 /* NSStatusItem+.m in Sources */, F417CB882E0EDECD0065B5D6 /* BackportedContentUnavailableView.swift in Sources */, F453C4B42DF20301007EAD5F /* RestoreImageURLInputView.swift in Sources */, F42C015A2888FC0C00EB15CD /* LibraryView.swift in Sources */, F444D0FA2DF37DFB0086537A /* InstallProgressDisplayView.swift in Sources */, F413696329916F6E002CE8D3 /* StatusBarPanelChrome.swift in Sources */, F49AA2C729BA3F2B009625F7 /* VBRestorableWindow+Resizing.swift in Sources */, F47BCDCD2C5C01EF00165191 /* RemoteImage.swift in Sources */, F4CD133E2E05AB280067DC75 /* BackwardsCompatibility.swift in Sources */, F4E7680A29B64C590075A897 /* GuestTypePicker.swift in Sources */, F4B5C5DB28873628005AA632 /* GroupedList.swift in Sources */, F4FC983B2BB386B500E511C9 /* MaskProgressView.swift in Sources */, F417256C2887500F004FF8A7 /* StorageConfigurationView.swift in Sources */, F42C014C2888C34B00EB15CD /* LogConsole.swift in Sources */, F49B832D2E046B8D00395F87 /* GuestAppConfigurationView.swift in Sources */, F42C015C2888FC0C00EB15CD /* VirtualMachineSessionView.swift in Sources */, F498AD182884C593006F1C00 /* NumericPropertyControl.swift in Sources */, F4CD133C2E05A9DF0067DC75 /* OpenVirtualBuddySettingsAction.swift in Sources */, F4CD13402E05AB8F0067DC75 /* GeneralSettingsView.swift in Sources */, F413696729916F6E002CE8D3 /* VUIAppKitViewControllerHost.swift in Sources */, F4450CCA2ACB0DB500092618 /* KeyboardDeviceConfigurationView.swift in Sources */, F444D0FC2DF37EF80086537A /* VirtualBuddyMonoProgressView.swift in Sources */, F444D0CE2DF32E100086537A /* BlurHashFullBleedBackground.swift in Sources */, F42C01612888FC3500EB15CD /* LibraryItemView.swift in Sources */, F41725682886E5AD004FF8A7 /* MaterialView.swift in Sources */, F413696E29916F6E002CE8D3 /* NSApplication+MenuBar.m in Sources */, F48E0D2B288883150080DDFA /* HostingWindowController.swift in Sources */, F47BCDCB2C5C01D100165191 /* BlurHashDecoding.swift in Sources */, F42258802885E71D009420AE /* PropertyControl.swift in Sources */, F4E7680F29B655DD0075A897 /* InstallMethod.swift in Sources */, F42C014A2888C2F800EB15CD /* InstallationConsole.swift in Sources */, F444D0F42DF34BE80086537A /* CALayer+Asset.swift in Sources */, F413696829916F6E002CE8D3 /* StatusItemMenuBarExtraView.swift in Sources */, F453C4B92DF21985007EAD5F /* VMInstallData.swift in Sources */, F48E0D24288882E50080DDFA /* NSAlert+Confirmation.swift in Sources */, F42258742885E10B009420AE /* VMConfigurationViewModel.swift in Sources */, F48E0D1A288882BD0080DDFA /* InstallationWizardTitle.swift in Sources */, F41725632886DD37004FF8A7 /* SharedFolderListItem.swift in Sources */, F42258782885E14A009420AE /* DisplayConfigurationView.swift in Sources */, F413697029916F6E002CE8D3 /* StatusItemManager.swift in Sources */, F48E0D1C288882BD0080DDFA /* RestoreImageSelectionStep.swift in Sources */, F47BCDD52C5C0B8F00165191 /* Array+Navigation.swift in Sources */, F42C014E2888CBCB00EB15CD /* InstallProgressStepView.swift in Sources */, F428622D2AE8726D0052F029 /* VirtualMachineControls.swift in Sources */, F413696929916F6E002CE8D3 /* StatusBarContentPanel.swift in Sources */, F4FC983D2BB386DD00E511C9 /* VMProgressOverlay.swift in Sources */, F48E0D2A288883150080DDFA /* OpenCocoaWindowAction.swift in Sources */, F4E4F6C52DEF96C200B3B8BA /* ChromeBorderModifier.swift in Sources */, F41725662886DF58004FF8A7 /* OpenSavePanelUtils.swift in Sources */, F498AD162884C36A006F1C00 /* ControlGroupChrome.swift in Sources */, F48E0D2C288883150080DDFA /* WindowEnvironment.swift in Sources */, F498AD1A2884C5FF006F1C00 /* SliderConversion.swift in Sources */, F422586D2885CC9F009420AE /* SharedFocusEnvironment.swift in Sources */, F422587C2885E1CE009420AE /* NetworkConfigurationView.swift in Sources */, F41369A329917FA0002CE8D3 /* ScreenChangeModifier.swift in Sources */, F48E0D1F288882BD0080DDFA /* VMInstallationViewModel.swift in Sources */, F4ECC6D52C63BFD5001DAC1D /* NumberDisplayMode.swift in Sources */, F4CD13482E05B67E0067DC75 /* SettingsFooter.swift in Sources */, F42258702885D537009420AE /* EphemeralTextField.swift in Sources */, F417CB8A2E0EDF1A0065B5D6 /* EqualWidthHStack.swift in Sources */, F49AA2C329BA22A5009625F7 /* VBRestorableWindow.swift in Sources */, F422587A2885E17D009420AE /* ConfigurationSection.swift in Sources */, F4B5C5D32886FA8D005AA632 /* SharedFoldersManagementView.swift in Sources */, F48E0D32288884A10080DDFA /* RestoreImageDownloadView.swift in Sources */, F4B5C5D52886FFB5005AA632 /* PointingDeviceConfigurationView.swift in Sources */, F49B82982E02F5A900395F87 /* VMArtworkView.swift in Sources */, F48E0D2F2888835A0080DDFA /* VirtualUIConstants.swift in Sources */, F47BCDD32C5C0AB300165191 /* KeyboardNavigationModifier.swift in Sources */, F444D0F62DF37D170086537A /* VirtualBuddyMonoIcon.swift in Sources */, F4A7FB6E2BB7206C00E4C12A /* SelfSizingGroupedForm.swift in Sources */, F47BCDD12C5C06CE00165191 /* CatalogGroupPicker.swift in Sources */, F4A7FB752BB7252A00E4C12A /* PreviewSupport-VirtualUI.swift in Sources */, F48E0D1B288882BD0080DDFA /* InstallMethodPicker.swift in Sources */, F42258722885E100009420AE /* VMConfigurationSheet.swift in Sources */, F4A7FB3F2BB5EBEF00E4C12A /* SavedStatePicker.swift in Sources */, F413697929917135002CE8D3 /* CGFloat+OnePixel.swift in Sources */, F42C015E2888FC0C00EB15CD /* SettingsScreen.swift in Sources */, F45502142DF394DC005582A4 /* VirtualBuddyInstallerInputView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F4BE9C4A27FF052100B648F8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F43B01442AD85A6500164CD1 /* VirtualBuddyDeepLinks.swift in Sources */, F43B014B2AD85ABB00164CD1 /* DeepLinkAuthDialog.swift in Sources */, F4D0F71A28674E76004D5782 /* SoftwareUpdateController.swift in Sources */, F4BE9C8127FF111100B648F8 /* VirtualBuddyAppDelegate.swift in Sources */, F453C43B2DF0B7A5007EAD5F /* Helpers.swift in Sources */, F453C43C2DF0B7A5007EAD5F /* VCTool.swift in Sources */, F453C43D2DF0B7A5007EAD5F /* URL+ContentLength.swift in Sources */, F453C43E2DF0B7A5007EAD5F /* IPSWCommand.swift in Sources */, F453C43F2DF0B7A5007EAD5F /* ImageCommand.swift in Sources */, F453C4402DF0B7A5007EAD5F /* BuildManifest.swift in Sources */, F453C4412DF0B7A5007EAD5F /* GroupCommand.swift in Sources */, F453C4422DF0B7A5007EAD5F /* MobileDeviceCommand.swift in Sources */, F453C4432DF0B7A5007EAD5F /* BuildManifest+Fetch.swift in Sources */, F453C4682DF10181007EAD5F /* BlurHashCommand.swift in Sources */, F453C44E2DF0B870007EAD5F /* VirtualBuddyCLI.swift in Sources */, F453C4442DF0B7A5007EAD5F /* TreeStringConvertible.swift in Sources */, F453C4452DF0B7A5007EAD5F /* MigrateCommand.swift in Sources */, F453C4462DF0B7A5007EAD5F /* ResolveCommand.swift in Sources */, F453C4472DF0B7A5007EAD5F /* CatalogCommand.swift in Sources */, F4BE9C5227FF052100B648F8 /* VirtualBuddyApp.swift in Sources */, F43B014E2AD86BFA00164CD1 /* DeepLinkHandler.swift in Sources */, F453C4232DF0B5B1007EAD5F /* VirtualBuddyEntryPoint.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F4BE9C6127FF053A00B648F8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F4E7DF922BB3338900C459FC /* NSImage+HEIC.swift in Sources */, F417257128877121004FF8A7 /* DiskImageGenerator.swift in Sources */, F4F9B416284CE0F900F21737 /* VBSettings.swift in Sources */, F42CF4A82DF5FEC3001DE049 /* BlurHashToken.swift in Sources */, F41725762887758A004FF8A7 /* RandomNameGenerator.swift in Sources */, F45502162DF45E53005582A4 /* VBSettings+CatalogDownload.swift in Sources */, F44C00FB2889CE1600640BF5 /* VBVirtualMachine+Virtualization.swift in Sources */, F453C4892DF1CDA0007EAD5F /* DownloadBackend.swift in Sources */, F4C947DA2E0B1E5D001ACC91 /* SoftwareCatalog+DownloadMatching.swift in Sources */, F4F9B418284CE12000F21737 /* VBSettingsContainer.swift in Sources */, F49B832B2E04593A00395F87 /* NSImage+DRMProtected.swift in Sources */, F4E7DF972BB33E1700C459FC /* VMLibraryController+SavedState.swift in Sources */, F48E0D0C2888760D0080DDFA /* VBMacDevice+Storage.swift in Sources */, F465C3AE284F93A5006E9ED4 /* VBAPIClient.swift in Sources */, F453C4A22DF1D7F6007EAD5F /* SimulatedRestoreBackend.swift in Sources */, F4DE1C0B2D6F54E700603527 /* VBSavedStateMetadata+Clone.swift in Sources */, F4D725FE286677B8001818F7 /* VBVirtualMachine+Metadata.swift in Sources */, F4A21BF428033102001072B8 /* VBError.swift in Sources */, F4D0F71F2867517A004D5782 /* AppUpdateChannel.swift in Sources */, F49FD8842DFB727B0019D638 /* VMImporter+Helpers.swift in Sources */, F453C49B2DF1D768007EAD5F /* SimulatedDownloadBackend.swift in Sources */, 4BA6BE7D293D22E500F396AE /* VirtualMachineConfigurationHelper.swift in Sources */, F48E0D0728885E140080DDFA /* PreviewSupport.swift in Sources */, F46FFBAA2804F0A000D61023 /* VZVirtualMachineConfiguration+NVRAM.swift in Sources */, F4C189F72848F6A600335EC7 /* VirtualCoreConstants.swift in Sources */, F46FFBA82804F07400D61023 /* VBNVRAMVariable.swift in Sources */, F453C4A42DF1D861007EAD5F /* VirtualizationRestoreBackend.swift in Sources */, F4BE9C7A27FF05B900B648F8 /* VMController.swift in Sources */, F453C4BB2DF231BB007EAD5F /* PreventTerminationAssertion.swift in Sources */, F417255F28861604004FF8A7 /* DecodableDefault.swift in Sources */, F485B91B2BB22D2D004B3C2B /* VBSavedStateMetadata.swift in Sources */, F465C3B8284FA252006E9ED4 /* URLSessionDownloadBackend.swift in Sources */, F4DE1C0F2D6F603300603527 /* VBSavedStatePackage+VM.swift in Sources */, F4A7FB3D2BB5E8A200E4C12A /* VMSavedStatesController.swift in Sources */, F49FD87D2DFB68F50019D638 /* VMImporter.swift in Sources */, F4A21BF228032FD8001072B8 /* VMLibraryController.swift in Sources */, F485B91F2BB2F4AC004B3C2B /* Bundle+Version.swift in Sources */, F49A68E12884917E00A17582 /* ConfigurationModels.swift in Sources */, F485B91D2BB2F0D9004B3C2B /* ProcessInfo+ECID.swift in Sources */, F444D1342BB478AD00AB786F /* VBMemoryLeakDebugAssertions.swift in Sources */, F4C237502888AF67001FF286 /* LogStreamer.swift in Sources */, F4F9B41A284CE37C00F21737 /* Logging.swift in Sources */, F4B5C5D728870619005AA632 /* ConfigurationModels+Validation.swift in Sources */, F4A7FB3B2BB5E79100E4C12A /* DirectoryObserver.swift in Sources */, F49FD8862DFB728A0019D638 /* UTMAppleConfiguration.swift in Sources */, F4B5C5D928870BBF005AA632 /* ConfigurationModels+Summary.swift in Sources */, F4C2374D2888A462001FF286 /* VolumeUtils.swift in Sources */, F4BE9C7627FF055100B648F8 /* VBVirtualMachine.swift in Sources */, F4D0F71528667984004D5782 /* VBVirtualMachine+Screenshot.swift in Sources */, F49FD87F2DFB6B670019D638 /* VMImporterRegistry.swift in Sources */, F49FD8812DFB6CDD0019D638 /* UTMImporter.swift in Sources */, F443620A29B7947A00745B43 /* GuestAdditionsDiskImage.swift in Sources */, F485B9232BB306AF004B3C2B /* VBDebugUtil.m in Sources */, F4C947BF2E0B0F71001ACC91 /* URL+ExtendedAttributes.swift in Sources */, F47BCDD72C5D2B4600165191 /* CatalogExtensions.swift in Sources */, F4BE9C7827FF055100B648F8 /* MacOSVirtualMachineConfigurationHelper.swift in Sources */, F4510A782AE2A16F00E24DD9 /* WeakReference.swift in Sources */, F4DE1C112D6F642E00603527 /* VBStorageDeviceContainer.swift in Sources */, F46FFBAC28059FF600D61023 /* VMInstance.swift in Sources */, F4E7DF952BB336F600C459FC /* VBSavedStatePackage.swift in Sources */, F40A1E9D2C1873CA0033E47D /* VBBuildType.swift in Sources */, F465C3B0284F9660006E9ED4 /* VBRestoreImagesResponse.swift in Sources */, F453C41D2DF0B43D007EAD5F /* ResolvedCatalog.swift in Sources */, F453C41E2DF0B43D007EAD5F /* LegacyCatalog.swift in Sources */, F453C41F2DF0B43D007EAD5F /* BlurHashEncode.swift in Sources */, F453C4A02DF1D7C0007EAD5F /* RestoreBackend.swift in Sources */, F4C947D62E0B12D0001ACC91 /* String+AppleOSBuild.swift in Sources */, F453C4202DF0B43D007EAD5F /* SoftwareCatalog.swift in Sources */, F453C4212DF0B43D007EAD5F /* MobileDeviceFramework.swift in Sources */, 0196B45329292B2A00614EF1 /* LinuxVirtualMachineConfigurationHelper.swift in Sources */, F465C3B2284F9666006E9ED4 /* VBRestoreImageInfo.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F4C189DC2848F59F00335EC7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F4D305AC29B8FEE90006E748 /* WHDarwinNotificationsService.swift in Sources */, F4C189FF2848FB3F00335EC7 /* WormholeServiceProtocol.swift in Sources */, F4C189FA2848F6F700335EC7 /* VirtualWormholeConstants.swift in Sources */, F4BE584229BA7BDB00C5525C /* WHDefaultsImportService.swift in Sources */, F4D305B029B900860006E748 /* SystemNotification.swift in Sources */, F42862372AE947C90052F029 /* WHPayload.swift in Sources */, F4BE581329BA6E0D00C5525C /* DefaultsDomainDescriptor.swift in Sources */, F49B83712E04837400395F87 /* CGImage+FullyTransparent.swift in Sources */, F4BE581129BA6DFC00C5525C /* DefaultsImportController.swift in Sources */, F49B82FC2E034EAD00395F87 /* WHDesktopPictureService.swift in Sources */, F4D305B229B907A10006E748 /* WHPing.swift in Sources */, F4D3059729B8D9D30006E748 /* WormholePacket.swift in Sources */, F4BE584029BA7B7A00C5525C /* DefaultsDomain+ExportImport.swift in Sources */, F4FC276A29BBAE590012CB65 /* WHDefaultsImportClient.swift in Sources */, F4C189FD2848F8F600335EC7 /* WHSharedClipboardService.swift in Sources */, F4FC276729BBAE350012CB65 /* WormholeServiceClient.swift in Sources */, F4C189EE2848F5B500335EC7 /* WormholeManager.swift in Sources */, F49B83002E034F6800395F87 /* NSImage+DesktopPicture.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F4C18A3E28491B8500335EC7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F4959F3C2992A284001DF4CB /* GuestAppInstaller.swift in Sources */, F41369CA2991A492002CE8D3 /* HostConnectionStateProvider.swift in Sources */, F4C18A4728491B8500335EC7 /* GuestDashboard.swift in Sources */, F41369CC2991A68F002CE8D3 /* GuestSharedFoldersManager.swift in Sources */, F4C18A4528491B8500335EC7 /* GuestAppDelegate.swift in Sources */, F4FC276C29BBB3030012CB65 /* GuestDefaultsImportView.swift in Sources */, F41369A6299183C8002CE8D3 /* GuestLaunchAtLoginManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F4D3059829B8DB700006E748 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F4D3059F29B8DB700006E748 /* WormholePacketTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ F413699D299179F8002CE8D3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F4BE9C6427FF053A00B648F8 /* VirtualCore */; targetProxy = F413699C299179F8002CE8D3 /* PBXContainerItemProxy */; }; F41369BD29918617002CE8D3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F41369AA29918576002CE8D3 /* VirtualBuddyGuestHelper */; targetProxy = F41369BC29918617002CE8D3 /* PBXContainerItemProxy */; }; F42862312AE87D7E0052F029 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F43B01142AD858FE00164CD1 /* DeepLinkSecurity */; targetProxy = F42862302AE87D7E0052F029 /* PBXContainerItemProxy */; }; F4510A7A2AE2B3AB00E24DD9 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F43B01142AD858FE00164CD1 /* DeepLinkSecurity */; targetProxy = F4510A792AE2B3AB00E24DD9 /* PBXContainerItemProxy */; }; F453C3ED2DF0A4D1007EAD5F /* PBXTargetDependency */ = { isa = PBXTargetDependency; productRef = F453C3EC2DF0A4D1007EAD5F /* BuddyKit */; }; F453C4272DF0B602007EAD5F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F498ACFD2884BF13006F1C00 /* VirtualUI */; targetProxy = F453C4262DF0B602007EAD5F /* PBXContainerItemProxy */; }; F453C4592DF0BCEB007EAD5F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F4BE9C4D27FF052100B648F8 /* VirtualBuddy */; targetProxy = F453C4582DF0BCEB007EAD5F /* PBXContainerItemProxy */; }; F498AD032884BF13006F1C00 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F498ACFD2884BF13006F1C00 /* VirtualUI */; targetProxy = F498AD022884BF13006F1C00 /* PBXContainerItemProxy */; }; F498AD0B2884BF60006F1C00 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F4BE9C6427FF053A00B648F8 /* VirtualCore */; targetProxy = F498AD0A2884BF60006F1C00 /* PBXContainerItemProxy */; }; F4C189F02848F5F100335EC7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F4C189DF2848F59F00335EC7 /* VirtualWormhole */; targetProxy = F4C189EF2848F5F100335EC7 /* PBXContainerItemProxy */; }; F4C18A5528491B9D00335EC7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F4C189DF2848F59F00335EC7 /* VirtualWormhole */; targetProxy = F4C18A5428491B9D00335EC7 /* PBXContainerItemProxy */; }; F4C18A5828491C0D00335EC7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F4C18A4128491B8500335EC7 /* VirtualBuddyGuest */; targetProxy = F4C18A5728491C0D00335EC7 /* PBXContainerItemProxy */; }; F4D305A229B8DB700006E748 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F4C189DF2848F59F00335EC7 /* VirtualWormhole */; targetProxy = F4D305A129B8DB700006E748 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ F41369B329918576002CE8D3 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( F41369B429918576002CE8D3 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F40A1E872C1869680033E47D /* Beta_Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4C1224E280715F200D359E2 /* Main.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Beta_Debug; }; F40A1E882C1869680033E47D /* Beta_Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4B068B528882F5D003743BF /* AppTarget.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddy/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; "DEVELOPMENT_TEAM[sdk=macosx*]" = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VirtualBuddy/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2023 Buddy Software LTD"; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddy"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Beta_Debug; }; F40A1E892C1869680033E47D /* Beta_Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuest/VirtualBuddyGuest.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddyGuest/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/VBGenerated-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddyGuest"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "VirtualBuddyGuest/Dashboard/Support/VirtualBuddyGuest-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Beta_Debug; }; F40A1E8A2C1869680033E47D /* Beta_Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuestHelper/VirtualBuddyGuestHelper.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSBackgroundOnly = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Beta_Debug; }; F40A1E8B2C1869680033E47D /* Beta_Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 8C7439RJLG; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.DeepLinkSecurity; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Beta_Debug; }; F40A1E8C2C1869680033E47D /* Beta_Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = VirtualCore/Source/Resources/Preview; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = "-Wl,-U,_OBJC_CLASS_$__VZVirtualMachineStartOptions"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualCore"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Beta_Debug; }; F40A1E8D2C1869680033E47D /* Beta_Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualUI"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Beta_Debug; }; F40A1E8E2C1869680033E47D /* Beta_Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8C7439RJLG; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.VirtualWormholeTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; }; name = Beta_Debug; }; F40A1E8F2C1869680033E47D /* Beta_Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualWormhole"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Beta_Debug; }; F40A1E902C18697B0033E47D /* Beta_Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4C1224E280715F200D359E2 /* Main.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Beta_Release; }; F40A1E912C18697B0033E47D /* Beta_Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4B068B528882F5D003743BF /* AppTarget.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddy/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; "DEVELOPMENT_TEAM[sdk=macosx*]" = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VirtualBuddy/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2023 Buddy Software LTD"; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddy"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Beta_Release; }; F40A1E922C18697B0033E47D /* Beta_Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuest/VirtualBuddyGuest.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddyGuest/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/VBGenerated-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddyGuest"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "VirtualBuddyGuest/Dashboard/Support/VirtualBuddyGuest-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Beta_Release; }; F40A1E932C18697B0033E47D /* Beta_Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuestHelper/VirtualBuddyGuestHelper.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSBackgroundOnly = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Beta_Release; }; F40A1E942C18697B0033E47D /* Beta_Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 8C7439RJLG; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.DeepLinkSecurity; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Beta_Release; }; F40A1E952C18697B0033E47D /* Beta_Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = VirtualCore/Source/Resources/Preview; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = "-Wl,-U,_OBJC_CLASS_$__VZVirtualMachineStartOptions"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualCore"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Beta_Release; }; F40A1E962C18697B0033E47D /* Beta_Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualUI"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Beta_Release; }; F40A1E972C18697B0033E47D /* Beta_Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8C7439RJLG; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.VirtualWormholeTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; }; name = Beta_Release; }; F40A1E982C18697B0033E47D /* Beta_Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualWormhole"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Beta_Release; }; F41369B829918576002CE8D3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuestHelper/VirtualBuddyGuestHelper.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSBackgroundOnly = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F41369B929918576002CE8D3 /* Debug_Managed */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuestHelper/VirtualBuddyGuestHelper.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSBackgroundOnly = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug_Managed; }; F41369BA29918576002CE8D3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuestHelper/VirtualBuddyGuestHelper.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSBackgroundOnly = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; F41369BB29918576002CE8D3 /* Release_Managed */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuestHelper/VirtualBuddyGuestHelper.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSBackgroundOnly = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release_Managed; }; F43B011D2AD858FE00164CD1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 8C7439RJLG; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.DeepLinkSecurity; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; F43B011E2AD858FE00164CD1 /* Debug_Managed */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 8C7439RJLG; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.DeepLinkSecurity; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug_Managed; }; F43B011F2AD858FE00164CD1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 8C7439RJLG; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.DeepLinkSecurity; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; F43B01202AD858FE00164CD1 /* Release_Managed */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 8C7439RJLG; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.DeepLinkSecurity; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release_Managed; }; F453C4512DF0BCE3007EAD5F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8C7439RJLG; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; F453C4522DF0BCE3007EAD5F /* Debug_Managed */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8C7439RJLG; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug_Managed; }; F453C4532DF0BCE3007EAD5F /* Beta_Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8C7439RJLG; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Beta_Debug; }; F453C4542DF0BCE3007EAD5F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8C7439RJLG; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; F453C4552DF0BCE3007EAD5F /* Release_Managed */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8C7439RJLG; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release_Managed; }; F453C4562DF0BCE3007EAD5F /* Beta_Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8C7439RJLG; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Beta_Release; }; F453C4572DF0BCE3007EAD5F /* Dev_Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8C7439RJLG; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Dev_Release; }; F498AD062884BF13006F1C00 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualUI"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F498AD072884BF13006F1C00 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualUI"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; F4A2770B2BF50EA00011B626 /* Dev_Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4C1224E280715F200D359E2 /* Main.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Dev_Release; }; F4A2770C2BF50EA00011B626 /* Dev_Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4B068B528882F5D003743BF /* AppTarget.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddy/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; "DEVELOPMENT_TEAM[sdk=macosx*]" = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VirtualBuddy/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2023 Buddy Software LTD"; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddy"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Dev_Release; }; F4A2770D2BF50EA00011B626 /* Dev_Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuest/VirtualBuddyGuest.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddyGuest/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/VBGenerated-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddyGuest"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "VirtualBuddyGuest/Dashboard/Support/VirtualBuddyGuest-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Dev_Release; }; F4A2770E2BF50EA00011B626 /* Dev_Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuestHelper/VirtualBuddyGuestHelper.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSBackgroundOnly = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Dev_Release; }; F4A2770F2BF50EA00011B626 /* Dev_Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 8C7439RJLG; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.DeepLinkSecurity; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Dev_Release; }; F4A277102BF50EA00011B626 /* Dev_Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = VirtualCore/Source/Resources/Preview; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = "-Wl,-U,_OBJC_CLASS_$__VZVirtualMachineStartOptions"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualCore"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Dev_Release; }; F4A277112BF50EA00011B626 /* Dev_Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualUI"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Dev_Release; }; F4A277122BF50EA00011B626 /* Dev_Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8C7439RJLG; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.VirtualWormholeTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; }; name = Dev_Release; }; F4A277132BF50EA00011B626 /* Dev_Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualWormhole"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Dev_Release; }; F4B068A828882E9A003743BF /* Debug_Managed */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4C1224E280715F200D359E2 /* Main.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug_Managed; }; F4B068A928882E9A003743BF /* Debug_Managed */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4B068B528882F5D003743BF /* AppTarget.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddy/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; "DEVELOPMENT_TEAM[sdk=macosx*]" = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VirtualBuddy/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2023 Buddy Software LTD"; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddy"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug_Managed; }; F4B068AA28882E9A003743BF /* Debug_Managed */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuest/VirtualBuddyGuest.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddyGuest/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/VBGenerated-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddyGuest"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "VirtualBuddyGuest/Dashboard/Support/VirtualBuddyGuest-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug_Managed; }; F4B068AB28882E9A003743BF /* Debug_Managed */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = VirtualCore/Source/Resources/Preview; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = "-Wl,-U,_OBJC_CLASS_$__VZVirtualMachineStartOptions"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualCore"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug_Managed; }; F4B068AC28882E9A003743BF /* Debug_Managed */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualUI"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug_Managed; }; F4B068AD28882E9A003743BF /* Debug_Managed */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualWormhole"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug_Managed; }; F4B068AE28882EA3003743BF /* Release_Managed */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4C1224E280715F200D359E2 /* Main.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release_Managed; }; F4B068AF28882EA3003743BF /* Release_Managed */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4B068B528882F5D003743BF /* AppTarget.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddy/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; "DEVELOPMENT_TEAM[sdk=macosx*]" = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VirtualBuddy/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2023 Buddy Software LTD"; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddy"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release_Managed; }; F4B068B028882EA3003743BF /* Release_Managed */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuest/VirtualBuddyGuest.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddyGuest/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/VBGenerated-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddyGuest"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "VirtualBuddyGuest/Dashboard/Support/VirtualBuddyGuest-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Release_Managed; }; F4B068B128882EA3003743BF /* Release_Managed */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = VirtualCore/Source/Resources/Preview; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = "-Wl,-U,_OBJC_CLASS_$__VZVirtualMachineStartOptions"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualCore"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release_Managed; }; F4B068B228882EA3003743BF /* Release_Managed */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualUI"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release_Managed; }; F4B068B328882EA3003743BF /* Release_Managed */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualWormhole"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release_Managed; }; F4BE9C5B27FF052100B648F8 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4C1224E280715F200D359E2 /* Main.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F4BE9C5C27FF052100B648F8 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4C1224E280715F200D359E2 /* Main.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; F4BE9C5E27FF052100B648F8 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4B068B528882F5D003743BF /* AppTarget.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddy/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VirtualBuddy/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2023 Buddy Software LTD"; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddy"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F4BE9C5F27FF052100B648F8 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = F4B068B528882F5D003743BF /* AppTarget.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddy/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VirtualBuddy/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2023 Buddy Software LTD"; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddy"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; F4BE9C6E27FF053A00B648F8 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = VirtualCore/Source/Resources/Preview; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = "-Wl,-U,_OBJC_CLASS_$__VZVirtualMachineStartOptions"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualCore"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F4BE9C6F27FF053A00B648F8 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = VirtualCore/Source/Resources/Preview; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = "-Wl,-U,_OBJC_CLASS_$__VZVirtualMachineStartOptions"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualCore"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; F4C189E82848F59F00335EC7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualWormhole"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F4C189EA2848F59F00335EC7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(VB_FRAMEWORK_RUNPATH_SEARCH_PATHS)"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualWormhole"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; F4C18A4F28491B8500335EC7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuest/VirtualBuddyGuest.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddyGuest/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/VBGenerated-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddyGuest"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "VirtualBuddyGuest/Dashboard/Support/VirtualBuddyGuest-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F4C18A5128491B8500335EC7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = VirtualBuddyGuest/VirtualBuddyGuest.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"VirtualBuddyGuest/Preview Content\""; DEVELOPMENT_TEAM = 8C7439RJLG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/VBGenerated-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Enable audio input for your virtual machines. Without this permission, virtual machines won't be able to record any audio."; LD_RUNPATH_SEARCH_PATHS = "$(VB_APP_RUNPATH_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(VB_BUNDLE_ID_PREFIX)codes.rambo.VirtualBuddyGuest"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "VirtualBuddyGuest/Dashboard/Support/VirtualBuddyGuest-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Release; }; F4D305A429B8DB700006E748 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8C7439RJLG; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.VirtualWormholeTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; }; name = Debug; }; F4D305A529B8DB700006E748 /* Debug_Managed */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8C7439RJLG; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.VirtualWormholeTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; }; name = Debug_Managed; }; F4D305A629B8DB700006E748 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8C7439RJLG; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.VirtualWormholeTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; }; name = Release; }; F4D305A729B8DB700006E748 /* Release_Managed */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8C7439RJLG; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.VirtualWormholeTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; }; name = Release_Managed; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F41369B729918576002CE8D3 /* Build configuration list for PBXNativeTarget "VirtualBuddyGuestHelper" */ = { isa = XCConfigurationList; buildConfigurations = ( F41369B829918576002CE8D3 /* Debug */, F41369B929918576002CE8D3 /* Debug_Managed */, F40A1E8A2C1869680033E47D /* Beta_Debug */, F41369BA29918576002CE8D3 /* Release */, F41369BB29918576002CE8D3 /* Release_Managed */, F40A1E932C18697B0033E47D /* Beta_Release */, F4A2770E2BF50EA00011B626 /* Dev_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F43B01212AD858FE00164CD1 /* Build configuration list for PBXNativeTarget "DeepLinkSecurity" */ = { isa = XCConfigurationList; buildConfigurations = ( F43B011D2AD858FE00164CD1 /* Debug */, F43B011E2AD858FE00164CD1 /* Debug_Managed */, F40A1E8B2C1869680033E47D /* Beta_Debug */, F43B011F2AD858FE00164CD1 /* Release */, F43B01202AD858FE00164CD1 /* Release_Managed */, F40A1E942C18697B0033E47D /* Beta_Release */, F4A2770F2BF50EA00011B626 /* Dev_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F453C4502DF0BCE3007EAD5F /* Build configuration list for PBXAggregateTarget "vctool" */ = { isa = XCConfigurationList; buildConfigurations = ( F453C4512DF0BCE3007EAD5F /* Debug */, F453C4522DF0BCE3007EAD5F /* Debug_Managed */, F453C4532DF0BCE3007EAD5F /* Beta_Debug */, F453C4542DF0BCE3007EAD5F /* Release */, F453C4552DF0BCE3007EAD5F /* Release_Managed */, F453C4562DF0BCE3007EAD5F /* Beta_Release */, F453C4572DF0BCE3007EAD5F /* Dev_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F498AD082884BF13006F1C00 /* Build configuration list for PBXNativeTarget "VirtualUI" */ = { isa = XCConfigurationList; buildConfigurations = ( F498AD062884BF13006F1C00 /* Debug */, F4B068AC28882E9A003743BF /* Debug_Managed */, F40A1E8D2C1869680033E47D /* Beta_Debug */, F498AD072884BF13006F1C00 /* Release */, F4B068B228882EA3003743BF /* Release_Managed */, F40A1E962C18697B0033E47D /* Beta_Release */, F4A277112BF50EA00011B626 /* Dev_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F4BE9C4927FF052100B648F8 /* Build configuration list for PBXProject "VirtualBuddy" */ = { isa = XCConfigurationList; buildConfigurations = ( F4BE9C5B27FF052100B648F8 /* Debug */, F4B068A828882E9A003743BF /* Debug_Managed */, F40A1E872C1869680033E47D /* Beta_Debug */, F4BE9C5C27FF052100B648F8 /* Release */, F4B068AE28882EA3003743BF /* Release_Managed */, F40A1E902C18697B0033E47D /* Beta_Release */, F4A2770B2BF50EA00011B626 /* Dev_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F4BE9C5D27FF052100B648F8 /* Build configuration list for PBXNativeTarget "VirtualBuddy" */ = { isa = XCConfigurationList; buildConfigurations = ( F4BE9C5E27FF052100B648F8 /* Debug */, F4B068A928882E9A003743BF /* Debug_Managed */, F40A1E882C1869680033E47D /* Beta_Debug */, F4BE9C5F27FF052100B648F8 /* Release */, F4B068AF28882EA3003743BF /* Release_Managed */, F40A1E912C18697B0033E47D /* Beta_Release */, F4A2770C2BF50EA00011B626 /* Dev_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F4BE9C6D27FF053A00B648F8 /* Build configuration list for PBXNativeTarget "VirtualCore" */ = { isa = XCConfigurationList; buildConfigurations = ( F4BE9C6E27FF053A00B648F8 /* Debug */, F4B068AB28882E9A003743BF /* Debug_Managed */, F40A1E8C2C1869680033E47D /* Beta_Debug */, F4BE9C6F27FF053A00B648F8 /* Release */, F4B068B128882EA3003743BF /* Release_Managed */, F40A1E952C18697B0033E47D /* Beta_Release */, F4A277102BF50EA00011B626 /* Dev_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F4C189EB2848F59F00335EC7 /* Build configuration list for PBXNativeTarget "VirtualWormhole" */ = { isa = XCConfigurationList; buildConfigurations = ( F4C189E82848F59F00335EC7 /* Debug */, F4B068AD28882E9A003743BF /* Debug_Managed */, F40A1E8F2C1869680033E47D /* Beta_Debug */, F4C189EA2848F59F00335EC7 /* Release */, F4B068B328882EA3003743BF /* Release_Managed */, F40A1E982C18697B0033E47D /* Beta_Release */, F4A277132BF50EA00011B626 /* Dev_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F4C18A4E28491B8500335EC7 /* Build configuration list for PBXNativeTarget "VirtualBuddyGuest" */ = { isa = XCConfigurationList; buildConfigurations = ( F4C18A4F28491B8500335EC7 /* Debug */, F4B068AA28882E9A003743BF /* Debug_Managed */, F40A1E892C1869680033E47D /* Beta_Debug */, F4C18A5128491B8500335EC7 /* Release */, F4B068B028882EA3003743BF /* Release_Managed */, F40A1E922C18697B0033E47D /* Beta_Release */, F4A2770D2BF50EA00011B626 /* Dev_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F4D305A329B8DB700006E748 /* Build configuration list for PBXNativeTarget "VirtualWormholeTests" */ = { isa = XCConfigurationList; buildConfigurations = ( F4D305A429B8DB700006E748 /* Debug */, F4D305A529B8DB700006E748 /* Debug_Managed */, F40A1E8E2C1869680033E47D /* Beta_Debug */, F4D305A629B8DB700006E748 /* Release */, F4D305A729B8DB700006E748 /* Release_Managed */, F40A1E972C18697B0033E47D /* Beta_Release */, F4A277122BF50EA00011B626 /* Dev_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ F43B01452AD85A7D00164CD1 /* XCRemoteSwiftPackageReference "URLQueryItemCoder" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kylehughes/URLQueryItemCoder.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.0; }; }; F453C3D82DF0A426007EAD5F /* XCRemoteSwiftPackageReference "BuddyKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/insidegui/BuddyKit"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.6.1; }; }; F453C4012DF0AEF5007EAD5F /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-argument-parser.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.5.1; }; }; F453C4482DF0B7F6007EAD5F /* XCRemoteSwiftPackageReference "libfragmentzip" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/insidegui/libfragmentzip.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.0; }; }; F4A7FB712BB7238A00E4C12A /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/siteline/swiftui-introspect"; requirement = { kind = exactVersion; version = "1.4.0-beta.2"; }; }; F4D0F71628674E4B004D5782 /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sparkle-project/Sparkle"; requirement = { kind = upToNextMajorVersion; minimumVersion = 2.0.0; }; }; F4E4F7242DF0A1CB00B3B8BA /* XCRemoteSwiftPackageReference "BuddyKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/insidegui/BuddyKit"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.6.1; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ F43B01462AD85A7D00164CD1 /* URLQueryItemCoder */ = { isa = XCSwiftPackageProductDependency; package = F43B01452AD85A7D00164CD1 /* XCRemoteSwiftPackageReference "URLQueryItemCoder" */; productName = URLQueryItemCoder; }; F453C3EC2DF0A4D1007EAD5F /* BuddyKit */ = { isa = XCSwiftPackageProductDependency; package = F453C3D82DF0A426007EAD5F /* XCRemoteSwiftPackageReference "BuddyKit" */; productName = BuddyKit; }; F453C4112DF0B1ED007EAD5F /* BuddyKit */ = { isa = XCSwiftPackageProductDependency; package = F453C3D82DF0A426007EAD5F /* XCRemoteSwiftPackageReference "BuddyKit" */; productName = BuddyKit; }; F453C42A2DF0B792007EAD5F /* ArgumentParser */ = { isa = XCSwiftPackageProductDependency; package = F453C4012DF0AEF5007EAD5F /* XCRemoteSwiftPackageReference "swift-argument-parser" */; productName = ArgumentParser; }; F453C4492DF0B7F6007EAD5F /* FragmentZip */ = { isa = XCSwiftPackageProductDependency; package = F453C4482DF0B7F6007EAD5F /* XCRemoteSwiftPackageReference "libfragmentzip" */; productName = FragmentZip; }; F453C45A2DF0C4BE007EAD5F /* BuddyKit */ = { isa = XCSwiftPackageProductDependency; package = F453C3D82DF0A426007EAD5F /* XCRemoteSwiftPackageReference "BuddyKit" */; productName = BuddyKit; }; F453C4622DF0E609007EAD5F /* BuddyKit */ = { isa = XCSwiftPackageProductDependency; package = F453C3D82DF0A426007EAD5F /* XCRemoteSwiftPackageReference "BuddyKit" */; productName = BuddyKit; }; F4A7FB722BB7238A00E4C12A /* SwiftUIIntrospect-Static */ = { isa = XCSwiftPackageProductDependency; package = F4A7FB712BB7238A00E4C12A /* XCRemoteSwiftPackageReference "swiftui-introspect" */; productName = "SwiftUIIntrospect-Static"; }; F4D0F71728674E4B004D5782 /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = F4D0F71628674E4B004D5782 /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; F4E4F7252DF0A1CB00B3B8BA /* BuddyKit */ = { isa = XCSwiftPackageProductDependency; package = F4E4F7242DF0A1CB00B3B8BA /* XCRemoteSwiftPackageReference "BuddyKit" */; productName = BuddyKit; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = F4BE9C4627FF052100B648F8 /* Project object */; } ================================================ FILE: VirtualBuddy.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: VirtualBuddy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "originHash" : "3fd24967a48b127010a1cab40c41fd70176878f897a946d6404561fee4decff1", "pins" : [ { "identity" : "buddykit", "kind" : "remoteSourceControl", "location" : "https://github.com/insidegui/BuddyKit", "state" : { "revision" : "21d183e3c9c55ad23eb795739ffd73a1e354bca5", "version" : "1.8.0" } }, { "identity" : "libfragmentzip", "kind" : "remoteSourceControl", "location" : "https://github.com/insidegui/libfragmentzip.git", "state" : { "revision" : "9f72ba6706fe4a16413d463a79eae52c374f3c29", "version" : "1.0.0" } }, { "identity" : "sparkle", "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { "revision" : "0ca3004e98712ea2b39dd881d28448630cce1c99", "version" : "2.7.0" } }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "011f0c765fb46d9cac61bca19be0527e99c98c8b", "version" : "1.5.1" } }, { "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", "location" : "https://github.com/siteline/swiftui-introspect", "state" : { "revision" : "c2b0625d0ef385994e4c6bc36116c0e7bfa6dafa", "version" : "1.4.0-beta.2" } }, { "identity" : "urlqueryitemcoder", "kind" : "remoteSourceControl", "location" : "https://github.com/kylehughes/URLQueryItemCoder.git", "state" : { "revision" : "a828b3bb5b273adf85a3ca95d9705987cb2de7c7", "version" : "1.0.0" } } ], "version" : 3 } ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcschemes/DeepLinkSecurity.xcscheme ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcschemes/VirtualBuddy (Dev Release).xcscheme ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcschemes/VirtualBuddy (Managed - Beta).xcscheme ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcschemes/VirtualBuddy (Managed).xcscheme ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcschemes/VirtualBuddy.xcscheme ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcschemes/VirtualBuddyGuest.xcscheme ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcschemes/VirtualCore.xcscheme ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcschemes/VirtualUI.xcscheme ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcschemes/VirtualWormhole.xcscheme ================================================ ================================================ FILE: VirtualBuddy.xcodeproj/xcshareddata/xcschemes/vctool.xcscheme ================================================ ================================================ FILE: VirtualBuddyGuest/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0.000", "green" : "0.551", "red" : "1.000" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddyGuest/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "icon_16x16@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "icon_32x32@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "icon_128x128@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "icon_256x256@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "icon_512x512@2x@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddyGuest/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddyGuest/Assets.xcassets/StatusItem.imageset/Contents.json ================================================ { "images" : [ { "filename" : "menubar.png", "idiom" : "mac", "scale" : "1x" }, { "filename" : "menubar@2x.png", "idiom" : "mac", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: VirtualBuddyGuest/Dashboard/GuestDashboard.swift ================================================ // // ContentView.swift // VirtualBuddyGuest // // Created by Guilherme Rambo on 02/06/22. // import SwiftUI import VirtualWormhole struct GuestDashboard: View { @EnvironmentObject private var launchAtLoginManager: GuestLaunchAtLoginManager @EnvironmentObject private var hostConnection: WormholeManager @EnvironmentObject private var sharedFolders: GuestSharedFoldersManager @State var activated = false #if ENABLE_USERDEFAULTS_SYNC @State private var showingDefaultsPopover = false #endif var body: some View { VStack { connectionState sharedFoldersState Spacer() Form { Toggle("Launch At Login", isOn: launchAtLoginBinding) #if ENABLE_USERDEFAULTS_SYNC Button("Defaults Import…") { showingDefaultsPopover.toggle() } .popover(isPresented: $showingDefaultsPopover) { GuestDefaultsImportView() } #endif } Spacer() } .padding() .frame(minWidth: 300, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity) } @ViewBuilder private var connectionState: some View { HStack(spacing: 4) { Circle() .foregroundColor(hostConnection.isConnected ? Color.green : Color.red) .frame(width: 6) if hostConnection.isConnected { Text("Connected to VirtualBuddy") } else { Text("Not Connected") } } .foregroundStyle(.secondary) .font(.caption) } @ViewBuilder private var sharedFoldersState: some View { if let error = sharedFolders.error { VStack { Text("Failed to mount shared folders:") .foregroundColor(.secondary) Text(error.localizedDescription) .foregroundColor(.red) } } else { HStack { Text("Shared Folders:") Button("Reveal in Finder") { sharedFolders.revealInFinder() } } } } private var launchAtLoginBinding: Binding { .init { launchAtLoginManager.isLaunchAtLoginEnabled } set: { newValue in Task { do { try await launchAtLoginManager.setLaunchAtLoginEnabled(newValue) } catch { await MainActor.run { _ = NSAlert(error: error).runModal() } } } } } } #if DEBUG struct GuestDashboard_Previews: PreviewProvider { static var previews: some View { GuestDashboard() .environmentObject(GuestLaunchAtLoginManager()) .environmentObject(WormholeManager.sharedGuest) .environmentObject(GuestSharedFoldersManager()) } } final class MockHostConnectionStateProvider: HostConnectionStateProvider { var isConnected: Bool = false } #endif ================================================ FILE: VirtualBuddyGuest/Dashboard/GuestDefaultsImportView.swift ================================================ #if ENABLE_USERDEFAULTS_SYNC import SwiftUI import VirtualCore import VirtualUI import VirtualWormhole import Combine final class DefaultsImportViewModel: ObservableObject { let connection: WormholeManager let controller = DefaultsImportController() @Published private(set) var domains = [DefaultsDomainDescriptor]() init(connection: WormholeManager = .sharedGuest) { self.connection = connection controller.$sortedDomains.assign(to: &$domains) } private var _client: WHDefaultsImportClient? private var client: WHDefaultsImportClient { get throws { if let _client { return _client } let newClient = try connection.makeClient(WHDefaultsImportClient.self) _client = newClient return newClient } } func importDomain(with id: DefaultsDomainDescriptor.ID) async throws { let defaultsClient = try client try await defaultsClient.importDomain(with: id) } } struct GuestDefaultsImportView: View { @StateObject var viewModel = DefaultsImportViewModel() var body: some View { List { ForEach(viewModel.domains) { domain in DefaultsItemView(domain: domain) .environmentObject(viewModel) } } .frame(minWidth: 200, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity) } } struct DefaultsItemView: View { @EnvironmentObject var viewModel: DefaultsImportViewModel var domain: DefaultsDomainDescriptor var body: some View { HStack { Image(nsImage: domain.target.iconImage()) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 64) VStack(alignment: .leading) { Text(domain.target.name) HStack { if isLoading { ProgressView() .controlSize(.small) } else { Button("Import") { importDomain() } } } .controlSize(.small) } } } @State private var isLoading = false private func importDomain() { isLoading = true Task { do { try await viewModel.importDomain(with: domain.id) } catch { NSAlert(error: error).runModal() } isLoading = false } } } #if DEBUG struct GuestDefaultsImportView_Previews: PreviewProvider { static var previews: some View { GuestDefaultsImportView() } } #endif #endif // ENABLE_USERDEFAULTS_SYNC ================================================ FILE: VirtualBuddyGuest/Dashboard/GuestSharedFoldersManager.swift ================================================ import Foundation import VirtualCore import OSLog final class GuestSharedFoldersManager: ObservableObject { private lazy var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: Self.self)) private let queue = DispatchQueue(label: "mount", qos: .userInitiated) @Published private(set) var error: Error? func mount() async throws { logger.notice("Mount shared folders") let alreadyMounted = await checkAlreadyMounted() guard !alreadyMounted else { logger.notice("Shared folders already mounted, skipping mount") return } do { if !FileManager.default.fileExists(atPath: Self.defaultMountPointURL.path) { logger.info("Shared folders mount point doesn't exist, creating at \(Self.defaultMountPointURL.path, privacy: .public)") try FileManager.default.createDirectory(at: Self.defaultMountPointURL, withIntermediateDirectories: true, attributes: nil) } try await runMount(with: [ "-t", "virtiofs", VBSharedFolder.virtualBuddyShareName, Self.defaultMountPointURL.path ]) } catch { logger.error("Mount shared folders failed with error: \(error, privacy: .public)") await MainActor.run { self.error = error } throw error } } func revealInFinder() { NSWorkspace.shared.selectFile(Self.defaultMountPointURL.path, inFileViewerRootedAtPath: Self.defaultMountPointURL.deletingLastPathComponent().path) } private static let defaultMountPointURL: URL = { URL(fileURLWithPath: NSHomeDirectory()) .appendingPathComponent("Desktop") .appendingPathComponent(VBSharedFolder.virtualBuddyShareName) }() /// `true` if the shared folder is already mounted. private func checkAlreadyMounted() async -> Bool { guard let mountPoints = (try? await runMount()).flatMap({ $0.components(separatedBy: .newlines) }) else { return false } logger.debug("Mount points: \(mountPoints.joined(separator: "\n"))") return mountPoints.contains { $0.contains(Self.defaultMountPointURL.path) } } @discardableResult private func runMount(with arguments: [String] = []) async throws -> String { try await withCheckedThrowingContinuation { continuation in queue.async { let proc = Process() proc.executableURL = URL(fileURLWithPath: "/sbin/mount") proc.arguments = arguments let outPipe = Pipe() let errPipe = Pipe() proc.standardOutput = outPipe proc.standardError = errPipe do { try proc.run() proc.waitUntilExit() if proc.terminationStatus == 0 { let output = (try? outPipe.fileHandleForReading.readToEnd()).flatMap({ String(decoding: $0, as: UTF8.self) }) continuation.resume(returning: output ?? "") } else { continuation.resume(throwing: CocoaError(.coderInvalidValue, userInfo: [NSLocalizedDescriptionKey: "Mount command failed with code \(proc.terminationStatus)."])) } } catch { continuation.resume(throwing: error) } } } } } ================================================ FILE: VirtualBuddyGuest/Dashboard/HostConnectionStateProvider.swift ================================================ import Foundation import VirtualWormhole protocol HostConnectionStateProvider: ObservableObject { var isConnected: Bool { get } } extension WormholeManager: HostConnectionStateProvider { } ================================================ FILE: VirtualBuddyGuest/Dashboard/Support/GuestLaunchAtLoginManager.swift ================================================ import Foundation import ServiceManagement import OSLog final class GuestLaunchAtLoginManager: ObservableObject { private lazy var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: Self.self)) var isLaunchAtLoginEnabled: Bool { SMAppService.guestHelper.status == .enabled } func setLaunchAtLoginEnabled(_ enabled: Bool) async throws { logger.debug("Set launch at login enabled: \(enabled, privacy: .public)") if enabled { try SMAppService.guestHelper.register() } else { try await SMAppService.guestHelper.unregister() } await MainActor.run { objectWillChange.send() } } private var hasAutoEnabled: Bool { get { UserDefaults.standard.bool(forKey: #function) } set { UserDefaults.standard.set(newValue, forKey: #function) UserDefaults.standard.synchronize() } } func autoEnableIfNeeded() { guard !hasAutoEnabled else { return } hasAutoEnabled = true logger.debug("Attempting to auto-enable launch at login") Task { do { try await setLaunchAtLoginEnabled(true) logger.debug("Successfully auto-enabled launch at login") } catch { logger.error("Failed to auto-enable launch at login: \(error, privacy: .public)") } } } } @available(macOS 13.0, *) private extension SMAppService { static let guestHelper = SMAppService.loginItem(identifier: kGuestAppLaunchAtLoginHelperBundleID) } ================================================ FILE: VirtualBuddyGuest/Dashboard/Support/VirtualBuddyGuest-Bridging-Header.h ================================================ #define kGuestAppLaunchAtLoginHelperBundleID GUEST_LAUNCH_AT_LOGIN_HELPER_BUNDLE_ID ================================================ FILE: VirtualBuddyGuest/GuestAppDelegate.swift ================================================ import Cocoa import SwiftUI import VirtualUI import VirtualWormhole import OSLog @NSApplicationMain final class GuestAppDelegate: NSObject, NSApplicationDelegate { private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Guest", category: "GuestAppDelegate") private lazy var launchAtLoginManager = GuestLaunchAtLoginManager() private lazy var sharedFolders = GuestSharedFoldersManager() private lazy var dashboardItem: StatusItemManager = { StatusItemManager( configuration: .default.id("dashboard"), statusItem: .button(label: { Image("StatusItem") }), content: GuestDashboard() .environmentObject(self.launchAtLoginManager) .environmentObject(WormholeManager.sharedGuest) .environmentObject(self.sharedFolders) ) }() private var shouldShowPanelAfterLaunching: Bool { get { !UserDefaults.standard.bool(forKey: "shownPanelOnFirstLauch") || UserDefaults.standard.bool(forKey: "ShowPanelOnLaunch") } set { UserDefaults.standard.set(!newValue, forKey: "shownPanelOnFirstLauch") UserDefaults.standard.synchronize() } } private let installer = GuestAppInstaller() func applicationWillFinishLaunching(_ notification: Notification) { do { try installer.installIfNeeded() } catch { let alert = NSAlert(error: error) alert.runModal() } } func applicationDidFinishLaunching(_ notification: Notification) { /// Skip regular app activation if installation is needed (i.e. running from disk image). guard !installer.needsInstall else { return } launchAtLoginManager.autoEnableIfNeeded() WormholeManager.sharedGuest.activate() Task { try? await sharedFolders.mount() } dashboardItem.install() perform(#selector(showPanelForFirstLaunchIfNeeded), with: nil, afterDelay: 0.5) NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willPowerOffNotification, object: nil, queue: nil) { _ in self.logger.notice("Received power off notification.") } } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { showPanel() return true } @objc private func showPanelForFirstLaunchIfNeeded() { guard shouldShowPanelAfterLaunching else { return } shouldShowPanelAfterLaunching = false NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(showPanelForFirstLaunchIfNeeded), object: nil) showPanel() } @objc private func showPanel() { dashboardItem.showPanel() } private var startedDesktopPictureSendBeforeTermination = false func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { logger.debug(#function) guard !startedDesktopPictureSendBeforeTermination else { return .terminateLater } startedDesktopPictureSendBeforeTermination = true Task { logger.notice("Requesting WH send desktop picture") try? await Task.sleep(for: .milliseconds(500)) await WormholeManager.sharedGuest.sendDesktopPicture() try? await Task.sleep(for: .seconds(500)) NSApp.reply(toApplicationShouldTerminate: true) } return .terminateLater } } ================================================ FILE: VirtualBuddyGuest/GuestAppInstaller.swift ================================================ import Cocoa import OSLog final class GuestAppInstaller { private lazy var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: Self.self)) func installIfNeeded() throws { do { let needsUpdate = mountedGuestImageNeedsInstall guard !needsUpdate else { logger.notice("🚀 Relaunching from mounted image for update") try NSApplication.shared.relaunch(at: mountedImageAppURL.path) return } guard needsInstall else { logger.debug("Install not needed: running from supported path and updated not needed. Path: \(Bundle.main.bundleURL.deletingLastPathComponent().path, privacy: .public)") return } let destURL = URL(fileURLWithPath: "/Applications") .appendingPathComponent(Bundle.main.bundleURL.lastPathComponent) logger.notice("Performing install (running from \(Bundle.main.bundleURL.deletingLastPathComponent().path, privacy: .public), installing to \(destURL.path, privacy: .public))") if FileManager.default.fileExists(atPath: destURL.path) { logger.debug("Removing existing app at \(destURL.path, privacy: .public)") if let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.bundleIdentifier!).first(where: { $0.bundleURL == destURL }) { logger.debug("Terminating existing app instance") if !runningApp.forceTerminate() { logger.error("Failed to terminate existing app instance") } } try FileManager.default.removeItem(at: destURL) } try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: destURL) try NSApplication.shared.relaunch(at: destURL.path) } catch { logger.error("Install failed: \(error, privacy: .public)") throw CocoaError(.coderInvalidValue, userInfo: [ NSLocalizedDescriptionKey: "Failed to install the VirtualBuddyGuest app. This can occur if the Mac user account on the virtual machine can't write to /Applications.", NSUnderlyingErrorKey: error ]) } } var needsInstall: Bool { guard !UserDefaults.standard.bool(forKey: "DisableInstall") else { return false } return !isRunningFromApplicationsDirectory } private var isRunningFromApplicationsDirectory: Bool { let directories = NSSearchPathForDirectoriesInDomains(.applicationDirectory, .allDomainsMask, true) for directory in directories { if Bundle.main.bundlePath.hasPrefix(directory) { return true } } return false } private var mountedImageAppURL: URL { URL(fileURLWithPath: "/Volumes/Guest/VirtualBuddyGuest.app") } /// `true` if there's a VirtualBuddyGuest image mounted at `/Volumes/Guest` /// for which the following conditions are true: /// 1 - The guest in the volume has a different `VBGuestBuildID` from this process /// 2 - The guest in the volume has a `CFBundleVersion` that's **greater than or equal to** the `CFBundleVersion` of this process private var mountedGuestImageNeedsInstall: Bool { guard FileManager.default.fileExists(atPath: "/Volumes/Guest") else { return false } logger.debug("Guest volume is mounted, checking app version") let imageURL = mountedImageAppURL guard imageURL.path != Bundle.main.bundleURL.path else { logger.debug("We're the mounted image app, skipping update checks.") return false } guard let imageBundle = Bundle(url: imageURL) else { logger.error("Couldn't get bundle at \(imageURL.path, privacy: .public)") return false } guard let imageBuildID = imageBundle.vbGuestBuildID else { logger.error("Couldn't find VBGuestBuildID in image bundle") return false } guard let imageBundleVersion = imageBundle.bundleVersion else { logger.error("Couldn't find CFBundleVersion in image bundle") return false } guard let currentBuildID = Bundle.main.vbGuestBuildID else { logger.error("Couldn't find VBGuestBuildID in current bundle") return false } guard let currentBundleVersion = Bundle.main.bundleVersion else { logger.error("Couldn't find CFBundleVersion in current bundle") return false } guard imageBuildID != currentBuildID else { logger.debug("Image build ID is same as current build ID (\(currentBuildID, privacy: .public)), update won't be performed") return false } guard imageBundleVersion >= currentBundleVersion else { logger.debug("Image build ID differs from current build ID, but image has a lower CFBundleVersion (\(imageBundleVersion, privacy: .public)), ignoring") return false } logger.notice("Mounted image qualifies for update with CFBundleVersion \(imageBundleVersion, privacy: .public), VBGuestBuildID \(imageBuildID, privacy: .public)") return true } } extension Bundle { var bundleVersion: Int? { guard let str = infoDictionary?[kCFBundleVersionKey as String] as? String else { return nil } return Int(str) } var vbGuestBuildID: String? { infoDictionary?["VBGuestBuildID"] as? String } } extension NSApplication { // Credit: Andy Kim (PFMoveApplication) func relaunch(at path: String) throws { let pid = ProcessInfo.processInfo.processIdentifier let xattrScript = "/usr/bin/xattr -d -r com.apple.quarantine \(path)" let script = "(while /bin/kill -0 \(pid) >&/dev/null; do /bin/sleep 0.1; done; \(xattrScript); /usr/bin/open \(path)) &" let proc = Process() proc.executableURL = URL(fileURLWithPath: "/bin/sh") proc.arguments = [ "-c", script ] try proc.run() exit(0) } } ================================================ FILE: VirtualBuddyGuest/Main.storyboard ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: VirtualBuddyGuest/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddyGuest/VirtualBuddyGuest.entitlements ================================================ ================================================ FILE: VirtualBuddyGuestHelper/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddyGuestHelper/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddyGuestHelper/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualBuddyGuestHelper/Base.lproj/Main.storyboard ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: VirtualBuddyGuestHelper/GuestHelperAppDelegate.swift ================================================ import Cocoa import os.log @main final class GuestHelperAppDelegate: NSObject, NSApplicationDelegate { private let log = OSLog(subsystem: "codes.rambo.VirtualBuddyGuestHelper", category: String(describing: GuestHelperAppDelegate.self)) func applicationDidFinishLaunching(_ aNotification: Notification) { let config = NSWorkspace.OpenConfiguration() config.activates = false config.addsToRecentItems = false config.promptsUserIfNeeded = false NSWorkspace.shared.openApplication( at: Bundle.main.mainAppBundleURL, configuration: config) { _, error in if let error = error { os_log("Failed to launch main app: %{public}@", log: self.log, type: .fault, String(describing: error)) } else { os_log("Main app launched successfully", log: self.log, type: .info) } DispatchQueue.main.async { NSApp?.terminate(nil) } } } } extension Bundle { var mainAppBundleURL: URL { bundleURL .deletingLastPathComponent() // VirtualBuddyGuestHelper.app .deletingLastPathComponent() // LoginItems .deletingLastPathComponent() // Library .deletingLastPathComponent() // Contents } } ================================================ FILE: VirtualBuddyGuestHelper/VirtualBuddyGuestHelper.entitlements ================================================ com.apple.security.app-sandbox ================================================ FILE: VirtualCore/Source/Definitions/Logging.swift ================================================ // // Logging.swift // VirtualCore // // Created by Guilherme Rambo on 05/06/22. // import Foundation import OSLog extension Error { var log: String { String(describing: self) } } public extension Logger { init(for type: T.Type, label: String? = nil) { let suffix = label.flatMap { "(\($0))" } ?? "" self.init(subsystem: VirtualCoreConstants.subsystemName, category: "\(String(describing: type))\(suffix)") } } extension Logger { func assert(_ message: String) { fault("\(message, privacy: .public)") assertionFailure(message) } } ================================================ FILE: VirtualCore/Source/Definitions/PreviewSupport.swift ================================================ #if DEBUG import Foundation import Virtualization let previewLibraryDirName = "PreviewLibrary" public extension VBVirtualMachine { static func previewMachine(named name: String) -> VBVirtualMachine { try! VBVirtualMachine(bundleURL: Bundle.virtualCore.url(forResource: name, withExtension: VBVirtualMachine.bundleExtension, subdirectory: previewLibraryDirName)!) } static let preview = VBVirtualMachine.previewMachine(named: "PreviewMac") static let previewBlurHash = VBVirtualMachine.previewMachine(named: "PreviewMacBlurHash") static let previewNoArtwork = VBVirtualMachine.previewMachine(named: "PreviewMacNoArtwork") static let previewLinux = VBVirtualMachine.previewMachine(named: "PreviewLinux") static let previewLinuxBlurHash = VBVirtualMachine.previewMachine(named: "PreviewLinuxBlurHash") static let previewLinuxNoArtwork = VBVirtualMachine.previewMachine(named: "PreviewLinuxNoArtwork") } extension Bundle { func requiredPreviewDirectoryURL(named name: String) -> URL { guard let url = Bundle.virtualCore.resourceURL?.appending(path: name, directoryHint: .isDirectory) else { fatalError("Couldn't get resources URL for VirtualCore bundle") } precondition(FileManager.default.fileExists(atPath: url.path), "Missing \(name) directory in VirtualCore resources") return url } } public extension VBSettingsContainer { static let preview: VBSettingsContainer = { let libraryURL = Bundle.virtualCore.requiredPreviewDirectoryURL(named: previewLibraryDirName) let container = VBSettingsContainer() container.settings.libraryURL = libraryURL return container }() } public extension VMLibraryController { static let preview: VMLibraryController = { VMLibraryController(settingsContainer: .preview) }() } public extension VMSavedStatesController { static var preview: VMSavedStatesController { fatalError("VMSavedStatesController.preview needs to be reimplemented with new VMSavedStatesController requirements") // VMSavedStatesController(directoryURL: Bundle.virtualCore.requiredPreviewDirectoryURL(named: "\(previewLibraryDirName)/_SavedStates")) } } @MainActor public extension VMController { static let preview = VMController(with: .preview, library: .preview) } public extension VBMacConfiguration { static let preview: VBMacConfiguration = { var c = VBMacConfiguration.default c.hardware.storageDevices.append(.init(isBootVolume: false, isEnabled: true, isReadOnly: false, isUSBMassStorageDevice: false, backing: .managedImage(VBManagedDiskImage(filename: "New Device", size: VBManagedDiskImage.minimumExtraDiskImageSize)))) c.hardware.storageDevices.append(.init(isBootVolume: false, isEnabled: true, isReadOnly: false, isUSBMassStorageDevice: false, backing: .managedImage(VBManagedDiskImage(filename: "Fake Managed Disk", size: VBManagedDiskImage.minimumExtraDiskImageSize, format: .raw)))) // c.hardware.storageDevices.append(.init(isBootVolume: false, isEnabled: true, isReadOnly: false, isUSBMassStorageDevice: false, backing: .customImage(Bundle.virtualCore.url(forResource: "Fake Custom Path Disk", withExtension: "dmg", subdirectory: "Preview.vbvm")!))) c.sharedFolders = [ .init(id: UUID(uuidString: "821BA195-D687-4B61-8412-0C6BA6C99074")!, url: URL(fileURLWithPath: "/Users/insidegui/Desktop"), isReadOnly: true), .init(id: UUID(uuidString: "821BA195-D687-4B61-8412-0C6BA6C99075")!, url: URL(fileURLWithPath: "/Users/insidegui/Downloads"), isReadOnly: false), .init(id: UUID(uuidString: "821BA195-D687-4B61-8412-0C6BA6C99076")!, url: URL(fileURLWithPath: "/Volumes/Rambo/Movies"), isEnabled: false, isReadOnly: false), .init(id: UUID(uuidString: "821BA195-D687-4B61-8412-0C6BA6C99077")!, url: URL(fileURLWithPath: "/Some/Invalid/Path"), isEnabled: true, isReadOnly: false), .init(id: UUID(uuidString: "821BA195-D687-4B61-8412-0C6BA6C99078")!, url: URL(fileURLWithPath: "/Users/insidegui/Music"), isEnabled: true, isReadOnly: true), .init(id: UUID(uuidString: "821BA195-D687-4B61-8412-0C6BA6C99079")!, url: URL(fileURLWithPath: "/Users/insidegui/Developer"), isEnabled: true, isReadOnly: true), ] return c }() static var networkPreviewNAT: VBMacConfiguration { var config = VBMacConfiguration.preview config.hardware.networkDevices = [VBNetworkDevice(id: "Default", name: "Default", kind: .NAT, macAddress: "0A:82:7F:CE:C0:58")] return config } static var networkPreviewBridge: VBMacConfiguration { var config = VBMacConfiguration.preview config.hardware.networkDevices = [VBNetworkDevice(id: VBNetworkDevice.defaultBridgeInterfaceID ?? "ERROR", name: "Bridge", kind: .bridge, macAddress: "0A:82:7F:CE:C0:58")] return config } static var networkPreviewNone: VBMacConfiguration { var config = VBMacConfiguration.preview config.hardware.networkDevices = [] return config } var removingSharedFolders: Self { var mSelf = self mSelf.sharedFolders = [] return mSelf } var linuxVirtualMachine: Self { var mSelf = self mSelf.systemType = .linux return mSelf } } public extension VZVirtualMachine { /// A dummy `VZVirtualMachine` instance for previews where an instance is needed but nothing is actually done with it. static let preview: VZVirtualMachine = { let config = VZVirtualMachineConfiguration() /// Sneaky little swizzle to get around validation exception. /// This is fine® because it's just for previews. if let method = class_getInstanceMethod(VZVirtualMachineConfiguration.self, #selector(VZVirtualMachineConfiguration.validate)) { let impBlock: @convention(block) () -> Bool = { return true } method_setImplementation(method, imp_implementationWithBlock(impBlock)) } return VZVirtualMachine(configuration: config) }() } public extension SoftwareCatalog { static let previewMac = try! VBAPIClient.fetchBuiltInCatalog(for: .mac) static let previewLinux = try! VBAPIClient.fetchBuiltInCatalog(for: .linux) } public extension ResolvedCatalog { static let previewMac = ResolvedCatalog(environment: .current.guest(platform: .mac), catalog: .previewMac) static let previewLinux = ResolvedCatalog(environment: .current.guest(platform: .linux), catalog: .previewLinux) } public extension ResolvedCatalogGroup { static let previewMac = ResolvedCatalog.previewMac.groups[0] static let previewLinux = ResolvedCatalog.previewLinux.groups[0] } public extension ResolvedRestoreImage { static let previewMac = ResolvedCatalog.previewMac.groups[0].restoreImages[0] static let previewLinux = ResolvedCatalog.previewLinux.groups[0].restoreImages[0] } #endif ================================================ FILE: VirtualCore/Source/Definitions/VirtualCoreConstants.swift ================================================ // // VirtualCoreConstants.swift // VirtualCore // // Created by Guilherme Rambo on 02/06/22. // import Foundation import OSLog @_exported import BuddyKit struct VirtualCoreConstants { static let subsystemName = "codes.rambo.VirtualCore" } private final class _VirtualCoreStub { } public extension Bundle { static let virtualCore = Bundle(for: _VirtualCoreStub.self) } ================================================ FILE: VirtualCore/Source/GuestSupport/CreateGuestImage.sh ================================================ #!/bin/sh : ' This script is used by VirtualBuddy to dynamically generate a disk image that can be mounted in a virtual machine, containing the current version of the VirtualBuddyGuest app embedded into VirtualBuddy itself. Images are stored in ~/Library/Application Support/VirtualBuddy/_GuestImage. Alongside the images, the app stores a digest of the entire contents of the Guest app bundle, so that it can be automatically updated whenever something changes in the Guest app. ' GUEST_APP_PATH="$1" GUEST_APP_DIGEST="$2" GUEST_SIZE_ALLOCATION="$3" GUEST_DMG_SUFFIX="$4" if [ -z "$GUEST_APP_PATH" ]; then echo "Shell script invocation error: missing GUEST_APP_PATH value as first argument" 1>&2 exit 7 fi if [ -z "$GUEST_APP_DIGEST" ]; then echo "Shell script invocation error: missing GUEST_APP_DIGEST value as second argument" 1>&2 exit 7 fi if [ -z "$GUEST_SIZE_ALLOCATION" ]; then echo "Shell script invocation error: missing GUEST_SIZE_ALLOCATION value as third argument" 1>&2 exit 7 fi if [ ! -d "$GUEST_APP_PATH" ]; then echo "Shell script invocation error: guest app bundle doesn't exist at $GUEST_APP_PATH" 1>&2 exit 7 fi VBROOT="$HOME/Library/Application Support/VirtualBuddy" GUEST_DMG_DEST_PATH="$VBROOT/_GuestImage" GUEST_DMG_NAME="VirtualBuddyGuest$GUEST_DMG_SUFFIX" GUEST_STAGING_PATH="$GUEST_DMG_DEST_PATH/staging" GUEST_TEMP_MOUNT_PATH="$GUEST_STAGING_PATH/VirtualBuddyGuest$GUEST_DMG_SUFFIX" GUEST_TEMP_DMG_PATH="$GUEST_STAGING_PATH/$GUEST_DMG_NAME.dmg" GUEST_TEMP_DIGEST_PATH="$GUEST_TEMP_MOUNT_PATH/.$GUEST_DMG_NAME.digest" # Make sure the temporary mount point exists mkdir -p "$GUEST_TEMP_MOUNT_PATH" 2>/dev/null || echo "" # Unmount and remove any leftovers from previous script invocation hdiutil detach -force "$GUEST_TEMP_MOUNT_PATH" 2>/dev/null || echo "" rm "$GUEST_TEMP_DMG_PATH" 2>/dev/null || echo "" # Create blank disk image hdiutil create -layout MBRSPUD -size $GUEST_SIZE_ALLOCATION -fs HFS+ -volname Guest "$GUEST_TEMP_DMG_PATH" || \ { echo "Failed to create VirtualBuddyGuest disk image: hdiutil exit code $?" 1>&2; exit 1; } # Mount image at staging location hdiutil attach -imagekey diskimage-class=CRawDiskImage -noverify "$GUEST_TEMP_DMG_PATH" -mountpoint "$GUEST_TEMP_MOUNT_PATH" || \ { echo "Failed to mount empty VirtualBuddyGuest disk image: hdiutil exit code $?" 1>&2; exit 1; } # Write digest to temporary mount echo "$GUEST_APP_DIGEST" > "$GUEST_TEMP_DIGEST_PATH" # Copy VirtualBuddyGuest.app into the temporary mount cp -R "$GUEST_APP_PATH" "$GUEST_TEMP_MOUNT_PATH/" || \ { echo "Failed to copy VirtualBuddyGuest.app into disk image: exit code $?" 1>&2; exit 1; } # Copy the digest to its final destination yes | cp -rf "$GUEST_TEMP_DIGEST_PATH" "$GUEST_DMG_DEST_PATH" || \ { echo "Failed to copy guest digest: exit code $?" 1>&2; } # Failure to copy digest is non-fatal # Eject the disk image hdiutil detach -force "$GUEST_TEMP_MOUNT_PATH" || \ { echo "Failed to eject VirtualBuddyGuest disk image: exit code $?" 1>&2; exit 1; } # Remove any extended attributes from the disk image xattr -cr "$GUEST_TEMP_DMG_PATH" 2>/dev/null || echo "" # Copy the finalized disk image to its final destination yes | cp -rf "$GUEST_TEMP_DMG_PATH" "$GUEST_DMG_DEST_PATH" || \ { echo "Failed to copy finalized disk image: exit code $?" 1>&2; exit 1; } # Cleanup rm "$GUEST_TEMP_DMG_PATH" 2>/dev/null || echo "" rm -Rf "$GUEST_TEMP_MOUNT_PATH" 2>/dev/null || echo "" rm -Rf "$GUEST_STAGING_PATH" 2>/dev/null || echo "" echo "OK" ================================================ FILE: VirtualCore/Source/GuestSupport/GuestAdditionsDiskImage.swift ================================================ // // GuestAdditionsDiskImage.swift // VirtualCore // // Created by Guilherme Rambo on 07/03/23. // import Foundation import Virtualization import CryptoKit import UniformTypeIdentifiers import OSLog import Combine public final class GuestAdditionsDiskImage: ObservableObject { private lazy var logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: String(describing: Self.self)) public static let current = GuestAdditionsDiskImage() public enum State: CustomStringConvertible { case ready case installing case installFailed(Error) public var description: String { switch self { case .ready: "Ready" case .installing: "Installing" case .installFailed(let error): "Failed: \(error)" } } } @MainActor @Published public private(set) var state = State.ready public func installIfNeeded() async throws { #if DEBUG if await simulateInstall() { return } #endif do { logger.debug(#function) func performInstall(with digest: String) async throws { await MainActor.run { state = .installing } try await writeGuestImage(with: digest) await MainActor.run { state = .ready } } let embeddedDigest = try computeEmbeddedGuestDigest() if let currentlyInstalledGuestImageDigest { logger.debug("Embedded guest app digest: \(embeddedDigest, privacy: .public) / Library guest app digest: \(currentlyInstalledGuestImageDigest, privacy: .public)") guard embeddedDigest != currentlyInstalledGuestImageDigest else { logger.debug("Guest digests match, skipping guest image generation") await MainActor.run { state = .ready } return } logger.debug("Guest digests don't match, generating new guest image with embedded guest") try await performInstall(with: embeddedDigest) } else { logger.debug("No digest for currently installed image, assuming not installed. Embedded guest app digest: \(embeddedDigest, privacy: .public)") try await performInstall(with: embeddedDigest) } } catch { logger.error("Guest disk image installation failed. \(error, privacy: .public)") await MainActor.run { state = .installFailed(error) } throw error } } // MARK: File Paths private var embeddedGuestAppURL: URL { get throws { guard let url = Bundle.main.sharedSupportURL?.appendingPathComponent("VirtualBuddyGuest.app") else { throw Failure("Couldn't get VirtualBuddyGuest.app URL within main app bundle") } guard FileManager.default.fileExists(atPath: url.path) else { throw Failure("VirtualBuddyGuest.app doesn't exist at \(url.path)") } return url } } private var generatorScriptURL: URL { get throws { guard let url = Bundle.virtualCore.url(forResource: "CreateGuestImage", withExtension: "sh") else { throw Failure("Couldn't get CreateGuestImage.sh URL within VirtualCore bundle") } guard FileManager.default.fileExists(atPath: url.path) else { throw Failure("CreateGuestImage.sj doesn't exist at \(url.path)") } return url } } private var _imageBaseName: String { "VirtualBuddyGuest" } private var imageName: String { if let suffix = VBBuildType.current.guestAdditionsImageSuffix { _imageBaseName + suffix } else { _imageBaseName } } static let imagesRootURL: URL = URL.defaultVirtualBuddyLibraryURL.appendingPathComponent("_GuestImage") private var imagesRootURL: URL { Self.imagesRootURL } private var installedImageDigestURL: URL { imagesRootURL .appendingPathComponent("." + imageName) .appendingPathExtension("digest") } public var installedImageURL: URL { imagesRootURL .appendingPathComponent(imageName) .appendingPathExtension("dmg") } // MARK: Digest private var currentlyInstalledGuestImageDigest: String? { guard FileManager.default.fileExists(atPath: installedImageDigestURL.path) else { return nil } do { return try String(contentsOf: installedImageDigestURL) .trimmingCharacters(in: .whitespacesAndNewlines) } catch { logger.error("Failed to read installed image digest at \(self.installedImageDigestURL.path): \(error, privacy: .public)") return nil } } private func computeEmbeddedGuestDigest() throws -> String { let url = try embeddedGuestAppURL guard let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.contentTypeKey]) else { throw Failure("Couldn't instantiate file enumerator for computing guest app bundle digest") } var hash = SHA256() while let url = enumerator.nextObject() as? URL { guard let values = try? url.resourceValues(forKeys: [.contentTypeKey]), let contentType = values.contentType else { continue } guard contentType.conforms(to: .executable), !contentType.conforms(to: .directory) else { continue } #if DEBUG logger.debug("Computing hash for \(url.lastPathComponent)") #endif do { let data = try Data(contentsOf: url, options: .mappedIfSafe) hash.update(data: data) } catch { logger.warning("Couldn't compute hash for \(url.lastPathComponent): \(error, privacy: .public)") } } let digest = hash.finalize() let hashStr = digest.map { String(format: "%02x", $0) }.joined() return hashStr } // MARK: Installation private func writeGuestImage(with digest: String) async throws { let scriptPath = try generatorScriptURL.path let guestURL = try embeddedGuestAppURL let guestPath = guestURL.path let size = computeImageSizeInMB(guestAppURL: guestURL) var args: [String] = [ scriptPath, guestPath, digest, "\(size)MB" ] if let suffix = VBBuildType.current.guestAdditionsImageSuffix { args.append(suffix) } let p = Process() p.executableURL = URL(fileURLWithPath: "/bin/sh") p.arguments = args let outPipe = Pipe() let errPipe = Pipe() p.standardOutput = outPipe p.standardError = errPipe try p.run() p.waitUntilExit() let outData = try outPipe.fileHandleForReading.readToEnd() let errData = try errPipe.fileHandleForReading.readToEnd() #if DEBUG if let outData, !outData.isEmpty { logger.debug("#### Generator script output (stdout): ####") logger.debug("\(String(decoding: outData, as: UTF8.self), privacy: .public)") } if let errData, !errData.isEmpty { logger.debug("#### Generator script output (stderr): ####") logger.debug("\(String(decoding: errData, as: UTF8.self), privacy: .public)") } #endif guard p.terminationStatus == 0 else { if let message = errData.flatMap({ String(decoding: $0, as: UTF8.self) }) { throw Failure(message) } else { throw Failure("Guest additions disk image generator failed with exit code \(p.terminationStatus)") } } logger.notice("Guest additions disk image generated at \(self.installedImageURL.path, privacy: .public)") } } // MARK: - Virtualization Extensions extension VZVirtioBlockDeviceConfiguration { static var guestAdditionsDisk: VZVirtioBlockDeviceConfiguration? { get throws { let guestImageURL = GuestAdditionsDiskImage.current.installedImageURL guard FileManager.default.fileExists(atPath: guestImageURL.path) else { return nil } let guestAttachment = try VZDiskImageStorageDeviceAttachment(url: guestImageURL, readOnly: true) return VZVirtioBlockDeviceConfiguration(attachment: guestAttachment) } } } // MARK: - Image Size Calculation private extension GuestAdditionsDiskImage { /// Fallback size in case image size can't be calculated. static let defaultImageSizeInMB = 32 /// Just being paranoid in case size computation goes haywire and ends up computing a huge image size. static let maxImageSizeInMB = 128 /// Increase image size slightly when compared to guest app size to account for extra space needed for disk image. static let imageSizeMultiplier: Double = 1.1 func computeImageSizeInMB(guestAppURL: URL) -> Int { do { guard let enumerator = FileManager.default.enumerator(at: guestAppURL, includingPropertiesForKeys: [.totalFileAllocatedSizeKey, .contentTypeKey], options: [], errorHandler: { url, error in self.logger.warning("Error enumerating guest app contents at \(url.lastPathComponent, privacy: .public) - \(error, privacy: .public)") return true }) else { throw Failure("Failed to create directory enumerator.") } var totalSize: Int = 0 while let file = enumerator.nextObject() as? URL { let values = try file.resourceValues(forKeys: [.contentTypeKey, .totalFileAllocatedSizeKey]) guard let type = values.contentType else { throw Failure("Content type not available for \(file.lastPathComponent)") } guard !type.conforms(to: .directory) else { continue } guard let size = values.totalFileAllocatedSize else { throw Failure("File size not available for \(file.lastPathComponent)") } totalSize += size } let totalSizeMB = Int(ceil(Double(totalSize) * Self.imageSizeMultiplier)) / 1000 / 1000 logger.info("Calculated guest disk image size: \(totalSizeMB, privacy: .public)MB") guard totalSizeMB <= Self.maxImageSizeInMB else { assertionFailure("\(#function) calculated a size that's larger than the maximum allowed size. Calculated size in MB: \(totalSizeMB), max size in MB: \(Self.maxImageSizeInMB)") return Self.maxImageSizeInMB } return totalSizeMB } catch { logger.fault("Error computing total guest disk image size. \(error, privacy: .public)") return Self.defaultImageSizeInMB } } } extension VBBuildType { var guestAdditionsImageSuffix: String? { switch self { case .debug: "_Debug" case .betaDebug: "_Beta_Debug" case .release: nil case .betaRelease: "_Beta" case .devRelease: "_Dev" } } } // MARK: - Debug Simulation #if DEBUG private extension GuestAdditionsDiskImage { func simulateInstall() async -> Bool { guard UserDefaults.standard.bool(forKey: "VBSimulateGuestDiskImageGeneration") else { return false } logger.debug("Guest disk image will not be generated because VBSimulateGuestDiskImageGeneration is enabled.") await MainActor.run { state = .installing } let delaySeconds = UserDefaults.standard.integer(forKey: "VBDelayGuestDiskImageGenerationBySeconds") if delaySeconds > 0 { logger.debug("Simulating guest disk image install with custom delay of \(delaySeconds) seconds") try? await Task.sleep(for: .seconds(delaySeconds)) } else { logger.debug("Simulating guest disk image install with default delay") try? await Task.sleep(for: .seconds(3)) } guard !UserDefaults.standard.bool(forKey: "VBSimulateGuestDiskImageGenerationError") else { logger.debug("Simulating guest disk image install error.") await MainActor.run { state = .installFailed("This is a simulated error for debugging.") } return true } logger.debug("Simulated guest disk image install completed") await MainActor.run { state = .ready } return true } } #endif ================================================ FILE: VirtualCore/Source/Headers/VirtualizationPrivate.h ================================================ // // VirtualizationPrivate.h // VirtualBuddy // // Created by Guilherme Rambo on 07/04/22. // #import NS_ASSUME_NONNULL_BEGIN // Classes defined here are no longer SPI in macOS 14 #if !defined(MAC_OS_VERSION_14_0) __attribute__((weak_import)) @interface _VZFramebuffer: NSObject - (void)takeScreenshotWithCompletionHandler:(void(^)(NSImage *__nullable screenshot, NSError *__nullable error))completion; @end @interface _VZGraphicsDevice: NSObject - (NSInteger)type; - (NSArray <_VZFramebuffer *> *)framebuffers; @end #endif @interface _VZMultiTouchDeviceConfiguration: NSObject @end @interface _VZAppleTouchScreenConfiguration: _VZMultiTouchDeviceConfiguration @end @interface _VZUSBTouchScreenConfiguration: _VZMultiTouchDeviceConfiguration @end __attribute__((weak_import)) @interface _VZVirtualMachineStartOptions: NSObject @property (assign) BOOL forceDFU; @property (assign) BOOL stopInIBootStage1; @property (assign) BOOL stopInIBootStage2; @property (assign) BOOL bootMacOSRecovery; @end @interface VZMacOSVirtualMachineStartOptions (VZPrivate) @property (assign, setter=_setForceDFU:) BOOL _forceDFU; @end @interface VZMacAuxiliaryStorage (Private) - (NSDictionary *)_allNVRAMVariablesWithError:(NSError **)outError; - (NSDictionary *)_allNVRAMVariablesInPartition:(NSUInteger)partition error:(NSError **)outError; - (id __nullable)_valueForNVRAMVariableNamed:(NSString *)name error:(NSError **)arg2; - (BOOL)_removeNVRAMVariableNamed:(NSString *)name error:(NSError **)arg2; - (BOOL)_setValue:(id)arg1 forNVRAMVariableNamed:(NSString *)name error:(NSError **)arg3; @end @interface VZVirtualMachineConfiguration (Private) @property (strong, setter=_setMultiTouchDevices:) NSArray <_VZMultiTouchDeviceConfiguration *> *_multiTouchDevices; @end @interface VZVirtualMachine (Private) #if !defined(MAC_OS_VERSION_13_0) || MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_VERSION_13_0 - (void)_startWithOptions:(_VZVirtualMachineStartOptions *__nullable)options completionHandler:(void (^__nonnull)(NSError * _Nullable errorOrNil))completionHandler; #endif - (id)_USBDevices; - (BOOL)_canAttachUSBDevices; - (BOOL)_canDetachUSBDevices; - (BOOL)_canAttachUSBDevice:(id)arg1; - (BOOL)_canDetachUSBDevice:(id)arg1; - (BOOL)_attachUSBDevice:(id)arg1 error:(void *)arg2; - (BOOL)_detachUSBDevice:(id)arg1 error:(void *)arg2; - (void)_getUSBControllerLocationIDWithCompletionHandler:(void(^)(id val))arg1; #if !defined(MAC_OS_VERSION_14_0) @property (nonatomic, readonly) NSArray <_VZGraphicsDevice *> *_graphicsDevices; #endif @end @interface VZMacPlatformConfiguration (Private) @property (nonatomic, assign, setter=_setProductionModeEnabled:) BOOL _isProductionModeEnabled; - (id __nullable)_platform; @end @interface VZVirtualMachineView (Private) - (void)_setDelegate:(id)delegate; @end @interface VZGraphicsDisplay (Private) - (void)_takeScreenshotWithCompletionHandler:(void(^__nonnull)(id __nullable image, id __nullable error))completion NS_SWIFT_UI_ACTOR API_AVAILABLE(macos(14.0)); @end NS_ASSUME_NONNULL_END ================================================ FILE: VirtualCore/Source/Import/UTM/UTMAppleConfiguration.swift ================================================ /* Code in this file is based on source code from the UTM project (https://github.com/utmapp/UTM) Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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 BuddyFoundation extension PropertyListDecoder { static let utm = PropertyListDecoder() } extension UTMAppleConfiguration { init(path: FilePath) throws { let data: Data = try path.read() let stub = try PropertyListDecoder.utm.decode(UTMConfigurationStub.self, from: data) guard stub.backend == "Apple" else { throw "Only UTM virtual machines based on Apple Virtualization are supported." } self = try PropertyListDecoder.utm.decode(UTMAppleConfiguration.self, from: data) } } // MARK: - UTM Data Structures struct UTMConfigurationStub: Decodable { var backend: String? var configurationVersion: Int? enum CodingKeys: String, CodingKey { case backend = "Backend" case configurationVersion = "ConfigurationVersion" } } struct UTMAppleConfiguration: Decodable { var backend: String? var configurationVersion: Int? var information: UTMConfigurationInfo var system: UTMAppleConfigurationSystem var virtualization: UTMAppleConfigurationVirtualization @DecodableDefault.EmptyList var sharedDirectories: [UTMAppleConfigurationSharedDirectory] var displays: [UTMAppleConfigurationDisplay] var drives: [UTMAppleConfigurationDrive] var networks: [UTMAppleConfigurationNetwork] private enum CodingKeys: String, CodingKey { case backend = "Backend" case configurationVersion = "ConfigurationVersion" case information = "Information" case system = "System" case virtualization = "Virtualization" case sharedDirectories = "SharedDirectory" case displays = "Display" case drives = "Drive" case networks = "Network" } } struct UTMConfigurationInfo: Decodable { var uuid: String var name: String private enum CodingKeys: String, CodingKey { case uuid = "UUID" case name = "Name" } } struct UTMAppleConfigurationSystem: Decodable { var architecture: String var cpuCount: Int var memorySize: Int var boot: UTMAppleConfigurationBoot var macPlatform: UTMAppleConfigurationMacPlatform? var genericPlatform: UTMAppleConfigurationGenericPlatform? private enum CodingKeys: String, CodingKey { case architecture = "Architecture" case cpuCount = "CPUCount" case memorySize = "MemorySize" case boot = "Boot" case macPlatform = "MacPlatform" case genericPlatform = "GenericPlatform" } } struct UTMAppleConfigurationBoot: Decodable { enum OperatingSystem: String, Codable { case none = "None" case linux = "Linux" case macOS = "macOS" } var operatingSystem: OperatingSystem var linuxKernelPath: String? var linuxCommandLine: String? var linuxInitialRamdiskPath: String? var efiVariableStoragePath: String? var hasUefiBoot: Bool private enum CodingKeys: String, CodingKey { case operatingSystem = "OperatingSystem" case linuxKernelPath = "LinuxKernelPath" case linuxCommandLine = "LinuxCommandLine" case linuxInitialRamdiskPath = "LinuxInitialRamdiskPath" case efiVariableStoragePath = "EfiVariableStoragePath" case hasUefiBoot = "UEFIBoot" } } struct UTMAppleConfigurationMacPlatform: Decodable { var hardwareModel: Data var machineIdentifier: Data var auxiliaryStoragePath: String? private enum CodingKeys: String, CodingKey { case hardwareModel = "HardwareModel" case machineIdentifier = "MachineIdentifier" case auxiliaryStoragePath = "AuxiliaryStoragePath" } } struct UTMAppleConfigurationGenericPlatform: Decodable { var machineIdentifier: Data? private enum CodingKeys: String, CodingKey { case machineIdentifier } } struct UTMAppleConfigurationSharedDirectory: Decodable { var bookmark: Data var isReadOnly: Bool private enum CodingKeys: String, CodingKey { case bookmark = "Bookmark" case isReadOnly = "ReadOnly" } } struct UTMAppleConfigurationDisplay: Decodable { var widthInPixels: Int var heightInPixels: Int var pixelsPerInch: Int var isDynamicResolution: Bool private enum CodingKeys: String, CodingKey { case widthInPixels = "WidthPixels" case heightInPixels = "HeightPixels" case pixelsPerInch = "PixelsPerInch" case isDynamicResolution = "DynamicResolution" } } struct UTMAppleConfigurationDrive: Decodable { var imageName: String? var isReadOnly: Bool var isNvme: Bool var identifier: String private enum CodingKeys: String, CodingKey { case imageName = "ImageName" case isReadOnly = "ReadOnly" case isNvme = "Nvme" case identifier = "Identifier" } } struct UTMAppleConfigurationNetwork: Decodable { enum NetworkMode: String, Codable { case shared = "Shared" case bridged = "Bridged" } var mode: NetworkMode var macAddress: String var bridgeInterface: String? private enum CodingKeys: String, CodingKey { case mode = "Mode" case macAddress = "MacAddress" case bridgeInterface = "BridgeInterface" } } struct UTMAppleConfigurationVirtualization: Decodable { enum PointerDevice: String, Codable { case disabled = "Disabled" case mouse = "Mouse" case trackpad = "Trackpad" } enum KeyboardDevice: String, Codable { case disabled = "Disabled" case generic = "Generic" case mac = "Mac" } var hasAudio: Bool var hasBalloon: Bool var hasEntropy: Bool var keyboard: KeyboardDevice var pointer: PointerDevice var hasRosetta: Bool? var hasClipboardSharing: Bool private enum CodingKeys: String, CodingKey { case hasAudio = "Audio" case hasBalloon = "Balloon" case hasEntropy = "Entropy" case keyboard = "Keyboard" case pointer = "Pointer" case hasTrackpad = "Trackpad" case rosetta = "Rosetta" case hasClipboardSharing = "ClipboardSharing" } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) hasAudio = try c.decode(Bool.self, forKey: .hasAudio) hasBalloon = try c.decode(Bool.self, forKey: .hasBalloon) hasEntropy = try c.decode(Bool.self, forKey: .hasEntropy) if let kbBool = try? c.decode(Bool.self, forKey: .keyboard) { keyboard = kbBool ? .generic : .disabled } else { keyboard = try c.decode(KeyboardDevice.self, forKey: .keyboard) } if let ptBool = try? c.decode(Bool.self, forKey: .pointer) { let tp = try c.decodeIfPresent(Bool.self, forKey: .hasTrackpad) ?? false pointer = tp ? .trackpad : (ptBool ? .mouse : .disabled) } else { pointer = try c.decode(PointerDevice.self, forKey: .pointer) } hasRosetta = try c.decodeIfPresent(Bool.self, forKey: .rosetta) hasClipboardSharing = try c.decode(Bool.self, forKey: .hasClipboardSharing) } } ================================================ FILE: VirtualCore/Source/Import/UTM/UTMImporter.swift ================================================ import Foundation import BuddyFoundation import UniformTypeIdentifiers import OSLog private let logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "UTMImporter") extension UTType { static let utmBundle = UTType(importedAs: "com.utmapp.utm") } struct UTMImporter: VMImporter { let appName = "UTM" let fileType = UTType.utmBundle @discardableResult func importVirtualMachine(from path: FilePath, into library: VMLibraryController) async throws -> VBVirtualMachine { let errorPrefix = "This \(appName) virtual machine has a configuration that’s not supported by VirtualBuddy." let configPath = path + "config.plist" try configPath.isFile.require("\(errorPrefix) Missing a config.plist file.") logger.debug("Config file path is \(configPath.string.quoted)") let dataPath = path + "Data" try dataPath.isDirectory.require("\(errorPrefix) Missing a \"Data\" directory.") logger.debug("Data path is \(dataPath.string.quoted)") let config = try UTMAppleConfiguration(path: configPath) logger.debug("Loaded UTM configuration with backend \(String(optional: config.backend?.quoted), privacy: .public), version \(String(optional: config.configurationVersion), privacy: .public)") let isMac = config.system.boot.operatingSystem != .linux try isMac.require("VirtualBuddy can only import Mac virtual machines from \(appName).") let drives = config.drives try (!drives.isEmpty).require("\(errorPrefix) No storage devices available. Needs at least a boot drive.") var model = try createBundle(forImportedVMPath: path, library: library) func createStorageDevice(for drive: UTMAppleConfigurationDrive, isBootDrive: Bool) throws -> VBStorageDevice { let imageName = try drive.imageName.require("\(errorPrefix) Boot drive is missing an image name.") let imageSource = dataPath + imageName logger.debug("Image source is \(imageSource)") try imageSource.isFile.require("\(errorPrefix) Storage device image not found at \(imageSource.string.quoted).") var image = if isBootDrive { VBManagedDiskImage.managedBootImage } else { VBManagedDiskImage(filename: imageSource.lastComponentWithoutExtension, size: 0) } image.size = UInt64(imageSource.fileSize ?? 0) /// ``VBManagedDiskImage/filename`` is the name without a file extension, which is respected by ``VBManagedDiskImage/managedBootImage``, but in UTM configs the image name includes the file extension. /// When importing from UTM, the file extension is removed from the image name and re-added here. let libraryImagePath = (FilePath(model.bundleURL) + image.filename).appendingExtension(image.format.fileExtension) logger.debug("Copying disk to VirtualBuddy bundle disk path \(libraryImagePath)") try imageSource.copy(libraryImagePath) return VBStorageDevice(utmDrive: drive, image: image, isBootDrive: isBootDrive) } if isMac { logger.debug("Detected Mac platform, gathering platform data files") let platform = try config.system.macPlatform.require("\(errorPrefix) Mac virtual machine is missing a Mac platform configuration.") let auxStorageName = platform.auxiliaryStoragePath ?? "AuxiliaryStorage" let auxStorageSource = dataPath + FilePath(auxStorageName) try auxStorageSource.isFile.require("\(errorPrefix) Mac platform file \(auxStorageName.quoted) was not found in \(dataPath.lastComponent.quoted) directory.") try auxStorageSource.copy(FilePath(model.auxiliaryStorageURL)) try platform.hardwareModel.write(to: model.hardwareModelURL) logger.debug("Wrote hardware model to \(model.hardwareModelURL.path)") try platform.machineIdentifier.write(to: model.machineIdentifierURL) logger.debug("Wrote machine identifier to \(model.machineIdentifierURL.path)") } model.metadata = VBVirtualMachine.Metadata(utm: config) model.configuration = VBMacConfiguration(utm: config) for (index, drive) in drives.enumerated() { logger.debug("Processing drive #\(index)") let device = try createStorageDevice(for: drive, isBootDrive: index == 0) model.configuration.hardware.storageDevices.append(device) } return model } } private extension VBVirtualMachine.Metadata { init(utm: UTMAppleConfiguration) { self.init( uuid: UUID(uuidString: utm.information.uuid) ?? UUID(), version: VBVirtualMachine.Metadata.currentVersion, installFinished: true, firstBootDate: nil, lastBootDate: nil, backgroundHash: .virtualBuddyBackground, remoteInstallImageURL: nil, installImageURL: nil ) } } private extension VBMacConfiguration { init(utm: UTMAppleConfiguration) { self.init( systemType: utm.system.boot.operatingSystem == .linux ? .linux : .mac, hardware: VBMacDevice(utm: utm), sharedFolders: [], guestAdditionsEnabled: true, rosettaSharingEnabled: utm.virtualization.hasRosetta == true, captureSystemKeys: true ) } } private extension VBMacDevice { private static let utmBytesInMib = UInt64(1048576) init(utm: UTMAppleConfiguration) { self.init( cpuCount: utm.system.cpuCount, memorySize: UInt64(utm.system.memorySize) * Self.utmBytesInMib, pointingDevice: utm.virtualization.pointer == .trackpad ? .trackpad : .mouse, keyboardDevice: utm.virtualization.keyboard == .mac ? .mac : .generic, displayDevices: utm.displays.map(VBDisplayDevice.init(utm:)), networkDevices: utm.networks.map(VBNetworkDevice.init(utm:)), soundDevices: [VBSoundDevice(utm: utm.virtualization)].compactMap { $0 }, storageDevices: [] ) } } private extension VBDisplayDevice { init(utm: UTMAppleConfigurationDisplay) { self.init( id: UUID(), name: "Default", width: utm.widthInPixels, height: utm.heightInPixels, pixelsPerInch: utm.pixelsPerInch, automaticallyReconfiguresDisplay: utm.isDynamicResolution ) } } private extension VBNetworkDevice { init(utm: UTMAppleConfigurationNetwork) { let kind: Kind = utm.mode == .bridged ? .bridge : .NAT let id: String = if kind == .bridge { if let interface = utm.bridgeInterface { interface } else { Self.automaticBridgeID } } else { Self.defaultID } self.init( id: id, name: "Default", kind: kind, macAddress: utm.macAddress ) } } private extension VBSoundDevice { init?(utm: UTMAppleConfigurationVirtualization) { guard utm.hasAudio else { return nil } self.init(enableOutput: utm.hasAudio, enableInput: utm.hasAudio) } } private extension VBStorageDevice { init(utmDrive: UTMAppleConfigurationDrive, image: VBManagedDiskImage, isBootDrive: Bool) { self.init( id: utmDrive.identifier, isBootVolume: isBootDrive, isEnabled: true, isReadOnly: utmDrive.isReadOnly, isUSBMassStorageDevice: false, backing: .managedImage(image) ) } } ================================================ FILE: VirtualCore/Source/Import/VMImporter+Helpers.swift ================================================ import Foundation import BuddyFoundation extension VMImporter { func createBundle(forImportedVMPath path: FilePath, library: VMLibraryController) throws -> VBVirtualMachine { let name = path.lastComponentWithoutExtension let vmURL = library.libraryURL .appendingPathComponent(name) .appendingPathExtension(VBVirtualMachine.bundleExtension) guard !vmURL.isReadableDirectory else { throw "You already have a virtual machine named \(name.quoted). If you’d like to import this virtual machine from \(appName), please rename it first." } let model = try VBVirtualMachine(bundleURL: vmURL) return model } } ================================================ FILE: VirtualCore/Source/Import/VMImporter.swift ================================================ import Foundation import BuddyFoundation import UniformTypeIdentifiers /// Adopted by types that can import virtual machines from other apps. @MainActor public protocol VMImporter { /// The name of the app that this importer can import VM bundles from. var appName: String { get } /// The UTI for the virtual machine bundle that can be imported by this importer. var fileType: UTType { get } /// Performs all actions required to convert the virtual machine from the other app into VirtualBuddy, including copying it into the user's library. /// /// - note: Importers don't need to call `saveMetadata` on the VM model, this is done automatically after a successful import. @discardableResult func importVirtualMachine(from path: FilePath, into library: VMLibraryController) async throws -> VBVirtualMachine } ================================================ FILE: VirtualCore/Source/Import/VMImporterRegistry.swift ================================================ import Foundation import BuddyFoundation import OSLog import UniformTypeIdentifiers private let logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "VMImporterRegistry") /// Keeps track of available importers and helps find the importer to use for a given external VM bundle. @MainActor public struct VMImporterRegistry { public static let `default` = VMImporterRegistry() private let importers: [VMImporter] = [ UTMImporter() ] public var supportedFileTypes: Set { Set(importers.map(\.fileType)) } /// Returns the importer that can handle the file at the specified path. public func importer(for filePath: FilePath) -> VMImporter? { logger.debug("Look up importer for \(filePath)") guard let uti = filePath.contentType else { logger.error("Couldn't determine UTI for importing \(filePath)") return nil } guard let importer = importers.first(where: { uti.conforms(to: $0.fileType) }) else { logger.notice("No importer found for type \(uti.identifier, privacy: .public)") return nil } logger.notice("Matched importer \(importer.appName.quoted, privacy: .public) for type \(uti.identifier, privacy: .public)") return importer } } ================================================ FILE: VirtualCore/Source/Models/BlurHashToken.swift ================================================ import Foundation /// Combination of blur hash number of components with the blur hash string itself. /// /// This is used when a blur hash must be stored and it's possible that the number of components /// will change between different sources of the blur hash, ensuring that clients rendering the blur hash /// image always use the correct number of components. public struct BlurHashToken: Hashable, Codable, Sendable, ProvidesEmptyPlaceholder { public var value: String public var size: Int public init(value: String, size: Int = .vbBlurHashSize) { self.value = value self.size = size } public static let empty = BlurHashToken.virtualBuddyBackground } public extension Int { /// The size of blur hash used by VirtualBuddy. static let vbBlurHashSize = 4 } public extension BlurHashToken { /// Hardcoded VirtualBuddy orange background blur hash. static let virtualBuddyBackground = BlurHashToken( value: "U4H09BEfIY$%U7ocVcM$8%R*M}f~zwIXcArd", size: 4 ) /// Hardcoded VirtualBuddy background blur hash for Linux VMs. static let virtualBuddyBackgroundLinux = BlurHashToken( value: "UsLn]CG1I;t19uxAR$jJOXjEj=ayn%fkjubI", size: 4 ) } ================================================ FILE: VirtualCore/Source/Models/Configuration/ConfigurationModels+Summary.swift ================================================ // // ConfigurationModels+Summary.swift // VirtualCore // // Created by Guilherme Rambo on 19/07/22. // import Foundation public extension VBMacConfiguration { var generalSummary: String { "\(hardware.cpuCount) CPUs / \(hardware.memorySize / 1024 / 1024 / 1024) GB" } var storageSummary: String { if hardware.storageDevices.count > 1 { return "\(hardware.storageDevices.count) Devices" } else { return "Boot Only" } } var displaySummary: String { guard let display = hardware.displayDevices.first else { return "No Displays" } return "\(display.width)x\(display.height)x\(display.pixelsPerInch)" } var soundSummary: String { guard let sound = hardware.soundDevices.first else { return "No Sound" } return sound.enableInput ? "Input / Output" : "Output Only" } var sharingSummary: String { let foldersSum: String if sharedFolders.count > 1 { foldersSum = "\(sharedFolders.count) Folders" } else if sharedFolders.isEmpty { foldersSum = "" } else { foldersSum = "One Folder" } return foldersSum.isEmpty ? "None" : foldersSum } var networkSummary: String { guard let network = hardware.networkDevices.first else { return "No Network" } return network.kind.name } var pointingDeviceSummary: String { hardware.pointingDevice.kind.name } var keyboardDeviceSummary: String { hardware.keyboardDevice.kind.name } var guestAppSummary: String { guestAdditionsEnabled ? "Enabled" : "Disabled" } } ================================================ FILE: VirtualCore/Source/Models/Configuration/ConfigurationModels+Validation.swift ================================================ // // ConfigurationModels+Validation.swift // VirtualCore // // Created by Guilherme Rambo on 19/07/22. // import Cocoa import UniformTypeIdentifiers import Virtualization public extension VBMacConfiguration { func validate(for model: VBVirtualMachine, skipVirtualizationConfig: Bool) async -> SupportState { var tempModel = model tempModel.configuration = self guard !skipVirtualizationConfig else { return hostSupportState } do { let config = try await VMInstance.makeConfiguration(for: tempModel) try config.validate() return hostSupportState } catch { return hostSupportState.merged(with: .unsupported([error.localizedDescription])) } } } public extension VBMacConfiguration { /// The state of this configuration for the current host, used to indicate /// possible issues the user may have with it, or to prevent unsupported /// configurations from being saved. var hostSupportState: SupportState { var warnings = [String]() var errors = [String]() if !hardware.pointingDevice.kind.isSupportedByHost { errors.append("\(hardware.pointingDevice.kind.name) requires macOS 13 or later.") } if hasSharedFolders { if VBMacConfiguration.isFileSharingSupported { warnings.append(VBMacConfiguration.fileSharingNotice) } else { errors.append(VBMacConfiguration.fileSharingNotice) } } if hardware.networkDevices.contains(where: { $0.kind == .bridge }), !VBNetworkDevice.appSupportsBridgedNetworking { errors.append(VBNetworkDevice.bridgeUnsupportedMessage) } return SupportState(errors: errors, warnings: warnings) } static let isFileSharingSupported = true static let rosettaSupported: Bool = { VZLinuxRosettaDirectoryShare.availability != VZLinuxRosettaAvailability.notSupported }() static func rosettaInstalled() -> Bool { VZLinuxRosettaDirectoryShare.availability == VZLinuxRosettaAvailability.installed } static let fileSharingNotice: String = { let tip = "For older versions, you can use the standard macOS file sharing feature in System Preferences > Sharing." if isFileSharingSupported { return "File sharing requires the virtual machine to be running macOS 13 or later. \(tip)" } else { return "File sharing requires both the host Mac and the virtual machine to be running macOS 13 or later. \(tip)" } }() static func rosettaSharingNotice() -> String? { if rosettaSupported { if rosettaInstalled() { return nil } else { return "Rosetta is not installed. Run `softwareupdate --install-rosetta` to install Rosetta." } } else { return "Rosetta for Linux requires the host Mac to be running macOS 13 or later." } } } public extension VBMacConfiguration.SupportState { var errors: [String] { guard case .unsupported(let errors) = self else { return [] } return errors } var warnings: [String] { guard case .warnings(let warnings) = self else { return [] } return warnings } var allowsSaving: Bool { errors.isEmpty } init(errors: [String] = [], warnings: [String] = []) { if errors.isEmpty { if warnings.isEmpty { self = .supported } else { self = .warnings(warnings) } } else { self = .unsupported(errors) } } func merged(with other: Self) -> Self { Self.init(errors: errors + other.errors, warnings: warnings + other.warnings) } } public extension VBNetworkDevice { static var appSupportsBridgedNetworking: Bool { NSApplication.shared.hasEntitlement("com.apple.vm.networking") } static let bridgeUnsupportedMessage = "Bridged network devices are not available in this build of the app." } public extension VBDisplayDevice { static var automaticallyReconfiguresDisplaySupportedByHost: Bool { if #available(macOS 14.0, *) { return true } else { return false } } } public extension VBPointingDevice.Kind { var warning: String? { guard self == .trackpad else { return nil } return "Trackpad is only recognized by VMs running macOS 13 and later." } var error: String? { guard !isSupportedByHost else { return nil } return "Trackpad requires both host and VM to be on macOS 13 or later." } var isSupportedByHost: Bool { true } } public extension VBKeyboardDevice.Kind { var warning: String? { guard self != .generic else { return nil } return "Mac keyboard is only recognized by VMs running macOS 13 and later." } var error: String? { guard !isSupportedByHost else { return nil } return "Mac keyboard requires macOS 14 or later on host and macOS 13 or later on VM." } var isSupportedByHost: Bool { switch self { case .generic: return true case .mac: if #available(macOS 14.0, *) { return true } else { return false } } } } public extension VBGuestType { var isSupportedByHost: Bool { true } static let supportedByHost: [VBGuestType] = { allCases.filter(\.isSupportedByHost) }() var supportsVirtualTrackpad: Bool { self == .mac } var supportsKeyboardCustomization: Bool { self == .mac } var supportsDisplayPPI: Bool { self == .mac } var supportsRosettaMount: Bool { self == .linux } var supportsStateRestoration: Bool { self == .mac } var supportedRestoreImageTypes: Set { switch self { case .mac: return [.ipsw] case .linux: return [.iso, .img] } } var supportsGuestApp: Bool { self == .mac } } public extension VBVirtualMachine { var supportsStateRestoration: Bool { configuration.systemType.supportsStateRestoration } } public extension UTType { static let ipsw = UTType(filenameExtension: "ipsw")! static let iso = UTType(filenameExtension: "iso")! static let img = UTType(filenameExtension: "img")! } ================================================ FILE: VirtualCore/Source/Models/Configuration/ConfigurationModels.swift ================================================ // // ConfigurationModels.swift // VirtualCore // // Created by Guilherme Rambo on 17/07/22. // import Foundation import SystemConfiguration /** ## Note to contributors: Care must be taken when changing any of the structs in this file that conform to `Codable`, since users may have VMs configured using older versions of the structs. Failure to decode the configuration after updates to how it's stored can result in data loss. In order to keep backwards-compatibility for new properties without having to make everything optional, the `@DecodableDefault` property wrapper can be used. */ public enum VBGuestType: String, Identifiable, Codable, CaseIterable, ProvidesEmptyPlaceholder { public var id: RawValue { rawValue } case mac case linux public static var empty: VBGuestType { .mac } } public struct VBMacConfiguration: Hashable, Codable { public enum SupportState: Hashable { case supported case warnings([String]) case unsupported([String]) } public static let currentVersion = 0 @DecodableDefault.Zero public var version = VBMacConfiguration.currentVersion @DecodableDefault.FirstCase public var systemType: VBGuestType = .mac public var hardware = VBMacDevice.default public var sharedFolders = [VBSharedFolder]() @DecodableDefault.True public var guestAdditionsEnabled = true @DecodableDefault.False public var rosettaSharingEnabled = false @DecodableDefault.True public var captureSystemKeys = true public var hasSharedFolders: Bool { !sharedFolders.filter(\.isEnabled).isEmpty } } // MARK: - Hardware Configuration /// Configures a disk image that's managed by VirtualBuddy, as opposed to a disk image that the user provides. /// **Read the note at the top of this file before modifying this** public struct VBManagedDiskImage: Identifiable, Hashable, Codable { public init(id: String = UUID().uuidString, filename: String, size: UInt64, format: VBManagedDiskImage.Format = .sparse) { self.id = id self.filename = filename self.size = size self.format = format } public static let defaultBootDiskImageSize: UInt64 = 64 * .storageGigabyte public static let minimumBootDiskImageSize: UInt64 = 2 * .storageGigabyte public static let maximumBootDiskImageSize: UInt64 = 8 * .storageTerabyte public static let minimumExtraDiskImageSize: UInt64 = 1 * .storageGigabyte public static let maximumExtraDiskImageSize: UInt64 = 512 * .storageGigabyte public enum Format: Int, Codable { case raw case dmg case sparse case asif /// The default format for the boot disk image in the current environment. static var defaultBootDisk: Format { if #available(macOS 26, *), VBSettings.current.bootDiskImagesUseASIF { .asif } else { .raw } } var fileExtension: String { switch self { case .raw: "img" case .dmg: "dmg" case .sparse: "sparseimage" case .asif: "asif" } } public var isSupported: Bool { switch self { case .raw, .dmg, .sparse: true case .asif: if #available(macOS 26, *) { true } else { false } } } } public var id: String = UUID().uuidString public var filename: String public var size: UInt64 public var format: Format = .sparse // Not a stored property because Format.defaultBootDisk can change based on user preferences. public static var managedBootImage: VBManagedDiskImage { VBManagedDiskImage( id: "__BOOT__", filename: "Disk", size: Self.defaultBootDiskImageSize, format: .defaultBootDisk ) } public static var template: VBManagedDiskImage { VBManagedDiskImage( filename: RandomNameGenerator.shared.newName(), size: VBManagedDiskImage.minimumExtraDiskImageSize, format: .raw ) } } /// Configures a storage device. /// **Read the note at the top of this file before modifying this** public struct VBStorageDevice: Identifiable, Hashable, Codable { public init(id: String = UUID().uuidString, isBootVolume: Bool, isEnabled: Bool = true, isReadOnly: Bool, isUSBMassStorageDevice: Bool, backing: VBStorageDevice.BackingStore) { self.id = id self.isBootVolume = isBootVolume self.isEnabled = isEnabled self.isReadOnly = isReadOnly self.isUSBMassStorageDevice = isUSBMassStorageDevice self.backing = backing } /// The underlying storage for the device, which currently can be either a custom disk image, /// or a disk image managed by VirtualBuddy. public enum BackingStore: Hashable, Codable { /// Image created and managed by VirtualBuddy. case managedImage(VBManagedDiskImage) /// Arbitrary image provided by the user, file must exist on disk at the same location /// if the image is to be used again in the future. case customImage(URL) } public var id: String = UUID().uuidString /// `true` for the initial boot volume (Disk.img) that's created by VirtualBuddy. public internal(set) var isBootVolume: Bool /// Setting to `false` disables the storage device without removing it from the VM. @DecodableDefault.True public var isEnabled: Bool /// `true` if this storage device represents a clone created for a virtual machine save state. @DecodableDefault.False public var isSavedStateClone: Bool /// `true` when the device can't be written to by the VM. public var isReadOnly: Bool /// `true` when the device represents an external USB mass storage device in the guest OS. public var isUSBMassStorageDevice: Bool /// The underlying storage for the storage device, which can currently be a disk image managed /// by VirtualBuddy, or a custom image provided by the user. public var backing: BackingStore public static var defaultBootDevice: VBStorageDevice { VBStorageDevice( isBootVolume: true, isReadOnly: false, isUSBMassStorageDevice: false, backing: .managedImage(.managedBootImage) ) } public static var template: VBStorageDevice { let name = RandomNameGenerator.shared.newName() let image = VBManagedDiskImage( filename: name, size: VBManagedDiskImage.minimumExtraDiskImageSize, format: .sparse ) return VBStorageDevice( isBootVolume: false, isReadOnly: false, isUSBMassStorageDevice: false, backing: .managedImage(image) ) } public var displayName: String { guard !isBootVolume else { return "Boot" } switch backing { case .customImage(let url): return url.deletingPathExtension().lastPathComponent case .managedImage(let image): return image.filename } } } /// Configures a display device. /// **Read the note at the top of this file before modifying this** public struct VBDisplayDevice: Identifiable, Hashable, Codable { public init(id: UUID = UUID(), name: String = "Default", width: Int = 1920, height: Int = 1080, pixelsPerInch: Int = 144, automaticallyReconfiguresDisplay: Bool = false) { self.id = id self.name = name self.width = width self.height = height self.pixelsPerInch = pixelsPerInch self.automaticallyReconfiguresDisplay = automaticallyReconfiguresDisplay } public var id = UUID() public var name = "Default" public var width = 1920 public var height = 1080 public var pixelsPerInch = 144 @DecodableDefault.False public var automaticallyReconfiguresDisplay = false } /// Configures a network device. /// **Read the note at the top of this file before modifying this** public struct VBNetworkDevice: Identifiable, Hashable, Codable { public static let defaultID = "Default" public static let automaticBridgeID = "Automatic Bridge" public init(id: String = VBNetworkDevice.defaultID, name: String = "Default", kind: VBNetworkDevice.Kind = Kind.NAT, macAddress: String = VZMACAddress.randomLocallyAdministered().string.uppercased()) { self.id = id self.name = name self.kind = kind self.macAddress = macAddress } public enum Kind: Int, Identifiable, CaseIterable, Codable { public var id: RawValue { rawValue } case NAT case bridge public var name: String { switch self { case .NAT: return "NAT" case .bridge: return "Bridge" } } } public var id = VBNetworkDevice.defaultID public var name = "Default" public var kind = Kind.NAT public var macAddress = VZMACAddress.randomLocallyAdministered().string.uppercased() } /// Configures a pointing device, such as a mouse or trackpad. /// **Read the note at the top of this file before modifying this** public struct VBPointingDevice: Hashable, Codable { public enum Kind: Int, Identifiable, CaseIterable, Codable { public var id: RawValue { rawValue } case mouse case trackpad public var name: String { switch self { case .mouse: return "Mouse" case .trackpad: return "Trackpad" } } } public var kind = Kind.mouse } /// Configures a keyboard device. /// **Read the note at the top of this file before modifying this** public struct VBKeyboardDevice: Hashable, Codable, ProvidesEmptyPlaceholder { public enum Kind: Int, Identifiable, CaseIterable, Codable { public var id: RawValue { rawValue } case generic case mac public var name: String { switch self { case .generic: return "Generic" case .mac: return "Mac" } } } public var kind = Kind.generic public static var empty: VBKeyboardDevice { VBKeyboardDevice(kind: .generic) } } /// Configures sound input/output. /// **Read the note at the top of this file before modifying this** public struct VBSoundDevice: Identifiable, Hashable, Codable { public var id = UUID() public var name = "Default" public var enableOutput = true public var enableInput = true } /// Describes a Mac VM with its associated hardware configuration. /// **Read the note at the top of this file before modifying this** public struct VBMacDevice: Hashable, Codable { public init(cpuCount: Int, memorySize: UInt64, pointingDevice: VBPointingDevice, keyboardDevice: VBKeyboardDevice, displayDevices: [VBDisplayDevice], networkDevices: [VBNetworkDevice], soundDevices: [VBSoundDevice], storageDevices: [VBStorageDevice], NVRAM: [VBNVRAMVariable] = [VBNVRAMVariable]()) { self.cpuCount = cpuCount self.memorySize = memorySize self.pointingDevice = pointingDevice self.keyboardDevice = keyboardDevice self.displayDevices = displayDevices self.networkDevices = networkDevices self.soundDevices = soundDevices self.storageDevices = storageDevices self.NVRAM = NVRAM } public var cpuCount: Int public var memorySize: UInt64 public var pointingDevice: VBPointingDevice @DecodableDefault.EmptyPlaceholder public var keyboardDevice: VBKeyboardDevice public var displayDevices: [VBDisplayDevice] public var networkDevices: [VBNetworkDevice] public var soundDevices: [VBSoundDevice] public var NVRAM = [VBNVRAMVariable]() public var storageDevices: [VBStorageDevice] { /// Special handling for migration from previous versions. /// Ensures all VMs have the boot storage device set if no storage devices are /// present in the loaded configuration. get { _storageDevices ?? [.defaultBootDevice] } set { _storageDevices = newValue } } private var _storageDevices: [VBStorageDevice]? = nil mutating func addMissingBootDeviceIfNeeded() { guard _storageDevices == nil else { return } _storageDevices = [.defaultBootDevice] } } // MARK: - Sharing And Other Features /// Configures a folder that's shared between the host and the guest. /// **Read the note at the top of this file before modifying this** public struct VBSharedFolder: Identifiable, Hashable, Codable { /// The name the VirtualBuddy share will have in the guest OS. /// /// This is the name that must be used with the `mount` command, like so: /// ``` /// mkdir -p ~/Desktop/VirtualBuddyShared && mount -t virtiofs VirtualBuddyShared ~/Desktop/VirtualBuddyShared /// ``` public static let virtualBuddyShareName = "VirtualBuddyShared" public static let rosettaShareName = "Rosetta" public init(id: UUID = UUID(), url: URL, isEnabled: Bool = true, isReadOnly: Bool = false, customMountPointName: String? = nil) { self.id = id self.url = url self.isEnabled = isEnabled self.isReadOnly = isReadOnly self.customMountPointName = customMountPointName } public var id = UUID() public var name: String { url.lastPathComponent } public var url: URL @DecodableDefault.True public var isEnabled = true public var isReadOnly = false /// A custom name for the folder when mounted in the guest OS. public var customMountPointName: String? = nil /// The default name for the folder when mounted in the guest OS var mountPointName: String { url.lastPathComponent } /// The effective name this folder will have when mounted in the guest OS. public var effectiveMountPointName: String { customMountPointName ?? mountPointName } } public extension VBMacConfiguration { func hasSharedFolder(with url: URL) -> Bool { sharedFolders.contains(where: { $0.url.path == url.path }) } @discardableResult mutating func addSharedFolder(with url: URL) throws -> VBSharedFolder { guard url.isReadableDirectory else { throw Failure("VirtualBuddy couldn't access the selected location, or it is not a directory.") } guard !hasSharedFolder(with: url) else { throw Failure("That directory is already in the shared folders.") } /// Figure out how many "Folder", "Folder 1", "Folder 2", and so on we have in the shared folders collection. let conflictingMountPointCount = sharedFolders.filter { $0.mountPointName.trimmingCharacters(in: .decimalDigits.union(.whitespacesAndNewlines)).hasPrefix(url.lastPathComponent) }.count let customMountPointName: String? if conflictingMountPointCount > 0 { customMountPointName = "\(url.lastPathComponent) \(conflictingMountPointCount + 1)" } else { customMountPointName = nil } let folder = VBSharedFolder(url: url, customMountPointName: customMountPointName) sharedFolders.append(folder) return folder } mutating func removeSharedFolders(with identifiers: Set) { sharedFolders.removeAll(where: { identifiers.contains($0.id) }) } func hasSharedFolders(inVolume volumeURL: URL) -> Bool { sharedFolders.contains(where: { $0.externalVolumeURL == volumeURL }) } } public extension VBSharedFolder { var shortName: String { if url.path.hasPrefix(NSHomeDirectory()) { return url.lastPathComponent } else { return url.path } } var shortNameForDialogs: String { url.lastPathComponent } var externalVolumeURL: URL? { url.externalVolumeURL } var errorMessage: String? { guard !url.isReadableDirectory else { return nil } if let externalVolumeURL, !externalVolumeURL.isReadableDirectory { return "This directory is in a removable volume that's not currently available." } else { return "This directory doesn't exist, or VirtualBuddy can't read it right now." } } var isAvailable: Bool { url.isReadableDirectory } } public extension URL { var isReadableDirectory: Bool { var isDir = ObjCBool(false) guard FileManager.default.fileExists(atPath: path, isDirectory: &isDir), isDir.boolValue else { return false } return true } } // MARK: - Default Devices public extension VBMacConfiguration { static var `default`: VBMacConfiguration { .init() } func guestType(_ type: VBGuestType) -> Self { var mSelf = self mSelf.systemType = type return mSelf } } public extension VBMacDevice { static var `default`: VBMacDevice { VBMacDevice( cpuCount: .vb_suggestedVirtualCPUCount, memorySize: .vb_suggestedMemorySize, pointingDevice: .default, keyboardDevice: .default, displayDevices: [.default], networkDevices: [.default], soundDevices: [.default], storageDevices: [.defaultBootDevice] ) } } public extension VBPointingDevice { static var `default`: VBPointingDevice { .init() } static let mouse = VBPointingDevice(kind: .mouse) static let trackpad = VBPointingDevice(kind: .mouse) } public extension VBKeyboardDevice { static var `default`: VBKeyboardDevice { .empty } static let generic = VBKeyboardDevice(kind: .generic) static let mac = VBKeyboardDevice(kind: .mac) } public extension VBNetworkDevice { static var `default`: VBNetworkDevice { .init() } } public extension VBSoundDevice { static var `default`: VBSoundDevice { .init() } } public extension VBDisplayDevice { static var `default`: VBDisplayDevice { .matchHost } static var fallback: VBDisplayDevice { .init() } static var matchHost: VBDisplayDevice { guard let screen = NSScreen.main else { return .fallback } let resolution = screen.dpi guard let size = screen.deviceDescription[.size] as? NSSize else { return .fallback } let pointHeight = size.height - screen.safeAreaInsets.top return VBDisplayDevice( id: UUID(), name: ProcessInfo.processInfo.vb_hostName, width: Int(size.width * screen.backingScaleFactor), height: Int(pointHeight * screen.backingScaleFactor), pixelsPerInch: Int(resolution.width) ) } static var sizeToFit: VBDisplayDevice { guard let screen = NSScreen.main, let size = screen.deviceDescription[.size] as? NSSize else { return .fallback } let reference = VZMacGraphicsDisplayConfiguration(for: screen, sizeInPoints: size) return VBDisplayDevice( id: UUID(), name: ProcessInfo.processInfo.vb_hostName, width: reference.widthInPixels, height: reference.heightInPixels, pixelsPerInch: reference.pixelsPerInch ) } } // MARK: - Presets public struct VBDisplayPreset: Identifiable, Hashable { public var id: String { name } public var name: String public var defaultForGuestType: VBGuestType? = nil public var limitToGuestType: VBGuestType? = nil public var device: VBDisplayDevice public var warning: String? = nil public var isAvailable: () -> Bool = { true } public func hash(into hasher: inout Hasher) { hasher.combine(id) } public static func ==(lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id } } public extension VBDisplayPreset { static let fullHD = VBDisplayPreset( name: "Full HD", defaultForGuestType: .linux, device: VBDisplayDevice( name: "1920x1080@144", width: 1920, height: 1080, pixelsPerInch: 144 ) ) static let fourK = VBDisplayPreset( name: "4K", device: .init( name: "3840x2160", width: 3840, height: 2160 ) ) static let four5K = VBDisplayPreset( name: "4.5K", limitToGuestType: .linux, device: .init( name: "4480x2520@72", width: 4480, height: 2520, pixelsPerInch: 72 ) ) static let four5KRetina = VBDisplayPreset( name: "4.5K Retina", defaultForGuestType: .mac, limitToGuestType: .mac, device: .init( name: "4480x2520@218", width: 4480, height: 2520, pixelsPerInch: 218 ) ) static let matchMainDisplay = VBDisplayPreset( name: "Match \"\(ProcessInfo.processInfo.vb_mainDisplayName)\"", device: .matchHost, warning: "If things look small in the VM after boot, go to System Preferences and select a HiDPI scaled reslution for the display.", isAvailable: { /// This preset is only relevant for displays with a notch. ProcessInfo.processInfo.vb_mainDisplayHasNotch }) static let sizeToFitMainDisplay = VBDisplayPreset( name: "Size to fit in \"\(ProcessInfo.processInfo.vb_mainDisplayName)\"", device: .sizeToFit ) static let presets: [VBDisplayPreset] = [ .fullHD, .fourK, .four5K, .four5KRetina, .matchMainDisplay, .sizeToFitMainDisplay, ] static func availablePresets(for guestType: VBGuestType) -> [VBDisplayPreset] { presets.filter { $0.isAvailable() && $0.limitToGuestType == nil || $0.limitToGuestType == guestType } } static func defaultPreset(for guestType: VBGuestType) -> VBDisplayPreset { presets.first(where: { $0.isAvailable() && $0.defaultForGuestType == guestType }) ?? presets[0] } } public struct VBNetworkDeviceInterface: Identifiable, Hashable { public var id: String public var name: String } extension VBNetworkDeviceInterface { init(_ interface: VZBridgedNetworkInterface) { self.id = interface.identifier self.name = interface.localizedDisplayName ?? interface.identifier } } public extension VBNetworkDeviceInterface { static let automatic = VBNetworkDeviceInterface(id: VBNetworkDevice.automaticBridgeID, name: "Automatic") } public extension VBNetworkDevice { static var defaultBridgeInterfaceID: String? { VZBridgedNetworkInterface.networkInterfaces.first?.identifier } static var bridgeInterfaces: [VBNetworkDeviceInterface] { VZBridgedNetworkInterface.networkInterfaces.map(VBNetworkDeviceInterface.init) } } // MARK: - Helpers public extension UInt64 { static let storageGigabyte = UInt64(1024 * 1024 * 1024) static let storageMegabyte = UInt64(1024 * 1024) static let storageTerabyte = storageGigabyte * 1024 } public extension VBStorageDevice { static func validationError(for name: String) -> String? { guard !name.isEmpty else { return "Name can't be empty." } do { try VZVirtioBlockDeviceConfiguration.validateBlockDeviceIdentifier(name) return nil } catch { return error.localizedDescription } } static var hostSupportsUSBMassStorage: Bool { true } func diskImageExists(for container: VBStorageDeviceContainer) -> Bool { let url = container.diskImageURL(for: self) return FileManager.default.fileExists(atPath: url.path) } } public extension VBNetworkDevice { static func validateMAC(_ address: String) -> Bool { VZMACAddress(string: address) != nil } } public extension VBMacDevice { static let minimumCPUCount: Int = VZVirtualMachineConfiguration.minimumAllowedCPUCount static let maximumCPUCount: Int = { min(ProcessInfo.processInfo.processorCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount) }() static let virtualCPUCountRange: ClosedRange = { minimumCPUCount...maximumCPUCount }() static let minimumMemorySizeInGigabytes = 2 static let maximumMemorySizeInGigabytes: Int = { let value = Swift.min(ProcessInfo.processInfo.physicalMemory, VZVirtualMachineConfiguration.maximumAllowedMemorySize) return Int(value / 1024 / 1024 / 1024) }() static let memorySizeRangeInGigabytes: ClosedRange = { minimumMemorySizeInGigabytes...maximumMemorySizeInGigabytes }() } public extension VBDisplayDevice { static let minimumDisplayWidth = 800 static let minimumDisplayHeight = 600 static var maximumDisplayWidth = 6016 static var maximumDisplayHeight = 3384 static let displayWidthRange: ClosedRange = { minimumDisplayWidth...maximumDisplayWidth }() static let displayHeightRange: ClosedRange = { minimumDisplayHeight...maximumDisplayHeight }() static let minimumDisplayPPI = 72 static let maximumDisplayPPI = 218 static let displayPPIRange: ClosedRange = { minimumDisplayPPI...maximumDisplayPPI }() } extension Int { static let vb_suggestedVirtualCPUCount: Int = { let totalAvailableCPUs = ProcessInfo.processInfo.processorCount var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs / 2 virtualCPUCount = Swift.max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount) virtualCPUCount = Swift.min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount) return virtualCPUCount }() } extension UInt64 { static let vb_suggestedMemorySize: UInt64 = { let hostMemory = ProcessInfo.processInfo.physicalMemory var memorySize = hostMemory / 2 memorySize = Swift.max(memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize) memorySize = Swift.min(memorySize, VZVirtualMachineConfiguration.maximumAllowedMemorySize) return memorySize }() } public extension ProcessInfo { var vb_hostName: String { SCDynamicStoreCopyComputerName(nil, nil) as? String ?? "This Mac" } var vb_mainDisplayName: String { guard let screen = NSScreen.main else { return "\(vb_hostName)" } return screen.localizedName } var vb_mainDisplayHasNotch: Bool { NSScreen.main?.auxiliaryTopLeftArea != nil } } public extension NSScreen { var dpi: CGSize { (deviceDescription[NSDeviceDescriptionKey.resolution] as? CGSize) ?? CGSize(width: 72.0, height: 72.0) } } ================================================ FILE: VirtualCore/Source/Models/Configuration/DecodableDefault.swift ================================================ import Foundation public protocol DecodableDefaultSource { associatedtype Value: Decodable static var defaultValue: Value { get } } public enum DecodableDefault {} public extension DecodableDefault { @propertyWrapper struct Wrapper { public typealias Value = Source.Value public var wrappedValue = Source.defaultValue public init(wrappedValue: Value = Source.defaultValue) { self.wrappedValue = wrappedValue } } } extension DecodableDefault.Wrapper: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = try container.decode(Value.self) } } public extension KeyedDecodingContainer { func decode(_ type: DecodableDefault.Wrapper.Type, forKey key: Key) throws -> DecodableDefault.Wrapper { try decodeIfPresent(type, forKey: key) ?? .init() } } public protocol ProvidesEmptyPlaceholder: Codable { static var empty: Self { get } } public extension DecodableDefault { typealias Source = DecodableDefaultSource typealias List = Decodable & ExpressibleByArrayLiteral typealias Map = Decodable & ExpressibleByDictionaryLiteral typealias Enum = Decodable & CaseIterable enum Sources { public enum Zero: Source { public static var defaultValue: Int { 0 } } public enum True: Source { public static var defaultValue: Bool { true } } public enum False: Source { public static var defaultValue: Bool { false } } public enum EmptyString: Source { public static var defaultValue: String { "" } } public enum EmptyList: Source { public static var defaultValue: T { [] } } public enum EmptyMap: Source { public static var defaultValue: T { [:] } } public enum EmptyPlaceholder: Source { public static var defaultValue: T { .empty } } public enum FirstCase: Source { public static var defaultValue: T { T.allCases.first! } } } } public extension DecodableDefault { typealias Zero = Wrapper typealias True = Wrapper typealias False = Wrapper typealias EmptyString = Wrapper typealias EmptyList = Wrapper> typealias EmptyMap = Wrapper> typealias EmptyPlaceholder = Wrapper> typealias FirstCase = Wrapper> } extension DecodableDefault.Wrapper: Equatable where Value: Equatable {} extension DecodableDefault.Wrapper: Hashable where Value: Hashable {} extension DecodableDefault.Wrapper: Encodable where Value: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(wrappedValue) } } ================================================ FILE: VirtualCore/Source/Models/Configuration/VBMacDevice+Storage.swift ================================================ // // VBMacDevice+Storage.swift // VirtualCore // // Created by Guilherme Rambo on 20/07/22. // import Foundation public extension VBMacDevice { mutating func addOrUpdate(_ storage: VBStorageDevice) { if let idx = storageDevices.firstIndex(where: { $0.id == storage.id }) { storageDevices[idx] = storage } else { storageDevices.append(storage) } } } ================================================ FILE: VirtualCore/Source/Models/SavedState/VBSavedStateMetadata+Clone.swift ================================================ import Foundation extension VBSavedStateMetadata { static func createStorageDeviceClones(packageURL: URL, model: VBVirtualMachine) async throws -> [VBStorageDevice] { let inputDevices = model.configuration.hardware.storageDevices var outputDevices = [VBStorageDevice]() for var device in inputDevices { guard case .managedImage = device.backing else { /// Custom images are arbirary, may be anywhere on disk or external storage. /// Such images are managed by the user, not the app, and thus are not cloned when saving state. outputDevices.append(device) continue } let inputURL: URL = model.diskImageURL(for: device) let cloneURL: URL = packageURL.appending(path: inputURL.lastPathComponent) try FileManager.default.copyItem(at: inputURL, to: cloneURL) device.isSavedStateClone = true outputDevices.append(device) } return outputDevices } mutating func createStorageDeviceClones(packageURL: URL, model: VBVirtualMachine) async throws { storageDevices = try await Self.createStorageDeviceClones(packageURL: packageURL, model: model) } } ================================================ FILE: VirtualCore/Source/Models/SavedState/VBSavedStateMetadata.swift ================================================ import Foundation import BuddyFoundation public struct VBSavedStateMetadata: Identifiable, Hashable, Codable { public var id: UUID public var vmUUID: UUID public var date: Date public var appVersion: SoftwareVersion public var appBuild: Int public var hostECID: UInt64? /// Copy of ``VBMacDevice/storageDevices`` as those existed at the time the snapshot was taken. /// The ``VBStorageDevice/isSavedStateClone`` property is set to `true` once the state has been saved. /// Only managed disk images are cloned alongside saved states, custom user-provided images are referenced from their original locations. @DecodableDefault.EmptyList public var storageDevices: [VBStorageDevice] init(id: UUID, vmUUID: UUID, date: Date, appVersion: SoftwareVersion, appBuild: Int, hostECID: UInt64? = nil, storageDevices: [VBStorageDevice]) { self.id = id self.vmUUID = vmUUID self.date = date self.appVersion = appVersion self.appBuild = appBuild self.hostECID = hostECID self.storageDevices = storageDevices } } // MARK: - Saved State Metadata Creation public extension VBSavedStateMetadata { init(model: VBVirtualMachine) { let ecid = ProcessInfo.processInfo.machineECID assert(ecid != nil, "Failed to get host machine ECID") self.init( id: UUID(), vmUUID: model.metadata.uuid, date: .now, appVersion: Bundle.main.vbVersion, appBuild: Bundle.main.vbBuild, hostECID: ecid, storageDevices: model.configuration.hardware.storageDevices // will be modified once package is saved ) } } // MARK: - Directory Helpers @MainActor public extension VBVirtualMachine { func savedStatesDirectoryURL(in library: VMLibraryController) -> URL { library.savedStateDirectoryURL(for: self) } func savedStatesDirectoryURLCreatingIfNeeded(in library: VMLibraryController) throws -> URL { try library.savedStateDirectoryURLCreatingIfNeeded(for: self) } /// Convenience for ``VMLibraryController/createSavedStatePackage(for:)``. func createSavedStatePackage(in library: VMLibraryController, snapshotName name: String) throws -> VBSavedStatePackage { try library.createSavedStatePackage(for: self, snapshotName: name) } } ================================================ FILE: VirtualCore/Source/Models/SavedState/VBSavedStatePackage+VM.swift ================================================ import Foundation extension VBSavedStatePackage: VBStorageDeviceContainer { public var bundleURL: URL { url } public var storageDevices: [VBStorageDevice] { metadata.storageDevices } } ================================================ FILE: VirtualCore/Source/Models/SavedState/VBSavedStatePackage.swift ================================================ import Foundation import UniformTypeIdentifiers import OSLog public extension UTType { static let virtualBuddySavedState = UTType( exportedAs: "codes.rambo.VirtualBuddy.SavedState", conformingTo: .bundle ) } /// Represents a `vbst` file on disk, encapsulating all operations related to saved state packages. public final class VBSavedStatePackage: Identifiable, Hashable { public var id: UUID { metadata.id } static let dataFilename = "State.vzvmsave" static let infoFilename = "Info.plist" static let screenshotFilename = "Screenshot.heic" static let thumbnailFilename = "Thumbnail.heic" static let fileExtension = "vbst" public let url: URL public let dataFileURL: URL public let infoFileURL: URL public let screenshotFileURL: URL public let thumbnailFileURL: URL private let manager: FileManager private let logger: Logger public var metadata: VBSavedStateMetadata { didSet { saveMetadata(oldValue) } } /// Creates a new package on disk for the given virtual machine, initializing the saved state package accordingly. public convenience init(creatingPackageInDirectoryAt baseURL: URL, model: VBVirtualMachine, snapshotName: String) throws { let url = baseURL.appendingPathComponent(snapshotName, conformingTo: .virtualBuddySavedState) let createdURL = try url.creatingDirectoryIfNeeded() try self.init(url: createdURL, metadata: VBSavedStateMetadata(model: model)) save() } /// Initializes a saved state package from an existing package on disk. public convenience init(url: URL) throws { try self.init(url: url, metadata: nil) } private init(url: URL, metadata: VBSavedStateMetadata?) throws { guard FileManager.default.fileExists(atPath: url.path) else { throw Failure("Saved state package doesn't exist at \(url.path)") } let infoURL = url.appending(path: Self.infoFilename) self.logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "VBSavedStatePackage(\(url.deletingPathExtension().lastPathComponent))") self.manager = FileManager() self.url = url self.dataFileURL = url.appending(path: Self.dataFilename) self.infoFileURL = infoURL self.screenshotFileURL = url.appending(path: Self.screenshotFilename) self.thumbnailFileURL = url.appending(path: Self.thumbnailFilename) if FileManager.default.fileExists(atPath: infoURL.path) { let data = try Data(contentsOf: infoURL) self.metadata = try PropertyListDecoder.virtualBuddy.decode(VBSavedStateMetadata.self, from: data) } else { guard let inputMetadata = metadata else { throw Failure("Initializing VBSavedStatePackage with new package requires metadata to be provided") } self.metadata = inputMetadata } } public var thumbnail: NSImage? { NSImage(contentsOf: thumbnailFileURL) } public var screenshot: NSImage? { get { NSImage(contentsOf: screenshotFileURL) } set { guard let newValue else { try? manager.removeItem(at: screenshotFileURL) try? manager.removeItem(at: thumbnailFileURL) return } do { try newValue.vb_encodeHEIC(to: screenshotFileURL) try newValue.vb_createThumbnail(at: thumbnailFileURL) } catch { logger.error("Error saving new screenshot/thumbnail: \(error, privacy: .public)") } } } public func save() { logger.debug(#function) saveMetadata(nil) } public func createStorageDeviceClones(model: VBVirtualMachine) async throws { try await metadata.createStorageDeviceClones(packageURL: url, model: model) } public func delete() throws { logger.debug(#function) try manager.removeItem(at: url) } /// Whether this saved state needs to be migrated by cloning existing storage devices /// because it was created in a version of VirtualBuddy that didn't do this. public var needsStorageCloneMigration: Bool { metadata.storageDevices.isEmpty } private func saveMetadata(_ oldValue: VBSavedStateMetadata?) { guard metadata != oldValue else { return } do { let encoded = try PropertyListEncoder.virtualBuddy.encode(metadata) try encoded.write(to: infoFileURL) } catch { logger.error("Error saving updated info: \(error, privacy: .public)") assertionFailure("Error saving updated info: \(error)") } } public func hash(into hasher: inout Hasher) { hasher.combine(metadata) } public static func ==(lhs: VBSavedStatePackage, rhs: VBSavedStatePackage) -> Bool { lhs.metadata == rhs.metadata } } // MARK: - Validation extension VBSavedStatePackage { func validate(for model: VBVirtualMachine) throws { if let stateHostECID = metadata.hostECID, let currentHostECID = ProcessInfo.processInfo.machineECID { guard stateHostECID == currentHostECID else { throw Failure("This saved state is not for the current host. Saved states are paired to the host machine and can't be restored on a different host.") } } guard metadata.vmUUID == model.metadata.uuid else { throw Failure("This saved state is not for this virtual machine. Saved states can only be restored on the virtual machine that saved the state.") } } } public extension VBSavedStateMetadata { init(packageAt url: URL) throws { let infoURL = url.appendingPathComponent(VBSavedStatePackage.infoFilename) self = try PropertyListDecoder.virtualBuddy.decode(Self.self, from: Data(contentsOf: infoURL)) } } ================================================ FILE: VirtualCore/Source/Models/SavedState/VMLibraryController+SavedState.swift ================================================ import Foundation @MainActor public extension VMLibraryController { nonisolated static let savedStateDirectoryName = "_SavedState" var savedStatesDirectoryURL: URL { self.libraryURL.appending(path: Self.savedStateDirectoryName, directoryHint: .isDirectory) } func savedStatesLibraryURLCreatingIfNeeded() throws -> URL { try savedStatesDirectoryURL.creatingDirectoryIfNeeded() } func savedStateDirectoryURL(for model: VBVirtualMachine) -> URL { savedStatesDirectoryURL .appending(path: model.metadata.uuid.uuidString, directoryHint: .isDirectory) } func savedStateDirectoryURLCreatingIfNeeded(for model: VBVirtualMachine) throws -> URL { try savedStateDirectoryURL(for: model) .creatingDirectoryIfNeeded() } func createSavedStatePackage(for model: VBVirtualMachine, snapshotName name: String) throws -> VBSavedStatePackage { let baseURL = try model.savedStatesDirectoryURLCreatingIfNeeded(in: self) return try VBSavedStatePackage(creatingPackageInDirectoryAt: baseURL, model: model, snapshotName: name) } func virtualMachine(with uuid: UUID) throws -> VBVirtualMachine { guard let model = virtualMachines.first(where: { $0.metadata.uuid == uuid }) else { throw Failure("Virtual machine not found with UUID \(uuid)") } return model } func virtualMachineURL(forSavedStatePackageURL url: URL) throws -> URL { try virtualMachine(forSavedStatePackageURL: url).bundleURL } func virtualMachine(forSavedStatePackageURL url: URL) throws -> VBVirtualMachine { let metadata = try VBSavedStateMetadata(packageAt: url) let model = try virtualMachine(forSavedStateMetadata: metadata) return model } func virtualMachine(forSavedStateMetadata metadata: VBSavedStateMetadata) throws -> VBVirtualMachine { try virtualMachine(with: metadata.vmUUID) } } ================================================ FILE: VirtualCore/Source/Models/VBError.swift ================================================ // // VBError.swift // VirtualCore // // Created by Guilherme Rambo on 10/04/22. // import Foundation public struct VBError: LocalizedError { public var errorDescription: String? init(_ desc: String) { self.errorDescription = desc } } ================================================ FILE: VirtualCore/Source/Models/VBNVRAMVariable.swift ================================================ // // VBNVRAMVariable.swift // VirtualCore // // Created by Guilherme Rambo on 11/04/22. // import Foundation public struct VBNVRAMVariable: Identifiable, Hashable, Codable { public var id: String { name } public let name: String public var value: String? public init(name: String, value: String?) { self.name = name self.value = value } } ================================================ FILE: VirtualCore/Source/Models/VBStorageDeviceContainer.swift ================================================ import Foundation /// Adopted by ``VMVirtualMachine`` and ``VBSavedStatePackage`` /// in order to help resolve storage devices when bootstrapping a VM. public protocol VBStorageDeviceContainer { var bundleURL: URL { get } var storageDevices: [VBStorageDevice] { get } var bootDevice: VBStorageDevice { get throws } var bootDiskImage: VBManagedDiskImage { get throws } var allowDiskImageCreation: Bool { get } func diskImageURL(for image: VBManagedDiskImage) -> URL func diskImageURL(for device: VBStorageDevice) -> URL } // MARK: - Default Implementations /// These default implementations take care of resolving disk images for both ``VBVirtualMachine`` and ``VBSavedStatePackage``. /// Which one is used will be determine in `VirtualMachineConfigurationHelper` when bootstrapping the VM. public extension VBStorageDeviceContainer { var allowDiskImageCreation: Bool { false } var bootDevice: VBStorageDevice { get throws { guard let device = storageDevices.first(where: { $0.isBootVolume }) else { throw Failure("The virtual machine lacks a bootable storage device.") } return device } } var bootDiskImage: VBManagedDiskImage { get throws { let device = try bootDevice guard case .managedImage(let image) = device.backing else { throw Failure("The boot device must use a disk image managed by VirtualBuddy") } return image } } func diskImageURL(for image: VBManagedDiskImage) -> URL { bundleURL .appendingPathComponent(image.filename) .appendingPathExtension(image.format.fileExtension) } func diskImageURL(for device: VBStorageDevice) -> URL { switch device.backing { case .managedImage(let image): return diskImageURL(for: image) case .customImage(let customURL): return customURL } } } ================================================ FILE: VirtualCore/Source/Models/VBVirtualMachine+Metadata.swift ================================================ // // VBVirtualMachine+Metadata.swift // VirtualCore // // Created by Guilherme Rambo on 24/06/22. // import Cocoa public extension VBVirtualMachine { func metadataDirectoryCreatingIfNeeded() throws -> URL { try metadataDirectoryURL.creatingDirectoryIfNeeded() } func write(_ data: Data, forMetadataFileNamed name: String) throws { let baseURL = try metadataDirectoryCreatingIfNeeded() let fileURL = baseURL.appendingPathComponent(name) try data.write(to: fileURL, options: .atomic) } func deleteMetadataFile(named name: String) throws { let fileURL = metadataDirectoryURL.appendingPathComponent(name) guard FileManager.default.fileExists(atPath: fileURL.path) else { return } try FileManager.default.removeItem(at: fileURL) } func metadataFileURL(_ fileName: String) -> URL { let fileURL = metadataDirectoryURL.appendingPathComponent(fileName) return fileURL } func metadataContents(_ fileName: String) -> Data? { let fileURL = metadataFileURL(fileName) guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil } return try? Data(contentsOf: fileURL) } } extension URL { func creatingDirectoryIfNeeded() throws -> Self { if !FileManager.default.fileExists(atPath: path) { try FileManager.default.createDirectory(at: self, withIntermediateDirectories: true) } return self } } extension URL { /// `true` if URL points to a file contained within a VirtualBuddy VM bundle metadata directory. var isVirtualBuddyDataDirectoryFile: Bool { deletingLastPathComponent().lastPathComponent == VBVirtualMachine.metadataDirectoryName } var virtualMachineBundleParent: URL? { var current = self while current.pathExtension != VBVirtualMachine.bundleExtension { current = current.deletingLastPathComponent() guard current.path != "/" else { return nil } } return current } } ================================================ FILE: VirtualCore/Source/Models/VBVirtualMachine+Screenshot.swift ================================================ import Cocoa public extension VBVirtualMachine { var screenshot: NSImage? { guard let imageData = metadataContents(VBVirtualMachine.screenshotFileName) ?? metadataContents(VBVirtualMachine._legacyScreenshotFileName) else { return nil } return NSImage(data: imageData) } var thumbnail: NSImage? { guard let imageData = metadataContents(VBVirtualMachine.thumbnailFileName) ?? metadataContents(VBVirtualMachine._legacyThumbnailFileName) else { return nil } return NSImage(data: imageData) } func thumbnailImage() -> NSImage? { let thumbnailURL = metadataFileURL(Self.thumbnailFileName) if let existingImage = NSImage(contentsOf: thumbnailURL) { return existingImage } return try? screenshot?.vb_createThumbnail(at: thumbnailURL) } func invalidateScreenshot() throws { try deleteMetadataFile(named: Self.screenshotFileName) } func invalidateThumbnail() throws { try deleteMetadataFile(named: Self.thumbnailFileName) } } ================================================ FILE: VirtualCore/Source/Models/VBVirtualMachine.swift ================================================ import Foundation import UniformTypeIdentifiers import Combine public typealias VoidSubject = PassthroughSubject<(), Never> public typealias BoolSubject = PassthroughSubject public struct VBVirtualMachine: Identifiable, VBStorageDeviceContainer { public struct Metadata: Hashable, Codable { public static let currentVersion = 1 @DecodableDefault.EmptyPlaceholder public var uuid = UUID() public var version = Self.currentVersion public var installFinished: Bool = false public var firstBootDate: Date? = nil public var lastBootDate: Date? = nil @DecodableDefault.EmptyPlaceholder public var backgroundHash: BlurHashToken = .virtualBuddyBackground /// If this VM was imported from some other app, contains the name of the ``VMImporter`` that was used. public var importedFromAppName: String? = nil /// The original remote URL that was specified for downloading the restore image (if downloaded from a remote source). public private(set) var remoteInstallImageURL: URL? = nil /// The original local file URL that was specified (or set after a successful download from ``remoteInstallImageURL``). public private(set) var installImageURL: URL? = nil /** Usage of the same property for both local and remote restore image URLs has been the source of recurring bugs in the past. Example: https://github.com/insidegui/VirtualBuddy/pull/395 To keep this struct backwards-compatible and still have better safeguards against regressions, ``remoteInstallImageURL`` and ``installImageURL`` are only settable by the struct itself. To update the metadata, clients must use this method, which will automatically set the correct property by inspecting the URL. */ public mutating func updateInstallImageURL(_ url: URL) { if url.isFileURL { installImageURL = url } else { remoteInstallImageURL = url } } /// If Linux VM is using fallback VirtualBuddy orange background hash, updates it to use the Linux-specific one. fileprivate mutating func setLinuxBackgroundHashIfNeeded() { guard backgroundHash == .virtualBuddyBackground else { return } backgroundHash = .virtualBuddyBackgroundLinux } } public var id: String { bundleURL.absoluteString } public internal(set) var bundleURL: URL public var name: String { bundleURL.deletingPathExtension().lastPathComponent } public internal(set) var uuid: UUID { get { metadata.uuid } set { metadata.uuid = newValue } } private var _configuration: VBMacConfiguration? private var _metadata: Metadata? private var _installRestoreData: Data? public var configuration: VBMacConfiguration { /// Masking private `_configuration` since it's initialized dynamically from a file. get { _configuration ?? .default } set { _configuration = newValue } } public var metadata: Metadata { /// Masking private `_metadata` since it's initialized dynamically from a file. get { _metadata ?? .init() } set { _metadata = newValue } } public var installRestoreData: Data? { /// Masking private `_installRestoreData` since it's initialized dynamically from a file. get { _installRestoreData } set { _installRestoreData = newValue } } } public extension VBVirtualMachine { static let bundleExtension = "vbvm" static let screenshotFileName = "Screenshot.heic" static let thumbnailFileName = "Thumbnail.heic" /// Only used for migrations. static let _legacyScreenshotFileName = "Screenshot.tiff" /// Only used for migrations. static let _legacyThumbnailFileName = "Thumbnail.jpg" } extension VBVirtualMachine { static let metadataFilename = "Metadata.plist" static let configurationFilename = "Config.plist" static let installRestoreFilename = "Install.plist" public var storageDevices: [VBStorageDevice] { configuration.hardware.storageDevices } var auxiliaryStorageURL: URL { bundleURL.appendingPathComponent("AuxiliaryStorage") } var machineIdentifierURL: URL { bundleURL.appendingPathComponent("MachineIdentifier") } var hardwareModelURL: URL { bundleURL.appendingPathComponent("HardwareModel") } public var metadataDirectoryURL: URL { Self.metadataDirectoryURL(for: bundleURL) } static let metadataDirectoryName = ".vbdata" static func metadataDirectoryURL(for bundleURL: URL) -> URL { bundleURL.appendingPathComponent(metadataDirectoryName) } public var needsInstall: Bool { guard configuration.systemType == .mac else { return false } return !metadata.installFinished || !FileManager.default.fileExists(atPath: hardwareModelURL.path) } /// Conforming to ``VBStorageDeviceContainer``, which defaults this to `false`. /// When restoring from a saved state, disk image creation is not allowed, but when bootstrapping /// a virtual machine, disk image creation is allowed. public var allowDiskImageCreation: Bool { true } } public extension UTType { static let virtualBuddyVM = UTType(exportedAs: "codes.rambo.VirtualBuddy.VM", conformingTo: .bundle) } public extension VBVirtualMachine { struct BundleDirectoryMissingError: Error { } init(bundleURL: URL, isNewInstall: Bool = false, createIfNeeded: Bool = true) throws { /// If we're not allowed to create the bundle and its metadata directory doesn't exist, throw a specific error type that's caught in ``VMLibraryController``. /// This is to prevent the app from creating a dummy VM bundle after a VM is deleted from the library. if !createIfNeeded { let metaDirectory = bundleURL.appending(path: Self.metadataDirectoryName, directoryHint: .isDirectory) guard metaDirectory.isReadableDirectory else { throw BundleDirectoryMissingError() } } if !FileManager.default.fileExists(atPath: bundleURL.path) { #if DEBUG guard !ProcessInfo.isSwiftUIPreview else { fatalError("Missing SwiftUI preview VM at \(bundleURL.path)") } #endif try FileManager.default.createDirectory(at: bundleURL, withIntermediateDirectories: true) } self.bundleURL = bundleURL var (metadata, config, installRestore) = try loadMetadata() if isNewInstall { config.hardware.displayDevices = [ VBDisplayPreset.defaultPreset(for: .mac).device ] } /// Migration from previous versions that didn't have a configuration file /// describing the storage devices. config.hardware.addMissingBootDeviceIfNeeded() /// Migration from previous versions that didn't have dedicated fallback artwork for Linux. if config.systemType == .linux { metadata?.setLinuxBackgroundHashIfNeeded() } self.configuration = config if let metadata { self.metadata = metadata } else { /// Migration from previous versions that didn't have a metadata file. self.metadata = Metadata(installFinished: !isNewInstall, firstBootDate: .now, lastBootDate: .now) } self.installRestoreData = installRestore } @available(macOS 13, *) init(creatingLinuxMachineAt bundleURL: URL) throws { guard !FileManager.default.fileExists(atPath: bundleURL.path) else { fatalError() } try FileManager.default.createDirectory(at: bundleURL, withIntermediateDirectories: true) self.bundleURL = bundleURL var hardware = VBMacDevice.default hardware.displayDevices = [ VBDisplayPreset.defaultPreset(for: .linux).device ] self.configuration = .init(systemType: .linux, hardware: hardware) self.metadata = Metadata(installFinished: false, firstBootDate: .now, lastBootDate: .now) metadata.setLinuxBackgroundHashIfNeeded() try saveMetadata() } func saveMetadata() throws { #if DEBUG guard !ProcessInfo.isSwiftUIPreview else { return } #endif let configData = try PropertyListEncoder.virtualBuddy.encode(configuration) try write(configData, forMetadataFileNamed: Self.configurationFilename) let metaData = try PropertyListEncoder.virtualBuddy.encode(metadata) try write(metaData, forMetadataFileNamed: Self.metadataFilename) try saveInstallData() } func saveInstallData() throws { if let installRestoreData { try write(installRestoreData, forMetadataFileNamed: Self.installRestoreFilename) } else { try? deleteMetadataFile(named: Self.installRestoreFilename) } } func loadMetadata() throws -> (Metadata?, VBMacConfiguration, Data?) { let metadata: Metadata? let config: VBMacConfiguration let installRestore: Data? if let data = metadataContents(Self.configurationFilename) { config = try PropertyListDecoder.virtualBuddy.decode(VBMacConfiguration.self, from: data) } else { /// Linux guests don't go through this code path, so it should be safe to assume Mac here (famous last words). config = .default.guestType(.mac) } if let data = metadataContents(Self.metadataFilename) { metadata = try PropertyListDecoder.virtualBuddy.decode(Metadata.self, from: data) } else { metadata = nil } if let data = metadataContents(Self.installRestoreFilename) { installRestore = data } else { installRestore = nil } return (metadata, config, installRestore) } mutating func reloadMetadata() { #if DEBUG guard !ProcessInfo.isSwiftUIPreview else { return } #endif guard let (metadata, config, installRestore) = try? loadMetadata() else { assertionFailure("Failed to reload metadata") return } self.metadata = metadata ?? .init() self.configuration = config self.installRestoreData = installRestore } } extension URL { var creationDate: Date { get { (try? resourceValues(forKeys: [.creationDateKey]))?.creationDate ?? .distantPast } set { var values = URLResourceValues() values.creationDate = newValue try? setResourceValues(values) } } } public extension PropertyListEncoder { static let virtualBuddy = PropertyListEncoder() } public extension PropertyListDecoder { static let virtualBuddy = PropertyListDecoder() } extension UUID: ProvidesEmptyPlaceholder { public static var empty: UUID { UUID() } } ================================================ FILE: VirtualCore/Source/ReleaseTrains/AppUpdateChannel.swift ================================================ // // AppUpdateChannel.swift // VirtualCore // // Created by Guilherme Rambo on 25/06/22. // import Foundation public struct AppUpdateChannel: Identifiable, Hashable, CustomStringConvertible, Sendable { public var id: String { name.lowercased() } public let name: String public let appCastURL: URL } public extension AppUpdateChannel { static let release = AppUpdateChannel( name: "Release", appCastURL: URL(string: "https://su.virtualbuddy.app/appcast.xml?channel=release")! ) static let beta = AppUpdateChannel( name: "Beta", appCastURL: URL(string: "https://su.virtualbuddy.app/appcast.xml?channel=beta")! ) static let channelsByID: [AppUpdateChannel.ID: AppUpdateChannel] = [ AppUpdateChannel.release.id: AppUpdateChannel.release, AppUpdateChannel.beta.id: AppUpdateChannel.beta ] static func defaultChannel(for buildType: VBBuildType) -> AppUpdateChannel { switch buildType { case .debug, .devRelease, .release: return .release case .betaDebug, .betaRelease: return .beta } } } public extension AppUpdateChannel { var description: String { id } } ================================================ FILE: VirtualCore/Source/ReleaseTrains/VBBuildType.swift ================================================ import Foundation /// Represents the type of app build, such as beta vs. release. public enum VBBuildType: String, CaseIterable, CustomStringConvertible { case debug case betaDebug case release case betaRelease case devRelease } public extension VBBuildType { /// The current build type according to compile-time flags. static let current: VBBuildType = { #if BUILDING_DEV_RELEASE return .devRelease #elseif BETA && DEBUG return .betaDebug #elseif BETA return .betaRelease #elseif DEBUG return .debug #else return .release #endif }() /// A user-facing name for the build type, or `nil` if it's a regular release build. var name: String? { switch self { case .debug: return "Debug" case .betaDebug: return "Beta Debug" case .release: return nil case .betaRelease: return "Beta" case .devRelease: return "Dev" } } var description: String { name ?? "Release" } } public extension Bundle { var vbBuildType: VBBuildType { .current } /// The build description, including build type, version, and build number. /// Example: "Beta 2.0 - 123" var vbBuildDescription: String { if let typeDescription = vbBuildType.name { return "\(typeDescription) \(vbShortVersionString) - \(vbBuild)" } else { return "\(vbShortVersionString) - \(vbBuild)" } } /// The full version description such as "VirtualBuddy (Beta 2.0 - 123)", or just "VirtualBuddy" for release builds. var vbFullVersionDescription: String { let appName = "VirtualBuddy" if vbBuildType.name != nil { return "\(appName) (\(vbBuildDescription))" } else { return appName } } } ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewLinux.vbvm/Disk.img ================================================ Fake ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewLinuxBlurHash.vbvm/Disk.img ================================================ Fake ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewLinuxNoArtwork.vbvm/Disk.img ================================================ Fake ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewMac.vbvm/Disk.img ================================================ Fake ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewMacBlurHash.vbvm/Disk.img ================================================ Fake ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewMacNoArtwork.vbvm/Disk.img ================================================ Fake ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;06;24.vbst/Info.plist ================================================ appBuild 110 appVersion 1.4.1 date 2024-03-27T19:06:24Z hostECID 2939957236449310 id 65D33ECA-ACF2-4241-9F45-00417FAFF473 vmUUID 96830330-D195-4211-BB4B-7D898177B12C ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;07;04.vbst/Info.plist ================================================ appBuild 110 appVersion 1.4.1 date 2024-03-27T19:07:04Z hostECID 2939957236449310 id 85CCCE19-1B97-45B5-A2E0-DF8A1F2DFA07 vmUUID 96830330-D195-4211-BB4B-7D898177B12C ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;06.vbst/Info.plist ================================================ appBuild 110 appVersion 1.4.1 date 2024-03-27T19:08:06Z hostECID 2939957236449310 id 3C248913-3420-4688-96FB-5B32E91026E4 vmUUID 96830330-D195-4211-BB4B-7D898177B12C ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;28.vbst/Info.plist ================================================ appBuild 110 appVersion 1.4.1 date 2024-03-27T19:08:28Z hostECID 2939957236449310 id FC0182EC-BC5C-4817-ABFF-8DD1B9E49735 vmUUID 96830330-D195-4211-BB4B-7D898177B12C ================================================ FILE: VirtualCore/Source/Resources/Preview/PreviewSavedStates/Save-2024-03-27_16;08;51.vbst/Info.plist ================================================ appBuild 110 appVersion 1.4.1 date 2024-03-27T19:08:51Z hostECID 2939957236449310 id F29733F1-0056-4C90-A9E5-04F42C127086 vmUUID 96830330-D195-4211-BB4B-7D898177B12C ================================================ FILE: VirtualCore/Source/Resources/Preview/_Downloads/UniversalMac_13.3.1_22E261_Restore.ipsw ================================================ dummy ================================================ FILE: VirtualCore/Source/Resources/Preview/_Downloads/UniversalMac_14.0_23A344_Restore.ipsw ================================================ dummy ================================================ FILE: VirtualCore/Source/Resources/Preview/_Downloads/UniversalMac_14.5_23F79_Restore.ipsw ================================================ dummy ================================================ FILE: VirtualCore/Source/Resources/Preview/_Downloads/UniversalMac_15.3_24D60_Restore.ipsw ================================================ dummy ================================================ FILE: VirtualCore/Source/Resources/Preview/_Downloads/UniversalMac_15.5_24F74_Restore.ipsw ================================================ dummy ================================================ FILE: VirtualCore/Source/Resources/VirtualCore.xcassets/Adjectives.dataset/Adjectives.txt ================================================ Able Above Absent Absolute Abstract Abundant Academic Acceptable Accepted Accessible Accurate Accused Active Actual Acute Added Additional Adequate Adjacent Administrative Adorable Advanced Adverse Advisory Aesthetic Afraid Aggregate Aggressive Agreeable Agreed Agricultural Alert Alive Alleged Allied Alone Alright Alternative Amateur Amazing Ambitious Amused Ancient Angry Annoyed Annual Anonymous Anxious Appalling Apparent Applicable Appropriate Arbitrary Architectural Armed Arrogant Artificial Artistic Ashamed Asleep Assistant Associated Atomic Attractive Automatic Autonomous Available Average Awake Aware Awful Awkward Back Bad Balanced Bare Basic Beautiful Beneficial Better Bewildered Big Binding Biological Bitter Bizarre Black Blank Blind Blonde Bloody Blue Blushing Boiling Bold Bored Boring Bottom Brainy Brave Breakable Breezy Brief Bright Brilliant Broad Broken Brown Bumpy Burning Busy Calm Capable Careful Casual Causal Cautious Central Certain Changing Characteristic Charming Cheap Cheerful Chemical Chief Chilly Chosen Chronic Chubby Circular Civic Civil Civilian Classic Classical Clean Clear Clever Clinical Close Closed Cloudy Clumsy Coastal Cognitive Coherent Cold Collective Colonial Colorful Colossal Colored Colorful Combative Combined Comfortable Coming Commercial Common Compact Comparable Comparative Compatible Competent Competitive Complete Complex Complicated Comprehensive Compulsory Conceptual Concerned Concrete Condemned Confident Confidential Confused Conscious Conservation Conservative Considerable Consistent Constant Constitutional Contemporary Content Continental Continued Continuing Continuous Controlled Controversial Convenient Conventional Convinced Convincing Cooing Cool Cooperative Corporate Correct Corresponding Costly Courageous Crazy Creative Creepy Criminal Critical Crooked Crowded Crucial Crude Cruel Cuddly Cultural Curious Curly Current Curved Cute Daily Damaged Damp Dangerous Dark Dead Deaf Deafening Dear Decent Decisive Deep Defeated Defensive Defiant Definite Deliberate Delicate Delicious Delighted Delightful Democratic Dependent Depressed Desirable Desperate Detailed Determined Developed Developing Devoted Different Difficult Digital Diplomatic Direct Dirty Disabled Disappointed Disastrous Disciplinary Disgusted Distant Distinct Distinctive Distinguished Disturbed Disturbing Diverse Divine Dizzy Domestic Dominant Double Doubtful Drab Dramatic Dreadful Driving Drunk Dry Dual Due Dull Dusty Dying Dynamic Eager Early Easy Economic Educational Eerie Effective Efficient Elaborate Elated Elderly Eldest Electoral Electric Electrical Electronic Elegant Eligible Embarrassed Embarrassing Emotional Empirical Empty Enchanting Encouraging Endless Energetic Enormous Enthusiastic Entire Entitled Envious Environmental Equal Equivalent Essential Established Estimated Ethical Eventual Everyday Evident Evil Evolutionary Exact Excellent Exceptional Excess Excessive Excited Exciting Exclusive Existing Exotic Expected Expensive Experienced Experimental Explicit Extended Extensive External Extra Extraordinary Extreme Exuberant Faint Fair Faithful Familiar Famous Fancy Fantastic Far Fascinating Fashionable Fast Fatal Favourable Favourite Federal Fellow Female Few Fierce Filthy Final Financial Fine Firm Fiscal Fit Fixed Flaky Flat Flexible Fluffy Fluttering Flying Following Fond Foolish Foreign Formal Formidable Forthcoming Fortunate Forward Fragile Frail Frantic Free French Frequent Fresh Friendly Frightened Front Frozen Full Full-time Fun Functional Fundamental Funny Furious Future Fuzzy Gastric General Generous Genetic Gentle Genuine Geographical Giant Gigantic Given Glad Glamorous Gleaming Global Glorious Golden Good Gorgeous Gothic Governing Graceful Gradual Grand Grateful Greasy Great Greek Green Grey Grieving Grim Gross Grotesque Growing Grubby Grumpy Guilty Handicapped Handsome Happy Hard Harsh Head Healthy Heavy Helpful Helpless Hidden High High-pitched Hilarious Hissing Historic Historical Hollow Holy Homeless Homely Hon Honest Horizontal Horrible Hostile Hot Huge Human Hungry Hurt Hushed Husky Icy Ideal Identical Ideological Ill Illegal Imaginative Immediate Immense Imperial Implicit Important Impossible Impressed Impressive Improved Inadequate Inappropriate Inclined Increased Increasing Incredible Independent Indirect Individual Industrial Inevitable Influential Informal Inherent Initial Injured Inland Inner Innocent Innovative Inquisitive Instant Institutional Insufficient Intact Integral Integrated Intellectual Intelligent Intense Intensive Interested Interesting Interim Interior Intermediate Internal International Intimate Invisible Involved Irrelevant Isolated Itchy Jealous Jittery Joint Jolly Joyous Judicial Juicy Junior Just Keen Key Kind Known Labour Large Large-scale Late Latin Lazy Leading Left Legal Legislative Legitimate Lengthy Lesser Level Lexical Liable Liberal Light Like Likely Limited Linear Linguistic Liquid Literary Little Live Lively Living Local Logical Lonely Long Long-term Loose Lost Loud Lovely Low Loyal Ltd Lucky Mad Magenta Magic Magnetic Magnificent Main Major Male Mammoth Managerial Managing Manual Many Marginal Marine Marked Married Marvellous Mass Massive Mathematical Mature Maximum Mean Meaningful Mechanical Medical Medieval Melodic Melted Mental Mere Metropolitan Mid Middle Middle-class Mighty Mild Military Miniature Minimal Minimum Ministerial Minor Miserable Misleading Missing Misty Mixed Moaning Mobile Moderate Modern Modest Molecular Monetary Monthly Moral Motionless Muddy Multiple Mushy Musical Mute Mutual Mysterious Naked Narrow Nasty Native Natural Naughty Naval Near Nearby Neat Necessary Negative Neighbouring Nervous Net Neutral New Nice Nineteenth-century Noble Noisy Normal Northern Nosy Notable Novel Nuclear Numerous Nursing Nutritious Nutty Obedient Objective Obliged Obnoxious Obvious Occasional Occupational Odd Official Ok Okay Old Old-fashioned Olympic Only Open Operational Opposite Optimistic Oral Orange Ordinary Organic Organisational Original Orthodox Other Outdoor Outer Outrageous Outside Outstanding Overall Overseas Overwhelming Painful Panicky Parallel Parental Parliamentary Part-time Partial Particular Passing Passive Past Patient Payable Peaceful Peculiar Perfect Permanent Persistent Personal Petite Philosophical Physical Pink Plain Planned Plastic Pleasant Pleased Poised Polite Poor Popular Positive Possible Post-war Potential Powerful Practical Precious Precise Preferred Pregnant Preliminary Premier Prepared Present Presidential Pretty Previous Prickly Primary Prime Primitive Principal Printed Prior Private Probable Productive Professional Profitable Profound Progressive Prominent Promising Proper Proposed Prospective Protective Protestant Proud Provincial Psychiatric Psychological Public Puny Pure Purple Purring Puzzled Quaint Qualified Quick Quickest Quiet Racial Radical Rainy Random Rapid Rare Raspy Rational Ratty Raw Ready Real Realistic Rear Reasonable Recent Red Reduced Redundant Regional Registered Regular Regulatory Related Relative Relaxed Relevant Reliable Relieved Religious Reluctant Remaining Remarkable Remote Renewed Representative Repulsive Required Resident Residential Resonant Respectable Respective Responsible Resulting Retail Retired Revolutionary Rich Ridiculous Right Rigid Ripe Rising Rival Roasted Robust Rolling Romantic Rotten Rough Round Royal Rubber Rude Ruling Running Rural Sacred Sad Safe Salty Satisfactory Satisfied Scared Scary Scattered Scientific Scornful Scrawny Screeching Secondary Secret Secure Select Selected Selective Selfish Semantic Senior Sensible Sensitive Separate Serious Severe Shaggy Shaky Shallow Shared Sharp Sheer Shiny Shivering Shocked Short Short-term Shrill Shy Sick Significant Silent Silky Silly Similar Simple Single Skilled Skinny Sleepy Slight Slim Slimy Slippery Slow Small Smart Smiling Smoggy Smooth So-called Social Soft Solar Sole Solid Sophisticated Sore Sorry Sound Sour Southern Soviet Spare Sparkling Spatial Special Specific Specified Spectacular Spicy Spiritual Splendid Spontaneous Sporting Spotless Spotty Square Squealing Stable Stale Standard Static Statistical Statutory Steady Steep Sticky Stiff Still Stingy Stormy Straight Straightforward Strange Strategic Strict Striking Striped Strong Structural Stuck Subjective Subsequent Substantial Subtle Successful Successive Sudden Sufficient Suitable Sunny Super Superb Superior Supporting Supposed Supreme Sure Surprised Surprising Surrounding Surviving Suspicious Sweet Swift Symbolic Sympathetic Systematic Tall Tame Tan Tart Tasteless Tasty Technical Technological Teenage Temporary Tender Tense Terrible Territorial Testy Then Theoretical Thick Thin Thirsty Thorough Thoughtful Thoughtless Thundering Tight Tiny Tired Top Tory Total Tough Toxic Traditional Tragic Tremendous Tricky Tropical Troubled Typical Ugliest Ultimate Unable Unacceptable Unaware Uncertain Unchanged Uncomfortable Unconscious Underground Underlying Unemployed Uneven Unexpected Unfair Unfortunate Unhappy Uniform Uninterested Unique United Universal Unknown Unlikely Unnecessary Unpleasant Unsightly Unusual Unwilling Upper Upset Uptight Urban Urgent Used Useful Useless Usual Vague Valid Valuable Variable Varied Various Varying Vast Verbal Vertical Very Victorious Video-taped Violent Visible Visiting Visual Vital Vivacious Vivid Vocational Voiceless Voluntary Vulnerable Wandering Warm Wasteful Watery Weak Wealthy Weary Wee Weekly Weird Welcome Well Well-known Welsh Wet Whispering White Whole Wicked Wide Wide-eyed Widespread Wild Willing Wise Witty Wonderful Wooden Working Worldwide Worried Worrying Worthwhile Worthy Written Wrong Yellow Young Yummy Zany Zealous ================================================ FILE: VirtualCore/Source/Resources/VirtualCore.xcassets/Adjectives.dataset/Contents.json ================================================ { "data" : [ { "filename" : "Adjectives.txt", "idiom" : "universal", "universal-type-identifier" : "public.plain-text" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualCore/Source/Resources/VirtualCore.xcassets/Animals.dataset/Animals.txt ================================================ Aardvark Albatross Alligator Alpaca Ant Anteater Antelope Ape Armadillo Donkey Baboon Badger Barracuda Bat Bear Beaver Bee Bison Boar Buffalo Butterfly Camel Capybara Caribou Cassowary Cat Caterpillar Cattle Chamois Cheetah Chicken Chimpanzee Chinchilla Chough Clam Cobra Cockroach Cod Cormorant Coyote Crab Crane Crocodile Crow Curlew Deer Dinosaur Dog Dogfish Dolphin Dotterel Dove Dragonfly Duck Dugong Dunlin Eagle Echidna Eel Eland Elephant Elk Emu Falcon Ferret Finch Fish Flamingo Fly Fox Frog Gaur Gazelle Gerbil Giraffe Gnat Gnu Goat Goldfinch Goldfish Goose Gorilla Goshawk Grasshopper Grouse Guanaco Gull Hamster Hare Hawk Hedgehog Heron Herring Hippopotamus Hornet Horse Human Hummingbird Hyena Ibex Ibis Jackal Jaguar Jay Jellyfish Kangaroo Kingfisher Koala Kookabura Kouprey Kudu Lapwing Lark Lemur Leopard Lion Llama Lobster Locust Loris Louse Lyrebird Magpie Mallard Manatee Mandrill Mantis Marten Meerkat Mink Mole Mongoose Monkey Moose Mosquito Mouse Mule Narwhal Newt Nightingale Octopus Okapi Opossum Oryx Ostrich Otter Owl Oyster Panther Parrot Partridge Peafowl Pelican Penguin Pheasant Pig Pigeon Pony Porcupine Porpoise Quail Quelea Quetzal Rabbit Raccoon Rail Ram Rat Raven Red deer Red panda Reindeer Rhinoceros Rook Salamander Salmon Sand Dollar Sandpiper Sardine Scorpion Seahorse Seal Shark Sheep Shrew Skunk Snail Snake Sparrow Spider Spoonbill Squid Squirrel Starling Stingray Stinkbug Stork Swallow Swan Tapir Tarsier Termite Tiger Toad Trout Turkey Turtle Viper Vulture Wallaby Walrus Wasp Weasel Whale Wildcat Wolf Wolverine Wombat Woodcock Woodpecker Worm Wren Yak Zebra ================================================ FILE: VirtualCore/Source/Resources/VirtualCore.xcassets/Animals.dataset/Contents.json ================================================ { "data" : [ { "filename" : "Animals.txt", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualCore/Source/Resources/VirtualCore.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualCore/Source/Restore/Download/DownloadBackend.swift ================================================ import Foundation import Combine public enum DownloadState: Hashable { case idle case preCheck(_ message: String) case downloading(_ progress: Double?, _ eta: Double?) case failed(_ error: String) case done(_ localURL: URL) } public protocol DownloadBackend: AnyObject { init(library: VMLibraryController, cookie: String?) var statePublisher: AnyPublisher { get } @MainActor func startDownload(with url: URL) @MainActor func cancelDownload() } ================================================ FILE: VirtualCore/Source/Restore/Download/SimulatedDownloadBackend.swift ================================================ import Foundation import Combine #if DEBUG public final class SimulatedDownloadBackend: NSObject, DownloadBackend { public static let localFileURL = Bundle.virtualCore.url(forResource: "FakeRestoreImage", withExtension: "ipsw")! public init(library: VMLibraryController, cookie: String?) { super.init() } public var statePublisher: AnyPublisher { stateSubject.eraseToAnyPublisher() } private let stateSubject = PassthroughSubject() private var timer: Timer? private var progress: Double = 0 public func startDownload(with url: URL) { let timer = Timer(timeInterval: 0.1, repeats: true) { [weak self] _ in guard let self else { return } progress += 0.01 if progress >= 1.0 { stateSubject.send(.done(Self.localFileURL)) } else { stateSubject.send(.downloading(progress, progress >= 0.15 ? 100 - progress * 100 : 0)) } } self.timer = timer /// Schedule timer manually so that it's not blocked by modal dialogs or event tracking. RunLoop.main.add(timer, forMode: .common) } public func cancelDownload() { stateSubject.send(.failed("Cancelled.")) } } #endif // DEBUG ================================================ FILE: VirtualCore/Source/Restore/Download/URLSessionDownloadBackend.swift ================================================ // // URLSessionDownloadBackend.swift // VirtualCore // // Created by Guilherme Rambo on 07/06/22. // import Foundation import Combine import OSLog import BuddyFoundation public final class URLSessionDownloadBackend: NSObject, ObservableObject, DownloadBackend { private let logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "URLSessionDownloadBackend") let library: VMLibraryController public var cookie: String? public init(library: VMLibraryController, cookie: String?) { self.library = library self.cookie = cookie } private var downloadTask: URLSessionDownloadTask? private var isInFailedState: Bool { guard case .failed = state else { return false } return true } public private(set) lazy var statePublisher: AnyPublisher = $state.eraseToAnyPublisher() @Published public private(set) var state = DownloadState.idle private lazy var session = makeSession() private func makeSession() -> URLSession { let config = URLSessionConfiguration.default config.httpMaximumConnectionsPerHost = 16 return URLSession(configuration: config, delegate: self, delegateQueue: .main) } private var destinationURL: URL? @MainActor public func startDownload(with url: URL) { logger.debug("Start download from \(url.absoluteString.quoted)") session = makeSession() resetProgress() state = .downloading(nil, nil) let filename = url.lastPathComponent self.destinationURL = VBSettings.current.downloadsDirectoryURL.appendingPathComponent(filename) var request = URLRequest(url: url) request.setValue(cookie, forHTTPHeaderField: "Cookie") downloadTask = session.downloadTask(with: request) downloadTask?.delegate = self downloadTask?.resume() } @MainActor public func cancelDownload() { downloadTask?.cancel() downloadTask = nil session.finishTasksAndInvalidate() } private let minElapsedProgressForETA: Double = 0.01 private var elapsedTime: Double = 0 private var ppsObservations: [Double] = [] private let ppsObservationsLimit = 500 private var ppsAverage: Double { guard !ppsObservations.isEmpty else { return -1 } return ppsObservations.reduce(Double(0), +) / Double(ppsObservations.count) } private var pps: Double = -1 private var eta: Double = -1 private var lastProgressDate = Date() private var progress: Double = 0 private func resetProgress() { elapsedTime = 0 eta = -1 pps = -1 ppsObservations = [] } } extension URLSessionDownloadBackend: URLSessionDownloadDelegate, URLSessionDelegate { public func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest) async -> URLRequest? { logger.debug("Will perform HTTP redirection \(response)") if request.url?.absoluteString.lowercased().contains("unauthorized") == true { DispatchQueue.main.async { self.state = .failed("The download failed due to missing authentication credentials.") } return nil } else { if let newCookie = response.value(forHTTPHeaderField: "Set-Cookie"), let firstItem = newCookie.components(separatedBy: ";").first { var newRequest = request let newCookieValue = (newRequest.value(forHTTPHeaderField: "Cookie") ?? "") + "; " + firstItem newRequest.setValue(newCookieValue, forHTTPHeaderField: "Cookie") return newRequest } else { return request } } } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { guard !isInFailedState else { return } logger.notice("Download finished to \(location.path.quoted)") if let response = downloadTask.response as? HTTPURLResponse { guard 200..<300 ~= response.statusCode else { logger.error("Download failed with HTTP \(response.statusCode)") state = .failed("HTTP error \(response.statusCode). Please check the download link.") return } } else { logger.fault("Download task finished without a valid response!") } guard let destinationURL = destinationURL else { state = .failed("Missing destination URL.") assertionFailure("WAT") return } do { if FileManager.default.fileExists(atPath: destinationURL.path) { try FileManager.default.removeItem(at: destinationURL) } try FileManager.default.moveItem(at: location, to: destinationURL) DispatchQueue.main.async { self.state = .done(destinationURL) } } catch { DispatchQueue.main.async { self.state = .failed("Failed to move downloaded file: \(error.localizedDescription)") } } } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let error { logger.error("Download failed - \(error, privacy: .public)") } else { logger.notice("Download completed") } // Successful completion is handled in `urlSession:downloadTask:didFinishDownloadingTo`. guard let error = error else { return } state = .failed(error.localizedDescription) } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { DispatchQueue.main.async { [self] in let interval = Date().timeIntervalSince(lastProgressDate) lastProgressDate = Date() let percent = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) updateProgress(with: percent, interval: interval) } } private func updateProgress(with progress: Double, interval: Double) { let currentPPS = progress / elapsedTime if currentPPS.isFinite && !currentPPS.isZero && !currentPPS.isNaN { ppsObservations.append(currentPPS) if ppsObservations.count >= ppsObservationsLimit { ppsObservations.removeFirst() } } elapsedTime += interval if self.progress > self.minElapsedProgressForETA { if pps < 0 { pps = progress / elapsedTime } eta = (1/ppsAverage) - elapsedTime self.state = .downloading(progress, eta) } else { self.state = .downloading(progress, nil) } self.progress = progress } } ================================================ FILE: VirtualCore/Source/Restore/Installation/RestoreBackend.swift ================================================ import Foundation import Virtualization import BuddyKit @MainActor public protocol RestoreBackend: AnyObject { init(model: VBVirtualMachine, restoringFromImageAt restoreImageFileURL: URL) var progress: Progress { get } func install() async throws func cancel() async } ================================================ FILE: VirtualCore/Source/Restore/Installation/SimulatedRestoreBackend.swift ================================================ import Foundation import Virtualization import BuddyKit import Combine #if DEBUG public final class SimulatedRestoreBackend: NSObject, RestoreBackend, @unchecked Sendable { public init(model: VBVirtualMachine, restoringFromImageAt restoreImageFileURL: URL) { super.init() } public let progress = Progress(totalUnitCount: 100) private var timer: Timer? private var cancellable: AnyCancellable? public func install() async throws { await withCheckedContinuation { continuation in cancellable = Timer.publish(every: 0.1, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in guard let self else { return } let count = progress.completedUnitCount + 1 progress.completedUnitCount = count if count >= progress.totalUnitCount { MainActor.assumeIsolated { self.timer?.invalidate() self.cancellable = nil } continuation.resume() } } } } public func cancel() async { } } #endif // DEBUG ================================================ FILE: VirtualCore/Source/Restore/Installation/VirtualizationRestoreBackend.swift ================================================ import Foundation import Virtualization import BuddyKit import OSLog import Combine public final class VirtualizationRestoreBackend: RestoreBackend { private let logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "VirtualizationRestoreBackend") public let model: VBVirtualMachine public let restoreImageFileURL: URL public init(model: VBVirtualMachine, restoringFromImageAt restoreImageFileURL: URL) { self.model = model self.restoreImageFileURL = restoreImageFileURL } private var cancellables = Set() public let progress = Progress() private var _installer: VZMacOSInstaller? private let virtualMachineSubject = PassthroughSubject() public var virtualMachine: AnyPublisher { virtualMachineSubject.eraseToAnyPublisher() } public func install() async throws { let installModel = model.forInstallation() let config = try await VMInstance.makeConfiguration(for: installModel, installImageURL: restoreImageFileURL) let vm = VZVirtualMachine(configuration: config) await MainActor.run { virtualMachineSubject.send(vm) } let installer = VZMacOSInstaller(virtualMachine: vm, restoringFromImageAt: restoreImageFileURL) _installer = installer createInternalProgressObservations(with: installer) defer { cleanup() } try Task.checkCancellation() try await installer.install() } public func cancel() async { logger.warning("Installation cancelled by client.") progress.cancel() if let _installer, _installer.virtualMachine.canStop { do { logger.info("Stopping installation VM...") try await _installer.virtualMachine.stop() logger.info("Installation VM stopped.") } catch { logger.error("Error forcing installation VM stop - \(error, privacy: .public)") } } cleanup() } private func cleanup() { logger.debug("Cleaning up installation.") cancellables.removeAll() _installer = nil virtualMachineSubject.send(nil) } private func createInternalProgressObservations(with installer: VZMacOSInstaller) { installer.progress .publisher(for: \.totalUnitCount, options: [.initial, .new]) .sink { [weak self] value in self?.progress.totalUnitCount = value } .store(in: &cancellables) installer.progress .publisher(for: \.completedUnitCount, options: [.initial, .new]) .sink { [weak self] value in self?.progress.completedUnitCount = value } .store(in: &cancellables) } } extension VBVirtualMachine { /// Returns a copy of the model configured for use during installation. func forInstallation() -> Self { var mself = self /// Use a fixed 1080p display resolution for installation. mself.configuration.hardware.displayDevices = [VBDisplayPreset.fullHD.device] return mself } } ================================================ FILE: VirtualCore/Source/Restore Images/Models/VBRestoreImageInfo.swift ================================================ // // VBRestoreImageInfo.swift // VirtualCore // // Created by Guilherme Rambo on 07/06/22. // import Foundation import BuddyFoundation public struct VBGuestReleaseChannel: Hashable, Identifiable, Codable { public struct Authentication: Hashable, Identifiable, Codable { public var id: String { name } public var name: String public var url: URL public var note: String } public var id: String public var name: String public var note: String public var icon: String public var authentication: Authentication? public init(id: String, name: String, note: String, icon: String, authentication: Authentication? = nil) { self.id = id self.name = name self.note = note self.icon = icon self.authentication = authentication } } public struct VBGuestReleaseGroup: Hashable, Identifiable, Codable { public var id: String public var name: String public var majorVersion: SoftwareVersion public var minHostVersion: SoftwareVersion public init(id: String, name: String, majorVersion: SoftwareVersion, minHostVersion: SoftwareVersion) { self.id = id self.name = name self.majorVersion = majorVersion self.minHostVersion = minHostVersion } } public struct VBRestoreImageInfo: Hashable, Identifiable, Codable { public var id: String { build } public var group: VBGuestReleaseGroup public var channel: VBGuestReleaseChannel public var name: String public var build: String public var url: URL @DecodableDefault.False public var needsCookie: Bool public init(group: VBGuestReleaseGroup, channel: VBGuestReleaseChannel, name: String, build: String, url: URL, needsCookie: Bool) { self.group = group self.channel = channel self.name = name self.build = build self.url = url self.needsCookie = needsCookie } } public extension VBRestoreImageInfo { var authenticationRequirement: VBGuestReleaseChannel.Authentication? { guard needsCookie else { return nil } return channel.authentication } } public extension VBGuestReleaseChannel.Authentication { func satisfiedCookieHeaderValue(with cookies: [HTTPCookie]) -> String? { let targetCookieNames = Set(["myacinfo", "aidshd", "DSESSIONID", "PHPSESSID", "dawsp", "aasp"]) guard Set(cookies.map(\.name)).intersection(targetCookieNames) == targetCookieNames else { return nil } let formattedCookies = cookies.map({ "\($0.name)=\($0.value)" }).joined(separator: "; ") return formattedCookies } } ================================================ FILE: VirtualCore/Source/Restore Images/Models/VBRestoreImagesResponse.swift ================================================ // // VBRestoreImagesResponse.swift // VirtualCore // // Created by Guilherme Rambo on 07/06/22. // import Foundation struct VBRestoreImagesResponse: Decodable { let success: Bool let error: String? let channels: [VBGuestReleaseChannel] let groups: [VBGuestReleaseGroup] let images: [VBRestoreImageInfo] } ================================================ FILE: VirtualCore/Source/Restore Images/VBAPIClient.swift ================================================ // // VBAPIClient.swift // VirtualCore // // Created by Guilherme Rambo on 07/06/22. // import Foundation import OSLog public final class VBAPIClient { private let logger = Logger(for: VBAPIClient.self) public struct Environment: Hashable { public var baseURL: URL public var apiKey: String #if DEBUG public static let local = Environment( baseURL: URL(string: "https://virtualbuddy.ngrok.io/v2")!, apiKey: "15A25D48-4A34-4EE4-A293-C22B0DE1B54E" ) public static let development = Environment( baseURL: URL(string: "https://virtualbuddy-api-dev.bestbuddyapps3496.workers.dev/v2")!, apiKey: "15A25D48-4A34-4EE4-A293-C22B0DE1B54E" ) #endif public static let production = Environment( baseURL: URL(string: "https://api.virtualbuddy.app/v2")!, apiKey: "15A25D48-4A34-4EE4-A293-C22B0DE1B54E" ) public static var current: Environment { #if DEBUG if let override = UserDefaults.standard.string(forKey: "VBAPIEnvironment") { if override == "development" { return .development } else if override == "local" { return .local } else { assertionFailure("Unknown API environment: \(override)") return .production } } else { return .production } #else return .production #endif } } public let environment: Environment public init(environment: Environment = .current) { self.environment = environment } private func request(for endpoint: String, query: [String: String] = [:], cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy) throws -> URLRequest { let url = environment.baseURL .appendingPathComponent(endpoint) var components = URLComponents(url: url, resolvingAgainstBaseURL: false)! components.queryItems = [ URLQueryItem(name: "apiKey", value: environment.apiKey) ] for (key, value) in query { components.queryItems?.append(URLQueryItem(name: key, value: value)) } var request = try URLRequest(url: components.url.require("Failed to construct request URL.")) request.cachePolicy = cachePolicy return request } private static let decoder = JSONDecoder() @MainActor public func fetchRestoreImages(for guest: VBGuestType, skipCache: Bool) async throws -> SoftwareCatalog { let catalog = try await performCatalogFetch(for: guest, skipCache: skipCache) SoftwareCatalog.setCurrent(catalog, for: guest) return catalog } @MainActor func performCatalogFetch(for guest: VBGuestType, skipCache: Bool) async throws -> SoftwareCatalog { #if DEBUG guard !ProcessInfo.isSwiftUIPreview, !UserDefaults.standard.bool(forKey: "VBForceBuiltInSoftwareCatalog") else { logger.debug("Forcing built-in catalog") return try Self.fetchBuiltInCatalog(for: guest) } #endif do { let remoteCatalog = try await fetchRemoteCatalog(for: guest, skipCache: skipCache) logger.debug("Fetched remote catalog with \(remoteCatalog.restoreImages.count, privacy: .public) restore images") return remoteCatalog } catch { logger.error("Remote catalog fetch failed: \(error, privacy: .public), using local fallback") do { let builtInCatalog = try Self.fetchBuiltInCatalog(for: guest) logger.debug("Fetched built-in catalog with \(builtInCatalog.restoreImages.count, privacy: .public) restore images") return builtInCatalog } catch { logger.fault("Built-in catalog load failed: \(error, privacy: .public)") assertionFailure("Built-in catalog load failed: \(error)") throw error } } } func fetchRemoteCatalog(for guest: VBGuestType, skipCache: Bool) async throws -> SoftwareCatalog { let req = try request( for: guest.restoreImagesAPIPath, cachePolicy: skipCache ? .reloadIgnoringLocalAndRemoteCacheData : .useProtocolCachePolicy ) logger.debug("Fetching remote catalog from \(req.url?.host() ?? "") (skipCache? \(skipCache, privacy: .public))") let (data, res) = try await URLSession.shared.data(for: req) let code = (res as! HTTPURLResponse).statusCode guard code == 200 else { throw "HTTP \(code)" } let response = try Self.decoder.decode(SoftwareCatalog.self, from: data) return response } static func fetchBuiltInCatalog(for guest: VBGuestType) throws -> SoftwareCatalog { let fileName = switch guest { case .mac: "ipsws_v2" case .linux: "linux_v2" } guard let url = Bundle.virtualCore.url(forResource: fileName, withExtension: "json", subdirectory: "SoftwareCatalog") else { throw "\(fileName) not found in VirtualCore SoftwareCatalog resources" } let data = try Data(contentsOf: url) return try decoder.decode(SoftwareCatalog.self, from: data) } @MainActor public func signingStatus(for ipswURL: URL) async -> BuildSigningStatus { #if DEBUG let simulatedStatus: BuildSigningStatus? = if UserDefaults.standard.bool(forKey: "VBForceSigningStatusUnsigned") { .unsigned("This version of macOS is not being signed by Apple for virtual machines, so it can’t be installed.") } else if UserDefaults.standard.bool(forKey: "VBForceSigningStatusSigned") { .signed } else if UserDefaults.standard.bool(forKey: "VBForceSigningStatusRequestFailed") { .checkFailed("Simulated request failure.") } else if ProcessInfo.isSwiftUIPreview { .signed } else { nil } if let simulatedStatus { try? await Task.sleep(for: .milliseconds(200)) return simulatedStatus } #endif // DEBUG do { let req = try request(for: "tss", query: ["ipsw": ipswURL.absoluteString]) logger.debug("Fetching signing status for \(ipswURL.absoluteString.quoted)") let (data, res) = try await URLSession.shared.data(for: req) let code = (res as! HTTPURLResponse).statusCode guard code == 200 else { throw "HTTP \(code)" } let response = try Self.decoder.decode(BuildSigningStatusResponse.self, from: data) logger.info("TSS check request succeeded with response:\n\(String(decoding: data, as: UTF8.self))") if response.isSigned { return .signed } else { assert(response.message != nil, "Expected API to return message for unsigned TSS response.") return .unsigned(response.message ?? "This build is not signed.") } } catch { logger.error("Signing status fetch failed - \(error, privacy: .public)") return .checkFailed(error.localizedDescription) } } } private extension VBGuestType { var restoreImagesAPIPath: String { switch self { case .mac: return "/restore/mac" case .linux: return "/restore/linux" } } } /// Vended by ``VBAPIClient/signingStatus(for:)``. public enum BuildSigningStatus { case signed case unsigned(_ message: String) case checkFailed(_ error: String) } /// TSS signing status check result. private struct BuildSigningStatusResponse: Decodable { /// Whether the build is signed by Apple. public private(set) var isSigned: Bool /// User-facing message when `isSigned` is `false`. public private(set) var message: String? } ================================================ FILE: VirtualCore/Source/Settings/VBSettings+CatalogDownload.swift ================================================ import Foundation import BuddyKit import OSLog private let logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "VBSettings+CatalogDownload") public extension VBSettings { private static let fallbackURL = FileManager.default.temporaryDirectory var downloadsDirectoryURL: URL { do { let baseURL = libraryURL.appendingPathComponent("_Downloads") if !FileManager.default.fileExists(atPath: baseURL.path) { logger.debug("Creating downloads directory at \(baseURL.path)") try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) } return baseURL } catch { logger.fault("Error getting downloads base URL - \(error, privacy: .public)") return Self.fallbackURL } } } extension VBSettings: CatalogDownloadsProvider { public func localFileURL(for restoreImage: RestoreImage) -> URL? { do { let files = try FilePath(downloadsDirectoryURL).children().map(\.url.vb_restoreImageStub) guard let stub = files.vb_elementMatchingDownloadableCatalogContent(at: restoreImage.url) else { return nil } logger.debug("Found download matching \(restoreImage.name.quoted) - \(stub)") /// Take this opportunity to set the extended attribute if it hasn't been set yet. stub.url.vb_addSoftwareCatalogExtendedAttributeIfNeeded(for: restoreImage) return stub.url } catch { logger.fault("Error enumerating downloads directory - \(error, privacy: .public)") return nil } } } extension URL { private static let logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "URL+SoftwareCatalogAttribute") func vb_addSoftwareCatalogExtendedAttributeIfNeeded(for restoreImage: RestoreImage) { guard vb_softwareCatalogData == nil else { return } Self.logger.debug("Adding software catalog extended attribute for \(restoreImage.build) to \(lastPathComponent.quoted)") vb_softwareCatalogData = VirtualBuddyCatalogData( build: restoreImage.build, filename: restoreImage.url.lastPathComponent ) } } ================================================ FILE: VirtualCore/Source/Settings/VBSettings.swift ================================================ // // VBSettings.swift // VirtualCore // // Created by Guilherme Rambo on 05/06/22. // import Foundation import OSLog private let logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: String(describing: VBSettings.self)) public struct VBSettings: Hashable, Sendable { public static var current: VBSettings { VBSettingsContainer.current.settings } public static let updateChannelDidChangeNotification = Notification.Name("VBSettingsUpdateChannelDidChangeNotification") public static let currentVersion = 3 public var version: Int = Self.currentVersion public var libraryURL: URL { didSet { guard libraryURL != oldValue else { return } isLibraryInRemovableVolume = libraryURL.isInRemovableVolume ?? false } } public var updateChannel: AppUpdateChannel { didSet { guard updateChannel != oldValue else { return } NotificationCenter.default.post(name: Self.updateChannelDidChangeNotification, object: updateChannel) } } public var enableTSSCheck: Bool /// Show desktop picture from guest VM in thumbnails instead of the blur hash version. /// Currently not exposed in the UI. public var showDesktopPictureThumbnails: Bool /// Enables using the new ASIF format for boot disk images (requires macOS 26+ host). public var bootDiskImagesUseASIF: Bool /// Updated when user changes the library directory. /// This helps ``VMLibraryController`` determine whether the library is in a removable volume /// so that it can better handle the case where the app is launched before the volume is mounted. public var isLibraryInRemovableVolume: Bool } extension VBSettings { static let defaultUpdateChannel: AppUpdateChannel = .release static let defaultEnableTSSCheck = true static let defaultShowDesktopPictureThumbnails = false static let defaultBootDiskImagesUseASIF: Bool = { if #available(macOS 26, *) { true } else { false } }() init() { self.libraryURL = .defaultVirtualBuddyLibraryURL self.updateChannel = Self.defaultUpdateChannel self.enableTSSCheck = Self.defaultEnableTSSCheck self.showDesktopPictureThumbnails = Self.defaultShowDesktopPictureThumbnails self.bootDiskImagesUseASIF = Self.defaultBootDiskImagesUseASIF self.isLibraryInRemovableVolume = false } private struct Keys { static let version = "version" static let libraryPath: String = { #if DEBUG return ProcessInfo.isSwiftUIPreview ? "libraryPath-preview" : "libraryPath" #else return "libraryPath" #endif }() static let updateChannel = "updateChannel" static let enableTSSCheck = "enableTSSCheck" static let showDesktopPictureThumbnails = "showDesktopPictureThumbnails" static let bootDiskImagesUseASIF = "bootDiskImagesUseASIF" static let isLibraryInRemovableVolume = "isLibraryInRemovableVolume" } init(with defaults: UserDefaults) throws { defaults.register(defaults: [ Keys.enableTSSCheck: Self.defaultEnableTSSCheck ]) self.version = defaults.integer(forKey: Keys.version) self.enableTSSCheck = defaults.bool(forKey: Keys.enableTSSCheck) self.showDesktopPictureThumbnails = defaults.bool(forKey: Keys.showDesktopPictureThumbnails) self.bootDiskImagesUseASIF = defaults.bool(forKey: Keys.bootDiskImagesUseASIF) self.isLibraryInRemovableVolume = defaults.bool(forKey: Keys.isLibraryInRemovableVolume) if let path = defaults.string(forKey: Keys.libraryPath) { self.libraryURL = URL(fileURLWithPath: path) } else { if version == 2 || version == 1 { /// Default library folder changed in VBSettings version 3, /// so if we're decoding settings from a previous release without a /// library defined, use the old default, which may have user VMs in it. /// /// There's a high chance this never gets hit because the legacy default folder is likely /// to be in user defaults, even if the user never changed it. self.libraryURL = ._legacyDefaultVirtualBuddyLibraryURLForMigrationOnly } else { self.libraryURL = .defaultVirtualBuddyLibraryURL } } if let appUpdateChannelID = defaults.string(forKey: Keys.updateChannel) { logger.debug("Found channel \(appUpdateChannelID, privacy: .public) in user defaults") let restoredChannel = AppUpdateChannel.channelsByID[appUpdateChannelID] ?? .release let defaultChannel = AppUpdateChannel.defaultChannel(for: .current) if restoredChannel == .release, defaultChannel != .release { logger.debug("Settings specify release channel, but current build is for \(defaultChannel, privacy: .public), setting channel to \(defaultChannel, privacy: .public)") self.updateChannel = defaultChannel } else { self.updateChannel = restoredChannel } } else { let defaultChannel = AppUpdateChannel.defaultChannel(for: .current) logger.debug("No channel set in preferences, using default channel \(defaultChannel, privacy: .public) for build type \(VBBuildType.current, privacy: .public)") self.updateChannel = defaultChannel } } func write(to defaults: UserDefaults) throws { defaults.set(version, forKey: Keys.version) defaults.set(libraryURL.path, forKey: Keys.libraryPath) defaults.set(updateChannel.id, forKey: Keys.updateChannel) defaults.set(enableTSSCheck, forKey: Keys.enableTSSCheck) defaults.set(showDesktopPictureThumbnails, forKey: Keys.showDesktopPictureThumbnails) defaults.set(bootDiskImagesUseASIF, forKey: Keys.bootDiskImagesUseASIF) defaults.set(isLibraryInRemovableVolume, forKey: Keys.isLibraryInRemovableVolume) } } public extension URL { static let _legacyDefaultVirtualBuddyLibraryURLForMigrationOnly: URL = { do { let baseURL = try FileManager.default.url( for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false ) return baseURL .appendingPathComponent("VirtualBuddy") } catch { fatalError("VirtualBuddy is unable to write to your user's documents directory, this is bad!") } }() static let defaultVirtualBuddyLibraryURL: URL = { do { let baseURL = try FileManager.default.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) let url = baseURL.appendingPathComponent("VirtualBuddy") try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) return url } catch { fatalError("VirtualBuddy is unable to write to your user's Library/Application Support directory, this is bad!") } }() } ================================================ FILE: VirtualCore/Source/Settings/VBSettingsContainer.swift ================================================ // // VBSettingsContainer.swift // VirtualCore // // Created by Guilherme Rambo on 05/06/22. // import Foundation import Combine import OSLog public final class VBSettingsContainer: ObservableObject { private lazy var logger = Logger(for: Self.self) public static let current = VBSettingsContainer() @Published public var settings = VBSettings() public let defaults: UserDefaults public init(with defaults: UserDefaults = .standard) { self.defaults = defaults read() bind() } private lazy var cancellables = Set() } // MARK: - Internal Extensions private extension VBSettingsContainer { func bind() { $settings .removeDuplicates() .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .sink { [weak self] settings in self?.write(settings) } .store(in: &cancellables) } func read() { logger.debug(#function) do { self.settings = try VBSettings(with: defaults) } catch { logger.assert("Failed to read settings from defaults: \(error.log)") } } func write(_ newSettings: VBSettings) { logger.debug(#function) do { try newSettings.write(to: defaults) } catch { logger.assert("Failed to write settings into defaults: \(error.log)") } } } ================================================ FILE: VirtualCore/Source/Utilities/Bundle+Version.swift ================================================ import Foundation import BuddyFoundation public extension Bundle { var vbShortVersionString: String { infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0" } var vbVersion: SoftwareVersion { SoftwareVersion(string: vbShortVersionString) ?? SoftwareVersion.empty } var vbBuild: Int { let str = infoDictionary?["CFBundleVersion"] as? String ?? "0" return Int(str) ?? 0 } } ================================================ FILE: VirtualCore/Source/Utilities/LogStreamer.swift ================================================ import Foundation import OSLog import Combine public struct LogEntry: Identifiable, Hashable, Codable, CustomStringConvertible { public enum Level: String, Codable { case debug = "Debug" case trace = "Trace" case notice = "Notice" case info = "Info" case `default` = "Default" case warning = "Warning" case error = "Error" case fault = "Fault" case critical = "Critical" } public var id = UUID() public var date: Date = .now public var level: Level { _level ?? .default } public let traceID: UInt64 public let message: String private var _level: Level? = nil public enum CodingKeys: String, CodingKey { case traceID = "traceID" case message = "eventMessage" case _level = "messageType" } } public extension LogEntry { private static let timeFormatter: DateFormatter = { let f = DateFormatter() f.dateFormat = "HH:mm:ss.sss" return f }() var formattedTime: String { Self.timeFormatter.string(from: date) } var description: String { "[\(formattedTime)] (\(level.rawValue)) - \(message)" } } public final class LogStreamer: ObservableObject { private lazy var logger = Logger(for: Self.self) private var logProcess: Process? @Published public private(set) var events = [LogEntry]() public let onEvent = PassthroughSubject() public enum Predicate: CustomStringConvertible { case library(String) case subsystem(String) case process(String) case custom(String) public var description: String { switch self { case .library(let name): return "library = '\(name)'" case .subsystem(let name): return "subsystem = '\(name)'" case .process(let name): return "process = '\(name)'" case .custom(let str): return str } } } public let predicate: Predicate public let throttleInterval: Double public init(predicate: Predicate, throttleInterval: Double = 0.5) { self.predicate = predicate self.throttleInterval = throttleInterval } private var eventsCancellable: Cancellable? public func activate() { logger.debug(#function) eventsCancellable = onEvent .throttle(for: .init(throttleInterval), scheduler: RunLoop.main, latest: true) .sink(receiveValue: { [weak self] newEvent in guard let self = self else { return } self.events.insert(newEvent, at: 0) }) let p = Process() p.executableURL = URL(fileURLWithPath: "/usr/bin/log") p.arguments = [ "stream", "--level", "debug", "--style", "ndjson", "--predicate", "\(predicate)" ] let outPipe = Pipe() p.standardError = Pipe() p.standardOutput = outPipe do { try p.run() self.logProcess = p startStreaming(with: outPipe.fileHandleForReading) } catch { logger.fault("Failed to launch log process: \(String(describing: error), privacy: .public)") } } private var streamTask: Task? private func startStreaming(with fileHandle: FileHandle) { streamTask = Task { do { for try await line in fileHandle.bytes.lines { await onTaskProduceEvent(for: line) } } catch { logger.error("AsyncSequence error: \(String(describing: error), privacy: .public)") } } } private func onTaskProduceEvent(for line: String) async { guard !line.contains("Filtering the log data using") else { return } do { let entry = try JSONDecoder().decode(LogEntry.self, from: Data(line.utf8)) await MainActor.run { onEvent.send(entry) } } catch { logger.error("Error decoding log entry \(line, privacy: .public): \(String(describing: error), privacy: .public)") } } public func invalidate() { logger.debug(#function) streamTask?.cancel() streamTask = nil logProcess?.terminate() logProcess = nil } deinit { invalidate() } } #if DEBUG public extension LogStreamer { static let previewSubsystemName = "codes.rambo.LogStreamer.PreviewSubsystem" static let previewLogger = Logger(subsystem: previewSubsystemName, category: previewSubsystemName) static var preview: LogStreamer { DispatchQueue.main.async { LogStreamer.previewLogger.debug("This is a debug message") LogStreamer.previewLogger.info("This is an info message") LogStreamer.previewLogger.notice("This is a notice message") LogStreamer.previewLogger.trace("This is a trace message") LogStreamer.previewLogger.log("This is a log message") LogStreamer.previewLogger.warning("This is a warning message") LogStreamer.previewLogger.error("This is an error message") LogStreamer.previewLogger.fault("This is a fault message") LogStreamer.previewLogger.critical("This is a critical message") } return LogStreamer(predicate: .subsystem(Self.previewSubsystemName)) } } #endif ================================================ FILE: VirtualCore/Source/Utilities/PreventTerminationAssertion.swift ================================================ import Cocoa import BuddyKit import OSLog /// An assertion that prevents the app from being terminated during its lifetime. public final class PreventTerminationAssertion: NSObject { public typealias ShouldTerminateHandler = @MainActor (PreventTerminationAssertion) -> NSApplication.TerminateReply public typealias InvalidationHandler = (PreventTerminationAssertion) -> () @Lock public private(set) var isValid: Bool = true private let logger: Logger public let id: String public let reason: String private let shouldTerminateHandler: ShouldTerminateHandler? private let invalidationHandler: InvalidationHandler? fileprivate init(id: String = UUID().uuidString, reason: String, shouldTerminate: ShouldTerminateHandler? = nil, invalidationHandler: InvalidationHandler? = nil) { self.id = id self.reason = reason self.shouldTerminateHandler = shouldTerminate self.invalidationHandler = invalidationHandler self.logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "PreventTerminationAssertion(\(reason.quoted))") super.init() } @MainActor public func handleShouldTerminate() -> NSApplication.TerminateReply? { guard let shouldTerminateHandler else { return nil } return shouldTerminateHandler(self) } public func invalidate() { logger.debug(#function) assert(isValid, "Attempt to invalidate \(description) multiple times!") isValid = false invalidationHandler?(self) } public override var description: String { "#\(id.shortID) (\(reason.quoted))" } deinit { if isValid { invalidate() } } } private let logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "NSApplication+PreventTermination") public extension NSApplication { var assertionsPreventingAppTermination: [PreventTerminationAssertion] { Self.preventTerminationAssertions.dictionaryRepresentation().values.filter(\.isValid) } var isTerminationBeingPreventedByAssertion: Bool { !assertionsPreventingAppTermination.isEmpty } /// Prevents the app from being terminated for the lifetime of the returned object, or until ``PreventTerminationAssertion/invalidate()`` is called on it. /// - Parameters: /// - id: An arbitrary identifier. Uses a random UUID by default. /// - reason: A user-facing string describing the reason why app termination is being prevented. This is used in the default UI alert if a custom `shouldTerminate` block is not provided. /// - shouldTerminate: A closure that can be used to present a custom confirmation alert and return the appropriate response for the app termination request. /// - invalidationHandler: A closure that's called when the assertion gets invalidated. /// - Returns: The assertion preventing app termination. Callers **must** retain a reference to this object until you want to invalidate the assertion. /// /// If multiple ``PreventTerminationAssertion`` objects are valid, the app will only terminate once all of them have been invalidated, /// and only if `.terminateLater` was returned from `applicationShouldTerminate`, implemented in the app delegate. func preventTermination(id: String = UUID().uuidString, reason: String, shouldTerminate: PreventTerminationAssertion.ShouldTerminateHandler? = nil, invalidationHandler: PreventTerminationAssertion.InvalidationHandler? = nil) -> PreventTerminationAssertion { let assertion = PreventTerminationAssertion(id: id, reason: reason, shouldTerminate: shouldTerminate) { assertion in invalidationHandler?(assertion) self.handleAssertionInvalidated(assertion) } Self.preventTerminationAssertions.setObject(assertion, forKey: assertion.id as NSString) logger.info("Activated prevent termination assertion: \(assertion, privacy: .public)") return assertion } /// Instructs the app to terminate as soon as the last ``PreventTerminationAssertion`` gets invalidated. @MainActor var shouldTerminateWhenLastAssertionInvalidated: Bool { get { Self.shouldTerminateWhenLastAssertionInvalidated } set { Self.shouldTerminateWhenLastAssertionInvalidated = newValue } } } // MARK: - Assertion Management private extension NSApplication { /// How long after the last assertion gets invalidated before the app will terminate automatically. /// If a new assertion is created before this time delay, then termination will wait until that new assertion gets invalidated before terminating. static let delayInSecondsBeforeAutomaticTermination: TimeInterval = 1 static let preventTerminationAssertions = NSMapTable(keyOptions: [.strongMemory, .objectPersonality], valueOptions: [.weakMemory]) @Lock static var shouldTerminateWhenLastAssertionInvalidated = false func handleAssertionInvalidated(_ assertion: PreventTerminationAssertion) { Self.preventTerminationAssertions.removeObject(forKey: assertion.id as NSString) guard !isTerminationBeingPreventedByAssertion else { return } logger.info("All prevent termination assertions invalidated") guard shouldTerminateWhenLastAssertionInvalidated else { return } DispatchQueue.main.asyncAfter(deadline: .now() + Self.delayInSecondsBeforeAutomaticTermination) { [self] in guard !isTerminationBeingPreventedByAssertion else { logger.info("New assertion prevents scheduled termination, waiting for it before terminating.") return } logger.info("Termination requested when all assertions invalidated, terminating now.") NSApp.reply(toApplicationShouldTerminate: true) } } } // MARK: - Convenience extension NSApplication.TerminateReply: @retroactive CustomStringConvertible { public var description: String { switch self { case .terminateCancel: "cancel" case .terminateNow: "now" case .terminateLater: "later" @unknown default: "unknown(\(rawValue))" } } } ================================================ FILE: VirtualCore/Source/Utilities/ProcessInfo+ECID.swift ================================================ import Foundation import IOKit extension ProcessInfo { var machineECID: UInt64? { let entry = IORegistryEntryFromPath(kIOMainPortDefault, "IODeviceTree:/chosen") guard entry != MACH_PORT_NULL else { return nil } guard let ecidData = IORegistryEntrySearchCFProperty(entry, "IODeviceTree:/chosen", "unique-chip-id" as CFString, kCFAllocatorDefault, 0) as? Data else { return nil } guard ecidData.count > 0 else { return nil } return ecidData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> UInt64? in guard let base = buffer.baseAddress else { return nil } return UnsafeRawPointer(base).load(as: UInt64.self) } } } ================================================ FILE: VirtualCore/Source/Utilities/VBMemoryLeakDebugAssertions.swift ================================================ import Foundation public final class VBMemoryLeakDebugAssertions { @inlinable public static func vb_objectShouldBeReleasedSoon(_ object: AnyObject, after interval: TimeInterval = 0.5) { #if DEBUG _vb_objectShouldBeReleasedSoon(object, after: interval) #endif } @inlinable public static func vb_objectIsBeingReleased(_ object: AnyObject) { #if DEBUG _vb_objectIsBeingReleased(object) #endif } #if DEBUG public static let _disableFlag = "VBDisableMemoryLeakAssertions" public static var _vb_debugAssertionsEnabled: Bool { !UserDefaults.standard.bool(forKey: _disableFlag) } public static var _releasedObjects = Set() @inlinable public static func _objectID(_ object: AnyObject) -> String { String(describing: Unmanaged.passUnretained(object).toOpaque()) } @inlinable public static func _vb_objectShouldBeReleasedSoon(_ object: AnyObject, after interval: TimeInterval) { guard _vb_debugAssertionsEnabled else { return } let id = _objectID(object) let className = String(describing: type(of: object)) let description: String if let window = object as? NSWindow { let title = window.title description = "#\(id) (\(className) - \"\(title)\")" } else { description = "#\(id) (\(className) - \"\(String(describing: object))\")" } DispatchQueue.main.asyncAfter(deadline: .now() + interval) { assert(_releasedObjects.contains(id), "💦 POSSIBLE LEAK: \(description) was not released when expected (set \(_disableFlag) defaults flag to disable this)") } } @inlinable public static func _vb_objectIsBeingReleased(_ object: AnyObject) { guard _vb_debugAssertionsEnabled else { return } _releasedObjects.insert(_objectID(object)) } #endif } ================================================ FILE: VirtualCore/Source/Utilities/VolumeUtils.swift ================================================ // // VolumeUtils.swift // VirtualCore // // Created by Guilherme Rambo on 20/07/22. // import Foundation public extension VBSettingsContainer { /// Returns `true` if the VirtualBuddy library seems to be in an APFS volume. var isLibraryInAPFSVolume: Bool { settings.libraryURL.hasAPFSIdentifier } /// Returns `true` if the volume where the VirtualBuddy library resides has enough /// free space to fit the given amount of bytes. /// If checking the free disk space fails, this falls back to returning `true`. func libraryVolumeCanFit(_ size: UInt64) -> Bool { guard let freeSize = settings.libraryURL.freeDiskSpaceOnVolume else { return true } return Int64(freeSize) - Int64(size) > 0 } } public extension VMLibraryController { /// Whether this library is stored in an APFS volume. var isInAPFSVolume: Bool { libraryURL.hasAPFSIdentifier } } public extension URL { /// Checks if the item at the URL contains an APFS content identifier, as a way to check for /// whether the containing volume is an APFS volume. var hasAPFSIdentifier: Bool { #if DEBUG guard !UserDefaults.standard.bool(forKey: "VBSimulateNonAPFSVolume") else { return false } #endif guard let values = try? resourceValues(forKeys: [.fileContentIdentifierKey]), values.fileContentIdentifier != nil else { return false } return true } /// The free disk space in the volume that contains this URL. var freeDiskSpaceOnVolume: UInt64? { do { let attrs = try FileManager.default.attributesOfFileSystem(forPath: path) guard let freeSize = attrs[.systemFreeSize] as? UInt64 else { return nil } return freeSize } catch { return nil } } /// User-friendly name for the volume that contains this URL. var containingVolumeName: String? { guard let volumeURL = (try? resourceValues(forKeys: [.volumeURLKey]))?.volume else { return nil } guard let values = try? volumeURL.resourceValues(forKeys: [.volumeLocalizedNameKey, .volumeNameKey]) else { return nil } if let localizedName = values.volumeLocalizedName { return localizedName } else if let name = values.volumeName { return name } else { return volumeURL.lastPathComponent } } /// `true` if the URL is contained in a volume that's not internal. var residesInExternalVolume: Bool { (try? resourceValues(forKeys: [.volumeIsInternalKey]))?.volumeIsInternal == false } /// Returns the URL for the external volume that contains this URL. /// Returns `nil` if the URL resides in internal storage, or if the volume couldn't be determined. var externalVolumeURL: URL? { guard residesInExternalVolume else { return nil } return try? resourceValues(forKeys: [.volumeURLKey]).volume } } ================================================ FILE: VirtualCore/Source/Utilities/WeakReference.swift ================================================ import Foundation /// Wraps a reference type as a weak reference. /// Useful as value type in collections when holding a strong reference to the objects is undesirable. public struct WeakReference { public private(set) weak var object: Object? public init(_ object: Object) { self.object = object } } extension WeakReference: Equatable where Object: Equatable { } extension WeakReference: Hashable where Object: Hashable { } ================================================ FILE: VirtualCore/Source/VirtualCatalog/LegacyCatalog.swift ================================================ import Foundation import BuddyFoundation public struct LegacyCatalog: Decodable { public let channels: [LegacyCatalogChannel] public let groups: [LegacyCatalogGroup] public let images: [LegacyRestoreImage] } public struct LegacyCatalogChannel: Hashable, Identifiable, Codable { public struct Authentication: Hashable, Identifiable, Codable { public var id: String { name } public var name: String public var url: URL public var note: String } public var id: String public var name: String public var note: String public var icon: String } public struct LegacyCatalogGroup: Hashable, Identifiable, Codable { public var id: String public var name: String public var majorVersion: SoftwareVersion public var minHostVersion: SoftwareVersion } public struct LegacyRestoreImage: Hashable, Identifiable, Codable { public var id: String { build } public var group: LegacyCatalogGroup public var channel: LegacyCatalogChannel public var name: String public var build: String public var url: URL public var needsCookie: Bool? } public extension LegacyCatalog { private static let decoder = JSONDecoder() init(data: Data) throws { self = try Self.decoder.decode(Self.self, from: data) } init(contentsOf url: URL) throws { let data = try Data(contentsOf: url) try self.init(data: data) } } ================================================ FILE: VirtualCore/Source/VirtualCatalog/MobileDeviceFramework.swift ================================================ import Foundation import BuddyFoundation /// A representation of the MobileDevice framework and its properties. public struct MobileDeviceFramework: Sendable { /// The version of the MobileDevice framework. public let version: SoftwareVersion /// The MobileDevice framework in the current system, or `nil` if it couldn't be found /// or its properties couldn't be parsed. public static var current: MobileDeviceFramework? { MobileDeviceFramework() } } private extension MobileDeviceFramework { init?() { #if DEBUG if let simulatedVersion = UserDefaults.standard.string(forKey: "VBSimulateMobileDeviceVersion") { self.init(version: SoftwareVersion(string: simulatedVersion)!) return } #endif let path = "/System/Library/PrivateFrameworks/MobileDevice.framework" guard let bundle = Bundle(path: path) else { assertionFailure("MobileDevice.framework not found at \(path)") return nil } guard let info = bundle.infoDictionary else { assertionFailure("MobileDevice.framework is missing an info dictionary") return nil } guard let rawVersion = info[kCFBundleVersionKey as String] as? String else { assertionFailure("MobileDevice.framework has missing or invalid CFBundleVersion") return nil } guard let version = SoftwareVersion(string: rawVersion) else { assertionFailure("MobileDevice.framework has invalid CFBundleVersion: \(rawVersion)") return nil } self.init(version: version) } } ================================================ FILE: VirtualCore/Source/VirtualCatalog/README.md ================================================ # VirtualCatalog This directory includes models and associated logic used by VirtualBuddy to inspect and resolve downloadable restore images for virtual machines. The catalog itself is a JSON file stored in the VirtualBuddy GitHub repository. There are two JSON files for the current version of the catalog: - `ipsws_v2.json`: for macOS virtual machines - `linux_v2.json`: for Linux virtual machines In order to avoid hitting GitHub too frequently and provide server-side logic, I run a small function on Cloudflare that the app talks to via HTTP. The server fetches the appropriate JSON file from the repository based on the request path, performs any compatibility conversions depending on the client version, then returns the catalog in the response, caching it on the CDN for a short period. ## Adding New Versions of macOS To add a new version of macOS to the catalog, use `vctool`, which is included as part of VirtualBuddy. If you have installed VirtualBuddy in `/Applications`, then `vctool` will be available at `/Applications/VirtualBuddy.app/Contents/MacOS/vctool`. You may symlink `vctool` to a directory in your `PATH` or just add the corresponding `VirtualBuddy.app/Contents/MacOS` directory to your `PATH` to make running the tool more convenient. You will also need to clone the VirtualBuddy repository itself so that you can open a pull request with the updated catalog after running `vctool`. To add a new release to the catalog, use the `vctool catalog image add` command: ```bash USAGE: vctool catalog image add --ipsw --channel --name --output [--force] OPTIONS: -i, --ipsw URL to the IPSW file. -c, --channel ID of the release channel (devbeta or regular). -n, --name User-facing name for the release (ex: "macOS 15.0 Developer Beta 4"). -o, --output Path to the software catalog JSON file that will be updated. -f, --force Replace existing build if it already exists in the catalog. -h, --help Show help information. ``` Assuming you have cloned the VirtualBuddy repository in `~/Developer/VirtualBuddy`, here's an example of how you could add a new image to the catalog: ```bash vctool catalog image add \ -i 'https://updates.cdn-apple.com/2025SpringFCS/fullrestores/082-44534/CE6C1054-99A3-4F67-A823-3EE9E6510CDE/UniversalMac_15.5_24F74_Restore.ipsw' \ -c regular \ -n 'macOS 15.5' \ -o ~/Developer/VirtualBuddy/data/ipsws_v2.json ``` If successful, the `ipsws_v2.json` file in your clone of the VirtualBuddy repository will have the new macOS release added to it. You may then create a new branch in your fork and submit a pull request. Shortly after a pull request that updates the software catalog is merged, the updated catalog is picked up by the server, making any new OS releases available in the app. ## Updating Existing Versions If an OS build that's already in the catalog needs to be updated, just use the same command but add the `--force` flag so that it doesn't complain about the version already being in the catalog. Any changes made to the title, channel, or other properties will be updated. ## ⚠️ Important Note About New Major Versions Adding new major OS versions (ex: macOS ~16~26) requires additional work because a corresponding release group has to be added, and that requires the availability of image assets that follow a certain specification used by the app. For that reason, adding major new OS versions is currently reserved to the maintainer of the repository (@insidegui). ================================================ FILE: VirtualCore/Source/VirtualCatalog/ResolvedCatalog.swift ================================================ import Foundation import BuddyFoundation import OSLog private let logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "ResolvedCatalog") public protocol ResolvedCatalogModel: Identifiable, Hashable, Sendable { } /// Represents a ``SoftwareCatalog`` that's been processed in order to resolve /// all channel and group members, as well as the supported features and requirement sets for the current environment. public struct ResolvedCatalog: Hashable, Sendable { /// The resolved release groups. public var groups: [ResolvedCatalogGroup] } public struct ResolvedCatalogGroup: ResolvedCatalogModel { public var id: CatalogGroup.ID { group.id } public var group: CatalogGroup public var restoreImages: [ResolvedRestoreImage] public var name: String { group.name } public var majorVersion: SoftwareVersion { group.majorVersion } public var image: CatalogGraphic { group.image } public var darkImage: CatalogGraphic { group.darkImage ?? group.image } public init(group: CatalogGroup, restoreImages: [ResolvedRestoreImage]) { self.group = group self.restoreImages = restoreImages } } public struct ResolvedRestoreImage: ResolvedCatalogModel, DownloadableCatalogContent { public var id: RestoreImage.ID { image.id } public var image: RestoreImage public var channel: CatalogChannel public var features: [ResolvedVirtualizationFeature] public var requirements: ResolvedRequirementSet public var deviceSupportVersion: CatalogDeviceSupportVersion? public var status: ResolvedFeatureStatus public var localFileURL: URL? public var name: String { image.name } public var build: String { image.build } public var version: SoftwareVersion { image.version } public var mobileDeviceMinVersion: SoftwareVersion { image.mobileDeviceMinVersion } public var url: URL { image.url } public var downloadSize: Int64 { Int64(image.downloadSize ?? 0) } public var isDownloaded: Bool { localFileURL != nil } public init(image: RestoreImage, channel: CatalogChannel, features: [ResolvedVirtualizationFeature], requirements: ResolvedRequirementSet, status: ResolvedFeatureStatus, localFileURL: URL?, deviceSupportVersion: CatalogDeviceSupportVersion?) { self.image = image self.channel = channel self.features = features self.requirements = requirements self.status = status self.localFileURL = localFileURL self.deviceSupportVersion = deviceSupportVersion } } /// The status of a feature or requirement set for the current environment. public enum ResolvedFeatureStatus: Hashable, Sendable { /// The feature is fully supported. case supported /// The feature is partially supported. case warning(title: String?, message: String) /// The feature is not supported. case unsupported(title: String?, message: String) var title: String? { switch self { case .supported: return nil case .warning(let title, _), .unsupported(let title, _): return title } } var message: String? { switch self { case .supported: return nil case .warning(_, let message), .unsupported(_, let message): return message } } } /// A feature that's been resolved for the current environment, indicating whether it is supported. public struct ResolvedVirtualizationFeature: ResolvedCatalogModel { public var feature: VirtualizationFeature public var status: ResolvedFeatureStatus public var platform: CatalogGuestPlatform public var id: VirtualizationFeature.ID { feature.id } public var minVersionGuest: SoftwareVersion { feature.minVersionGuest } public var minVersionHost: SoftwareVersion { feature.minVersionHost } public var name: String { feature.name } public var unsupportedPlatform: Bool { feature.unsupportedPlatform } } /// A requirement set that's been resolved for the current environment. public struct ResolvedRequirementSet: ResolvedCatalogModel { public var id: RequirementSet.ID { requirements.id } public var requirements: RequirementSet public var status: ResolvedFeatureStatus public init(requirements: RequirementSet, status: ResolvedFeatureStatus) { self.requirements = requirements self.status = status } } // MARK: - Catalog Resolution /// Represents a guest platform such as Mac or Linux. /// Not an enum just in case more platforms are added in the future (iOS? 🤞🏻) public struct CatalogGuestPlatform: ResolvedCatalogModel, RawRepresentable, CaseIterable, CustomStringConvertible { public typealias RawValue = String public var rawValue: String { id } public var id: String public var name: String public static let mac = CatalogGuestPlatform(id: "mac", name: "Mac") public static let linux = CatalogGuestPlatform(id: "linux", name: "Linux") public static let unknown = CatalogGuestPlatform(id: "_unknown", name: "Unknown") public static let allCases: [CatalogGuestPlatform] = [.mac, .linux] public init(rawValue: String) { if let platform = Self.allCases.first(where: { $0.rawValue.caseInsensitiveCompare(rawValue) == .orderedSame }) { self = platform } else { assertionFailure("Unsupported platform \"\(rawValue)\"") self = .unknown } } init(id: String, name: String) { self.id = id self.name = name } public var description: String { id } } /// Protocol adopted by types that can provide local file URLs to a ``CatalogResolutionEnvironment``. /// /// This type is used when resolving download status for restore images. public protocol CatalogDownloadsProvider: Sendable { /// Returns the local file URL matching the remote restore image. /// /// Implementations must return `nil` if a local file matching the remote restore image has not been downloaded yet. func localFileURL(for restoreImage: RestoreImage) -> URL? } /// Properties used when resolving a catalog for a given environment. /// These are used to assess the support status for different features/requirements. public struct CatalogResolutionEnvironment: Sendable { /// The host OS version. public var hostVersion: SoftwareVersion /// The guest OS version. public var guestVersion: SoftwareVersion /// The guest OS platform. public var guestPlatform: CatalogGuestPlatform /// The version of the host app. public var appVersion: SoftwareVersion /// The version of the MobileDevice framework on the host. public var mobileDeviceVersion: SoftwareVersion /// The number of CPU cores configured for the VM. /// Can be set to nil if performing resolution before configuration, /// in which case the requirement set will be considered as satisfied. public var cpuCoreCount: Int? /// The number of CPU cores configured for the VM. /// Can be set to nil if performing resolution before configuration, /// in which case the requirement set will be considered as satisfied. public var memorySizeMB: Int? /// Information about catalog content that's already been downloaded by the host. public var downloadsProvider: CatalogDownloadsProvider? public init(hostVersion: SoftwareVersion, guestVersion: SoftwareVersion, guestPlatform: CatalogGuestPlatform, appVersion: SoftwareVersion, mobileDeviceVersion: SoftwareVersion, cpuCoreCount: Int? = nil, memorySizeMB: Int? = nil, downloadsProvider: CatalogDownloadsProvider? = nil) { self.hostVersion = hostVersion self.guestVersion = guestVersion self.guestPlatform = guestPlatform self.appVersion = appVersion self.mobileDeviceVersion = mobileDeviceVersion self.cpuCoreCount = cpuCoreCount self.memorySizeMB = memorySizeMB self.downloadsProvider = downloadsProvider } } public extension ResolvedCatalog { init(environment: CatalogResolutionEnvironment, catalog: SoftwareCatalog) { self.groups = catalog.groups.map { group in let images = catalog.restoreImages.filter({ $0.group == group.id }) /// Resolve images one by one to prevent error on single image from taking down entire catalog. var resolvedImages = [ResolvedRestoreImage]() for image in images { do { let resolvedImage = try ResolvedRestoreImage(environment: environment, catalog: catalog, image: image) resolvedImages.append(resolvedImage) } catch { logger.error("Error resolving image \(image.id, privacy: .public) - \(error, privacy: .public)") } } return ResolvedCatalogGroup(group: group, restoreImages: resolvedImages) } } } public extension ResolvedRestoreImage { init(environment: CatalogResolutionEnvironment, catalog: SoftwareCatalog, image: RestoreImage) throws { try self.init( image: image, channel: catalog.channel(with: image.channel), features: catalog.features.map { ResolvedVirtualizationFeature(feature: $0, status: .supported, platform: environment.guestPlatform) }, requirements: ResolvedRequirementSet(requirements: catalog.requirementSet(with: image.requirements), status: .supported), status: .supported, localFileURL: environment.downloadsProvider?.localFileURL(for: image), deviceSupportVersion: catalog.deviceSupportVersion(for: image) ) update(with: environment) } mutating func update(with environment: CatalogResolutionEnvironment) { /// Adds the guest OS version to the environment so that features/requirements that depend on it get the correct status. let versionedEnvironment = environment.guest(version: image.version) /// Mobile device requirement is isolated from min host/app requirements. if versionedEnvironment.mobileDeviceVersion < image.mobileDeviceMinVersion { if let deviceSupportVersion { self.status = .unsupported(title: deviceSupportVersion.title, message: deviceSupportVersion.instructions) } else { self.status = .mobileDeviceOutdated } } features = features.map { $0.updated(with: versionedEnvironment) } requirements.update(with: versionedEnvironment) /// Only override status if it's "more important" than the current status. if requirements.status.isMoreImportant(than: status) { status = requirements.status } } } extension SoftwareCatalog { func deviceSupportVersion(for image: RestoreImage) -> CatalogDeviceSupportVersion? { deviceSupportVersions.first(where: { $0.mobileDeviceMinVersion == image.mobileDeviceMinVersion || ($0.osVersion.major == image.version.major && $0.osVersion.minor == image.version.minor) || $0.osVersion.major == image.version.major }) } } public extension ResolvedVirtualizationFeature { mutating func update(with environment: CatalogResolutionEnvironment) { guard !feature.unsupportedPlatform else { self.status = .unsupportedGuestPlatform(feature, platform: environment.guestPlatform) return } guard environment.hostVersion >= self.feature.minVersionHost else { if self.feature.minVersionGuest == self.feature.minVersionHost { self.status = .unsupportedHostAndGuestAligned(feature) } else { self.status = .unsupportedHost(feature) } return } guard environment.guestVersion >= self.feature.minVersionGuest else { if self.feature.minVersionGuest == self.feature.minVersionHost { self.status = .unsupportedHostAndGuestAligned(feature) } else { self.status = .unsupportedGuest(feature) } return } self.status = .supported } func updated(with environment: CatalogResolutionEnvironment) -> Self { var mSelf = self mSelf.update(with: environment) return mSelf } } public extension ResolvedRequirementSet { mutating func update(with environment: CatalogResolutionEnvironment) { guard environment.hostVersion >= self.requirements.minVersionHost else { self.status = .unsupportedHost(requirements) return } self.status = .supported } func updated(with environment: CatalogResolutionEnvironment) -> Self { var mSelf = self mSelf.update(with: environment) return mSelf } } extension ResolvedFeatureStatus { static func unsupported(_ message: String?...) -> Self { .unsupported(title: nil, message: message.compactMap({ $0 }).joined(separator: "\n")) } static func unsupportedHostAndGuestAligned(_ feature: VirtualizationFeature) -> Self { .unsupported("Requires macOS \(feature.minVersionHost.shortDescription) or later.", feature.detail) } static func unsupportedHost(_ feature: VirtualizationFeature) -> Self { .unsupportedHost(feature.minVersionHost, detail: feature.detail) } static func unsupportedHost(_ requirements: RequirementSet) -> Self { .unsupportedHost(requirements.minVersionHost) } static func unsupportedHost(_ minVersionHost: SoftwareVersion, detail: String? = nil) -> Self { .unsupported("Requires a Mac on macOS \(minVersionHost.shortDescription) or later.", detail) } static func unsupportedGuest(_ feature: VirtualizationFeature) -> Self { .unsupported("Requires virtual machine running macOS \(feature.minVersionHost.shortDescription) or later.", feature.detail) } static func unsupportedGuestPlatform(_ feature: VirtualizationFeature, platform: CatalogGuestPlatform) -> Self { .unsupported("Not supported for \(platform.name) guests.", feature.detail) } static var mobileDeviceOutdated: Self { .unsupported(title: Self.defaultDeviceSupportUpdateNeededTitle, message: Self.defaultDeviceSupportUpdateNeededInstructions) } static let defaultDeviceSupportUpdateNeededTitle = "Device Support Update Required" static let defaultDeviceSupportUpdateNeededInstructions = """ This version of macOS requires device support files which are not currently installed on your system. It's likely that this is a beta for a major macOS version and your Mac is not running the corresponding macOS beta. Device support files can be obtained by installing the Xcode beta, they are sometimes made available separately in the Apple Developer portal. """ } struct CatalogError: LocalizedError, CustomStringConvertible { var errorDescription: String? init(_ errorDescription: String) { self.errorDescription = errorDescription } var description: String { errorDescription ?? "" } } public extension SoftwareCatalog { func group(with id: CatalogGroup.ID) throws -> CatalogGroup { guard let group = groups.first(where: { $0.id == id }) else { throw CatalogError("Group not found with id \"\(id)\"") } return group } func channel(with id: CatalogGroup.ID) throws -> CatalogChannel { guard let channel = channels.first(where: { $0.id == id }) else { throw CatalogError("Channel not found with id \"\(id)\"") } return channel } func requirementSet(with id: RequirementSet.ID) throws -> RequirementSet { guard let requirements = requirementSets.first(where: { $0.id == id }) else { throw CatalogError("Requirement set not found with id \"\(id)\"") } return requirements } } public extension CatalogResolutionEnvironment { /** Catalog resolution environment for the current state of the host. This property is computed because certain aspects can change during the app's lifecycle, such as MobileDevice version and downloads. */ static var current: CatalogResolutionEnvironment { CatalogResolutionEnvironment( hostVersion: .currentHost, guestVersion: .currentHost, guestPlatform: .mac, appVersion: Bundle.main.softwareVersion, mobileDeviceVersion: MobileDeviceFramework.current?.version ?? .init(major: 0, minor: 0, patch: 0), downloadsProvider: VBSettings.current ) } func guest(platform: CatalogGuestPlatform, version: SoftwareVersion) -> Self { var mSelf = self mSelf.guestPlatform = platform mSelf.guestVersion = version return mSelf } func guest(platform: CatalogGuestPlatform) -> Self { var mSelf = self mSelf.guestPlatform = platform return mSelf } func guest(version: SoftwareVersion) -> Self { var mSelf = self mSelf.guestVersion = version return mSelf } } extension SoftwareVersion { static let currentHost: SoftwareVersion = { #if DEBUG if let simulatedVersionString = UserDefaults.standard.string(forKey: "VBSimulateHostVersion") { return SoftwareVersion(string: simulatedVersionString)! } #endif let v = ProcessInfo.processInfo.operatingSystemVersion return SoftwareVersion(major: v.majorVersion, minor: v.minorVersion, patch: v.patchVersion) }() } // MARK: - Resolved Feature Detail public extension ResolvedVirtualizationFeature { var detail: String { switch status { case .supported: switch platform { case .mac: if minVersionHost == minVersionGuest { "Supported. Requires macOS \(minVersionHost.shortDescription) or later." } else { "Supported. Requires host on macOS \(minVersionHost.shortDescription) or later and guest on macOS \(minVersionGuest.shortDescription) or later." } case .linux: "Supported. Requires host on macOS \(minVersionHost.shortDescription) or later." default: "Supported." } case .warning(_, let message), .unsupported(_, let message): message } } } // MARK: - Resolved Status Merging public extension ResolvedFeatureStatus { var isSupported: Bool { if case .supported = self { true } else { false } } var isWarning: Bool { if case .warning = self { true } else { false } } var isUnsupported: Bool { if case .unsupported = self { true } else { false } } var level: Int { switch self { case .supported: 0 case .warning: 1 case .unsupported: 2 } } func isMoreImportant(than other: ResolvedFeatureStatus) -> Bool { level > other.level } } ================================================ FILE: VirtualCore/Source/VirtualCatalog/SoftwareCatalog+DownloadMatching.swift ================================================ import Foundation import BuddyFoundation import OSLog private let matchLogger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "SoftwareCatalog+DownloadMatching") public extension URL { private static let virtualBuddySoftwareCatalogDataKey = "codes.rambo.VirtualBuddy.SoftwareCatalogData" /// Custom metadata stored by VirtualBuddy as an extended attribute. /// This is used to match restore images with those in a software catalog even if they are /// renamed by the user or moved within the same volume. struct VirtualBuddyCatalogData: Codable, Hashable, Sendable { /// The build number for the OS version represented by the restore image file. public var build: String /// The original name of the corresponding file in the software catalog. public var filename: String public init(build: String, filename: String) { self.build = build self.filename = filename } public init(_ image: RestoreImage) { self.init(build: image.build, filename: image.url.lastPathComponent) } } var vb_softwareCatalogData: VirtualBuddyCatalogData? { get { if let value: VirtualBuddyCatalogData = vb_decodeExtendedAttribute(forKey: Self.virtualBuddySoftwareCatalogDataKey) { value } else if let downloadedFromURL = vb_whereFromsSpotlightMetadata.first, let build = downloadedFromURL.lastPathComponent.matchAppleOSBuild() { /// The `com.apple.metadata:kMDItemWhereFroms` extended attribute can be used to determine where a file was originally downloaded from. /// If the original download URL had a well-formed OS version build in it, then we can use that attribute even if the file doesn't have the custom VirtualBuddy attribute. VirtualBuddyCatalogData(build: build, filename: downloadedFromURL.lastPathComponent) } else { nil } } nonmutating set { guard let newValue else { try? vb_removeExtendedAttribute(forKey: Self.virtualBuddySoftwareCatalogDataKey) return } try? vb_encodeExtendedAttribute(newValue, forKey: Self.virtualBuddySoftwareCatalogDataKey) } } } extension URL { /// Parses a Spotlight attribute that includes the URL that was used to download the file. /// This attribute is added automatically and can be used when matching local files with software catalog contents. var vb_whereFromsSpotlightMetadata: [URL] { guard let data = vb_extendedAttributeData(forKey: "com.apple.metadata:kMDItemWhereFroms", base64: false) else { return [] } guard let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [Any] else { return [] } return plist .compactMap { $0 as? String } .compactMap { URL(string: $0) } } /// Loads properties that can be used to match a local file URL with a restore image in the software catalog. /// Used when matching user-provided restore image files or previously-downloaded restore images with catalog content. struct RestoreImageStub: Hashable, Sendable, DownloadableCatalogContent, CustomStringConvertible { var id: String { build } var build: String var url: URL init(build: String, url: URL) { self.build = build self.url = url } init(url: URL) { /// We need some way to determine the OS build corresponding to this file URL. /// This will be first read from the extended attributes set by the app itself when it downloads a software image. /// This metadata will survive file renames and files being moved within the same volume. /// If no metadata can be found, attempt to parse an OS build string from the file name itself. let build = url.vb_softwareCatalogData?.build ?? url.lastPathComponent.matchAppleOSBuild() ?? "" self.init(build: build, url: url) } var description: String { "\(url.lastPathComponent) (build \(build.isEmpty ? "?" : build))" } } /// Container for properties of a restore image that can be inferred from a local file by reading from extended attributes or parsing from the file name. var vb_restoreImageStub: RestoreImageStub { RestoreImageStub(url: self) } /// Attempts to infer the OS version represented by a restore image file or URL. var vb_restoreImageVersion: SoftwareVersion? { let candidates = [ vb_softwareCatalogData?.filename, lastPathComponent, vb_whereFromsSpotlightMetadata.first?.lastPathComponent ].compactMap { $0 } for candidate in candidates { if let version = candidate.matchAppleOSVersion() { return version } } return nil } } public extension SoftwareCatalog { /// Returns the restore image in the catalog that corresponds to the restore image in the file URL. /// /// This matches local restore images with catalog images by file name, build number, or using extended attributes that /// the app automatically sets on restore images downloaded through the app. func restoreImageMatchingDownloadableCatalogContent(at fileURL: URL) -> RestoreImage? { restoreImages.vb_elementMatchingDownloadableCatalogContent(at: fileURL) } } public extension ResolvedCatalog { func restoreImageMatchingDownloadableCatalogContent(at fileURL: URL) -> ResolvedRestoreImage? { let restoreImages: [ResolvedRestoreImage] = groups.flatMap(\.restoreImages) return restoreImages.vb_elementMatchingDownloadableCatalogContent(at: fileURL) } } extension Array where Element: DownloadableCatalogContent { func vb_elementMatchingDownloadableCatalogContent(at url: URL) -> Element? { if let match = first(where: { $0.url.lastPathComponent.caseInsensitiveCompare(url.lastPathComponent) == .orderedSame }) { matchLogger.debug("Matched by file name: \(url.lastPathComponent.quoted) <> \(match.url.lastPathComponent.quoted)") return match } else if url.isFileURL, let catalogData = url.vb_softwareCatalogData, let match = first(where: { $0.build == catalogData.build || $0.url.lastPathComponent.caseInsensitiveCompare(catalogData.filename) == .orderedSame }) { matchLogger.debug("Matched by metadata: \(url.lastPathComponent.quoted) <> \(match.url.lastPathComponent.quoted)") return match } else if let build = url.lastPathComponent.matchAppleOSBuild(), let match = first(where: { $0.build.caseInsensitiveCompare(build) == .orderedSame }) { matchLogger.debug("Matched by build: \(url.lastPathComponent.quoted) <> \(match.url.lastPathComponent.quoted)") return match } else { return nil } } } // MARK: - Best-effort Resolution public extension SoftwareCatalog { /// Attempts to resolve a restore image using catalog matching. If no catalog entry matches, /// creates a best-effort inferred restore image using metadata from the URL. func resolvedRestoreImage(matching url: URL, guestType: VBGuestType) -> ResolvedRestoreImage? { if let match = restoreImageMatchingDownloadableCatalogContent(at: url) { return try? ResolvedRestoreImage( environment: .current.guestType(guestType), catalog: self, image: match ) } guard let inferredImage = inferredRestoreImage(for: url, guestType: guestType) else { return nil } return try? ResolvedRestoreImage( environment: .current.guestType(guestType), catalog: self, image: inferredImage ) } } private extension SoftwareCatalog { func inferredRestoreImage(for url: URL, guestType: VBGuestType) -> RestoreImage? { guard let version = url.vb_restoreImageVersion else { return nil } let build = url.vb_restoreImageStub.build let resolvedBuild = build.isEmpty ? url.deletingPathExtension().lastPathComponent : build let groupID = groupID(for: version) ?? "custom" let channelID = channels.first?.id ?? "custom" let requirementID = bestRequirementSetID(for: version) ?? "custom" let name = inferredName(for: version, guestType: guestType) let mobileDeviceMinVersion = inferredMobileDeviceMinVersion(for: version) return RestoreImage( id: resolvedBuild, group: groupID, channel: channelID, requirements: requirementID, name: name, build: resolvedBuild, version: version, mobileDeviceMinVersion: mobileDeviceMinVersion, url: url, downloadSize: nil ) } func inferredName(for version: SoftwareVersion, guestType: VBGuestType) -> String { switch guestType { case .mac: return "macOS \(version.shortDescription)" case .linux: return "Linux \(version.shortDescription)" } } func groupID(for version: SoftwareVersion) -> CatalogGroup.ID? { groups.first(where: { $0.majorVersion.major == version.major })?.id } func inferredMobileDeviceMinVersion(for version: SoftwareVersion) -> SoftwareVersion { if let deviceSupportVersion = deviceSupportVersions.first(where: { ($0.osVersion.major == version.major && $0.osVersion.minor == version.minor) || $0.osVersion.major == version.major }) { return deviceSupportVersion.mobileDeviceMinVersion } return .empty } func bestRequirementSetID(for version: SoftwareVersion) -> RequirementSet.ID? { guard !requirementSets.isEmpty else { return nil } if let minHost13 = requirementSets.first(where: { $0.id == "min_host_13" }), let minHost12 = requirementSets.first(where: { $0.id == "min_host_12" }), let threshold = SoftwareVersion(string: "13.3") { return version >= threshold ? minHost13.id : minHost12.id } return requirementSets .max(by: { $0.minVersionHost < $1.minVersionHost })? .id } } ================================================ FILE: VirtualCore/Source/VirtualCatalog/SoftwareCatalog.swift ================================================ import Foundation import BuddyFoundation /// Adopted by all models present in a VirtualBuddy software catalog. public protocol CatalogModel: Identifiable, Hashable, Codable, Sendable { } /// Defines a set of requirements a given software image needs from the virtual machine in order to work. public struct RequirementSet: CatalogModel { /// Identifies the requirement set, used to reference a requirement set from a software image definition. public var id: String /// The minimum number of CPU cores. public var minCPUCount: Int /// The minimum amount of RAM the VM needs. public var minMemorySizeMB: Int /// The minimum host operating system version required to run the system. public var minVersionHost: SoftwareVersion public init(id: String, minCPUCount: Int, minMemorySizeMB: Int, minVersionHost: SoftwareVersion) { self.id = id self.minCPUCount = minCPUCount self.minMemorySizeMB = minMemorySizeMB self.minVersionHost = minVersionHost } } /// Defines a feature of Virtualization and its associated requirements. public struct VirtualizationFeature: CatalogModel { /// Identifies the feature, used to reference a feature from a software image definition. public var id: String /// The minimum guest OS version required to use the feature. public var minVersionGuest: SoftwareVersion /// The minimum host OS version required to use the feature. public var minVersionHost: SoftwareVersion /// A user-facing name for the feature, which is used in the beginning of a phrase like ` requires macOS xx.xx...`. public var name: String /// Additional information displayed when the feature is not supported by the current configuration. public var detail: String? /// `true` if this feature is not supported due to the guest's platform (ex: a feature that only works on Mac and not Linux). public var unsupportedPlatform: Bool public init(id: String, minVersionGuest: SoftwareVersion, minVersionHost: SoftwareVersion, name: String, detail: String? = nil, unsupportedPlatform: Bool = false) { self.id = id self.minVersionGuest = minVersionGuest self.minVersionHost = minVersionHost self.name = name self.detail = detail self.unsupportedPlatform = unsupportedPlatform } public enum CodingKeys: String, CodingKey { case id, minVersionGuest, minVersionHost, name, detail, unsupportedPlatform } } /// Defines an image that can be referenced by other items in the catalog. /// Currently used to represent macOS release groups by the corresponding default wallpaper image. public struct CatalogGraphic: CatalogModel { public struct Thumbnail: Hashable, Codable, Sendable { public var url: URL public var width: Int public var height: Int public var blurHash: String public init(url: URL, width: Int, height: Int, blurHash: String) { self.url = url self.width = width self.height = height self.blurHash = blurHash } } /// Identifies the graphic, used to reference a graphic from another catalog model. public var id: String /// URL to the graphic image file, in its highest resolution. public var url: URL /// Thumbnail representation of the image, with metadata and blur hash. public var thumbnail: Thumbnail public init(id: String, url: URL, thumbnail: Thumbnail) { self.id = id self.url = url self.thumbnail = thumbnail } } /// Defines a grouping of software images by major OS version. /// This can be used to group releases like `macOS Sonoma`, `macOS Sequoia`, etc, /// making it easier for users to find the desired OS version. public struct CatalogGroup: CatalogModel { /// Identifies the group, used to reference a group from a software image definition. public var id: String /// A user-facing name for the release group. public var name: String /// The major OS version for releases in this group. public var majorVersion: SoftwareVersion /// The image that can be used to represent this group. public var image: CatalogGraphic /// The image that can be used to represent this group when dark mode is enabled. public var darkImage: CatalogGraphic? public init(id: String, name: String, majorVersion: SoftwareVersion, image: CatalogGraphic, darkImage: CatalogGraphic?) { self.id = id self.name = name self.majorVersion = majorVersion self.image = image self.darkImage = darkImage } } /// Defines a release channel such as `Beta` or `Release`. /// Can be used to allow filtering for specific release types. public struct CatalogChannel: CatalogModel { /// Identifies the channel, used to reference a channel from a software image definition. public var id: String /// User-facing name for the channel. public var name: String /// User-facing note describing the contents in this channel. public var note: String /// SF Symbol name for icon that can be used to represent this channel. public var icon: String public init(id: String, name: String, note: String, icon: String) { self.id = id self.name = name self.note = note self.icon = icon } } /// Describes a "device support files" installation and the instructions that should be presented to the user when it's required. public struct CatalogDeviceSupportVersion: CatalogModel { public var id: String /// Hint for which MobileDevice version this entry refers to. public var mobileDeviceMinVersion: SoftwareVersion /// OS version this entry refers to. Matching will be attempted by `major.minor` first, then `major` only. public var osVersion: SoftwareVersion /// User-facing title displayed on the list of software images. public var title: String /// User-facing instructions displayed in interstitial or when user clicks the warning. May contain markdown. public var instructions: String } /// Adopted by both ``RestoreImage`` and ``ResolvedRestoreImage`` to make download lookup more convenient to implement. public protocol DownloadableCatalogContent: Identifiable, Hashable, Sendable { var build: String { get } var url: URL { get } } /// Defines an individual macOS restore image in the catalog. public struct RestoreImage: CatalogModel, DownloadableCatalogContent { /// Unique identifier for this restore image. public var id: String /// Identifier of the ``CatalogGroup`` this restore image is a part of. public var group: CatalogGroup.ID /// Identifier of the ``CatalogChannel`` this restore image is a part of. public var channel: CatalogChannel.ID /// Identifier of the ``RequirementSet`` describing the requirements for this image to be installed/run. public var requirements: RequirementSet.ID /// User-facing name for this restore image, usually in a form like `macOS 15.0 Developer Beta 4`. public var name: String /// OS build this restore image provides. public var build: String /// OS version this restore image provides. public var version: SoftwareVersion /// The minimum version of the MobileDevice framework required to install this guest. public var mobileDeviceMinVersion: SoftwareVersion /// URL to the IPSW file for this restore image. public var url: URL /// The size of the download in bytes. public var downloadSize: UInt64? public init(id: String, group: CatalogGroup.ID, channel: CatalogChannel.ID, requirements: RequirementSet.ID, name: String, build: String, version: SoftwareVersion, mobileDeviceMinVersion: SoftwareVersion, url: URL, downloadSize: UInt64?) { self.id = id self.group = group self.channel = channel self.requirements = requirements self.name = name self.build = build self.version = version self.mobileDeviceMinVersion = mobileDeviceMinVersion self.url = url self.downloadSize = downloadSize } } /// This is the root data structure for the VirtualBuddy restore image catalog. public struct SoftwareCatalog: Codable, Sendable { /// The API version implemented by this software catalog. public var apiVersion: Int /// The minimum version of the app that can read this catalog. /// The app should reject catalogs with a higher `minAppVersion` and /// direct users to update the app in order to use the catalog. public var minAppVersion: SoftwareVersion /// Channel definitions. public var channels: [CatalogChannel] /// Release group definitions. public var groups: [CatalogGroup] /// Restore image definitions. public var restoreImages: [RestoreImage] /// Feature definitions. public var features: [VirtualizationFeature] /// Requirement set definitions. public var requirementSets: [RequirementSet] /// Device support files definitions. public var deviceSupportVersions: [CatalogDeviceSupportVersion] public init(apiVersion: Int, minAppVersion: SoftwareVersion, channels: [CatalogChannel], groups: [CatalogGroup], restoreImages: [RestoreImage], features: [VirtualizationFeature], requirementSets: [RequirementSet], deviceSupportVersions: [CatalogDeviceSupportVersion]) { self.apiVersion = apiVersion self.minAppVersion = minAppVersion self.channels = channels self.groups = groups self.restoreImages = restoreImages self.features = features self.requirementSets = requirementSets self.deviceSupportVersions = deviceSupportVersions } public static let empty = SoftwareCatalog(apiVersion: 0, minAppVersion: .empty, channels: [], groups: [], restoreImages: [], features: [], requirementSets: [], deviceSupportVersions: []) } public extension SoftwareCatalog { private static let decoder = JSONDecoder() private static let encoder: JSONEncoder = { let e = JSONEncoder() e.outputFormatting = [.prettyPrinted, .sortedKeys] return e }() init(data: Data) throws { self = try Self.decoder.decode(Self.self, from: data) } init(contentsOf url: URL) throws { let data = try Data(contentsOf: url) try self.init(data: data) } func write(to url: URL) throws { try Self.encoder.encode(self).write(to: url) } } public extension CatalogGraphic { static let placeholder = CatalogGraphic( id: "placeholder", url: URL(string: "https://example.com")!, thumbnail: Thumbnail(url: URL(string: "https://example.com")!, width: 640, height: 360, blurHash: "XXX") ) } // MARK: - Custom Codable Conformances public extension VirtualizationFeature { init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(String.self, forKey: .id) self.minVersionGuest = try container.decode(SoftwareVersion.self, forKey: .minVersionGuest) self.minVersionHost = try container.decode(SoftwareVersion.self, forKey: .minVersionHost) self.name = try container.decode(String.self, forKey: .name) self.detail = try container.decodeIfPresent(String.self, forKey: .detail) self.unsupportedPlatform = (try? container.decodeIfPresent(Bool.self, forKey: .unsupportedPlatform)) ?? false } } ================================================ FILE: VirtualCore/Source/VirtualCatalog/Utilities/BlurHashEncode.swift ================================================ #if canImport(AppKit) import AppKit public extension NSImage { func draw(at point: CGPoint) { draw(in: CGRect(origin: point, size: size)) } func blurHash(numberOfComponents components: (Int, Int)) -> String? { let scale: CGFloat = 1.0 let pixelWidth = Int(round(size.width * scale)) let pixelHeight = Int(round(size.height * scale)) let context = CGContext( data: nil, width: pixelWidth, height: pixelHeight, bitsPerComponent: 8, bytesPerRow: pixelWidth * 4, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue )! context.scaleBy(x: scale, y: -scale) context.translateBy(x: 0, y: -size.height) NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: true) context.saveGState() draw(at: .zero) context.restoreGState() guard let cgImage = context.makeImage(), let dataProvider = cgImage.dataProvider, let data = dataProvider.data, let pixels = CFDataGetBytePtr(data) else { assertionFailure("Unexpected error!") return nil } let width = cgImage.width let height = cgImage.height let bytesPerRow = cgImage.bytesPerRow var factors: [(Float, Float, Float)] = [] for y in 0 ..< components.1 { for x in 0 ..< components.0 { let normalisation: Float = (x == 0 && y == 0) ? 1 : 2 let factor = multiplyBasisFunction(pixels: pixels, width: width, height: height, bytesPerRow: bytesPerRow, bytesPerPixel: cgImage.bitsPerPixel / 8, pixelOffset: 0) { normalisation * cos(Float.pi * Float(x) * $0 / Float(width)) as Float * cos(Float.pi * Float(y) * $1 / Float(height)) as Float } factors.append(factor) } } let dc = factors.first! let ac = factors.dropFirst() var hash = "" let sizeFlag = (components.0 - 1) + (components.1 - 1) * 9 hash += sizeFlag.encode83(length: 1) let maximumValue: Float if ac.count > 0 { let actualMaximumValue = ac.map({ max(abs($0.0), abs($0.1), abs($0.2)) }).max()! let quantisedMaximumValue = Int(max(0, min(82, floor(actualMaximumValue * 166 - 0.5)))) maximumValue = Float(quantisedMaximumValue + 1) / 166 hash += quantisedMaximumValue.encode83(length: 1) } else { maximumValue = 1 hash += 0.encode83(length: 1) } hash += encodeDC(dc).encode83(length: 4) for factor in ac { hash += encodeAC(factor, maximumValue: maximumValue).encode83(length: 2) } return hash } private func multiplyBasisFunction(pixels: UnsafePointer, width: Int, height: Int, bytesPerRow: Int, bytesPerPixel: Int, pixelOffset: Int, basisFunction: (Float, Float) -> Float) -> (Float, Float, Float) { var r: Float = 0 var g: Float = 0 var b: Float = 0 let buffer = UnsafeBufferPointer(start: pixels, count: height * bytesPerRow) for x in 0 ..< width { for y in 0 ..< height { let basis = basisFunction(Float(x), Float(y)) r += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 0 + y * bytesPerRow]) g += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 1 + y * bytesPerRow]) b += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 2 + y * bytesPerRow]) } } let scale = 1 / Float(width * height) return (r * scale, g * scale, b * scale) } } private func encodeDC(_ value: (Float, Float, Float)) -> Int { let roundedR = linearTosRGB(value.0) let roundedG = linearTosRGB(value.1) let roundedB = linearTosRGB(value.2) return (roundedR << 16) + (roundedG << 8) + roundedB } private func encodeAC(_ value: (Float, Float, Float), maximumValue: Float) -> Int { let quantR = Int(max(0, min(18, floor(signPow(value.0 / maximumValue, 0.5) * 9 + 9.5)))) let quantG = Int(max(0, min(18, floor(signPow(value.1 / maximumValue, 0.5) * 9 + 9.5)))) let quantB = Int(max(0, min(18, floor(signPow(value.2 / maximumValue, 0.5) * 9 + 9.5)))) return quantR * 19 * 19 + quantG * 19 + quantB } private func signPow(_ value: Float, _ exp: Float) -> Float { return copysign(pow(abs(value), exp), value) } private func linearTosRGB(_ value: Float) -> Int { let v = max(0, min(1, value)) if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } } private func sRGBToLinear(_ value: Type) -> Float { let v = Float(Int64(value)) / 255 if v <= 0.04045 { return v / 12.92 } else { return pow((v + 0.055) / 1.055, 2.4) } } private let encodeCharacters: [String] = { return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } }() extension BinaryInteger { func encode83(length: Int) -> String { var result = "" for i in 1 ... length { let digit = (Int(self) / pow(83, length - i)) % 83 result += encodeCharacters[Int(digit)] } return result } } private func pow(_ base: Int, _ exponent: Int) -> Int { return (0 ..< exponent).reduce(1) { value, _ in value * base } } #endif ================================================ FILE: VirtualCore/Source/VirtualCatalog/Utilities/String+AppleOSBuild.swift ================================================ import Foundation import BuddyFoundation extension String { static let appleOSBuildRegex = /[0-9]{2}[A-Z][0-9]{2,}[a-z]?/ static let appleOSVersionRegex = /[0-9]+(?:\.[0-9]+){1,2}/ /// Returns the first regex match for an Apple OS build number (ex: `23A5276f`). func matchAppleOSBuild() -> String? { (try? Self.appleOSBuildRegex.firstMatch(in: self)?.output).flatMap { String($0) } } /// Returns the first regex match for an Apple OS version (ex: `15.5` or `15.5.1`). func matchAppleOSVersion() -> SoftwareVersion? { (try? Self.appleOSVersionRegex.firstMatch(in: self)?.output) .flatMap { SoftwareVersion(string: String($0)) } } } ================================================ FILE: VirtualCore/Source/VirtualCatalog/Utilities/URL+ExtendedAttributes.swift ================================================ import Foundation import BuddyFoundation extension URL { func vb_encodeExtendedAttribute(_ value: T, forKey key: String) throws { let data = try JSONEncoder.vb_extendedAttribute.encode(value) try vb_setExtendedAttributeData(data, forKey: key) } func vb_decodeExtendedAttribute(forKey key: String) -> T? { try? vb_extendedAttributeData(forKey: key).flatMap { try JSONDecoder.vb_extendedAttribute.decode(T.self, from: $0) } } func vb_setExtendedAttributeData(_ value: Data, forKey key: String, base64: Bool = true) throws { let effectiveValue = base64 ? value.base64EncodedData() : value let size = effectiveValue.count let err = effectiveValue.withUnsafeBytes { ptr in setxattr(path, key, ptr.baseAddress, size, 0, 0) } guard err == 0 else { throw "setxattr error code \(err)" } } func vb_extendedAttributeData(forKey key: String, base64: Bool = true) -> Data? { var size = getxattr(path, key, nil, .max, 0, 0) guard size > 0 else { return nil } let pointer = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: 2) size = getxattr(path, key, pointer, size, 0, 0) guard size > 0 else { pointer.deallocate() return nil } let data = Data(bytesNoCopy: pointer, count: size, deallocator: .free) guard base64 else { return data } return Data(base64Encoded: data) } func vb_removeExtendedAttribute(forKey key: String) throws { let err = removexattr(path, key, 0) guard err == 0 else { throw "removexattr error code \(err)" } } } private extension JSONEncoder { static let vb_extendedAttribute = JSONEncoder() } private extension JSONDecoder { static let vb_extendedAttribute = JSONDecoder() } ================================================ FILE: VirtualCore/Source/Virtualization/Helpers/CatalogExtensions.swift ================================================ // // CatalogExtensions.swift // VirtualBuddy // // Created by Guilherme Rambo on 02/08/24. // import Foundation @MainActor public extension SoftwareCatalog { /// The most up-to-date software catalog available for Mac releases. /// This is updated when the API client fetches a new catalog from the server. private(set) static var currentMacCatalog: SoftwareCatalog = { do { return try VBAPIClient.fetchBuiltInCatalog(for: .mac) } catch { assertionFailure("Built-in catalog load failed: \(error)") return SoftwareCatalog.empty } }() /// The most up-to-date software catalog available for Linux releases. /// This is updated when the API client fetches a new catalog from the server. private(set) static var currentLinuxCatalog: SoftwareCatalog = { do { return try VBAPIClient.fetchBuiltInCatalog(for: .linux) } catch { assertionFailure("Built-in catalog load failed: \(error)") return SoftwareCatalog.empty } }() static func current(for guestType: VBGuestType) -> SoftwareCatalog { switch guestType { case .mac: return .currentMacCatalog case .linux: return .currentLinuxCatalog } } static func setCurrent(_ catalog: SoftwareCatalog, for guestType: VBGuestType) { switch guestType { case .mac: self.currentMacCatalog = catalog case .linux: self.currentLinuxCatalog = catalog } } } public extension CatalogGuestPlatform { init(_ guestType: VBGuestType) { switch guestType { case .mac: self = .mac case .linux: self = .linux } } } public extension CatalogResolutionEnvironment { func guestType(_ guestType: VBGuestType) -> Self { guest(platform: CatalogGuestPlatform(guestType)) } } public extension VBVirtualMachine { @MainActor func resolveCatalogImage(_ image: RestoreImage, catalog: SoftwareCatalog? = nil) throws -> ResolvedRestoreImage { try configuration.resolveCatalogImage(image, catalog: catalog) } } public extension VBMacConfiguration { @MainActor func resolveCatalogImage(_ image: RestoreImage, catalog: SoftwareCatalog? = nil) throws -> ResolvedRestoreImage { let effectiveCatalog = catalog ?? SoftwareCatalog.current(for: systemType) return try ResolvedRestoreImage( environment: .current.guestType(systemType), catalog: effectiveCatalog, image: image ) } } ================================================ FILE: VirtualCore/Source/Virtualization/Helpers/DirectoryObserver.swift ================================================ import Foundation import Combine import OSLog final class DirectoryObserver: NSObject, NSFilePresenter { private let logger: Logger var presentedItemURL: URL? var presentedItemOperationQueue: OperationQueue = .main let signal: PassthroughSubject let fileExtensions: Set init(presentedItemURL: URL?, fileExtensions: Set, label: String, signal: PassthroughSubject) { self.logger = Logger(for: DirectoryObserver.self, label: label) self.presentedItemURL = presentedItemURL self.fileExtensions = fileExtensions self.signal = signal super.init() NSFileCoordinator.addFilePresenter(self) } private func sendSignalIfNeeded(for url: URL) { guard fileExtensions.contains(url.pathExtension) else { return } signal.send(url) } func presentedSubitemDidAppear(at url: URL) { logger.debug("Added: \(url.path)") sendSignalIfNeeded(for: url) } func presentedSubitemDidChange(at url: URL) { sendSignalIfNeeded(for: url) } func presentedSubitem(at oldURL: URL, didMoveTo newURL: URL) { logger.debug("Moved: \(oldURL.path) -> \(newURL.path)") sendSignalIfNeeded(for: newURL) } func accommodatePresentedSubitemDeletion(at url: URL) async throws { logger.debug("Deleted: \(url.path)") sendSignalIfNeeded(for: url) } } ================================================ FILE: VirtualCore/Source/Virtualization/Helpers/DiskImageGenerator.swift ================================================ // // DiskImageGenerator.swift // VirtualCore // // Created by Guilherme Rambo on 19/07/22. // import Foundation fileprivate extension VBManagedDiskImage.Format { var hdiutilType: String { switch self { case .raw: assertionFailure(".raw not supported with hdiutil") return "UDIF" case .dmg: return "UDIF" case .sparse: return "SPARSE" case .asif: return "ASIF" } } } public final class DiskImageGenerator { public struct ImageSettings { public var url: URL public var template: VBManagedDiskImage public init(for image: VBManagedDiskImage, in vm: VBVirtualMachine) { self.url = vm.diskImageURL(for: image) self.template = image } } public static func generateImage(with settings: ImageSettings) async throws { guard settings.template.format.isSupported else { throw "Unsupported disk image format \(settings.template.format.hdiutilType.quoted)." } switch settings.template.format { case .raw: try generateRaw(with: settings) case .dmg, .sparse: try await generateDMG(with: settings) case .asif: try await generateBlankASIF(with: settings) } } private static func generateRaw(with settings: ImageSettings) throws { let diskFd = open(settings.url.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR) if diskFd == -1 { throw Failure("Cannot create disk image.") } var result = ftruncate(diskFd, off_t(settings.template.size)) if result != 0 { throw Failure("ftruncate() failed.") } result = close(diskFd) if result != 0 { throw Failure("Failed to close the disk image.") } } private static func generateDMG(with settings: ImageSettings) async throws { try await hdiutil(arguments: [ "create", "-layout", "GPTSPUD", "-type", settings.template.format.hdiutilType, "-megabytes", "\(settings.template.size / .storageMegabyte)", "-fs", "APFS", "-volname", settings.template.filename, "-nospotlight", settings.url.path ]) } private static func generateBlankASIF(with settings: ImageSettings) async throws { try await diskutil(arguments: [ "image", "create", "blank", "--fs", "none", "--format", settings.template.format.hdiutilType, "--size", "\(settings.template.size / .storageGigabyte)G", settings.url.path ]) } private static func hdiutil(arguments: [String]) async throws { try await runCommand("/usr/bin/hdiutil", with: arguments) } private static func diskutil(arguments: [String]) async throws { try await runCommand("/usr/sbin/diskutil", with: arguments) } private static func runCommand(_ path: String, with arguments: [String]) async throws { let process = Process() process.executableURL = URL(fileURLWithPath: path) process.arguments = arguments #if DEBUG print("💻 \(path) arguments: \(process.arguments!.joined(separator: " "))") #endif let err = Pipe() let out = Pipe() process.standardError = err process.standardOutput = out try process.run() var error = "" for try await line in err.fileHandleForReading.bytes.lines { error.append("\(line)\n") } process.waitUntilExit() guard process.terminationStatus != 0 else { return } if error.trimmingCharacters(in: .newlines).count > 0 { throw Failure(error) } else { throw Failure("Command \(path) failed with exit code \(process.terminationStatus)") } } } ================================================ FILE: VirtualCore/Source/Virtualization/Helpers/LinuxVirtualMachineConfigurationHelper.swift ================================================ /* See LICENSE folder for this sample’s licensing information. Abstract: Helper that creates various configuration objects exposed in the `VZVirtualMachineConfiguration`. */ import Foundation import Virtualization @available(macOS 13.0, *) struct LinuxVirtualMachineConfigurationHelper: VirtualMachineConfigurationHelper { let vm: VBVirtualMachine let savedState: VBSavedStatePackage? init(vm: VBVirtualMachine) { self.vm = vm self.savedState = nil } func createInstallDevice(installImageURL: URL) throws -> VZStorageDeviceConfiguration { let attachment = try VZDiskImageStorageDeviceAttachment(url: installImageURL, readOnly: true, cachingMode: .cached, synchronizationMode: .fsync) let usbDeviceConfiguration = VZUSBMassStorageDeviceConfiguration(attachment: attachment) return usbDeviceConfiguration } func createBootLoader() throws -> VZBootLoader { let efi = VZEFIBootLoader() let storeURL = vm.metadataDirectoryURL.appendingPathComponent("nvram") if FileManager.default.fileExists(atPath: storeURL.path) { efi.variableStore = VZEFIVariableStore(url: storeURL) } else { efi.variableStore = try VZEFIVariableStore(creatingVariableStoreAt: storeURL, options: []) } return efi } func createGraphicsDevices() -> [VZGraphicsDeviceConfiguration] { let graphicsConfiguration = VZVirtioGraphicsDeviceConfiguration() graphicsConfiguration.scanouts = vm.configuration.hardware.displayDevices.map(\.vzScanout) return [graphicsConfiguration] } @available(macOS 13.0, *) func createSpiceAgentConsoleDeviceConfiguration() -> VZVirtioConsoleDeviceConfiguration? { let consoleDevice = VZVirtioConsoleDeviceConfiguration() let spiceAgentPort = VZVirtioConsolePortConfiguration() spiceAgentPort.name = VZSpiceAgentPortAttachment.spiceAgentPortName spiceAgentPort.attachment = VZSpiceAgentPortAttachment() consoleDevice.ports[0] = spiceAgentPort return consoleDevice } } // MARK: - Configuration Models -> Virtualization @available(macOS 13.0, *) extension VBDisplayDevice { var vzScanout: VZVirtioGraphicsScanoutConfiguration { VZVirtioGraphicsScanoutConfiguration(widthInPixels: width, heightInPixels: height) } } ================================================ FILE: VirtualCore/Source/Virtualization/Helpers/MacOSVirtualMachineConfigurationHelper.swift ================================================ /* See LICENSE folder for this sample’s licensing information. Abstract: Helper that creates various configuration objects exposed in the `VZVirtualMachineConfiguration`. */ import Foundation import Virtualization struct MacOSVirtualMachineConfigurationHelper: VirtualMachineConfigurationHelper { let vm: VBVirtualMachine let savedState: VBSavedStatePackage? func createInstallDevice(installImageURL: URL) throws -> VZStorageDeviceConfiguration { fatalError() } func createBootLoader() -> VZBootLoader { return VZMacOSBootLoader() } func createGraphicsDevices() -> [VZGraphicsDeviceConfiguration] { let graphicsConfiguration = VZMacGraphicsDeviceConfiguration() graphicsConfiguration.displays = vm.configuration.hardware.displayDevices.map(\.vzDisplay) return [graphicsConfiguration] } func createAdditionalBlockDevices() async throws -> [VZVirtioBlockDeviceConfiguration] { var devices = try storageDeviceContainer.additionalBlockDevices(guestType: vm.configuration.systemType) if vm.configuration.guestAdditionsEnabled, let disk = try? VZVirtioBlockDeviceConfiguration.guestAdditionsDisk { devices.append(disk) } return devices } func createKeyboardConfiguration() -> VZKeyboardConfiguration { if #available(macOS 14.0, *) { switch vm.configuration.hardware.keyboardDevice.kind { case .generic: return VZUSBKeyboardConfiguration() case .mac: return VZMacKeyboardConfiguration() } } else { return VZUSBKeyboardConfiguration() } } func createEntropyDevices() -> [VZEntropyDeviceConfiguration] { [VZVirtioEntropyDeviceConfiguration()] } @available(macOS 15.0, *) func createUSBControllers() -> [VZUSBControllerConfiguration] { let xhci = VZXHCIControllerConfiguration() return [xhci] } } // MARK: - Configuration Models -> Virtualization extension VBDisplayDevice { var vzDisplay: VZMacGraphicsDisplayConfiguration { VZMacGraphicsDisplayConfiguration(widthInPixels: width, heightInPixels: height, pixelsPerInch: pixelsPerInch) } } ================================================ FILE: VirtualCore/Source/Virtualization/Helpers/RandomNameGenerator.swift ================================================ // // RandomNameGenerator.swift // VirtualCore // // Created by Guilherme Rambo on 19/07/22. // import Foundation public final class RandomNameGenerator { public static let shared = RandomNameGenerator() private var adjectives = [String]() private var animals = [String]() private init() { guard let animalsData = NSDataAsset(name: "Animals", bundle: .virtualCore)?.data, let adjectivesData = NSDataAsset(name: "Adjectives", bundle: .virtualCore)?.data else { assertionFailure("Couldn't load random name generator asssets") return } adjectives = String(decoding: adjectivesData, as: UTF8.self).components(separatedBy: .newlines) animals = String(decoding: animalsData, as: UTF8.self).components(separatedBy: .newlines) } public func newName() -> String { guard let adjective = adjectives.randomElement(), let animal = animals.randomElement() else { return UUID().uuidString } return adjective + " " + animal } } ================================================ FILE: VirtualCore/Source/Virtualization/Helpers/VBDebugUtil.h ================================================ #if DEBUG #import @class VZVirtualMachine; NS_ASSUME_NONNULL_BEGIN /// This is used to stop VirtualBuddy in the debugger at specific points in a VM's lifecycle, /// so that exploring Virtualization internals can be done in Objective-C. @interface VBDebugUtil : NSObject + (void)debugVirtualMachineBeforeStart:(VZVirtualMachine *_Nonnull)vm; + (void)debugVirtualMachineAfterStart:(VZVirtualMachine *_Nonnull)vm; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: VirtualCore/Source/Virtualization/Helpers/VBDebugUtil.m ================================================ #if DEBUG #import "VBDebugUtil.h" @import Virtualization; #import @implementation VBDebugUtil /// Runs in debug builds when VM is about to be started. + (void)debugVirtualMachineBeforeStart:(VZVirtualMachine *_Nonnull)vm { NSLog(@"Debug virtual machine before start: %@", vm); return; } /// Runs in debug builds right after the VM has been started. + (void)debugVirtualMachineAfterStart:(VZVirtualMachine *_Nonnull)vm { NSLog(@"Debug virtual machine after start: %@", vm); __weak typeof(vm) weakVM = vm; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (!weakVM) return; [self debugVirtualMachineAfterStartWithDelay:weakVM]; }); } /// Runs in debug builds several seconds after the VM has been started (likely after it's finished booting up). + (void)debugVirtualMachineAfterStartWithDelay:(VZVirtualMachine *_Nonnull)vm { NSLog(@"Debug virtual machine after start with delay: %@", vm); return; } @end #endif ================================================ FILE: VirtualCore/Source/Virtualization/Helpers/VZVirtualMachineConfiguration+NVRAM.swift ================================================ // // VZVirtualMachineConfiguration+NVRAM.swift // VirtualCore // // Created by Guilherme Rambo on 11/04/22. // import Foundation import Virtualization public struct Failure: LocalizedError { public var errorDescription: String? public init(_ msg: String) { self.errorDescription = msg } } public extension VZMacAuxiliaryStorage { func fetchNVRAMVariables() throws -> [VBNVRAMVariable] { var error: NSError? let variables = _allNVRAMVariablesWithError(&error) if let error = error { throw error } let vars = variables.map { VBNVRAMVariable(name: $0.key, value: $0.value as? String) } return vars } func updateNVRAM(_ variable: VBNVRAMVariable) throws { if let value = variable.value { try _setValue(value, forNVRAMVariableNamed: variable.name) } else { try _removeNVRAMVariableNamed(variable.name) } } } ================================================ FILE: VirtualCore/Source/Virtualization/Helpers/VirtualMachineConfigurationHelper.swift ================================================ /* See LICENSE folder for this sample’s licensing information. Abstract: Helper that creates various configuration objects exposed in the `VZVirtualMachineConfiguration`. */ import Foundation import Virtualization import BuddyFoundation protocol VirtualMachineConfigurationHelper { var vm: VBVirtualMachine { get } var savedState: VBSavedStatePackage? { get } func createInstallDevice(installImageURL: URL) throws -> VZStorageDeviceConfiguration func createBootLoader() throws -> VZBootLoader func createBootBlockDevice() async throws -> VZVirtioBlockDeviceConfiguration func createAdditionalBlockDevices() async throws -> [VZVirtioBlockDeviceConfiguration] func createKeyboardConfiguration() -> VZKeyboardConfiguration func createGraphicsDevices() -> [VZGraphicsDeviceConfiguration] func createEntropyDevices() -> [VZVirtioEntropyDeviceConfiguration] @available(macOS 13.0, *) func createSpiceAgentConsoleDeviceConfiguration() -> VZVirtioConsoleDeviceConfiguration? @available(macOS 15.0, *) func createUSBControllers() -> [VZUSBControllerConfiguration] } func createVZDiskImageStorageDeviceAttachment(url: URL, readOnly: Bool, guestType: VBGuestType) throws -> VZDiskImageStorageDeviceAttachment { if guestType == .linux { // Linux guest is bound to cause IO errors. // Referring to https://github.com/utmapp/UTM/issues/4840, seems like setting the cachingMode to cached // fixes this IO errors and disk corruption issues for Linux guest. return try VZDiskImageStorageDeviceAttachment(url: url, readOnly: readOnly, cachingMode: .cached, synchronizationMode: .fsync) } else { return try VZDiskImageStorageDeviceAttachment(url: url, readOnly: readOnly) } } extension VirtualMachineConfigurationHelper { var storageDeviceContainer: VBStorageDeviceContainer { savedState ?? vm } func createBootBlockDevice() async throws -> VZVirtioBlockDeviceConfiguration { do { let bootDevice = try storageDeviceContainer.bootDevice let bootDiskImage = try storageDeviceContainer.bootDiskImage if !bootDevice.diskImageExists(for: storageDeviceContainer) { guard storageDeviceContainer.allowDiskImageCreation else { throw Failure("Boot disk image does not exist.") } let settings = DiskImageGenerator.ImageSettings(for: bootDiskImage, in: vm) try await DiskImageGenerator.generateImage(with: settings) } let bootURL = storageDeviceContainer.diskImageURL(for: bootDiskImage) let diskImageAttachment = try createVZDiskImageStorageDeviceAttachment(url: bootURL, readOnly: false, guestType: vm.configuration.systemType) let disk = VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment) return disk } catch { throw Failure("Failed to instantiate a disk image for the VM: \(error.localizedDescription).") } } func createAdditionalBlockDevices() async throws -> [VZVirtioBlockDeviceConfiguration] { try storageDeviceContainer.additionalBlockDevices(guestType: vm.configuration.systemType) } func createKeyboardConfiguration() -> VZKeyboardConfiguration { VZUSBKeyboardConfiguration() } func createEntropyDevices() -> [VZVirtioEntropyDeviceConfiguration] { [] } @available(macOS 13.0, *) func createSpiceAgentConsoleDeviceConfiguration() -> VZVirtioConsoleDeviceConfiguration? { nil } @available(macOS 15.0, *) func createUSBControllers() -> [VZUSBControllerConfiguration] { [] } } extension VBStorageDeviceContainer { func additionalBlockDevices(guestType: VBGuestType) throws -> [VZVirtioBlockDeviceConfiguration] { var output = [VZVirtioBlockDeviceConfiguration]() for device in storageDevices { guard device.isEnabled, !device.isBootVolume else { continue } let url = diskImageURL(for: device) let attachment = try createVZDiskImageStorageDeviceAttachment(url: url, readOnly: device.isReadOnly, guestType: guestType) output.append(VZVirtioBlockDeviceConfiguration(attachment: attachment)) } return output } } extension VBMacConfiguration { var vzNetworkDevices: [VZNetworkDeviceConfiguration] { get throws { try hardware.networkDevices.map { try $0.vzConfiguration } } } var vzAudioDevices: [VZAudioDeviceConfiguration] { hardware.soundDevices.map(\.vzConfiguration) } var vzPointingDevices: [VZPointingDeviceConfiguration] { get throws { try hardware.pointingDevice.vzConfigurations } } } extension VBNetworkDevice { var vzConfiguration: VZNetworkDeviceConfiguration { get throws { let config = VZVirtioNetworkDeviceConfiguration() guard let addr = VZMACAddress(string: macAddress) else { throw Failure("Invalid MAC address") } config.macAddress = addr config.attachment = try vzAttachment return config } } private var vzAttachment: VZNetworkDeviceAttachment { get throws { switch kind { case .NAT: return VZNATNetworkDeviceAttachment() case .bridge: let interface = try resolveBridge(with: id) return VZBridgedNetworkDeviceAttachment(interface: interface) } } } private func resolveBridge(with identifier: String) throws -> VZBridgedNetworkInterface { guard identifier != VBNetworkDeviceInterface.automatic.id else { return try VZBridgedNetworkInterface.networkInterfaces.first.require("There are no network interfaces available on the host for bridging.") } return try VZBridgedNetworkInterface.networkInterfaces.first(where: { $0.identifier == identifier }) .require("The bridged network interface \(identifier.quoted) is not available.") } } extension VBPointingDevice { var vzConfigurations: [VZPointingDeviceConfiguration] { get throws { switch kind { case .mouse: return [VZUSBScreenCoordinatePointingDeviceConfiguration()] case .trackpad: return [ VZMacTrackpadConfiguration(), VZUSBScreenCoordinatePointingDeviceConfiguration() ] } } } } extension VBSoundDevice { var vzConfiguration: VZAudioDeviceConfiguration { let audioConfiguration = VZVirtioSoundDeviceConfiguration() if enableInput { let inputStream = VZVirtioSoundDeviceInputStreamConfiguration() inputStream.source = VZHostAudioInputStreamSource() audioConfiguration.streams.append(inputStream) } if enableOutput { let outputStream = VZVirtioSoundDeviceOutputStreamConfiguration() outputStream.sink = VZHostAudioOutputStreamSink() audioConfiguration.streams.append(outputStream) } return audioConfiguration } } extension VBMacConfiguration { var vzSharedFoldersFileSystemDevices: [VZDirectorySharingDeviceConfiguration] { get throws { var directories: [String: VZSharedDirectory] = [:] for folder in sharedFolders { guard let dir = folder.vzSharedFolder else { continue } directories[folder.effectiveMountPointName] = dir } var devices: [VZDirectorySharingDeviceConfiguration] = [] // standard directory share try VZVirtioFileSystemDeviceConfiguration.validateTag(VBSharedFolder.virtualBuddyShareName) do { let share = VZMultipleDirectoryShare(directories: directories) let device = VZVirtioFileSystemDeviceConfiguration(tag: VBSharedFolder.virtualBuddyShareName) device.share = share devices.append(device) } if self.systemType == .linux && self.rosettaSharingEnabled { // Rosetta directory share for Linux VMs try VZVirtioFileSystemDeviceConfiguration.validateTag(VBSharedFolder.rosettaShareName) let share = try VZLinuxRosettaDirectoryShare() let device = VZVirtioFileSystemDeviceConfiguration(tag: VBSharedFolder.rosettaShareName) device.share = share devices.append(device) } return devices } } } extension VBSharedFolder { var vzSharedFolder: VZSharedDirectory? { guard isAvailable, isEnabled else { return nil } return VZSharedDirectory(url: url, readOnly: isReadOnly) } } ================================================ FILE: VirtualCore/Source/Virtualization/Screenshot/NSImage+DRMProtected.swift ================================================ import Cocoa import BuddyKit import Vision @available(macOS 15.0, *) extension NSImage { /// This is kinda silly. /// /// All this does is it attempts to detect the string "DRM Protected" inside of a thumbnail image. /// This is to work around a bug that happened in macOS 26 where screenshots used for thumbnails /// would display a "DRM Protected Video" message with a black background. /// /// Some users ended up with these bad thumbnails in their library and with the new background hash /// thumbnails I didn't want them to get a black background with a white blurry blob as their VM thumbnail. /// /// See: https://github.com/insidegui/VirtualBuddy/discussions/533 func detectDRMProtectedVideoBug() async -> Bool { guard let cgImage else { return false } var request = RecognizeTextRequest() request.automaticallyDetectsLanguage = false request.recognitionLanguages = [.init(components: .init(languageCode: .english, script: nil, region: nil))] request.recognitionLevel = .fast request.minimumTextHeightFraction = 0.05 guard let observations = try? await request.perform(on: cgImage) else { return false } return observations.contains(where: { $0.text.localizedCaseInsensitiveContains("DRM Protected") }) } } @available(macOS 15.0, *) private extension RecognizedTextObservation { var text: String { topCandidates(1).first?.string ?? "" } } ================================================ FILE: VirtualCore/Source/Virtualization/Screenshot/NSImage+HEIC.swift ================================================ import Cocoa import struct AVFoundation.AVFileType public extension NSImage { static let defaultThumbnailProperties = [ kCGImageDestinationLossyCompressionQuality: 0.9, kCGImageDestinationImageMaxPixelSize: 1024 ] as CFDictionary static let defaultHEICProperties = [ kCGImageDestinationLossyCompressionQuality: 1, kCGImageDestinationImageMaxPixelSize: 4096 ] as CFDictionary // TODO: Adopt BuddyImageKit @discardableResult func vb_createThumbnail(at url: URL, options: CFDictionary = NSImage.defaultThumbnailProperties) throws -> NSImage { guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { throw Failure("Failed to create a CGImage") } try cgImage.vb_encodeHEIC(to: url, options: options) guard let thumbnailImage = NSImage(contentsOf: url) else { throw Failure("Failed to load generated thumbnail") } return thumbnailImage } // TODO: Adopt BuddyImageKit func vb_heicEncodedData(options: CFDictionary = NSImage.defaultHEICProperties) throws -> Data { guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { throw Failure("Failed to create a CGImage") } return try cgImage.vb_heicEncodedData(options: options) } // TODO: Adopt BuddyImageKit @discardableResult func vb_encodeHEIC(to url: URL, options: CFDictionary = NSImage.defaultHEICProperties) throws -> URL { guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { throw Failure("Failed to create a CGImage") } return try cgImage.vb_encodeHEIC(to: url, options: options) } } public extension CGImage { // TODO: Adopt BuddyImageKit func vb_heicEncodedData(options: CFDictionary = NSImage.defaultHEICProperties) throws -> Data { guard let cfData = CFDataCreateMutable(kCFAllocatorDefault, 0) else { throw Failure("Failed to create CFMutableData") } guard let destination = CGImageDestinationCreateWithData(cfData, AVFileType.heic.rawValue as CFString, 1, nil) else { throw Failure("Failed to create image destination") } CGImageDestinationAddImage(destination, self, options) CGImageDestinationFinalize(destination) return cfData as Data } // TODO: Adopt BuddyImageKit @discardableResult func vb_encodeHEIC(to url: URL, options: CFDictionary = NSImage.defaultHEICProperties) throws -> URL { guard let destination = CGImageDestinationCreateWithURL(url as CFURL, AVFileType.heic.rawValue as CFString, 1, nil) else { throw Failure("Failed to create image destination") } CGImageDestinationAddImage(destination, self, options) CGImageDestinationFinalize(destination) return url } } ================================================ FILE: VirtualCore/Source/Virtualization/VBVirtualMachine+Virtualization.swift ================================================ // // VBVirtualMachine+Virtualization.swift // VirtualCore // // Created by Guilherme Rambo on 21/07/22. // import Foundation import Virtualization extension VBVirtualMachine { func fetchOrGenerateAuxiliaryStorage(hardwareModel: VZMacHardwareModel? = nil) throws -> VZMacAuxiliaryStorage { if FileManager.default.fileExists(atPath: auxiliaryStorageURL.path) { return VZMacAuxiliaryStorage(contentsOf: auxiliaryStorageURL) } else { return try generateAuxiliaryStorage(hardwareModel: hardwareModel) } } @discardableResult func generateAuxiliaryStorage(hardwareModel: VZMacHardwareModel? = nil) throws -> VZMacAuxiliaryStorage { if FileManager.default.fileExists(atPath: auxiliaryStorageURL.path) { try FileManager.default.removeItem(at: auxiliaryStorageURL) } let hw: VZMacHardwareModel if let hardwareModel { hw = hardwareModel } else { hw = try fetchOrGenerateHardwareModel(with: nil) } return try VZMacAuxiliaryStorage( creatingStorageAt: auxiliaryStorageURL, hardwareModel: hw ) } func fetchOrGenerateHardwareModel(with restoreImage: VZMacOSRestoreImage?) throws -> VZMacHardwareModel { let hardwareModel: VZMacHardwareModel if FileManager.default.fileExists(atPath: hardwareModelURL.path) { guard let hardwareModelData = try? Data(contentsOf: hardwareModelURL) else { throw Failure("Failed to retrieve hardware model data.") } guard let hw = VZMacHardwareModel(dataRepresentation: hardwareModelData) else { throw Failure("Failed to create hardware model.") } hardwareModel = hw } else { guard let image = restoreImage else { throw Failure("Hardware model data doesn't exist, but a restore image was not provided to create the initial data.") } guard let hw = image.mostFeaturefulSupportedConfiguration?.hardwareModel else { throw Failure("Failed to obtain hardware model from restore image") } hardwareModel = hw try hw.dataRepresentation.write(to: hardwareModelURL) } guard hardwareModel.isSupported else { throw Failure("The hardware model is not supported on the current host") } return hardwareModel } func fetchExistingMachineIdentifier() throws -> VZMacMachineIdentifier { guard let machineIdentifierData = try? Data(contentsOf: machineIdentifierURL) else { throw Failure("Failed to retrieve machine identifier data.") } guard let mid = VZMacMachineIdentifier(dataRepresentation: machineIdentifierData) else { throw Failure("Failed to create machine identifier.") } return mid } func fetchOrGenerateMachineIdentifier() throws -> VZMacMachineIdentifier { let identifier: VZMacMachineIdentifier if FileManager.default.fileExists(atPath: machineIdentifierURL.path) { identifier = try fetchExistingMachineIdentifier() } else { identifier = try generateNewMachineIdentifier() } return identifier } @discardableResult func generateNewMachineIdentifier() throws -> VZMacMachineIdentifier { if FileManager.default.fileExists(atPath: machineIdentifierURL.path) { try FileManager.default.removeItem(at: machineIdentifierURL) } let identifier = VZMacMachineIdentifier() try identifier.dataRepresentation.write(to: machineIdentifierURL) return identifier } } public extension VBVirtualMachine { var ECID: UInt64? { guard let machineIdentifier = try? self.fetchExistingMachineIdentifier() else { return nil } let data = machineIdentifier.dataRepresentation guard let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { return nil } return dict["ECID"] as? UInt64 } } ================================================ FILE: VirtualCore/Source/Virtualization/VMController.swift ================================================ // // VMController.swift // VirtualCore // // Created by Guilherme Rambo on 07/04/22. // import Cocoa import Foundation import Virtualization import Combine import OSLog public struct VMSessionOptions: Hashable, Codable { @DecodableDefault.False public var bootInRecoveryMode = false { didSet { guard bootInRecoveryMode != oldValue else { return } resolveMutuallyExclusiveOptions() } } @DecodableDefault.False public var bootInDFUMode = false { didSet { guard bootInDFUMode != oldValue else { return } resolveMutuallyExclusiveOptions() } } @DecodableDefault.False public var bootOnInstallDevice = false @DecodableDefault.False public var autoBoot = false /// Used when restoring from a previously-saved state. public var stateRestorationPackageURL: URL? public static let `default` = VMSessionOptions() public init(bootInRecoveryMode: Bool = false, bootInDFUMode: Bool = false, bootOnInstallDevice: Bool = false, autoBoot: Bool = false, stateRestorationPackageURL: URL? = nil) { self.bootInRecoveryMode = bootInRecoveryMode self.bootInDFUMode = bootInDFUMode self.bootOnInstallDevice = bootOnInstallDevice self.autoBoot = autoBoot self.stateRestorationPackageURL = stateRestorationPackageURL resolveMutuallyExclusiveOptions() } private mutating func resolveMutuallyExclusiveOptions() { if bootInDFUMode { bootInRecoveryMode = false } if bootInRecoveryMode { bootInDFUMode = false } } } public enum VMState: Equatable { case idle case starting(_ message: String?) case running(VZVirtualMachine) case paused(VZVirtualMachine) case savingState(VZVirtualMachine) case stateSaveCompleted(VZVirtualMachine, VBSavedStatePackage) case restoringState(VZVirtualMachine, VBSavedStatePackage) case stopped(Error?) } @MainActor public final class VMController: ObservableObject { public let id: VBVirtualMachine.ID private let name: String private let library: VMLibraryController private lazy var logger = Logger(for: Self.self) @Published public var options = VMSessionOptions.default { didSet { instance?.options = options } } public typealias State = VMState @Published public private(set) var state = State.idle private(set) var virtualMachine: VZVirtualMachine? @Published public var virtualMachineModel: VBVirtualMachine public private(set) var savedStatesController: VMSavedStatesController private lazy var cancellables = Set() public init(with vm: VBVirtualMachine, library: VMLibraryController, options: VMSessionOptions? = nil) { self.id = vm.id self.name = vm.name self.virtualMachineModel = vm self.library = library self.savedStatesController = VMSavedStatesController(library: library, virtualMachine: vm) #if DEBUG if ProcessInfo.isSwiftUIPreview { self.savedStatesController = .preview } #endif virtualMachineModel.reloadMetadata() if virtualMachineModel.metadata.installImageURL != nil && !virtualMachineModel.metadata.installFinished { self.options.bootOnInstallDevice = true } if let options { self.options = options } /// Ensure configuration is persisted whenever it changes. $virtualMachineModel .dropFirst() .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) .sink { updatedModel in do { try updatedModel.saveMetadata() } catch { assertionFailure("Failed to save configuration: \(error)") } } .store(in: &cancellables) library.addController(self) /// Make sure DFU mode flag is turned off if the app build doesn't allow DFU boot. if !VBMacConfiguration.appBuildAllowsDFUMode { self.options.bootInDFUMode = false } } private var instance: VMInstance? private func createInstance() throws -> VMInstance { let newInstance = VMInstance(with: virtualMachineModel, library: library, onVMStop: { [weak self] error in self?.state = .stopped(error) }) newInstance.options = options return newInstance } public func start() async throws { state = .starting(nil) await waitForGuestDiskImageReadyIfNeeded() try await updatingState { let newInstance = try createInstance() self.instance = newInstance if #available(macOS 14.0, *), let restorePackageURL = options.stateRestorationPackageURL { do { let package = try VBSavedStatePackage(url: restorePackageURL) try await newInstance.restoreState(from: package) { vm, package in try? await updatingState { state = .restoringState(vm, package) } } } catch { guard !(error is CancellationError) else { state = .idle return } throw error } } else { try await newInstance.startVM() } let vm = try newInstance.virtualMachine state = .running(vm) virtualMachineModel.metadata.installFinished = true } } /// If the virtual machine supports the guest app and has the toggle to auto-mount the guest image enabled, /// this method waits until the guest disk image is ready before returning. /// /// This is used to wait for the guest disk image to be ready before starting a virtual machine, which may occur /// if the user launches VirtualBuddy then quickly attempts to start up a machine right after installing an app update. /// /// It will also alert the user in case guest disk image generation has failed so that they know there's something wrong/ private func waitForGuestDiskImageReadyIfNeeded() async { guard virtualMachineModel.configuration.guestAdditionsEnabled, virtualMachineModel.configuration.systemType.supportsGuestApp else { return } let guestDiskState = GuestAdditionsDiskImage.current.state logger.info("Guest disk image state is \(guestDiskState, privacy: .public)") switch guestDiskState { case .ready: break case .installing: await waitForGuestDiskImageReady() case .installFailed(let error): runGuestDiskImageErrorAlert(error: error) } } private func waitForGuestDiskImageReady() async { state = .starting("Preparing guest app disk image") for await state in GuestAdditionsDiskImage.current.$state.values { switch state { case .ready: logger.debug("Guest disk image is ready 🚀") return case .installFailed(let error): logger.error("Guest disk image install failed - \(error, privacy: .public)") return runGuestDiskImageErrorAlert(error: error) case .installing: logger.debug("Guest disk image is installing...") } } } private func runGuestDiskImageErrorAlert(error: Error) { logger.debug(#function) let alertSuppressionKey = "VBGuestDiskImageAlertSuppressed" guard !UserDefaults.standard.bool(forKey: alertSuppressionKey) else { logger.debug("Guest disk image error alert suppressed, ignoring error.") return } let alert = NSAlert() alert.messageText = "Guest App Image Error" alert.informativeText = """ An error occurred when VirtualBuddy attempted to generate the disk image for the guest app. Restarting the app might fix it. The virtual machine will boot normally, but the guest app disk image will not be mounted. If the virtual machine already has the guest app installed, it will not be updated to the latest version. \(error) """ alert.addButton(withTitle: "Continue") alert.showsSuppressionButton = true alert.runModal() if let suppressionButton = alert.suppressionButton, suppressionButton.state == .on { logger.info("Guest disk image error alert will be suppressed in the future.") UserDefaults.standard.set(true, forKey: alertSuppressionKey) } } public func pause() async throws { try await updatingState { let instance = try ensureInstance() try await instance.pause() let vm = try instance.virtualMachine state = .paused(vm) } unhideCursor() } public func resume() async throws { try await updatingState { let instance = try ensureInstance() try await instance.resume() let vm = try instance.virtualMachine state = .running(vm) } unhideCursor() } public func stop() async throws { try await updatingState { let instance = try ensureInstance() try await instance.stop() } unhideCursor() } public func forceStop() async throws { try await updatingState { let instance = try ensureInstance() try await instance.forceStop() state = .stopped(nil) } unhideCursor() } @available(macOS 14.0, *) public func saveState(snapshotName name: String) async throws { try await updatingState { let instance = try ensureInstance() let vm = try instance.virtualMachine do { let package = try await instance.saveState(snapshotName: name) { state = .savingState(vm) } state = .stateSaveCompleted(vm, package) } catch is CancellationError { /// User cancellation is not an error, it may just be ignored here. /// As of the current implementation of `VMInstance.saveState`, the VM won't be paused /// because the only cancellation point is before that happens, but check for pause in here just in /// case that behavior changes in the future. try await resumeIfNeeded() } catch { throw error } try await resumeIfNeeded() } unhideCursor() } private func resumeIfNeeded() async throws { guard !state.isRunning else { return } try await Task.sleep(for: .seconds(1.5)) try await resume() } private func updatingState(perform block: () async throws -> Void) async throws { do { try await block() } catch { state = .stopped(error) throw error } } private func ensureInstance() throws -> VMInstance { guard let instance = instance else { throw CocoaError(.validationMissingMandatoryProperty) } instance.options = options return instance } public func storeScreenshot(with data: Data) { do { try virtualMachineModel.write(data, forMetadataFileNamed: VBVirtualMachine.screenshotFileName) try virtualMachineModel.invalidateThumbnail() } catch { logger.error("Error storing screenshot: \(error)") } } public func invalidate() { library.removeController(self) } deinit { #if DEBUG print("\(name) Bye bye 👋") #endif library.removeController(self) VBMemoryLeakDebugAssertions.vb_objectIsBeingReleased(self) } } public extension VMState { static func ==(lhs: VMState, rhs: VMState) -> Bool { switch lhs { case .idle: return rhs.isIdle case .starting: return rhs.isStarting case .running: return rhs.isRunning case .paused: return rhs.isPaused case .stopped: return rhs.isStopped case .savingState: return rhs.isSavingState case .restoringState: return rhs.isRestoringState case .stateSaveCompleted: return rhs.isStateSaveCompleted } } var isIdle: Bool { guard case .idle = self else { return false } return true } var isStarting: Bool { guard case .starting = self else { return false } return true } var isRunning: Bool { guard case .running = self else { return false } return true } var isPaused: Bool { guard case .paused = self else { return false } return true } var isStopped: Bool { guard case .stopped = self else { return false } return true } var isSavingState: Bool { guard case .savingState = self else { return false } return true } var isRestoringState: Bool { guard case .restoringState = self else { return false } return true } var isStateSaveCompleted: Bool { guard case .stateSaveCompleted = self else { return false } return true } var canStart: Bool { switch self { case .idle, .stopped: return true default: return false } } var canResume: Bool { switch self { case .paused: return true default: return false } } var canPause: Bool { switch self { case .running: return true default: return false } } } public extension VMController { var canStart: Bool { state.canStart } var canResume: Bool { state.canResume } var canPause: Bool { state.canPause } } public extension VMController { /// Workaround for cursor disappearing due to it being captured /// by Virtualization during state transitions. func unhideCursor() { Task { try? await Task.sleep(nanoseconds: 100_000_000) NSCursor.unhide() } } } public extension VBMacConfiguration { /// DFU mode option is currently shown in debug builds or when `VBShowDFUModeBootOption` is set in user defaults. /// To enable in release builds: `defaults write codes.rambo.VirtualBuddy VBShowDFUModeBootOption -bool YES` static var appBuildAllowsDFUMode: Bool { #if DEBUG return true #else return UserDefaults.standard.bool(forKey: "VBShowDFUModeBootOption") #endif } } ================================================ FILE: VirtualCore/Source/Virtualization/VMInstance.swift ================================================ // // VMInstance.swift // VirtualCore // // Created by Guilherme Rambo on 12/04/22. // import Cocoa import Foundation import Virtualization import Combine import OSLog import VirtualWormhole @MainActor public final class VMInstance: NSObject, ObservableObject { private let library: VMLibraryController private let logger: Logger var options = VMSessionOptions.default private var _virtualMachine: VZVirtualMachine? var virtualMachine: VZVirtualMachine { get throws { guard let vm = _virtualMachine else { throw CocoaError(.validationMissingMandatoryProperty) } return vm } } let wormhole: WormholeManager = .sharedHost private var isLoadingNVRAM = false var virtualMachineModel: VBVirtualMachine { didSet { precondition(oldValue.id == virtualMachineModel.id, "Can't change the virtual machine identity after initializing the controller") } } var onVMStop: (Error?) -> Void = { _ in } init(with vm: VBVirtualMachine, library: VMLibraryController, onVMStop: @escaping (Error?) -> Void) { self.virtualMachineModel = vm self.library = library self.onVMStop = onVMStop self.logger = Logger(subsystem: VirtualCoreConstants.subsystemName, category: "VMInstance(\(vm.name))") } // MARK: Create the Mac Platform Configuration private static func loadRestoreImage(from url: URL) async throws -> VZMacOSRestoreImage { try await withCheckedThrowingContinuation { continuation in VZMacOSRestoreImage.load(from: url) { result in continuation.resume(with: result) } } } public static func createMacPlatform(for model: VBVirtualMachine, installImageURL: URL?) async throws -> VZMacPlatformConfiguration { let image: VZMacOSRestoreImage? if let installImageURL = installImageURL { image = try await loadRestoreImage(from: installImageURL) } else { image = nil } let macPlatform = VZMacPlatformConfiguration() let hardwareModel = try model.fetchOrGenerateHardwareModel(with: image) macPlatform.hardwareModel = hardwareModel macPlatform.auxiliaryStorage = try model.fetchOrGenerateAuxiliaryStorage(hardwareModel: hardwareModel) macPlatform.machineIdentifier = try model.fetchOrGenerateMachineIdentifier() return macPlatform } @available(macOS 13.0, *) public static func createGenericPlatform(for model: VBVirtualMachine, installImageURL: URL?) async throws -> VZGenericPlatformConfiguration { let genericPlatform = VZGenericPlatformConfiguration() return genericPlatform } // MARK: Create the Virtual Machine Configuration and instantiate the Virtual Machine public static func makeConfiguration(for model: VBVirtualMachine, installImageURL: URL? = nil, savedState: VBSavedStatePackage? = nil) async throws -> VZVirtualMachineConfiguration { let helper: VirtualMachineConfigurationHelper let platform: VZPlatformConfiguration let installDevice: [VZStorageDeviceConfiguration] switch model.configuration.systemType { case .mac: helper = MacOSVirtualMachineConfigurationHelper(vm: model, savedState: savedState) platform = try await Self.createMacPlatform(for: model, installImageURL: installImageURL) installDevice = [] case .linux: helper = LinuxVirtualMachineConfigurationHelper(vm: model) platform = try await Self.createGenericPlatform(for: model, installImageURL: nil) if let installImageURL { installDevice = [try helper.createInstallDevice(installImageURL: installImageURL)] } else { installDevice = [] } } let c = VZVirtualMachineConfiguration() c.platform = platform c.bootLoader = try helper.createBootLoader() c.cpuCount = model.configuration.hardware.cpuCount c.memorySize = model.configuration.hardware.memorySize c.graphicsDevices = helper.createGraphicsDevices() c.networkDevices = try model.configuration.vzNetworkDevices c.pointingDevices = try model.configuration.vzPointingDevices c.keyboards = [helper.createKeyboardConfiguration()] c.entropyDevices = helper.createEntropyDevices() c.audioDevices = model.configuration.vzAudioDevices c.directorySharingDevices = try model.configuration.vzSharedFoldersFileSystemDevices if let spiceAgent = helper.createSpiceAgentConsoleDeviceConfiguration() { c.consoleDevices = [spiceAgent] } if #available(macOS 15.0, *) { c.usbControllers = helper.createUSBControllers() } let bootDevice = try await helper.createBootBlockDevice() let additionalBlockDevices = try await helper.createAdditionalBlockDevices() c.storageDevices = installDevice + [bootDevice] + additionalBlockDevices return c } private func createVirtualMachine(savedState: VBSavedStatePackage?) async throws { logger.debug(#function) let installImage: URL? if options.bootOnInstallDevice { installImage = virtualMachineModel.metadata.installImageURL } else { installImage = nil } let config = try await Self.makeConfiguration(for: virtualMachineModel, installImageURL: installImage, savedState: savedState) // add install iso here for linux (hack) await setupWormhole(for: config) do { try config.validate() logger.info("Configuration validated") } catch { logger.fault("Invalid configuration: \(String(describing: error))") throw Failure("Failed to validate configuration: \(String(describing: error))") } _virtualMachine = VZVirtualMachine(configuration: config) } private func setupWormhole(for config: VZVirtualMachineConfiguration) async { guard virtualMachineModel.configuration.systemType == .mac else { return } wormhole.activate() let guestPort = VZVirtioConsoleDeviceSerialPortConfiguration() let inputPipe = Pipe() let outputPipe = Pipe() let inputHandle = inputPipe.fileHandleForWriting let outputHandle = outputPipe.fileHandleForReading guestPort.attachment = VZFileHandleSerialPortAttachment( fileHandleForReading: outputHandle, fileHandleForWriting: inputHandle ) config.serialPorts = [guestPort] await wormhole.register( input: inputPipe.fileHandleForReading, output: outputPipe.fileHandleForWriting, for: virtualMachineModel.wormholeID ) streamGuestNotifications() streamGuestDesktopPictureMessages() } private lazy var guestIOTasks = [Task]() public func streamGuestNotifications() { logger.debug(#function) let notificationNames: Set = [ "com.apple.shieldWindowRaised", "com.apple.shieldWindowLowered" ] let task = Task { do { for await notification in try await wormhole.darwinNotifications(matching: notificationNames, from: virtualMachineModel.wormholeID) { if notification == "com.apple.shieldWindowRaised" { logger.debug("🔒 Guest locked") } else if notification == "com.apple.shieldWindowLowered" { logger.debug("🔓 Guest unlocked") } } } catch { logger.error("Error subscribing to Darwin notifications: \(error, privacy: .public)") } } guestIOTasks.append(task) } public func streamGuestDesktopPictureMessages() { logger.debug(#function) let task = Task { do { for await message in try await wormhole.desktopPictureMessages(from: virtualMachineModel.wormholeID) { do { let fileURL = virtualMachineModel.metadataFileURL(VBVirtualMachine.thumbnailFileName) try message.content.write(to: fileURL, options: .atomic) if let image = NSImage(data: message.content), let blurHash = image.blurHash(numberOfComponents: (Int.vbBlurHashSize, Int.vbBlurHashSize)) { virtualMachineModel.metadata.backgroundHash = BlurHashToken(value: blurHash, size: .vbBlurHashSize) } try virtualMachineModel.saveMetadata() } catch { logger.error("Error handling desktop picture message: \(error, privacy: .public)") } } } catch { logger.error("Error subscribing to desktop picture messages: \(error, privacy: .public)") } } guestIOTasks.append(task) } func startVM() async throws { try await bootstrap() let vm = try ensureVM() try await vm.start(options: startOptions) #if DEBUG VBDebugUtil.debugVirtualMachine(afterStart: vm) #endif } private func bootstrap(savedState: VBSavedStatePackage? = nil) async throws { try await createVirtualMachine(savedState: savedState) let vm = try ensureVM() vm.delegate = self library.registerBootedVM(self) #if DEBUG VBDebugUtil.debugVirtualMachine(beforeStart: vm) #endif } @available(macOS 13, *) private var startOptions: VZVirtualMachineStartOptions { switch virtualMachineModel.configuration.systemType { case .mac: return VZMacOSVirtualMachineStartOptions(options: options) case .linux: return VZVirtualMachineStartOptions() } } func pause() async throws { logger.debug(#function) let vm = try ensureVM() try await vm.pause() } func resume() async throws { logger.debug(#function) let vm = try ensureVM() try await vm.resume() } func stop() async throws { logger.debug(#function) let vm = try ensureVM() try vm.requestStop() } func forceStop() async throws { logger.debug(#function) let vm = try ensureVM() try await vm.stop() library.unregisterBootedVM(self) } @available(macOS 14.0, *) @discardableResult func saveState(snapshotName name: String, onStart: () -> ()) async throws -> VBSavedStatePackage { logger.debug(#function) let vm = try ensureVM() guard confirmSaveStateIfNotOnAPFSVolume() else { logger.info("State save denied by user.") throw CancellationError() } /// Callback so that caller may update UI to indicate that saving has actually started, /// but only after the user has performed pre-save confirmation steps. onStart() logger.debug("Pausing to save state") try await pause() logger.debug("VM paused, requesting state save") let package = try virtualMachineModel.createSavedStatePackage(in: library, snapshotName: name) logger.debug("VM state package will be written to \(package.url.path)") do { try await package.createStorageDeviceClones(model: virtualMachineModel) try await vm.saveMachineStateTo(url: package.dataFileURL) logger.log("VM state saved to \(package.dataFileURL.path)") return package } catch { try? FileManager.default.removeItem(at: package.url) logger.error("VM state save failed: \(error, privacy: .public)") throw error } } /// Asks user for confirmation before saving state if the volume where the VirtualBuddy library /// resides is not an APFS volume, meaning that cloning is not available. @available(macOS 14.0, *) private func confirmSaveStateIfNotOnAPFSVolume() -> Bool { guard !library.isInAPFSVolume else { return true } let suppressionKey = "SuppressConfirmSaveStateNonAPFSVolumeAlert" guard !UserDefaults.standard.bool(forKey: suppressionKey) else { return true } let alert = NSAlert() alert.messageText = "Disk Space Warning" alert.informativeText = """ It seems like your virtual machine data can’t be cloned because your library isn’t in an APFS volume. Creating this snapshot might take up several gigabytes of storage space. Would you like to continue? """ alert.addButton(withTitle: "Create Snapshot") alert.addButton(withTitle: "Cancel") alert.showsSuppressionButton = true guard alert.runModal() == .alertFirstButtonReturn else { return false } if alert.suppressionButton?.state == .on { UserDefaults.standard.set(true, forKey: suppressionKey) } return true } @available(macOS 14.0, *) func restoreState(from package: VBSavedStatePackage, updateHandler: (_ vm: VZVirtualMachine, _ package: VBSavedStatePackage) async -> Void) async throws { logger.debug("Restore state requested with package \(package.url.path)") try await runSavedStateMigrationIfNeeded(for: package) try package.validate(for: virtualMachineModel) if _virtualMachine == nil { logger.debug("Bootstrapping VM for state restoration") try await bootstrap(savedState: package) } let vm = try ensureVM() await updateHandler(vm, package) logger.debug("Restoring state from \(package.dataFileURL.path)") do { try await vm.restoreMachineStateFrom(url: package.dataFileURL) logger.log("Successfully restored state from \(package.dataFileURL.path), resuming VM") try await resume() #if DEBUG VBDebugUtil.debugVirtualMachine(afterStart: vm) #endif } catch { logger.error("VM state restoration failed: \(error, privacy: .public). State file: \(package.dataFileURL.path)") throw error } } @available(macOS 14.0, *) private func runSavedStateMigrationIfNeeded(for package: VBSavedStatePackage) async throws { guard package.needsStorageCloneMigration else { return } guard confirmSavedStateMigration() else { throw CancellationError() } guard confirmSaveStateIfNotOnAPFSVolume() else { throw CancellationError() } try await package.createStorageDeviceClones(model: virtualMachineModel) } @available(macOS 14.0, *) private func confirmSavedStateMigration() -> Bool { let suppressionKey = "SuppressConfirmSavedStateMigrationAlert" guard !UserDefaults.standard.bool(forKey: suppressionKey) else { return true } let alert = NSAlert() alert.messageText = "Migration Required" alert.informativeText = """ The virtual machine’s state was saved in an older version of VirtualBuddy that didn’t create clones of the storage devices. \ This could lead to data corruption over time. To use this saved state, we need to migrate it to include storage device clones. """ alert.addButton(withTitle: "Migrate and Restore") alert.addButton(withTitle: "Cancel") alert.showsSuppressionButton = true guard alert.runModal() == .alertFirstButtonReturn else { return false } if alert.suppressionButton?.state == .on { UserDefaults.standard.set(true, forKey: suppressionKey) } return true } private func ensureVM() throws -> VZVirtualMachine { guard let vm = _virtualMachine else { let e = Failure("The virtual machine instance is not available.") DispatchQueue.main.async { self.onVMStop(e) } throw e } return vm } } // MARK: - VZVirtualMachineDelegate extension VMInstance: VZVirtualMachineDelegate { public nonisolated func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) { MainActor.assumeIsolated { handleGuestStopped(with: error) } } public nonisolated func guestDidStop(_ virtualMachine: VZVirtualMachine) { MainActor.assumeIsolated { handleGuestStopped(with: nil) } } public nonisolated func virtualMachine(_ virtualMachine: VZVirtualMachine, networkDevice: VZNetworkDevice, attachmentWasDisconnectedWithError error: Error) { } private func handleGuestStopped(with error: Error?) { guestIOTasks.forEach { $0.cancel() } guestIOTasks.removeAll() if let error { logger.error("Guest stopped with error: \(String(describing: error), privacy: .public)") } else { logger.debug("Guest stopped") } DispatchQueue.main.async { [self] in library.unregisterBootedVM(self) Task { await wormhole.unregister(virtualMachineModel.wormholeID) } onVMStop(error) } } } extension NSApplication { func entitlementValue(for entitlement: String) -> V? { guard let task = SecTaskCreateFromSelf(nil) else { assertionFailure("SecTaskCreateFromSelf returned nil") return nil } return SecTaskCopyValueForEntitlement(task, entitlement as CFString, nil) as? V } func hasEntitlement(_ entitlement: String) -> Bool { entitlementValue(for: entitlement) == true } } private extension VBVirtualMachine { /// ``VBVirtualMachine/id`` uses the VM's filesystem URL, /// but that looks ugly in logs and whatnot, so this returns a cleaned up version. var wormholeID: WHPeerID { let cleanID = URL(fileURLWithPath: id) .deletingPathExtension() .lastPathComponent return cleanID.removingPercentEncoding ?? cleanID } } extension VZMacOSVirtualMachineStartOptions { convenience init(options: VMSessionOptions) { self.init() startUpFromMacOSRecovery = options.bootInRecoveryMode if options.bootInDFUMode, VBMacConfiguration.appBuildAllowsDFUMode, self.responds(to: NSSelectorFromString("_setForceDFU:")) { _forceDFU = true startUpFromMacOSRecovery = false } } } ================================================ FILE: VirtualCore/Source/Virtualization/VMLibraryController.swift ================================================ // // VMLibraryController.swift // VirtualCore // // Created by Guilherme Rambo on 10/04/22. // import SwiftUI import Combine import OSLog import BuddyFoundation @MainActor public final class VMLibraryController: ObservableObject { private let logger = Logger(for: VMLibraryController.self) public enum State: Identifiable { public enum ID: Int { case loading case loaded case empty case volumeNotMounted case directoryMissing } public var id: ID { switch self { case .loading: .loading case .loaded: .loaded case .empty: .empty case .volumeNotMounted: .volumeNotMounted case .directoryMissing: .directoryMissing } } case loading case loaded([VBVirtualMachine]) case empty case volumeNotMounted case directoryMissing var isVolumeNotMounted: Bool { id == .volumeNotMounted } } @Published public private(set) var state = State.loading { didSet { if case .loaded(let machines) = state { self.virtualMachines = machines } else { self.virtualMachines = [] } } } @Published public private(set) var virtualMachines: [VBVirtualMachine] = [] /// Identifiers for all VMs that are currently in a "booted" state (starting, booted, or paused). @Published public private(set) var bootedMachineIdentifiers = Set() private let bootedInstances = NSMapTable(keyOptions: [.objectPersonality, .strongMemory], valueOptions: [.objectPersonality, .weakMemory]) /// Populated when ``bootedMachineIdentifiers`` is not empty, invalidated when the last booted machine identifier is unregistered via ``unregisterBootedVM(identifier:)``. private var preventTerminationAssertion: PreventTerminationAssertion? /// Observes notifications about volume mount/unmount that are used when library resides in a removable volume. private var volumeNotificationsTask: Task? /// Set when the library is loaded. private var isLibraryInRemovableVolume = false let settingsContainer: VBSettingsContainer private let filePresenter: DirectoryObserver private let updateSignal = PassthroughSubject() private static let observedFileExtensions: Set = [ VBVirtualMachine.bundleExtension, "plist", "heic" ] public init(settingsContainer: VBSettingsContainer = .current) { self.settingsContainer = settingsContainer self.settings = settingsContainer.settings self.libraryURL = settingsContainer.settings.libraryURL self.filePresenter = DirectoryObserver( presentedItemURL: settingsContainer.settings.libraryURL, fileExtensions: Self.observedFileExtensions, label: "Library", signal: updateSignal ) loadMachines() bind() } private var settings: VBSettings { didSet { self.libraryURL = settings.libraryURL } } @Published public private(set) var libraryURL: URL { didSet { guard oldValue != libraryURL else { return } stopObservingRemovableVolumeNotifications() loadMachines() } } private lazy var cancellables = Set() private lazy var fileManager = FileManager() private var hasLoadedMachinesOnce = false private func bind() { settingsContainer.$settings.sink { [weak self] newSettings in self?.settings = newSettings } .store(in: &cancellables) updateSignal .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) .sink { [weak self] url in guard let self else { return } /// Ignore file change notifications in the guest additions or downloads directories. guard !url.path.contains(GuestAdditionsDiskImage.imagesRootURL.path) else { return } guard !url.path.contains(VBSettings.current.downloadsDirectoryURL.path) else { return } guard let bundleURL = url.virtualMachineBundleParent else { self.logger.fault("Failed to determine VM bundle URL for changed file \(url.lastPathComponent)") return } self.handleBundleChanged(at: bundleURL) } .store(in: &cancellables) } private func handleBundleChanged(at bundleURL: URL) { logger.debug("Bundle changed: \(bundleURL.lastPathComponent)") loadMachines() } public func loadMachines(createLibrary: Bool = false) { #if DEBUG guard !simulateState() else { return } #endif let path = libraryURL.path logger.debug("Loading machines from \(path.quoted)") if createLibrary, !libraryURL.isReadableDirectory { do { try fileManager.createDirectory(at: libraryURL, withIntermediateDirectories: true) } catch { NSApp.presentError(error) return } } isLibraryInRemovableVolume = Self.isLibraryURLInRemovableVolume(libraryURL) observeRemovableVolumeNotifications() guard libraryURL.isReadableDirectory else { if isLibraryInRemovableVolume { logger.warning("External volume library directory not found at \(path.quoted)") state = .volumeNotMounted } else { logger.warning("Library directory not found at \(path.quoted)") state = .directoryMissing } return } filePresenter.presentedItemURL = libraryURL guard let enumerator = fileManager.enumerator(at: libraryURL, includingPropertiesForKeys: [.creationDateKey], options: [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants], errorHandler: nil) else { logger.fault("Failed to create directory enumerator for library at \(path.quoted)") state = .directoryMissing return } var machines = [VBVirtualMachine]() while let url = enumerator.nextObject() as? URL { guard url.pathExtension == VBVirtualMachine.bundleExtension else { continue } do { let machine = try VBVirtualMachine(bundleURL: url, createIfNeeded: false) if let index = machines.firstIndex(where: { $0.id == machine.id }) { machines[index] = machine } else { machines.append(machine) } } catch is VBVirtualMachine.BundleDirectoryMissingError { /// This error can occur when the bundle is deleted in Finder. logger.debug("Ignoring BundleDirectoryMissingError for \(url.lastPathComponent)") } catch { assertionFailure("Failed to construct VM model: \(error)") } } let sortedMachines = machines.sorted(by: { $0.bundleURL.creationDate > $1.bundleURL.creationDate }) self.state = sortedMachines.isEmpty ? .empty : .loaded(sortedMachines) if !hasLoadedMachinesOnce { hasLoadedMachinesOnce = true migrateBackgroundHashesForLegacyThumbnails() } } public func reload(animated: Bool = true) { if animated { withAnimation(.spring()) { loadMachines() } } else { loadMachines() } } public func validateNewName(_ name: String, for vm: VBVirtualMachine) throws { /// No need to validate if name is not changed. guard name != vm.name else { return } try urlForRenaming(vm, to: name) } // MARK: - VM Controller References private final class Coordinator { private let lock = NSRecursiveLock() private var _activeVMControllers = [VBVirtualMachine.ID: WeakReference]() /// References to all active `VMController` instances by VM identifier. /// May hold references to invalidated controllers because this does not hold a strong reference to them. var activeVMControllers: [VBVirtualMachine.ID: WeakReference] { get { lock.withLock { _activeVMControllers } } set { lock.withLock { _activeVMControllers = newValue } } } func activeController(for virtualMachineID: VBVirtualMachine.ID) -> VMController? { activeVMControllers[virtualMachineID]?.object } /// Called when a new `VMController` is initialized so that we can reference it /// outside the scope of the view hierarchy (for automation). func addController(_ controller: VMController) { activeVMControllers[controller.id] = WeakReference(controller) } /// Called when a `VMController` is dying so that we can cleanup our reference to it. func removeController(_ controller: VMController) { activeVMControllers[controller.id] = nil } } nonisolated(unsafe) private let coordinator = Coordinator() public nonisolated var activeVMControllers: [WeakReference] { Array(coordinator.activeVMControllers.values) } public nonisolated func activeController(for virtualMachineID: VBVirtualMachine.ID) -> VMController? { coordinator.activeVMControllers[virtualMachineID]?.object } /// Called when a new `VMController` is initialized so that we can reference it /// outside the scope of the view hierarchy (for automation). nonisolated func addController(_ controller: VMController) { coordinator.activeVMControllers[controller.id] = WeakReference(controller) } /// Called when a `VMController` is dying so that we can cleanup our reference to it. nonisolated func removeController(_ controller: VMController) { coordinator.activeVMControllers[controller.id] = nil } // MARK: - Migration private var alwaysAttemptLegacyThumbnailMigration: Bool { #if DEBUG UserDefaults.standard.bool(forKey: "VBAlwaysAttemptLegacyThumbnailMigration") #else false #endif } private static let migratedLegacyThumbnailBackgroundHashesDefaultsKey = "migratedLegacyThumbnailBackgroundHashes_2" private var migratedLegacyThumbnailBackgroundHashes: Bool { get { guard !alwaysAttemptLegacyThumbnailMigration else { return false } return UserDefaults.standard.bool(forKey: Self.migratedLegacyThumbnailBackgroundHashesDefaultsKey) } set { guard !alwaysAttemptLegacyThumbnailMigration else { return } UserDefaults.standard.set(newValue, forKey: Self.migratedLegacyThumbnailBackgroundHashesDefaultsKey) } } private func migrateBackgroundHashesForLegacyThumbnails() { guard !migratedLegacyThumbnailBackgroundHashes else { return } let machines = self.virtualMachines /// Skip setting migration flag for empty machines in case user library has not been mounted yet. guard !machines.isEmpty else { return } defer { migratedLegacyThumbnailBackgroundHashes = true } logger.debug("Generating missing background hashes for legacy thumbnails...") Task { let needingMigration = machines.filter { $0.configuration.systemType == .mac && $0.metadata.backgroundHash == .virtualBuddyBackground } guard !needingMigration.isEmpty else { logger.debug("Found no machines needing background hash migration.") return } logger.debug("Found machines needing background hash migration: \(needingMigration.map(\.name).formatted(.list(type: .and)))") for var machine in needingMigration { do { guard let thumbnail = machine.thumbnailImage() else { logger.debug("Ignoring \(machine.name) for background hash migration because it doesn't have a thumbnail.") continue } if #available(macOS 15.0, *) { let isDRMProtectedBug = await thumbnail.detectDRMProtectedVideoBug() guard !isDRMProtectedBug else { logger.notice("Invalidating thumbnail for \(machine.name): detected \"DRM Protected Video\" bug in its thumbnail.") try machine.invalidateThumbnail() try machine.invalidateScreenshot() continue } } let hash = try thumbnail.blurHash(numberOfComponents: (.vbBlurHashSize, .vbBlurHashSize)) .require("Background hash generation failed.") machine.metadata.backgroundHash = BlurHashToken(value: hash) try await MainActor.run { try machine.saveMetadata() } logger.info("Migrated background hash for \(machine.name)") } catch { logger.warning("Error migrating background hash for \(machine.name) - \(error, privacy: .public)") } } } } } // MARK: - Queries public extension VMLibraryController { func virtualMachines(matching predicate: (VBVirtualMachine) -> Bool) -> [VBVirtualMachine] { virtualMachines.filter(predicate) } func virtualMachine(named name: String) -> VBVirtualMachine? { virtualMachines(matching: { $0.name.caseInsensitiveCompare(name) == .orderedSame }).first } } // MARK: - Management Actions public extension VMLibraryController { @discardableResult func duplicate(_ vm: VBVirtualMachine) throws -> VBVirtualMachine { let newName = "Copy of " + vm.name let copyURL = try urlForRenaming(vm, to: newName) try fileManager.copyItem(at: vm.bundleURL, to: copyURL) var newVM = try VBVirtualMachine(bundleURL: copyURL) newVM.bundleURL.creationDate = .now newVM.uuid = UUID() try newVM.saveMetadata() reload() return newVM } func moveToTrash(_ vm: VBVirtualMachine) async throws { try await NSWorkspace.shared.recycle([vm.bundleURL]) reload() } func rename(_ vm: VBVirtualMachine, to newName: String) throws { let newURL = try urlForRenaming(vm, to: newName) try fileManager.moveItem(at: vm.bundleURL, to: newURL) reload(animated: false) } @discardableResult func urlForRenaming(_ vm: VBVirtualMachine, to name: String) throws -> URL { guard name.count >= 3 else { throw Failure("Name must be at least 3 characters long.") } let newURL = vm .bundleURL .deletingLastPathComponent() .appendingPathComponent(name) .appendingPathExtension(VBVirtualMachine.bundleExtension) guard !fileManager.fileExists(atPath: newURL.path) else { throw Failure("Another virtual machine is already using this name, please choose another one.") } return newURL } } extension NSWorkspace: @retroactive @unchecked Sendable { } // MARK: - Booted VM Tracking public extension VMLibraryController { /// Adds virtual machine to the list of booted machines. func registerBootedVM(_ instance: VMInstance) { let id = instance.virtualMachineModel.id logger.debug("Registering booted VM \(id.shortID, privacy: .public)") bootedMachineIdentifiers.insert(id) bootedInstances.setObject(instance, forKey: id as NSString) startPreventingAppTerminationIfNeeded() } /// Removes virtual machine from the list of booted machines. func unregisterBootedVM(_ instance: VMInstance) { let id = instance.virtualMachineModel.id logger.debug("Unregistering booted VM \(id.shortID, privacy: .public)") bootedMachineIdentifiers.remove(id) bootedInstances.removeObject(forKey: id as NSString) stopPreventingAppTerminationIfNeeded() } func shutdownAll() { logger.debug(#function) for instance in bootedInstances.dictionaryRepresentation().values { let id = instance.virtualMachineModel.id Task { do { logger.debug("Requesting stop for \(id.shortID, privacy: .public)") try await instance.stop() } catch { logger.error("Error requesting stop for \(id.shortID, privacy: .public) - \(error, privacy: .public)") } } } } } // MARK: - App Termination Assertion private extension VMLibraryController { func startPreventingAppTerminationIfNeeded() { guard !bootedMachineIdentifiers.isEmpty else { return } guard preventTerminationAssertion == nil || preventTerminationAssertion?.isValid == false else { return } logger.notice("Start preventing app termination") preventTerminationAssertion = NSApp.preventTermination(reason: "virtual machines are currently running", shouldTerminate: { [weak self] _ in self?.handleAppTerminationAttempt() ?? .terminateNow }) } func stopPreventingAppTerminationIfNeeded() { guard bootedMachineIdentifiers.isEmpty else { return } guard preventTerminationAssertion != nil else { return } logger.notice("Stop preventing app termination") preventTerminationAssertion?.invalidate() preventTerminationAssertion = nil } func handleAppTerminationAttempt() -> NSApplication.TerminateReply { let alert = NSAlert() alert.messageText = "Quit VirtualBuddy?" alert.informativeText = "VirtualBuddy is currently running virtual machines. Quitting the app without shutting them down first can result in data loss." let button = alert.addButton(withTitle: "Quit Now") button.hasDestructiveAction = true let button2 = alert.addButton(withTitle: "Shutdown") button2.keyEquivalent = "\r" alert.addButton(withTitle: "Cancel") let response = alert.runModal() switch response { case .alertFirstButtonReturn: return .terminateNow case .alertSecondButtonReturn: defer { shutdownAll() } return .terminateLater default: return .terminateCancel } } } // MARK: - Removable Volume Detection /// External volume detection extension VMLibraryController { /// `true` if the specified file URL lives in a removable volume that could be unmounted when the app attempts to access it. static func isLibraryURLInRemovableVolume(_ url: URL) -> Bool { if let isInRemovableVolume = url.isInRemovableVolume { isInRemovableVolume } else if url.path.hasPrefix("/Volumes") { /// Assume removable volume when path starts with `/Volumes`. /// This is technically wrong as an external volume can be mounted anywhere and the built-in data /// volume is also located in `/Volumes`, but that one will always be mounted when this is used. /// It's fine to do here because this is only meant to be used for reporting things in the UI. true } else { /// Settings stores the removable volume state of the library directory when the user customizes it, /// use that fact as the final response if above checks fail. /// This is needed because we could be checking a URL for a directory that doesn't currently exist, /// and the mount could be somewhere other than `/Volumes`, so we have to go with what we knew before. VBSettings.current.isLibraryInRemovableVolume } } } extension URL { /// Whether the file resides in a removable volume, or `nil` if it can't be determined. var isInRemovableVolume: Bool? { guard let volumeURL = try? resourceValues(forKeys: [.volumeURLKey]).volume else { return nil } return (try? volumeURL.resourceValues(forKeys: [.volumeIsRemovableKey]))?.volumeIsRemovable } } private extension VMLibraryController { func observeRemovableVolumeNotifications() { /// Only observe when library lives in a removable volume. guard isLibraryInRemovableVolume else { return } guard volumeNotificationsTask == nil || volumeNotificationsTask?.isCancelled == true else { return } logger.debug(#function) volumeNotificationsTask = Task.detached(priority: .background) { [weak self] in await withTaskGroup { group in group.addTask { for await notification in NSWorkspace.shared.notificationCenter.notifications(named: NSWorkspace.didMountNotification) { guard !Task.isCancelled else { return } await self?.handleVolumeNotification(notification) } } group.addTask { for await notification in NSWorkspace.shared.notificationCenter.notifications(named: NSWorkspace.didUnmountNotification) { guard !Task.isCancelled else { return } await self?.handleVolumeNotification(notification) } } } } } func stopObservingRemovableVolumeNotifications() { logger.debug(#function) volumeNotificationsTask?.cancel() volumeNotificationsTask = nil } func handleVolumeNotification(_ notification: Notification) { let isMount = notification.name == NSWorkspace.didMountNotification if isMount { /// Don't care about mount notifications unless we're currently waiting for the volume to mount. guard state.isVolumeNotMounted else { return } } guard let volumeURL = notification.userInfo?[NSWorkspace.volumeURLUserInfoKey] as? URL else { return } /// Cheap way to check if mounted volume is our library volume. guard libraryURL.path.hasPrefix(volumeURL.path) else { return } logger.notice("Library volume mounted: \(volumeURL.path.quoted)") /// Try to load machines again now that library is mounted. loadMachines() } } #if DEBUG // MARK: - Debug State Simulation private extension VMLibraryController { func simulateState() -> Bool { if UserDefaults.standard.bool(forKey: "VBSimulateLibraryVolumeNotMounted") { state = .volumeNotMounted return true } else if UserDefaults.standard.bool(forKey: "VBSimulateLibraryDirectoryMissing") { state = .directoryMissing return true } else if UserDefaults.standard.bool(forKey: "VBSimulateLibraryEmpty") { state = .empty return true } else { return false } } } #endif ================================================ FILE: VirtualCore/Source/Virtualization/VMSavedStatesController.swift ================================================ import SwiftUI import Combine import OSLog @MainActor public final class VMSavedStatesController: ObservableObject { @Published public private(set) var states = [VBSavedStatePackage]() private let logger = Logger(for: VMSavedStatesController.self) private let filePresenter: DirectoryObserver private let updateSignal = PassthroughSubject() private let directoryURL: URL private let virtualMachine: VBVirtualMachine public init(library: VMLibraryController, virtualMachine: VBVirtualMachine) { self.virtualMachine = virtualMachine self.directoryURL = library.savedStateDirectoryURL(for: virtualMachine) self.filePresenter = DirectoryObserver( presentedItemURL: directoryURL, fileExtensions: [VBSavedStatePackage.fileExtension], label: "SavedStates", signal: updateSignal ) loadStates() bind() } private lazy var cancellables = Set() private lazy var fileManager = FileManager() private func bind() { updateSignal .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) .sink { [weak self] _ in self?.loadStates() } .store(in: &cancellables) } public func loadStates() { guard let enumerator = fileManager.enumerator(at: directoryURL, includingPropertiesForKeys: [.creationDateKey], options: [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants], errorHandler: nil) else { logger.error("Failed to open directory at \(self.directoryURL.path, privacy: .public)") return } var loadedStates = [VBSavedStatePackage]() while let url = enumerator.nextObject() as? URL { guard url.pathExtension == VBSavedStatePackage.fileExtension else { continue } do { let package = try VBSavedStatePackage(url: url) loadedStates.append(package) } catch { assertionFailure("Failed to construct saved state package: \(error)") } } loadedStates.sort(by: { $0.metadata.date > $1.metadata.date }) self.states = loadedStates } public func reload(animated: Bool = true) { if animated { withAnimation(.spring()) { loadStates() } } else { loadStates() } } } ================================================ FILE: VirtualCore/VirtualCore.h ================================================ // // VirtualCore.h // VirtualCore // // Created by Guilherme Rambo on 07/04/22. // #import //! Project version number for VirtualCore. FOUNDATION_EXPORT double VirtualCoreVersionNumber; //! Project version string for VirtualCore. FOUNDATION_EXPORT const unsigned char VirtualCoreVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import #import #import ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/FirstLaunchExperience.dataset/Contents.json ================================================ { "data" : [ { "filename" : "FirstLaunchExperience.caar", "idiom" : "universal", "universal-type-identifier" : "com.apple.coreanimation-archive" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/FullBleedBlurHash.dataset/Contents.json ================================================ { "data" : [ { "filename" : "FullBleedBlurHash.caar", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/GuestSymbol.imageset/Contents.json ================================================ { "images" : [ { "filename" : "GuestSymbol.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/StatusItemPanelChromeBorder.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0.686", "green" : "0.686", "red" : "0.686" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0.251", "green" : "0.251", "red" : "0.251" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/ThumbnailPlaceholder.imageset/Contents.json ================================================ { "images" : [ { "filename" : "ThumbnailPlaceholder.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "ThumbnailPlaceholder@2x.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "original" } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/VBGuestType/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "provides-namespace" : true } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/VBGuestType/linux.imageset/Contents.json ================================================ { "images" : [ { "filename" : "icon-linux.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/VBGuestType/mac.imageset/Contents.json ================================================ { "images" : [ { "filename" : "icon-mac.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/VirtualBuddyMono.imageset/Contents.json ================================================ { "images" : [ { "filename" : "VirtualBuddyMono.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/VirtualBuddyMonoHappy.imageset/Contents.json ================================================ { "images" : [ { "filename" : "VirtualBuddyMonoHappy.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: VirtualUI/Resources/VirtualUI.xcassets/VirtualBuddyMonoSad.imageset/Contents.json ================================================ { "images" : [ { "filename" : "VirtualBuddyMonoSad.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: VirtualUI/Source/Building Blocks/CGFloat+OnePixel.swift ================================================ import CoreGraphics public extension CGFloat { static func onePixel(in view: NSView?) -> CGFloat { guard let screen = view?.window?.screen ?? NSScreen.main else { return 1 } return 1 / screen.backingScaleFactor } static var onePixel: CGFloat { let scale = NSScreen.main?.backingScaleFactor ?? 2 return 1 / scale } } ================================================ FILE: VirtualUI/Source/Building Blocks/ChromeBorderModifier.swift ================================================ import SwiftUI extension View { func chromeBorder( radius: CGFloat, highlightEnabled: Bool = true, rimEnabled: Bool = true, shadowEnabled: Bool = true, highlightIntensity: Double = 0.5, placeholderAnimationEnabled: Bool = true ) -> some View { chromeBorder( shape: RoundedRectangle(cornerRadius: radius, style: .continuous), highlightEnabled: highlightEnabled, rimEnabled: rimEnabled, shadowEnabled: shadowEnabled, highlightIntensity: highlightIntensity, placeholderAnimationEnabled: placeholderAnimationEnabled ) } func chromeBorder( shape: BorderShape, highlightEnabled: Bool = true, rimEnabled: Bool = true, shadowEnabled: Bool = true, highlightIntensity: Double = 0.5, placeholderAnimationEnabled: Bool = true ) -> some View { modifier(ChromeBorderModifier( shape: shape, highlightEnabled: highlightEnabled, rimEnabled: rimEnabled, shadowEnabled: shadowEnabled, highlightIntensity: highlightIntensity, placeholderAnimationEnabled: placeholderAnimationEnabled )) } } private struct ChromeBorderModifier: ViewModifier { var shape: BorderShape var highlightEnabled = true var rimEnabled = true var shadowEnabled = true var highlightIntensity = 0.5 var placeholderAnimationEnabled = true @State private var animate = false @Environment(\.redactionReasons) private var redaction func body(content: Content) -> some View { content .shadow(color: .black.opacity(shadowEnabled ? 0.2 : 0), radius: 6, x: 0, y: 0) .shadow(color: .black.opacity(rimEnabled ? 0.5 : 0), radius: 1, x: 0, y: 0) .overlay { if highlightEnabled { ZStack { shape .strokeBorder(Color.white, lineWidth: 1) LinearGradient(colors: [.white.opacity(0.4), .white.opacity(0.7)], startPoint: .top, endPoint: .bottom) .blendMode(.destinationOut) } .compositingGroup() .blendMode(.plusLighter) .opacity(highlightIntensity) } } .overlay { if placeholderAnimationEnabled, !redaction.isEmpty { LinearGradient(colors: [.white.opacity(0), .white.opacity(0.5), .white.opacity(0.6), .white.opacity(0.5), .white.opacity(0)], startPoint: .leading, endPoint: .trailing) .scaleEffect(x: animate ? 1 : 2, anchor: .trailing) .scaleEffect(x: animate ? 2 : 1, anchor: .leading) .clipShape(shape) .blendMode(.plusLighter) .opacity(0.2) } } .task(id: placeholderAnimationEnabled && !redaction.isEmpty) { if placeholderAnimationEnabled, !redaction.isEmpty { withAnimation(.easeInOut(duration: 5).repeatForever()) { animate.toggle() } } } } } #if DEBUG @available(macOS 14.0, *) #Preview { @Previewable @State var isRedacted = true Button { } label: { CatalogGroupView(group: .placeholder) } .buttonStyle(CatalogGroupButtonStyle(isSelected: false)) .aspectRatio(CatalogGroupPicker.buttonAspectRatio, contentMode: .fit) .frame(width: 220) .padding(32) .redacted(reason: isRedacted ? [.placeholder] : []) .task { try? await Task.sleep(for: .seconds(3)) isRedacted = false } } #endif ================================================ FILE: VirtualUI/Source/Building Blocks/Configuration Controls/EphemeralTextField.swift ================================================ // // EphemeralTextField.swift // VirtualUI // // Created by Guilherme Rambo on 18/07/22. // import SwiftUI import VirtualCore private struct EphemeralTextFieldContentWidthKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } struct EphemeralTextField: View where StaticContent: View, EditableContent: View, Value: Equatable { @Binding var value: Value var staticContent: (Value) -> StaticContent var editableContent: (Binding) -> EditableContent var clamp: (Value) -> Value var validate: (Value) -> Bool var alignment: Alignment var setFocus: BoolSubject init(_ value: Binding, alignment: Alignment = .trailing, setFocus: BoolSubject = .init(), @ViewBuilder staticContent: @escaping (Value) -> StaticContent, @ViewBuilder editableContent: @escaping (Binding) -> EditableContent, clamp: @escaping (Value) -> Value = { $0 }, validate: @escaping (Value) -> Bool = { _ in true }) { self._value = value self._internalValue = .init(wrappedValue: value.wrappedValue) self.alignment = alignment self.setFocus = setFocus self.staticContent = staticContent self.editableContent = editableContent self.clamp = clamp self.validate = validate } @State private var internalValue: Value @Environment(\.isEnabled) private var isEnabled @Environment(\.unfocusActiveField) private var unfocus @FocusState private var isFocused: Bool @State private var isInEditMode = false @State private var contentWidth: CGFloat = 40 @State private var shakeOffset: CGFloat = 0 var body: some View { ZStack { staticContent(internalValue) .lineLimit(1) .contentShape(Rectangle()) .onTapGesture { beginEditing() } .onHover { isHovered = $0 } .opacity(isInEditMode ? 0 : 1) .background { GeometryReader { proxy in Color.clear .preference(key: EphemeralTextFieldContentWidthKey.self, value: proxy.size.width) } } .onPreferenceChange(EphemeralTextFieldContentWidthKey.self) { newValue in contentWidth = newValue } if isInEditMode { editableContent($internalValue) .focused($isFocused) .textFieldStyle(.plain) .frame(width: contentWidth, alignment: alignment) .onChange(of: value) { newValue in // Unfocus when changing the value externally. isFocused = false internalValue = newValue } .onSubmit { commit() } .onExitCommand { cancel() } .onReceive(unfocus) { action in switch action { case .commit: commit() case .cancel: cancel() } } .onChange(of: isFocused) { newValue in if !newValue { isInEditMode = false } } } } .monospacedDigit() .multilineTextAlignment(TextAlignment(alignment)) .padding(.vertical, 4) .padding(.horizontal, 8) .background(hoverBackground) .padding(.vertical, -4) .padding(.horizontal, -8) .offset(x: shakeOffset) .onChange(of: isInEditMode) { newValue in if newValue { internalValue = value isFocused = true } } .onChange(of: value) { newValue in internalValue = newValue } .onReceive(setFocus) { focus in guard focus != isInEditMode else { return } if focus { beginEditing() } else { cancel() } } } private func beginEditing() { guard isEnabled else { return } isInEditMode = true } private func commit() { guard validate(internalValue) else { self.internalValue = value shake() return } /// Update the external value with the edited value on submit, /// limiting to the allowed range. value = clamp(internalValue) internalValue = clamp(internalValue) isFocused = false } private func cancel() { /// Unfocus the field when pressing the escape key. isFocused = false /// Ugly hack alert! DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { /// Restore initial value when cancelling out of the field. internalValue = value } } private func shake() { withAnimation(.easeInOut(duration: 0.04).repeatCount(7, autoreverses: true)) { shakeOffset = -7 } /// Ugly hack alert (2)! DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { shakeOffset = 0 } } @State private var isHovered = false private var drawHoverBackground: Bool { isEnabled && (isHovered || isInEditMode) } @ViewBuilder private var hoverBackground: some View { RoundedRectangle(cornerRadius: 6, style: .continuous) .foregroundColor(.white.opacity(0.07)) .opacity(drawHoverBackground ? 1 : 0) .animation(.easeOut(duration: 0.24), value: isHovered) } } extension TextAlignment { init(_ alignment: Alignment) { switch alignment.horizontal { case .leading: self = .leading case .trailing: self = .trailing default: self = .center } } } #if DEBUG struct EphemeralTextField_Previews: PreviewProvider { static var previews: some View { _Template() } struct _Template: View { @State var text = "Hello, World" var body: some View { VStack { EphemeralTextField($text) { value in Text(value) } editableContent: { value in TextField("", text: value) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } } } #endif ================================================ FILE: VirtualUI/Source/Building Blocks/Configuration Controls/NumericPropertyControl.swift ================================================ import SwiftUI import Combine import VirtualCore struct NumericPropertyControl: View { @Binding var value: Value var range: ClosedRange var step: Value? = nil var hideSlider = false var label: String var formatter: F var unfocus = VoidSubject() var spacing: CGFloat = 2 var body: some View { VStack(alignment: .leading, spacing: spacing) { HStack { PropertyControlLabel(label) Spacer() NumericValueField( label: label, value: $value, range: range, formatter: formatter ) .textFieldStyle(.plain) .multilineTextAlignment(.trailing) .monospacedDigit() } if !hideSlider { Group { if let step { Slider(value: $value.sliderValue, in: range.sliderRange, step: Double(step), onEditingChanged: sliderEditingChanged) } else { Slider(value: $value.sliderValue, in: range.sliderRange, onEditingChanged: sliderEditingChanged) } } .controlSize(.mini) } } .transition(.asymmetric(insertion: .offset(x: 0, y: -40), removal: .offset(x: 0, y: 40)).combined(with: .opacity)) } private func sliderEditingChanged(_ isEditing: Bool) { guard isEditing else { return } unfocus.send() } } extension NumberFormatter { static let numericPropertyControlDefault: NumberFormatter = { let f = NumberFormatter() f.numberStyle = .decimal f.maximumFractionDigits = 0 f.minimumFractionDigits = 0 f.hasThousandSeparators = false return f }() } #if DEBUG struct PropertySlider_Previews: PreviewProvider { static var previews: some View { _Template() } struct _Template: View { @State private var value = 1 var body: some View { NumericPropertyControl(value: $value, range: 0...10, step: 1, hideSlider: false, label: "Preview", formatter: NumberFormatter.numericPropertyControlDefault) .padding() .frame(maxWidth: 200) } } } #endif ================================================ FILE: VirtualUI/Source/Building Blocks/Configuration Controls/NumericValueField.swift ================================================ import SwiftUI import Combine import VirtualCore struct NumericValueField: View { var label: String @Binding var value: Value var range: ClosedRange var formatter: F @Environment(\.unfocusActiveField) private var unfocus init(label: String, value: Binding, range: ClosedRange, formatter: F) { self.label = label self._value = value self.range = range self.formatter = formatter } @FocusState private var isFocused: Bool @State private var isInEditMode = false var body: some View { EphemeralTextField($value) { currentValue in Text(formatter.string(for: currentValue) ?? "") } editableContent: { binding in TextField(label, value: binding, formatter: formatter) } clamp: { $0.limited(to: range) } } } extension BinaryInteger { func limited(to range: ClosedRange) -> Self { Swift.min(range.upperBound, Swift.max(range.lowerBound, self)) } } #if DEBUG struct NumericValueField_Previews: PreviewProvider { static var previews: some View { _Template() } struct _Template: View { @State var value = 1 @Environment(\.unfocusActiveField) private var unfocusActiveField private let formatter: NumberFormatter = { let f = NumberFormatter() f.numberStyle = .decimal f.maximumFractionDigits = 0 f.minimumFractionDigits = 0 f.hasThousandSeparators = false return f }() var body: some View { VStack { NumericValueField( label: "Test", value: $value, range: 1...10, formatter: formatter ) Button("Unfocus") { unfocusActiveField.send(.cancel) } .controlSize(.small) } .padding() } } } #endif ================================================ FILE: VirtualUI/Source/Building Blocks/Configuration Controls/PropertyControl.swift ================================================ // // PropertyControl.swift // VirtualUI // // Created by Guilherme Rambo on 18/07/22. // import SwiftUI struct PropertyControl: View { var label: String var spacing: CGFloat var content: () -> Content init(_ label: String, spacing: CGFloat = 4, @ViewBuilder content: @escaping () -> Content) { self.label = label self.spacing = spacing self.content = content } var body: some View { VStack(alignment: .leading, spacing: spacing) { PropertyControlLabel(label) content().labelsHidden() } } } struct PropertyControlLabel: View { init(_ title: String) { self.title = title } var title: String var body: some View { Text(title) .foregroundColor(.white.opacity(0.7)) .blendMode(.plusLighter) } } ================================================ FILE: VirtualUI/Source/Building Blocks/Configuration Controls/SharedFocusEnvironment.swift ================================================ import SwiftUI import Combine enum UnfocusFieldAction { case commit case cancel } typealias UnfocusFieldSubject = PassthroughSubject extension EnvironmentValues { var unfocusActiveField: UnfocusFieldSubject { get { self[UnfocusActiveFieldEnvironmentKey.self] } set { self[UnfocusActiveFieldEnvironmentKey.self] = newValue } } } extension View { func unfocusOnTap() -> some View { modifier(UnfocusOnTap()) } } private struct UnfocusOnTap: ViewModifier { @Environment(\.unfocusActiveField) private var unfocus func body(content: Content) -> some View { content .onTapGesture { unfocus.send(.commit) } } } private struct UnfocusActiveFieldEnvironmentKey: EnvironmentKey { static var defaultValue = UnfocusFieldSubject() } ================================================ FILE: VirtualUI/Source/Building Blocks/ControlGroupChrome.swift ================================================ import SwiftUI public enum ControlGroupLevel: Int { case primary case secondary } public struct ControlGroupMetrics { static let primaryGroupRadius: Double = 14 static let secondaryGroupRadius: Double = 8 } public extension ControlGroupLevel { var cornerRadius: Double { switch self { case .primary: ControlGroupMetrics.primaryGroupRadius case .secondary: ControlGroupMetrics.secondaryGroupRadius } } } public extension View { func controlGroup(level: ControlGroupLevel = .primary) -> some View { modifier(ControlGroupChrome(level: level, shapeBuilder: { RoundedRectangle(cornerRadius: level.cornerRadius, style: .continuous) })) } func controlGroup(_ shape: S, level: ControlGroupLevel = .primary) -> some View where S: InsettableShape { modifier(ControlGroupChrome(level: level, shapeBuilder: { shape })) } } private struct ControlGroupChrome: ViewModifier { @Environment(\.colorScheme) private var colorScheme var level: ControlGroupLevel var shapeBuilder: () -> Shape var dark: Bool { colorScheme == .dark } private var innerRimOpacity: Double { switch level { case .primary: return dark ? 0.15 : 0 case .secondary: return dark ? 0.1 : 0 } } private var outerRimOpacity: Double { switch level { case .primary: return dark ? 0.5 : 0.8 case .secondary: return dark ? 0.4 : 0.7 } } private var shadowOpacity: Double { switch level { case .primary: return dark ? 0.2 : 0.1 case .secondary: return dark ? 0.1 : 0.05 } } private var material: Material { switch level { case .primary: return .thin case .secondary: return .regular } } func body(content: Content) -> some View { content .background(material, in: shape) .clipShape(shape) .shadow(color: Color.black.opacity(outerRimOpacity), radius: 1, x: 0, y: 0) .shadow(color: Color.black.opacity(shadowOpacity), radius: dark ? 8 : 10, x: 0, y: 0) .chromeBorder(shape: shape, highlightEnabled: true, rimEnabled: false, shadowEnabled: false, highlightIntensity: innerRimOpacity) .unfocusOnTap() .containerShape(shape) } private var shape: Shape { shapeBuilder() } } #if DEBUG struct ControlGroupChrome_Previews: PreviewProvider { static var previews: some View { VStack { Text("Primary Group") VStack { Text("Secondary Group") } .padding() .controlGroup(level: .secondary) } .padding(30) .controlGroup() .padding(50) } } #endif ================================================ FILE: VirtualUI/Source/Building Blocks/SliderConversion.swift ================================================ // // SliderConversion.swift // VirtualUI // // Created by Guilherme Rambo on 17/07/22. // import SwiftUI extension Binding where Value: BinaryInteger { var sliderValue: Binding { .init { Double(wrappedValue) } set: { wrappedValue = Value($0) } } } extension ClosedRange where Bound: BinaryInteger { var sliderRange: ClosedRange { Double(lowerBound)...Double(upperBound) } } extension Binding where Value == UInt64 { var gbValue: Binding { .init { Int(wrappedValue / 1024 / 1024 / 1024) } set: { wrappedValue = Value($0 * 1024 * 1024 * 1024) } } var gbStorageValue: Binding { .init { Int(wrappedValue / .storageGigabyte) } set: { wrappedValue = Value(UInt64($0) * .storageGigabyte) } } } extension UInt64 { var gbStorageValue: Int { Int(self / .storageGigabyte) } } ================================================ FILE: VirtualUI/Source/Components/Array+Navigation.swift ================================================ import Foundation extension Array where Element: Identifiable { func next(from selection: Element?) -> Element? { guard let index = self.lastIndex(where: { $0.id == selection?.id }) else { return nil } let nextIndex = index + 1 guard nextIndex < count else { return nil } return self[nextIndex] } func previous(from selection: Element?) -> Element? { guard let index = self.firstIndex(where: { $0.id == selection?.id }) else { return nil } let previousIndex = index - 1 guard previousIndex >= 0 else { return nil } return self[previousIndex] } } ================================================ FILE: VirtualUI/Source/Components/BackportedContentUnavailableView.swift ================================================ import SwiftUI struct BackportedContentUnavailableView: View { var title: LocalizedStringKey var image: Image var description: Text? @ViewBuilder var actions: () -> Actions init(_ title: LocalizedStringKey, image: Image, description: Text? = nil, @ViewBuilder actions: @escaping () -> Actions) { self.title = title self.image = image self.description = description self.actions = actions } init(_ title: LocalizedStringKey, systemImage: String, description: Text? = nil, @ViewBuilder actions: @escaping () -> Actions) { self.init(title, image: Image(systemName: systemImage), description: description, actions: actions) } var body: some View { VStack(spacing: 16) { VStack(spacing: 6) { image .imageScale(.large) .symbolVariant(.fill) Text(title) } .font(.system(.title, design: .rounded, weight: .medium)) .foregroundStyle(.secondary) description .foregroundStyle(.secondary) .multilineTextAlignment(.center) .font(.system(.title3, weight: .regular)) if Actions.self != EmptyView.self { EqualWidthHStack { actions() } .controlSize(.large) .buttonStyle(.backportedContentUnavailableAction) } } .textSelection(.enabled) .frame(maxWidth: 520) } } extension BackportedContentUnavailableView where Actions == EmptyView { init(_ title: LocalizedStringKey, image: Image, description: Text? = nil) { self.title = title self.image = image self.description = description self.actions = { EmptyView() } } init(_ title: LocalizedStringKey, systemImage: String, description: Text? = nil) { self.init(title, image: Image(systemName: systemImage), description: description) } } private struct BackportedContentUnavailableActionButtonStyle: PrimitiveButtonStyle { func makeBody(configuration: Configuration) -> some View { Button(role: configuration.role) { configuration.trigger() } label: { configuration.label .frame(maxWidth: .infinity) } } } private extension PrimitiveButtonStyle where Self == BackportedContentUnavailableActionButtonStyle { static var backportedContentUnavailableAction: BackportedContentUnavailableActionButtonStyle { BackportedContentUnavailableActionButtonStyle() } } #if DEBUG #Preview { BackportedContentUnavailableView( "Hello World", systemImage: "globe", description: Text(""" Something that should be here is not here. Because that something that should be here is not here, you're seeing this message. """) ) { Button("Primary Action") { } .keyboardShortcut(.defaultAction) Button("Other Action") { } } .frame(minWidth: 500, minHeight: 300) .padding(32) } #endif ================================================ FILE: VirtualUI/Source/Components/BlurHash/BlurHashDecoding.swift ================================================ /** Copyright (c) 2018 Wolt Enterprises Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import SwiftUI public extension CGSize { /// The size of blur hash used by VirtualBuddy. /// /// - warning: This can't be changed without updating the server-side catalog groups with updated blur hashes matching this size! static let vbBlurHashSize = CGSize(width: Int.vbBlurHashSize, height: Int.vbBlurHashSize) } public extension Float { /// Default `punch` value used for blur hashes in VirtualBuddy. static let vbBlurHashPunch: Float = 1 } public extension Image { init(blurHash: String, size: CGSize = .vbBlurHashSize, punch: Float = .vbBlurHashPunch) { if let decodedImage = NSImage(blurHash: blurHash, size: size, punch: punch) { self.init(nsImage: decodedImage) } else { self.init(nsImage: .blurHashPlaceholder(size: size)) } } init(blurHash: BlurHashToken, punch: Float = .vbBlurHashPunch) { self.init( blurHash: blurHash.value, size: CGSize(width: blurHash.size, height: blurHash.size), punch: punch ) } } private extension NSImage { static func blurHashPlaceholder(size: CGSize) -> NSImage { NSImage(size: size, flipped: true) { rect in NSColor.quaternaryLabelColor.setFill() rect.fill() return true } } } public extension NSImage { static func blurHash(_ token: BlurHashToken) -> NSImage { if let image = NSImage(blurHash: token.value, size: CGSize(width: token.size, height: token.size)) { image } else { .blurHashPlaceholder(size: CGSize(width: token.size, height: token.size)) } } convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { guard blurHash.count >= 6 else { return nil } let sizeFlag = String(blurHash[0]).decode83() let numY = (sizeFlag / 9) + 1 let numX = (sizeFlag % 9) + 1 let quantisedMaximumValue = String(blurHash[1]).decode83() let maximumValue = Float(quantisedMaximumValue + 1) / 166 guard blurHash.count == 4 + 2 * numX * numY else { return nil } let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in if i == 0 { let value = String(blurHash[2 ..< 6]).decode83() return decodeDC(value) } else { let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83() return decodeAC(value, maximumValue: maximumValue * punch) } } let width = Int(size.width) let height = Int(size.height) let bytesPerRow = width * 3 guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } CFDataSetLength(data, bytesPerRow * height) guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } for y in 0 ..< height { for x in 0 ..< width { var r: Float = 0 var g: Float = 0 var b: Float = 0 for j in 0 ..< numY { for i in 0 ..< numX { let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) let colour = colours[i + j * numX] r += colour.0 * basis g += colour.1 * basis b += colour.2 * basis } } let intR = UInt8(linearTosRGB(r)) let intG = UInt8(linearTosRGB(g)) let intB = UInt8(linearTosRGB(b)) pixels[3 * x + 0 + y * bytesPerRow] = intR pixels[3 * x + 1 + y * bytesPerRow] = intG pixels[3 * x + 2 + y * bytesPerRow] = intB } } let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) guard let provider = CGDataProvider(data: data) else { return nil } guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil } self.init(cgImage: cgImage, size: size) } } private func decodeDC(_ value: Int) -> (Float, Float, Float) { let intR = value >> 16 let intG = (value >> 8) & 255 let intB = value & 255 return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) } private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { let quantR = value / (19 * 19) let quantG = (value / 19) % 19 let quantB = value % 19 let rgb = ( signPow((Float(quantR) - 9) / 9, 2) * maximumValue, signPow((Float(quantG) - 9) / 9, 2) * maximumValue, signPow((Float(quantB) - 9) / 9, 2) * maximumValue ) return rgb } private func signPow(_ value: Float, _ exp: Float) -> Float { return copysign(pow(abs(value), exp), value) } private func linearTosRGB(_ value: Float) -> Int { let v = max(0, min(1, value)) if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } } private func sRGBToLinear(_ value: Type) -> Float { let v = Float(Int64(value)) / 255 if v <= 0.04045 { return v / 12.92 } else { return pow((v + 0.055) / 1.055, 2.4) } } private let encodeCharacters: [String] = { return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } }() private let decodeCharacters: [String: Int] = { var dict: [String: Int] = [:] for (index, character) in encodeCharacters.enumerated() { dict[character] = index } return dict }() private extension String { func decode83() -> Int { var value: Int = 0 for character in self { if let digit = decodeCharacters[String(character)] { value = value * 83 + digit } } return value } } private extension String { subscript (offset: Int) -> Character { return self[index(startIndex, offsetBy: offset)] } subscript (bounds: CountableClosedRange) -> Substring { let start = index(startIndex, offsetBy: bounds.lowerBound) let end = index(startIndex, offsetBy: bounds.upperBound) return self[start...end] } subscript (bounds: CountableRange) -> Substring { let start = index(startIndex, offsetBy: bounds.lowerBound) let end = index(startIndex, offsetBy: bounds.upperBound) return self[start.. CALayer? { guard let asset = NSDataAsset(name: assetName, bundle: bundle) else { assertionFailure("Asset not found") log.fault("Missing asset \(assetName, privacy: .public)") return nil } do { let unarchiver = try NSKeyedUnarchiver(forReadingFrom: asset.data) unarchiver.requiresSecureCoding = false let rootObject = unarchiver.decodeObject(forKey: NSKeyedArchiveRootObjectKey) guard let dictionary = rootObject as? NSDictionary else { assertionFailure("Failed to load asset") log.fault("Failed to load asset \(assetName, privacy: .public)") return nil } guard let layer = dictionary["rootLayer"] as? CALayer else { assertionFailure("Root layer not found") log.fault("Failed to load root layer from asset \(assetName, privacy: .public)") return nil } return layer } catch { assertionFailure(String(describing: error)) log.fault("Unarchive failed: \(String(describing: error), privacy: .public)") return nil } } func sublayer(named name: String, of type: T.Type = CALayer.self) -> T? { return sublayers?.first(where: { $0.name == name }) as? T } func sublayer(path: String, of type: T.Type = CALayer.self) -> T? { let components = path.components(separatedBy: ".") var target: CALayer? = self for component in components { target = target?.sublayer(named: component, of: CALayer.self) } return target as? T } } extension CALayer { static func resize(_ targetLayer: CALayer?, within containerBounds: CGRect, multiplier: CGFloat = 1, offset: CGPoint = .zero, gravity: CALayerContentsGravity = .resizeAspect, resetPosition: Bool = false, disableAnimations: Bool = false) { assert([.resizeAspect, .resizeAspectFill, .resize, .center].contains(gravity), "Unsupported layer gravity") if disableAnimations { CATransaction.begin() CATransaction.setDisableActions(true) CATransaction.setAnimationDuration(0) } defer { if disableAnimations { CATransaction.commit() } } let fn: (CGFloat, CGFloat) -> CGFloat = gravity == .resizeAspectFill ? max : min guard let targetLayer = targetLayer else { return } let layerWidth = targetLayer.bounds.width let layerHeight = targetLayer.bounds.height let aspectWidth = containerBounds.width / layerWidth let aspectHeight = containerBounds.height / layerHeight let ratioWidth: CGFloat let ratioHeight: CGFloat switch gravity { case .resize: ratioWidth = aspectWidth * multiplier ratioHeight = aspectHeight * multiplier case .center: ratioWidth = multiplier ratioHeight = multiplier default: ratioWidth = fn(aspectWidth, aspectHeight) * multiplier ratioHeight = fn(aspectWidth, aspectHeight) * multiplier } let scale = CATransform3DMakeScale(ratioWidth, ratioHeight, 1) let translation = CATransform3DMakeTranslation((containerBounds.width - (layerWidth * ratioWidth)) / 2.0 - offset.x, (containerBounds.height - (layerHeight * ratioHeight)) / 2.0 - offset.y, 0) if resetPosition { targetLayer.anchorPoint = .zero targetLayer.position = .zero } targetLayer.transform = CATransform3DConcat(scale, translation) } } ================================================ FILE: VirtualUI/Source/Components/DecentFormView.swift ================================================ // // AirFormView.swift // AirUI // // Created by Guilherme Rambo on 30/05/22. // Copyright © 2022 Guilherme Rambo. All rights reserved. // import SwiftUI /// A form that aligns labels and controls in a way consistent with AirBuddy's preferences UI. /// Contents should be instances of `AirFormControl`. public struct DecentFormView: View where Content: View { var content: () -> Content var spacing: CGFloat? public init(spacing: CGFloat? = nil, @ViewBuilder content: @escaping () -> Content) { self.content = content self.spacing = spacing } public var body: some View { VStack(alignment: .customLabelAlignmentGuideHorizontal, spacing: spacing) { content() } .labelsHidden() } } /// Encapsulates a control and a label to be displayed within an `AirFormView`. /// The view can be flipped so that the label is shown to the right of the control, such as AirBuddy does with big switches in its settings UI. public struct DecentFormControl: View where ContentA: View, ContentB: View { var alignment: VerticalAlignment var flipped: Bool var leading: () -> ContentA var trailing: () -> ContentB public init(alignment: VerticalAlignment = .firstTextBaseline, flipped: Bool = false, @ViewBuilder control: @escaping () -> ContentB, @ViewBuilder label: @escaping () -> ContentA) { self.alignment = alignment self.flipped = flipped self.leading = label self.trailing = control } private var leadingContent: some View { leading() .alignedLabel(alignment) } private var trailingContent: some View { trailing() .alignedLabel(alignment) } public var body: some View { HStack(alignment: .customLabelAlignmentGuide) { Group { if flipped { trailingContent } else { leadingContent } } .trailingAlignedLabel() if flipped { leadingContent } else { trailingContent } } } } /// A placeholder view that does not show any content on screen and can be used /// as the label in an `AirFormControl` where alignment of the control is desired, /// but no label should be shown. public struct AirFormHiddenLabel: View { public init() { } public var body: some View { Text(String("_")) .opacity(0) .accessibilityHidden(true) } } public extension DecentFormControl where ContentA == AirFormHiddenLabel { init(alignment: VerticalAlignment = .firstTextBaseline, @ViewBuilder control: @escaping () -> ContentB) { self.init(alignment: alignment, flipped: false, control: control, label: { AirFormHiddenLabel() }) } } public extension View { func alignedLabel(_ alignment: VerticalAlignment = .firstTextBaseline) -> some View { alignmentGuide(.customLabelAlignmentGuide, computeValue: { $0[alignment] }) } func trailingAlignedLabel() -> some View { alignmentGuide(.customLabelAlignmentGuideHorizontal, computeValue: { $0[.trailing] }) } } public extension VerticalAlignment { private struct CustomLabelAlignment: AlignmentID { static func defaultValue(in context: ViewDimensions) -> CGFloat { context[VerticalAlignment.bottom] } } static let customLabelAlignmentGuide = VerticalAlignment( CustomLabelAlignment.self ) } public extension HorizontalAlignment { private struct CustomLabelAlignmentHorizontal: AlignmentID { static func defaultValue(in context: ViewDimensions) -> CGFloat { context[HorizontalAlignment.trailing] } } static let customLabelAlignmentGuideHorizontal = HorizontalAlignment( CustomLabelAlignmentHorizontal.self ) } ================================================ FILE: VirtualUI/Source/Components/EqualWidthHStack.swift ================================================ /* Copyright © 2022 Apple Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import SwiftUI /// A custom horizontal stack that offers all its subviews the width of its /// widest subview. /// /// This custom layout arranges views horizontally, giving each the width needed /// by the widest subview. /// /// ![Three rectangles arranged in a horizontal line. Each rectangle contains /// one smaller rectangle. The smaller rectangles have varying widths. Dashed /// lines above each of the container rectangles show that the larger rectangles /// all have the same width as each other.](voting-buttons) /// /// The custom stack implements the protocol's two required methods. First, /// ``sizeThatFits(proposal:subviews:cache:)`` reports the container's size, /// given a set of subviews. /// /// ```swift /// let maxSize = maxSize(subviews: subviews) /// let spacing = spacing(subviews: subviews) /// let totalSpacing = spacing.reduce(0) { $0 + $1 } /// /// return CGSize( /// width: maxSize.width * CGFloat(subviews.count) + totalSpacing, /// height: maxSize.height) /// ``` /// /// This method combines the largest size in each dimension with the horizontal /// spacing between subviews to find the container's total size. Then, /// ``placeSubviews(in:proposal:subviews:cache:)`` tells each of the subviews /// where to appear within the layout's bounds. /// /// ```swift /// let maxSize = maxSize(subviews: subviews) /// let spacing = spacing(subviews: subviews) /// /// let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height) /// var nextX = bounds.minX + maxSize.width / 2 /// /// for index in subviews.indices { /// subviews[index].place( /// at: CGPoint(x: nextX, y: bounds.midY), /// anchor: .center, /// proposal: placementProposal) /// nextX += maxSize.width + spacing[index] /// } /// ``` /// /// The method creates a single size proposal for the subviews, and then uses /// that, along with a point that changes for each subview, to arrange the /// subviews in a horizontal line with default spacing. struct EqualWidthHStack: Layout { /// Returns a size that the layout container needs to arrange its subviews /// horizontally. /// - Tag: sizeThatFitsHorizontal func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) -> CGSize { guard !subviews.isEmpty else { return .zero } let maxSize = maxSize(subviews: subviews) let spacing = spacing(subviews: subviews) let totalSpacing = spacing.reduce(0) { $0 + $1 } return CGSize( width: maxSize.width * CGFloat(subviews.count) + totalSpacing, height: maxSize.height) } /// Places the subviews in a horizontal stack. /// - Tag: placeSubviewsHorizontal func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { guard !subviews.isEmpty else { return } let maxSize = maxSize(subviews: subviews) let spacing = spacing(subviews: subviews) let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height) var nextX = bounds.minX + maxSize.width / 2 for index in subviews.indices { subviews[index].place( at: CGPoint(x: nextX, y: bounds.midY), anchor: .center, proposal: placementProposal) nextX += maxSize.width + spacing[index] } } /// Finds the largest ideal size of the subviews. private func maxSize(subviews: Subviews) -> CGSize { let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) } let maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in CGSize( width: max(currentMax.width, subviewSize.width), height: max(currentMax.height, subviewSize.height)) } return maxSize } /// Gets an array of preferred spacing sizes between subviews in the /// horizontal dimension. private func spacing(subviews: Subviews) -> [CGFloat] { subviews.indices.map { index in guard index < subviews.count - 1 else { return 0 } return subviews[index].spacing.distance( to: subviews[index + 1].spacing, along: .horizontal) } } } ================================================ FILE: VirtualUI/Source/Components/HostingWindowController/FB18383725Window.swift ================================================ // // FB18383725Window.swift // VirtualBuddy // // Created by Guilherme Rambo on 25/6/25. // import SwiftUI import Combine import BuddyKit /// Implements a workaround for a crash that occurs in macOS 26 when built with the macOS 26 SDK. class FB18383725Window: NSWindow { private var toolbarToRestoreAfterFullScreenTransition: NSToolbar? private var cancellables = Set() override func toggleFullScreen(_ sender: Any?) { guard #available(macOS 26, *) else { return super.toggleFullScreen(sender) } /** Filed as `FB18383725`. Really ugly hack to work around a crash in macOS 26 when built with the macOS 26 SDK. The crash occurs when the window content is an `NSHostingController` and the root view has a `toolbar` modifier: ``` *** Terminating app due to uncaught exception 'NSGenericException', reason: 'The window has been marked as needing another Layout Window pass, but it has already had more Layout Window passes than there are views in the window. 0x20d8 (8408) {{0, -180}, {1512, 1077}} en' terminating due to uncaught exception of type NSException */ /// Grab the current toolbar, then remove it from the window. toolbarToRestoreAfterFullScreenTransition = toolbar toolbar = nil /// Trigger full screen toggle. super.toggleFullScreen(sender) /// When window finishes entering or exiting full screen, set its toolbar back to the previous value. guard cancellables.isEmpty else { return } NotificationCenter.default .publisher(for: NSWindow.didEnterFullScreenNotification, object: self) .sink { [weak self] _ in self?.restoreToolbarAfterFullScreenTransitionIfNeeded() } .store(in: &cancellables) NotificationCenter.default .publisher(for: NSWindow.didExitFullScreenNotification, object: self) .sink { [weak self] _ in self?.restoreToolbarAfterFullScreenTransitionIfNeeded() } .store(in: &cancellables) } private func restoreToolbarAfterFullScreenTransitionIfNeeded() { guard #available(macOS 26, *) else { return } UILog(#function) toolbar = toolbarToRestoreAfterFullScreenTransition } } ================================================ FILE: VirtualUI/Source/Components/HostingWindowController/HostingWindowController.swift ================================================ import Cocoa import SwiftUI import VirtualCore let defaultHostingWindowStyleMask: NSWindow.StyleMask = [.titled, .miniaturizable, .resizable, .closable, .fullSizeContentView] public final class HostingWindowController: NSWindowController, NSWindowDelegate where Content: View { public typealias WindowCloseBlock = ((HostingWindowController) -> Void) /// Invoked shortly before the hosting window controller's window is closed. private var onWindowClose: WindowCloseBlock? private static func makeDefaultWindow() -> NSWindow { HostingWindow( contentRect: NSRect(x: 0, y: 0, width: NSView.noIntrinsicMetric, height: NSView.noIntrinsicMetric), styleMask: defaultHostingWindowStyleMask, backing: .buffered, defer: false, screen: nil ) } public init(id: String? = nil, rootView: Content, windowFactory: (() -> NSWindow)? = nil, onWindowClose: WindowCloseBlock? = nil) { let window = windowFactory?() ?? Self.makeDefaultWindow() if let id { window.identifier = .init(id) } super.init(window: window) let controller = NSHostingController( rootView: rootView .environment(\.closeWindow, { [weak self] in self?.close() }) .environment(\.cocoaWindow, window) ) contentViewController = controller window.setContentSize(controller.view.fittingSize) window.isReleasedWhenClosed = false window.delegate = self self.onWindowClose = onWindowClose } public required init?(coder: NSCoder) { fatalError() } public override func showWindow(_ sender: Any?) { super.showWindow(sender) window?.center() } public func windowWillClose(_ notification: Notification) { contentViewController = nil onWindowClose?(self) /// Ensures references in the closure are not retained past the lifetime of the window. /// Opening a new hosting window controller requires going through ``OpenCocoaWindowAction``, /// which sets up the callback again if needed. onWindowClose = nil } var viewRendersWindowChrome: Bool = false { didSet { guard viewRendersWindowChrome != oldValue else { return } DispatchQueue.main.async { self.configureChrome() } } } private func configureChrome() { if viewRendersWindowChrome { window?.styleMask.remove(.titled) window?.styleMask.insert(.borderless) window?.backgroundColor = .clear window?.isOpaque = false } else { window?.styleMask.remove(.borderless) window?.styleMask.insert(.titled) window?.backgroundColor = .windowBackgroundColor window?.isOpaque = true } } var onWindowOcclusionStateChanged: ((NSWindow.OcclusionState) -> Void)? = nil public func windowDidChangeOcclusionState(_ notification: Notification) { guard let state = window?.occlusionState else { return } onWindowOcclusionStateChanged?(state) } public func window(_ window: NSWindow, willUseFullScreenPresentationOptions proposedOptions: NSApplication.PresentationOptions = []) -> NSApplication.PresentationOptions { [ .autoHideDock, .autoHideMenuBar, .autoHideToolbar, .fullScreen ] } var confirmBeforeClosingCallback: () async -> Bool { get { guard let hostingWindow = window as? HostingWindow else { preconditionFailure("confirmBeforeClosing can't be used with custom window types in HostingWindowController") } return hostingWindow.confirmBeforeClosingCallback } set { precondition(window is HostingWindow, "confirmBeforeClosing can't be used with custom window types in HostingWindowController") (window as? HostingWindow)?.confirmBeforeClosingCallback = newValue } } } extension HostingWindowController: WindowChromeConsumer { } protocol WindowChromeConsumer: AnyObject { var viewRendersWindowChrome: Bool { get set } var onWindowOcclusionStateChanged: ((NSWindow.OcclusionState) -> Void)? { get set } var confirmBeforeClosingCallback: () async -> Bool { get set } } fileprivate final class HostingWindow: VBRestorableWindow { override var canBecomeKey: Bool { true } override var canBecomeMain: Bool { true } override var acceptsFirstResponder: Bool { true } var confirmBeforeClosingCallback: () async -> Bool = { true } var enableMemoryLeakAssertion = true override func performClose(_ sender: Any?) { /// This addresses a weird issue introduced after #257 where for some reason `VMController` /// was being retained by a SwiftUI button when closing a VM window using Command+W /// after opening multiple VM windows. /// The button that was retaining the controller though a closure context was one of /// the toolbar buttons, and for some reason not going directly through our `close()` /// implementation here was triggering the leak :( close() } override func close() { Task { @MainActor in guard await confirmBeforeClosingCallback() else { return } await MainActor.run { closeWithoutConfirmation() } } } private func closeWithoutConfirmation() { super.close() VBMemoryLeakDebugAssertions.vb_objectShouldBeReleasedSoon(self) } deinit { VBMemoryLeakDebugAssertions.vb_objectIsBeingReleased(self) } } ================================================ FILE: VirtualUI/Source/Components/HostingWindowController/OpenCocoaWindowAction.swift ================================================ import SwiftUI import Combine public extension EnvironmentValues { /// Call to open a SwiftUI view hierarchy in a new macOS window. /// The SwiftUI view defines the size and resizability of the window, use SwiftUI's `frame` /// modifier to customize the behavior. /// /// See ``OpenCocoaWindowAction`` for more information. fileprivate(set) var openCocoaWindow: OpenCocoaWindowAction { get { self[OpenCocoaWindowActionKey.self] } set { self[OpenCocoaWindowActionKey.self] = newValue } } } fileprivate struct OpenCocoaWindowActionKey: EnvironmentKey { static let defaultValue = OpenCocoaWindowAction.default } /// An action that opens a new Mac window with SwiftUI content. /// /// Read the `openCocoaWindow` environment value to get an instance of this structure for a given Environment. /// Call the instance to open a new window with the SwiftUI content provided. You call the instance directly because it defines a ``callAsFunction(_:)`` /// method that Swift calls when you call the instance. /// /// ## Example: /// /// ```swift /// struct TestNewWindowView: View { /// /// @Environment(\.openCocoaWindow) private var openWindow /// /// var body: some View { /// Button { /// openWindow { /// Text("I'm in a new window") /// .frame(minWidth: 200, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity) /// } /// } label: { /// Text("Open New Window") /// } /// } /// /// } /// ``` public struct OpenCocoaWindowAction { public static let `default` = OpenCocoaWindowAction() /// A token represents a window that's been opened by ``OpenCocoaWindowAction``. /// /// You may use the token to close the window from outside the view hierarchy hosted within it, such /// as from the SwiftUI view hierarchy that created the window in the first place. /// /// ``Token`` is an `ObservableObject`, so your view may observe it in order to be notified of /// when the window has been closed via the ``isOpen`` published property. public final class Token: Hashable, ObservableObject { private var id: String init(id: String) { self.id = id } fileprivate lazy var cancellables = Set() fileprivate func invalidate() { cancellables.removeAll() } /// `true` when the window represented by this token has been opened and hasn't yet been closed /// by either the user or your code. @Published public internal(set) var isOpen = false /// Closes the window that was opened using ``OpenCocoaWindowAction``. @MainActor public func close() { OpenCocoaWindowManager.shared[self]?.close() } } @MainActor private var manager: OpenCocoaWindowManager { .shared } /// Opens a new Mac window with the SwiftUI content provided. /// - Parameter content: A view builder that provides the contents of the window. /// - Returns: A ``Token`` that can be used to monitor or modify the state of the new window. /// /// Don’t call this method directly. Swift calls it when you call the ``OpenCocoaWindowAction`` /// structure that you get from the Environment. @MainActor @discardableResult public func callAsFunction(id: String? = nil, animationBehavior: NSWindow.AnimationBehavior? = nil, @ViewBuilder _ content: @escaping () -> Content, onClose: (() -> Void)? = nil) -> Token where Content: View { let token = Token(id: id ?? UUID().uuidString) if let existingController = manager[token] { existingController.showWindow(nil) } else { let controller = HostingWindowController(id: id, rootView: content(), onWindowClose: { _ in onClose?() }) if let animationBehavior { controller.window?.animationBehavior = animationBehavior } manager[token] = controller } return token } /// Closes a window previously opened through ``OpenCocoaWindowAction``. /// - Parameter token: The token returned from calling ``OpenCocoaWindowAction`` as a function. /// /// You may also just call ``Token/close()`` instead on your token, which has the same effect. @MainActor public func close(_ token: Token) { OpenCocoaWindowManager.shared[token]?.close() } @MainActor public func close(id: String) { close(Token(id: id)) } @MainActor public func hasWindow(for id: String) -> Bool { OpenCocoaWindowManager.shared[Token(id: id)] != nil } } // MARK: - Window Manager @MainActor private final class OpenCocoaWindowManager { static let shared = OpenCocoaWindowManager() private lazy var windowControllers = [OpenCocoaWindowAction.Token: NSWindowController]() subscript(_ token: OpenCocoaWindowAction.Token) -> NSWindowController? { get { windowControllers[token] } set { windowControllers[token] = newValue if let newValue = newValue { openWindow(for: newValue, token: token) } } } private func openWindow(for controller: NSWindowController, token: OpenCocoaWindowAction.Token) { controller.window?.isReleasedWhenClosed = true controller.showWindow(nil) setupObservations(for: controller, token: token) } private func setupObservations(for controller: NSWindowController, token: OpenCocoaWindowAction.Token) { guard let window = controller.window else { return } NotificationCenter.default .publisher(for: NSWindow.willCloseNotification, object: window) .sink { [weak self] note in guard let self = self else { return } guard let window = note.object as? NSWindow else { return } self.handleWindowWillClose(window, token: token) } .store(in: &token.cancellables) } private func handleWindowWillClose(_ window: NSWindow, token: OpenCocoaWindowAction.Token) { token.isOpen = false windowControllers[token] = nil } } // MARK: - Token conformances public extension OpenCocoaWindowAction.Token { func hash(into hasher: inout Hasher) { hasher.combine(id) } static func == (lhs: OpenCocoaWindowAction.Token, rhs: OpenCocoaWindowAction.Token) -> Bool { lhs.id == rhs.id } } ================================================ FILE: VirtualUI/Source/Components/HostingWindowController/VBRestorableWindow+Resizing.swift ================================================ // // VBRestorableWindow+Resizing.swift // VirtualUI // // Created by Guilherme Rambo on 09/03/23. // import Cocoa import AVFoundation import VirtualCore extension VBRestorableWindow { func resize(to size: VirtualMachineSessionUI.WindowSize, for display: VBDisplayDevice) { guard let screen else { return } let targetWidth: CGFloat let targetHeight: CGFloat let displaySize = CGSize(width: display.width, height: display.height) let toolbarHeight: CGFloat = frameRect(forContentRect: NSRect()).height // screen.visibleFrame.height is a "net" value after taking into account menu bar, dock, etc. let availableHeight: CGFloat = screen.visibleFrame.height - toolbarHeight switch size { case .pointAccurate: targetWidth = CGFloat(display.width) targetHeight = CGFloat(display.height) case .pixelAccurate: targetWidth = CGFloat(display.width) / screen.backingScaleFactor targetHeight = CGFloat(display.height) / screen.backingScaleFactor case .fitScreen: let windowAspectRatio = displaySize.width / displaySize.height let containerAspectRatio = screen.visibleFrame.width / availableHeight let widthFirst = windowAspectRatio > containerAspectRatio if widthFirst { targetWidth = screen.visibleFrame.width targetHeight = targetWidth / windowAspectRatio } else { targetHeight = availableHeight targetWidth = availableHeight * windowAspectRatio } } // clamped targetSize to the available screen's real estate let targetSize = CGSize( width: min(targetWidth, screen.visibleFrame.width), height: min(targetHeight, availableHeight) ) let targetRect = NSRect( x: screen.visibleFrame.origin.x + screen.visibleFrame.width / 2 - targetSize.width / 2, y: screen.visibleFrame.origin.y + availableHeight / 2 - targetSize.height / 2, width: targetSize.width, height: targetSize.height ) let frameRect = frameRect(forContentRect: targetRect) withFrameConstraintsDisabled(size != .fitScreen) { setFrame(frameRect, display: true) } } func applyAspectRatio(_ ratio: CGSize?) { guard let ratio else { resizeIncrements = CGSize(width: 1, height: 1) return } contentAspectRatio = ratio let newFrame = frameRect(forContentRect: AVMakeRect(aspectRatio: ratio, insideRect: contentRect(forFrameRect: frame))) setFrame(newFrame, display: true) } } ================================================ FILE: VirtualUI/Source/Components/HostingWindowController/VBRestorableWindow.swift ================================================ // // VBRestorableWindow.swift // VirtualUI // // Created by Guilherme Rambo on 09/03/23. // import Cocoa class VBRestorableWindow: FB18383725Window { override func close() { vbSaveFrame() super.close() } private var savedFrameKey: String? { guard let identifier, let screen else { return nil } guard let screenNumber = screen.deviceDescription[.init("NSScreenNumber")] as? Int else { return nil } return "window-\(identifier.rawValue)-\(screenNumber)" } var hasSavedFrame: Bool { savedFrame != nil } private var savedFrame: NSRect? { guard let savedFrameKey else { return nil } guard let dict = UserDefaults.standard.dictionary(forKey: savedFrameKey) else { return nil } return NSRect(dictionaryRepresentation: dict as CFDictionary) } private var applicationWillTerminateObserver: Any? private var keyRequested = false override func makeKey() { super.makeKey() defer { keyRequested = true } if applicationWillTerminateObserver == nil { applicationWillTerminateObserver = NotificationCenter.default.addObserver(forName: NSApplication.willTerminateNotification, object: nil, queue: nil, using: { [weak self] _ in self?.vbSaveFrame() }) } if !keyRequested, let savedFrame { setFrame(savedFrame, display: true) } } override func center() { guard savedFrame == nil else { makeKey() return } super.center() } private func vbSaveFrame() { guard let savedFrameKey else { return } UserDefaults.standard.set(frame.dictionaryRepresentation, forKey: savedFrameKey) UserDefaults.standard.synchronize() } private var frameConstraintsDisabled = false func withFrameConstraintsDisabled(_ disabled: Bool = true, perform block: () -> Void) { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(resetConstraintsDisabledFlag), object: nil) frameConstraintsDisabled = disabled block() perform(#selector(resetConstraintsDisabledFlag), with: nil, afterDelay: 0.2) } override func constrainFrameRect(_ frameRect: NSRect, to screen: NSScreen?) -> NSRect { guard frameConstraintsDisabled else { return super.constrainFrameRect(frameRect, to: screen) } return frameRect } @objc private func resetConstraintsDisabledFlag() { frameConstraintsDisabled = false } } ================================================ FILE: VirtualUI/Source/Components/HostingWindowController/WindowEnvironment.swift ================================================ // // WindowEnvironment.swift // StatusBuddy // // Created by Guilherme Rambo on 21/12/21. // Copyright © 2021 Guilherme Rambo. All rights reserved. // import SwiftUI import Combine // MARK: - Public API public extension EnvironmentValues { /// Closes the window that's hosting this view. /// Only available when the view hierarchy is being presented with `HostingWindowController`. var closeWindow: () -> Void { get { self[CloseWindowEnvironmentKey.self] } set { self[CloseWindowEnvironmentKey.self] = newValue } } } public extension View { /// Sets the title for the window that contains this SwiftUI view. /// Only available when the view hierarchy is being presented with `HostingWindowController`. func windowTitle(_ title: String) -> some View { environment(\.windowTitle, title) } /// Sets the title visibility for the window that contains this SwiftUI view. /// Only available when the view hierarchy is being presented with `HostingWindowController`. func windowTitleHidden(_ hidden: Bool) -> some View { environment(\.windowTitleVisibility, hidden ? .hidden : .visible) } /// Sets whether the title bar background is hidden for the window that contains this SwiftUI view. /// Only available when the view hierarchy is being presented with `HostingWindowController`. func windowTitleBarTransparent(_ transparent: Bool) -> some View { environment(\.windowTitleBarTransparent, transparent) } /// When `true`, indicates that the view hierarchy is responsible for render its own chrome for /// the Cocoa window that's hosting it. Causes `HostingWindowController` to configure /// its window to provide no chrome. func rendersWindowChrome(_ flag: Bool = true) -> some View { environment(\.rendersWindowChrome, flag) } /// When `true`, the hosting window can be moved by either clicking and dragging in the title bar, /// or by clicking and dragging the window's background when there's no title bar. /// Only available when the view hierarchy is being presented with `HostingWindowController`. func windowMovable(_ flag: Bool = true) -> some View { environment(\.windowMovable, flag) } /// Sets the level of the Cocoa window that's hosting this view hierarchy. /// Only available when the view hierarchy is being presented with `HostingWindowController`. func windowLevel(_ level: NSWindow.Level) -> some View { environment(\.windowLevel, level) } /// Sets the style mask of the Cocoa window that's hosting this view hierarchy. /// Note that usage of `rendersWindowChrome` may override the style mask set using this modifier and vice-versa. /// Only available when the view hierarchy is being presented with `HostingWindowController`. func windowStyleMask(_ mask: NSWindow.StyleMask) -> some View { environment(\.windowStyleMask, mask) } /// Performs the specific block when the window hosting the view hierarchy changes its occlusion state. func onWindowOcclusionStateChanged(perform block: @escaping (NSWindow.OcclusionState) -> Void) -> some View { environment(\.onWindowOcclusionStateChanged, block) } func confirmBeforeClosingWindow(callback: @escaping () async -> Bool) -> some View { environment(\.confirmBeforeClosingWindow, callback) } func onWindowKeyChange(perform callback: @escaping (Bool) -> Void) -> some View { modifier(OnWindowKeyChangedModifier(callback: callback)) } } // MARK: - Hosting Window Environment Keys private struct CloseWindowEnvironmentKey: EnvironmentKey { static let defaultValue: () -> Void = { } } private struct ConfirmBeforeClosingWindowEnvironmentKey: EnvironmentKey { static let defaultValue: () async -> Bool = { false } } private struct WindowTitleEnvironmentKey: EnvironmentKey { static let defaultValue: String = "" } private struct WindowTitleVisibilityEnvironmentKey: EnvironmentKey { static let defaultValue: NSWindow.TitleVisibility = .visible } private struct WindowTitleBarTransparentEnvironmentKey: EnvironmentKey { static let defaultValue: Bool = false } private struct HostingWindowKey: EnvironmentKey { static let defaultValue: () -> NSWindow? = { nil } } private struct RendersWindowChromeEnvironmentKey: EnvironmentKey { static let defaultValue: Bool = true } private struct WindowMovableEnvironmentKey: EnvironmentKey { static let defaultValue: Bool = true } private struct WindowLevelEnvironmentKey: EnvironmentKey { static let defaultValue: NSWindow.Level = .normal } private struct WindowStyleMaskEnvironmentKey: EnvironmentKey { static let defaultValue: NSWindow.StyleMask = defaultHostingWindowStyleMask } private struct WindowOnOcclusionStateChangedEnvironmentKey: EnvironmentKey { static let defaultValue: ((NSWindow.OcclusionState) -> Void)? = nil } extension EnvironmentValues { private var windowChromeConsumer: WindowChromeConsumer? { cocoaWindow?.windowController as? WindowChromeConsumer } /// Set and used internally by `HostingWindowController`. var cocoaWindow: NSWindow? { get { self[HostingWindowKey.self]() } set { self[HostingWindowKey.self] = { [weak newValue] in newValue } } } var windowTitle: String { get { self[WindowTitleEnvironmentKey.self] } set { self[WindowTitleEnvironmentKey.self] = newValue cocoaWindow?.title = newValue } } var windowTitleVisibility: NSWindow.TitleVisibility { get { self[WindowTitleVisibilityEnvironmentKey.self] } set { self[WindowTitleVisibilityEnvironmentKey.self] = newValue cocoaWindow?.titleVisibility = newValue } } var windowTitleBarTransparent: Bool { get { self[WindowTitleBarTransparentEnvironmentKey.self] } set { self[WindowTitleBarTransparentEnvironmentKey.self] = newValue cocoaWindow?.titlebarAppearsTransparent = newValue } } var rendersWindowChrome: Bool { get { self[RendersWindowChromeEnvironmentKey.self] } set { self[RendersWindowChromeEnvironmentKey.self] = newValue windowChromeConsumer?.viewRendersWindowChrome = newValue } } var windowMovable: Bool { get { self[WindowMovableEnvironmentKey.self] } set { self[WindowMovableEnvironmentKey.self] = newValue cocoaWindow?.isMovable = newValue cocoaWindow?.isMovableByWindowBackground = newValue } } var windowLevel: NSWindow.Level { get { self[WindowLevelEnvironmentKey.self] } set { self[WindowLevelEnvironmentKey.self] = newValue cocoaWindow?.level = newValue } } var windowStyleMask: NSWindow.StyleMask { get { self[WindowStyleMaskEnvironmentKey.self] } set { self[WindowStyleMaskEnvironmentKey.self] = newValue guard let cocoaWindow = cocoaWindow else { return } var effectiveNewValue = newValue // Can't remove .fullScreen when the window is in full screen (causes crash). if cocoaWindow.styleMask.contains(.fullScreen) { effectiveNewValue.insert(.fullScreen) } // TODO: HACK! /// Setting styleMask without this dispatch results in a crash when /// built with the macOS 15 SDK and running under macOS 15. /// The crash is a precondition failure: `setting value during update: 360192`. DispatchQueue.main.async { cocoaWindow.styleMask = effectiveNewValue } } } var onWindowOcclusionStateChanged: ((NSWindow.OcclusionState) -> Void)? { get { self[WindowOnOcclusionStateChangedEnvironmentKey.self] } set { self[WindowOnOcclusionStateChangedEnvironmentKey.self] = newValue windowChromeConsumer?.onWindowOcclusionStateChanged = newValue } } var confirmBeforeClosingWindow: () async -> Bool { get { self[ConfirmBeforeClosingWindowEnvironmentKey.self] } set { self[ConfirmBeforeClosingWindowEnvironmentKey.self] = newValue windowChromeConsumer?.confirmBeforeClosingCallback = newValue } } } private struct OnWindowKeyChangedModifier: ViewModifier { var callback: (Bool) -> Void @Environment(\.cocoaWindow) private var window func body(content: Content) -> some View { content .onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification, object: window)) { notification in guard notification.object as? NSWindow === window else { return } callback(true) } .onReceive(NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification, object: window)) { notification in guard notification.object as? NSWindow === window else { return } callback(false) } } } ================================================ FILE: VirtualUI/Source/Components/KeyboardNavigationModifier.swift ================================================ import SwiftUI extension View { @ViewBuilder func keyboardNavigation(autofocus: Bool = true, onMove: @escaping (_ direction: MoveCommandDirection) -> Void) -> some View { if #available(macOS 14.0, *) { modifier(KeyboardNavigationModifier_Modern(autofocus: autofocus, onMove: onMove)) } else { modifier(KeyboardNavigationModifier_Legacy(autofocus: autofocus, onMove: onMove)) } } } @available(macOS 14.0, *) private struct KeyboardNavigationModifier_Modern: ViewModifier { var autofocus: Bool var onMove: (MoveCommandDirection) -> Void @FocusState private var isFocused func body(content: Content) -> some View { content .focusable(true) .focused($isFocused) .onMoveCommand { direction in onMove(direction) } .focusEffectDisabled() .task { guard autofocus else { return } isFocused = true } } } private struct KeyboardNavigationModifier_Legacy: ViewModifier { var autofocus: Bool var onMove: (MoveCommandDirection) -> Void @FocusState private var isFocused func body(content: Content) -> some View { content .overlay { /// Horrible hack to hide the focus ring while still allowing for keyboard navigation. Rectangle() .frame(width: 0, height: 0) .opacity(0) .focusable(true) .focused($isFocused) .onMoveCommand { direction in onMove(direction) } } .task { guard autofocus else { return } isFocused = true } } } extension View { @ViewBuilder func backported_focusEffectDisabled(_ disabled: Bool = true) -> some View { if #available(macOS 14.0, *) { focusEffectDisabled(disabled) } else { self } } } ================================================ FILE: VirtualUI/Source/Components/LogConsole.swift ================================================ import SwiftUI import VirtualCore import UniformTypeIdentifiers struct LogConsole: View { @StateObject private var streamer: LogStreamer init(predicate: LogStreamer.Predicate) { self._streamer = .init(wrappedValue: LogStreamer(predicate: predicate)) } init(streamer: LogStreamer) { self._streamer = .init(wrappedValue: streamer) } @State private var searchTerm = "" private var filteredEvents: [LogEntry] { guard searchTerm.count >= 3 else { return streamer.events } return streamer.events.filter { $0.message.localizedCaseInsensitiveContains(searchTerm) } } var body: some View { ScrollView(.vertical) { LazyVStack(alignment: .leading, spacing: 8) { ForEach(filteredEvents) { entry in Text(entry.formattedTime + " ") .foregroundColor(.secondary) + Text(entry.message) .foregroundColor(entry.level.color) } } .font(.system(.body).monospaced()) .padding(.horizontal) .padding(.top, 6) .textSelection(.enabled) } .safeAreaInset(edge: .top, content: { searchBar }) .safeAreaInset(edge: .bottom, content: { bottomBar }) .onAppear(perform: streamer.activate) } @ViewBuilder private var searchBar: some View { ZStack { searchField } .frame(maxWidth: .infinity) .padding() .background(Material.thick, in: Rectangle()) .overlay(alignment: .bottom) { Divider() } } @FocusState private var searchFieldFocused: Bool @ViewBuilder private var searchField: some View { TextField("Search Logs", text: $searchTerm) .focused($searchFieldFocused) .onExitCommand { if searchTerm == "" { searchFieldFocused = false } searchTerm = "" } .textFieldStyle(.roundedBorder) } private var fullLogText: String { filteredEvents .map(\.description) .joined(separator: "\n") } @ViewBuilder private var bottomBar: some View { ZStack { HStack(spacing: 16) { Button { NSPasteboard.general.clearContents() NSPasteboard.general.setString(fullLogText, forType: .string) } label: { Text("Copy Text") } Button { NSSavePanel.run(saving: Data(fullLogText.utf8), as: .logFile) } label: { Text("Save to File…") } } .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .secondary) } .padding() .frame(maxWidth: .infinity, alignment: .bottomTrailing) .controlSize(.small) .buttonStyle(.link) } } extension LogEntry.Level { var color: Color { switch self { case .debug: return .gray.opacity(0.6) case .trace: return .gray.opacity(0.8) case .notice: return .gray.opacity(0.9) case .info: return .gray case .default: return .primary case .warning: return .yellow case .error: return .orange case .fault: return .red case .critical: return Color(nsColor: .magenta) } } } #if DEBUG struct LogConsole_Previews: PreviewProvider { static var previews: some View { LogConsole(streamer: .preview) } } #endif extension UTType { static let logFile: UTType = { UTType(filenameExtension: "log", conformingTo: .log) ?? .plainText }() } ================================================ FILE: VirtualUI/Source/Components/MaterialView.swift ================================================ // // MaterialView.swift // AirUI // // Created by Guilherme Rambo on 16/03/22. // import SwiftUI public typealias MaterialType = NSVisualEffectView.Material public typealias MaterialBlendingMode = NSVisualEffectView.BlendingMode public typealias MaterialState = NSVisualEffectView.State public extension MaterialBlendingMode { /// Uses within window blending mode when running in SwiftUI previews, /// but the behind window blending mode when running normally. /// Does not affect release builds, which always return `.behindWindow`. static var behindWindowForPreviews: Self { #if DEBUG if ProcessInfo.isSwiftUIPreview { return .withinWindow } else { return .behindWindow } #else return .behindWindow #endif } } public struct MaterialView: NSViewRepresentable { public typealias NSViewType = NSVisualEffectView public init() { } public func makeNSView(context: Context) -> NSVisualEffectView { let v = NSVisualEffectView(frame: .zero) v.material = context.environment.materialType v.blendingMode = context.environment.materialBlendingMode v.state = context.environment.materialState return v } public func updateNSView(_ nsView: NSVisualEffectView, context: Context) { if (nsView.material != context.environment.materialType) { nsView.material = context.environment.materialType } if (nsView.blendingMode != context.environment.materialBlendingMode) { nsView.blendingMode = context.environment.materialBlendingMode } if context.environment.accessibilityReduceTransparency { nsView.state = .inactive } else { if (nsView.state != context.environment.materialState) { nsView.state = context.environment.materialState } } } } public extension View { /// Applies a background material to the view, clipped to the specified shape. /// - Parameters: /// - type: The type of material to be used. Will use the material from the current environment if `nil`. /// - blendMode: The material blending mode. Will use the blending mode from the current environment if `nil`. /// - state: The material state. Will use the state from the current environment if `nil`. /// - shape: The shape clipping the material. /// - Returns: The modified view. func materialBackground(_ type: MaterialType? = nil, blendMode: MaterialBlendingMode? = nil, state: MaterialState? = nil, in shape: S) -> some View where S: Shape { background( MaterialView() .clipShape(shape) .materialType(type) .materialBlendingMode(blendMode) .materialState(state) ) } @ViewBuilder func materialType(_ material: MaterialType?) -> some View { if let material = material { environment(\.materialType, material) } else { self } } @ViewBuilder func materialBlendingMode(_ mode: MaterialBlendingMode?) -> some View { if let mode = mode { environment(\.materialBlendingMode, mode) } else { self } } @ViewBuilder func materialState(_ state: MaterialState?) -> some View { if let state = state { environment(\.materialState, state) } else { self } } } fileprivate struct MaterialViewStateKey: EnvironmentKey { static var defaultValue: MaterialState = .active } fileprivate struct MaterialViewBlendingModeKey: EnvironmentKey { static var defaultValue: MaterialBlendingMode = .withinWindow } fileprivate struct MaterialViewMaterialKey: EnvironmentKey { static var defaultValue: MaterialType = .popover } fileprivate extension EnvironmentValues { var materialState: MaterialState { get { self[MaterialViewStateKey.self] } set { self[MaterialViewStateKey.self] = newValue } } var materialType: MaterialType { get { self[MaterialViewMaterialKey.self] } set { self[MaterialViewMaterialKey.self] = newValue } } var materialBlendingMode: MaterialBlendingMode { get { self[MaterialViewBlendingModeKey.self] } set { self[MaterialViewBlendingModeKey.self] = newValue } } } ================================================ FILE: VirtualUI/Source/Components/NSAlert+Confirmation.swift ================================================ // // NSAlert+Confirmation.swift // VirtualBuddy // // Created by Guilherme Rambo on 29/06/22. // import Cocoa public extension NSAlert { static func runConfirmationAlert(title: String, message: String, continueButtonTitle: String = "Continue", cancelButtonTitle: String = "Cancel", continueButtonIsDefault: Bool = false) async -> Bool { let alert = NSAlert() alert.messageText = title alert.informativeText = message if continueButtonIsDefault { alert.addButton(withTitle: continueButtonTitle) alert.addButton(withTitle: cancelButtonTitle) } else { alert.addButton(withTitle: cancelButtonTitle) alert.addButton(withTitle: continueButtonTitle) } let response: NSApplication.ModalResponse if let window = NSApp?.keyWindow { response = await alert.beginSheetModal(for: window) } else { response = alert.runModal() } return continueButtonIsDefault ? response == .alertFirstButtonReturn : response == .alertSecondButtonReturn } } ================================================ FILE: VirtualUI/Source/Components/OnAppearOnce.swift ================================================ // // OnAppearOnce.swift // Uploader // // Created by Guilherme Rambo on 22/04/22. // import SwiftUI public extension View { /// Performs the specified code block the first time the view this modifier is attached to appears. /// - Parameter block: The callback to be performed only the first time the view appears. func onAppearOnce(perform block: @escaping () -> Void) -> some View { modifier(OnAppearOnce(callback: block)) } } private struct OnAppearOnce: ViewModifier { @State private var performed = false let callback: () -> Void func body(content: Content) -> some View { content .onAppear { guard !performed else { return } performed = true callback() } } } ================================================ FILE: VirtualUI/Source/Components/OpenSavePanelUtils.swift ================================================ import SwiftUI import UniformTypeIdentifiers import OSLog private let logger = Logger(subsystem: VirtualUIConstants.subsystemName, category: "OpenSavePanelUtils") public extension NSOpenPanel { static func run(accepting contentTypes: Set, directoryURL: URL? = nil, defaultDirectoryKey: String? = nil, prompt: String? = nil) -> URL? { let panel = NSOpenPanel() if contentTypes == [.folder] || contentTypes == [.directory] { panel.canChooseFiles = false panel.canChooseDirectories = true } else { panel.allowedContentTypes = Array(contentTypes) } if let prompt { panel.prompt = prompt } panel.treatsFilePackagesAsDirectories = true let defaultsKey = defaultDirectoryKey.flatMap { "defaultDirectory-\($0)" } if let defaultsKey, let defaultDirectoryPath = UserDefaults.standard.string(forKey: defaultsKey) { panel.directoryURL = URL(fileURLWithPath: defaultDirectoryPath) } else if let directoryURL { panel.directoryURL = directoryURL } guard panel.runModal() == .OK, let url = panel.url else { return nil } if let defaultsKey { /// If user is choosing a folder, then just store the path to the folder itself. /// If user is choosing files, then remove the last path component to save the path to the file's directory instead. let effectiveURL = contentTypes.contains(.folder) ? url : url.deletingLastPathComponent() UserDefaults.standard.set(effectiveURL.path, forKey: defaultsKey) } return url } } public extension NSSavePanel { static func run(for contentTypes: Set, directoryURL: URL? = nil) -> URL? { let panel = NSSavePanel() panel.allowedContentTypes = Array(contentTypes) panel.treatsFilePackagesAsDirectories = true panel.directoryURL = directoryURL guard panel.runModal() == .OK, let url = panel.url else { return nil } return url } static func run(saving data: Data, as contentType: UTType, directoryURL: URL? = nil) { guard let url = run(for: [contentType], directoryURL: directoryURL) else { return } do { try data.write(to: url, options: .atomic) } catch { logger.error("Save failed: \(error.localizedDescription, privacy: .public)") } } } ================================================ FILE: VirtualUI/Source/Components/RemoteImage/RemoteImage.swift ================================================ import SwiftUI import CryptoKit import VirtualCore import OSLog struct RemoteImage: View { var url: URL var blurHash: String? var blurHashSize: CGSize var loader: RemoteImageLoader init(url: URL, blurHash: String? = nil, blurHashSize: CGSize = .vbBlurHashSize, loader: RemoteImageLoader = .default) { self.url = url self.blurHash = blurHash self.blurHashSize = blurHashSize self.loader = loader self._nsImage = .init(initialValue: loader.cachedImage(for: url)) } @State private var nsImage: NSImage? var body: some View { ZStack { if let nsImage { Image(nsImage: nsImage) .resizable() } else { if let blurHash { Image(blurHash: blurHash, size: blurHashSize) .resizable() } else { Rectangle() .fill(.quaternary) } } } .task(id: url) { nsImage = await loader.load(from: url) } } } final class RemoteImageLoader { private let logger = Logger(subsystem: "codes.rambo.RemoteImageLoader", category: "RemoteImageLoader") private let memoryCache = NSCache() static let `default` = RemoteImageLoader() private lazy var session: URLSession = { let config = URLSessionConfiguration.default return URLSession(configuration: config) }() func load(from remoteURL: URL) async -> NSImage? { guard !remoteURL.isFileURL else { return NSImage(contentsOf: remoteURL) } if let cached = cachedImage(for: remoteURL) { return cached } do { let (fileURL, response) = try await session.download(from: remoteURL) let stagedFileURL = try fileURL.temporaryCopy(usingPathExtensionFrom: remoteURL) let code = (response as? HTTPURLResponse)?.statusCode ?? -1 guard code == 200 else { throw Failure("HTTP \(code)") } guard let image = NSImage(contentsOf: stagedFileURL) else { throw Failure("Image initialization failed") } store(image: image, localFileURL: stagedFileURL, for: remoteURL) return image } catch { logger.warning("Image download failed: \(error, privacy: .public). Image URL: \(remoteURL.absoluteString)") return nil } } func cachedImage(for url: URL) -> NSImage? { let key = cacheKey(for: url) if let memImage = memoryCache.object(forKey: key as NSString) { return memImage } else { let storageURL = diskURL(for: key) guard FileManager.default.fileExists(atPath: storageURL.path) else { return nil } return NSImage(contentsOf: storageURL) } } private func cacheKey(for url: URL) -> String { SHA256.hash(data: Data(url.absoluteString.utf8)) .map { String(format: "%02x", $0) } .joined() + ".\(url.pathExtension)" } private func diskURL(for key: String) -> URL { do { let baseURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) .appendingPathComponent(Bundle.main.bundleURL.deletingPathExtension().lastPathComponent) .appendingPathComponent("ImageCache") if !FileManager.default.fileExists(atPath: baseURL.path) { try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) } return baseURL.appendingPathComponent(key) } catch { assertionFailure("Failed to create cache directory: \(error)") return URL(fileURLWithPath: NSTemporaryDirectory()) } } private func store(image: NSImage, localFileURL: URL, for url: URL) { let key = cacheKey(for: url) memoryCache.setObject(image, forKey: key as NSString) let storageURL = diskURL(for: key) do { try FileManager.default.copyItem(at: localFileURL, to: storageURL) } catch { logger.error("Cache write failed: \(error, privacy: .public)") assertionFailure("Cache write failed: \(error)") } } } private extension URL { func temporaryCopy(usingPathExtensionFrom other: URL) throws -> URL { let fileName = deletingPathExtension().lastPathComponent let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent(fileName) .appendingPathExtension(other.pathExtension) try FileManager.default.copyItem(at: self, to: tempURL) return tempURL } } ================================================ FILE: VirtualUI/Source/Components/SelfSizingGroupedForm.swift ================================================ import SwiftUI import SwiftUIIntrospect /// A `Form` with the `.grouped` style that automatically resizes itself so that it /// perfectly fits its contents in the vertical axis. struct SelfSizingGroupedForm: View { var minHeight: CGFloat @ViewBuilder var content: () -> Content @State private var contentHeight: CGFloat = 0 private let disabled = UserDefaults.standard.bool(forKey: "VBDisableSelfSizingGroupedForm") var body: some View { ZStack { Form { content() } .formStyle(.grouped) .introspect(.scrollView, on: .macOS(.v13, .v14, .v15, .v26)) { scrollView in guard !disabled else { return } guard let frame = scrollView.documentView?.frame else { return } guard frame.height != contentHeight else { return } guard frame.height > 0, frame.height.isFinite, !frame.height.isNaN else { return } /// Ugly, I know, but I reaaaaally wanted the form to look a specific way 🥺 DispatchQueue.main.async { contentHeight = frame.height } } } .frame(height: max(minHeight, contentHeight)) } } #if DEBUG private struct _Preview: View { @State var someText = "Hello, World" @State var someBool = true var body: some View { SelfSizingGroupedForm(minHeight: 100) { TextField("This is a text field", text: $someText) Toggle("This is a toggle", isOn: $someBool) } } } #Preview("SelfSizingGroupedForm") { _Preview() } #endif ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/Cocoa/StatusBarContentPanel.swift ================================================ import Cocoa final class StatusBarContentPanel: NSPanel { override var canBecomeKey: Bool { true } @objc func hasKeyAppearance() -> Bool { return true } @objc func hasMainAppearance() -> Bool { return true } } ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/Cocoa/StatusBarHighlightView.swift ================================================ import Cocoa import SwiftUI struct StatusBarHighlightView: NSViewRepresentable { typealias NSViewType = NSView var isHighlighted: Bool func makeNSView(context: Context) -> NSViewType { let v = NSView() updateHighlight(in: v) return v } func updateNSView(_ nsView: NSViewType, context: Context) { updateHighlight(in: nsView) } private func updateHighlight(in view: NSViewType) { NSStatusItem.vui_drawMenuBarHighlight( in: view, highlighted: isHighlighted, inset: StatusItemButtonStyle.highlightCornerRadius * 0.5 ) } } #if DEBUG struct StatusBarHighlightView_Previews: PreviewProvider { static var previews: some View { StatusBarHighlightView(isHighlighted: true) .frame(width: 30, height: 30) .previewDisplayName("Highlighted") StatusBarHighlightView(isHighlighted: false) .frame(width: 30, height: 30) .previewDisplayName("Normal") } } #endif ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/Cocoa/StatusItemMenuBarExtraView.swift ================================================ import Cocoa final class StatusItemMenuBarExtraView: NSView { override var isOpaque: Bool { false } override func viewDidMoveToSuperview() { super.viewDidMoveToSuperview() guard let superview else { return } wantsLayer = true layer?.masksToBounds = false superview.wantsLayer = true superview.layer?.masksToBounds = false superview.superview?.layer?.masksToBounds = false } } ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/Cocoa/StatusItemPanelContentController.swift ================================================ import Cocoa final class StatusItemPanelContentController: NSViewController { let child: NSViewController var onContentSizeChange: ((CGSize) -> Void)? init(child: NSViewController, onContentSizeChange: ((CGSize) -> Void)? = nil) { self.child = child self.onContentSizeChange = onContentSizeChange super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError() } override func loadView() { view = NSView() view.wantsLayer = true addChild(child) child.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(child.view) NSLayoutConstraint.activate([ child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: StatusBarPanelChromeMetrics.shadowPadding), child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -StatusBarPanelChromeMetrics.shadowPadding), child.view.topAnchor.constraint(equalTo: view.topAnchor, constant: StatusBarPanelChromeMetrics.shadowPadding), child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -StatusBarPanelChromeMetrics.shadowPadding), ]) } var contentSize: CGSize { view.bounds.size } private var previouslyReportedSize: CGSize = .zero override func viewDidLayout() { super.viewDidLayout() let newSize = view.bounds.size guard newSize != previouslyReportedSize else { return } previouslyReportedSize = newSize self.onContentSizeChange?(newSize) } } ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/Cocoa/VUIAppKitViewControllerHost.swift ================================================ import Cocoa import SwiftUI /// A SwiftUI view that hosts any AppKit view controller so that it can be presented in a /// context that requires the contents to be SwiftUI views, such as with ``StatusItemManager``. struct VUIAppKitViewControllerHost: NSViewControllerRepresentable where ChildController: NSViewController { typealias NSViewControllerType = _VUIViewControllerHostingController private let contentControllerBuilder: () -> ChildController init(with contentController: @escaping @autoclosure () -> ChildController) { self.contentControllerBuilder = contentController } func makeNSViewController(context: Context) -> NSViewControllerType { let host = _VUIViewControllerHostingController(with: contentControllerBuilder) return host } func updateNSViewController(_ nsViewController: NSViewControllerType, context: Context) { } } final class _VUIViewControllerHostingController: NSViewController where ChildController: NSViewController { private let contentControllerBuilder: () -> ChildController fileprivate init(with contentController: @escaping () -> ChildController) { self.contentControllerBuilder = contentController super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError() } override func loadView() { view = NSView() view.wantsLayer = true let contentController = contentControllerBuilder() addChild(contentController) contentController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(contentController.view) NSLayoutConstraint.activate([ contentController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), contentController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), contentController.view.topAnchor.constraint(equalTo: view.topAnchor), contentController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } } ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/ObjC/NSApplication+MenuBar.h ================================================ #import NS_ASSUME_NONNULL_BEGIN @interface NSApplication (MenuBar) - (void)__vui_setMenuBarVisible:(BOOL)visible; @end NS_ASSUME_NONNULL_END ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/ObjC/NSApplication+MenuBar.m ================================================ #import "NSApplication+MenuBar.h" @import os.log; #import #import Boolean __vui_softLinkHIMenuBarRequestVisibility(Boolean visibility, Boolean *outAlreadyInState, void (^completion)(void)); @implementation NSApplication (MenuBar) + (os_log_t)__vui_menuBarLog { static os_log_t _log; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _log = os_log_create([[_VirtualUIConstantsObjC subsystemName] UTF8String], "NSApplication+VUIMenuBar"); }); return _log; } - (void)__vui_setMenuBarVisible:(BOOL)visible { os_log_t log = [NSApplication __vui_menuBarLog]; os_log_debug(log, "Set menu bar visibility to %{public}d", visible); Boolean result = false; Boolean alreadyInState = false; result = __vui_softLinkHIMenuBarRequestVisibility(visible, &alreadyInState, ^{ }); os_log_debug(log, "Set menu bar visibility result: %{public}d, already in state: %{public}d", visible, alreadyInState); } @end typedef Boolean (*_VUIHIMenuBarRequestVisibilityPtr)(Boolean visibility, Boolean *outAlreadyInState, void (^completion)(void)); Boolean __vui_softLinkHIMenuBarRequestVisibility(Boolean visibility, Boolean *outAlreadyInState, void (^completion)(void)) { static _VUIHIMenuBarRequestVisibilityPtr fnptr; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ void *handle = dlopen("/System/Library/Frameworks/Carbon.framework/Versions/Current/Carbon", RTLD_NOW); if (!handle) { __assert("Couldn't load Carbon dylib", __FILE__, __LINE__); return; } void *fn = dlsym(handle, "_HIMenuBarRequestVisibility"); if (!fn) { __assert("Couldn't load _HIMenuBarRequestVisibility symbol", __FILE__, __LINE__); return; } fnptr = (_VUIHIMenuBarRequestVisibilityPtr)fn; }); if (!fnptr) return false; return fnptr(visibility, outAlreadyInState, completion); } ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/ObjC/NSStatusBarPrivate.h ================================================ @import Cocoa; NS_ASSUME_NONNULL_BEGIN @interface NSStatusBar (Private) @property (nonatomic, readonly) CGFloat contentPadding; - (void)drawBackgroundInRect:(NSRect *)rect inView:(NSView *)view highlight:(BOOL)highlight; @end @interface NSStatusItem (Private) - (void)setAllowsVibrancy:(BOOL)flag; @end NS_ASSUME_NONNULL_END ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/ObjC/NSStatusItem+.h ================================================ #import NS_ASSUME_NONNULL_BEGIN @interface NSStatusItem (VUIAdditions) @property (nonatomic, strong) NSView *__nullable vui_contentView; - (void)vui_disableVibrancy; @property (class, nonatomic, readonly) CGFloat vui_idealPadding; + (void)vui_drawMenuBarHighlightInView:(NSView *)view highlighted:(BOOL)isHighlighted inset:(CGFloat)insetAmount; @end NS_ASSUME_NONNULL_END ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/ObjC/NSStatusItem+.m ================================================ #import "NSStatusItem+.h" #import "NSStatusBarPrivate.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @implementation NSStatusItem (VUIAdditions) - (NSView *)vui_contentView { return self.view; } - (void)setVui_contentView:(NSView *)vui_contentView { self.view = vui_contentView; } - (void)vui_disableVibrancy { if ([self respondsToSelector:@selector(setAllowsVibrancy:)]) { [self setAllowsVibrancy:NO]; } } + (CGFloat)vui_idealPadding { if ([[NSStatusBar systemStatusBar] respondsToSelector:@selector(contentPadding)]) { return [[NSStatusBar systemStatusBar] contentPadding]; } else { return 16.0; } } + (void)vui_drawMenuBarHighlightInView:(NSView *)view highlighted:(BOOL)isHighlighted inset:(CGFloat)insetAmount { if (!view.window || !view.superview) return; if (!view.window.isVisible) return; if (view.bounds.size.width <= 0 || view.bounds.size.height <= 0) return; if (![[NSStatusBar systemStatusBar] respondsToSelector:@selector(drawBackgroundInRect:inView:highlight:)]) { return [self __vui_drawFallbackMenuBarHighlightInView:view]; } NSRect rect = NSInsetRect(view.bounds, insetAmount, 0); [[NSStatusBar systemStatusBar] drawBackgroundInRect:&rect inView:view highlight:isHighlighted]; } + (void)__vui_drawFallbackMenuBarHighlightInView:(NSView *)view { [view lockFocus]; [[[NSColor blackColor] colorWithAlphaComponent:0.1] setFill]; NSRectFill(view.bounds); [view unlockFocus]; } @end #pragma clang diagnostic pop ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/ScreenChangeModifier.swift ================================================ import SwiftUI import Combine typealias ViewScreenChangeBlock = (NSScreen?) -> Void extension View { /// Calls the specified block whenever the window that's hosting the view moves between displays. func onScreenChanged(perform block: @escaping ViewScreenChangeBlock) -> some View { modifier(ScreenChangeModifier(onScreenChanged: block)) } /// Injects the `screen` environment value, updating it whenever the window that's hosting /// the view moves between displays. func trackScreen() -> some View { modifier(ScreenEnvironmentInjectionModifier()) } } private struct ScreenEnvironmentKey: EnvironmentKey { static var defaultValue: NSScreen? = .main } extension EnvironmentValues { fileprivate(set) var screen: NSScreen? { get { self[ScreenEnvironmentKey.self] } set { self[ScreenEnvironmentKey.self] = newValue } } } private struct ScreenChangeModifier: ViewModifier { var onScreenChanged: ViewScreenChangeBlock func body(content: Content) -> some View { content .background(ScreenTrackingHostView(onScreenChanged: onScreenChanged)) } } private struct ScreenEnvironmentInjectionModifier: ViewModifier { @State private var screen: NSScreen? = .main func body(content: Content) -> some View { content .environment(\.screen, screen) .onScreenChanged { screen = $0 } } } private struct ScreenTrackingHostView: NSViewRepresentable { var onScreenChanged: ViewScreenChangeBlock typealias NSViewType = _ScreenTrackingView func makeNSView(context: Context) -> _ScreenTrackingView { _ScreenTrackingView(onScreenChanged: onScreenChanged) } func updateNSView(_ nsView: _ScreenTrackingView, context: Context) { } } private final class _ScreenTrackingView: NSView { var onScreenChanged: ViewScreenChangeBlock private var previousScreenDisplayID: Int? init(onScreenChanged: @escaping ViewScreenChangeBlock) { self.onScreenChanged = onScreenChanged super.init(frame: .zero) } required init?(coder: NSCoder) { fatalError() } override func hitTest(_ point: NSPoint) -> NSView? { nil } override var isOpaque: Bool { false } private var screenCancellable: AnyCancellable? override func viewDidMoveToWindow() { super.viewDidMoveToWindow() screenCancellable?.cancel() guard let window else { return } screenCancellable = window .publisher(for: \.screen, options: [.initial, .new]) .sink { [weak self] screen in guard let self = self else { return } let currentScreenDisplayID = screen?.displayID?.intValue guard currentScreenDisplayID != self.previousScreenDisplayID else { return } self.onScreenChanged(screen) self.previousScreenDisplayID = currentScreenDisplayID } } } extension NSScreen { var displayID: NSNumber? { deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber } var hasTallMenuBar: Bool { safeAreaInsets.top > 0 } } ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/StatusBarPanelChrome.swift ================================================ import SwiftUI struct StatusBarPanelChromeMetrics { static var shadowPadding: CGFloat { 26 } static var cornerRadius: CGFloat { 15 } static var innerStrokeWidth: CGFloat { 1 } static var innerStrokeInset: CGFloat { innerStrokeWidth * 0.5 } static var outerStrokeWidth: CGFloat { .onePixel } } struct StatusBarPanelChrome: View { var contentBuilder: () -> Content var shape: S var body: some View { contentBuilder() .background(chromeBackground) } @ViewBuilder private var chromeBackground: some View { MaterialView() .materialType(.hudWindow) .materialBlendingMode(.behindWindowForPreviews) .clipShape(shape) .shadow(color: Color.black.opacity(0.7), radius: 1, x: 0, y: 0) .shadow(color: Color.black.opacity(0.28), radius: 13, x: 0, y: 0) .compositingGroup() .overlay(innerStroke) .overlay(outerStroke) } @Environment(\.colorScheme) private var colorScheme private var outerStrokeWidth: CGFloat { 0.5 } private var innerStroke: some View { shape .inset(by: StatusBarPanelChromeMetrics.innerStrokeInset) .stroke(Color.statusItemPanelChromeBorder.opacity(0.7), lineWidth: StatusBarPanelChromeMetrics.innerStrokeWidth) .blendMode(.plusLighter) .opacity(colorScheme == .dark ? 1 : 0) .zIndex(9999) } private var outerStroke: some View { shape .inset(by: -outerStrokeWidth * 0.5) .stroke(Color.black, lineWidth: outerStrokeWidth) .opacity(colorScheme == .dark ? 1 : 0) } } extension StatusBarPanelChrome where S == RoundedRectangle { init(contentBuilder: @escaping () -> Content) { self.init(contentBuilder: contentBuilder, shape: RoundedRectangle(cornerRadius: StatusBarPanelChromeMetrics.cornerRadius, style: .continuous)) } } #if DEBUG struct StatusBarPanelChrome_Previews: PreviewProvider { static var previews: some View { StatusBarPanelChrome { Text("Hello, World!") .frame(width: 300, height: 300) } .padding(100) .previewDisplayName("Light") StatusBarPanelChrome { Text("Hello, World!") .frame(width: 300, height: 300) } .padding(100) .preferredColorScheme(.dark) .previewDisplayName("Dark") } } #endif ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/StatusItemButton.swift ================================================ import Cocoa import SwiftUI private struct StatusItemHighlightedEnvironmentKey: EnvironmentKey { static var defaultValue = false } extension EnvironmentValues { /// Whether the status item should be drawn highlighted. fileprivate(set) var isStatusItemHighlighted: Bool { get { self[StatusItemHighlightedEnvironmentKey.self] } set { self[StatusItemHighlightedEnvironmentKey.self] = newValue } } } /// A button that's used in ``StatusItemManager`` to provide the contents for the status item /// when using the `.button` content type. /// Custom views can use ``StatusItemButtonLook`` to implement a view that's compatible /// with status items, but that aren't necessarily a button. struct StatusItemButton: View { @EnvironmentObject private var provider: Provider var label: () -> Label init(@ViewBuilder label: @escaping () -> Label) { self.label = label } var body: some View { Button { provider.togglePanelVisible() } label: { label() } .buttonStyle(StatusItemButtonStyle()) .statusItemHighlightedEnvironment(from: provider) } } extension View { /// Reads properties from the specified provider and sets the `isStatusItemHighlighted` environment value accordingly. /// Apply at the root of hierarchies that expect to be able to read the `isStatusItemHighlighted` value. /// /// This is handy because the `isStatusItemHighlighted` property from `StatusItemManager` is not always /// enough to determine whether to draw the highlight, which also depends on status item occlusion state. func statusItemHighlightedEnvironment(from provider: Provider) -> some View where Provider: StatusItemProvider { environment(\.isStatusItemHighlighted, provider.isStatusItemHighlighted && !provider.isStatusItemOccluded) } } /// A view that looks like ``StatusItemButton``, but can be used as a wrapper /// for completely custom status items that do not use the button view. struct StatusItemButtonLook: View { var label: () -> Label init(@ViewBuilder label: @escaping () -> Label) { self.label = label } @Environment(\.isStatusItemHighlighted) private var isHighlighted @State private var screen: NSScreen? private var height: CGFloat { StatusItemButtonStyle.effectiveHeight(for: screen) } var body: some View { label() .font(.system(size: StatusItemButtonStyle.glyphFontSize)) .offset(y: StatusItemButtonStyle.glyphOffsetY) .frame(minWidth: StatusItemButtonStyle.effectiveWidth, minHeight: height, maxHeight: height) .background(background) .contentShape(Rectangle()) .onScreenChanged { screen = $0 } } @ViewBuilder private var background: some View { StatusBarHighlightView(isHighlighted: isHighlighted) .frame(width: nil) .clipShape(RoundedRectangle(cornerRadius: StatusItemButtonStyle.highlightCornerRadius, style: .continuous)) } } struct StatusItemButtonStyle: ButtonStyle { static var heightRegular: CGFloat { 22 } static var heightTall: CGFloat { 37 } static var width: CGFloat { 36 } static var verticalPadding: CGFloat { 6 } static var horizontalPadding: CGFloat { NSStatusItem.vui_idealPadding } static var glyphFontSize: CGFloat { 14 } static var glyphOffsetY: CGFloat { 0.5 } static var highlightCornerRadius: CGFloat { 4 } static func effectiveHeight(for screen: NSScreen?) -> CGFloat { guard let screen else { return Self.heightRegular } if screen.hasTallMenuBar { return Self.heightTall - Self.verticalPadding * 2 } else { return Self.heightRegular } } static var effectiveWidth: CGFloat { Self.width - Self.horizontalPadding } func makeBody(configuration: Configuration) -> some View { StatusItemButtonLook(label: { configuration.label }) } } ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/Components/StatusItemProviderProtocol.swift ================================================ import Cocoa import SwiftUI public protocol StatusItemProvider: ObservableObject { /// `true` when the background of the status item's view should be highlighted. var isStatusItemHighlighted: Bool { get } var isStatusItemOccluded: Bool { get } /// Show/hide the status item's content panel. func togglePanelVisible() /// Show a pop up menu produced by running the builder closure. func showPopUpMenu(using builder: () -> NSMenu) } ================================================ FILE: VirtualUI/Source/Components/SwiftUI Status Item/StatusItemManager.swift ================================================ import Cocoa import SwiftUI import Combine import OSLog import notify public final class StatusItemManager: NSObject, NSWindowDelegate, StatusItemProvider { private lazy var logger: Logger = { Logger(subsystem: VirtualUIConstants.subsystemName, category: "StatusItemManager(\(configuration.id))") }() /// Configures the status item's identity and behavior. public struct Configuration { /// Unique identifier for this status item. public internal(set) var id: String /// The behavior for allowing removal of the status item by dragging out of the menu bar. public var behavior: NSStatusItem.Behavior /// Custom autosave name used by AppKit to store item's on/off and position preferences. public var autosaveName: String? public static var `default`: Configuration { Configuration(id: UUID().uuidString, behavior: .removalAllowed, autosaveName: nil) } public func id(_ id: String) -> Self { var mSelf = self mSelf.id = id return mSelf } public init(id: String, behavior: NSStatusItem.Behavior, autosaveName: String? = nil) { self.id = id self.behavior = behavior self.autosaveName = autosaveName } } @Published public private(set) var isStatusItemHighlighted: Bool = false @Published public private(set) var isPanelVisible = false @Published public private(set) var isStatusItemVisible = true /// `true` whenever the status item is not actually visible in the Menu Bar. /// This differs from `isStatusItemVisible`, which reflects a user-defined setting. /// It will be `false` if the status item is not visible in the Menu Bar because not enough space was available, /// when using tools such as Bartender to hide status items, or if there's UI covering the status item. @Published public private(set) var isStatusItemOccluded = false public let willShowPanel = PassthroughSubject() public let willClosePanel = PassthroughSubject() private let configuration: Configuration public enum StatusItemView { case button(label: () -> V) case custom(body: () -> V) } public init(configuration: Configuration = .default, statusItem: StatusItemView, content: @escaping @autoclosure () -> Content) { self.configuration = configuration /// This is implemented this way due to a Swift compiler crash 🤦🏻‍♂️ let group: Group<_ConditionalContent, StatusItem>> = Group { switch statusItem { case .button(let label): StatusItemButton { label() } case .custom(let customBody): customBody() } } self.statusItemViewBuilder = { AnyView(erasing: group) } self.contentViewBuilder = { AnyView(erasing: content()) } super.init() } public convenience init(configuration: Configuration = .default, statusItem: StatusItemView, content: @escaping @autoclosure () -> Content) { self.init( configuration: configuration, statusItem: statusItem, content: VUIAppKitViewControllerHost(with: content()) ) } var statusItemToPanelPadding: CGFloat { if screenTopInset > 0 { // Tall Menu Bar (i.e. device with notch) return 12 } else { // Short Menu Bar (i.e. device without notch) return 5 } } private var screenTopInset: CGFloat { guard let screen = panel?.screen ?? NSScreen.main else { return 0 } return screen.safeAreaInsets.top } private var statusItemViewBuilder: () -> AnyView private var contentViewBuilder: () -> AnyView private lazy var cancellables = Set() private lazy var item: NSStatusItem = { let i = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) i.behavior = configuration.behavior i.autosaveName = configuration.autosaveName return i }() private var installed = false public func install() { guard !installed else { return } installed = true setup() } private var eventObservers = [Any]() private var statusItemWindowCancellable: AnyCancellable? private func setup() { registerEventObservers() item.vui_disableVibrancy() let contentView = StatusItemMenuBarExtraView() item.vui_contentView = contentView let hostingView = NSHostingView(rootView: statusItemViewBuilder().environmentObject(self)) observeStatusItemOcclusionState(with: hostingView) hostingView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(hostingView) NSLayoutConstraint.activate([ hostingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), hostingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), hostingView.topAnchor.constraint(equalTo: contentView.topAnchor), hostingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) $isPanelVisible.removeDuplicates().dropFirst(1).sink { [weak self] panelVisible in guard let self = self else { return } if panelVisible { self.panelShown() } else { self.panelHidden() } } .store(in: &cancellables) } /// Call this method when a user preference has changed that affects the visibility of the managed item. func visibilityPreferenceChanged(to newValue: Bool) { logger.debug("Visibility preference changed to \(newValue, privacy: .public)") // Set the flag now to prevent delays for possible obsevers that could cause loops. isStatusItemVisible = newValue item.isVisible = newValue } private var panel: StatusBarContentPanel? public func togglePanelVisible() { if isPanelVisible { hidePanel() isStatusItemHighlighted = false } else { showPanel() isStatusItemHighlighted = true } } private var primaryMenuObserver: Any? public func showPopUpMenu(using builder: () -> NSMenu) { guard let view = item.vui_contentView else { return } let origin = NSEvent.mouseLocation(in: view) let menu = builder() if menu.responds(to: NSSelectorFromString("setAppearance:")) { menu.appearance = NSApp.effectiveAppearance } isStatusItemHighlighted = true primaryMenuObserver = NotificationCenter.default.addObserver(forName: NSMenu.didEndTrackingNotification, object: menu, queue: .main, using: { [weak self] _ in guard let self = self else { return } self.primaryMenuObserver = nil self.isStatusItemHighlighted = false }) menu.popUp(positioning: nil, at: origin, in: view) } public func showPanel() { if let panel { guard !panel.isVisible else { return } } defer { willShowPanel.send() } let basePanelSize = NSSize(width: 300, height: 300) // These were taken from ControlCenter on macOS 12.4 let style: NSWindow.StyleMask = [.fullSizeContentView, .nonactivatingPanel] let level: NSWindow.Level = .popUpMenu let newPanel = StatusBarContentPanel(contentRect: NSRect(origin: .zero, size: basePanelSize), styleMask: style, backing: .buffered, defer: false) newPanel.backgroundColor = NSColor.clear newPanel.isOpaque = false newPanel.collectionBehavior = [.ignoresCycle, .fullScreenAuxiliary, .fullScreenDisallowsTiling] newPanel.hidesOnDeactivate = false newPanel.level = level newPanel.hasShadow = false newPanel.isMovable = false let chrome = StatusBarPanelChrome { self.contentViewBuilder() } let contentController = StatusItemPanelContentController( child: NSHostingController(rootView: chrome) ) newPanel.contentViewController = contentController contentController.view.needsLayout = true newPanel.delegate = self self.panel = newPanel /// Give the system time to perform a layout pass caused by the needsLayout call above, /// so that by the time the panel is shown, we already know the size of the contents. DispatchQueue.main.async { [self] in finishShowingPanel() } isPanelVisible = true } private var contentController: StatusItemPanelContentController? { panel?.contentViewController as? StatusItemPanelContentController } private func finishShowingPanel() { guard let panel, let contentController else { return } contentController.onContentSizeChange = { [weak self] newSize in guard let self = self else { return } guard let panel = self.panel else { return } self.repositionContent(panel, contentSize: newSize, display: true, animate: false) } repositionContent( panel, contentSize: contentController.contentSize, display: false, animate: false ) panel.makeKeyAndOrderFront(nil) } private func repositionContent(_ panel: NSWindow, contentSize: CGSize, display: Bool, animate: Bool) { guard let refView = item.vui_contentView?.superview, let refWindow = refView.window else { assertionFailure("Missing reference status item view or window") return } let reference = refWindow.convertToScreen(refView.frame) let panelFrame = NSRect( x: reference.midX - contentSize.width / 2, y: reference.minY - contentSize.height - statusItemToPanelPadding + StatusBarPanelChromeMetrics.shadowPadding, width: contentSize.width, height: contentSize.height ) #if DEBUG logger.debug("⬛️ contentSize = \(contentSize.width, privacy: .public)x\(contentSize.height, privacy: .public)") #endif panel.setFrame(panelFrame, display: display, animate: animate) } private var panelIsClosing = false public func hidePanel(animated: Bool = true) { guard isPanelVisible, !panelIsClosing else { return } guard animated else { panel?.close() return } panelIsClosing = true NSAnimationContext.runAnimationGroup { [weak self] ctx in guard let panel = self?.panel else { return } panel.animator().alphaValue = 0 } completionHandler: { [weak self] in defer { self?.panelIsClosing = false} guard let panel = self?.panel else { return } panel.close() } } public func windowWillClose(_ notification: Notification) { willClosePanel.send() DispatchQueue.main.async { self.panel = nil self.isStatusItemHighlighted = false self.isPanelVisible = false } } public func windowDidResignKey(_ notification: Notification) { logger.debug("🔑 RESIGNED KEY") delayedHidePanel() } public func windowDidBecomeKey(_ notification: Notification) { logger.debug("🔑 BECAME KEY") panelHideDelayItem?.cancel() panelHideDelayItem = nil } private var panelHideDelayItem: DispatchWorkItem? private func delayedHidePanel() { panelHideDelayItem?.cancel() panelHideDelayItem = nil let item = DispatchWorkItem { [weak self] in guard let self = self else { return } guard self.isPanelVisible, !self.panelIsClosing else { return } /// Do not hide if we're showing a sheet in the panel, which can happen for alerts. guard self.panel?.sheets.isEmpty == true else { self.logger.debug("Panel hiding due to resigning key cancelled: showing a sheet") return } self.logger.debug("Hiding panel due to window resigning key") self.hidePanel() } panelHideDelayItem = item DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: item) } private var highlightOffWorkItem: DispatchWorkItem? func flash() { highlightOffWorkItem?.cancel() highlightOffWorkItem = nil isStatusItemHighlighted = true let item = DispatchWorkItem { [weak self] in self?.isStatusItemHighlighted = false self?.highlightOffWorkItem = nil } highlightOffWorkItem = item DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: item) } private func observeStatusItemOcclusionState(with hostingView: NSView) { hostingView.publisher(for: \.window, options: [.initial, .new]).sink { [weak self] window in guard let self = self else { return } self.statusItemWindowCancellable = nil self.logger.debug("🤲🏻 Status item host window: \(String(describing: window), privacy: .public)") guard let window else { return } self.evaluateStatusItemOcclusion(in: window) self.statusItemWindowCancellable = NotificationCenter.default.publisher(for: NSWindow.didChangeOcclusionStateNotification, object: window).sink(receiveValue: { note in guard let w = note.object as? NSWindow else { return } self.logger.debug("🤲🏻 Status item host window occlusion state: \(w.occlusionState.description, privacy: .public)") self.evaluateStatusItemOcclusion(in: w) }) } .store(in: &cancellables) } private func evaluateStatusItemOcclusion(in window: NSWindow) { let newState = !window.occlusionState.isVisible guard newState != isStatusItemOccluded else { return } isStatusItemOccluded = newState logger.debug("🤲🏻 isStatusItemOccluded = \(newState, privacy: .public)") } deinit { unregisterEventObservers() } } // MARK: - Menu Bar Integration private extension StatusItemManager { private func panelShown() { logger.debug("🪟 \(#function, privacy: .public)") postBeginMenuTrackingNotification() NSApplication.shared.__vui_setMenuBarVisible(true) notify_post("com.apple.hitoolbox.menubar.position.lock") } private func panelHidden() { logger.debug("🪟 \(#function, privacy: .public)") postEndMenuTrackingNotification() notify_post("com.apple.hitoolbox.menubar.position.unlock") } func postBeginMenuTrackingNotification() { let name = "com.apple.HIToolbox.beginMenuTrackingNotification" let pidStr = "\(ProcessInfo.processInfo.processIdentifier)" logger.debug("🪟 \(name, privacy: .public) \(pidStr, privacy: .public)") DistributedNotificationCenter.default().post(name: .init(name), object: pidStr) } func postEndMenuTrackingNotification() { let name = "com.apple.HIToolbox.endMenuTrackingNotification" let pidStr = "\(ProcessInfo.processInfo.processIdentifier)" logger.debug("🪟 \(name, privacy: .public) \(pidStr, privacy: .public)") DistributedNotificationCenter.default().post(name: .init(name), object: pidStr) } func registerEventObservers() { let clickOutsideObserver = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDown) { [weak self] _ in guard let self = self else { return } self.hidePanel() } if let clickOutsideObserver { eventObservers.append(clickOutsideObserver) } let statusItemVisibilityObserver = item.observe(\.isVisible, options: [.new, .old], changeHandler: { [weak self] _, change in guard let self = self else { return } guard let newValue = change.newValue else { return } guard newValue != change.oldValue, newValue != self.isStatusItemVisible else { return } self.logger.debug("Status item visibility changed to \(newValue, privacy: .public)") self.isStatusItemVisible = newValue }) eventObservers.append(statusItemVisibilityObserver) } func unregisterEventObservers() { eventObservers.removeAll() } } extension NSEvent { /// Returns the current mouse cursor location relative to the view's coordinate space. /// Handy for popping up contextual menus in response to clicking a view. static func mouseLocation(in view: NSView) -> NSPoint { let mp = NSEvent.mouseLocation guard let window = view.window else { return mp } let p = window.convertFromScreen(NSRect(origin: mp, size: CGSize(width: 1, height: 1))) return view.convert(p, from: nil).origin } /// Returns the current mouse cursor location relative to the view's coordinate space. /// Handy for popping up contextual menus in response to clicking a view. static func mouseLocation(in window: NSWindow) -> NSPoint { let mp = NSEvent.mouseLocation return window.convertFromScreen(NSRect(origin: mp, size: CGSize(width: 1, height: 1))).origin } } extension NSWindow.OcclusionState: @retroactive CustomStringConvertible { public var description: String { return isVisible ? "\(rawValue) (Visible)" : "\(rawValue) (Hidden)" } var isVisible: Bool { isStrictSuperset(of: .visible) } } ================================================ FILE: VirtualUI/Source/Components/VMArtworkView.swift ================================================ import SwiftUI import VirtualCore public struct VMArtworkView: View { public var virtualMachine: VBVirtualMachine public var showsIcon: Bool public var iconSize: CGFloat private let alwaysUseBlurHash: Bool public init(virtualMachine: VBVirtualMachine, alwaysUseBlurHash: Bool = false, showsIcon: Bool = true, iconSize: CGFloat = 44) { self.virtualMachine = virtualMachine self.showsIcon = showsIcon self.iconSize = iconSize self.alwaysUseBlurHash = alwaysUseBlurHash if alwaysUseBlurHash { self._content = .init(initialValue: .blurHash(virtualMachine.metadata.backgroundHash)) } else { self._content = .init(initialValue: virtualMachine.artworkContent) } } enum Content { case image(Image) case blurHash(BlurHashToken) } @State private var content: Content public var body: some View { ZStack { switch content { case .image(let image): image.resizable() case .blurHash(let token): BlurHashFullBleedBackground(blurHash: token) .fullBleedBackgroundBrightness(-0.2) .fullBleedBackgroundSaturation(1.4) .fullBleedBackgroundIsThumbnail() } if showsIcon, case .blurHash = content { virtualMachine.configuration.systemType.icon .resizable() .foregroundStyle(.white) .aspectRatio(contentMode: .fit) .frame(width: iconSize, height: iconSize) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .task(id: virtualMachine.metadata) { withTransaction(\.disablesAnimations, true) { if alwaysUseBlurHash { self.content = .blurHash(virtualMachine.metadata.backgroundHash) } else { self.content = virtualMachine.artworkContent } } } } } private extension VBVirtualMachine { var artworkContent: VMArtworkView.Content { if let thumbnail { .image(Image(nsImage: thumbnail)) } else { .blurHash(metadata.backgroundHash) } } } #if DEBUG private struct PreviewWrapper: View { var virtualMachine: VBVirtualMachine var body: some View { VMArtworkView(virtualMachine: virtualMachine) .aspectRatio(contentMode: .fill) .frame(width: 480, height: 270) .clipShape(RoundedRectangle(cornerRadius: 8)) .padding(32) } } #Preview("Mac - Thumbnail") { PreviewWrapper(virtualMachine: .preview) } #Preview("Mac - Blur Hash") { PreviewWrapper(virtualMachine: .previewBlurHash) } #Preview("Mac - None") { PreviewWrapper(virtualMachine: .previewNoArtwork) } #Preview("Linux - Thumbnail") { PreviewWrapper(virtualMachine: .previewLinux) } #Preview("Linux - Blur Hash") { PreviewWrapper(virtualMachine: .previewLinuxBlurHash) } #Preview("Linux - None") { PreviewWrapper(virtualMachine: .previewLinuxNoArtwork) } #endif ================================================ FILE: VirtualUI/Source/Definitions/PreviewSupport-VirtualUI.swift ================================================ #if DEBUG import SwiftUI import VirtualCore @MainActor public extension VirtualMachineSessionUI { static let preview = VirtualMachineSessionUI(controller: .preview) } #endif ================================================ FILE: VirtualUI/Source/Definitions/VirtualUIConstants.swift ================================================ // // VirtualUIConstants.swift // VirtualUI // // Created by Guilherme Rambo on 20/07/22. // import Foundation import OSLog @_exported import VirtualCore struct VirtualUIConstants { static let subsystemName = "codes.rambo.VirtualUI" } @available(swift, obsoleted: 1.0, message: "Provided for Objective-C compatibility, don't use in Swift code.") @objcMembers public final class _VirtualUIConstantsObjC: NSObject { public class var subsystemName: String { VirtualUIConstants.subsystemName } } private final class _VirtualUIStub { } public extension Bundle { static let virtualUI = Bundle(for: _VirtualUIStub.self) } ================================================ FILE: VirtualUI/Source/Installer/Components/AuthenticatingWebView.swift ================================================ // // AuthenticatingWebView.swift // VirtualBuddy // // Created by Guilherme Rambo on 07/06/22. // import SwiftUI import WebKit struct AuthenticatingWebView: NSViewControllerRepresentable { typealias NSViewControllerType = AuthenticatingWebViewController let url: URL let onCookiesChanged: ([HTTPCookie]) -> Void init(url: URL, onCookiesChanged: @escaping ([HTTPCookie]) -> Void) { self.url = url self.onCookiesChanged = onCookiesChanged } func makeNSViewController(context: Context) -> AuthenticatingWebViewController { AuthenticatingWebViewController(url: url) { 🍪 in DispatchQueue.main.async { self.onCookiesChanged(🍪) } } } func updateNSViewController(_ nsViewController: AuthenticatingWebViewController, context: Context) { } } final class AuthenticatingWebViewController: NSViewController, WKUIDelegate, WKNavigationDelegate, WKHTTPCookieStoreObserver { var url: URL var cookiesChangedCallback: ([HTTPCookie]) -> Void init(url: URL, cookiesChangedCallback: @escaping ([HTTPCookie]) -> Void) { self.url = url self.cookiesChangedCallback = cookiesChangedCallback super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError() } private var requestSent = false private lazy var webView: WKWebView = { let config = WKWebViewConfiguration() let v = WKWebView(frame: .zero, configuration: config) v.uiDelegate = self v.navigationDelegate = self config.websiteDataStore.httpCookieStore.add(self) return v }() override func loadView() { view = NSView() view.wantsLayer = true webView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(webView) NSLayoutConstraint.activate([ webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), webView.topAnchor.constraint(equalTo: view.topAnchor), webView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } override func viewDidAppear() { super.viewDidAppear() guard !requestSent else { return } requestSent = true webView.load(URLRequest(url: url)) } func cookiesDidChange(in cookieStore: WKHTTPCookieStore) { print("Cookies changed") cookieStore.getAllCookies { [weak self] cookies in self?.cookiesChangedCallback(cookies) } } } ================================================ FILE: VirtualUI/Source/Installer/Components/InstallationConsole.swift ================================================ // // InstallationConsole.swift // VirtualUI // // Created by Guilherme Rambo on 20/07/22. // import SwiftUI import VirtualCore struct InstallationConsole: View { var overridePredicate: LogStreamer.Predicate? = nil private var predicate: LogStreamer.Predicate { overridePredicate ?? .process("com.apple.Virtualization.Installation") } var body: some View { LogConsole(predicate: predicate) .frame(minWidth: 200, maxWidth: .infinity, minHeight: 100, maxHeight: 400) .controlGroup(level: .secondary) } } #if DEBUG #Preview { InstallationConsole(overridePredicate: .process("Xcode")) .padding() .frame(minWidth: 200, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity, alignment: .bottom) } #endif ================================================ FILE: VirtualUI/Source/Installer/Components/InstallationWizardTitle.swift ================================================ // // InstallationWizardTitle.swift // VirtualBuddy // // Created by Guilherme Rambo on 20/07/22. // import SwiftUI struct InstallationWizardTitle: View { var text: String init(_ text: String) { self.text = text } var body: some View { Text(text) .font(.system(.title, design: .rounded).weight(.medium)) .padding(.vertical, 22) .multilineTextAlignment(.center) } } ================================================ FILE: VirtualUI/Source/Installer/Components/RestoreImageURLInputView.swift ================================================ import SwiftUI import VirtualCore struct RestoreImageURLInputView: View { @EnvironmentObject var viewModel: VMInstallationViewModel var body: some View { VirtualBuddyInstallerInputView { TextField("Custom Download Link", text: $viewModel.data.customInstallImageRemoteURL, onCommit: viewModel.next) } .onChange(of: viewModel.data.customInstallImageRemoteURL) { _ in viewModel.validateCustomRemoteURL() } } } #if DEBUG #Preview { VMInstallationWizard.preview(step: .restoreImageInput) } #endif // DEBUG ================================================ FILE: VirtualUI/Source/Installer/Components/VirtualBuddyInstallerInputView.swift ================================================ import SwiftUI struct VirtualBuddyInstallerInputView: View { @ViewBuilder var content: () -> Content @FocusState private var isFocused: Bool var body: some View { content() .focused($isFocused) .task { isFocused = true } .textFieldStyle(.roundedBorder) .controlSize(.large) .padding() .controlGroup() .frame(maxWidth: 500) } } ================================================ FILE: VirtualUI/Source/Installer/Components/VirtualMachineNameInputView.swift ================================================ // // VirtualMachineNameInputView.swift // VirtualUI // // Created by Guilherme Rambo on 01/08/22. // import SwiftUI import VirtualCore struct VirtualMachineNameInputView: View { @Binding var name: String var body: some View { VirtualBuddyInstallerInputView { HStack { TextField("Virtual Machine Name", text: $name) Spacer() Button { name = RandomNameGenerator.shared.newName() } label: { Image(systemName: "arrow.clockwise") .help("Generate new name") } .buttonStyle(.borderless) .font(.system(size: 15, weight: .medium, design: .rounded)) .keyboardShortcut(.init("r", modifiers: .command)) } } } } #if DEBUG #Preview { VMInstallationWizard.preview(step: .name) } #endif // DEBUG ================================================ FILE: VirtualUI/Source/Installer/Steps/GuestTypePicker.swift ================================================ // // GuestTypePicker.swift // VirtualUI // // Created by Guilherme Rambo on 06/03/23. // import SwiftUI import VirtualCore extension VBGuestType { var name: String { switch self { case .mac: return "macOS" case .linux: return "Linux" } } var icon: Image { Image("VBGuestType/\(rawValue)", bundle: .virtualUI) } } struct GuestTypePicker: View { static var buttonRadius: Double { 24 } static var selectionIndicatorID = "SelectionIndicator" @Binding var selection: VBGuestType private var previousMethod: VBGuestType? { VBGuestType.allCases.previous(from: selection) } private var nextMethod: VBGuestType? { VBGuestType.allCases.next(from: selection) } @Namespace private var selectionIndicator var body: some View { HStack(spacing: 16) { ForEach(VBGuestType.supportedByHost) { type in GuestTypeItemView( type: type, isSelected: selection == type, selectionIndicator: selectionIndicator ) .onTapGesture { selection = type } } } .overlay { RoundedRectangle(cornerRadius: Self.buttonRadius, style: .continuous) .stroke(Color.accentColor, lineWidth: 2) .blendMode(.plusLighter) .opacity(0.6) .matchedGeometryEffect(id: Self.selectionIndicatorID, in: selectionIndicator, isSource: false) .animation(.snappy, value: selection) } .accessibilityRepresentation { Picker(selection: $selection) { ForEach(VBGuestType.allCases) { type in Text(type.name) .tag(type) } } label: { } } .keyboardNavigation { direction in if direction == .right { guard let nextMethod else { return } selection = nextMethod } else if direction == .left { guard let previousMethod else { return } selection = previousMethod } } .frame(maxWidth: .infinity, maxHeight: .infinity) } } struct GuestTypeItemView: View { let type: VBGuestType let isSelected: Bool let selectionIndicator: Namespace.ID init(type: VBGuestType, isSelected: Bool, selectionIndicator: Namespace.ID) { self.type = type self.isSelected = isSelected self.selectionIndicator = selectionIndicator } var lineWidth: CGFloat { isSelected ? 2 : 1 } var body: some View { VStack(spacing: 18) { type.icon .resizable() .aspectRatio(contentMode: .fit) .frame(maxHeight: 80) Text(type.name) } .shadow(color: .black.opacity(0.5), radius: 1, x: 0.5, y: 0.5) .padding() .multilineTextAlignment(.center) .font(.system(size: 24, weight: .medium, design: .rounded)) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .aspectRatio(1, contentMode: .fit) .frame(maxWidth: 260) .background { ZStack { // if isSelected { // Rectangle().foregroundStyle(Material.thick) // // Color.accentColor // .blendMode(.plusLighter) // .opacity(0.2) // } else { Rectangle().foregroundStyle(Material.thin) // } } .clipShape(shape) } .chromeBorder(shape: shape, shadowEnabled: false, highlightIntensity: 0.5) .overlay { if isSelected { Rectangle() .opacity(0) .matchedGeometryEffect(id: GuestTypePicker.selectionIndicatorID, in: selectionIndicator, isSource: true) } } } private var borderColor: Color { isSelected ? .accentColor : .primary.opacity(0.2) } private var shape: some InsettableShape { RoundedRectangle(cornerRadius: GuestTypePicker.buttonRadius, style: .continuous) } } #if DEBUG @available(macOS 14.0, *) #Preview { @Previewable @State var selection: VBGuestType = .mac GuestTypePicker(selection: $selection) .padding(22) .frame(width: VMInstallationWizard.maxContentWidth, height: 600) .background(BlurHashFullBleedBackground(blurHash: .virtualBuddyBackground)) } #endif ================================================ FILE: VirtualUI/Source/Installer/Steps/InstallConfigurationStepView.swift ================================================ // // InstallConfigurationStepView.swift // VirtualUI // // Created by Guilherme Rambo on 20/07/22. // import SwiftUI import VirtualCore struct InstallConfigurationStepView: View { @StateObject private var viewModel: VMConfigurationViewModel @State private var vm: VBVirtualMachine var onSave: (VBVirtualMachine) -> Void init(vm: VBVirtualMachine, resolvedRestoreImage: ResolvedRestoreImage? = nil, onSave: @escaping (VBVirtualMachine) -> Void) { self._vm = .init(wrappedValue: vm) self._viewModel = .init(wrappedValue: VMConfigurationViewModel(vm, context: .preInstall, resolvedRestoreImage: resolvedRestoreImage)) self.onSave = onSave } var body: some View { VMConfigurationSheet(configuration: $vm.configuration, customConfirmationButtonAction: { configuration in var updatedVM = vm updatedVM.configuration = configuration self.vm = updatedVM onSave(updatedVM) }) .environmentObject(viewModel) } } #if DEBUG struct VMInstallerConfigurationStepView_Previews: PreviewProvider { static var previews: some View { _Template() } struct _Template: View { @State private var vm = VBVirtualMachine.preview var body: some View { InstallConfigurationStepView(vm: vm, onSave: { _ in }) } } } #endif ================================================ FILE: VirtualUI/Source/Installer/Steps/InstallMethod.swift ================================================ // // InstallMethod.swift // VirtualUI // // Created by Guilherme Rambo on 06/03/23. // import Foundation import VirtualCore enum InstallMethod: String, Identifiable, CaseIterable, Codable, ProvidesEmptyPlaceholder { var id: RawValue { rawValue } case remoteOptions case localFile case remoteManual static var empty: InstallMethod { .remoteOptions } } enum InstallMethodSelection: Identifiable, Hashable, Codable { case remoteOptions(RestoreImage) case localFile(URL) case remoteManual(URL) var id: InstallMethod { switch self { case .remoteOptions: .remoteOptions case .localFile: .localFile case .remoteManual: .remoteManual } } } extension InstallMethod { func description(for type: VBGuestType) -> String { switch self { case .localFile: switch type { case .mac: return "Open custom IPSW file from local storage" case .linux: return "Open custom ISO file from local storage" } case .remoteOptions: return "Download \(type.name) installer from a list of options" case .remoteManual: return "Download \(type.name) installer from a custom URL" } } var imageName: String { switch self { case .localFile: return "folder.fill" case .remoteOptions: return "square.and.arrow.down.fill" case .remoteManual: return "text.cursor" } } } extension VBGuestType { var customURLPrompt: String { switch self { case .mac: return "Enter the macOS IPSW URL" case .linux: return "Enter the Linux ISO URL" } } var restoreImagePickerPrompt: String { "Pick a \(name) Version to Install" } var installFinishedMessage: String { "Your \(name) Virtual Machine is Ready!" } } ================================================ FILE: VirtualUI/Source/Installer/Steps/InstallMethodPicker.swift ================================================ // // InstallMethodPicker.swift // VirtualBuddy // // Created by Guilherme Rambo on 07/06/22. // import SwiftUI struct InstallMethodPicker: View { var guestType: VBGuestType @Binding var selection: InstallMethod @FocusState private var isFocused: Bool private var selectionIndex: Int { InstallMethod.allCases.firstIndex(of: selection) ?? 0 } private var previousMethod: InstallMethod? { guard selectionIndex > 0 else { return nil } return InstallMethod.allCases[selectionIndex - 1] } private var nextMethod: InstallMethod? { guard selectionIndex < InstallMethod.allCases.count - 1 else { return nil } return InstallMethod.allCases[selectionIndex + 1] } var body: some View { VStack(spacing: 16) { ForEach(InstallMethod.allCases) { method in InstallMethodView( method: method, description: method.description(for: guestType), isSelected: selection == method ) .onTapGesture { selection = method } } } .accessibilityRepresentation { Picker(selection: $selection) { ForEach(InstallMethod.allCases) { method in Text(method.description(for: guestType)) .tag(method) } } label: { } } .overlay { /// Horrible hack to hide the focus ring while still allowing for keyboard navigation. Rectangle() .frame(width: 0, height: 0) .opacity(0) .focusable(true) .focused($isFocused) .onMoveCommand { direction in if direction == .down { guard let nextMethod else { return } selection = nextMethod } else if direction == .up { guard let previousMethod else { return } selection = previousMethod } } } .onAppearOnce { isFocused = true } } } struct InstallMethodView: View { let method: InstallMethod let description: String let isSelected: Bool var lineWidth: CGFloat { isSelected ? 2 : 1 } var body: some View { HStack { Image(systemName: method.imageName) Text(description) } .foregroundColor(isSelected ? .accentColor : .secondary) .padding() .multilineTextAlignment(.center) .font(.system(size: 14)) .frame(maxWidth: .infinity, alignment: .leading) .overlay(shape.stroke(borderColor, style: StrokeStyle(lineWidth: lineWidth))) .materialBackground(.menu, blendMode: .withinWindow, state: isSelected ? .active : .inactive, in: shape) .clipShape(shape) .shadow(color: .black.opacity(0.24), radius: 8, x: 0, y: 0) } private var borderColor: Color { isSelected ? .accentColor : .primary.opacity(0.2) } private var shape: some Shape { RoundedRectangle(cornerRadius: 14, style: .continuous) } } #if DEBUG struct InstallMethodPicker_Previews: PreviewProvider { static var previews: some View { InstallMethodPicker(guestType: .mac, selection: .constant(.remoteOptions)) } } #endif ================================================ FILE: VirtualUI/Source/Installer/Steps/InstallProgressStepView.swift ================================================ // // InstallProgressStepView.swift // VirtualUI // // Created by Guilherme Rambo on 20/07/22. // import SwiftUI import VirtualCore import Virtualization struct InstallProgressStepView: View { @EnvironmentObject var viewModel: VMInstallationViewModel private var progress: Double? { switch viewModel.state { case .loading(let progress, _): progress ?? 0 case .idle: 0 case .error: nil } } private var status: Text? { switch viewModel.state { case .loading(_, let info): info.flatMap { Text($0) } case .error(let message): Text(message) case .idle: Text("Installing") } } private var style: VirtualBuddyMonoStyle { switch viewModel.state { case .idle, .loading: .default case .error: .failure } } var body: some View { if let status { VirtualBuddyMonoProgressView(progress: progress, status: status, style: style) .textSelection(.enabled) } else if let virtualMachine = viewModel.virtualMachine { InstallerVirtualMachineView(virtualMachine: virtualMachine) .frame(maxWidth: .infinity, maxHeight: .infinity) } else { VirtualBuddyMonoProgressView(progress: progress, status: Text(""), style: style) } } } private struct InstallerVirtualMachineView: NSViewRepresentable { typealias NSViewType = VZVirtualMachineView let virtualMachine: VZVirtualMachine func makeNSView(context: Context) -> VZVirtualMachineView { VZVirtualMachineView(frame: .zero) } func updateNSView(_ nsView: VZVirtualMachineView, context: Context) { nsView.virtualMachine = virtualMachine } } #if DEBUG #Preview { VMInstallationWizard.preview(step: .install) } #endif ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/Components/BlurHashFullBleedBackground.swift ================================================ import SwiftUI import VirtualCore import BuddyKit private extension EnvironmentValues { @Entry var fullBleedBackgroundTransitionDuration: TimeInterval = BlurHashFullBleedBackground.defaultTransitionDuration @Entry var fullBleedBackgroundBrightness: Double = BlurHashFullBleedBackground.defaultBrightness @Entry var fullBleedBackgroundSaturation: Double = BlurHashFullBleedBackground.defaultSaturation @Entry var fullBleedBackgroundBlurRadius: Double = BlurHashFullBleedBackground.defaultBlurRadius @Entry var fullBleedBackgroundIsThumbnail: Bool = false var fullBleedBackgroundDimmed: Bool { get { fullBleedBackgroundBrightness < BlurHashFullBleedBackground.defaultBrightness } set { fullBleedBackgroundBrightness = newValue ? BlurHashFullBleedBackground.defaultBrightnessDimmed : BlurHashFullBleedBackground.defaultBrightness fullBleedBackgroundSaturation = newValue ? BlurHashFullBleedBackground.defaultSaturationDimmed : BlurHashFullBleedBackground.defaultSaturation } } } public extension View { func fullBleedBackgroundDimmed(_ dimmed: Bool = true) -> some View { environment(\.fullBleedBackgroundDimmed, dimmed) } func fullBleedBackgroundBrightness(_ brightness: Double?) -> some View { environment(\.fullBleedBackgroundBrightness, brightness ?? BlurHashFullBleedBackground.defaultBrightness) } func fullBleedBackgroundSaturation(_ saturation: Double?) -> some View { environment(\.fullBleedBackgroundSaturation, saturation ?? BlurHashFullBleedBackground.defaultSaturation) } func fullBleedBackgroundBlurRadius(_ radius: Double?) -> some View { environment(\.fullBleedBackgroundBlurRadius, radius ?? BlurHashFullBleedBackground.defaultBlurRadius) } func fullBleedBackgroundIsThumbnail(_ isThumbnail: Bool = true) -> some View { environment(\.fullBleedBackgroundIsThumbnail, isThumbnail) } } struct BlurHashFullBleedBackground: View { static let defaultTransitionDuration: TimeInterval = 1.0 static let defaultBrightness: Double = -0.2 static let defaultBrightnessDimmed: Double = -0.3 static let defaultSaturation: Double = 1.3 static let defaultSaturationDimmed: Double = 0.8 static let defaultBlurRadius: Double = 22 static let reduceTransparencyMultiplierSaturation: Double = 0.4 static let reduceTransparencyOffsetBrightness: Double = -0.15 static let reduceTransparencyMultiplierBlurRadius: Double = 2.0 enum Content: Hashable { case blurHash(BlurHashToken) case customImage(NSImage) } var content: Content? init(content: Content?) { self.content = content } init(blurHash: BlurHashToken?) { self.content = blurHash.flatMap { .blurHash($0) } } init(image: NSImage?) { self.content = image.flatMap { .customImage($0) } } init(_ blurHashValue: String?) { self.init(blurHash: blurHashValue.flatMap { BlurHashToken(value: $0) }) } @Environment(\.fullBleedBackgroundIsThumbnail) private var isThumbnail @Environment(\.accessibilityReduceTransparency) private var reduceTransparency var body: some View { ZStack { _BlurHashRepresentable(content: content) if reduceTransparency, !isThumbnail { Color(nsColor: .windowBackgroundColor) .opacity(0.5) } } .ignoresSafeArea() .frame(maxWidth: .infinity, maxHeight: .infinity) } } private struct _BlurHashRepresentable: NSViewRepresentable { typealias Content = BlurHashFullBleedBackground.Content var content: Content? typealias NSViewType = _BlurHashNSView func makeNSView(context: Context) -> _BlurHashNSView { _BlurHashNSView(frame: .zero) } func updateNSView(_ nsView: _BlurHashNSView, context: Context) { nsView.animationsDisabled = context.transaction.disablesAnimations nsView.transitionDuration = context.environment.fullBleedBackgroundTransitionDuration switch content { case .blurHash(let token): nsView.blurHash = token case .customImage(let image): nsView.customImage = image case .none: nsView.blurHash = nil nsView.customImage = nil } /// Reduce transparency modifications only apply when the view is being used as a background, not when it's used as a standalone image thumbnail. let reduceTransparency = context.environment.accessibilityReduceTransparency && !context.environment.fullBleedBackgroundIsThumbnail let brightnessOffset = reduceTransparency ? BlurHashFullBleedBackground.reduceTransparencyOffsetBrightness : 0 let saturationMultiplier = reduceTransparency ? BlurHashFullBleedBackground.reduceTransparencyMultiplierSaturation : 1 let blurRadiusMultiplier = reduceTransparency ? BlurHashFullBleedBackground.reduceTransparencyMultiplierBlurRadius : 1 nsView.brightness = context.environment.fullBleedBackgroundBrightness + brightnessOffset nsView.saturation = context.environment.fullBleedBackgroundSaturation * saturationMultiplier nsView.blurRadius = context.environment.fullBleedBackgroundBlurRadius * blurRadiusMultiplier } final class _BlurHashNSView: NSView { private lazy var assetLayer: CALayer = .load(assetNamed: "FullBleedBlurHash", bundle: .virtualUI) ?? CALayer() var blurHash: BlurHashToken? { didSet { guard blurHash != oldValue else { return } image = blurHash.flatMap { NSImage.blurHash($0) } } } var customImage: NSImage? { didSet { guard customImage != oldValue else { return } guard let customImage else { image = nil return } let scale = 0.01 let size = CGSize(width: customImage.size.width * scale, height: customImage.size.height * scale) UILog("Custom image size: \(size)") image = NSImage(size: size, flipped: true) { rect in customImage.draw(in: rect) return true } } } private var lastRenderedToken: BlurHashToken? private var lastRenderedImage: NSImage? @Invalidating(.layout) private var image: NSImage? = nil @Invalidating(.layout) var transitionDuration: TimeInterval = BlurHashFullBleedBackground.defaultTransitionDuration @Invalidating(.layout) var animationsDisabled: Bool = false var brightness: Double = BlurHashFullBleedBackground.defaultBrightness { didSet { withCurrentEnvironment { assetLayer.setValue(brightness, forKeyPath: "filters.colorBrightness.inputAmount") } } } var saturation: Double = BlurHashFullBleedBackground.defaultSaturation { didSet { withCurrentEnvironment { assetLayer.setValue(saturation, forKeyPath: "filters.colorSaturate.inputAmount") } } } var blurRadius: Double = BlurHashFullBleedBackground.defaultBlurRadius { didSet { withCurrentEnvironment { assetLayer.setValue(blurRadius, forKeyPath: "filters.gaussianBlur.inputRadius") } } } override func layout() { CATransaction.begin() CATransaction.setDisableActions(true) if assetLayer.superlayer == nil { platformLayer.addSublayer(assetLayer) } assetLayer.frame = bounds CATransaction.commit() guard blurHash != lastRenderedToken || image != lastRenderedImage else { return } defer { lastRenderedToken = blurHash lastRenderedImage = image } withCurrentEnvironment { assetLayer.contents = image } } private func withCurrentEnvironment(perform block: () -> ()) { CATransaction.begin() CATransaction.setDisableActions(animationsDisabled) CATransaction.setAnimationDuration(animationsDisabled ? 0 : transitionDuration) block() CATransaction.commit() } } } #if DEBUG extension BlurHashToken { static let previewSequoia = BlurHashToken(value: "eN86q8M1Hbyya7t-g$MpnTx,b;k9X8Vgr?osbHenWEeYoGj@aNaPah") static let previewSonoma = BlurHashToken(value: "ec8rzYaIWBj?a}iqosaxj?fkRFa2axayj[t%ofV[ayf6V]pGkBf5fi") static let previewVentura = BlurHashToken(value: "enHk%=ocoeW:Nb-8xEODaya#1fWWJAWExDEjR-jGazoJWCagw^s-Wp") } private struct BlurHashFullBleedBackgroundPreview: View { @State var token = BlurHashToken.virtualBuddyBackground @State private var brightness: Double = BlurHashFullBleedBackground.defaultBrightness @State private var effectiveBrightness: Double = BlurHashFullBleedBackground.defaultBrightness @State private var dimmed = false private let tokens: [BlurHashToken] = [ .virtualBuddyBackground, .previewSequoia, .previewSonoma, .previewVentura ] var enableCycle = true var body: some View { BlurHashFullBleedBackground(blurHash: token) /// Swap below environment modifiers to be able to test arbitrary brightness values in preview // .environment(\.fullBleedBackgroundBrightness, effectiveBrightness) .environment(\.fullBleedBackgroundDimmed, dimmed) .task(id: enableCycle) { guard enableCycle else { return } let delay = 3 while true { do { try await Task.sleep(for: .seconds(delay)) let index = tokens.firstIndex(of: token)! if index < tokens.count - 1 { token = tokens[index + 1] } else { token = tokens[0] } } catch { break } } } .frame(width: 512, height: 512) .overlay(alignment: .bottom) { Form { Slider(value: $brightness, in: -1.0...1.0) { HStack { Text("Brightness") Text(brightness, format: .percent.rounded(rule: .toNearestOrEven, increment: 1)) .foregroundStyle(.secondary) .monospacedDigit() .frame(width: 40, alignment: .trailing) } } Toggle("Dimmed", isOn: $dimmed) } .frame(width: 400) .padding() .background(Material.thin, in: RoundedRectangle(cornerRadius: 16)) .chromeBorder(radius: 16) .padding() } .onChange(of: brightness) { newValue in withTransaction(\.disablesAnimations, true) { effectiveBrightness = newValue } } } } #Preview("Blur Hash") { BlurHashFullBleedBackgroundPreview() } #Preview("Custom Image") { BlurHashFullBleedBackground(image: .blurHashPreview) } private extension NSImage { static let blurHashPreview = NSImage(contentsOfFile: "/System/Library/Desktop Pictures/Sonoma.heic")! } #endif // DEBUG ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/Components/CatalogGroupPicker.swift ================================================ import SwiftUI import VirtualCore struct CatalogGroupPicker: View { static let buttonAspectRatio: Double = 320 / 180 @EnvironmentObject private var controller: RestoreImageSelectionController var groups: [ResolvedCatalogGroup]? @Binding var selectedGroup: ResolvedCatalogGroup? @State private var scrolledGroupID: ResolvedCatalogGroup.ID? var width: CGFloat { 220 } var spacing: CGFloat { containerPadding } var body: some View { if #available(macOS 14.0, *) { container .scrollPosition(id: $scrolledGroupID, anchor: .center) } else { container } } @Environment(\.containerPadding) private var containerPadding @FocusState private var focus: RestoreImageSelectionFocus? @ViewBuilder private var container: some View { ScrollView(.vertical, showsIndicators: false) { LazyVStack(alignment: .center, spacing: spacing) { if #available(macOS 14.0, *) { list.scrollTargetLayout() } else { list } } .frame(width: width) .padding([.top, .leading, .bottom], containerPadding) .padding(.trailing, containerPadding * 0.5) } .focusable() .focused($focus, equals: RestoreImageSelectionFocus.groups) .backported_focusEffectDisabled() .onMoveCommand { direction in switch direction { case .up: if let previous = groups?.previous(from: selectedGroup) { selectedGroup = previous } case .down: if let next = groups?.next(from: selectedGroup) { selectedGroup = next } case .right: controller.focusedElement = .images default: break } } .accessibilityRepresentation { if let groups { Picker("Choose option", selection: $selectedGroup) { ForEach(groups) { group in Text(group.name) .tag(group) } } } } .onChange(of: selectedGroup?.id) { groupID in withAnimation(.snappy) { scrolledGroupID = groupID } } .onReceive(controller.$focusedElement) { focus = $0 } } @ViewBuilder private var list: some View { if let groups { ForEach(groups) { group in groupButton(for: group) } } else if controller.isLoading { /// Placeholders are only displayed when controller is loading to avoid jumps when loading happens quickly (or not at all). ForEach(0...5, id: \.self) { _ in groupButton(for: .placeholder) } } } @ViewBuilder private func groupButton(for group: ResolvedCatalogGroup) -> some View { Button { selectedGroup = group } label: { CatalogGroupView(group: group) } .buttonStyle(CatalogGroupButtonStyle(isSelected: group.id == selectedGroup?.id)) .aspectRatio(Self.buttonAspectRatio, contentMode: .fit) } } struct CatalogGroupButtonStyle: ButtonStyle { var isSelected: Bool @Environment(\.isFocused) private var isFocused func makeBody(configuration: Configuration) -> some View { configuration.label .chromeBorder(radius: CatalogGroupView.cornerRadius, highlightEnabled: !isSelected) .overlay { if isSelected { shape .strokeBorder(Color.white, lineWidth: 2) .blendMode(.plusLighter) .opacity(isFocused ? 0.8 : 0.4) } } .scaleEffect(configuration.isPressed ? 0.98 : 1) } private var shape: some InsettableShape { RoundedRectangle(cornerRadius: CatalogGroupView.cornerRadius, style: .continuous) } } #if DEBUG #Preview { VMInstallationWizard.preview } #endif ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/Components/CatalogGroupView.swift ================================================ import SwiftUI import VirtualCore struct CatalogGroupView: View { static var cornerRadius: CGFloat { 14 } var group: ResolvedCatalogGroup var thumbnail: CatalogGraphic.Thumbnail { group.darkImage.thumbnail } @Environment(\.redactionReasons) private var redaction var body: some View { ZStack { RemoteImage( url: thumbnail.url, blurHash: thumbnail.blurHash, blurHashSize: .vbBlurHashSize ) Text(group.name) .font(.system(size: 32, weight: .semibold, design: .rounded)) .shadow(color: .black.opacity(0.2), radius: 3) .lineLimit(1) .minimumScaleFactor(0.3) .padding(.horizontal, 22) .opacity(redaction.isEmpty ? 1 : 0) } .clipShape(shape) .contentShape(shape) } private var shape: some InsettableShape { RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous) } } extension CatalogGroupView { static let placeholder = CatalogGroupView(group: .placeholder) } #if DEBUG @available(macOS 14.0, *) #Preview { @Previewable @State var isSelected = false Button { isSelected.toggle() } label: { CatalogGroupView(group: ResolvedCatalog.previewMac.groups[0]) } .buttonStyle(CatalogGroupButtonStyle(isSelected: isSelected)) .aspectRatio(320/180, contentMode: .fit) .frame(width: 320, height: 180) .padding(64) } #endif ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/Components/InstallProgressDisplayView.swift ================================================ import SwiftUI import VirtualCore import BuddyKit struct InstallProgressDisplayView: View { @EnvironmentObject private var viewModel: VMInstallationViewModel var body: some View { VirtualDisplayView { VStack { switch viewModel.step { case .download: RestoreImageDownloadView() case .install: InstallProgressStepView() case .done: VirtualBuddyMonoProgressView( status: Text(viewModel.data.systemType.installFinishedMessage), style: .success ) default: EmptyView() } } } } } #if DEBUG #Preview { VMInstallationWizard.preview(step: .done) } #endif ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/Components/RestoreImageBrowser.swift ================================================ // // RestoreImageBrowser.swift // VirtualBuddy // // Created by Guilherme Rambo on 02/08/24. // import SwiftUI import VirtualCore import Combine import BuddyKit struct ChannelGroup: Identifiable, Hashable { var id: CatalogChannel.ID { channel.id } var order: Int var channel: CatalogChannel var images: [ResolvedRestoreImage] } struct RestoreImageBrowser: View { @EnvironmentObject private var controller: RestoreImageSelectionController @Binding var selection: ResolvedRestoreImage? init(selection: Binding) { self._selection = selection } @Environment(\.containerPadding) private var containerPadding @Environment(\.maxContentWidth) private var maxContentWidth @FocusState private var focus: RestoreImageSelectionFocus? @State private var scrolledImageID: ResolvedRestoreImage.ID? var body: some View { Group { if #available(macOS 14.0, *) { scrollView .scrollPosition(id: $scrolledImageID, anchor: .center) } else { scrollView } } .safeAreaInset(edge: .top, spacing: 0) { Color.clear.frame(height: containerPadding) } .safeAreaInset(edge: .bottom, spacing: 0) { Color.clear.frame(height: containerPadding) } .focusable() .focused($focus, equals: RestoreImageSelectionFocus.images) .backported_focusEffectDisabled() .onMoveCommand { direction in switch direction { case .down: if let previous = controller.images.next(from: controller.selectedRestoreImage) { controller.selectedRestoreImage = previous } case .up: if let next = controller.images.previous(from: controller.selectedRestoreImage) { controller.selectedRestoreImage = next } case .left: controller.focusedElement = .groups default: break } } .onChange(of: selection) { image in guard let image else { return } scrolledImageID = image.id guard image.id != controller.selectedRestoreImage?.id else { return } controller.selectedRestoreImage = image } .onReceive(controller.$focusedElement) { focus = $0 } .onReceive(controller.$selectedRestoreImage.removeDuplicates()) { guard let newSelection = $0 else { return } guard newSelection.id != selection?.id else { return } guard newSelection.image.group == controller.selectedGroup?.id else { return } selection = $0 } } @Environment(\.redactionReasons) private var redaction @ViewBuilder private var scrollView: some View { ScrollView(.vertical, showsIndicators: false) { if #available(macOS 14.0, *) { stack.scrollTargetLayout() } else { stack } } } @ViewBuilder private var stack: some View { LazyVStack(alignment: .leading, spacing: 8, pinnedViews: .sectionHeaders) { if redaction.isEmpty { ForEach(controller.channelGroups) { group in section(for: group) } } else if controller.isLoading { /// Placeholders are only displayed when controller is loading to avoid jumps when loading happens quickly (or not at all). ForEach(0...12, id: \.self) { _ in RestoreImageButton(image: .placeholder, isSelected: false, action: { }, deleteDownload: { }) } } } .padding(.trailing, containerPadding) .padding(.leading, containerPadding * 0.5) } @ViewBuilder private func section(for group: ChannelGroup) -> some View { Section { ForEach(group.images) { image in RestoreImageButton(image: image, isSelected: image.id == selection?.id) { selection = image } deleteDownload: { controller.deleteLocalDownload(for: image) } .tag(image) } } } } private struct RestoreImageButton: View { var image: ResolvedRestoreImage var isSelected: Bool var action: () -> () var deleteDownload: () -> () var body: some View { Button { action() } label: { label } .buttonStyle(RestoreImageButtonStyle(isSelected: isSelected)) } @State private var showingSupportDetail = false @ViewBuilder var label: some View { VStack(alignment: .leading, spacing: 6) { HStack { downloadState Spacer() details supportState } if case .unsupported(let title, _) = image.status, let title { Button { showingSupportDetail.toggle() } label: { Text(title) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity, alignment: .trailing) .font(.subheadline) } .buttonStyle(.borderless) .foregroundStyle(Color.red) .blendMode(.plusLighter) .popover(isPresented: $showingSupportDetail) { RestoreImageFeatureDetailView(image: image) } } } .monospacedDigit() .contextMenu { Button("Copy Download Link") { Pasteboard.general.string = image.url.absoluteString } Button("Copy Build Number") { Pasteboard.general.string = image.build } if image.isDownloaded { Divider() Button(role: .destructive) { deleteDownload() } label: { Text("Delete Download…") } } } } @ViewBuilder private var downloadState: some View { HStack { Image(systemName: image.isDownloaded ? "internaldrive" : "arrow.down.circle") .frame(width: 16) .foregroundStyle(.secondary) .help(image.isDownloaded ? "This version is available from your previous downloads." : "This version needs to be downloaded.") Text(image.name) .minimumScaleFactor(0.8) .lineLimit(1) .help(image.name) } .font(.headline) } @ViewBuilder private var details: some View { HStack(spacing: 4) { Text(image.build) Text("·") Text(image.formattedDownloadSize) } .font(.subheadline) .foregroundStyle(.secondary) .multilineTextAlignment(.trailing) } @ViewBuilder private var supportState: some View { RestoreImageFeatureStatusButton(image: image) } } struct RestoreImageFeatureStatusButton: View { let image: ResolvedRestoreImage var status: ResolvedFeatureStatus { image.status } var helpText: String { switch status { case .supported: "This version is supported on your Mac. Click for details about supported features." case .warning(let title, let message): title ?? message case .unsupported(let title, let message): title ?? message } } @State private var showingDetail = false var body: some View { Button { showingDetail.toggle() } label: { FeatureStatusLabel(status: status) } .buttonStyle(.borderless) .help(helpText) .popover(isPresented: $showingDetail) { RestoreImageFeatureDetailView(image: image) } } } struct FeatureStatusLabel: View { var status: ResolvedFeatureStatus var body: some View { Image(systemName: status.systemImage) .foregroundStyle(status.color) .symbolVariant(.circle.fill) } } struct RestoreImageFeatureDetailView: View { static var padding: Double { 12 } let image: ResolvedRestoreImage var body: some View { VStack(spacing: 16) { topLevelStatus VStack(alignment: .leading, spacing: 8) { ForEach(image.features) { feature in RestoreImageFeatureDetailItem(feature: feature) } } } .frame(width: 340, alignment: .leading) .padding() } @ViewBuilder private var topLevelStatus: some View { switch image.status { case .supported: EmptyView() case .warning(let title, let message), .unsupported(let title, let message): VStack(alignment: .leading, spacing: 12) { HStack { FeatureStatusLabel(status: image.status) if let title { Text(title) } else { Text("This Version May Not Work") } } .imageScale(.large) .font(.headline) Group { if let attributedMessage = try? AttributedString(markdown: message, options: .init(allowsExtendedAttributes: true, interpretedSyntax: .inlineOnlyPreservingWhitespace, failurePolicy: .returnPartiallyParsedIfPossible, languageCode: nil)) { Text(attributedMessage) } else { Text(message) } } .fixedSize(horizontal: false, vertical: true) } .frame(maxWidth: .infinity, alignment: .leading) .padding() .background { image.status.color .blendMode(.plusDarker) .opacity(0.2) } .controlGroup() .textSelection(.enabled) } } } struct RestoreImageFeatureDetailItem: View { let feature: ResolvedVirtualizationFeature var status: ResolvedFeatureStatus { feature.status } var body: some View { HStack(alignment: .center) { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 4) { FeatureStatusLabel(status: status) Text(feature.name) Spacer() } .font(.headline) Text(feature.detail) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } } .textSelection(.enabled) .padding(RestoreImageFeatureDetailView.padding) .frame(maxWidth: .infinity, alignment: .leading) .controlGroup(level: .secondary) } } extension ResolvedFeatureStatus { var topLevelTitle: String { switch self { case .supported: "This Version Should Work" case .warning: "This Version May Not Work" case .unsupported: "This Version Will Not Work" } } var systemImage: String { switch self { case .supported: "checkmark" case .warning: "exclamationmark.triangle" case .unsupported: "xmark" } } var color: Color { switch self { case .supported: .green case .warning: .yellow case .unsupported: .red } } var textColor: Color { switch self { case .supported: .green case .warning, .unsupported: .yellow } } var subtitle: String { switch self { case .supported: "Supported" case .warning: "Warning" case .unsupported: "Not Supported" } } } struct RestoreImageButtonStyle: ButtonStyle { var isSelected = false var cornerRadius: CGFloat = 14 @Environment(\.isFocused) private var isFocused func makeBody(configuration: Configuration) -> some View { configuration.label .padding(.vertical, 12) .padding(.horizontal, 16) .frame(maxWidth: .infinity, alignment: .leading) .background(Material.thin, in: shape) .background(Color.black.opacity(0.14).blendMode(.plusDarker), in: shape) .chromeBorder(radius: cornerRadius, highlightEnabled: !isSelected, shadowEnabled: false, highlightIntensity: 0.4) .overlay { if isSelected { shape .strokeBorder(Color.white, lineWidth: 2) .blendMode(.plusLighter) .opacity(isFocused ? 0.8 : 0.4) } } } private var shape: some InsettableShape { RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) } } extension ResolvedRestoreImage { var formattedDownloadSize: String { ByteCountFormatter.string(fromByteCount: downloadSize, countStyle: .file) } } #if DEBUG @available(macOS 14.0, *) #Preview { VMInstallationWizard.preview } #Preview("Feature Detail") { RestoreImageFeatureDetailView(image: .previewMac) } #endif ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/Components/SoftwareCatalog+Placeholder.swift ================================================ import SwiftUI import VirtualCore extension String { static let placeholderID = "__PLACEHOLDER__" } extension URL { static let catalogPlaceholder = URL(string: "https://example.com")! static let catalogGroupPlaceholderImage = Bundle.virtualUI.url(forResource: "CatalogGroupPlaceholder", withExtension: "heic") ?? URL(filePath: "/dev/null") } extension CatalogGraphic { static let placeholder = CatalogGraphic( id: .placeholderID, url: .catalogGroupPlaceholderImage, thumbnail: CatalogGraphic.Thumbnail( url: .catalogGroupPlaceholderImage, width: 340, height: 720, blurHash: "U0Eo[I?bfQ?b?bj[fQj[fQfQfQfQ?bj[fQj[" ) ) } extension CatalogGroup { static let placeholder = CatalogGroup( id: .placeholderID, name: "macOS Placeholder", majorVersion: "15.0", image: .placeholder, darkImage: .placeholder ) } extension ResolvedCatalogGroup { static let placeholder = ResolvedCatalogGroup( group: .placeholder, restoreImages: [] ) } extension RestoreImage { static let placeholder = RestoreImage( id: .placeholderID, group: .placeholderID, channel: .placeholderID, requirements: .placeholderID, name: "macOS 15.3 Developer Beta", build: "ABC123F", version: "15.3", mobileDeviceMinVersion: "1.0", url: .catalogPlaceholder, downloadSize: 1024 * 1024 * 1024 * 8 ) } extension CatalogChannel { static let placeholder = CatalogChannel(id: .placeholderID, name: "Placeholder", note: "Placeholder", icon: "checkmark.seal") } extension RequirementSet { static let placeholder = RequirementSet(id: .placeholderID, minCPUCount: 0, minMemorySizeMB: 0, minVersionHost: "1.0") } extension ResolvedRequirementSet { static let placeholder = ResolvedRequirementSet(requirements: .placeholder, status: .supported) } extension SoftwareCatalog { static let placeholder = SoftwareCatalog(apiVersion: 1, minAppVersion: "1.0", channels: [.placeholder], groups: [.placeholder], restoreImages: [.placeholder], features: [], requirementSets: [.placeholder], deviceSupportVersions: []) } extension ResolvedRestoreImage { static let placeholder = ResolvedRestoreImage(image: .placeholder, channel: .placeholder, features: [], requirements: .placeholder, status: .supported, localFileURL: nil, deviceSupportVersion: nil) } ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/Components/VirtualBuddyMonoIcon.swift ================================================ import SwiftUI import BuddyKit struct VirtualBuddyMonoIcon: View { var size: Double = 90 var style: VirtualBuddyMonoStyle = .default var resource: ImageResource { switch style { case .default: .virtualBuddyMono case .success: .virtualBuddyMonoHappy case .failure: .virtualBuddyMonoSad } } var body: some View { Image(resource) .resizable() .aspectRatio(contentMode: .fit) .frame(width: size, height: size) } } ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/Components/VirtualBuddyMonoProgressView.swift ================================================ import SwiftUI import VirtualCore import BuddyKit enum VirtualBuddyMonoStyle: Hashable { case `default` case success case failure } struct VirtualBuddyMonoProgressView: View { var progress: Double? var status: Text var style: VirtualBuddyMonoStyle = .default var spacing: Double { 16 } private var foregroundColor: Color { switch style { case .default: .white case .success: .green case .failure: .red } } var body: some View { VStack { Spacer() VirtualBuddyMonoIcon(style: style) Spacer() VStack(spacing: spacing) { RamRodProgressView(progress: progress ?? 0) .opacity(style == .default && progress != nil ? 1 : 0) status .font(.subheadline) } .padding(.bottom, spacing) } .monospacedDigit() .frame(width: 240) .multilineTextAlignment(.center) .foregroundStyle(foregroundColor) .tint(foregroundColor) } } private struct RamRodProgressView: View { var progress: Double var body: some View { ZStack { Rectangle().fill(Color(white: 0.16)) ProgressBarShapeView(progress: progress) } .clipShape(shape) .overlay(shape.stroke(Color(white: 0.25), lineWidth: 1)) .frame(height: 6) } private var shape: some InsettableShape { Capsule(style: .continuous) } } /** You see, animating a white rectangle growing in width is a very expensive operation that SwiftUI is completely unable to do by itself without consuming an unhealthy amount of CPU, so this uses Core Animation instead to offload that expensive computation to the WindowServer/GPU. */ private struct ProgressBarShapeView: NSViewRepresentable { typealias NSViewType = _Representable var progress: Double = 0 func makeNSView(context: Context) -> _Representable { _Representable(frame: .zero) } func updateNSView(_ nsView: _Representable, context: Context) { nsView.progress = progress } final class _Representable: NSView { private lazy var bar = CALayer() @Invalidating(.layout) var progress: Double = 0 override init(frame frameRect: NSRect) { super.init(frame: frameRect) platformLayer.addSublayer(bar) bar.backgroundColor = .white bar.anchorPoint = CGPoint(x: 0, y: 0.5) } required init?(coder: NSCoder) { fatalError() } override func layout() { super.layout() CATransaction.begin() CATransaction.setDisableActions(true) bar.position = CGPoint(x: bounds.minX, y: bounds.midY) bar.frame.size.height = bounds.height CATransaction.commit() CATransaction.begin() CATransaction.setAnimationDuration(0.2) CATransaction.setAnimationTimingFunction(.init(name: .linear)) bar.frame.size.width = bounds.width * progress CATransaction.commit() } } } #if DEBUG #Preview { VMInstallationWizard.preview(step: .download) } #endif // DEBUG ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/Components/VirtualDisplayView.swift ================================================ import SwiftUI import BuddyKit /// A view that simulates a display chrome, currently used during installation. struct VirtualDisplayView: View { @ViewBuilder var content: () -> Content @EnvironmentObject private var viewModel: VMInstallationViewModel static var cornerRadius: CGFloat { 12 } var body: some View { ZStack { content() .frame(maxWidth: .infinity, maxHeight: .infinity) } .aspectRatio(16/9, contentMode: .fit) .background(Color(white: 0.03)) .overlay { LinearGradient(colors: [.white.opacity(0.7), .white.opacity(0.1)], startPoint: .init(x: 0.2, y: 0), endPoint: .init(x: 0.3, y: 1.2)) .blendMode(.plusLighter) .opacity(0.1) } .clipShape(shape) .chromeBorder(shape: shape, highlightEnabled: false) } private var shape: some InsettableShape { RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous) } } #if DEBUG #Preview { VMInstallationWizard.preview(step: .download) } #endif ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/RestoreImageSelectionController.swift ================================================ import SwiftUI import VirtualCore import Combine import OSLog enum RestoreImageSelectionFocus: Hashable { case groups case images } extension VBAPIClient { static let shared = VBAPIClient() } @MainActor final class RestoreImageSelectionController: ObservableObject { /// If loading takes less than this amount of time, then the controller will never even set the `isLoading` property. private static let minLoadingTimeInMilliseconds = 100 private let logger = Logger(subsystem: VirtualUIConstants.subsystemName, category: String(describing: RestoreImageSelectionController.self)) init() { $selectedGroup.removeDuplicates().sink { [weak self] group in guard let self else { return } guard let group else { return } /// Selected group has changed, update available channel groups, images, and selected image. let updatedChannelGroups = ChannelGroup.groups(with: group.restoreImages) channelGroups = updatedChannelGroups images = updatedChannelGroups.flatMap(\.images) selectedRestoreImage = updatedChannelGroups.first?.images.first } .store(in: &cancellables) } private lazy var api = VBAPIClient.shared private var cancellables = Set() @Published private(set) var catalog: ResolvedCatalog? @Published private(set) var channelGroups: [ChannelGroup] = [] @Published private(set) var images: [ResolvedRestoreImage] = [] @Published var selectedGroup: ResolvedCatalogGroup? @Published var selectedRestoreImage: ResolvedRestoreImage? @Published var errorMessage: String? @Published var focusedElement = RestoreImageSelectionFocus.groups @Published private(set) var isLoading = false { didSet { if !isLoading { deferredLoadingTask?.cancel() deferredLoadingTask = nil } } } /// The controller will only set the`isLoading` property if loading takes a while. private var deferredLoadingTask: Task? private func deferredStartLoading() { deferredLoadingTask?.cancel() deferredLoadingTask = Task { [weak self] in guard let self else { return } defer { deferredLoadingTask = nil } do { try await Task.sleep(for: .milliseconds(Self.minLoadingTimeInMilliseconds)) logger.debug("Reached loading time delay, setting isLoading.") isLoading = true } catch { } } } private var inputCatalog: SoftwareCatalog? private var guestType = VBGuestType.mac func loadRestoreImageOptions(for guest: VBGuestType, skipCache: Bool = false) { logger.debug("Loading restore image options.") guestType = guest deferredStartLoading() Task { let start = ContinuousClock.now defer { logger.debug("Loading restore images took \(start.duration(to: .now).formatted(.units(allowed: [.milliseconds])), privacy: .public)") isLoading = false } do { #if DEBUG if UserDefaults.standard.bool(forKey: "VBSimulateSlowCatalogFetch") { logger.notice("⚠️ Delaying restore image options load due to VBSimulateSlowCatalogFetch debug flag!") try await Task.sleep(for: .seconds(2)) } #endif let catalog = try await api.fetchRestoreImages(for: guest, skipCache: skipCache) inputCatalog = catalog await refreshResolvedCatalog(with: catalog) } catch { logger.error("Loading restore images failed - \(error, privacy: .public)") await MainActor.run { self.catalog = nil self.errorMessage = error.localizedDescription } } } } private func refreshResolvedCatalog(with catalog: SoftwareCatalog) async { logger.debug(#function) let platform: CatalogGuestPlatform = guestType == .linux ? .linux : .mac let resolved = ResolvedCatalog(environment: .current.guest(platform: platform), catalog: catalog) await MainActor.run { self.selectedGroup = resolved.groups.first(where: { $0.id == selectedGroup?.id }) ?? resolved.groups.first self.selectedRestoreImage = selectedGroup?.restoreImages.first(where: { $0.id == self.selectedRestoreImage?.id }) self.catalog = resolved } } func deleteLocalDownload(for image: ResolvedRestoreImage) { logger.debug("Delete download requested for \(image.id)") /// Remove selection to force refresh of image browser. selectedRestoreImage = nil do { let fileURL = try image.localFileURL.require("File not found.") Task { do { try await NSWorkspace.shared.recycle([fileURL]) if let inputCatalog { await refreshResolvedCatalog(with: inputCatalog) } } catch { logger.error("Recycle failed for \(fileURL.path) - \(error, privacy: .public)") NSApp.presentError(error) } } } catch { logger.error("Delete download failed for \(image.id) - \(error, privacy: .public)") NSApp.presentError(error) } } } extension ChannelGroup { static func groups(with restoreImages: [ResolvedRestoreImage]) -> [ChannelGroup] { var groupsByChannel = [CatalogChannel: ChannelGroup]() for image in restoreImages { /// Ensures images from each channel group are listed in the same order as the channels are ordered in the catalog. let order = groupsByChannel.keys.count groupsByChannel[image.channel, default: ChannelGroup( order: order, channel: image.channel, images: [] )].images.append(image) } return groupsByChannel.values.sorted(by: { $0.order < $1.order }) } } ================================================ FILE: VirtualUI/Source/Installer/Steps/Restore Image Selection/RestoreImageSelectionStep.swift ================================================ // // RestoreImageSelectionStep.swift // VirtualBuddy // // Created by Guilherme Rambo on 20/07/22. // import SwiftUI import VirtualCore import Combine struct RestoreImageSelectionStep: View { @StateObject private var controller = RestoreImageSelectionController() @EnvironmentObject private var viewModel: VMInstallationViewModel @Environment(\.containerPadding) private var containerPadding @Environment(\.maxContentWidth) private var maxContentWidth private var browserInsetTop: CGFloat { 100 } var body: some View { HStack(spacing: 0) { CatalogGroupPicker(groups: controller.catalog?.groups, selectedGroup: $controller.selectedGroup) RestoreImageBrowser(selection: $viewModel.data.resolvedRestoreImage) } .redacted(reason: controller.isLoading ? .placeholder : []) .frame(maxWidth: maxContentWidth) .frame(maxWidth: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity) .environmentObject(controller) .task(id: viewModel.data.systemType) { loadOptions() } .task(id: controller.selectedGroup) { if let group = controller.selectedGroup { viewModel.data.backgroundHash = BlurHashToken(value: group.darkImage.thumbnail.blurHash) } else { viewModel.data.backgroundHash = .virtualBuddyBackground } } .toolbar { ToolbarItemGroup(placement: .primaryAction) { Button { loadOptions(skipCache: true) } label: { Image(systemName: "arrow.clockwise") } .help(Text("Reload")) .keyboardShortcut("r", modifiers: .command) } } } private func loadOptions(skipCache: Bool = false) { controller.loadRestoreImageOptions(for: viewModel.data.systemType, skipCache: skipCache) } } #if DEBUG #Preview { VMInstallationWizard.preview } #endif ================================================ FILE: VirtualUI/Source/Installer/Steps/RestoreImageDownloadView.swift ================================================ // // RestoreImageDownloadView.swift // VirtualUI // // Created by Guilherme Rambo on 20/07/22. // import SwiftUI import VirtualCore import Combine struct RestoreImageDownloadView: View { @EnvironmentObject var viewModel: VMInstallationViewModel @State private var downloadState = DownloadState.idle private var progress: Double? { switch downloadState { case .idle, .preCheck: 0 case .failed: nil case .downloading(let progress, _): progress ?? 0 case .done: 1 } } private var status: Text { switch downloadState { case .idle: Text("Preparing Download") case .preCheck(let message): Text(message) case .downloading(_, let eta): eta.flatMap { Text(formattedETA(from: $0)) } ?? Text("Downloading") case .done: Text("Done!") case .failed(let message): Text(message) } } private var style: VirtualBuddyMonoStyle { switch downloadState { case .idle, .downloading, .preCheck: .default case .failed: .failure case .done: .success } } var body: some View { VirtualBuddyMonoProgressView( progress: progress, status: status, style: style ) .onReceive(viewModel.$downloadState) { downloadState = $0 } } private func formattedETA(from eta: Double) -> String { let time = Int(eta) let seconds = time % 60 let minutes = (time / 60) % 60 let hours = (time / 3600) if hours >= 1 { return String(format: "%0.2d:%0.2d:%0.2d",hours,minutes,seconds) } else { return String(format: "%0.2d:%0.2d",minutes,seconds) } } } #if DEBUG #Preview { VMInstallationWizard.preview(step: .download) } #endif ================================================ FILE: VirtualUI/Source/Installer/VMInstallData.swift ================================================ import Foundation import VirtualCore import BuddyKit struct VMInstallData: Hashable, Codable { // MARK: Persisted State @DecodableDefault.EmptyPlaceholder var systemType: VBGuestType = .empty var installMethod: InstallMethod { installMethodSelection?.id ?? .empty } var installMethodSelection: InstallMethodSelection? = nil var backgroundHash: BlurHashToken = .virtualBuddyBackground var name = RandomNameGenerator.shared.newName() /// URL to the local restore image that will be used to restore the VM. /// This will be the custom local file selected by the user, or the URL to the local file /// that's been downloaded from either a custom remote URL or a selected restore image option. private(set) var localRestoreImageURL: URL? = nil enum CodingKeys: String, CodingKey { /// Cookie is not stored because it would end up in clear text on the file system when the struct is encoded... case systemType, installMethodSelection, backgroundHash, name, localRestoreImageURL } // MARK: Temporary State /// This can be the restore image selected by the user in the UI, or a matching restore image /// from the software catalog inferred from a user-provided custom download link or existing local file. @MainActor private(set) var restoreImage: RestoreImage? = nil { didSet { /// Ensure background hash is set to whatever restore image group ends up being used, /// even if it's matched from user-provided file/url. if let group = catalog.groups.first(where: { $0.id == restoreImage?.group }), let value = group.darkImage?.thumbnail.blurHash { backgroundHash = BlurHashToken(value: value) } else { backgroundHash = systemType == .mac ? .virtualBuddyBackground : .virtualBuddyBackgroundLinux } } } var resolvedRestoreImage: ResolvedRestoreImage? = nil { didSet { restoreImage = resolvedRestoreImage?.image localRestoreImageURL = resolvedRestoreImage?.localFileURL } } @DecodableDefault.EmptyString var customInstallImageRemoteURL: String = "" var cookie: String? = nil } // MARK: Convenience extension VMInstallData { var downloadURL: URL? { switch installMethodSelection { case .remoteManual(let url): url case .remoteOptions(let image): image.url case .localFile: nil case .none: nil } } @MainActor var catalog: SoftwareCatalog { SoftwareCatalog.current(for: systemType) } } // MARK: Updates / Validation extension VMInstallData { func canContinue(from step: VMInstallationStep) -> Bool { switch step { case .systemType: true case .restoreImageInput: installMethodSelection != nil case .restoreImageSelection: restoreImage != nil case .name: !name.isEmpty case .configuration: true // TODO: Implement case .download: true // TODO: Implement case .install: true // TODO: Implement case .done: true // TODO: Implement } } private static let allowedCustomDownloadSchemes: Set = [ "http", "https", "ftp" ] func validateCustomRestoreImageRemoteURL() -> Bool { guard !customInstallImageRemoteURL.isEmpty else { return false } guard let url = URL(string: customInstallImageRemoteURL) else { return false } guard let scheme = url.scheme else { return false } guard Self.allowedCustomDownloadSchemes.contains(scheme.lowercased()) else { return false } return true } mutating func commitSelectedRestoreImage() throws { UILog("\(#function) \(String(optional: restoreImage?.url.absoluteString.quoted))") installMethodSelection = try .remoteOptions(restoreImage.require("Please select one of the OS versions available.")) } @MainActor mutating func commitCustomRestoreImageURL() throws { UILog("\(#function) \(customInstallImageRemoteURL.quoted)") let customURL = try URL(string: customInstallImageRemoteURL).require("Invalid URL: \(customInstallImageRemoteURL.quoted).") installMethodSelection = .remoteManual(customURL) /// Attempt to resolve a catalog image for custom URL. resolveCatalogImage(for: customURL) } @MainActor mutating func commitCustomRestoreImageLocalFile(path: String) { UILog("\(#function) \(path.quoted)") let fileURL = URL(fileURLWithPath: path) installMethodSelection = .localFile(fileURL) commitLocalRestoreImageURL(fileURL) /// Attempt to resolve a catalog image for custom local file. resolveCatalogImage(for: fileURL, localFileURL: fileURL) } @MainActor mutating func resolveCatalogImageIfNeeded(with model: VBVirtualMachine) throws { guard resolvedRestoreImage == nil else { return } switch installMethodSelection { case .remoteOptions(let restoreImage): resolvedRestoreImage = try model.resolveCatalogImage(restoreImage) case .remoteManual(let url): resolveCatalogImage(for: url) case .localFile(let url): resolveCatalogImage(for: url, localFileURL: url) case .none: break } } mutating func commitLocalRestoreImageURL(_ url: URL) { localRestoreImageURL = url } /// Removes any data associated with the current install method selection if the new selection is a different install method. mutating func resetInstallMethodSelectionIfNeeded(selectedMethod: InstallMethod) { guard let installMethodSelection else { return } guard selectedMethod != installMethodSelection.id else { return } self.installMethodSelection = nil self.resolvedRestoreImage = nil } var needsDownload: Bool { guard let downloadURL else { return false } switch installMethodSelection { case .none: UILog("[\(#function)] ⚠️ Method is nil!") return false case .localFile: UILog("[\(#function)] Method is \(installMethod), download never needed.") return false case .remoteManual, .remoteOptions: /// `localRestoreImageURL` is set when user selects a remote URL but that file has already been downloaded to the local library. /// Check that the file name matches and skip download when that's the case. /// Also ensure the local file actually exists, as it could have been deleted before the setup process read this property. if let localRestoreImageURL, localRestoreImageURL.lastPathComponent == downloadURL.lastPathComponent, FileManager.default.fileExists(atPath: localRestoreImageURL.path) { UILog("[\(#function)] Method is \(installMethod), remote URL is \(downloadURL.absoluteString.quoted), found matching download at \(localRestoreImageURL.path.quoted).") return false } else { UILog("[\(#function)] Method is \(installMethod), remote URL is \(downloadURL.absoluteString.quoted), download is needed.") return true } } } @MainActor mutating func resetRestoreImageSelection() { installMethodSelection = nil resolvedRestoreImage = nil localRestoreImageURL = nil restoreImage = nil } } // MARK: - Catalog Resolution private extension VMInstallData { @MainActor mutating func resolveCatalogImage(for url: URL, localFileURL: URL? = nil) { guard var resolved = catalog.resolvedRestoreImage(matching: url, guestType: systemType) else { restoreImage = catalog.restoreImageMatchingDownloadableCatalogContent(at: url) return } if let localFileURL { resolved.localFileURL = localFileURL } resolvedRestoreImage = resolved } } extension VBVirtualMachine.Metadata { mutating func updateRestoreImageURLs(with data: VMInstallData) { /// Always save whatever URL the restore image was downloaded from and the local file URL, regardless of the install method. if let downloadURL = data.downloadURL { updateInstallImageURL(downloadURL) } if let localRestoreImageURL = data.localRestoreImageURL { updateInstallImageURL(localRestoreImageURL) } } } ================================================ FILE: VirtualUI/Source/Installer/VMInstallationViewModel.swift ================================================ // // VMInstallationViewModel.swift // VirtualBuddy // // Created by Guilherme Rambo on 07/06/22. // import Foundation import UniformTypeIdentifiers import Combine import Virtualization import VirtualCore import BuddyKit public enum VMInstallationStep: Int, Hashable, Codable { case systemType case restoreImageInput case restoreImageSelection case name case configuration case download case install case done } @MainActor final class VMInstallationViewModel: ObservableObject, @unchecked Sendable { struct RestorableState: Codable { var data: VMInstallData var step: Step } typealias Step = VMInstallationStep enum State: Hashable { case idle case loading(_ progress: Double?, _ info: String?) case error(_ message: String) } private var restorableState: RestorableState { RestorableState( data: self.data, step: self.step ) } @Published var machine: VBVirtualMachine? @Published var data = VMInstallData() { didSet { guard data != oldValue else { return } validate() } } @Published private(set) var state = State.idle @Published var step = Step.systemType { didSet { guard step != oldValue else { return } performActions(for: step) writeRestorationData() } } var canGoBack: Bool { switch step { case .systemType: false case .restoreImageInput, .restoreImageSelection: /// Only allow going back from restore image selection if a VM bundle has not been created yet. /// It's possible for this condition to be reached when user goes back after the name step, /// and we don't want the user to change the guest type after that. machine == nil case .name, .configuration, .done: true case .download: if case .failed = downloadState { true } else { false } case .install: if case .error = state { true } else { false } } } @Published private(set) var buttonTitle = "Continue" @Published private(set) var showNextButton = true @Published var disableNextButton = false private let library: VMLibraryController init(library: VMLibraryController, restoring restoreVM: VBVirtualMachine?) { self.library = library /// Skip OS selection if there's only a single supported OS. step = VBGuestType.supportedByHost.count > 1 ? .systemType : .restoreImageSelection if let restoreVM { restoreInstallation(with: restoreVM) } } init(library: VMLibraryController, restoringAt restoreURL: URL?, initialStep: Step? = nil) { self.library = library /// Skip OS selection if there's only a single supported OS. step = initialStep ?? (VBGuestType.supportedByHost.count > 1 ? .systemType : .restoreImageSelection) if let restoreURL { restoreInstallation(with: restoreURL) } } private func restoreInstallation(with url: URL) { do { let vm = try VBVirtualMachine(bundleURL: url) restoreInstallation(with: vm) } catch { assertionFailure("Couldn't restore install: \(error)") NSAlert(error: error).runModal() } } private func restoreInstallation(with model: VBVirtualMachine) { do { guard let restoreData = model.installRestoreData else { throw CocoaError(.coderInvalidValue, userInfo: [NSLocalizedDescriptionKey: "VM is missing install restore data"]) } var restoredState = try PropertyListDecoder.virtualBuddy.decode(RestorableState.self, from: restoreData) try restoredState.data.resolveCatalogImageIfNeeded(with: model) self.data = restoredState.data self.machine = model self.step = restoredState.step } catch { assertionFailure("Couldn't restore install: \(error)") NSAlert(error: error).runModal() } } private var preventTerminationAssertion: PreventTerminationAssertion? private func startPreventingAppTermination(forReason reason: String) { stopPreventingAppTermination() preventTerminationAssertion = NSApp.preventTermination(reason: reason) } private func stopPreventingAppTermination() { preventTerminationAssertion?.invalidate() preventTerminationAssertion = nil } private func writeRestorationData() { guard var machine else { return } machine.metadata.updateRestoreImageURLs(with: data) do { let restoreData = try PropertyListEncoder.virtualBuddy.encode(restorableState) machine.installRestoreData = restoreData try machine.saveMetadata() self.machine = machine } catch { assertionFailure("Failed to save install restore data: \(error)") } } @Published private(set) var downloader: DownloadBackend? @SubjectPublisher private(set) var downloadState: DownloadState = .idle private func validate() { disableNextButton = !data.canContinue(from: step) } func next() { /// It's possible for the user to go back in case of a failed download/install, /// in which case the naming step will be skipped because the VM has already been named. func goNextAfterRestoreImageSelection() { if machine == nil { step = .name } else { step = .configuration } } switch step { case .systemType: step = .restoreImageSelection case .restoreImageInput: commitCustomRestoreImageURL() goNextAfterRestoreImageSelection() case .restoreImageSelection: commitSelectedRestoreImage() goNextAfterRestoreImageSelection() case .name: step = .configuration case .configuration: step = data.needsDownload ? .download : .install case .download: step = .install case .install: step = .done case .done: break } } func back() { guard canGoBack else { return } switch step { case .systemType, .done: break case .restoreImageInput: selectInstallMethod(.remoteOptions) case .restoreImageSelection: data.backgroundHash = .virtualBuddyBackground step = .systemType case .download, .install: /// Always nuke restore image selection when backing out of download/install. /// This ensures that the new selection will be respected when moving forward again. data.resetRestoreImageSelection() state = .idle step = .restoreImageSelection case .name, .configuration: /// **NOTE:** When going back from configuration step, all configuration is lost due to how configuration saving is currently tied /// to the specific "continue" button in the configuration sheet view. This will be addressed alongside other configuration UI changes soon... /// Re-trigger any UI prompts associated with the install method, such as entering remote URL or selecting local file. selectInstallMethod(data.installMethod) } } private func performActions(for step: Step) { defer { validate() } switch step { case .systemType: showNextButton = true case .restoreImageInput: showNextButton = true validateCustomRemoteURL() case .restoreImageSelection: showNextButton = true case .name: showNextButton = true case .configuration: showNextButton = true do { try prepareModel() } catch { state = .error("Failed to prepare VM model: \(error.localizedDescription)") } case .download: showNextButton = false DispatchQueue.main.async { self.setupDownload() } case .install: Task { await startInstallation() } showNextButton = false case .done: showNextButton = true buttonTitle = "Back to Library" } } func selectInstallMethod(_ method: InstallMethod) { UILog("\(#function) \(method)") data.resetInstallMethodSelectionIfNeeded(selectedMethod: method) switch method { case .localFile: selectInstallFile() case .remoteOptions: step = .restoreImageSelection case .remoteManual: step = .restoreImageInput } } private func commitCustomRestoreImageURL() { do { try data.commitCustomRestoreImageURL() } catch { state = .error("\(error)") } } private func commitSelectedRestoreImage() { guard data.installMethod != .localFile, data.installMethod != .remoteManual else { return } do { try data.commitSelectedRestoreImage() } catch { state = .error("\(error)") } } private func createDownloadBackend(cookie: String?) -> DownloadBackend { let Backend: DownloadBackend.Type #if DEBUG if UserDefaults.standard.bool(forKey: "VBSimulateDownload") || ProcessInfo.isSwiftUIPreview { Backend = SimulatedDownloadBackend.self } else { Backend = URLSessionDownloadBackend.self } #else Backend = URLSessionDownloadBackend.self #endif return Backend.init(library: library, cookie: cookie) } private var downloadURL: URL? { #if DEBUG guard let url = data.downloadURL else { if ProcessInfo.isSwiftUIPreview { return .catalogPlaceholder } else { assertionFailure("Expected download URL to be available for download") return nil } } return url #else return data.downloadURL #endif } private func setupDownload() { guard let url = downloadURL else { return } /// Run TSS check before download to prevent user from spending the time/bandwidth to download a build that will not install successfully. if data.systemType == .mac, VBSettings.current.enableTSSCheck { UILog("Requesting TSS check before download.") Task { downloadState = .preCheck("Checking Signing Status") let status = await VBAPIClient.shared.signingStatus(for: url) switch status { case .signed: UILog("TSS check signed, proceeding with download.") startDownload(with: url) case .unsigned(let message): UILog("TSS check UNSIGNED, failing now.") downloadState = .failed(message) stopPreventingAppTermination() case .checkFailed: /// Performing the check itself failed is ignored to avoid server-side issues preventing users from downloading/installing macOS. UILog("Ignoring check failed TSS status.") startDownload(with: url) } } } else { startDownload(with: url) } } private func startDownload(with url: URL) { let backend = createDownloadBackend(cookie: data.cookie) backend.statePublisher.sink { [weak self] state in self?.downloadState = state } .store(in: &cancellables) self.downloader = backend backend.statePublisher .receive(on: DispatchQueue.main) .sink { [weak self] state in guard let self = self else { return } switch state { case .done(let localURL): stopPreventingAppTermination() self.handleDownloadCompleted(with: localURL) case .failed: downloader = nil stopPreventingAppTermination() case .idle, .preCheck, .downloading: break } }.store(in: &cancellables) startPreventingAppTermination(forReason: "downloading operating system image") backend.startDownload(with: url) } func handleDownloadCompleted(with fileURL: URL) { downloader = nil do { /// Attach metadata to the file so that VirtualBuddy can match it with the restore image in the software catalog /// even if the user renames the file or moves it within the same volume. if data.systemType == .mac, let restoreImage = data.restoreImage { UILog("Attaching metadata for \(restoreImage.name.quoted) to downloaded file \(fileURL.lastPathComponent.quoted)") fileURL.vb_softwareCatalogData = .init(restoreImage) } try updateModelInstallerURL(with: fileURL) next() } catch { state = .error("Failed to update the virtual machine settings after downloading the installer. \(error)") } } private lazy var cancellables = Set() private var installer: RestoreBackend? private var progressObservation: NSKeyValueObservation? private func prepareModel() throws { /// It's possible for the model to already be available in case installation failed and the user navigated back to restore image selection. guard machine == nil else { return } let vmURL = library.libraryURL .appendingPathComponent(data.name) .appendingPathExtension(VBVirtualMachine.bundleExtension) var model: VBVirtualMachine switch data.systemType { case .mac: model = try VBVirtualMachine(bundleURL: vmURL, isNewInstall: true) case .linux: model = try VBVirtualMachine(creatingLinuxMachineAt: vmURL) } model.metadata.backgroundHash = data.backgroundHash self.machine = model writeRestorationData() } private func updateModelInstallerURL(with newURL: URL) throws { assert(machine != nil || ProcessInfo.isSwiftUIPreview, "This method requires the VM model to be available") assert(newURL.isFileURL || ProcessInfo.isSwiftUIPreview, "This method should be updating the installer URL with a local file URL, not a remote one!") guard var machine else { return } data.commitLocalRestoreImageURL(newURL) machine.metadata.updateRestoreImageURLs(with: data) try machine.saveMetadata() } private func startInstallation() async { switch machine?.configuration.systemType { case .mac: startMacInstallation() case .linux: await startLinuxInstallation() case .none: state = .error("Missing VM model or system type") } } @available(macOS 13, *) private func startLinuxInstallation() async { guard let installURL = data.localRestoreImageURL else { state = .error("Missing install image URL") return } guard let model = machine else { state = .error("Missing VM model") return } do { let config = try await VMInstance.makeConfiguration(for: model, installImageURL: installURL) try config.validate() step = .done } catch let error as VZError { handleVirtualMachineValidationError(error) } catch { state = .error(error.localizedDescription) } } private func handleVirtualMachineValidationError(_ error: VZError) { UILog("Validation failed with error \(error)") let baseMessage = """ Virtualization error \(error.code.rawValue). \(error.localizedDescription) """ switch error.code { case .invalidDiskImage, .invalidRestoreImage, .restoreImageLoadFailed: let imageType = switch data.systemType { case .mac: "restore image" case .linux: "iso image" } let detail = """ The \(imageType) on your computer may be corrupted from a faulty download. Please delete the existing download and try again, or use a different \(imageType). """ state = .error(baseMessage + "\n" + detail) default: state = .error(baseMessage) } } private func createRestoreBackend(for model: VBVirtualMachine, restoreURL: URL) -> RestoreBackend { let Backend: RestoreBackend.Type #if DEBUG if UserDefaults.standard.bool(forKey: "VBSimulateInstall") || ProcessInfo.isSwiftUIPreview { Backend = SimulatedRestoreBackend.self } else if restoreURL == SimulatedDownloadBackend.localFileURL { UILog("⚠️ Using simulated installer because the download was also simulated.") Backend = SimulatedRestoreBackend.self } else { Backend = VirtualizationRestoreBackend.self } #else Backend = VirtualizationRestoreBackend.self #endif return Backend.init(model: model, restoringFromImageAt: restoreURL) } @Published private(set) var virtualMachine: VZVirtualMachine? = nil private var installationTask: Task? private func startMacInstallation() { installationTask = Task { await _runMacInstallation() } } private func _runMacInstallation() async { guard let restoreURL = data.localRestoreImageURL else { state = .error("Missing local restore image URL") return } guard let model = machine else { state = .error("Missing VM model") return } state = .loading(nil, "Preparing Installation\nThis may take a moment") let backend = createRestoreBackend(for: model, restoreURL: restoreURL) installer = backend if let realBackend = backend as? VirtualizationRestoreBackend { realBackend.virtualMachine.assign(to: &$virtualMachine) } progressObservation = backend.progress.observe(\.completedUnitCount) { [weak self] progress, _ in guard let self = self else { return } DispatchQueue.main.async { let percent = Double(progress.completedUnitCount) / Double(progress.totalUnitCount) self.state = .loading(percent, nil) } } @Sendable func cleanup() { DispatchQueue.main.async { [weak self] in guard let self else { return } self.stopPreventingAppTermination() self.library.loadMachines() self.installationTask = nil self.cleanupInstallerArtifacts() } } defer { cleanup() } await withTaskCancellationHandler { do { startPreventingAppTermination(forReason: "restoring virtual machine") try await backend.install() try Task.checkCancellation() self.machine?.metadata.backgroundHash = self.data.backgroundHash self.machine?.metadata.installFinished = true self.step = .done UILog("Installation task finished successfully") } catch is CancellationError { } catch let error as VZError { handleVirtualMachineValidationError(error) } catch { UILog("Installation task finished with error \(error)") self.state = .error(error.localizedDescription) } } onCancel: { UILog("Installation task cancelled") cleanup() } } func validateCustomRemoteURL() { let isValid = data.validateCustomRestoreImageRemoteURL() disableNextButton = !isValid } func selectInstallFile() { guard let url = NSOpenPanel.run(accepting: data.systemType.supportedRestoreImageTypes, defaultDirectoryKey: "restoreImage") else { return } data.commitCustomRestoreImageLocalFile(path: url.path) next() } private func cleanupInstallerArtifacts() { UILog(#function) progressObservation?.invalidate() progressObservation = nil virtualMachine = nil installer = nil } var confirmBeforeClosing: () async -> Bool { { [weak self] in guard let self else { return true } guard self.needsConfirmationBeforeClosing else { return true } let confirmed = await NSAlert.runConfirmationAlert( title: "Cancel Installation?", message: "If you close the window now, the virtual machine will not be ready for use. You can continue the installation later.", continueButtonTitle: "Cancel Installation", cancelButtonTitle: "Continue" ) guard confirmed else { return false } await cancelInstallation() return true } } func cancelInstallation() async { UILog(#function) downloader?.cancelDownload() downloader = nil await installer?.cancel() installationTask?.cancel() } private var needsConfirmationBeforeClosing: Bool { /// Require confirmation as soon as a VM bundle has been created for this install and it's not finished. step != .done && machine != nil } } ================================================ FILE: VirtualUI/Source/Installer/VMInstallationWizard.swift ================================================ // // VMInstallationWizard.swift // VirtualBuddy // // Created by Guilherme Rambo on 07/06/22. // import SwiftUI import VirtualCore import Combine extension EnvironmentValues { /// Defines the padding for a container where the children must adopt the padding in their implementations. /// Currently used for the `VMInstallationWizard` to allow children to apply padding in a custom way, /// retaining the standard padding between all steps. @Entry var containerPadding: CGFloat = 16 /// The maximum width for the content area in the current context. @Entry var maxContentWidth: CGFloat? = nil } public struct VMInstallationWizard: View { static var padding: CGFloat { 22 } static var maxContentWidth: CGFloat { 720 } @ObservedObject var library: VMLibraryController @StateObject var viewModel: VMInstallationViewModel @Environment(\.dismiss) var closeWindow public init(library: VMLibraryController, restoringAt restoreURL: URL? = nil, initialStep: VMInstallationStep? = nil) { self._library = .init(initialValue: library) self._viewModel = .init(wrappedValue: VMInstallationViewModel(library: library, restoringAt: restoreURL, initialStep: initialStep)) } private let stepValidationStateChanged = PassthroughSubject() /// Some step views can't have the default padding applied because they need /// to handle padding in a specific way. Those may read `containerPadding` from the environment. private var effectivePadding: CGFloat { switch viewModel.step { case .restoreImageSelection, .configuration: 0 default: Self.padding } } private var effectiveMaxContentWidth: CGFloat { switch viewModel.step { case .configuration: VMConfigurationSheet.minWidth default: Self.maxContentWidth } } private var hideBottomBar: Bool { switch viewModel.step { case .systemType, .restoreImageInput, .restoreImageSelection, .name: false case .configuration, .download, .install, .done: true } } @State private var showingConsole = false public var body: some View { NavigationStack { VStack { switch viewModel.step { case .systemType: guestSystemTypeSelection case .restoreImageInput: restoreImageURLInput case .restoreImageSelection: restoreImageSelection case .configuration: configureVM case .name: renameVM case .download, .install, .done: InstallProgressDisplayView().environmentObject(viewModel) } } .onReceive(stepValidationStateChanged) { isValid in viewModel.disableNextButton = !isValid } .navigationTitle(Text("Virtual Machine Setup")) .navigationSubtitle(Text(viewModel.step.subtitle)) .padding(effectivePadding) } .toolbar { ToolbarItemGroup(placement: .navigation) { Button { if viewModel.step == .done { closeWindow() } else { viewModel.back() } } label: { Image(systemName: "chevron.left") } .disabled(!viewModel.canGoBack) } ToolbarItemGroup(placement: .confirmationAction) { if viewModel.step == .done { Button("Done") { closeWindow() } .keyboardShortcut(.defaultAction) } } ToolbarItemGroup(placement: .primaryAction) { if viewModel.step == .install { Toggle(isOn: $showingConsole) { Image(systemName: "terminal") } .help("Logs") } } } .frame(minWidth: 800, maxWidth: .infinity, minHeight: 600, maxHeight: .infinity) .overlay(alignment: .bottom) { if showingConsole { InstallationConsole() .padding(.horizontal, Self.padding * 2) .padding(.bottom, Self.padding) .transition(.move(edge: .bottom)) } } .animation(.snappy, value: showingConsole) .safeAreaInset(edge: .bottom, spacing: 0) { if !hideBottomBar { bottomBar } } .background { BlurHashFullBleedBackground(blurHash: viewModel.data.backgroundHash) .fullBleedBackgroundDimmed(dimBackground) } .environment(\.containerPadding, Self.padding) .environment(\.maxContentWidth, effectiveMaxContentWidth) .confirmBeforeClosingWindow { [weak viewModel] in await viewModel?.confirmBeforeClosing() ?? true } } private var dimBackground: Bool { switch viewModel.step { case .systemType: false case .restoreImageInput: false case .restoreImageSelection: false case .name: false case .configuration: false case .download: true case .install: true case .done: false } } @ViewBuilder private var bottomBar: some View { HStack { Group { switch viewModel.step { case .restoreImageSelection: HStack(spacing: 12) { Button("Local File") { viewModel.selectInstallMethod(.localFile) } Divider() .frame(height: 22) Button("Custom Link") { viewModel.selectInstallMethod(.remoteManual) } } default: EmptyView() } } .buttonStyle(.link) if case .error(let message) = viewModel.state { Spacer() errorView(message: message, multiline: false) } Spacer() if viewModel.showNextButton { nextButton } } .virtualBuddyBottomBarStyle() } @ViewBuilder private func errorView(message: String, multiline: Bool) -> some View { Text(message) .font(.subheadline) .foregroundStyle(.red) .textSelection(.enabled) .multilineTextAlignment(.center) .lineLimit(multiline ? nil : 1) .minimumScaleFactor(0.8) .help(message) } @ViewBuilder private var nextButton: some View { Button(viewModel.buttonTitle, action: { if viewModel.step == .done { library.loadMachines() closeWindow() } else { viewModel.next() } }) .keyboardShortcut(.defaultAction) .disabled(viewModel.disableNextButton) } @ViewBuilder private var guestSystemTypeSelection: some View { GuestTypePicker(selection: $viewModel.data.systemType) } @ViewBuilder private var restoreImageURLInput: some View { RestoreImageURLInputView().environmentObject(viewModel) } @ViewBuilder private var restoreImageSelection: some View { RestoreImageSelectionStep() .environmentObject(viewModel) } @ViewBuilder private var configureVM: some View { if let machine = viewModel.machine { InstallConfigurationStepView(vm: machine, resolvedRestoreImage: viewModel.data.resolvedRestoreImage) { configuredModel in viewModel.machine = configuredModel try? viewModel.machine?.saveMetadata() viewModel.next() } } else { preparingStatus } } @ViewBuilder private var preparingStatus: some View { Text("Preparing…") .foregroundStyle(.tertiary) .frame(maxWidth: .infinity, maxHeight: .infinity) } @ViewBuilder private var renameVM: some View { VirtualMachineNameInputView(name: $viewModel.data.name) } } extension VMInstallationStep { var subtitle: String { switch self { case .systemType: "Choose Operating System" case .restoreImageInput: "Select Custom Restore Image" case .restoreImageSelection: "Choose Version" case .name: "Name Your Virtual Machine" case .configuration: "Configure Your Virtual Machine" case .download: "Downloading" case .install: "Installing" case .done: "Finished" } } } extension View { @ViewBuilder func virtualBuddyBottomBarStyle() -> some View { frame(maxWidth: .infinity) .controlSize(.large) .padding() .background(Material.bar) .overlay(alignment: .top) { Divider() } } } #if DEBUG extension VMInstallationWizard { @ViewBuilder static func preview(step: VMInstallationStep) -> some View { VMInstallationWizard(library: .preview, initialStep: step) .frame(width: 900) } @ViewBuilder static var preview: some View { preview(step: .restoreImageSelection) } } #Preview { VMInstallationWizard.preview(step: .install) } #endif // DEBUG ================================================ FILE: VirtualUI/Source/Library/Components/FirstLaunchExperienceView.swift ================================================ import SwiftUI import BuddyKit struct FirstLaunchExperienceView: View { var action: () -> () @State private var buttonRevealed = false var body: some View { ZStack { AnimationView() .ignoresSafeArea() .frame(maxWidth: .infinity, maxHeight: .infinity) VStack { Spacer() if buttonRevealed { Button { action() } label: { Text("Create Virtual Machine") .font(.system(.title2, design: .rounded, weight: .medium)) } .keyboardShortcut(.defaultAction) .buttonStyle(FirstLaunchButtonStyle()) .controlSize(.large) .transition(.scale(scale: 1.5).combined(with: .opacity)) } Spacer() } } .task { withAnimation(.snappy.delay(7.5)) { buttonRevealed = true } } } private struct AnimationView: NSViewRepresentable { typealias NSViewType = FirstLaunchExperienceNSView func makeNSView(context: Context) -> FirstLaunchExperienceNSView { FirstLaunchExperienceNSView(frame: .zero) } func updateNSView(_ nsView: FirstLaunchExperienceNSView, context: Context) { } final class FirstLaunchExperienceNSView: NSView { override init(frame frameRect: NSRect) { super.init(frame: frameRect) setup() } required init?(coder: NSCoder) { fatalError() } private lazy var assetLayer = CALayer.load(assetNamed: "FirstLaunchExperience", bundle: .virtualUI) ?? CALayer() #if DEBUG private var debugSkipAnimation: Bool { false } #endif private func setup() { platformLayer.addSublayer(assetLayer) assetLayer.isGeometryFlipped = false assetLayer.beginTime = CACurrentMediaTime() assetLayer.speed = 1 #if DEBUG if debugSkipAnimation { assetLayer.timeOffset = 10 } #endif } override var isFlipped: Bool { true } override func layout() { super.layout() CATransaction.begin() CATransaction.setDisableActions(true) defer { CATransaction.commit() } assetLayer.frame = bounds assetLayer.sublayer(named: "background")?.frame = bounds assetLayer.sublayer(path: "background.fullBleed")?.frame = bounds assetLayer.sublayer(path: "background.brighten")?.frame = bounds assetLayer.sublayer(named: "backdrop")?.frame = bounds assetLayer.sublayer(named: "iconTransform")?.position = CGPoint(x: bounds.midX, y: bounds.midY) assetLayer.sublayer(named: "spotlightBleed")?.frame = bounds assetLayer.sublayer(named: "scanlines")?.frame.size.width = bounds.width if let backdropMask = assetLayer.sublayer(named: "backdrop")?.mask { backdropMask.bounds.size = bounds.size backdropMask.position = CGPoint(x: bounds.midX, y: bounds.maxY) } } } } } private struct FirstLaunchButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundStyle(.secondary) .padding(.horizontal, 22) .padding(.vertical, 14) .background(Material.regular, in: shape) .overlay { ZStack { Color.accentColor .blendMode(.color) .opacity(configuration.isPressed ? 1 : 0.7) shape .inset(by: 10) .fill(Color.accentColor) .blur(radius: 10) .blendMode(.plusLighter) .opacity(configuration.isPressed ? 0.1 : 0) LinearGradient(colors: [.accentColor.opacity(0.9), .accentColor.opacity(0.4)], startPoint: .top, endPoint: .bottom) .mask { shape.strokeBorder(Color.white, lineWidth: 1) } .blendMode(.plusLighter) .opacity(0.3) } } .clipShape(shape) .shadow(color: .black.opacity(0.2), radius: 6, x: 0, y: 0) .scaleEffect(configuration.isPressed ? 0.98 : 1) .animation(configuration.isPressed ? .linear(duration: 0) : .snappy, value: configuration.isPressed) } var shape: some InsettableShape { Capsule(style: .continuous) } } #if DEBUG #Preview { FirstLaunchExperienceView() { } .frame(width: 900, height: 600) } #endif ================================================ FILE: VirtualUI/Source/Library/Components/LibraryItemView.swift ================================================ // // LibraryItemView.swift // VirtualUI // // Created by Guilherme Rambo on 21/07/22. // import SwiftUI import VirtualCore public extension EnvironmentValues { /// This is injected by reading from ``VBSettings``. /// When `true`, virtual machine thumbnails in the library show the actual desktop picture thumbnail instead of the blurred version. @Entry var virtualBuddyShowDesktopPictureThumbnails = false } /// This button style achieves a couple of things: /// - Gives its label a `vbLibraryButtonPressed` environment value that can be used to react to button presses /// - Fixes an annoying behavior common to all standard SwiftUI button styles where pressing the space bar /// with one of its subviews in focus would trigger the button's action instead of entering a space in a text field, for example struct VBLibraryItemButtonStyle: PrimitiveButtonStyle { @State private var isPressed = false /// The rectangle of the button's contents in the local coordinate space. @State private var rect: CGRect = .zero func makeBody(configuration: Configuration) -> some View { configuration.label .environment(\.vbLibraryButtonPressed, isPressed) .overlay { GeometryReader { proxy in Color.clear .preference(key: VBLibraryButtonSizePreferenceKey.self, value: proxy.size) } } .onPreferenceChange(VBLibraryButtonSizePreferenceKey.self) { rect = CGRect(origin: .zero, size: $0) } .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local).onChanged { value in /// Replicate standard button behavior where dragging outside the button cancels the click. isPressed = rect.contains(value.location) }.onEnded { _ in /// If the button is not currently pressed, then don't perform the action. guard isPressed else { return } configuration.trigger() isPressed = false }) } } struct VBLibraryButtonSizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } } extension PrimitiveButtonStyle where Self == VBLibraryItemButtonStyle { static var vbLibraryItem: VBLibraryItemButtonStyle { VBLibraryItemButtonStyle() } } private struct VBLibraryItemButtonPressedEnvironmentKey: EnvironmentKey { static var defaultValue = false } private extension EnvironmentValues { var vbLibraryButtonPressed: VBLibraryItemButtonPressedEnvironmentKey.Value { get { self[VBLibraryItemButtonPressedEnvironmentKey.self] } set { self[VBLibraryItemButtonPressedEnvironmentKey.self] = newValue } } } @MainActor struct LibraryItemView: View { @EnvironmentObject var library: VMLibraryController var vm: VBVirtualMachine @State var name: String @Environment(\.vbLibraryButtonPressed) private var isPressed var nameFieldFocus = BoolSubject() private var isVMBooted: Bool { library.bootedMachineIdentifiers.contains(vm.id) } var body: some View { VStack(spacing: 12) { ArtworkView(virtualMachine: vm) EphemeralTextField($name, alignment: .leading, setFocus: nameFieldFocus) { name in Text(name) } editableContent: { name in TextField("VM Name", text: name) .onSubmit { rename(name.wrappedValue) } } validate: { name in do { try library.validateNewName(name, for: vm) return true } catch { return false } } .font(.system(size: 16, weight: .medium, design: .rounded)) .disabled(isVMBooted) } .padding([.leading, .trailing, .top], 8) .padding(.bottom, 12) .background(Material.regular, in: backgroundShape) .highlightBorder(backgroundShape, color: .accentColor, opacity: 0.2) .clipShape(backgroundShape) .shadow(color: Color.black.opacity(0.14), radius: 12) .shadow(color: Color.black.opacity(0.56), radius: 1) .scaleEffect(isPressed ? 0.98 : 1) .contextMenu { contextMenuItems } .task(id: vm.name) { self.name = vm.name } .animation(isPressed ? .linear(duration: 0) : .snappy, value: isPressed) } private func rename(_ newName: String) { guard newName != vm.name else { return } do { try library.rename(vm, to: name) } catch { NSAlert(error: error).runModal() } } private var backgroundShape: some InsettableShape { RoundedRectangle(cornerRadius: 12, style: .continuous) } @ViewBuilder private var contextMenuItems: some View { Button { NSWorkspace.shared.selectFile(vm.bundleURL.path, inFileViewerRootedAtPath: vm.bundleURL.deletingLastPathComponent().path) } label: { Label("Show in Finder", systemImage: "folder") } Divider() Button { duplicate() } label: { Text("Duplicate") } Button { nameFieldFocus.send(true) } label: { Text("Rename") } .disabled(isVMBooted) #if DEBUG Button { NSWorkspace.shared.open(vm.metadataDirectoryURL) } label: { Text("Open Data Folder…") } #endif Divider() Button { library.performMoveToTrash(for: vm) } label: { Text("Move to Trash") } .disabled(isVMBooted) } private func duplicate() { Task { do { try library.duplicate(vm) } catch { NSAlert(error: error).runModal() } } } private struct ArtworkView: View { var virtualMachine: VBVirtualMachine @Environment(\.virtualBuddyShowDesktopPictureThumbnails) private var showDesktopPicture var body: some View { VMArtworkView(virtualMachine: virtualMachine, alwaysUseBlurHash: !showDesktopPicture) .id(virtualMachine.blurHashBackgroundContent) .aspectRatio(contentMode: .fill) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .aspectRatio(16/9, contentMode: .fit) .highlightBorder(shape) .clipShape(shape) } private var shape: some InsettableShape { RoundedRectangle(cornerRadius: 8, style: .continuous) } } } struct HighlightBorderModifier: ViewModifier { var shape: Shape var color: Color var opacity: Double func body(content: Content) -> some View { ZStack { content ZStack { LinearGradient(colors: [color.opacity(1), color.opacity(0.4)], startPoint: .top, endPoint: .bottom) shape .inset(by: 1) .blendMode(.destinationOut) } .compositingGroup() .clipShape(shape) .blendMode(.plusLighter) .opacity(opacity) } } } extension View { func highlightBorder(_ shape: Shape, color: Color = .white, opacity: Double = 0.14) -> some View { modifier(HighlightBorderModifier(shape: shape, color: color, opacity: opacity)) } } extension VMLibraryController { func performMoveToTrash(for vm: VBVirtualMachine) { Task { do { try await moveToTrash(vm) } catch { NSAlert(error: error).runModal() } } } } #if DEBUG #Preview { LibraryView() .environmentObject(VMLibraryController.preview) .environmentObject(VirtualMachineSessionUIManager.shared) } #endif ================================================ FILE: VirtualUI/Source/Library/LibraryView.swift ================================================ // // LibraryView.swift // VirtualBuddy // // Created by Guilherme Rambo on 10/04/22. // import SwiftUI import VirtualCore public extension String { static let vb_libraryWindowID = "library" } private extension UserDefaults { /// Not using `AppStorage` because this can't update the view. var hasSeenFirstLaunchExperience: Bool { get { bool(forKey: #function) } set { set(newValue, forKey: #function) synchronize() } } } public struct LibraryView: View { @ObservedObject private var settingsContainer = VBSettingsContainer.current @EnvironmentObject private var library: VMLibraryController @EnvironmentObject private var sessionManager: VirtualMachineSessionUIManager @Environment(\.openCocoaWindow) private var openCocoaWindow @Environment(\.openVirtualBuddySettings) private var openSettings /// Whether the first launch experience has been presented in the current app session. /// The defaults flag itself is not read again, but this is set whenever a non-empty library state is shown. @State private var canShowFirstLaunchExperience = true private var shouldShowFirstLaunchExperienceOnEmptyLibrary: Bool { guard #available(macOS 15.0, *) else { return false } return canShowFirstLaunchExperience && (!hasSeenFirstLaunchExperience || UserDefaults.standard.bool(forKey: "VBForceFirstLaunchExperience")) } /// Set on view initialization only so that when the defaults flag is updated, it doesn't cause the first launch experience to disappear. private let hasSeenFirstLaunchExperience: Bool public init() { hasSeenFirstLaunchExperience = UserDefaults.standard.hasSeenFirstLaunchExperience } public var body: some View { libraryContents .frame(minWidth: 900, maxWidth: .infinity, minHeight: 600, maxHeight: .infinity) .toolbar(content: { toolbarContents }) .task { #if DEBUG if UserDefaults.standard.bool(forKey: "VBOpenSettings") { openSettings() } #endif } } @ToolbarContentBuilder private var toolbarContents: some ToolbarContent { ToolbarItemGroup(placement: .primaryAction) { Button { openCocoaWindow { VMInstallationWizard(library: library) } } label: { Image(systemName: "plus") } .help("New virtual machine") } } private var gridSpacing: CGFloat { 16 } private var gridItemMinSize: CGFloat { 240 } private var gridColumns: [GridItem] { [.init(.adaptive(minimum: gridItemMinSize), spacing: gridSpacing)] } @ViewBuilder private var libraryContents: some View { ZStack { BlurHashFullBleedBackground(content: .blurHash(.virtualBuddyBackground)) .ignoresSafeArea() .frame(maxWidth: .infinity, maxHeight: .infinity) .fullBleedBackgroundDimmed(![.loaded, .loading].contains(library.state.id)) Group { switch library.state { case .loaded(let machines): grid(machines) .task { canShowFirstLaunchExperience = false } case .empty: if shouldShowFirstLaunchExperienceOnEmptyLibrary { FirstLaunchExperienceView { sessionManager.launchInstallWizard(library: library) } .task { UserDefaults.standard.hasSeenFirstLaunchExperience = true } } else { libraryEmptyMessage } case .loading: EmptyView() case .volumeNotMounted: libraryVolumeNotMountedMessage case .directoryMissing: libraryDirectoryMissingMessage } } .transition(.scale(scale: 1.5).combined(with: .opacity)) } .animation(.snappy, value: library.state.id) } @ViewBuilder private func grid(_ machines: [VBVirtualMachine]) -> some View { ScrollView(.vertical) { LazyVGrid(columns: gridColumns, spacing: gridSpacing) { ForEach(machines) { vm in Button { sessionManager.launch(vm, library: library, options: nil) } label: { LibraryItemView(vm: vm, name: vm.name) } .buttonStyle(.vbLibraryItem) .environmentObject(library) .transition(.scale(scale: 0.3).combined(with: .opacity)) } } .padding() .padding(.top) } .environment(\.virtualBuddyShowDesktopPictureThumbnails, settingsContainer.settings.showDesktopPictureThumbnails) } @ViewBuilder private var libraryVolumeNotMountedMessage: some View { BackportedContentUnavailableView( "Library Not Mounted", systemImage: "externaldrive.badge.questionmark", description: Text(""" The volume containing your VirtualBuddy library is not currently mounted. Once mounted, your virtual machines will appear here. """) ) { Button("Try Again") { library.loadMachines() } .keyboardShortcut(.defaultAction) Button("Open Settings") { openSettings() } } } @ViewBuilder private var libraryDirectoryMissingMessage: some View { BackportedContentUnavailableView( "Library Missing", systemImage: "questionmark.folder", description: Text(""" VirtualBuddy is unable to locate your library directory. Review your settings to ensure the directory is set correctly. If it exists, there may be a permission problem. If you’ve deleted the library directory, you can start a new empty library. """) ) { Button("Open Settings") { openSettings() } .keyboardShortcut(.defaultAction) Button("Create Empty Library") { library.loadMachines(createLibrary: true) } } } @ViewBuilder private var libraryEmptyMessage: some View { BackportedContentUnavailableView( "No Virtual Machines", systemImage: "square.grid.2x2", description: Text(""" You haven’t created any virtual machines yet. You can create a new one, or select a different library directory in settings. """) ) { Button("Create Virtual Machine") { sessionManager.launchInstallWizard(library: library) } .keyboardShortcut(.defaultAction) Button("Open Settings") { openSettings() } } } } fileprivate extension URL { var collapsedHomePath: String { path.replacingOccurrences(of: NSHomeDirectory(), with: "~") } } #if DEBUG #Preview { LibraryView() .environmentObject(VMLibraryController.preview) .environmentObject(VirtualMachineSessionUIManager.shared) } #endif ================================================ FILE: VirtualUI/Source/Session/Components/ContinuousProgressIndicator.swift ================================================ import SwiftUI struct ContinuousProgressIndicator: View { var duration: TimeInterval @ViewBuilder var content: (Double) -> Content @State private var progress = Double(0) var body: some View { ZStack { content(progress) } .task { @MainActor in withAnimation(.easeIn(duration: duration)) { progress = 1 } } } } ================================================ FILE: VirtualUI/Source/Session/Components/MaskProgressView.swift ================================================ import SwiftUI struct MaskProgressView: View { var progress: Double var background: Background var foreground: Foreground @ViewBuilder var mask: (Double) -> Mask var body: some View { ZStack { let content = mask(progress) content .foregroundStyle(background) content .foregroundStyle(foreground) .mask { GeometryReader { proxy in Rectangle().fill(.white) .frame(width: proxy.size.width * progress, alignment: .leading) } } } } } ================================================ FILE: VirtualUI/Source/Session/Components/NumberDisplayMode.swift ================================================ import SwiftUI enum NumberDisplayMode: Int, CaseIterable { case hex case decimal var title: String { switch self { case .hex: return "Hex" case .decimal: return "Decimal" } } } extension EnvironmentValues { @Entry var numberDisplayMode: NumberDisplayMode = .decimal } protocol FormattableNumber: CVarArg, FixedWidthInteger { func formatted(mode: NumberDisplayMode) -> String } extension FormattableNumber where Self: SignedInteger { func formatted(mode: NumberDisplayMode) -> String { switch mode { case .hex: return String(format: "0x%02llX", Int64(self)) case .decimal: return String(format: "%lld", Int64(self)) } } } extension FormattableNumber where Self: UnsignedInteger { func formatted(mode: NumberDisplayMode) -> String { switch mode { case .hex: return String(format: "0x%02llX", UInt64(self)) case .decimal: return String(format: "%llu", UInt64(self)) } } } extension Int64: FormattableNumber { } extension UInt64: FormattableNumber { } extension Int32: FormattableNumber { } extension UInt32: FormattableNumber { } extension Int16: FormattableNumber { } extension UInt16: FormattableNumber { } extension Int8: FormattableNumber { } extension UInt8: FormattableNumber { } ================================================ FILE: VirtualUI/Source/Session/Components/SavedStatePicker.swift ================================================ import SwiftUI import VirtualCore struct SavedStatePicker: View { @EnvironmentObject private var controller: VMSavedStatesController @Binding var selectedStateURL: URL? var body: some View { Picker("State", selection: $selectedStateURL) { if controller.states.isEmpty { Text("No Saved States") .tag(Optional.none) } else { Text("Don’t Restore") .tag(Optional.none) Divider() } ForEach(controller.states) { state in Text(state.url.deletingPathExtension().lastPathComponent) .tag(Optional.some(state.url)) } } .disabled(controller.states.isEmpty) } } #if DEBUG private struct _Preview: View { @StateObject private var controller = VMSavedStatesController.preview @State private var selectedStateURL: URL? var body: some View { Form { SavedStatePicker(selectedStateURL: $selectedStateURL) .environmentObject(controller) } .formStyle(.grouped) } } #Preview("SavedStatePicker") { _Preview() } #endif ================================================ FILE: VirtualUI/Source/Session/Components/SwiftUIVMView.swift ================================================ // // SwiftUIVMView.swift // VirtualBuddy // // Created by Guilherme Rambo on 07/04/22. // import SwiftUI import Cocoa import Virtualization import VirtualCore struct SwiftUIVMView: NSViewControllerRepresentable { typealias NSViewControllerType = VMViewController @Binding var controllerState: VMController.State let captureSystemKeys: Bool var isDFUModeVM: Bool var vmECID: UInt64? @Binding var automaticallyReconfiguresDisplay: Bool func makeNSViewController(context: Context) -> VMViewController { let controller = VMViewController() controller.vmECID = vmECID controller.isDFUModeVM = isDFUModeVM controller.captureSystemKeys = captureSystemKeys controller.automaticallyReconfiguresDisplay = automaticallyReconfiguresDisplay return controller } func updateNSViewController(_ nsViewController: VMViewController, context: Context) { nsViewController.automaticallyReconfiguresDisplay = automaticallyReconfiguresDisplay nsViewController.vmECID = vmECID nsViewController.isDFUModeVM = isDFUModeVM if case .running(let vm) = controllerState { nsViewController.virtualMachine = vm } else { nsViewController.virtualMachine = nil } } } final class VMViewController: NSViewController { var isDFUModeVM: Bool = false { didSet { guard isDFUModeVM != oldValue, isViewLoaded else { return } handleDFUTransition(.init(wasInDFU: oldValue, isInDFU: isDFUModeVM)) } } var vmECID: UInt64? { didSet { guard vmECID != nil, vmECID != oldValue, isDFUModeVM, isViewLoaded else { return } /// Force update of DFU state to display the ECID. handleDFUTransition(.enter) } } var captureSystemKeys: Bool = false { didSet { guard captureSystemKeys != oldValue, isViewLoaded else { return } vmView.capturesSystemKeys = captureSystemKeys } } var automaticallyReconfiguresDisplay: Bool = true { didSet { guard #available(macOS 14.0, *) else { return } vmView.automaticallyReconfiguresDisplay = automaticallyReconfiguresDisplay } } var virtualMachine: VZVirtualMachine? { didSet { vmView.virtualMachine = virtualMachine } } private var canShowDFUView: Bool { #if DEBUG return ProcessInfo.isSwiftUIPreview || virtualMachine != nil #else return virtualMachine != nil #endif } private lazy var vmView: VZVirtualMachineView = { VZVirtualMachineView(frame: .zero) }() override func loadView() { view = NSView() view.wantsLayer = true view.layer?.backgroundColor = NSColor.black.cgColor vmView.capturesSystemKeys = captureSystemKeys if #available(macOS 14.0, *) { vmView.automaticallyReconfiguresDisplay = automaticallyReconfiguresDisplay } vmView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(vmView) NSLayoutConstraint.activate([ vmView.leadingAnchor.constraint(equalTo: view.leadingAnchor), vmView.trailingAnchor.constraint(equalTo: view.trailingAnchor), vmView.topAnchor.constraint(equalTo: view.topAnchor), vmView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } override func viewDidAppear() { super.viewDidAppear() guard let window = view.window else { return } window.makeFirstResponder(vmView) if isDFUModeVM { handleDFUTransition(.enter) } } enum DFUTransition: Hashable { case enter case exit case invalid init(wasInDFU: Bool, isInDFU: Bool) { if wasInDFU, !isInDFU { self = .exit } else if isInDFU, !wasInDFU { self = .enter } else { self = .invalid } } } private func handleDFUTransition(_ transition: DFUTransition) { switch transition { case .enter: showDFUView() case .exit: hideDFUView() case .invalid: break } } private var currentDFUView: NSView? private func showDFUView() { currentDFUView?.removeFromSuperview() guard canShowDFUView else { return } let dfuView = NSHostingView(rootView: DFUStatusView(ecid: vmECID)) dfuView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(dfuView) NSLayoutConstraint.activate([ dfuView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 16), dfuView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -16), dfuView.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: 16), dfuView.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -16), dfuView.centerXAnchor.constraint(equalTo: view.centerXAnchor), dfuView.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) } private func hideDFUView() { currentDFUView?.removeFromSuperview() } } struct DFUStatusView: View { var ecid: UInt64? @Environment(\.numberDisplayMode) private var numberDisplayMode var body: some View { VStack(spacing: 22) { VStack { Image(systemName: "cpu") .imageScale(.large) Text("DFU Mode Active") } .font(.system(.largeTitle, design: .rounded)) VStack(spacing: 8) { Text("This virtual machine is running in DFU mode.") .font(.system(.title2, design: .rounded, weight: .medium)) if let ecid { HStack(spacing: 0) { Text("ECID: ") .font(.headline) Text("\(ecid.formatted(mode: numberDisplayMode))") .textSelection(.enabled) .font(.headline.weight(.regular).monospaced()) } .foregroundStyle(.secondary) } } } } } #if DEBUG #Preview("VM View - DFU") { SwiftUIVMView( controllerState: .constant(.starting(nil)), captureSystemKeys: false, isDFUModeVM: true, vmECID: 7788022887768653863, automaticallyReconfiguresDisplay: .constant(false) ) } #endif ================================================ FILE: VirtualUI/Source/Session/Components/VMProgressOverlay.swift ================================================ import SwiftUI struct VMProgressOverlay: View { let message: String let duration: TimeInterval var body: some View { ContinuousProgressIndicator(duration: duration) { progress in MaskProgressView(progress: progress * 1.2, background: .tertiary, foreground: .primary) { _ in Text(message) .font(.system(.title, design: .rounded, weight: .semibold)) } .scaleEffect(0.85 + 0.15 * progress) } .id(message) .transition(.scale(scale: 0.2).combined(with: .opacity)) } } ================================================ FILE: VirtualUI/Source/Session/Components/VirtualMachineControls.swift ================================================ // // VirtualMachineControls.swift // VirtualUI // // Created by Guilherme Rambo on 24/10/23. // import SwiftUI import VirtualCore @MainActor protocol VirtualMachineStateController: ObservableObject { var state: VMState { get } func start() async throws func stop() async throws func pause() async throws func resume() async throws @available(macOS 14.0, *) func saveState(snapshotName: String) async throws var virtualMachineModel: VBVirtualMachine { get } } extension VMController: VirtualMachineStateController { } @available(macOS 14.0, *) struct VirtualMachineControls: View { @EnvironmentObject private var controller: Controller @State private var actionTask: Task? @State private var isPopoverPresented = false @State private var textFieldContent = "" var body: some View { Group { switch controller.state { case .idle, .paused, .stopped, .savingState, .restoringState, .stateSaveCompleted: Button { runToolbarAction { if controller.state.canResume { try await controller.resume() } else { try await controller.start() } } } label: { Image(systemName: "play") } .disabled(controller.state.isSavingState || controller.state.isRestoringState) case .starting, .running: if #available(macOS 14.0, *), controller.virtualMachineModel.supportsStateRestoration { Button { /** Ability to save new states has been temporarily disabled in version 2 due to issues with its implementation. This prevents users from creating bad state saves before the correct implementation is shipped. */ guard UserDefaults.standard.bool(forKey: "VBForceEnableSaveStateFeature") else { NSAlert(error: "Sorry, this feature has been temporarily disabled. It will be back in a future update.").runModal() return } runToolbarAction { textFieldContent = "Save-" + DateFormatter.savedStateFileName.string(from: .now) isPopoverPresented = true } } label: { Image(systemName: "tray.and.arrow.down") } .help("Save current state") .popover(isPresented: $isPopoverPresented) { VStack { Text("Save current state") .font(.headline) TextField("Name current state", text: $textFieldContent) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(.top, 15) .padding(.bottom, 15) HStack { Spacer() Button("Cancel") { isPopoverPresented = false } .padding(.trailing, 8) .keyboardShortcut(.cancelAction) Button("Save") { isPopoverPresented = false runToolbarAction { try await saveState() } } .keyboardShortcut(.defaultAction) } } .frame(width: 300) .padding() } Button { runToolbarAction { try await controller.pause() } } label: { Image(systemName: "pause") } .help("Pause") Button { runToolbarAction { try await controller.stop() } } label: { Image(systemName: "power") } .help("Shut down") } } } .symbolVariant(.fill) .disabled(actionTask != nil) } private func runToolbarAction(alertForErrors: Bool = false, action: @escaping () async throws -> Void) { actionTask = Task { defer { actionTask = nil } do { try await action() } catch { guard alertForErrors else { return } NSAlert(error: error).runModal() } } } private func saveState() async throws { do { try await controller.saveState(snapshotName: textFieldContent) } catch { guard !(error is CancellationError) else { return } throw error } } } private extension DateFormatter { static let savedStateFileName: DateFormatter = { let f = DateFormatter() f.calendar = .init(identifier: .gregorian) f.dateFormat = "yyyy-MM-dd_HH;mm;ss" return f }() } #if DEBUG private final class PreviewVirtualMachineStateController: VirtualMachineStateController { @MainActor @Published var state: VMState = .idle @Published var virtualMachineModel = VBVirtualMachine.preview @MainActor func start() async throws { state = .starting(nil) try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) state = .running(.preview) } @MainActor func stop() async throws { try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) state = .stopped(nil) } @MainActor func pause() async throws { state = .paused(.preview) } @MainActor func resume() async throws { state = .running(.preview) } @available(macOS 14.0, *) @MainActor func saveState(snapshotName: String) async throws { state = .paused(.preview) } } @available(macOS 14.0, *) private struct _VirtualMachineControlsPreview: View { var body: some View { Text("Preview") .frame(minWidth: 200, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity) .toolbar { ToolbarItemGroup(placement: .primaryAction) { VirtualMachineControls() .environmentObject(PreviewVirtualMachineStateController()) } } } } @available(macOS 14.0, *) #Preview { _VirtualMachineControlsPreview() } #endif ================================================ FILE: VirtualUI/Source/Session/Configuration/VMSessionConfigurationView.swift ================================================ // // VMSessionConfigurationView.swift // VirtualBuddy // // Created by Guilherme Rambo on 07/04/22. // import SwiftUI import VirtualCore struct VMSessionConfigurationView: View { @EnvironmentObject var controller: VMController @State private var isShowingVMSettings = false private var vm: VBVirtualMachine { controller.virtualMachineModel } private var resolvedRestoreImage: ResolvedRestoreImage? { let catalog = SoftwareCatalog.current(for: vm.configuration.systemType) if let remoteURL = vm.metadata.remoteInstallImageURL, let resolved = catalog.resolvedRestoreImage(matching: remoteURL, guestType: vm.configuration.systemType) { return resolved } if let localURL = vm.metadata.installImageURL, let resolved = catalog.resolvedRestoreImage(matching: localURL, guestType: vm.configuration.systemType) { return resolved } return nil } var body: some View { SelfSizingGroupedForm(minHeight: 100) { if showSavedStatePicker { SavedStatePicker(selectedStateURL: $controller.options.stateRestorationPackageURL) .environmentObject(controller.savedStatesController) } if showInstallDeviceOption { Toggle("Boot on install drive", isOn: $controller.options.bootOnInstallDevice) } if showRecoveryModeOption { Toggle("Boot in recovery mode", isOn: $controller.options.bootInRecoveryMode) .disabled(controller.options.bootInDFUMode) } if showDFUOption { Toggle("Boot in DFU mode", isOn: $controller.options.bootInDFUMode) .disabled(controller.options.bootInRecoveryMode) } Toggle("Capture system keyboard shortcuts", isOn: $controller.virtualMachineModel.configuration.captureSystemKeys) Button("Virtual Machine Settings…") { isShowingVMSettings.toggle() } .frame(maxWidth: .infinity, alignment: .trailing) } .sheet(isPresented: $isShowingVMSettings) { VMConfigurationSheet( configuration: $controller.virtualMachineModel.configuration ) .environmentObject(VMConfigurationViewModel(vm, resolvedRestoreImage: resolvedRestoreImage)) } } private var showInstallDeviceOption: Bool { vm.configuration.systemType == .linux && vm.metadata.installImageURL != nil } private var showRecoveryModeOption: Bool { vm.configuration.systemType == .mac } private var showDFUOption: Bool { VBMacConfiguration.appBuildAllowsDFUMode && vm.configuration.systemType == .mac } private var showSavedStatePicker: Bool { vm.configuration.systemType.supportsStateRestoration } } #if DEBUG #Preview { VirtualMachineSessionViewPreview() } #endif ================================================ FILE: VirtualUI/Source/Session/VirtualMachineSessionUI.swift ================================================ import SwiftUI import VirtualCore import Combine import AVFoundation public final class VirtualMachineSessionUI: ObservableObject { public enum WindowSize: Int { case pointAccurate case pixelAccurate case fitScreen } @Published public var lockProportions = false let setWindowAspectRatio = PassthroughSubject() let resizeWindow = PassthroughSubject() let makeWindowKey = PassthroughSubject() public let controller: VMController public let virtualMachine: VBVirtualMachine private lazy var cancellables = Set() @MainActor public convenience init(with virtualMachine: VBVirtualMachine, library: VMLibraryController, options: VMSessionOptions?) { self.init(controller: VMController(with: virtualMachine, library: library, options: options)) } @MainActor public init(controller: VMController) { self.controller = controller self.virtualMachine = controller.virtualMachineModel $lockProportions.dropFirst().removeDuplicates().sink { [weak self] newValue in guard let self = self else { return } guard let display = self.virtualMachine.configuration.hardware.displayDevices.first else { assertionFailure("VM doesn't have a display") return } let ratio = newValue ? CGSize(width: display.width, height: display.height) : nil self.setWindowAspectRatio.send(ratio) } .store(in: &cancellables) } @MainActor public func update(with newOptions: VMSessionOptions?) { guard let newOptions else { return } if newOptions != controller.options { /// If we're trying to launch a virtual machine with custom options and those don't match the current options, /// we must ensure that the VM is not currently running/paused, otherwise changing the options won't have any effect. /// If the VM is currently not in a state where options can be changed, then the user will be prompted to shut it down before doing so. guard controller.canStart else { let alert = NSAlert() alert.messageText = "Virtual Machine Already in Use" alert.informativeText = "\"\(virtualMachine.name)\" is already in use. Please shut down the virtual machine before starting it with the new options." alert.addButton(withTitle: "OK") alert.runModal() return } } /// The VM is currently in a state where it's safe to change the options, just update the options for the controller. controller.options = newOptions } @MainActor public func bringToFront() { makeWindowKey.send() } deinit { VBMemoryLeakDebugAssertions.vb_objectIsBeingReleased(self) } } public struct VirtualMachineWindowCommands: View { @EnvironmentObject private var manager: VirtualMachineSessionUIManager @State private var focusedSessionReference: WeakReference? private var focusedSession: VirtualMachineSessionUI? { focusedSessionReference?.object } @AppStorage("vm.window.proportions.locked") private var lockProportions = false public init() { } public var body: some View { Group { Toggle("Lock Proportions", isOn: $lockProportions) Button("Point Accurate") { focusedSession?.resizeWindow.send(.pointAccurate) } .keyboardShortcut("1", modifiers: .command) Button("Pixel Accurate") { focusedSession?.resizeWindow.send(.pixelAccurate) } .keyboardShortcut("2", modifiers: .command) Button("Fit Screen") { focusedSession?.resizeWindow.send(.fitScreen) } .keyboardShortcut("3", modifiers: .command) } .disabled(focusedSession == nil) .onReceive(manager.focusedSessionChanged) { ref in focusedSessionReference = ref ref?.object?.lockProportions = lockProportions } .onChange(of: lockProportions) { [lockProportions] newValue in guard newValue != lockProportions else { return } focusedSession?.lockProportions = newValue } Divider() } } ================================================ FILE: VirtualUI/Source/Session/VirtualMachineSessionUIManager.swift ================================================ import SwiftUI import VirtualCore import Combine import OSLog /// Controls active virtual machine sessions, managing the lifecycle of session windows, controllers, and opening VMs from files or URLs. @MainActor public final class VirtualMachineSessionUIManager: ObservableObject { private let logger = Logger(for: VirtualMachineSessionUIManager.self) public let focusedSessionChanged = PassthroughSubject?, Never>() public static let shared = VirtualMachineSessionUIManager() private let openWindow = OpenCocoaWindowAction.default private var sessions = [VBVirtualMachine.ID: VirtualMachineSessionUI]() private init() { } private func createSession(for vm: VBVirtualMachine, library: VMLibraryController, options: VMSessionOptions?) -> VirtualMachineSessionUI { let ui = VirtualMachineSessionUI(with: vm, library: library, options: options) sessions[vm.id] = ui return ui } public func session(for vm: VBVirtualMachine) -> VirtualMachineSessionUI? { sessions[vm.id] } public func launch(_ vm: VBVirtualMachine, library: VMLibraryController, options: VMSessionOptions?) { guard !vm.needsInstall else { recoverInstallation(for: vm, library: library) return } if let existingSession = session(for: vm) { existingSession.update(with: options) existingSession.bringToFront() } else { launchNewSession(for: vm, library: library, options: options) } } private func launchNewSession(for vm: VBVirtualMachine, library: VMLibraryController, options: VMSessionOptions?) { let vmID = vm.id let session = createSession(for: vm, library: library, options: options) openWindow(id: vmID) { VirtualMachineSessionView() .environmentObject(session) .environmentObject(session.controller) .environmentObject(library) .environmentObject(self) } onClose: { [weak self] in guard let self else { return } guard let session = sessions[vmID] else { return } VBMemoryLeakDebugAssertions.vb_objectShouldBeReleasedSoon(session) VBMemoryLeakDebugAssertions.vb_objectShouldBeReleasedSoon(session.controller) sessions[vmID] = nil } } public func recoverInstallation(for vm: VBVirtualMachine, library: VMLibraryController) { let alert = NSAlert() alert.messageText = "Finish Installation" alert.informativeText = "In order to start this virtual machine, its operating system needs to be installed. Would you like to install it now?" alert.addButton(withTitle: "Install") let deleteButton = alert.addButton(withTitle: "Delete") deleteButton.hasDestructiveAction = true alert.addButton(withTitle: "Cancel") let choice = alert.runModal() switch choice { case .alertFirstButtonReturn: launchInstallWizard(restoringAt: vm.bundleURL, library: library) case .alertSecondButtonReturn: library.performMoveToTrash(for: vm) default: break } } public func launchInstallWizard(restoringAt restoreURL: URL? = nil, library: VMLibraryController) { openWindow(animationBehavior: .documentWindow) { VMInstallationWizard(library: library, restoringAt: restoreURL) .environmentObject(library) } } public func launchImportVirtualMachinePanel(library: VMLibraryController) { guard let url = NSOpenPanel.run(accepting: VMImporterRegistry.default.supportedFileTypes, directoryURL: nil, defaultDirectoryKey: "importVirtualMachine", prompt: "Import") else { return } open(fileURL: url, library: library) } private func importVirtualMachine(from path: FilePath, using importer: any VMImporter, library: VMLibraryController) async { do { guard await confirmImport(using: importer) else { throw CancellationError() } logger.debug("Import authorized from \(importer.appName) - \(path)") var model = try await importer.importVirtualMachine(from: path, into: library) model.metadata.importedFromAppName = importer.appName try model.saveMetadata() library.reload() open(fileURL: model.bundleURL, library: library) } catch is CancellationError { logger.notice("Import cancelled") } catch { NSApp.presentError(error) } } private var importTask: Task? private func beginImportVirtualMachine(from path: FilePath, using importer: any VMImporter, library: VMLibraryController) { importTask = Task { await importVirtualMachine(from: path, using: importer, library: library) } } func confirmImport(using importer: any VMImporter) async -> Bool { #if DEBUG guard !Self.testImportSkipConfirmation else { return true } #endif return await NSAlert .runConfirmationAlert( title: "Import From \(importer.appName)?", message: "VirtualBuddy will attempt to import your virtual machine from \(importer.appName). All data from this virtual machine in \(importer.appName) will be copied into your VirtualBuddy library. Would you like to proceed?", continueButtonTitle: "Import", cancelButtonTitle: "Cancel", continueButtonIsDefault: true ) } } // MARK: - File Opening @MainActor public extension VirtualMachineSessionUIManager { func open(fileURL: URL, library: VMLibraryController) { do { let values = try fileURL.resourceValues(forKeys: [.contentTypeKey]) switch values.contentType { case .none: return case .virtualBuddyVM: handleOpenVirtualMachineFile(fileURL, library: library, options: nil) case .virtualBuddySavedState: handleOpenSavedStateFile(fileURL, library: library) default: let path = FilePath(fileURL) if let importer = VMImporterRegistry.default.importer(for: path) { beginImportVirtualMachine(from: path, using: importer, library: library) } else { throw Failure("Unsupported file type \(values.contentType?.identifier ?? "?")") } } } catch { logger.error("Error loading virtual machine from file at \(fileURL.path, privacy: .public): \(error, privacy: .public)") } } } @MainActor private extension VirtualMachineSessionUIManager { func handleOpenVirtualMachineFile(_ url: URL, library: VMLibraryController, options: VMSessionOptions?) { if let loadedVM = library.virtualMachines.first(where: { $0.bundleURL.path == url.path }) { launch(loadedVM, library: library, options: options) } else { do { let vm = try VBVirtualMachine(bundleURL: url) launch(vm, library: library, options: options) } catch { let alert = NSAlert(error: error) alert.runModal() } } } func handleOpenSavedStateFile(_ url: URL, library: VMLibraryController) { guard #available(macOS 14.0, *) else { let alert = NSAlert() alert.messageText = "State Restoration Not Supported" alert.informativeText = "Virtual machine state restoration requires macOS 14 or later." alert.addButton(withTitle: "OK") alert.runModal() return } do { let model = try library.virtualMachine(forSavedStatePackageURL: url) let options = VMSessionOptions(stateRestorationPackageURL: url) launch(model, library: library, options: options) } catch { NSAlert(error: error).runModal() } } } #if DEBUG // MARK: - Import Testing (Debug) @MainActor extension VirtualMachineSessionUIManager { static let testImportSkipConfirmation = UserDefaults.standard.bool(forKey: "VBTestImportSkipConfirmation") static let testImportVMPath: FilePath? = UserDefaults.standard.string(forKey: "VBTestImport").flatMap { FilePath($0) } public func testImportVMIfEnabled(library: VMLibraryController) { guard let path = Self.testImportVMPath else { return } open(fileURL: path.url, library: library) } } #endif ================================================ FILE: VirtualUI/Source/Session/VirtualMachineSessionView.swift ================================================ // // VirtualMachineSessionView.swift // VirtualBuddy // // Created by Guilherme Rambo on 07/04/22. // import SwiftUI import VirtualCore import Combine public struct VirtualMachineSessionView: View { @EnvironmentObject private var library: VMLibraryController @EnvironmentObject private var sessionManager: VirtualMachineSessionUIManager @EnvironmentObject private var controller: VMController @EnvironmentObject private var ui: VirtualMachineSessionUI @Environment(\.cocoaWindow) private var window /// ``VirtualMachineSessionView`` should only be initialized by ``VirtualMachineSessionUIManager``. internal init() { } private var vbWindow: VBRestorableWindow? { guard !ProcessInfo.isSwiftUIPreview else { return nil } guard let window = window as? VBRestorableWindow else { assertionFailure("VM window must be a VBRestorableWindow") return nil } return window } public var body: some View { ZStack { controllerStateView } .edgesIgnoringSafeArea(.all) .frame(minWidth: 400, maxWidth: .infinity, minHeight: 400, maxHeight: .infinity) .background(backgroundView) .environmentObject(controller) .windowTitle(controller.virtualMachineModel.name) .windowStyleMask([.titled, .miniaturizable, .closable, .resizable]) .confirmBeforeClosingWindow(callback: confirmBeforeClosing) .onWindowKeyChange { [weak sessionManager, weak ui] isKey in guard let sessionManager, let ui else { return } sessionManager.focusedSessionChanged.send(isKey ? .init(ui) : nil) } .onAppearOnce { guard vbWindow?.hasSavedFrame == false else { return } guard let display = controller.virtualMachineModel.configuration.hardware.displayDevices.first else { return } vbWindow?.resize(to: .fitScreen, for: display) } .onReceive(ui.resizeWindow) { size in guard let display = controller.virtualMachineModel.configuration.hardware.displayDevices.first else { assertionFailure("VM doesn't have a display") return } vbWindow?.resize(to: size, for: display) } .onReceive(ui.setWindowAspectRatio) { ratio in vbWindow?.applyAspectRatio(ratio) } .onReceive(ui.makeWindowKey) { window?.makeKeyAndOrderFront(nil) } .task { if controller.options.autoBoot { Task { try? await controller.start() } } } .toolbar { if #available(macOS 14.0, *) { VirtualMachineControls() .environmentObject(controller) } } } @ViewBuilder private var controllerStateView: some View { switch controller.state { case .idle: startableStateView(with: nil) case .stopped(let error): startableStateView(with: error) case .starting(let message): VStack(spacing: 12) { ProgressView() if let message { Text(message) .foregroundStyle(.secondary) .font(.subheadline) .multilineTextAlignment(.center) .frame(maxWidth: 400) } } case .running(let vm): vmView(with: vm) case .paused(let vm), .savingState(let vm), .restoringState(let vm, _), .stateSaveCompleted(let vm, _): pausedView(with: vm) } } @ViewBuilder private func vmView(with vm: VZVirtualMachine) -> some View { SwiftUIVMView( controllerState: .constant(.running(vm)), captureSystemKeys: controller.virtualMachineModel.configuration.captureSystemKeys, isDFUModeVM: controller.options.bootInDFUMode, vmECID: controller.virtualMachineModel.ECID, automaticallyReconfiguresDisplay: .constant(controller.virtualMachineModel.configuration.hardware.displayDevices.count > 0 ? controller.virtualMachineModel.configuration.hardware.displayDevices[0].automaticallyReconfiguresDisplay : false) ) } @ViewBuilder private func pausedView(with vm: VZVirtualMachine) -> some View { ZStack { vmView(with: vm) Rectangle() .foregroundStyle(Material.regular) ZStack { switch controller.state { case .paused: circularStartButton case .savingState, .stateSaveCompleted: VMProgressOverlay( message: controller.state.isStateSaveCompleted ? "State Saved!" : "Saving Virtual Machine State", duration: controller.state.isStateSaveCompleted ? 0 : 14 ) case .restoringState: VMProgressOverlay(message: "Restoring Virtual Machine State", duration: 14) default: EmptyView() } } .animation(.bouncy, value: controller.state) } } private func startableStateView(with error: Error?) -> some View { VStack(spacing: 28) { if let error = error { Text(startupErrorMessage(for: error)) .multilineTextAlignment(.center) .foregroundStyle(.secondary) .lineLimit(nil) .font(.caption) } circularStartButton VMSessionConfigurationView() .environment(\.backgroundMaterial, Material.thin) .environmentObject(controller) .frame(maxWidth: 400) } } private func startupErrorMessage(for error: Error) -> String { if error.isMaximumActiveVirtualMachinesError { return """ VirtualBuddy can't start this virtual machine because macOS has reached the system limit for active virtual machines. \ This is a system limitation. Shut down another virtual machine before starting this one. """ } return "The machine has stopped due to an error: \(error.localizedDescription)" } @ViewBuilder private var circularStartButton: some View { Button { if controller.canStart { Task { try? await controller.start() } } else if controller.canResume { Task { try? await controller.resume() } } } label: { Image(systemName: "play") } .buttonStyle(VMCircularButtonStyle()) } @ViewBuilder private var backgroundView: some View { VirtualMachineSessionBackgroundView( content: controller.virtualMachineModel.blurHashBackgroundContent, isRunning: controller.isRunning ) } private var confirmBeforeClosing: () async -> Bool { { [weak controller] in guard let controller else { return true } if controller.isIdle || controller.isStopped { return true } let confirmed = await NSAlert.runConfirmationAlert( title: "Stop Virtual Machine?", message: "If you close the window now, the virtual machine will be stopped.", continueButtonTitle: "Stop VM", cancelButtonTitle: "Cancel" ) guard confirmed else { return false } try? await controller.forceStop() return true } } } struct VirtualMachineSessionBackgroundView: View { var content: BlurHashFullBleedBackground.Content var isRunning: Bool var body: some View { ZStack { Color.black if !isRunning { switch content { case .blurHash(let token): BlurHashFullBleedBackground(blurHash: token) .fullBleedBackgroundBrightness(-0.2) case .customImage(let image): BlurHashFullBleedBackground(image: image) .fullBleedBackgroundBrightness(-0.1) .fullBleedBackgroundSaturation(0.8) } Color.black.opacity(0.3) } } } } struct VMCircularButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { mainContent(with: configuration) .contentShape(Circle()) } private func mainContent(with configuration: Configuration) -> some View { configuration.label .font(.system(size: 50, weight: .bold, design: .rounded)) .foregroundStyle(.primary) .padding(30) .background(Circle().fill(Material.thin)) .brightness(configuration.isPressed ? 0.3 : 0) .symbolVariant(.fill) } } extension VMController { var isIdle: Bool { return state == .idle } var isStarting: Bool { guard case .starting = state else { return false } return true } var isRunning: Bool { guard case .running = state else { return false } return true } var isStopped: Bool { guard case .stopped = state else { return false } return true } } extension VBVirtualMachine { var blurHashBackgroundContent: BlurHashFullBleedBackground.Content { if let thumbnail { .customImage(thumbnail) } else { .blurHash(metadata.backgroundHash) } } } private extension Error { var isMaximumActiveVirtualMachinesError: Bool { let nsError = self as NSError guard nsError.domain == "VZErrorDomain" else { return false } if nsError.code == 6 { return true } if let reason = nsError.localizedFailureReason, reason.localizedCaseInsensitiveContains("maximum supported number of active virtual machines") { return true } return nsError.localizedDescription.localizedCaseInsensitiveContains("maximum supported number of active virtual machines") } } #if DEBUG struct VirtualMachineSessionViewPreview: View { var body: some View { VirtualMachineSessionView() .frame(minWidth: 800, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity) .environmentObject(VMLibraryController.preview) .environmentObject(VMController.preview) .environmentObject(VirtualMachineSessionUI.preview) .environmentObject(VirtualMachineSessionUIManager.shared) } } #Preview { VirtualMachineSessionViewPreview() } #endif ================================================ FILE: VirtualUI/Source/Settings/AutomationSettingsView.swift ================================================ // // AutomationSettingsView.swift // VirtualBuddy // // Created by Guilherme Rambo on 20/06/25. // import SwiftUI import VirtualCore import BuddyKit import DeepLinkSecurity struct AutomationSettingsView: View { @EnvironmentObject private var sentinel: DeepLinkSentinel private var store: DeepLinkManagementStore { sentinel.managementStore } @State private var descriptors = [DeepLinkClientDescriptor]() var body: some View { Form { Section { if descriptors.isEmpty { Text("Apps that try to control VirtualBuddy will show up here.") .foregroundStyle(.tertiary) } else { ForEach(descriptors) { descriptor in Toggle(isOn: toggleBinding(for: descriptor)) { Label { Text(descriptor.displayName) } icon: { Image(nsImage: descriptor.icon.image) .resizable() .frame(width: 32, height: 32) .aspectRatio(contentMode: .fit) } } } } } header: { Text("Allow Apps to Control VirtualBuddy") } footer: { if !descriptors.isEmpty { SettingsFooter { Text(""" These apps have previously tried to open a deep link in VirtualBuddy. When an app tries to open a deep link in VirtualBuddy for the first time, you'll be asked to grant permission. \ Once you've allowed it, the app can open deep links without asking again. """) } } } } .task { for await descriptors in store.clientDescriptors() { self.descriptors = descriptors } } .navigationTitle(Text("Automation")) } private func toggleBinding(for descriptor: DeepLinkClientDescriptor) -> Binding { .init { descriptor.authorization == .authorized } set: { granted in Task { @MainActor in do { try await sentinel.setAuthorization(granted ? .authorized : .denied, for: descriptor) } catch { let alert = NSAlert(error: error) alert.runModal() } } } } } // MARK: - Preview #if DEBUG final class PreviewDeepLinkAuthUI: DeepLinkAuthUI { func presentDeepLinkAuth(for request: OpenDeepLinkRequest) async throws -> DeepLinkClientAuthorization { return .authorized } } extension DeepLinkSentinel { static let preview: DeepLinkSentinel = { let s = DeepLinkSentinel( authUI: PreviewDeepLinkAuthUI(), authStore: MemoryDeepLinkAuthStore(), managementStore: UserDefaultsDeepLinkManagementStore(namespace: "preview", inMemory: true) ) Task { do { guard let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: "/System/Applications"), includingPropertiesForKeys: [.contentTypeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants, .skipsPackageDescendants]) else { throw Failure("Can't enumerate /System/Applications") } while let url = enumerator.nextObject() as? URL { guard (try? url.resourceValues(forKeys: [.contentTypeKey]))?.contentType?.conforms(to: .application) == true else { continue } let auth: DeepLinkClientAuthorization = Int.random(in: 0...1024) % 2 == 0 ? .authorized : .denied guard let client = try? DeepLinkClient(url: url) else { continue } try? await s.authStore.setAuthorization(auth, for: client) let descriptor = DeepLinkClientDescriptor(client: client, authorization: auth) try? await s.managementStore.insert(descriptor) } } catch { print("Error populating preview sentinel: \(error)") } } return s }() } #Preview("Automation Settings") { SettingsScreen.preview(.automation) } #endif ================================================ FILE: VirtualUI/Source/Settings/Components/BackwardsCompatibility.swift ================================================ import SwiftUI extension View { @ViewBuilder func toolbarRemovingSidebarToggle() -> some View { if #available(macOS 14.0, *) { toolbar(removing: .sidebarToggle) } else { self } } @ViewBuilder func sidebarAdaptableTabViewStyle() -> some View { if #available(macOS 15.0, *) { tabViewStyle(.sidebarAdaptable) } else { self } } } ================================================ FILE: VirtualUI/Source/Settings/Components/FileSystemPathFormControl.swift ================================================ import SwiftUI import BuddyKit import VirtualCore import UniformTypeIdentifiers struct FileSystemPathFormControl: View { var url: URL var contentTypes: Set var defaultDirectoryKey: String var label: Text = Text("Location") var buttonLabel: Text = Text("Choose…") var setURL: (URL) -> () @State private var isDraggingURL = false var body: some View { LabeledContent { HStack(alignment: .center, spacing: 4) { Text(url.path) .lineLimit(1) .truncationMode(.middle) .help(url.path) Button { url.revealInFinder() } label: { Image(systemName: "arrow.right") } .buttonStyle(.link) .foregroundStyle(Color.accentColor) .font(.subheadline.weight(.medium)) } } label: { HStack { label Spacer() Button { showOpenPanel() } label: { buttonLabel } .controlSize(.small) } } .labeledContentStyle(.vertical) .dropDestination(for: URL.self) { items, _ in guard items.count == 1 else { return false } guard let type = FilePath(items[0]).contentType else { return false } guard contentTypes.contains(where: { type.conforms(to: $0) }) else { return false } setURL(items[0]) return true } isTargeted: { isDraggingURL = $0 } .background { if isDraggingURL { Color.accentColor .clipShape(RoundedRectangle(cornerRadius: 4)) .padding(-10) .opacity(0.4) } } } private func showOpenPanel() { guard let newURL = NSOpenPanel.run(accepting: [.folder], directoryURL: url, defaultDirectoryKey: "library") else { return } guard newURL != url else { return } setURL(newURL) } } ================================================ FILE: VirtualUI/Source/Settings/Components/OpenVirtualBuddySettingsAction.swift ================================================ // // OpenVirtualBuddySettingsAction.swift // VirtualBuddy // // Created by Guilherme Rambo on 20/06/25. // import SwiftUI import VirtualCore import DeepLinkSecurity import BuddyKit public struct OpenVirtualBuddySettingsAction { public var run: @MainActor () -> () public init(run: @escaping @MainActor () -> () = { preconditionFailure("Missing openVirtualBuddySettings in environment.") }) { self.run = run } @MainActor public func callAsFunction() { run() } } public extension EnvironmentValues { @Entry var openVirtualBuddySettings = OpenVirtualBuddySettingsAction() } ================================================ FILE: VirtualUI/Source/Settings/Components/SettingsFooter.swift ================================================ import SwiftUI import BuddyKit struct SettingsFooter: View { var summaryText: () -> Text var helpText: (() -> Text)? = nil @State private var helpExpanded = false var body: some View { VStack(alignment: .listRowSeparatorLeading, spacing: 8) { HStack(spacing: 0) { summaryText() Spacer() if helpText != nil { Group { if #available(macOS 14.0, *) { HelpLink { helpExpanded.toggle() } } else { Button { helpExpanded.toggle() } label: { Image(systemName: "questionmark.circle") } .controlSize(.small) .buttonStyle(.borderless) } } .controlSize(.small) } } if let helpText, helpExpanded { helpText() } } .settingsFooterStyle() } } extension View { @ViewBuilder func settingsFooterStyle() -> some View { let padding: Double = if #available(macOS 26, *) { 0 } else { 8 } frame(maxWidth: .infinity, alignment: .leading) .foregroundStyle(.secondary) .font(.footnote) .multilineTextAlignment(.leading) .padding(.leading, padding) .textSelection(.enabled) } } ================================================ FILE: VirtualUI/Source/Settings/Components/VerticalLabeledContentStyle.swift ================================================ import SwiftUI struct VerticalLabeledContentStyle: LabeledContentStyle { static let defaultSpacing: Double = 6 var spacing: Double? = nil func makeBody(configuration: Configuration) -> some View { VStack(alignment: .listRowSeparatorLeading, spacing: spacing ?? Self.defaultSpacing) { configuration.label configuration.content .foregroundStyle(.secondary) .textSelection(.enabled) } } } extension LabeledContentStyle where Self == VerticalLabeledContentStyle { static var vertical: VerticalLabeledContentStyle { VerticalLabeledContentStyle() } static func vertical(spacing: Double) -> VerticalLabeledContentStyle { VerticalLabeledContentStyle(spacing: spacing) } } ================================================ FILE: VirtualUI/Source/Settings/GeneralSettingsView.swift ================================================ // // GeneralSettingsView.swift // VirtualBuddy // // Created by Guilherme Rambo on 20/06/25. // import SwiftUI import VirtualCore import BuddyKit struct GeneralSettingsView: View { @Binding var settings: VBSettings @Binding var enableAutomaticUpdates: Bool @Binding var alert: AlertContent @State private var libraryPathText = "" private var libraryPath: String { settings.libraryURL.absoluteURL.path(percentEncoded: false) } var body: some View { Form { Section { FileSystemPathFormControl(url: settings.libraryURL, contentTypes: [.folder], defaultDirectoryKey: "library") { newURL in setLibraryPath(to: newURL.path) } } header: { Text("Storage") } footer: { Text("VirtualBuddy saves your virtual machines and downloaded installer images here.") .settingsFooterStyle() } Section { Toggle("Automatically check for updates", isOn: $enableAutomaticUpdates) betaSection } header: { Text("App Updates") } } .navigationTitle(Text("General")) .task(id: settings.libraryURL.path) { libraryPathText = settings.libraryURL.path } } private func setLibraryPath(to newValue: String) { let url = URL(fileURLWithPath: newValue) if let errorMessage = url.performWriteTest() { libraryPathText = settings.libraryURL.path alert = AlertContent(errorMessage) } else { settings.libraryURL = url libraryPathText = url.path alert = .init() } } @ViewBuilder private var betaSection: some View { LabeledContent("Beta Updates") { if settings.updateChannel == .beta { Button("Disable") { confirmDisableBeta() } } else { Button("Join Beta") { confirmJoinBeta() } } } } private func confirmDisableBeta() { if AppUpdateChannel.defaultChannel(for: .current) == .beta { /// If beta is disabled while running a beta release, user needs to reinstall release build manually. confirmDisableBetaNeedsReinstall() } else { /// If beta is disabled while running a non-beta release, no further action is needed. settings.updateChannel = .release } } /// Shown when user disables beta while running a beta release, which requires reinstalling a release version /// in order to effectivelly get out of the beta train. private func confirmDisableBetaNeedsReinstall() { let alert = NSAlert() alert.messageText = "Disable VirtualBuddy Beta" alert.informativeText = "In order to go back to the release version of VirtualBuddy, please download the latest release from GitHub and replace the current version you have installed." alert.addButton(withTitle: "Open Website") alert.addButton(withTitle: "Cancel") guard alert.runModal() == .alertFirstButtonReturn else { return } settings.updateChannel = .release guard let url = URL(string: "https://github.com/insidegui/VirtualBuddy/releases/latest") else { return } NSWorkspace.shared.open(url) } private func confirmJoinBeta() { let alert = NSAlert() alert.messageText = "Join VirtualBuddy Beta" alert.informativeText = """ Would like to join the beta and receive pre-release updates for testing? If you decide to stop receiving beta updates in the future, you will have to manually download and install the release version of VirtualBuddy. """ alert.addButton(withTitle: "Join Beta") alert.addButton(withTitle: "Cancel") guard alert.runModal() == .alertFirstButtonReturn else { return } settings.updateChannel = .beta } } extension URL { func performWriteTest() -> String? { if !FileManager.default.fileExists(atPath: path) { return "The directory \(lastPathComponent) doesn't exist." } do { let testFileURL = appendingPathComponent(".vbtest-\(UUID().uuidString)") guard FileManager.default.createFile(atPath: testFileURL.path, contents: nil) else { throw CocoaError(.fileWriteNoPermission) } try FileManager.default.removeItem(at: testFileURL) return nil } catch { return "VirtualBuddy is unable to write files to the directory \(lastPathComponent). Please check the permissions for that directory or choose a different one." } } } struct LibraryPathError: LocalizedError { var errorDescription: String? init(_ msg: String) { self.errorDescription = msg } } #if DEBUG #Preview("Library Settings") { SettingsScreen.preview(.general) } #endif ================================================ FILE: VirtualUI/Source/Settings/SettingsScreen.swift ================================================ // // PreferencesView.swift // VirtualBuddy // // Created by Guilherme Rambo on 05/06/22. // import SwiftUI import VirtualCore import DeepLinkSecurity import BuddyKit public enum SettingsTab: Int, Identifiable, CaseIterable { public var id: RawValue { rawValue } case general case virtualization case automation } extension SettingsTab { var label: Label { switch self { case .general: Label("General", systemImage: "gear") case .virtualization: Label("Virtualization", systemImage: "cpu") case .automation: Label("Automation", systemImage: "rectangle.grid.1x2") } } } private let kSelectedTabKey = "SettingsScreen.selectedTab" public struct SettingsScreen: View { #if DEBUG private let previewTab: SettingsTab? #endif @EnvironmentObject private var container: VBSettingsContainer @Binding private var enableAutomaticUpdates: Bool private var deepLinkSentinel: () -> DeepLinkSentinel public init(previewTab: SettingsTab? = nil, enableAutomaticUpdates: Binding, deepLinkSentinel: @escaping @autoclosure () -> DeepLinkSentinel) { self._enableAutomaticUpdates = enableAutomaticUpdates self.deepLinkSentinel = deepLinkSentinel #if DEBUG self.previewTab = previewTab #endif } @State private var alert = AlertContent() @AppStorage(kSelectedTabKey) private var selectedTab: SettingsTab = .general public static var width: CGFloat { 640 } public static var minHeight: CGFloat { 420 } public var body: some View { NavigationSplitView { List(selection: $selectedTab) { ForEach(SettingsTab.allCases) { tab in NavigationLink(value: tab) { tab.label } } } .toolbarRemovingSidebarToggle() .toolbar { // HACK! Don't want sidebar toggle, but want the toolbar visible Button("") { } .opacity(0) .accessibilityHidden(true) } .navigationSplitViewColumnWidth(160) } detail: { switch selectedTab { case .general: GeneralSettingsView( settings: $container.settings, enableAutomaticUpdates: $enableAutomaticUpdates, alert: $alert ) case .virtualization: VirtualizationSettingsView( settings: $container.settings ) case .automation: AutomationSettingsView() .environmentObject(deepLinkSentinel()) } } .formStyle(.grouped) .frame(minWidth: Self.width, maxWidth: Self.width, minHeight: Self.minHeight, maxHeight: .infinity) .alert($alert) .task { #if DEBUG guard let previewTab else { return } self.selectedTab = previewTab #endif } } } // MARK: - Previews #if DEBUG private extension VBSettingsContainer { static let preview: VBSettingsContainer = { VBSettingsContainer(with: UserDefaults()) }() } extension SettingsScreen { @ViewBuilder static func preview(_ tab: SettingsTab? = nil) -> some View { SettingsScreen(previewTab: tab, enableAutomaticUpdates: .constant(true), deepLinkSentinel: .preview) .environmentObject(VBSettingsContainer.preview) } } #Preview("Settings") { SettingsScreen.preview() } #endif ================================================ FILE: VirtualUI/Source/Settings/VirtualizationSettingsView.swift ================================================ // // VirtualizationSettingsView.swift // VirtualBuddy // // Created by Guilherme Rambo on 20/06/25. // import SwiftUI import VirtualCore import BuddyKit struct VirtualizationSettingsView: View { @Binding var settings: VBSettings #if DEBUG private var _forceShowBootImageFormatSettings: Bool { false } #endif private var showBootImageFormatSection: Bool { #if DEBUG guard !_forceShowBootImageFormatSettings else { return true } #endif return VBManagedDiskImage.Format.asif.isSupported } var body: some View { Form { Section { Toggle("Enable Apple Signing Status Check", isOn: $settings.enableTSSCheck) } header: { Text("Signing Check") } footer: { SettingsFooter { Text("Whether VirtualBuddy should verify macOS build signatures before downloading.") } helpText: { Text(""" With this enabled, VirtualBuddy will check if the selected macOS build is being signed by Apple \ before attempting to download it. Unsigned builds can not be installed in virtual machines, so this saves time and bandwidth, \ allowing you to choose another version before waiting for the entire download and install attempt. """) } } if showBootImageFormatSection { Section { Picker("Boot Image Format", selection: $settings.bootDiskImagesUseASIF) { Text("Most Efficient") .tag(true) Text("Most Compatible") .tag(false) } } header: { Text("Disk Images") } footer: { SettingsFooter { Text("Select the disk image format for new virtual machines") } helpText: { Text(""" - **Most Efficient:** Uses the ASIF format. Requires macOS 26 or later on the host. - **Most Compatible:** Uses a raw image format, supported by all macOS versions. You should only change this setting if you plan on using the same virtual machines in hosts \ that are on macOS 15 or earlier. """) } } } } .navigationTitle(Text("Virtualization")) } } #if DEBUG #Preview("Virtualization Settings") { SettingsScreen.preview(.virtualization) } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Components/ConfigurationSection.swift ================================================ // // ConfigurationSection.swift // VirtualUI // // Created by Guilherme Rambo on 18/07/22. // import SwiftUI import VirtualCore struct ConfigurationSection: View { @Binding private var collapsedStateBinding: Bool @State private var isCollapsed: Bool var content: () -> Content var header: () -> Header var collapsingDisabled: Bool init(_ collapsed: Binding? = nil, collapsingDisabled: Bool = false, @ViewBuilder _ content: @escaping () -> Content, @ViewBuilder header: @escaping () -> Header) { if collapsingDisabled { self._collapsedStateBinding = .constant(false) self._isCollapsed = .init(wrappedValue: false) } else { self._collapsedStateBinding = collapsed ?? .constant(true) self._isCollapsed = .init(wrappedValue: collapsed?.wrappedValue ?? true) } self.collapsingDisabled = collapsingDisabled self.header = header self.content = content } var body: some View { VStack(alignment: .leading, spacing: 0) { styledHeader if !isCollapsed { VStack(alignment: .leading, spacing: 6) { content() } .padding() .transition(.opacity) } } .controlGroup() .onChange(of: collapsedStateBinding) { newValue in guard newValue != isCollapsed else { return } isCollapsed = newValue } .onChange(of: isCollapsed) { newValue in guard collapsedStateBinding != newValue else { return } collapsedStateBinding = newValue } } @ViewBuilder private var styledHeader: some View { HStack { header() .frame(maxWidth: .infinity, alignment: .leading) Spacer() if !collapsingDisabled { Image(systemName: "chevron.down") .rotationEffect(.degrees(isCollapsed ? -90 : 0)) } } .font(.system(size: 14, weight: .medium, design: .rounded)) .padding(.horizontal) .padding(.vertical, 12) .frame(maxWidth: .infinity, alignment: .leading) .background { if isCollapsed { ContainerRelativeShape().inset(by: 2).foregroundStyle(Material.ultraThin) } } .overlay(alignment: .bottom) { Rectangle() .frame(maxWidth: .infinity, maxHeight: 0.5) .foregroundColor(.white.opacity(isCollapsed ? 0 : 0.1)) .padding(.horizontal, 1) .blendMode(.plusLighter) } .contentShape(ContainerRelativeShape()) .onTapGesture { guard !collapsingDisabled else { return } withAnimation(.default) { isCollapsed.toggle() } } } } ================================================ FILE: VirtualUI/Source/VM Configuration/Components/GroupedList.swift ================================================ // // GroupedList.swift // VirtualUI // // Created by Guilherme Rambo on 19/07/22. // import SwiftUI struct GroupedList: View { var content: () -> Content var headerAccessory: () -> HeaderAccessory var footerAccessory: () -> FooterAccessory var emptyOverlay: () -> EmptyOverlay var addButton: (Label) -> AddButton? var removeButton: (Label) -> RemoveButton? init(@ViewBuilder _ content: @escaping () -> Content, headerAccessory: @escaping () -> HeaderAccessory, footerAccessory: @escaping () -> FooterAccessory, emptyOverlay: @escaping () -> EmptyOverlay, addButton: @escaping (Label) -> AddButton? = { _ in nil }, removeButton: @escaping (Label) -> RemoveButton? = { _ in nil }) { self.content = content self.headerAccessory = headerAccessory self.footerAccessory = footerAccessory self.emptyOverlay = emptyOverlay self.addButton = addButton self.removeButton = removeButton } var body: some View { VStack(alignment: .leading, spacing: 8) { header content() .listStyle(.plain) .frame(minHeight: 140) .overlay { emptyOverlayContents } .safeAreaInset(edge: .bottom, alignment: .leading, spacing: 0, content: { listButtons }) .materialBackground(.contentBackground, blendMode: .withinWindow, state: .active, in: listShape) .controlGroup(level: .secondary) } } @ViewBuilder private var emptyOverlayContents: some View { VStack { emptyOverlay() } .buttonStyle(.link) .frame(maxWidth: .infinity) .controlSize(.small) } private var listRadius: CGFloat { 8 } private var listShape: some InsettableShape { RoundedRectangle(cornerRadius: listRadius, style: .continuous) } @State private var showTip = false @ViewBuilder private var header: some View { headerAccessory() .padding(.horizontal, 2) } private let addLabel = Label("Add", systemImage: "plus") private let removeLabel = Label("Remove", systemImage: "minus") @ViewBuilder private var listButtons: some View { if addButton(addLabel) != nil || removeButton(removeLabel) != nil || FooterAccessory.self != EmptyView.self { HStack { Group { if let addButton = addButton(addLabel) { addButton } if let removeButton = removeButton(removeLabel) { removeButton } } .labelStyle(.iconOnly) .applyGroupedListCommandButtonStyle() footerAccessory() } .padding(8) .frame(maxWidth: .infinity, alignment: .leading) .background(Material.thick, in: Rectangle()) .overlay(alignment: .top) { Rectangle() .frame(height: 0.5) .foregroundColor(.black.opacity(0.5)) } } } } extension GroupedList where HeaderAccessory == EmptyView, FooterAccessory == EmptyView, EmptyOverlay == EmptyView, AddButton == EmptyView, RemoveButton == EmptyView { init(@ViewBuilder _ content: @escaping () -> Content) { self.content = content self.headerAccessory = { EmptyView() } self.footerAccessory = { EmptyView() } self.emptyOverlay = { EmptyView() } self.addButton = { _ in nil } self.removeButton = { _ in nil } } } extension GroupedList where FooterAccessory == EmptyView, EmptyOverlay == EmptyView, AddButton == EmptyView, RemoveButton == EmptyView { init(@ViewBuilder _ content: @escaping () -> Content, headerAccessory: @escaping () -> HeaderAccessory) { self.content = content self.headerAccessory = headerAccessory self.footerAccessory = { EmptyView() } self.emptyOverlay = { EmptyView() } self.addButton = { _ in nil } self.removeButton = { _ in nil } } } extension GroupedList where HeaderAccessory == EmptyView, FooterAccessory == EmptyView { init(@ViewBuilder _ content: @escaping () -> Content, emptyOverlay: @escaping () -> EmptyOverlay, addButton: @escaping (Label) -> AddButton? = { _ in nil }, removeButton: @escaping (Label) -> RemoveButton? = { _ in nil }) { self.content = content self.headerAccessory = { EmptyView() } self.footerAccessory = { EmptyView() } self.emptyOverlay = emptyOverlay self.addButton = addButton self.removeButton = removeButton } } fileprivate extension View { func applyGroupedListCommandButtonStyle() -> some View { // the .accessoryBarAction looks nicer, but only available on macOS Sonoma // on older version of macOS, use the default style (equivalent to .bordered) if #available(macOS 14.0, *) { return self.buttonStyle(.accessoryBarAction) } else { return self } } } ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/DisplayConfigurationView.swift ================================================ // // DisplayConfigurationView.swift // VirtualUI // // Created by Guilherme Rambo on 18/07/22. // import SwiftUI import VirtualCore struct DisplayConfigurationView: View { @Binding var device: VBDisplayDevice @Binding var selectedPreset: VBDisplayPreset? var canChangePPI: Bool @Environment(\.resolvedRestoreImage) private var resolvedRestoreImage private var displayResizeStatus: ResolvedFeatureStatus? { resolvedRestoreImage?.feature(id: CatalogFeatureID.displayResize)?.status } private var displayResizeUnsupported: Bool { displayResizeStatus?.isUnsupported == true } var body: some View { if let warning = selectedPreset?.warning { Text(warning) .foregroundColor(.yellow) .padding(.bottom, 8) } NumericPropertyControl( value: $device.width, range: VBDisplayDevice.displayWidthRange, label: "Width (Pixels)", formatter: NumberFormatter.numericPropertyControlDefault, spacing: VMConfigurationView.labelSpacing ) NumericPropertyControl( value: $device.height, range: VBDisplayDevice.displayHeightRange, label: "Height (Pixels)", formatter: NumberFormatter.numericPropertyControlDefault, spacing: VMConfigurationView.labelSpacing ) if canChangePPI { NumericPropertyControl( value: $device.pixelsPerInch, range: VBDisplayDevice.displayPPIRange, label: "Pixels Per Inch", formatter: NumberFormatter.numericPropertyControlDefault, spacing: VMConfigurationView.labelSpacing ) } if VBDisplayDevice.automaticallyReconfiguresDisplaySupportedByHost { Group { if displayResizeUnsupported { let helpMessage = displayResizeStatus?.supportMessage ?? "Not supported." Toggle("Automatically Configure Display", isOn: $device.automaticallyReconfiguresDisplay) .disabled(true) .help(helpMessage) } else { Toggle("Automatically Configure Display", isOn: $device.automaticallyReconfiguresDisplay) } } .onChange(of: displayResizeUnsupported) { isUnsupported in if isUnsupported { device.automaticallyReconfiguresDisplay = false } } .onAppear { if displayResizeUnsupported { device.automaticallyReconfiguresDisplay = false } } } } @ViewBuilder var presetPicker: some View { DisplayPresetPicker(display: $device, selection: $selectedPreset) } } private struct DisplayPresetPicker: View { @Binding var display: VBDisplayDevice @Binding var selection: VBDisplayPreset? @State private var presets = [VBDisplayPreset]() @Environment(\.configurationGuestType) private var guestType: VBGuestType var body: some View { Menu { menuItems } label: { Image(systemName: "wand.and.stars") .foregroundColor(.accentColor) } .menuStyle(.borderlessButton) .help("Display Suggestions") .onAppear { presets = VBDisplayPreset.availablePresets(for: guestType) } } @ViewBuilder var menuItems: some View { ForEach(presets) { preset in Button(preset.name) { selection = preset display = preset.device } } } } #if DEBUG struct DisplayConfigurationView_Previews: PreviewProvider { static var previews: some View { _ConfigurationSectionPreview { DisplayConfigurationView(device: $0.hardware.displayDevices[0], selectedPreset: .constant(nil), canChangePPI: true) } } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/GuestAppConfigurationView.swift ================================================ // // GuestAppConfigurationView.swift // VirtualBuddy // // Created by Guilherme Rambo on 19/06/25. // import SwiftUI import VirtualCore struct GuestAppConfigurationView: View { @Binding var configuration: VBMacConfiguration @Environment(\.resolvedRestoreImage) private var resolvedRestoreImage private var guestAppStatus: ResolvedFeatureStatus? { resolvedRestoreImage?.feature(id: CatalogFeatureID.guestApp)?.status } private var guestAppUnsupported: Bool { guestAppStatus?.isUnsupported == true } private var guestAppHelp: String? { guestAppUnsupported ? (guestAppStatus?.supportMessage ?? "Not supported.") : nil } var body: some View { VStack(alignment: .leading, spacing: 16) { Group { if let guestAppHelp { Toggle("Enable VirtualBuddy Guest App", isOn: $configuration.guestAdditionsEnabled) .disabled(true) .help(guestAppHelp) } else { Toggle("Enable VirtualBuddy Guest App", isOn: $configuration.guestAdditionsEnabled) } } .onChange(of: guestAppUnsupported) { isUnsupported in if isUnsupported { configuration.guestAdditionsEnabled = false } } .onAppear { if guestAppUnsupported { configuration.guestAdditionsEnabled = false } } Text(""" The guest app mounts shared directories and shares the clipboard between your Mac and virtual machines. To install the app in your virtual machine, look for a disk image named “Guest” in the Finder sidebar. \ Double-click the VirtualBuddyGuest app icon to install the app. """) .font(.caption) .foregroundStyle(.secondary) .textSelection(.enabled) } } } #if DEBUG #Preview { _ConfigurationSectionPreview { GuestAppConfigurationView(configuration: $0) } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/HardwareConfigurationView.swift ================================================ // // HardwareConfigurationView.swift // VirtualUI // // Created by Guilherme Rambo on 18/07/22. // import SwiftUI import VirtualCore struct HardwareConfigurationView: View { @Binding var device: VBMacDevice var body: some View { NumericPropertyControl( value: $device.cpuCount, range: VBMacDevice.virtualCPUCountRange, label: "Virtual CPUs", formatter: NumberFormatter.numericPropertyControlDefault, spacing: VMConfigurationView.labelSpacing ) NumericPropertyControl( value: $device.memorySize.gbValue, range: VBMacDevice.memorySizeRangeInGigabytes, label: "Memory (GB)", formatter: NumberFormatter.numericPropertyControlDefault, spacing: VMConfigurationView.labelSpacing ) } } #if DEBUG struct HardwareConfigurationView_Previews: PreviewProvider { static var previews: some View { _ConfigurationSectionPreview { HardwareConfigurationView(device: $0.hardware) } } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/KeyboardDeviceConfigurationView.swift ================================================ // // KeyboardDeviceConfigurationView.swift // VirtualUI // // Created by Guilherme Rambo on 02/10/23. // import SwiftUI import VirtualCore struct KeyboardDeviceConfigurationView: View { @Binding var hardware: VBMacDevice @Environment(\.resolvedRestoreImage) private var resolvedRestoreImage private var macKeyboardFeature: ResolvedVirtualizationFeature? { resolvedRestoreImage?.feature(id: CatalogFeatureID.macKeyboard) } private var macKeyboardStatus: ResolvedFeatureStatus? { macKeyboardFeature?.status } private var macKeyboardUnsupported: Bool { macKeyboardStatus?.isUnsupported == true } private var availableKinds: [VBKeyboardDevice.Kind] { VBKeyboardDevice.Kind.allCases.filter { kind in kind != .mac || !macKeyboardUnsupported } } var body: some View { PropertyControl("Device Type", spacing: 8) { VStack(alignment: .leading) { Picker("Device Type", selection: $hardware.keyboardDevice.kind) { ForEach(availableKinds) { kind in Text(kind.name) .tag(kind) } } .onChange(of: macKeyboardUnsupported) { isUnsupported in if isUnsupported, hardware.keyboardDevice.kind == .mac { hardware.keyboardDevice.kind = .generic } } .onAppear { if macKeyboardUnsupported, hardware.keyboardDevice.kind == .mac { hardware.keyboardDevice.kind = .generic } } } } } } #if DEBUG struct KeyboardDeviceConfigurationView_Previews: PreviewProvider { static var previews: some View { _ConfigurationSectionPreview { KeyboardDeviceConfigurationView(hardware: $0.hardware) } } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/NetworkConfigurationView.swift ================================================ // // NetworkConfigurationView.swift // VirtualUI // // Created by Guilherme Rambo on 18/07/22. // import SwiftUI import VirtualCore import BuddyFoundation enum NetworkDeviceSelection: Identifiable, Hashable { var id: String { switch self { case .disabled: "DISABLED" case .NAT: "NAT" case .bridge(let interfaceID): "BRIDGE_\(interfaceID)" } } case disabled case NAT case bridge(_ interface: VBNetworkDeviceInterface.ID) } struct NetworkConfigurationView: View { @Binding var hardware: VBMacDevice var body: some View { VStack(alignment: .leading, spacing: 16) { NetworkDevicePicker(hardware: $hardware) if hardware.networkDevices.isEmpty { Text("This virtual machine will have no internet or local network access.") .foregroundColor(.secondary) } else { macAddressField } } } @ViewBuilder private var macAddressField: some View { PropertyControl("MAC Address") { EphemeralTextField($hardware.networkMACAddress, alignment: .leading) { addr in Text(addr) .textCase(.uppercase) } editableContent: { value in TextField("", text: .init(get: { value.wrappedValue.uppercased() }, set: { value.wrappedValue = $0.uppercased() })) } validate: { value in return VBNetworkDevice.validateMAC(value) } } } } extension VBMacDevice { var networkDeviceSelection: NetworkDeviceSelection { get { if let device = networkDevices.first { switch device.kind { case .NAT: .NAT case .bridge: .bridge(device.id) } } else { .disabled } } set { let restoreMACAddress = networkMACAddress switch newValue { case .disabled: networkDevices.removeAll() case .NAT: networkDevices = [.default.withMACAddress(restoreMACAddress)] case .bridge(let id): networkDevices = [.init(id: id, name: id, kind: .bridge).withMACAddress(restoreMACAddress)] } } } var networkMACAddress: String { get { switch networkDeviceSelection { case .disabled: "" case .NAT, .bridge: networkDevices.first?.macAddress ?? "" } } set { switch networkDeviceSelection { case .disabled: break case .NAT, .bridge: guard !networkDevices.isEmpty else { return } networkDevices[0].macAddress = newValue } } } } extension VBNetworkDevice { func withMACAddress(_ address: String) -> VBNetworkDevice { guard !address.isEmpty else { return self } var mself = self mself.macAddress = address return mself } } struct NetworkDevicePicker: View { @Binding var hardware: VBMacDevice @State private var selectedOption: VBNetworkDeviceInterface? @State private var interfaces: [VBNetworkDeviceInterface] = [.automatic] var body: some View { PropertyControl("Interface") { HStack { Picker("Interface", selection: $hardware.networkDeviceSelection) { Text("Disabled").tag(NetworkDeviceSelection.disabled) Text("NAT").tag(NetworkDeviceSelection.NAT) Section("Bridge") { ForEach(interfaces) { interface in Text(interface.name) .tag(NetworkDeviceSelection.bridge(interface.id)) } } } .labelsHidden() Spacer() Button { refresh() } label: { Image(systemName: "arrow.clockwise") } .buttonStyle(.plain) .help("Reload interfaces") } .task { refresh() } } } private func refresh() { interfaces = [.automatic] + VBNetworkDevice.bridgeInterfaces } } #if DEBUG #Preview { _ConfigurationSectionPreview(.networkPreviewNAT) { NetworkConfigurationView(hardware: $0.hardware) } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/PointingDeviceConfigurationView.swift ================================================ // // PointingDeviceConfigurationView.swift // VirtualUI // // Created by Guilherme Rambo on 19/07/22. // import SwiftUI import VirtualCore struct PointingDeviceConfigurationView: View { @Binding var hardware: VBMacDevice @Environment(\.resolvedRestoreImage) private var resolvedRestoreImage private var trackpadFeature: ResolvedVirtualizationFeature? { resolvedRestoreImage?.feature(id: CatalogFeatureID.trackpad) } private var trackpadStatus: ResolvedFeatureStatus? { trackpadFeature?.status } private var trackpadUnsupported: Bool { trackpadStatus?.isUnsupported == true } private var availableKinds: [VBPointingDevice.Kind] { VBPointingDevice.Kind.allCases.filter { kind in kind != .trackpad || !trackpadUnsupported } } var body: some View { PropertyControl("Device Type", spacing: 8) { VStack(alignment: .leading) { Picker("Device Type", selection: $hardware.pointingDevice.kind) { ForEach(availableKinds) { kind in Text(kind.name) .tag(kind) } } .onChange(of: trackpadUnsupported) { isUnsupported in if isUnsupported, hardware.pointingDevice.kind == .trackpad { hardware.pointingDevice.kind = .mouse } } .onAppear { if trackpadUnsupported, hardware.pointingDevice.kind == .trackpad { hardware.pointingDevice.kind = .mouse } } } } } } #if DEBUG struct PointingDeviceConfigurationView_Previews: PreviewProvider { static var previews: some View { _ConfigurationSectionPreview { PointingDeviceConfigurationView(hardware: $0.hardware) } } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/Sharing/SharedFolderListItem.swift ================================================ // // SharedFolderListItem.swift // VirtualUI // // Created by Guilherme Rambo on 19/07/22. // import SwiftUI import VirtualCore struct SharedFolderListItem: View { @Binding var folder: VBSharedFolder var body: some View { HStack(spacing: 2) { Toggle(folder.shortName, isOn: $folder.isEnabled) label } .lineLimit(1) .truncationMode(.middle) .controlSize(.mini) .disabled(folder.errorMessage != nil) .labelsHidden() .padding(.vertical, 4) /// Easier to hit trailing edge buttons without hovering floating scroll bar. .padding(.trailing, 4) } @ViewBuilder private var label: some View { HStack(spacing: 4) { folder.icon(maxHeight: 14) Text(folder.shortName) .help(folder.url.path) Spacer() if let errorMessage = folder.errorMessage { Image(systemName: "exclamationmark.triangle.fill") .symbolRenderingMode(.multicolor) .help(errorMessage) } else { Button { folder.isReadOnly.toggle() } label: { Image(systemName: folder.isReadOnly ? "pencil.slash" : "pencil") } .help(folder.isReadOnly ? "Make writable" : "Make read only") .buttonStyle(.plain) .disabled(!folder.isEnabled) } } .padding(.leading, 6) .opacity(folder.isEnabled ? 1 : 0.8) .opacity(folder.errorMessage != nil ? 0.3 : 1) .font(.system(size: 11)) } } extension VBSharedFolder { func icon(maxHeight: CGFloat) -> some View { let image: NSImage if let externalVolumeURL { image = NSWorkspace.shared.icon(forFile: externalVolumeURL.path) } else { image = NSWorkspace.shared.icon(forFile: url.path) } let dimension = min(image.size.width, image.size.height) let scale = (maxHeight) / dimension image.size = NSSize(width: image.size.width * scale, height: image.size.height * scale) return Image(nsImage: image) .resizable() .aspectRatio(contentMode: .fit) .frame(maxHeight: maxHeight) } } ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/Sharing/SharedFoldersManagementView.swift ================================================ // // SharedFoldersManagementView.swift // VirtualUI // // Created by Guilherme Rambo on 19/07/22. // import SwiftUI import VirtualCore struct SharedFoldersManagementView: View { @Binding var configuration: VBMacConfiguration @Environment(\.resolvedRestoreImage) private var resolvedRestoreImage @StateObject private var availabilityProvider: SharedFoldersAvailabilityProvider init(configuration: Binding) { self._configuration = configuration self._availabilityProvider = .init(wrappedValue: SharedFoldersAvailabilityProvider(configuration.wrappedValue)) self._showTip = .init(wrappedValue: !configuration.sharedFolders.isEmpty) } @State private var isShowingError = false @State private var errorMessage = "Error" @State private var selection = Set() @State private var selectionBeingRemoved: Set? @State private var isShowingRemovalConfirmation = false @State private var isShowingHelpPopover = false private var fileSharingStatus: ResolvedFeatureStatus? { guard configuration.systemType == .mac else { return nil } return resolvedRestoreImage?.feature(id: CatalogFeatureID.fileSharing)?.status } private var fileSharingUnsupported: Bool { fileSharingStatus?.isUnsupported == true } private var fileSharingHelp: String? { fileSharingUnsupported ? (fileSharingStatus?.supportMessage ?? "Not supported.") : nil } private var rosettaStatus: ResolvedFeatureStatus? { guard configuration.systemType == .linux else { return nil } return resolvedRestoreImage?.feature(id: CatalogFeatureID.rosettaSharing)?.status } private var rosettaUnsupported: Bool { rosettaStatus?.isUnsupported == true } private var rosettaHelp: String? { rosettaUnsupported ? (rosettaStatus?.supportMessage ?? "Not supported.") : nil } @ViewBuilder private var sharedFoldersList: some View { GroupedList { List(selection: $selection) { ForEach($configuration.sharedFolders) { $folder in SharedFolderListItem(folder: $folder) .contextMenu { folderMenu(for: $folder) } .tag(folder.id) } } } headerAccessory: { headerAccessory } footerAccessory: { EmptyView() } emptyOverlay: { emptyOverlay } addButton: { label in Button { addFolder() } label: { label } .help("Add shared folder") } removeButton: { label in Button { confirmRemoval() } label: { label } .help("Remove selection from shared folders") .disabled(selection.isEmpty) } } var body: some View { VStack(alignment: .leading, spacing: 16) { Group { if let fileSharingHelp { sharedFoldersList .help(fileSharingHelp) } else { sharedFoldersList } } .disabled(fileSharingUnsupported) if configuration.systemType == .mac, fileSharingUnsupported { Text(VBMacConfiguration.fileSharingNotice) .font(.caption) .foregroundColor(.yellow) } if configuration.systemType == .linux { Group { let rosettaToggleBind = if VBMacConfiguration.rosettaSupported && !rosettaUnsupported { $configuration.rosettaSharingEnabled } else { Binding.constant(false) } if let rosettaHelp { Toggle("Share Rosetta for Linux", isOn: rosettaToggleBind) .disabled(true) .help(rosettaHelp) } else { Toggle("Share Rosetta for Linux", isOn: rosettaToggleBind) .disabled(!VBMacConfiguration.rosettaSupported || rosettaUnsupported) } } .onChange(of: rosettaUnsupported) { isUnsupported in if isUnsupported { configuration.rosettaSharingEnabled = false } } .onAppear { if rosettaUnsupported { configuration.rosettaSharingEnabled = false } } if !rosettaUnsupported, let rosettaSharingNotice = VBMacConfiguration.rosettaSharingNotice() { Text(try! AttributedString(markdown: rosettaSharingNotice)) .font(.caption) .foregroundColor(.yellow) } } } .onReceive(NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didMountNotification)) { note in availabilityProvider.refreshAvailabilityIfNeeded(with: note) } .onReceive(NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didUnmountNotification)) { note in availabilityProvider.refreshAvailabilityIfNeeded(with: note) } .onChange(of: configuration) { availabilityProvider.configuration = $0 } .onChange(of: configuration.sharedFolders.count) { newValue in if newValue > 0 { withAnimation(.spring()) { showTip = true } } } .confirmationDialog("Remove Folders", isPresented: $isShowingRemovalConfirmation, titleVisibility: .visible, presenting: selectionBeingRemoved) { folders in Button(role: .cancel) { isShowingRemovalConfirmation = false } label: { Text("Cancel") } Button(role: .destructive) { guard let selectionBeingRemoved else { assertionFailure("How did we get here without a selection?") return } remove(selectionBeingRemoved) } label: { Text(removalConfirmationTitle(with: folders)) } } message: { folders in Text(removalConfirmationMessage(with: folders)) } } @ViewBuilder private var emptyOverlay: some View { if configuration.sharedFolders.isEmpty { Text("This VM has no shared folders.") Button("Add Shared Folder") { addFolder() } .buttonStyle(.link) } } @State private var showTip = false @ViewBuilder private var headerAccessory: some View { HStack { Text("Shared Folders") .frame(maxWidth: .infinity, alignment: .leading) if showTip { Button { isShowingHelpPopover.toggle() } label: { Image(systemName: "questionmark.circle.fill") } .buttonStyle(.borderless) .transition(.opacity) .popover(isPresented: $isShowingHelpPopover) { mountTip } .help("Shared folders help") } } } @ViewBuilder private var mountTip: some View { let tooltipCommon = """ To make your shared folders available in the virtual machine, run the following command in Terminal (Applications > Utilities > Terminal): ``` mkdir -p ~/Desktop/VirtualBuddyShared && mount -t virtiofs VirtualBuddyShared ~/Desktop/VirtualBuddyShared ``` A folder named "VirtualBuddyShared" will show up on the Desktop. """ let rosettaVm = VBMacConfiguration.rosettaSupported && configuration.systemType == .linux let tooltipRosetta = if rosettaVm { """ To make Rosetta binaries available in the Linux virtual machine, run the following command in the Linux guest's Terminal: ``` mount -t virtiofs Rosetta /mnt ``` The Rosetta binaries will be ready in `/mnt`. Follow [this instruction](https://developer.apple.com/documentation/virtualization/running_intel_binaries_in_linux_vms_with_rosetta#3978489) to allow x86-64 binaries to be run on the Linux guest. """ } else { "" } let tooltipRosettaInstall = if rosettaVm && !VBMacConfiguration.rosettaInstalled() { """ Rosetta cannot be used unless installed on the host. To install Rosetta, run the following command in host's Terminal: ``` softwareupdate --install-rosetta ``` """ } else { "" } let tooltipTexts = [tooltipCommon, tooltipRosetta, tooltipRosettaInstall].joined() let tooltipAttributedText = try! AttributedString( markdown: tooltipTexts, options: AttributedString.MarkdownParsingOptions(interpretedSyntax:.inlineOnlyPreservingWhitespace) ) Text(tooltipAttributedText) .textSelection(.enabled) .foregroundColor(.white) .padding() .multilineTextAlignment(.leading) } @ViewBuilder private func folderMenu(for folder: Binding) -> some View { Group { Toggle("Enabled", isOn: folder.isEnabled) Toggle("Read Only", isOn: folder.isReadOnly) Divider() Button("Reveal In Finder") { NSWorkspace.shared.selectFile(folder.wrappedValue.url.path, inFileViewerRootedAtPath: folder.wrappedValue.url.deletingLastPathComponent().path) } } .disabled(!availabilityProvider.isFolderAvailable(folder.wrappedValue)) Button("Remove") { confirmRemoval(for: [folder.wrappedValue.id]) } } private func addFolder() { guard let newFolderURL = NSOpenPanel.run(accepting: [.folder], defaultDirectoryKey: "sharedFolders") else { return } do { try configuration.addSharedFolder(with: newFolderURL) } catch { errorMessage = error.localizedDescription isShowingError = true } } private func confirmRemoval(for folders: Set? = nil) { let targetFolders = folders ?? selection guard !targetFolders.isEmpty else { return } selectionBeingRemoved = targetFolders isShowingRemovalConfirmation = true } private func remove(_ identifiers: Set) { configuration.removeSharedFolders(with: identifiers) } private func removalConfirmationTitle(with selection: Set) -> String { guard selection.count == 1, let singleID = selection.first, let folder = configuration.sharedFolders.first(where: { $0.id == singleID }) else { return "Remove \(selection.count) Folders" } return "Remove \"\(folder.shortNameForDialogs)\"" } private func removalConfirmationMessage(with selection: Set) -> String { guard selection.count == 1, let singleID = selection.first, let folder = configuration.sharedFolders.first(where: { $0.id == singleID }) else { return "Are you sure you'd like to remove \(selection.count) shared folders?" } return "Are you sure you'd like to remove \"\(folder.shortNameForDialogs)\" from the shared folders?" } } private final class SharedFoldersAvailabilityProvider: ObservableObject { var configuration: VBMacConfiguration init(_ configuration: VBMacConfiguration) { self.configuration = configuration refreshAvailability() } @Published private(set) var folderAvailability: [VBSharedFolder.ID: Bool] = [:] func isFolderAvailable(_ folder: VBSharedFolder) -> Bool { folderAvailability[folder.id] ?? false } func refreshAvailability() { for folder in configuration.sharedFolders { folderAvailability[folder.id] = folder.isAvailable } } func refreshAvailabilityIfNeeded(with notification: Notification) { guard let volumeURL = notification.userInfo?["NSWorkspaceVolumeURLKey"] as? URL else { return } guard configuration.hasSharedFolders(inVolume: volumeURL) else { return } refreshAvailability() } } #if DEBUG struct SharedFoldersManagementView_Previews: PreviewProvider { static var previews: some View { SharingConfigurationView_Previews.previews } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/Sharing/SharingConfigurationView.swift ================================================ // // SharingConfigurationView.swift // VirtualUI // // Created by Guilherme Rambo on 18/07/22. // import SwiftUI import VirtualCore struct SharingConfigurationView: View { @Binding var configuration: VBMacConfiguration var body: some View { SharedFoldersManagementView(configuration: $configuration) } } #if DEBUG struct _ConfigurationSectionPreview: View { @State private var config: VBMacConfiguration var content: (Binding) -> C var ungrouped: Bool init(_ config: VBMacConfiguration = .preview, ungrouped: Bool = false, @ViewBuilder _ content: @escaping (Binding) -> C) { self._config = .init(wrappedValue: config) self.ungrouped = ungrouped self.content = content } var body: some View { Group { if ungrouped { content($config) } else { ConfigurationSection(.constant(false), { content($config) }, header: { Label("SwiftUI Preview", systemImage: "eye") }) } } .frame(maxWidth: 320, maxHeight: .infinity, alignment: .top) .padding() .controlGroup() .padding(30) .frame(maxWidth: .infinity, maxHeight: .infinity) } } struct SharingConfigurationView_Previews: PreviewProvider { static var previews: some View { _ConfigurationSectionPreview { SharingConfigurationView(configuration: $0) } _ConfigurationSectionPreview(.preview.linuxVirtualMachine) { SharingConfigurationView(configuration: $0) } .previewDisplayName("Linux Sharing Configuration View") _ConfigurationSectionPreview(.preview.removingSharedFolders) { SharingConfigurationView(configuration: $0) } .previewDisplayName("Empty") } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/SoundConfigurationView.swift ================================================ // // SoundConfigurationView.swift // VirtualUI // // Created by Guilherme Rambo on 18/07/22. // import SwiftUI import VirtualCore struct SoundConfigurationView: View { @Binding var hardware: VBMacDevice var body: some View { VStack(alignment: .leading, spacing: 16) { Toggle("Enable Sound", isOn: soundEnabled) if hardware.soundDevices.isEmpty { Toggle("Enable Sound Input", isOn: .constant(false)) .disabled(true) } else { Toggle("Enable Sound Input", isOn: $hardware.soundDevices[0].enableInput) } } } private var soundEnabled: Binding { .init(get: { !hardware.soundDevices.isEmpty }, set: { newValue in if newValue, hardware.soundDevices.isEmpty { hardware.soundDevices = [.default] } else { hardware.soundDevices.removeAll() } }) } } #if DEBUG struct SoundConfigurationView_Previews: PreviewProvider { static var previews: some View { _ConfigurationSectionPreview { SoundConfigurationView(hardware: $0.hardware) } } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/Storage/ManagedDiskImageEditor.swift ================================================ // // ManagedDiskImageEditor.swift // VirtualUI // // Created by Guilherme Rambo on 20/07/22. // import SwiftUI import VirtualCore struct ManagedDiskImageEditor: View { @State private var image: VBManagedDiskImage var minimumSize: UInt64 var isExistingDiskImage: Bool var onSave: (VBManagedDiskImage) -> Void var isBootVolume: Bool init(image: VBManagedDiskImage, isExistingDiskImage: Bool, isForBootVolume: Bool, onSave: @escaping (VBManagedDiskImage) -> Void) { self._image = .init(wrappedValue: image) self.isExistingDiskImage = isExistingDiskImage self.onSave = onSave let fallbackMinimumSize = isForBootVolume ? VBManagedDiskImage.minimumBootDiskImageSize : VBManagedDiskImage.minimumExtraDiskImageSize self.minimumSize = isExistingDiskImage ? image.size : fallbackMinimumSize self.isBootVolume = isForBootVolume } private let formatter: ByteCountFormatter = { let f = ByteCountFormatter() f.allowedUnits = [.useGB, .useMB, .useTB] f.formattingContext = .standalone f.countStyle = .file return f }() @State private var nameError: String? @Environment(\.dismiss) private var dismiss var body: some View { VStack(alignment: .leading) { VStack(alignment: .leading) { if let nameError { Text(nameError) .font(.caption) .foregroundColor(.red) .fixedSize(horizontal: false, vertical: true) .lineLimit(nil) .padding(.bottom) } } let maximumSize = isBootVolume ? VBManagedDiskImage.maximumBootDiskImageSize : VBManagedDiskImage.maximumExtraDiskImageSize NumericPropertyControl( value: $image.size.gbStorageValue, range: minimumSize.gbStorageValue...maximumSize.gbStorageValue, hideSlider: isExistingDiskImage, label: isBootVolume ? "Boot Disk Size (GB)" : "Disk Image Size (GB)", formatter: NumberFormatter.numericPropertyControlDefault ) .disabled(isExistingDiskImage) .foregroundColor(sizeWarning != nil ? .yellow : .primary) VStack(alignment: .leading, spacing: 8) { if !isExistingDiskImage, !isBootVolume { Text("You'll have to use Disk Utility in the guest operating system to initialize the disk image. If you see an error after it boots up, choose the \"Initialize\" option.") .foregroundColor(.yellow) } if let sizeWarning { Text(sizeWarning) .foregroundColor(.yellow) } if isBootVolume { Text(sizeChangeInfo) .foregroundColor(.yellow) if let sizeMessagePrefix { Text(sizeMessagePrefix) } } else { Text(sizeMessage) } } .font(.callout) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) .lineLimit(nil) } .onChange(of: image) { newValue in onSave(newValue) } } private var sizeMessagePrefix: String? { VBSettingsContainer.current.isLibraryInAPFSVolume ? "The storage space you make available for the disk won't be used immediately, only the space that's used by the virtual machine will be consumed. " : nil } private var sizeChangeInfo: String { if isBootVolume { return "Be sure to reserve enough space, since it won't be possible to change the size of the disk later." } else { return "It's not possible to change the size of an existing storage device." } } private var sizeMessage: String { if isExistingDiskImage { return sizeChangeInfo } else { return "\(sizeMessagePrefix ?? "")After adding the storage device, it won't be possible to change the size of its disk image with VirtualBuddy." } } private var sizeWarning: String? { guard !VBSettingsContainer.current.libraryVolumeCanFit(image.size) else { return nil } let volumeDescription: String if let volumeName = VBSettingsContainer.current.settings.libraryURL.containingVolumeName { volumeDescription = "\"\(volumeName)\"" } else { volumeDescription = "where your library is stored" } return "The volume \(volumeDescription) doesn't have enough free space to fit the full size of the disk image." } } #if DEBUG struct ManagedDiskImageEditor_Previews: PreviewProvider { static var previews: some View { StorageDeviceDetailView_Previews.previews } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/Storage/StorageConfigurationView.swift ================================================ // // StorageConfigurationView.swift // VirtualUI // // Created by Guilherme Rambo on 19/07/22. // import SwiftUI import VirtualCore struct StorageConfigurationView: View { @EnvironmentObject var viewModel: VMConfigurationViewModel @Binding var hardware: VBMacDevice @State private var selection = Set() @State private var isShowingDeviceConfigurationSheet = false @State private var deviceBeingConfigured: VBStorageDevice? private var hideBootDisk: Bool { viewModel.context == .preInstall } private func shouldHide(_ device: VBStorageDevice) -> Bool { if hideBootDisk { return device.isBootVolume } else { return false } } var body: some View { GroupedList { List(selection: $selection) { ForEach($hardware.storageDevices.filter({ !shouldHide($0.wrappedValue) })) { $device in StorageDeviceListItem(device: $device) { configure(device) } .tag(device.id) } } } emptyOverlay: { EmptyView() } addButton: { label in Button { create() } label: { label } .help("Add storage device") } removeButton: { label in Button { for deviceID in selection { guard let idx = hardware.storageDevices.firstIndex(where: { $0.id == deviceID }) else { continue } guard !hardware.storageDevices[idx].isBootVolume else { continue } hardware.storageDevices.remove(at: idx) } } label: { label } .disabled(selection.isEmpty) .help("Remove selected devices") } .sheet(isPresented: $isShowingDeviceConfigurationSheet) { let device = deviceBeingConfigured ?? .template let isNewDevice = deviceBeingConfigured == nil StorageDeviceDetailView(device: device, isNewDevice: isNewDevice, onSave: { updatedDevice in if isNewDevice { try await createImageIfNeeded(for: updatedDevice) } hardware.addOrUpdate(updatedDevice) }) .environmentObject(viewModel) .padding() .frame(minWidth: 280, idealWidth: 340, maxWidth: .infinity) } } private func configure(_ device: VBStorageDevice?) { deviceBeingConfigured = device isShowingDeviceConfigurationSheet = true } private func create() { configure(nil) } private func createImageIfNeeded(for newDevice: VBStorageDevice) async throws { guard newDevice.usesManagedDiskImage else { return } try await viewModel.createImage(for: newDevice) } } struct StorageDeviceListItem: View { @Binding var device: VBStorageDevice var configureDevice: () -> Void var body: some View { HStack(spacing: 4) { Toggle(device.displayName, isOn: $device.isEnabled) .disabled(device.isBootVolume) .help(device.isBootVolume ? "The boot storage device can't be disabled" : "Enable/disable this storage device") label } .lineLimit(1) .truncationMode(.middle) .labelsHidden() .padding(6) } @ViewBuilder private var label: some View { HStack(spacing: 6) { device.iconView Text(device.displayName) Spacer() Button { configureDevice() } label: { Image(systemName: "ellipsis.circle") } .help("Device settings") .buttonStyle(.plain) .disabled(device.isBootVolume) } .padding(.leading, 6) .opacity(device.isEnabled ? 1 : 0.8) } } extension VBStorageDevice { var icon: Image { if isUSBMassStorageDevice { return Image(systemName: "externaldrive.fill") } else if isBootVolume { return Image(systemName: "wrench.and.screwdriver.fill") } else { return Image(systemName: "internaldrive.fill") } } @ViewBuilder var iconView: some View { icon .resizable() .aspectRatio(contentMode: .fit) .frame(maxWidth: 16) .symbolRenderingMode(.hierarchical) } } #if DEBUG struct StorageConfigurationView_Previews: PreviewProvider { static var previews: some View { _ConfigurationSectionPreview { StorageConfigurationView(hardware: $0.hardware) } .environmentObject(VMConfigurationViewModel(.preview)) } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/Sections/Storage/StorageDeviceDetailView.swift ================================================ // // StorageDeviceDetailView.swift // VirtualUI // // Created by Guilherme Rambo on 19/07/22. // import SwiftUI import VirtualCore struct StorageDeviceDetailView: View { @EnvironmentObject var viewModel: VMConfigurationViewModel @State private var device: VBStorageDevice var onSave: (VBStorageDevice) async throws -> Void init(device: VBStorageDevice, isNewDevice: Bool, onSave: @escaping (VBStorageDevice) async throws -> Void) { self._device = .init(wrappedValue: device) self.isNewDevice = isNewDevice self._imageType = .init(wrappedValue: isNewDevice ? nil : device.usesManagedDiskImage ? .managed : .custom) self.onSave = onSave } @State private var isLoading = false private var canSave: Bool { guard !isLoading else { return false } if imageType == .managed { return device.managedImage != nil } else { return device.customImageURL != nil } } @State private var managedImage: VBManagedDiskImage? @State private var customImageURL: URL? private var isNewDevice: Bool @State private var nameError: String? @Environment(\.dismiss) private var dismiss private var canEditName: Bool { device.usesManagedDiskImage && !device.diskImageExists(for: viewModel.vm) } var body: some View { VStack(alignment: .leading) { if isNewDevice { if imageType != nil { detail } else { imageTypePicker } } else { detail } Spacer() HStack { Button("Cancel") { dismiss() } .keyboardShortcut(.cancelAction) Spacer() Button("Done") { save() } .keyboardShortcut(.defaultAction) .disabled(!canSave) } .padding(.top) } } private func save() { isLoading = true Task { do { try await onSave(device) dismiss() } catch { NSAlert(error: error).runModal() } isLoading = false } } @ViewBuilder private var detail: some View { VStack(alignment: .leading, spacing: 2) { HStack { device.iconView EphemeralTextField($device.nameBinding, alignment: .leading) { name in Text(name) } editableContent: { binding in TextField("", text: binding) } .disabled(!canEditName) } diskImageDetail } .padding(.bottom) Spacer() VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 4) { Toggle("External Device", isOn: $device.isUSBMassStorageDevice) .disabled(!VBStorageDevice.hostSupportsUSBMassStorage) Text(VBStorageDevice.hostSupportsUSBMassStorage ? "Exposes the disk image as an external USB mass storage device to the virtual machine." : "This feature requires macOS 13 or later.") .font(.callout) .foregroundColor(.secondary) .lineLimit(nil) .fixedSize(horizontal: false, vertical: true) } VStack(alignment: .leading, spacing: 4) { Toggle("Read Only", isOn: $device.isReadOnly) Text("Makes the storage device appear read-only to the virtual machine. This doesn't affect the disk image.") .font(.callout) .foregroundColor(.secondary) .lineLimit(nil) .fixedSize(horizontal: false, vertical: true) } } .disabled(device.isBootVolume) .help(device.isBootVolume ? "These options are not available for the boot device" : "") } @State private var imageType: ImageType? private enum ImageType: Int, Identifiable, CaseIterable { var id: RawValue { rawValue } case managed case custom var name: String { switch self { case .managed: return "VirtualBuddy Disk Image" case .custom: return "Custom Image File" } } } @ViewBuilder private var imageTypePicker: some View { VStack(alignment: .leading) { VStack(alignment: .leading, spacing: 32) { HStack { device.iconView Text("New Storage Device") } VStack(alignment: .leading, spacing: 16) { Text("How would you like to create this storage device?") .padding(.bottom, 6) .font(.headline) Button { imageType = .managed device.backing = .managedImage(.template) } label: { Label("Create a new disk image with VirtualBuddy", systemImage: "externaldrive.fill.badge.plus") } Button { selectCustomImage() } label: { Label("Select an existing disk image file", systemImage: "folder.fill.badge.plus") } } .buttonStyle(.link) .symbolRenderingMode(.hierarchical) } .padding(.bottom) } .frame(maxWidth: .infinity, alignment: .leading) } @ViewBuilder private var diskImageDetail: some View { VStack(alignment: .leading) { switch device.backing { case .managedImage(let image): ManagedDiskImageEditor( image: image, isExistingDiskImage: device.diskImageExists(for: viewModel.vm), isForBootVolume: device.isBootVolume, onSave: { device.update(with: $0, type: .size) } ) case .customImage(let url): customDiskImageURLView(with: url) } } } @ViewBuilder private func customDiskImageURLView(with url: URL) -> some View { PropertyControl("Custom Disk Image File:", spacing: 8) { HStack { Text(url.path) .truncationMode(.middle) .lineLimit(1) Button("Change…") { selectCustomImage() } .controlSize(.small) } } .padding(.top) } private func selectCustomImage() { guard let url = NSOpenPanel.run(accepting: [.diskImage], defaultDirectoryKey: "storageCustomImage") else { return } device.backing = .customImage(url) imageType = .custom } private func updateImage(with newImage: VBManagedDiskImage) { device.backing = .managedImage(newImage) } } extension VBStorageDevice { var usesManagedDiskImage: Bool { guard case .managedImage = backing else { return false } return true } var usesCustomDiskImage: Bool { guard case .customImage = backing else { return false } return true } var managedImage: VBManagedDiskImage? { guard case .managedImage(let image) = backing else { return nil } return image } var customImageURL: URL? { guard case .customImage(let url) = backing else { return nil } return url } } extension Binding where Value == VBStorageDevice { var nameBinding: Binding { switch wrappedValue.backing { case .customImage: return .constant(wrappedValue.displayName) case .managedImage: return .init { wrappedValue.managedImage?.filename ?? "" } set: { newValue in guard var image = wrappedValue.managedImage else { return } image.filename = newValue wrappedValue.backing = .managedImage(image) } } } } enum VBStorageImageUpdate { case name case size } extension VBStorageDevice { mutating func update(with image: VBManagedDiskImage, type: VBStorageImageUpdate) { guard var managedImage else { backing = .managedImage(image) return } switch type { case .name: managedImage.filename = image.filename case .size: managedImage.size = image.size } backing = .managedImage(managedImage) } } #if DEBUG struct StorageDeviceDetailView_Previews: PreviewProvider { static var previews: some View { let config = VBMacConfiguration.preview ForEach(config.hardware.storageDevices.indices, id: \.self) { preview(at: $0) .previewDisplayName(config.hardware.storageDevices[$0].displayName) } } @ViewBuilder static func preview(at index: Int) -> some View { _ConfigurationSectionPreview(ungrouped: true) { StorageDeviceDetailView(device: $0.wrappedValue.hardware.storageDevices[index], isNewDevice: $0.wrappedValue.hardware.storageDevices[index].displayName == "New Device", onSave: { _ in }) } .frame(maxHeight: 400) .environmentObject(VMConfigurationViewModel(.preview)) } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/VMConfigurationSheet.swift ================================================ // // VMConfigurationSheet.swift // VirtualUI // // Created by Guilherme Rambo on 18/07/22. // import SwiftUI import VirtualCore public struct VMConfigurationSheet: View { public static let minWidth: CGFloat = 520 @EnvironmentObject private var viewModel: VMConfigurationViewModel /// The VM configuration as it existed when the user opened the configuration UI. /// Can be used to reset aspects of the configuration to their previous values. private var initialConfiguration: VBMacConfiguration /// The configuration that gets saved with the VM. /// Setting this saves the configuration. @Binding private var savedConfiguration: VBMacConfiguration @State private var showValidationErrors = false private var showsCancelButton: Bool { viewModel.context == .postInstall } private var customConfirmationButtonAction: ((VBMacConfiguration) -> Void)? = nil /// Initializes the VM configuration sheet, bound to a VM configuration model. /// - Parameter configuration: The binding that will be updated when the user saves the configuration by clicking the "Done" button. public init(configuration: Binding) { self.init(configuration: configuration, showingValidationErrors: false) } init(configuration: Binding, showingValidationErrors: Bool = false, customConfirmationButtonAction: ((VBMacConfiguration) -> Void)? = nil) { self.initialConfiguration = configuration.wrappedValue self._savedConfiguration = configuration self._showValidationErrors = .init(wrappedValue: showingValidationErrors) self.customConfirmationButtonAction = customConfirmationButtonAction } @Environment(\.dismiss) private var dismiss @Environment(\.containerPadding) private var containerPadding @Environment(\.maxContentWidth) private var maxContentWidth private var isInstall: Bool { viewModel.context == .preInstall } public var body: some View { ScrollView(.vertical) { VMConfigurationView(initialConfiguration: initialConfiguration) .environmentObject(viewModel) .frame(maxWidth: isInstall ? maxContentWidth : nil) .padding(containerPadding) .frame(maxWidth: .infinity) } .frame(maxWidth: .infinity, maxHeight: .infinity) .safeAreaInset(edge: .bottom, spacing: 0) { buttons } .resizableSheet(minWidth: Self.minWidth, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity) } @ViewBuilder private var buttons: some View { VStack(alignment: .leading, spacing: 16) { if showValidationErrors { validationErrors } HStack { if showsCancelButton { Button("Cancel") { dismiss() } .keyboardShortcut(.cancelAction) } Spacer() Button(viewModel.context == .preInstall ? "Continue" : "Done") { validateAndSave() } .keyboardShortcut(.defaultAction) .disabled(showValidationErrors) } } .virtualBuddyBottomBarStyle() .onChange(of: viewModel.config) { newValue in guard showValidationErrors else { return } Task { if await viewModel.validate() == .supported { showValidationErrors = false } } } } @ViewBuilder private var validationErrors: some View { if case .unsupported(let errors) = viewModel.supportState { ForEach(errors, id: \.self) { Text($0) } .foregroundColor(.red) } } private func validateAndSave() { showValidationErrors = true Task { let state = await viewModel.validate() guard state.allowsSaving else { return } savedConfiguration = viewModel.config if let customConfirmationButtonAction { customConfirmationButtonAction(savedConfiguration) } else { dismiss() } } } } #if DEBUG struct VMConfigurationSheet_Previews: PreviewProvider { static var height: Double { 1200 } static var previews: some View { _Template(vm: .preview, context: .preInstall) .previewDisplayName("Pre Install") _Template(vm: .preview, context: .postInstall) .previewDisplayName("Post Install") _Template(vm: .previewLinux, context: .postInstall) .previewDisplayName("Linux - Post") _Template(vm: .previewLinux, context: .preInstall) .previewDisplayName("Linux - Pre") } struct _Template: View { @State var vm: VBVirtualMachine var context: VMConfigurationContext var body: some View { if context == .postInstall { PreviewSheet { VMConfigurationSheet(configuration: $vm.configuration) .environmentObject(VMConfigurationViewModel(vm, context: context)) .frame(width: VMConfigurationSheet.minWidth, height: VMConfigurationSheet_Previews.height, alignment: .top) } } else { VMConfigurationSheet(configuration: $vm.configuration) .environmentObject(VMConfigurationViewModel(vm, context: context)) .frame(width: VMConfigurationSheet.minWidth, height: VMConfigurationSheet_Previews.height, alignment: .top) .background(BlurHashFullBleedBackground(blurHash: .virtualBuddyBackground)) } } } } /// Simulates a macOS sheet for SwiftUI previews. struct PreviewSheet: View { var content: () -> Content init(@ViewBuilder _ content: @escaping () -> Content) { self.content = content } var body: some View { ZStack {} .frame(width: VMConfigurationSheet.minWidth, height: VMConfigurationSheet_Previews.height) .padding() .background(Color.black.opacity(0.5)) .overlay { content() .controlGroup() } } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/VMConfigurationView.swift ================================================ // // VMConfigurationView.swift // VirtualBuddy // // Created by Guilherme Rambo on 17/07/22. // import SwiftUI import VirtualCore extension EnvironmentValues { /// Type of guest that's currently being configured in a `VMConfigurationView`. @Entry fileprivate(set) var configurationGuestType: VBGuestType = .mac } private struct ResolvedRestoreImageKey: EnvironmentKey { static let defaultValue: ResolvedRestoreImage? = nil } extension EnvironmentValues { var resolvedRestoreImage: ResolvedRestoreImage? { get { self[ResolvedRestoreImageKey.self] } set { self[ResolvedRestoreImageKey.self] = newValue } } } enum CatalogFeatureID { static let fileSharing = "file_sharing" static let guestApp = "guest_app" static let trackpad = "trackpad" static let macKeyboard = "mac_keyboard" static let stateRestoration = "state_restoration" static let displayResize = "display_resize" static let rosettaSharing = "rosetta_sharing" } extension ResolvedRestoreImage { func feature(id: String) -> ResolvedVirtualizationFeature? { features.first { $0.id == id } } } extension ResolvedFeatureStatus { var supportMessage: String? { switch self { case .supported: return nil case .warning(let title, let message), .unsupported(let title, let message): return title ?? message } } var supportMessageColor: Color { switch self { case .supported: return .secondary case .warning: return .yellow case .unsupported: return .red } } } struct VMConfigurationView: View { @EnvironmentObject private var viewModel: VMConfigurationViewModel var initialConfiguration: VBMacConfiguration static var labelSpacing: CGFloat { 2 } @AppStorage("config.general.collapsed") private var generalCollapsed = true @AppStorage("config.storage.collapsed") private var storageCollapsed = true @AppStorage("config.display.collapsed") private var displayCollapsed = true @AppStorage("config.pointing.collapsed") private var pointingCollapsed = true @AppStorage("config.keyboard.collapsed") private var keyboardCollapsed = true @AppStorage("config.network.collapsed") private var networkCollapsed = true @AppStorage("config.sound.collapsed") private var soundCollapsed = true @AppStorage("config.sharing.collapsed") private var sharingCollapsed = true @AppStorage("config.guestApp.collapsed") private var guestAppCollapsed = true private var systemType: VBGuestType { viewModel.config.systemType } private var showBootDiskSection: Bool { viewModel.context == .preInstall } private var showPointingDeviceSection: Bool { systemType.supportsVirtualTrackpad } private var showKeyboardDeviceSection: Bool { systemType.supportsKeyboardCustomization } private var showDisplayPPISection: Bool { systemType.supportsDisplayPPI } private var showGuestAppSection: Bool { systemType.supportsGuestApp } var body: some View { VStack(alignment: .leading, spacing: 16) { if showBootDiskSection { bootDisk } general storage display if showPointingDeviceSection { pointingDevice } if showKeyboardDeviceSection { keyboardDevice } network sound if showGuestAppSection { guestApp } sharing .frame(minWidth: 0, idealWidth: VMConfigurationSheet.minWidth) } .font(.system(size: 12)) .environment(\.configurationGuestType, viewModel.config.systemType) .environment(\.resolvedRestoreImage, viewModel.resolvedRestoreImage) } @ViewBuilder private var general: some View { ConfigurationSection($generalCollapsed) { HardwareConfigurationView(device: $viewModel.config.hardware) } header: { SummaryHeader( "General", systemImage: "memorychip", summary: viewModel.config.generalSummary ) } } @ViewBuilder private var bootDisk: some View { ConfigurationSection(.constant(false), collapsingDisabled: true) { if let image = (try? viewModel.vm.bootDevice)?.managedImage { ManagedDiskImageEditor(image: image, isExistingDiskImage: false, isForBootVolume: true) { image in viewModel.updateBootStorageDevice(with: image) } } else { Text("Something went terribly wrong: VM doesn't have a boot storage device with a managed disk image.") .foregroundColor(.red) } } header: { SummaryHeader( "Boot Disk", systemImage: "wrench.and.screwdriver" ) } } private var storageSummary: String { if showBootDiskSection { return viewModel.config.hardware.storageDevices.count == 1 ? "None" : viewModel.config.storageSummary } else { return viewModel.config.storageSummary } } @ViewBuilder private var storage: some View { ConfigurationSection($storageCollapsed) { StorageConfigurationView(hardware: $viewModel.config.hardware) .environmentObject(viewModel) } header: { SummaryHeader( showBootDiskSection ? "Additional Storage" : "Storage", systemImage: "externaldrive", summary: storageSummary ) } .contextMenu { Button("Reset General Settings") { viewModel.config.hardware.cpuCount = initialConfiguration.hardware.cpuCount viewModel.config.hardware.memorySize = initialConfiguration.hardware.memorySize } } } @ViewBuilder private var display: some View { ConfigurationSection($displayCollapsed) { DisplayConfigurationView( device: $viewModel.config.hardware.displayDevices[0], selectedPreset: $viewModel.selectedDisplayPreset, canChangePPI: showDisplayPPISection ) } header: { SummaryHeader("Display", systemImage: "display", summary: viewModel.config.displaySummary) { DisplayConfigurationView( device: $viewModel.config.hardware.displayDevices[0], selectedPreset: $viewModel.selectedDisplayPreset, canChangePPI: showDisplayPPISection ) .presetPicker .frame(width: 24) } } } @ViewBuilder private var pointingDevice: some View { ConfigurationSection($pointingCollapsed) { PointingDeviceConfigurationView(hardware: $viewModel.config.hardware) } header: { SummaryHeader( "Pointing Device", systemImage: "cursorarrow", summary: viewModel.config.pointingDeviceSummary ) } } @ViewBuilder private var keyboardDevice: some View { ConfigurationSection($keyboardCollapsed) { KeyboardDeviceConfigurationView(hardware: $viewModel.config.hardware) } header: { SummaryHeader( "Keyboard Device", systemImage: "keyboard", summary: viewModel.config.keyboardDeviceSummary ) } } @ViewBuilder private var network: some View { ConfigurationSection($networkCollapsed) { NetworkConfigurationView(hardware: $viewModel.config.hardware) } header: { SummaryHeader( "Network", systemImage: "network", summary: viewModel.config.networkSummary ) } } @ViewBuilder private var sound: some View { ConfigurationSection($soundCollapsed) { SoundConfigurationView(hardware: $viewModel.config.hardware) } header: { SummaryHeader( "Sound", systemImage: viewModel.config.hardware.soundDevices.isEmpty ? "speaker.slash" : "speaker.3", summary: viewModel.config.soundSummary ) } } @ViewBuilder private var sharing: some View { ConfigurationSection($sharingCollapsed) { SharingConfigurationView(configuration: $viewModel.config) } header: { SummaryHeader( "Sharing", systemImage: "folder", summary: viewModel.config.sharingSummary ) } } @ViewBuilder private var guestApp: some View { ConfigurationSection($guestAppCollapsed) { GuestAppConfigurationView(configuration: $viewModel.config) } header: { SummaryHeader( "Guest App", summary: viewModel.config.guestAppSummary ) { Image(.guestSymbol) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 15) } } } } // MARK: - Section Header private struct SummaryHeader: View { var title: String var summary: String? @ViewBuilder var icon: () -> Icon @ViewBuilder var accessory: () -> Accessory init(_ title: String, summary: String? = nil, @ViewBuilder icon: @escaping () -> Icon, @ViewBuilder accessory: @escaping () -> Accessory) { self.title = title self.summary = summary self.icon = icon self.accessory = accessory } var body: some View { HStack { HStack { icon().frame(width: 22) Text(title) } accessory() Spacer() if let summary { Text(summary) .font(.caption) .monospacedDigit() .foregroundColor(.secondary) } } } } private extension SummaryHeader where Icon == Image { init(_ title: String, image: Image, summary: String? = nil, @ViewBuilder accessory: @escaping () -> Accessory) { self.title = title self.summary = summary self.icon = { image } self.accessory = accessory } init(_ title: String, systemImage: String, summary: String? = nil, @ViewBuilder accessory: @escaping () -> Accessory) { self.init(title, image: Image(systemName: systemImage), summary: summary, accessory: accessory) } } private extension SummaryHeader where Icon == Image, Accessory == EmptyView { init(_ title: String, image: Image, summary: String? = nil) { self.init(title, image: image, summary: summary, accessory: { EmptyView() }) } init(_ title: String, systemImage: String, summary: String? = nil) { self.init(title, systemImage: systemImage, summary: summary, accessory: { EmptyView() }) } } private extension SummaryHeader where Accessory == EmptyView { init(_ title: String, summary: String? = nil, @ViewBuilder icon: @escaping () -> Icon) { self.init(title, summary: summary, icon: icon, accessory: { EmptyView() }) } } #if DEBUG struct VMConfigurationView_Previews: PreviewProvider { static var previews: some View { VMConfigurationSheet_Previews.previews } } #endif ================================================ FILE: VirtualUI/Source/VM Configuration/VMConfigurationViewModel.swift ================================================ // // VMConfigurationViewModel.swift // VirtualUI // // Created by Guilherme Rambo on 18/07/22. // import SwiftUI import VirtualCore public enum VMConfigurationContext: Int { case preInstall case postInstall } public final class VMConfigurationViewModel: ObservableObject { @Published var config: VBMacConfiguration { didSet { /// Reset display preset when changing display settings. /// This is so the warning goes away, if any warning is being shown. if config.hardware.displayDevices != oldValue.hardware.displayDevices, config.hardware.displayDevices.first != selectedDisplayPreset?.device { selectedDisplayPreset = nil } } } @Published public internal(set) var supportState: VBMacConfiguration.SupportState = .supported @Published public internal(set) var resolvedRestoreImage: ResolvedRestoreImage? { didSet { applyResolvedFeatureDefaultsIfNeeded() } } @Published var selectedDisplayPreset: VBDisplayPreset? @Published private(set) var vm: VBVirtualMachine public let context: VMConfigurationContext public init(_ vm: VBVirtualMachine, context: VMConfigurationContext = .postInstall, resolvedRestoreImage: ResolvedRestoreImage? = nil) { self.config = vm.configuration self.vm = vm self.context = context self.resolvedRestoreImage = resolvedRestoreImage applyResolvedFeatureDefaultsIfNeeded() Task { await validate() } } @discardableResult public func validate() async -> VBMacConfiguration.SupportState { let updatedState = await config.validate(for: vm, skipVirtualizationConfig: context == .preInstall) await MainActor.run { supportState = updatedState } return updatedState } public func createImage(for device: VBStorageDevice) async throws { guard let image = device.managedImage else { throw Failure("Only managed disk images can be created.") } let settings = DiskImageGenerator.ImageSettings(for: image, in: vm) try await DiskImageGenerator.generateImage(with: settings) } public func updateBootStorageDevice(with image: VBManagedDiskImage) { guard let idx = config.hardware.storageDevices.firstIndex(where: { $0.isBootVolume }) else { fatalError("Missing boot device in VM configuration") } var device = config.hardware.storageDevices[idx] device.backing = .managedImage(image) config.hardware.addOrUpdate(device) } } // MARK: - Feature Defaults private extension VMConfigurationViewModel { func applyResolvedFeatureDefaultsIfNeeded() { guard context == .preInstall else { return } guard let resolvedRestoreImage else { return } var updated = config if resolvedRestoreImage.feature(id: CatalogFeatureID.guestApp)?.status.isUnsupported == true { updated.guestAdditionsEnabled = false } if resolvedRestoreImage.feature(id: CatalogFeatureID.trackpad)?.status.isUnsupported == true, updated.hardware.pointingDevice.kind == .trackpad { updated.hardware.pointingDevice.kind = .mouse } if resolvedRestoreImage.feature(id: CatalogFeatureID.macKeyboard)?.status.isUnsupported == true, updated.hardware.keyboardDevice.kind == .mac { updated.hardware.keyboardDevice.kind = .generic } if resolvedRestoreImage.feature(id: CatalogFeatureID.displayResize)?.status.isUnsupported == true { updated.hardware.displayDevices = updated.hardware.displayDevices.map { device in var updatedDevice = device updatedDevice.automaticallyReconfiguresDisplay = false return updatedDevice } } if resolvedRestoreImage.feature(id: CatalogFeatureID.rosettaSharing)?.status.isUnsupported == true { updated.rosettaSharingEnabled = false } if updated != config { config = updated } } } ================================================ FILE: VirtualUI/VirtualUI.h ================================================ // // VirtualUI.h // VirtualUI // // Created by Guilherme Rambo on 17/07/22. // #import //! Project version number for VirtualUI. FOUNDATION_EXPORT double VirtualUIVersionNumber; //! Project version string for VirtualUI. FOUNDATION_EXPORT const unsigned char VirtualUIVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import #import #import ================================================ FILE: VirtualWormhole/Source/Definitions/VirtualWormholeConstants.swift ================================================ // // VirtualWormholeConstants.swift // VirtualWormholeConstants // // Created by Guilherme Rambo on 02/06/22. // import Foundation import OSLog struct VirtualWormholeConstants { static let subsystemName = "codes.rambo.VirtualWormhole" static let verboseLoggingEnabled: Bool = { #if DEBUG return UserDefaults.standard.bool(forKey: "WHVerbosePacketLogging") #else return false #endif }() static let payloadPropagationEnabled: Bool = { !UserDefaults.standard.bool(forKey: "WHDisablePayloadPropagation") }() static let connectionTimeoutInNanoseconds: UInt64 = 15 * NSEC_PER_SEC static let pingIntervalInSeconds: TimeInterval = 5.0 } extension Logger { init(for type: T.Type) { self.init(subsystem: VirtualWormholeConstants.subsystemName, category: String(describing: type)) } } private final class _VirtualWormholeStub { } public extension Bundle { static let virtualWormhole = Bundle(for: _VirtualWormholeStub.self) } ================================================ FILE: VirtualWormhole/Source/Services/Base/WormholeServiceClient.swift ================================================ // // WormholeServiceClient.swift // VirtualWormhole // // Created by Guilherme Rambo on 10/03/23. // import Foundation public protocol WormholeServiceClient { associatedtype ServiceType: WormholeService init(with service: ServiceType) } ================================================ FILE: VirtualWormhole/Source/Services/Base/WormholeServiceProtocol.swift ================================================ // // WormholeServiceProtocol.swift // VirtualWormhole // // Created by Guilherme Rambo on 02/06/22. // import Foundation import Virtualization public protocol WormholeMultiplexer: AnyObject { var side: WHConnectionSide { get } func send(_ payload: T, to peerID: WHPeerID?) async func stream(for payloadType: T.Type) -> AsyncThrowingStream<(senderID: WHPeerID, payload: T), Error> } public protocol WormholeService: AnyObject { static var id: String { get } init(with connection: WormholeMultiplexer) func activate() } ================================================ FILE: VirtualWormhole/Source/Services/DarwinNotifications/SystemNotification.swift ================================================ // // SystemNotification.swift // VirtualWormhole // // Created by Guilherme Rambo on 08/03/23. // import Foundation import notify /// Swift wrapper for the `notify_register_dispatch()` API. public final class SystemNotification { public struct LibNotifyError: LocalizedError { public var errorDescription: String? init(code: UInt32) { let name: String switch Int32(code) { case NOTIFY_STATUS_OK: name = "NOTIFY_STATUS_OK" case NOTIFY_STATUS_INVALID_NAME: name = "NOTIFY_STATUS_INVALID_NAME" case NOTIFY_STATUS_INVALID_TOKEN: name = "NOTIFY_STATUS_INVALID_TOKEN" case NOTIFY_STATUS_INVALID_PORT: name = "NOTIFY_STATUS_INVALID_PORT" case NOTIFY_STATUS_INVALID_FILE: name = "NOTIFY_STATUS_INVALID_FILE" case NOTIFY_STATUS_INVALID_SIGNAL: name = "NOTIFY_STATUS_INVALID_SIGNAL" case NOTIFY_STATUS_INVALID_REQUEST: name = "NOTIFY_STATUS_INVALID_REQUEST" case NOTIFY_STATUS_NOT_AUTHORIZED: name = "NOTIFY_STATUS_NOT_AUTHORIZED" case NOTIFY_STATUS_OPT_DISABLE: name = "NOTIFY_STATUS_OPT_DISABLE" case NOTIFY_STATUS_SERVER_NOT_FOUND: name = "NOTIFY_STATUS_SERVER_NOT_FOUND" case NOTIFY_STATUS_NULL_INPUT: name = "NOTIFY_STATUS_NULL_INPUT" default: name = "unknown" } self.errorDescription = "Notify error \(code) (\(name))" } } public let name: String private let queue: DispatchQueue private let callback: (SystemNotification) -> Void public private(set) var notificationToken: Int32 = 0 private var activated = false public init(with name: String, queue: DispatchQueue = .main, callback: @escaping (SystemNotification) -> Void) { self.name = name self.queue = queue self.callback = callback } public init(with name: String, queue: DispatchQueue = .main, callback: @escaping () -> Void) { self.name = name self.queue = queue self.callback = { _ in callback() } } public func activate() throws { guard !activated else { return } let status = notify_register_dispatch( name, ¬ificationToken, queue, notificationReceived ) guard status == NOTIFY_STATUS_OK else { throw LibNotifyError(code: status) } activated = true } public func invalidate() { guard activated else { return } notify_cancel(notificationToken) notificationToken = 0 activated = false } deinit { invalidate() } private func notificationReceived(_ token: Int32) { callback(self) } func getState() throws -> UInt64 { var state: UInt64 = 0 let status = notify_get_state(notificationToken, &state) guard status == NOTIFY_STATUS_OK else { throw LibNotifyError(code: status) } return state } } ================================================ FILE: VirtualWormhole/Source/Services/DarwinNotifications/WHDarwinNotificationsService.swift ================================================ // // WHDarwinNotificationsService.swift // VirtualWormhole // // Created by Guilherme Rambo on 08/03/23. // import Cocoa import OSLog import Combine enum DarwinNotificationMessage: WHPayload { static let resendOnReconnect = true case post(String) case subscribe(String) } final class WHDarwinNotificationsService: WormholeService { static let id = "darwinNotifications" private lazy var logger = Logger(for: Self.self) private let peerPostedNotificationSubject = PassthroughSubject<(name: String, peerID: WHPeerID), Never>() var onPeerNotificationReceived: AnyPublisher<(name: String, peerID: WHPeerID), Never> { peerPostedNotificationSubject.eraseToAnyPublisher() } var connection: WormholeMultiplexer init(with connection: WormholeMultiplexer) { self.connection = connection } func activate() { logger.debug(#function) Task { for try await message in connection.stream(for: DarwinNotificationMessage.self) { handle(message.payload, from: message.senderID) } } } private func handle(_ message: DarwinNotificationMessage, from peerID: WHPeerID) { logger.debug("Handle message: \(String(describing: message))") switch message { case .post(let name): peerPostedNotificationSubject.send((name, peerID)) case .subscribe(let name): createSubscription(for: name, peerID: peerID) } } private var subscriptions = [SystemNotification]() private func createSubscription(for name: String, peerID: WHPeerID) { do { let note = SystemNotification(with: name) { [weak self] in guard let self = self else { return } self.sendPostMessage(with: name, to: peerID) } subscriptions.append(note) DistributedNotificationCenter.default().addObserver(forName: .init(name), object: nil, queue: nil) { [weak self] _ in guard let self = self else { return } self.sendPostMessage(with: name, to: peerID) } try note.activate() } catch { logger.error("Error creating notification subscription for \"\(name)\": \(error, privacy: .public)") } } private func sendPostMessage(with name: String, to peerID: WHPeerID) { #if DEBUG logger.debug("Posting \(name, privacy: .public) to \(peerID, privacy: .public)") #endif Task { await connection.send(DarwinNotificationMessage.post(name), to: peerID) } } } ================================================ FILE: VirtualWormhole/Source/Services/DefaultsImport/Implementation/DefaultsDomain+ExportImport.swift ================================================ // // DefaultsDomain+ExportImport.swift // VirtualWormhole // // Created by Guilherme Rambo on 09/03/23. // import Cocoa import OSLog public extension DefaultsDomainDescriptor { func exportDefaults(to url: URL) async throws { try await runDefaults("export", domainName: id, plistPath: url.path) try postProcessPlist(at: url) } func importDefaults(from url: URL) async throws { try await runDefaults("import", domainName: id, plistPath: url.path) try await performRestartIfNeeded() } private func runDefaults(_ verb: String, domainName: String, plistPath: String) async throws { let proc = Process() proc.executableURL = URL(fileURLWithPath: "/usr/bin/defaults") proc.arguments = [ verb, domainName, plistPath ] try proc.checkRun() } func performRestartIfNeeded() async throws { guard let restart else { return } guard target.isRunning else { return } if restart.needsConfirmation { let shouldRestart = await MainActor.run { let alert = NSAlert() alert.messageText = "Restart \(target.name)?" alert.informativeText = "To apply the new settings, \(target.name) must be restarted. Would you like to restart it now?" alert.addButton(withTitle: "Restart Now") alert.addButton(withTitle: "Later") return alert.runModal() == .alertFirstButtonReturn } guard shouldRestart else { return } } let proc = Process() proc.executableURL = URL(fileURLWithPath: "/bin/sh") proc.arguments = [ "-c", restart.command ] try proc.checkRun() guard restart.shouldRelaunch, let url = target.bundleURL else { return } try await NSWorkspace.shared.openApplication(at: url, configuration: .init()) } private func postProcessPlist(at url: URL) throws { guard !ignoredKeyPaths.isEmpty else { return } let data = try Data(contentsOf: url) let plist = try PropertyListSerialization.propertyList(from: data, options: .mutableContainersAndLeaves, format: nil) guard let dict = plist as? NSMutableDictionary else { throw CocoaError(.coderReadCorrupt, userInfo: [NSLocalizedDescriptionKey: "The exported defaults domain is not a valid property list."]) } for key in ignoredKeyPaths { dict.setValue(nil, forKeyPath: key) } let updatedPlist = try PropertyListSerialization.data(fromPropertyList: dict, format: .xml, options: 0) try updatedPlist.write(to: url) } } extension Pipe { func readString() -> String? { guard let data = try? fileHandleForReading.readToEnd() else { return nil } guard !data.isEmpty else { return nil } return String(decoding: data, as: UTF8.self) } } extension Process { private static let logger = Logger(subsystem: VirtualWormholeConstants.subsystemName, category: "Process") @discardableResult func checkRun(expectedStatus: Int32 = 0) throws -> Data? { let errPipe = Pipe() let outPipe = Pipe() standardError = errPipe standardOutput = outPipe try run() waitUntilExit() let errStr = errPipe.readString() guard terminationStatus == expectedStatus else { var info: [String: Any] = [ NSLocalizedDescriptionKey: "Command failed with exit code \(terminationStatus)" ] if let errStr { Self.logger.error("Command \(self.executableURL?.lastPathComponent ?? "", privacy: .public) failed with exit code \(self.terminationStatus, privacy: .public): \(errStr, privacy: .public)") info[NSLocalizedFailureReasonErrorKey] = errStr } throw CocoaError(.coderReadCorrupt, userInfo: info) } return try? outPipe.fileHandleForReading.readToEnd() } } ================================================ FILE: VirtualWormhole/Source/Services/DefaultsImport/Implementation/DefaultsDomainDescriptor.swift ================================================ // // DefaultsDomainDescriptor.swift // VirtualWormhole // // Created by Guilherme Rambo on 09/03/23. // import Cocoa import UniformTypeIdentifiers public struct DefaultsDomainDescriptor: Identifiable, Codable { public struct Target: Identifiable, Codable { public var id: String { bundleIdentifier } public var bundleIdentifier: String public var name: String public var isSystemService: Bool } public struct Restart: Codable { public var command: String public var needsConfirmation = true public var shouldRelaunch = true } public var id: Target.ID { target.id } public var target: Target public var ignoredKeyPaths: [String] = [] public var restart: Restart? } public extension DefaultsDomainDescriptor.Target { var isRunning: Bool { /// System services are not included in `NSRunningApplication.runningApplications`, /// so just assume they're always running (which will be the case most of the time). guard !isSystemService else { return true } return NSRunningApplication.runningApplications(withBundleIdentifier: bundleIdentifier).contains(where: { !$0.isTerminated }) } var bundleURL: URL? { NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) } func iconImage(with size: CGSize = CGSize(width: 128, height: 128)) -> NSImage { func getIcon() -> NSImage { guard let url = bundleURL else { return NSWorkspace.shared.icon(for: .application) } return NSWorkspace.shared.icon(forFile: url.path) } let image = getIcon() image.size = size return image } } ================================================ FILE: VirtualWormhole/Source/Services/DefaultsImport/Implementation/DefaultsImportController.swift ================================================ // // DefaultsImportController.swift // VirtualWormhole // // Created by Guilherme Rambo on 09/03/23. // import Foundation import OSLog import Combine public typealias DefaultsDomainCollection = [DefaultsDomainDescriptor.ID: DefaultsDomainDescriptor] public final class DefaultsImportController: ObservableObject { private lazy var logger = Logger(subsystem: VirtualWormholeConstants.subsystemName, category: String(describing: Self.self)) @Published public private(set) var sortedDomains = [DefaultsDomainDescriptor]() @Published public private(set) var descriptors = DefaultsDomainCollection() private lazy var cancellables = Set() public init() { $descriptors .map { $0.values.sorted(by: { $0.target.name.localizedStandardCompare($1.target.name) == .orderedAscending }) } .assign(to: &$sortedDomains) loadDomains() } private func loadDomains() { do { guard let url = Bundle.virtualWormhole.url(forResource: "DefaultsDomains", withExtension: "plist") else { throw CocoaError(.fileNoSuchFile, userInfo: [NSLocalizedDescriptionKey: "DefaultsDomains.plist missing from VirtualWormhole bundle"]) } let data = try Data(contentsOf: url) let loadedDescriptors = try PropertyListDecoder().decode(DefaultsDomainCollection.self, from: data) self.descriptors = loadedDescriptors } catch { logger.fault("Failed to load descriptors: \(error, privacy: .public)") assertionFailure("Failed to load descriptors: \(error)") } } } ================================================ FILE: VirtualWormhole/Source/Services/DefaultsImport/Resources/DefaultsDomains.plist ================================================ com.apple.Terminal target bundleIdentifier com.apple.Terminal name Terminal isSystemService ignoredKeyPaths restart command killall Terminal && sleep 1 needsConfirmation shouldRelaunch com.apple.Dock target bundleIdentifier com.apple.Dock name Dock isSystemService ignoredKeyPaths persistent-apps persistent-others recent-apps restart command killall Dock needsConfirmation shouldRelaunch com.apple.Finder target bundleIdentifier com.apple.Finder name Finder isSystemService ignoredKeyPaths NSNavLastRootDirectory RecentMoveAndCopyDestinations NewWindowTargetPath FXConnectToLastURL GoToField GoToFieldHistory restart command killall Finder needsConfirmation shouldRelaunch ================================================ FILE: VirtualWormhole/Source/Services/DefaultsImport/WHDefaultsImportClient.swift ================================================ // // WHDefaultsImportClient.swift // VirtualWormhole // // Created by Guilherme Rambo on 10/03/23. // import Foundation import OSLog public final class WHDefaultsImportClient: WormholeServiceClient { private lazy var logger = Logger(subsystem: VirtualWormholeConstants.subsystemName, category: String(describing: Self.self)) public typealias ServiceType = WHDefaultsImportService let service: WHDefaultsImportService public init(with service: WHDefaultsImportService) { self.service = service } public func importDomain(with id: DefaultsDomainDescriptor.ID) async throws { logger.debug("Requesting export for \(id, privacy: .public)") let response = Task { let stream = service.onDomainResponseReceived.filter({ $0.domainID == id }).values for await response in stream { switch response { case .failure(_, let message): logger.error("Export request for \(id, privacy: .public) resolved with error: \(message, privacy: .public)") throw CocoaError(.coderInvalidValue, userInfo: [NSLocalizedDescriptionKey: message]) case .success(let id, let data): logger.debug("Export request for \(id, privacy: .public) resolved successfully") try await performImport(for: id, with: data) default: continue } break } } await service.sendExportRequest(for: id) logger.debug("Export request for \(id, privacy: .public) sent, waiting for response") try await response.value } private func performImport(for domainID: String, with data: Data) async throws { let domain = try service.fetchDescriptor(for: domainID) let tempURL = service.temporaryURL(for: domainID) try data.write(to: tempURL) try await domain.importDefaults(from: tempURL) try? FileManager.default.removeItem(at: tempURL) } } ================================================ FILE: VirtualWormhole/Source/Services/DefaultsImport/WHDefaultsImportService.swift ================================================ // // WHDefaultsImportService.swift // VirtualWormhole // // Created by Guilherme Rambo on 09/03/23. // import Cocoa import OSLog import Combine enum DefaultsImportMessage: WHPayload { /// Guest requesting domain export from host. case request(domainID: String) /// Host responding to guest request with domain ID and associated plist. case success(domainID: String, plist: Data) /// Host responding to guest request with domain ID and error message. case failure(domainID: String, error: String) } extension DefaultsImportMessage { var domainID: String { switch self { case .request(let domainID), .success(let domainID, _), .failure(let domainID, _): return domainID } } } public final class WHDefaultsImportService: WormholeService { public static let id = "defaultsImport" private lazy var logger = Logger(for: Self.self) var connection: WormholeMultiplexer public init(with connection: WormholeMultiplexer) { self.connection = connection } public func activate() { logger.debug(#function) Task { for try await message in connection.stream(for: DefaultsImportMessage.self) { await handle(message.payload, from: message.senderID) } } } private lazy var controller = DefaultsImportController() let onDomainResponseReceived = PassthroughSubject() func sendExportRequest(for domainID: String) async { assert(connection.side == .guest, "Requesting defaults export is only possible from guest to host") await connection.send(DefaultsImportMessage.request(domainID: domainID), to: nil) } private func handle(_ message: DefaultsImportMessage, from peerID: WHPeerID) async { logger.debug("Handle message: \(String(describing: message))") switch message { case .request(let domainID): await handleDomainRequest(for: domainID, from: peerID) case .success, .failure: await MainActor.run { onDomainResponseReceived.send(message) } } } private func handleDomainRequest(for domainID: String, from peerID: WHPeerID) async { do { let data = try await fetchDomainData(for: domainID) await connection.send(DefaultsImportMessage.success(domainID: domainID, plist: data), to: peerID) } catch { logger.error("Export failed: \(error, privacy: .public)") await connection.send(DefaultsImportMessage.failure(domainID: domainID, error: error.localizedDescription), to: peerID) } } func fetchDescriptor(for domainID: String) throws -> DefaultsDomainDescriptor { guard let domain = controller.descriptors[domainID] else { throw CocoaError(.fileNoSuchFile, userInfo: [NSLocalizedDescriptionKey: "Domain \(domainID) not found."]) } return domain } func temporaryURL(for domainID: String) -> URL { URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent("VirtualBuddyDefaultsExport-\(domainID)-\(Int(Date.now.timeIntervalSinceReferenceDate))") .appendingPathExtension("plist") } private func fetchDomainData(for domainID: String) async throws -> Data { let domain = try fetchDescriptor(for: domainID) let tempURL = temporaryURL(for: domainID) try await domain.exportDefaults(to: tempURL) let result = try Data(contentsOf: tempURL) try? FileManager.default.removeItem(at: tempURL) return result } } ================================================ FILE: VirtualWormhole/Source/Services/DesktopPicture/CGImage+FullyTransparent.swift ================================================ // // CGImage+FullyTransparent.swift // VirtualBuddy // // Created by Guilherme Rambo on 19/06/25. // import Cocoa import CoreImage import CoreImage.CIFilterBuiltins private let transparentCheckContext = CIContext(options: [.workingColorSpace: NSNull()]) extension CGImage { /// Returns `true` if the image is fully transparent or has width and height equal to zero. func isFullyTransparent() -> Bool { /// Zero size image is considered fully transparent. guard width > 0, height > 0 else { return true } /// If image has no alpha, then it can't be fully transparent. switch alphaInfo { case .none, .noneSkipFirst, .noneSkipLast: return false default: break } let ciImage = CIImage(cgImage: self) let filter = CIFilter.areaMaximumAlpha() filter.inputImage = ciImage filter.extent = ciImage.extent guard let reduced = filter.outputImage else { assertionFailure("Error reducing image area maximum alpha") return false } var alpha: UInt32 = 0 transparentCheckContext .render( reduced, toBitmap: &alpha, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .A8, colorSpace: nil ) return alpha == 0 } static func load(from url: URL) throws -> CGImage { guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { throw CocoaError(.fileReadNoSuchFile) } guard let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else { throw CocoaError(.coderInvalidValue) } return cgImage } static func load(from data: Data) throws -> CGImage { guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { throw CocoaError(.coderValueNotFound) } guard let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else { throw CocoaError(.coderInvalidValue) } return cgImage } } ================================================ FILE: VirtualWormhole/Source/Services/DesktopPicture/NSImage+DesktopPicture.h ================================================ #import NS_ASSUME_NONNULL_BEGIN @interface NSImage (DesktopPicture) @property (nonatomic, readonly, class) NSImage *_Nullable desktopPicture NS_SWIFT_UI_ACTOR; + (void)desktopPictureForScreen:(NSScreen *)screen completion:(void(^)(NSImage *_Nullable desktopPicture))completion; @end NS_ASSUME_NONNULL_END ================================================ FILE: VirtualWormhole/Source/Services/DesktopPicture/NSImage+DesktopPicture.m ================================================ #import "NSImage+DesktopPicture.h" @interface NSScreen (DisplayInfo) @property (readonly) NSNumber *displayID; @end @implementation NSImage (DesktopPicture) + (dispatch_queue_t)_vb_desktopPictureQueue { static dispatch_once_t onceToken; static dispatch_queue_t _queue; dispatch_once(&onceToken, ^{ _queue = dispatch_queue_create("DesktopPicture", dispatch_queue_attr_make_with_qos_class(NULL, QOS_CLASS_USER_INITIATED, 0)); }); return _queue; } + (NSImage *)desktopPicture { return [self _vb_desktopPictureForScreen:[NSScreen mainScreen]]; } + (void)desktopPictureForScreen:(NSScreen *)screen completion:(void(^)(NSImage *_Nullable desktopPicture))completion { dispatch_async([self _vb_desktopPictureQueue], ^{ NSImage *picture = [self _vb_desktopPictureForScreen:screen]; dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(picture); }); }); } + (NSImage *)_vb_desktopPictureForScreen:(NSScreen *)screen { if (!screen) return nil; CFArrayRef windowList = CGWindowListCreate(kCGWindowListOptionOnScreenOnly, kCGNullWindowID); NSArray *descriptions = (__bridge id)CGWindowListCreateDescriptionFromArray(windowList); NSArray *> *wallpaperWindows = [descriptions filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"(%K CONTAINS %@ OR %K CONTAINS %@) AND %K == %@", kCGWindowName, @"Wallpaper", kCGWindowName, @"Desktop Picture", kCGWindowOwnerName, @"Dock"]]; CGDirectDisplayID displayID = screen.displayID.unsignedIntValue; CGRect displayBounds = CGDisplayBounds(displayID); NSDictionary *wallpaperWindow = nil; for (NSDictionary *windowDict in wallpaperWindows) { NSDictionary *boundsDict = windowDict[(__bridge id)kCGWindowBounds]; if (!boundsDict) continue; CGRect windowRect = CGRectZero; if (!CGRectMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)boundsDict, &windowRect)) continue; if (CGRectContainsPoint(displayBounds, windowRect.origin)) { wallpaperWindow = windowDict; break; } } if (!wallpaperWindow) return nil; NSNumber *windowNumber = [wallpaperWindow objectForKey:(__bridge id)kCGWindowNumber]; if (!windowNumber) return nil; CGImageRef cgImage = CGWindowListCreateImage(displayBounds, kCGWindowListOptionIncludingWindow, (CGWindowID)[windowNumber unsignedIntValue], kCGWindowImageDefault); if (!cgImage) return nil; return [[NSImage alloc] initWithCGImage:cgImage size:NSMakeSize(CGImageGetWidth(cgImage), CGImageGetHeight(cgImage))]; } @end @implementation NSScreen (DisplayInfo) - (NSNumber*)displayID { return [[self deviceDescription] valueForKey:@"NSScreenNumber"]; } @end ================================================ FILE: VirtualWormhole/Source/Services/DesktopPicture/WHDesktopPictureService.swift ================================================ // // WHDesktopPictureService.swift // VirtualBuddy // // Created by Guilherme Rambo on 18/06/25. // import Cocoa import OSLog import AVFoundation import Combine public struct DesktopPictureMessage: WHPayload { public internal(set) var type: String public internal(set) var content: Data public static let resendOnReconnect = true } final class WHDesktopPictureService: WormholeService { static let id = "desktopPicture" private lazy var logger = Logger(for: Self.self) var connection: WormholeMultiplexer init(with connection: WormholeMultiplexer) { self.connection = connection } static let imageProperties = [ kCGImageDestinationLossyCompressionQuality: 0.8, kCGImageDestinationImageMaxPixelSize: 512 ] as CFDictionary private let peerSentDesktopPictureSubject = PassthroughSubject<(message: DesktopPictureMessage, peerID: WHPeerID), Never>() var onPeerPeerDesktopPictureReceived: AnyPublisher<(message: DesktopPictureMessage, peerID: WHPeerID), Never> { peerSentDesktopPictureSubject.eraseToAnyPublisher() } func activate() { logger.debug(#function) Task { for try await message in connection.stream(for: DesktopPictureMessage.self) { logger.debug("Received desktop picture message with \(message.payload.content.count) bytes of image data.") peerSentDesktopPictureSubject.send((message.payload, message.senderID)) } } guard connection.side == .guest else { return } Task { try? await Task.sleep(for: .seconds(2)) await sendDesktopPicture() } } func sendDesktopPicture() async { guard let image = await MainActor.run(body: { NSImage.desktopPicture }) else { logger.error("Error getting desktop picture for main screen.") return } guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { logger.error("Error getting CGImage from desktop picture.") return } guard !cgImage.isFullyTransparent() else { logger.warning("Skipping send desktop picture because it generated a fully transparent image.") return } guard let cfData = CFDataCreateMutable(kCFAllocatorDefault, 0) else { logger.error("Failed to create CFMutableData") return } guard let destination = CGImageDestinationCreateWithData(cfData, AVFileType.heic.rawValue as CFString, 1, nil) else { logger.error("Failed to create CGImageDestination") return } CGImageDestinationAddImage(destination, cgImage, Self.imageProperties) CGImageDestinationFinalize(destination) let payload = DesktopPictureMessage(type: AVFileType.heic.rawValue, content: cfData as Data) logger.info("Sending payload with \(payload.content.count) bytes") await connection.send(payload, to: nil) } } extension NSImage: @retroactive @unchecked Sendable { } ================================================ FILE: VirtualWormhole/Source/Services/WHSharedClipboardService.swift ================================================ // // WHSharedClipboardService.swift // VirtualWormhole // // Created by Guilherme Rambo on 02/06/22. // import Cocoa import OSLog struct ClipboardData: Codable, Hashable { var type: NSPasteboard.PasteboardType.RawValue var value: Data } struct ClipboardMessage: WHPayload, Hashable { var timestamp: Date var data: [ClipboardData] static let propagateBetweenGuests = true } final class WHSharedClipboardService: WormholeService { static let id = "clipboard" private lazy var logger = Logger(for: Self.self) var connection: WormholeMultiplexer init(with connection: WormholeMultiplexer) { self.connection = connection } private var previousMessage: ClipboardMessage? func activate() { logger.debug(#function) Task { for try await message in connection.stream(for: ClipboardMessage.self) { handle(message.payload) } } startObservingClipboard() } private let pasteboard = NSPasteboard.general private func handle(_ message: ClipboardMessage) { guard !message.data.isEmpty, message.data != previousMessage?.data else { return } logger.debug("Handle clipboard message: \(String(describing: message))") previousMessage = message pasteboard.read(from: message.data) #if DEBUG logger.debug("⏱️ Clipboard message roundtrip time: \(String(format: "%.03f", Date.now.timeIntervalSince(message.timestamp)), privacy: .public)") #endif } private var clipboardTimer: Timer? private func startObservingClipboard() { clipboardTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in self?.updateIfNeeded() }) } private func updateIfNeeded() { let currentData = ClipboardData.current guard currentData != previousMessage?.data else { return } #if DEBUG logger.debug("Clipboard contents changed: \(String(describing: currentData), privacy: .public)") #endif let message = ClipboardMessage( timestamp: .now, data: currentData ) previousMessage = message Task { await connection.send(message, to: nil) } } } private extension ClipboardData { static let supportedTypes: [NSPasteboard.PasteboardType] = [ .string, .rtf, .rtfd, .pdf, .png, .tiff, ] static var current: [ClipboardData] { guard let availableTypes = NSPasteboard.general.types else { return [] } return supportedTypes.compactMap { type in /// PNG and TIFF data are often present at the same time. /// Ignore TIFF data and preserve only the PNG data when that's the case, /// since TIFF data is usually much larger and unused. if type == .tiff { guard !availableTypes.contains(.png) else { return nil } } guard let data = NSPasteboard.general.data(forType: type) else { return nil } return ClipboardData(type: type.rawValue, value: data) } } } private extension NSPasteboard { func read(from data: [ClipboardData]) { clearContents() for item in data { setData(item.value, forType: PasteboardType(rawValue: item.type)) } } } ================================================ FILE: VirtualWormhole/Source/WireProtocol/WHPayload.swift ================================================ // // WHPayload.swift // VirtualWormhole // // Created by Guilherme Rambo on 25/10/23. // import Foundation /// Protocol adopted by types that can be sent over the guest <> host connection. public protocol WHPayload: Codable, Sendable { /// When `true`, the payload will be sent again if connection gets interrupted and re-established. static var resendOnReconnect: Bool { get } /// When `true`, the host will distribute the payload to all booted guests /// upon receiving the payload from one of the guests. static var propagateBetweenGuests: Bool { get } } public extension WHPayload { static var resendOnReconnect: Bool { false } static var propagateBetweenGuests: Bool { false } } ================================================ FILE: VirtualWormhole/Source/WireProtocol/WHPing.swift ================================================ // // WHPing.swift // VirtualWormhole // // Created by Guilherme Rambo on 08/03/23. // import Foundation struct WHPing: WHPayload { var date = Date.now } struct WHPong: WHPayload { var date = Date.now } extension WormholePacket { var isPing: Bool { payloadType == String(describing: WHPing.self) } var isPong: Bool { payloadType == String(describing: WHPong.self) } } extension WormholePacket { static var ping: WormholePacket { get throws { try WormholePacket(WHPing()) } } static var pong: WormholePacket { get throws { try WormholePacket(WHPong()) } } } ================================================ FILE: VirtualWormhole/Source/WireProtocol/WormholePacket.swift ================================================ // // WormholePacket.swift // VirtualWormhole // // Created by Guilherme Rambo on 08/03/23. // import Foundation struct WormholePacket { var magic: UInt32 = Self.magicValue var payloadType: String var payloadLength: UInt64 var payload: Data } extension WormholePacket { static let magicValue: UInt32 = 0x0DF0FECA static let magicValueCompressed: UInt32 = 0x01F0FECA static let maxUncompressedPayloadSize = 1000000 static let compressionAlgorithm = NSData.CompressionAlgorithm.lzma /// The absolute minimum size an entire packet could be. /// Any packet that's not at least this size has something wrong with it. static let minimumSize: Int = { MemoryLayout.size // magic + 2 // payloadType // 1 byte for single character + null terminator + MemoryLayout.size // payloadLength + 1 // payload // at least 1 byte of payload data }() } // MARK: - Encoding extension WormholePacket { init(_ payload: T) throws { let data = try JSONEncoder.wormhole.encode(payload) let typeName = String(describing: type(of: payload)) self.init(payloadType: typeName, payloadLength: UInt64(data.count), payload: data) } func encoded() throws -> Data { var encodedMagic = magic var encodedPayloadLength = payloadLength var encodedPayload = payload if payload.count >= Self.maxUncompressedPayloadSize { encodedMagic = Self.magicValueCompressed let compressedPayload = try (payload as NSData).compressed(using: Self.compressionAlgorithm) encodedPayloadLength = UInt64(compressedPayload.count) encodedPayload = compressedPayload as Data } return Data(bytes: &encodedMagic, count: MemoryLayout.size) + Data(payloadType.utf8 + [0]) + Data(bytes: &encodedPayloadLength, count: MemoryLayout.size) + encodedPayload } } // MARK: - Decoding extension WormholePacket { static func decode(from data: Data) throws -> WormholePacket { guard data.count >= Self.minimumSize else { throw CocoaError(.coderInvalidValue, userInfo: [NSLocalizedDescriptionKey: "Packet data with length \(data.count) is smaller than the minimum packet length"]) } return try data.withUnsafeBytes { buffer in guard let pointer = buffer.baseAddress else { throw CocoaError(.coderReadCorrupt, userInfo: [NSLocalizedDescriptionKey: "Couldn't get buffer base address"]) } var byteOffset = 0 let magic = pointer.load(as: UInt32.self) byteOffset += MemoryLayout.size let strptr = pointer .advanced(by: byteOffset) .assumingMemoryBound(to: UInt8.self) let payloadType = String(cString: strptr) byteOffset += payloadType.count + 1 var payloadLength = pointer.loadUnaligned(fromByteOffset: byteOffset, as: UInt64.self) byteOffset += MemoryLayout.size guard UInt64(data.count) > payloadLength else { throw CocoaError(.coderReadCorrupt, userInfo: [NSLocalizedDescriptionKey: "Packet payload length \(payloadLength) is out of bounds"]) } let upperBound = Int(byteOffset)+Int(truncatingIfNeeded: payloadLength) guard data.count >= upperBound else { throw CocoaError(.coderReadCorrupt, userInfo: [NSLocalizedDescriptionKey: "Packet payload length \(payloadLength) is out of bounds"]) } var payload = Data(data[byteOffset.. AsyncThrowingStream { AsyncThrowingStream { continuation in Self.logger.debug("Activating stream") let task = Task { do { var buffer = Data(capacity: WormholePacket.minimumSize) for try await byte in bytes { guard !Task.isCancelled else { break } // Self.logger.debug("RECV: \(buffer.map({ String(format: "%02X", $0) }).joined())") buffer.append(byte) guard buffer.count >= WormholePacket.minimumSize else { continue } if let packet = try? WormholePacket.decode(from: buffer) { continuation.yield(packet) buffer = Data(capacity: WormholePacket.minimumSize) } } Self.logger.debug("Stream ended/cancelled") } catch { Self.logger.error("Stream failed: \(error, privacy: .public)") continuation.finish(throwing: error) } } continuation.onTermination = { @Sendable _ in task.cancel() } } } } extension JSONDecoder { static let wormhole = JSONDecoder() } extension JSONEncoder { static let wormhole = JSONEncoder() } ================================================ FILE: VirtualWormhole/Source/WormholeManager.swift ================================================ // // WormholeManager.swift // VirtualWormhole // // Created by Guilherme Rambo on 02/06/22. // import Foundation import Virtualization import OSLog @preconcurrency import Combine public typealias WHPeerID = String public extension WHPeerID { static let host = "Host" } public enum WHConnectionSide: Hashable, CustomStringConvertible { case host case guest public var description: String { switch self { case .host: return "Host" case .guest: return "Guest" } } } public final class WormholeManager: NSObject, ObservableObject, WormholeMultiplexer { /// Singleton manager used by the VirtualBuddy app to talk /// to VirtualBuddyGuest running in virtual machines. public static let sharedHost = WormholeManager(for: .host) /// Singleton manager used by the VirtualBuddyGuest app in a virtual machine /// to talk to VirtualBuddy running in the host. public static let sharedGuest = WormholeManager(for: .guest) @Published private(set) var peers = [WHPeerID: WormholeChannel]() @Published public private(set) var isConnected = false private lazy var logger = Logger(for: Self.self) let serviceTypes: [WormholeService.Type] = [ WHSharedClipboardService.self, WHDarwinNotificationsService.self, WHDefaultsImportService.self, WHDesktopPictureService.self ] var activeServices: [WormholeService] = [] public let side: WHConnectionSide public init(for side: WHConnectionSide) { self.side = side super.init() } public func makeClient(_ type: C.Type) throws -> C { guard let service = activeServices.compactMap({ $0 as? C.ServiceType }).first else { throw CocoaError(.coderInvalidValue, userInfo: [NSLocalizedDescriptionKey: "Service unavailable."]) } return C(with: service) } private var activated = false private lazy var cancellables = Set() public func activate() { guard !activated else { return } activated = true logger.debug("Activate side \(String(describing: self.side))") Task { do { try await activateGuestIfNeeded() } catch { logger.fault("Failed to register host peer: \(error, privacy: .public)") } } activeServices = serviceTypes .map { $0.init(with: self) } activeServices.forEach { $0.activate() } #if DEBUG $peers.removeDuplicates(by: { $0.keys != $1.keys }).sink { [weak self] currentPeers in guard let self = self else { return } self.logger.debug("Peers: \(currentPeers.keys.joined(separator: ", "), privacy: .public)") } .store(in: &cancellables) #endif Timer .publish(every: VirtualWormholeConstants.pingIntervalInSeconds, tolerance: VirtualWormholeConstants.pingIntervalInSeconds * 0.5, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in guard let self = self else { return } Task { await self.send(WHPing(), to: nil) } } .store(in: &cancellables) } private let packetSubject = PassthroughSubject<(peerID: WHPeerID, packet: WormholePacket), Never>() public func register(input: FileHandle, output: FileHandle, for peerID: WHPeerID) async { if let existing = peers[peerID] { await existing.invalidate() } let channel = await WormholeChannel( input: input, output: output, peerID: peerID ).onPacketReceived { [weak self] senderID, packet in guard let self = self else { return } self.packetSubject.send((senderID, packet)) } /// When running in guest mode, observe the channel's connection state and bind it to the manager's state. if self.side == .guest { await channel.$isConnected.removeDuplicates().receive(on: DispatchQueue.main).sink { [weak self] isConnected in guard let self = self else { return } self.logger.notice("Connection to host changed state (isConnected = \(isConnected, privacy: .public))") self.isConnected = isConnected }.store(in: &cancellables) } peers[peerID] = channel await channel.activate() } public func unregister(_ peerID: WHPeerID) async { guard let channel = peers[peerID] else { return } await channel.invalidate() peers[peerID] = nil } public func send(_ payload: T, to peerID: WHPeerID?) async { guard !peers.isEmpty else { return } if side == .guest { guard peerID == nil || peerID == .host else { logger.fault("Guest can only send messages to host!") assertionFailure("Guest can only send messages to host!") return } } do { let packet = try WormholePacket(payload) if let peerID { guard let channel = peers[peerID] else { logger.error("Couldn't find channel for peer \(peerID)") return } /// Message will be repeated if other side disconnects and reconnects. if T.resendOnReconnect { await channel.connected { do { /// Make sure there's a fresh packet every time the message is sent. let newPacket = try WormholePacket(payload) try await $0.send(newPacket) } catch { assertionFailure("Failed to send packet: \(error)") } } } else { try await channel.send(packet) } } else { for channel in peers.values { try await channel.send(packet) } } } catch { logger.fault("Failed to send packet: \(error, privacy: .public)") assertionFailure("Failed to send packet: \(error)") } } public func stream(for payloadType: T.Type) -> AsyncThrowingStream<(senderID: WHPeerID, payload: T), Error> { AsyncThrowingStream { [weak self] continuation in guard let self = self else { continuation.finish() return } let typeName = String(describing: payloadType) let cancellable = self.packetSubject .filter { $0.packet.payloadType == typeName } .sink { [weak self] peerID, packet in guard let self = self else { return } guard let decodedPayload = try? JSONDecoder().decode(payloadType, from: packet.payload) else { return } self.propagateIfNeeded(packet, type: payloadType, from: peerID) continuation.yield((peerID, decodedPayload)) } continuation.onTermination = { @Sendable _ in cancellable.cancel() } } } private func propagateIfNeeded(_ packet: WormholePacket, type: P.Type, from senderID: WHPeerID) { guard type.propagateBetweenGuests, VirtualWormholeConstants.payloadPropagationEnabled else { return } let propagationChannels = self.peers.filter({ $0.key != senderID }) Task { for (id, channel) in propagationChannels { do { if VirtualWormholeConstants.verboseLoggingEnabled { logger.debug("⬆️ PROPAGATE \(packet.payloadType, privacy: .public) from \(senderID, privacy: .public) to \(id, privacy: .public)") } try await channel.send(packet) } catch { logger.error("Packet propagation to \(id, privacy: .public) failed: \(error, privacy: .public)") } } } } // MARK: - Ping /// Waits for a connection with the given peer. private func wait(for peerID: WHPeerID) async { guard let channel = peers[peerID] else { logger.error("Can't wait for peer \(peerID) for which a channel doesn't exist") return } guard await channel.isConnected == false else { return } for await state in await channel.$isConnected.values { guard state else { continue } break } } /// Performs the specified asynchronous closure whenever the connection state for the peer changes /// from not connected to connected. Also runs the closure if peer is already connected at the time of calling. private func connected(to peerID: WHPeerID, perform block: @escaping (WormholeChannel) async -> Void) async { guard let channel = peers[peerID] else { logger.error("Can't wait for peer \(peerID) for which a channel doesn't exist") return } await channel.connected(perform: block) } // MARK: - Service Interfaces private func service(_ serviceType: T.Type) -> T? { activeServices.first(where: { type(of: $0).id == serviceType.id }) as? T } public func darwinNotifications(matching names: Set, from peerID: WHPeerID) async throws -> AsyncStream { try ensurePeerAvailable(peerID) guard let notificationService = service(WHDarwinNotificationsService.self) else { throw CocoaError(.coderValueNotFound, userInfo: [NSLocalizedDescriptionKey: "Darwin notifications service not available"]) } try Task.checkCancellation() for name in names { await send(DarwinNotificationMessage.subscribe(name), to: peerID) } var iterator = notificationService.onPeerNotificationReceived.values .filter { $0.peerID == peerID } .map(\.name) .makeAsyncIterator() return AsyncStream { await iterator.next() } } public func desktopPictureMessages(from peerID: WHPeerID) async throws -> AsyncStream { try ensurePeerAvailable(peerID) guard let desktopPictureService = service(WHDesktopPictureService.self) else { throw CocoaError(.coderValueNotFound, userInfo: [NSLocalizedDescriptionKey: "Desktop picture service not available"]) } try Task.checkCancellation() var iterator = desktopPictureService.onPeerPeerDesktopPictureReceived.values .filter { $0.peerID == peerID } .map(\.message) .makeAsyncIterator() return AsyncStream { await iterator.next() } } /// Can be called on the guest to force-send the current desktop picture. public func sendDesktopPicture() async { guard side == .guest else { return } do { guard let desktopPictureService = service(WHDesktopPictureService.self) else { throw CocoaError(.coderValueNotFound, userInfo: [NSLocalizedDescriptionKey: "Desktop picture service not available"]) } logger.debug("Sending desktop picture") await desktopPictureService.sendDesktopPicture() logger.debug("Finished sending desktop picture") } catch { logger.error("Send desktop picture failed - \(error, privacy: .public)") } } private func ensurePeerAvailable(_ peerID: WHPeerID) throws { guard peers[peerID] != nil else { throw CocoaError(.coderValueNotFound, userInfo: [NSLocalizedDescriptionKey: "Peer \(peerID) is not registered"]) } } // MARK: - Guest Mode private let ttyPath = "/dev/cu.virtio" private var hostOutputHandle: FileHandle { get throws { try FileHandle(forReadingFrom: URL(fileURLWithPath: ttyPath)) } } private var hostInputHandle: FileHandle { get throws { try FileHandle(forWritingTo: URL(fileURLWithPath: ttyPath)) } } private func activateGuestIfNeeded() async throws { guard side == .guest else { return } configureTTY() logger.debug("Running in guest mode, registering host peer") let input = try hostOutputHandle let output = try hostInputHandle await register(input: input, output: output, for: .host) } private func configureTTY() { do { let proc = Process() proc.executableURL = URL(fileURLWithPath: "/bin/stty") proc.arguments = [ "-f", ttyPath, "115200" ] let errPipe = Pipe() let outPipe = Pipe() proc.standardError = errPipe proc.standardOutput = outPipe try proc.run() proc.waitUntilExit() if let errData = try? errPipe.fileHandleForReading.readToEnd(), !errData.isEmpty { logger.debug("stty stdout: \(String(decoding: errData, as: UTF8.self), privacy: .public)") } if let outData = try? outPipe.fileHandleForReading.readToEnd(), !outData.isEmpty { logger.debug("stty stderr: \(String(decoding: outData, as: UTF8.self), privacy: .public)") } } catch { logger.error("stty error: \(error, privacy: .public)") } } } // MARK: - Channel Actor actor WormholeChannel: ObservableObject { let input: FileHandle let output: FileHandle let peerID: WHPeerID private let logger: Logger init(input: FileHandle, output: FileHandle, peerID: WHPeerID) { self.input = input self.output = output self.peerID = peerID self.logger = Logger(subsystem: VirtualWormholeConstants.subsystemName, category: "WormholeChannel-\(peerID)") } @Published private(set) var isConnected = false { didSet { guard isConnected != oldValue else { return } logger.debug("isConnected = \(self.isConnected, privacy: .public)") } } private let packetSubject = PassthroughSubject() private lazy var cancellables = Set() @discardableResult func onPacketReceived(perform block: @escaping (WHPeerID, WormholePacket) -> Void) -> Self { packetSubject.sink { [weak self] packet in guard let self = self else { return } block(self.peerID, packet) } .store(in: &cancellables) return self } private var activated = false func activate() { guard !activated else { return } activated = true logger.debug(#function) stream() } func invalidate() { guard activated else { return } activated = false logger.debug(#function) cancellables.removeAll() timeoutTask?.cancel() internalTasks.forEach { $0.cancel() } internalTasks.removeAll() } func send(_ packet: WormholePacket) async throws { let data = try packet.encoded() if VirtualWormholeConstants.verboseLoggingEnabled { if !packet.isPing, !packet.isPong { logger.debug("⬆️ SEND \(packet.payloadType, privacy: .public) (\(packet.payload.count) bytes)") logger.debug("⏫ \(data.map({ String(format: "%02X", $0) }).joined(), privacy: .public)") } } try output.write(contentsOf: data) } private var internalTasks = [Task]() private func stream() { logger.debug(#function) let streamingTask = Task { do { for try await packet in WormholePacket.stream(from: input.bytes) { if VirtualWormholeConstants.verboseLoggingEnabled { if !packet.isPing, !packet.isPong { logger.debug("⬇️ RECEIVE \(packet.payloadType, privacy: .public) (\(packet.payload.count) bytes)") logger.debug("⏬ \(packet.payload.map({ String(format: "%02X", $0) }).joined(), privacy: .public)") } } guard !Task.isCancelled else { break } guard !packet.isPing && !packet.isPong else { await handlePingPong(packet) continue } packetSubject.send(packet) } logger.debug("⬇️ Packet streaming cancelled") } catch { logger.error("⬇️ Serial read failure: \(error, privacy: .public)") try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) guard !Task.isCancelled else { return } stream() } } internalTasks.append(streamingTask) } private var timeoutTask: Task? private func handlePingPong(_ packet: WormholePacket) async { self.isConnected = true if packet.isPing { if VirtualWormholeConstants.verboseLoggingEnabled { logger.debug("🏓 Received ping") } do { try await send(.pong) } catch { logger.error("🏓 Pong send failure: \(error, privacy: .public)") } } else { if VirtualWormholeConstants.verboseLoggingEnabled { logger.debug("🏓 Received pong") } } timeoutTask?.cancel() timeoutTask = Task { try? await Task.sleep(nanoseconds: VirtualWormholeConstants.connectionTimeoutInNanoseconds) guard !Task.isCancelled else { return } logger.warning("🏓 Connection timed out") self.isConnected = false } } func connected(perform block: @escaping (WormholeChannel) async -> Void) { let task = Task { [weak self] in guard let self = self else { return } for await state in await self.$isConnected.removeDuplicates().values { if state { await block(self) } } } internalTasks.append(task) } } ================================================ FILE: VirtualWormhole/VirtualWormhole.h ================================================ // // VirtualWormhole.h // VirtualWormhole // // Created by Guilherme Rambo on 02/06/22. // #import //! Project version number for VirtualWormhole. FOUNDATION_EXPORT double VirtualWormholeVersionNumber; //! Project version string for VirtualWormhole. FOUNDATION_EXPORT const unsigned char VirtualWormholeVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import #import ================================================ FILE: VirtualWormholeTests/WormholePacketTests.swift ================================================ // // VirtualWormholeTests.swift // VirtualWormholeTests // // Created by Guilherme Rambo on 08/03/23. // import XCTest @testable import VirtualWormhole final class WormholePacketTests: XCTestCase { func testPacketEncodingWithTestPayload() throws { let payload = TestPayload() let packet = try WormholePacket(payload) let data = try packet.encoded() XCTAssertEqual(data.hexDump, "CAFEF00D546573745061796C6F6164003A000000000000007B226D657373616765223A2248656C6C6F2C20576F726C6421222C226E756D626572223A34322C2264617461223A227172764D3365375C2F227D") } func testPacketDecodingWithTestPayload() throws { let payload = TestPayload() let packet = try WormholePacket(payload) let data = try packet.encoded() let decodedPacket = try WormholePacket.decode(from: data) XCTAssertEqual(decodedPacket.magic, packet.magic) XCTAssertEqual(decodedPacket.payloadType, packet.payloadType) XCTAssertEqual(decodedPacket.payloadLength, packet.payloadLength) XCTAssertEqual(decodedPacket.payload.count, packet.payload.count) XCTAssertEqual(decodedPacket.payload, packet.payload) } func testPacketDecodingRespectsLength() throws { let payload = TestPayload() var packet = try WormholePacket(payload) packet.payloadLength = 1 let data = try packet.encoded() let decodedPacket = try WormholePacket.decode(from: data) XCTAssertEqual(Int(packet.payloadLength), decodedPacket.payload.count) } func testPacketStreaming() async throws { let handle = FileHandle.testStream var packets = [WormholePacket]() for try await packet in WormholePacket.stream(from: handle.bytes) { packets.append(packet) guard packets.count < 6 else { break } } XCTAssertEqual(packets.count, 6) for packet in packets { XCTAssertEqual(packet.magic, 0x0DF0FECA) XCTAssertEqual(packet.payloadType, "TestPayload") XCTAssertEqual(packet.payloadLength, 58) XCTAssertEqual(packet.payload.count, 58) XCTAssertEqual(packet.payload, Data("{\"message\":\"Hello, World!\",\"number\":42,\"data\":\"qrvM3e7\\/\"}".utf8)) } } func testCompressedPacketEncodeDecode() async throws { let payload = TestPayload(data: .empty(count: WormholePacket.maxUncompressedPayloadSize + 1)) let packet = try WormholePacket(payload) let data = try packet.encoded() let decodedPacket = try WormholePacket.decode(from: data) XCTAssertEqual(decodedPacket.magic, WormholePacket.magicValueCompressed) XCTAssertEqual(decodedPacket.payload, packet.payload) } } struct TestPayload: Codable { var message = "Hello, World!" var number = 42 var data = Data([0xAA,0xBB,0xCC,0xDD,0xEE,0xFF]) } extension Data { var hexDump: String { map { String(format: "%02X", $0) }.joined() } static func empty(count: Int) -> Data { let bytes = [UInt8](repeating: 0, count: count) return Data(bytes) } } extension FileHandle { static var testStream: FileHandle { guard let url = Bundle(for: WormholePacketTests.self).url(forResource: "TestStream", withExtension: "bin") else { fatalError("Missing TestStream.bin in WormholeTests bundle!") } return try! FileHandle(forReadingFrom: url) } } ================================================ FILE: data/ipsws_v1.json ================================================ { "apiVersion": 1, "channels": [ { "id": "regular", "name": "Release", "note": "Public, stable releases.", "icon": "checkmark.seal" }, { "id": "devbeta", "name": "Developer Beta", "note": "Betas meant for developer testing.", "icon": "wrench.and.screwdriver", "authentication": { "name": "Developer Portal", "url": "https://developer.apple.com/download", "note": "Perform the authentication using the web view that will be opened. Your credentials will be sent directly to Apple and will not be stored locally or on any servers." } }, { "id": "pubbeta", "name": "Public Beta", "note": "Betas meant for customer testing.", "icon": "sun.and.horizon" }, { "id": "seed", "name": "Apple Seed", "note": "Private betas for select customers.", "icon": "tree", "authentication": { "name": "AppleSeed Portal", "url": "https://appleseed.apple.com/", "note": "Perform the authentication using the web view that will be opened. Your credentials will be sent directly to Apple and will not be stored locally or on any servers." } } ], "groups": [ { "id": "sequoia", "name": "macOS Sequoia", "majorVersion": "15.0", "minHostVersion": "14.0" }, { "id": "sonoma", "name": "macOS Sonoma", "majorVersion": "14.0", "minHostVersion": "13.0" }, { "id": "ventura", "name": "macOS Ventura", "majorVersion": "13.0", "minHostVersion": "12.0" }, { "id": "monterey", "name": "macOS Monterey", "majorVersion": "12.0", "minHostVersion": "12.3" } ], "restoreImages": [ { "group": "sequoia", "name": "macOS 15.5", "build": "24F74", "url": "https://updates.cdn-apple.com/2025SpringFCS/fullrestores/082-44534/CE6C1054-99A3-4F67-A823-3EE9E6510CDE/UniversalMac_15.5_24F74_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.5 Developer Beta 4", "build": "24F5068b", "url": "https://updates.cdn-apple.com/2025SpringSeed/fullrestores/082-42002/44CF75E3-7765-4356-A88C-5EEEC4471CA4/UniversalMac_15.5_24F5068b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.5 Developer Beta 3", "build": "24F5053j", "url": "https://updates.cdn-apple.com/2025SpringSeed/fullrestores/072-92980/8E6DFD3B-8438-423F-8FFE-A95234775E84/UniversalMac_15.5_24F5053j_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.5 Developer Beta 2", "build": "24F5053f", "url": "https://updates.cdn-apple.com/2025SpringSeed/fullrestores/082-24561/B199D1B2-296D-43E5-A29A-5FC27796F36D/UniversalMac_15.5_24F5053f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.5 Developer Beta 1", "build": "24F5042g", "url": "https://updates.cdn-apple.com/2025SpringSeed/fullrestores/072-94893/B85463BB-3ED6-4A5A-BE78-92AD86B531D7/UniversalMac_15.5_24F5042g_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.4.1", "build": "24E263", "url": "https://updates.cdn-apple.com/2025SpringFCS/fullrestores/082-23340/61923341-EE73-4C6E-BB3E-DAB3069548BF/UniversalMac_15.4.1_24E263_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.4", "build": "24E248", "url": "https://updates.cdn-apple.com/2025SpringFCS/fullrestores/082-16517/AACDDC33-9683-4431-98AF-F04EF7C15EE3/UniversalMac_15.4_24E248_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.4 RC 2", "build": "24E247", "url": "https://updates.cdn-apple.com/2025SpringFCS/fullrestores/082-13483/7D547A66-A1A6-48C1-A8E1-FE78559BDD34/UniversalMac_15.4_24E247_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.4 RC", "build": "24E246", "url": "https://updates.cdn-apple.com/2025SpringFCS/fullrestores/082-13013/C3CAC9AF-B139-4924-BE1B-10ED6EF931A4/UniversalMac_15.4_24E246_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.4 Developer Beta 4", "build": "24E5238a", "url": "https://updates.cdn-apple.com/2025WinterSeed/fullrestores/082-08323/FD7D7539-514F-4F2E-AD3B-4D6D0D248706/UniversalMac_15.4_24E5238a_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.4 Developer Beta 3", "build": "24E5228e", "url": "https://updates.cdn-apple.com/2025WinterSeed/fullrestores/072-96972/D76A7AE6-3A35-4295-B102-E5FC5C84B4DC/UniversalMac_15.4_24E5228e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.4 Developer Beta 2", "build": "24E5222f", "url": "https://updates.cdn-apple.com/2025WinterSeed/fullrestores/072-90273/76B9EE73-0836-47FB-8CBA-E3E902ACD21A/UniversalMac_15.4_24E5222f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.4 Developer Beta 1", "build": "24E5206s", "url": "https://updates.cdn-apple.com/2025WinterSeed/fullrestores/072-92205/1FCDCB29-B8A3-4461-A98A-270F1D1121C8/UniversalMac_15.4_24E5206s_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.3.2", "build": "24D81", "url": "https://updates.cdn-apple.com/2025WinterFCS/fullrestores/082-01504/828B8EF9-8134-49D5-B24A-0BA504FC5ECC/UniversalMac_15.3.2_24D81_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.3.1", "build": "24D70", "url": "https://updates.cdn-apple.com/2025WinterFCS/fullrestores/072-70618/42F1A8CC-7E07-4329-958A-757FF600C303/UniversalMac_15.3.1_24D70_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.3", "build": "24D60", "url": "https://updates.cdn-apple.com/2025WinterFCS/fullrestores/072-08269/7CAAB9F7-E970-428D-8764-4CD7BCD105CD/UniversalMac_15.3_24D60_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.3 Developer Beta 3", "build": "24D5055b", "url": "https://updates.cdn-apple.com/2025WinterSeed/fullrestores/072-61910/64022727-D907-4A4F-963E-CADF5C74E1A7/UniversalMac_15.3_24D5055b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.3 Developer Beta 2", "build": "24D5040f", "url": "https://updates.cdn-apple.com/2025WinterSeed/fullrestores/072-51416/CB0B4DD0-1512-47FF-91B8-862ECB6CD2CF/UniversalMac_15.3_24D5040f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.3 Developer Beta 1", "build": "24D5034f", "url": "https://updates.cdn-apple.com/2024FallSeed/fullrestores/072-29298/D5353B95-E6B5-42DA-8DC3-664A28FFB1B9/UniversalMac_15.3_24D5034f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.2", "build": "24C101", "url": "https://updates.cdn-apple.com/2024FallFCS/fullrestores/072-44245/E811A1B0-28A9-4FCD-AE32-322E796F0EB8/UniversalMac_15.2_24C101_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.2 RC 2", "build": "24C100", "url": "https://updates.cdn-apple.com/2024FallFCS/fullrestores/072-39903/D467F0BB-9D59-4DCF-A355-DD200CE808A8/UniversalMac_15.2_24C100_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.2 RC", "build": "24C98", "url": "https://updates.cdn-apple.com/2024FallFCS/fullrestores/072-38683/C48192CB-7F42-4DD2-9EE6-0F6691DC088E/UniversalMac_15.2_24C98_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.2 Developer Beta 4", "build": "24C5089c", "url": "https://updates.cdn-apple.com/2024FallSeed/fullrestores/072-35468/AF95DCD4-137A-4E7D-A5D8-E12009F4B0EF/UniversalMac_15.2_24C5089c_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.2 Developer Beta 3", "build": "24C5079e", "url": "https://updates.cdn-apple.com/2024FallSeed/fullrestores/072-18002/44DBAF90-E56D-43E0-81F1-016B12E7FE27/UniversalMac_15.2_24C5079e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.2 Developer Beta 2", "build": "24C5073e", "url": "https://updates.cdn-apple.com/2024FallSeed/fullrestores/072-11046/F7A6FD60-0556-437D-88B6-6F2DD2CD8B96/UniversalMac_15.2_24C5073e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.1.1 (24B2091)", "build": "24B2091", "url": "https://updates.cdn-apple.com/2024FallFCS/fullrestores/072-29960/5EEC3C20-D7CB-4DD1-9CE4-7C177F531A41/UniversalMac_15.1.1_24B2091_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.1.1", "build": "24B91", "url": "https://updates.cdn-apple.com/2024FallFCS/fullrestores/072-30094/44BD016F-6EE3-4EE5-8890-6F9AA008C537/UniversalMac_15.1.1_24B91_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.1 (24B2083)", "build": "24B2083", "url": "https://updates.cdn-apple.com/2024FallFCS/fullrestores/072-12302/3786987A-AD94-4BFB-81B8-56D3841CA81B/UniversalMac_15.1_24B2083_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.1", "build": "24B83", "url": "https://updates.cdn-apple.com/2024FallFCS/fullrestores/072-12340/78D28AC4-CCFC-45D2-BD27-1E5D915E43F9/UniversalMac_15.1_24B83_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.1 RC", "build": "24B82", "url": "https://updates.cdn-apple.com/2024FallFCS/fullrestores/062-24344/4BBFA6BD-C58F-4C82-B793-6ECA98024379/UniversalMac_15.1_24B82_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.1 Developer Beta 6", "build": "24B5070a", "url": "https://updates.cdn-apple.com/2024FallSeed/fullrestores/072-00383/7EB106DF-9B03-4C2A-ACD1-997B0BFF9364/UniversalMac_15.1_24B5070a_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.1 Developer Beta 5", "build": "24B5055e", "url": "https://updates.cdn-apple.com/2024FallSeed/fullrestores/062-91081/FADE991A-C188-4D32-8F97-313F57E27359/UniversalMac_15.1_24B5055e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.1 Developer Beta 4", "build": "24B5046f", "url": "https://updates.cdn-apple.com/2024FallSeed/fullrestores/062-80453/61A07BD2-553D-425D-8B93-7E5A730AA4CA/UniversalMac_15.1_24B5046f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.1 Developer Beta 3", "build": "24B5035e", "url": "https://updates.cdn-apple.com/2024SummerSeed/fullrestores/062-74506/D50AC8F9-4795-4711-9C1A-907B6EB829A2/UniversalMac_15.1_24B5035e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0.1", "build": "24A348", "url": "https://updates.cdn-apple.com/2024FallFCS/fullrestores/072-01423/566E5B4E-1100-4643-91B3-131247351844/UniversalMac_15.0.1_24A348_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0", "build": "24A335", "url": "https://updates.cdn-apple.com/2024FallFCS/fullrestores/062-78489/BDA44327-C79E-4608-A7E0-455A7E91911F/UniversalMac_15.0_24A335_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0 Developer Beta 8", "build": "24A5331b", "url": "https://updates.cdn-apple.com/2024SummerSeed/fullrestores/062-71949/A67919DD-2AAC-4324-99BF-70765065DD70/UniversalMac_15.0_24A5331b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0 Developer Beta 7", "build": "24A5327a", "url": "https://updates.cdn-apple.com/2024SummerSeed/fullrestores/062-64520/F98C92F6-656A-46BB-A1DB-F447698DBB72/UniversalMac_15.0_24A5327a_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0 Developer Beta 6", "build": "24A5320a", "url": "https://updates.cdn-apple.com/2024SummerSeed/fullrestores/062-59123/7B77FE01-E040-4D4A-8E93-64BBF212A351/UniversalMac_15.0_24A5320a_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0 Developer Beta 5", "build": "24A5309e", "url": "https://updates.cdn-apple.com/2024SummerSeed/fullrestores/062-45756/5B063A0C-5ECA-434B-A462-CD1F65737105/UniversalMac_15.0_24A5309e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0 Developer Beta 4", "build": "24A5298h", "url": "https://updates.cdn-apple.com/2024SummerSeed/fullrestores/062-39288/8A58D88A-ED62-4E46-A406-13F0294EB4F5/UniversalMac_15.0_24A5298h_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0 Developer Beta 3 (24A5289h)", "build": "24A5289h", "url": "https://updates.cdn-apple.com/2024SummerSeed/fullrestores/062-36090/83523325-6540-4A17-BA93-A5849E4E9AC2/UniversalMac_15.0_24A5289h_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0 Developer Beta 3", "build": "24A5289g", "url": "https://updates.cdn-apple.com/2024SummerSeed/fullrestores/062-33928/E22622FB-DAFC-4C64-8CC6-B7CAF89477F7/UniversalMac_15.0_24A5289g_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0 Developer Beta 2", "build": "24A5279h", "url": "https://updates.cdn-apple.com/2024SummerSeed/fullrestores/062-22022/AB066FFB-B7FE-4132-83AC-E58A323805C1/UniversalMac_15.0_24A5279h_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sequoia", "name": "macOS 15.0 Developer Beta 1", "build": "24A5264n", "url": "https://updates.cdn-apple.com/2024SummerSeed/fullrestores/052-49083/ED8F54D6-A7BF-488A-85E5-617E08C41383/UniversalMac_15.0_24A5264n_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.6.1", "build": "23G93", "url": "https://updates.cdn-apple.com/2024SummerFCS/fullrestores/062-52859/932E0A8F-6644-4759-82DA-F8FA8DEA806A/UniversalMac_14.6.1_23G93_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.6", "build": "23G80", "url": "https://updates.cdn-apple.com/2024SummerFCS/fullrestores/052-69922/F5DA2B64-25EB-4370-9E89-FA5689859796/UniversalMac_14.6_23G80_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.6 Developer Beta 4", "build": "23G5075b", "url": "https://updates.cdn-apple.com/2024SpringSeed/fullrestores/062-37092/7D35E462-CD30-4B65-A1D6-D2AF0DA9AED8/UniversalMac_14.6_23G5075b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.6 Developer Beta 3", "build": "23G5066c", "url": "https://updates.cdn-apple.com/2024SpringSeed/fullrestores/062-28262/A4400A1D-9F38-41F7-B1CD-B3CB782DA2A3/UniversalMac_14.6_23G5066c_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.6 Developer Beta 2", "build": "23G5061b", "url": "https://updates.cdn-apple.com/2024SpringSeed/fullrestores/062-20934/B03E5DE3-1484-489F-AC53-956AC17DB7F0/UniversalMac_14.6_23G5061b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.6 Developer Beta 1", "build": "23G5052d", "url": "https://updates.cdn-apple.com/2024SpringSeed/fullrestores/052-91270/121EBE77-A313-4250-9F8D-55F2AABBCD4C/UniversalMac_14.6_23G5052d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.5", "build": "23F79", "url": "https://updates.cdn-apple.com/2024SpringFCS/fullrestores/062-01897/C874907B-9F82-4109-87EB-6B3C9BF1507D/UniversalMac_14.5_23F79_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.5 Developer Beta 4", "build": "23F5074a", "url": "https://updates.cdn-apple.com/2024SpringSeed/fullrestores/052-93222/8E2C4935-6FAA-4624-88D5-008966BD1A4C/UniversalMac_14.5_23F5074a_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.5 Developer Beta 3", "build": "23F5064f", "url": "https://updates.cdn-apple.com/2024SpringSeed/fullrestores/052-86827/74D9C6E6-396C-451B-8ACD-DB523D9FA93F/UniversalMac_14.5_23F5064f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.5 Developer Beta 2", "build": "23F5059e", "url": "https://updates.cdn-apple.com/2024SpringSeed/fullrestores/052-84344/599ECB0B-BF84-449F-B0D1-1428CFFAACC5/UniversalMac_14.5_23F5059e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.5 Developer Beta 1", "build": "23F5049f", "url": "https://updates.cdn-apple.com/2024SpringSeed/fullrestores/052-63630/11AFEFAF-A420-4CAE-84E7-D66530A7E9CA/UniversalMac_14.5_23F5049f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.4", "build": "23E214", "url": "https://updates.cdn-apple.com/2024WinterFCS/fullrestores/052-61990/47F0DD06-1106-4F2E-9CD6-AE6B361A0EC6/UniversalMac_14.4_23E214_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.4 Developer Beta 5", "build": "23E5211a", "url": "https://updates.cdn-apple.com/2024WinterSeed/fullrestores/052-59970/42D9D34A-DD6E-40D5-AA2B-EEB8E65EE64E/UniversalMac_14.4_23E5211a_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.4 Developer Beta 4", "build": "23E5205c", "url": "https://updates.cdn-apple.com/2024WinterSeed/fullrestores/052-58286/18DAC58E-4161-45D4-BE02-AE47B12B87B8/UniversalMac_14.4_23E5205c_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.4 Developer Beta 3", "build": "23E5196e", "url": "https://updates.cdn-apple.com/2024WinterSeed/fullrestores/052-51632/C6A7A58A-3CB1-4271-9CED-1D5DAE9078CF/UniversalMac_14.4_23E5196e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.4 Developer Beta 2", "build": "23E5191e", "url": "https://updates.cdn-apple.com/2024WinterSeed/fullrestores/052-45324/985FEED9-1A9E-49EB-A904-467E5E7EEE9C/UniversalMac_14.4_23E5191e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.4 Developer Beta 1", "build": "23E5180j", "url": "https://updates.cdn-apple.com/2024WinterSeed/fullrestores/052-42583/F09F735D-D3C3-4AF7-9BEC-F9C5D3B84363/UniversalMac_14.4_23E5180j_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.3", "build": "23D56", "url": "https://updates.cdn-apple.com/2024WinterFCS/fullrestores/042-78241/B45074EB-2891-4C05-BCA4-7463F3AC0982/UniversalMac_14.3_23D56_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.3 Developer Beta 3", "build": "23D5051b", "url": "https://updates.cdn-apple.com/2023WinterSeed/fullrestores/052-27952/6B395980-45C8-4E7C-9252-9F4CE35C7EDB/UniversalMac_14.3_23D5051b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.3 Developer Beta 2", "build": "23D5043d", "url": "https://updates.cdn-apple.com/2023WinterSeed/fullrestores/052-23267/60655998-DD9A-40BF-BFAB-6D2A4442DE83/UniversalMac_14.3_23D5043d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.3 Developer Beta 1", "build": "23D5033f", "url": "https://updates.cdn-apple.com/2023WinterSeed/fullrestores/052-04657/4126C431-B3EA-42C2-BC36-ED83715B8700/UniversalMac_14.3_23D5033f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.2.1", "build": "23C71", "url": "https://updates.cdn-apple.com/2023FallFCS/fullrestores/052-22662/ECE59A41-DACC-4CA5-AB23-FDED1A4567DE/UniversalMac_14.2.1_23C71_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.2", "build": "23C64", "url": "https://updates.cdn-apple.com/2023FallFCS/fullrestores/052-15117/DC2EE605-ABF3-41AE-9652-D137A8AA5907/UniversalMac_14.2_23C64_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.2 RC", "build": "23C63", "url": "https://updates.cdn-apple.com/2023FallFCS/fullrestores/052-14744/7215DBB4-BEAD-4A9C-9202-276ABA6832D5/UniversalMac_14.2_23C63_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.2 Developer Beta 4", "build": "23C5055b", "url": "https://updates.cdn-apple.com/2023WinterFCS/fullrestores/052-05962/A8344C85-06CE-43C7-9FF6-7B477A4DB8BA/UniversalMac_14.2_23C5055b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.2 Developer Beta 3", "build": "23C5047e", "url": "https://updates.cdn-apple.com/2023FallSeed/fullrestores/042-99526/0A9085CC-B36A-400A-86D8-B9FE23B1DA29/UniversalMac_14.2_23C5047e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.2 Developer Beta 2", "build": "23C5041e", "url": "https://updates.cdn-apple.com/2023FallSeed/fullrestores/042-92143/F642F928-DEE0-4C3F-A416-85745C778855/UniversalMac_14.2_23C5041e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.2 Developer Beta 1", "build": "23C5030f", "url": "https://updates.cdn-apple.com/2023FallSeed/fullrestores/042-71093/ECEFE157-E28B-40B4-9F21-CFD075129029/UniversalMac_14.2_23C5030f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.1.1", "build": "23B81", "url": "https://updates.cdn-apple.com/2023FallFCS/fullrestores/042-89681/55BD14DB-5535-4203-9359-E2C070E43FBE/UniversalMac_14.1.1_23B81_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.1", "build": "23B74", "url": "https://updates.cdn-apple.com/2023FallFCS/fullrestores/042-86430/DBE44960-58A6-4715-948B-D64F33F769BD/UniversalMac_14.1_23B74_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.1 RC", "build": "23B73", "url": "https://updates.cdn-apple.com/2023FallFCS/fullrestores/042-10900/347B734E-BC0B-41FA-9671-8000FCB5B0BB/UniversalMac_14.1_23B73_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.1 Developer Beta 3", "build": "23B5067a", "url": "https://updates.cdn-apple.com/2023FallSeed/fullrestores/042-73325/B24FC9CF-34D1-44A6-B977-FA718FE83DEB/UniversalMac_14.1_23B5067a_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.1 Developer Beta 2", "build": "23B5056e", "url": "https://updates.cdn-apple.com/2023FallSeed/fullrestores/042-65885/0FA6C4A7-C21A-4A5F-84C8-8FEE0D98A153/UniversalMac_14.1_23B5056e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.1 Developer Beta 1", "build": "23B5046f", "url": "https://updates.cdn-apple.com/2023FallSeed/fullrestores/042-60177/C4B6F5B3-8B66-461A-A048-4A9925F36FCD/UniversalMac_14.1_23B5046f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.0", "build": "23A344", "url": "https://updates.cdn-apple.com/2023FallFCS/fullrestores/042-54934/0E101AD6-3117-4B63-9BF1-143B6DB9270A/UniversalMac_14.0_23A344_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.0 RC", "build": "23A339", "url": "https://updates.cdn-apple.com/2023FallFCS/fullrestores/002-81996/596571C1-9856-4BB3-B5BF-B5A48F4B406E/UniversalMac_14.0_23A339_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.0 Developer Beta 7", "build": "23A5337a", "url": "https://updates.cdn-apple.com/2023SummerSeed/fullrestores/042-41500/D1789AEF-013B-4112-8A1E-401589023267/UniversalMac_14.0_23A5337a_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.0 Developer Beta 6", "build": "23A5328b", "url": "https://updates.cdn-apple.com/2023SummerSeed/fullrestores/042-37824/AA6B32A0-3C2C-4BEB-95A1-64E601934330/UniversalMac_14.0_23A5328b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.0 Developer Beta 5", "build": "23A5312d", "url": "https://updates.cdn-apple.com/2023SummerSeed/fullrestores/042-27168/7E046825-8EBA-4AAE-8ECC-DDD51B9306D2/UniversalMac_14.0_23A5312d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.0 Developer Beta 4", "build": "23A5301h", "url": "https://updates.cdn-apple.com/2023SummerSeed/fullrestores/042-25548/9EA6EC3D-5A7D-4D53-A17C-70EE71393921/UniversalMac_14.0_23A5301h_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.0 Developer Beta 3 (23A5286i)", "build": "23A5286i", "url": "https://updates.cdn-apple.com/2023SummerSeed/fullrestores/042-13887/3B4075C1-B695-49EA-82D9-4B720699D341/UniversalMac_14.0_23A5286i_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.0 Developer Beta 3", "build": "23A5286g", "url": "https://updates.cdn-apple.com/2023SummerSeed/fullrestores/042-06324/379026FA-C14F-4095-99FD-19F607D10EBF/UniversalMac_14.0_23A5286g_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.0 Developer Beta 2", "build": "23A5276g", "url": "https://updates.cdn-apple.com/2023SummerSeed/fullrestores/032-95861/67600C59-8516-4A46-B9D7-4007D395CEF5/UniversalMac_14.0_23A5276g_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "sonoma", "name": "macOS 14.0 Developer Beta 1", "build": "23A5257q", "url": "https://updates.cdn-apple.com/2023SummerSeed/fullrestores/032-94355/CBE8CBE1-750D-487E-A393-B90FEF60CEBA/UniversalMac_14.0_23A5257q_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.6", "build": "22G120", "url": "https://updates.cdn-apple.com/2023FallFCS/fullrestores/042-55833/C0830847-A2F8-458F-B680-967991820931/UniversalMac_13.6_22G120_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.5.2", "build": "22G91", "url": "https://updates.cdn-apple.com/2023SummerFCS/fullrestores/042-43686/945D434B-DA5D-48DB-A558-F6D18D11AD69/UniversalMac_13.5.2_22G91_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.5.1", "build": "22G90", "url": "https://updates.cdn-apple.com/2023SummerFCS/fullrestores/042-25658/2D6BE8DB-5549-4F85-8C54-39FC23BABC68/UniversalMac_13.5.1_22G90_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.5", "build": "22G74", "url": "https://updates.cdn-apple.com/2023SummerFCS/fullrestores/032-69606/D3E05CDF-E105-434C-A4A1-4E3DC7668DD0/UniversalMac_13.5_22G74_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.5 Developer Beta 5", "build": "22G5072a", "url": "https://updates.cdn-apple.com/2023SpringSeed/fullrestores/042-09570/8DA0B0AA-6FD4-42C4-A54E-BC0D53B92AC0/UniversalMac_13.5_22G5072a_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.5 Developer Beta 4", "build": "22G5059d", "url": "https://updates.cdn-apple.com/2023SpringSeed/fullrestores/042-03209/55CBE04D-FD90-483B-A6D7-45E0FBC1C94F/UniversalMac_13.5_22G5059d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.5 Developer Beta 3", "build": "22G5048d", "url": "https://updates.cdn-apple.com/2023SpringSeed/fullrestores/032-93679/1D39F2AC-8FD4-46A3-A159-478C76472B16/UniversalMac_13.5_22G5048d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.5 Developer Beta 2", "build": "22G5038d", "url": "https://updates.cdn-apple.com/2023SpringSeed/fullrestores/032-92523/E476F5EC-D046-4A76-889B-F19DA354459E/UniversalMac_13.5_22G5038d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.5 Developer Beta 1", "build": "22G5027e", "url": "https://updates.cdn-apple.com/2023SpringSeed/fullrestores/032-86178/CE6C5645-C5C3-41A9-B986-D5F0BD7BB10B/UniversalMac_13.5_22G5027e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.4.1", "build": "22F82", "url": "https://updates.cdn-apple.com/2023SpringFCS/fullrestores/042-01877/2F49A9FE-7033-41D0-9D0C-64EFCE6B4C22/UniversalMac_13.4.1_22F82_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.4.1 (WWDC23 M2 Macs)", "build": "22F2083", "url": "https://updates.cdn-apple.com/2023SpringFCS/fullrestores/042-01864/A8378F91-BA71-40DF-8F0D-606A16F1836B/UniversalMac_13.4.1_22F2083_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.4", "build": "22F66", "url": "https://updates.cdn-apple.com/2023SpringFCS/fullrestores/032-84884/F97A22EE-9B5E-4FD5-94C1-B39DCEE8D80F/UniversalMac_13.4_22F66_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.4 RC 2", "build": "22F63", "url": "https://updates.cdn-apple.com/2023SpringFCS/fullrestores/032-83954/6E06237C-1B56-4932-A8E1-3A07A3EE03A8/UniversalMac_13.4_22F63_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.4 RC", "build": "22F62", "url": "https://updates.cdn-apple.com/2023SpringFCS/fullrestores/032-44024/731F1533-53BE-4CEB-AA05-74F333CA904A/UniversalMac_13.4_22F62_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.4 Developer Beta 4", "build": "22F5059b", "url": "https://updates.cdn-apple.com/2023SpringSeed/fullrestores/032-79565/BA9CBFB7-152C-4FB6-B0B3-47769997BFA1/UniversalMac_13.4_22F5059b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.4 Developer Beta 3", "build": "22F5049e", "url": "https://updates.cdn-apple.com/2023SpringSeed/fullrestores/032-76661/573284E7-4A4A-440C-AC01-6065C7A8E667/UniversalMac_13.4_22F5049e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.4 Developer Beta 2", "build": "22F5037d", "url": "https://updates.cdn-apple.com/2023SpringSeed/fullrestores/032-69885/ECFA1532-C633-4ACE-9D2C-3B5FD19510D4/UniversalMac_13.4_22F5037d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.4 Developer Beta 1", "build": "22F5027f", "url": "https://updates.cdn-apple.com/2023SpringSeed/fullrestores/032-69187/B11709E0-1CF5-4460-A069-D12E1243E2AD/UniversalMac_13.4_22F5027f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.3.1", "build": "22E261", "url": "https://updates.cdn-apple.com/2023WinterFCS/fullrestores/032-66602/418BC37A-FCD9-400A-B4FA-022A19576CD4/UniversalMac_13.3.1_22E261_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.3", "build": "22E252", "url": "https://updates.cdn-apple.com/2023WinterSeed/fullrestores/002-75537/8250FA0E-0962-46D6-8A90-57A390B9FFD7/UniversalMac_13.3_22E252_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.3 Developer Beta 4", "build": "22E5246b", "url": "https://updates.cdn-apple.com/2023WinterSeed/fullrestores/032-63669/7C0F9BA8-35C0-457F-AF56-6943D58A2CDB/UniversalMac_13.3_22E5246b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.3 Developer Beta 3", "build": "22E5236f", "url": "https://updates.cdn-apple.com/2023WinterSeed/fullrestores/032-60411/1DDA996F-B620-4770-8FFE-87AB2043784D/UniversalMac_13.3_22E5236f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.3 Developer Beta 2", "build": "22E5230e", "url": "https://updates.cdn-apple.com/2023WinterSeed/fullrestores/032-54760/AE02E378-FD59-474D-93AB-C52617103C72/UniversalMac_13.3_22E5230e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.3 Developer Beta 1", "build": "22E5219e", "url": "https://updates.cdn-apple.com/2023WinterSeed/fullrestores/032-01932/676E0981-4535-4942-A4AE-E14C604CE719/UniversalMac_13.3_22E5219e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.2.1", "build": "22D68", "url": "https://updates.cdn-apple.com/2023WinterFCS/fullrestores/032-48346/EFF99C1E-C408-4E7A-A448-12E1468AF06C/UniversalMac_13.2.1_22D68_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.2", "build": "22D49", "url": "https://updates.cdn-apple.com/2023WinterFCS/fullrestores/032-35688/0350BB21-2B4B-4850-BF77-70B830283B28/UniversalMac_13.2_22D49_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.2 Developer Beta 2", "build": "22D5038i", "url": "https://updates.cdn-apple.com/2023WinterSeed/fullrestores/032-33181/62ECE236-5806-4136-AD08-EDC026FD80A5/UniversalMac_13.2_22D5038i_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.2 Developer Beta 1", "build": "22D5027d", "url": "https://updates.cdn-apple.com/2023WinterSeed/fullrestores/032-12640/6B472BA3-E678-4251-92D1-7AA23B66F53E/UniversalMac_13.2_22D5027d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.1", "build": "22C65", "url": "https://updates.cdn-apple.com/2022FallFCS/fullrestores/012-60270/0A7F49BA-FC31-4AD9-8E45-49B1FB9128A6/UniversalMac_13.1_22C65_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.1 Developer Beta 4", "build": "22C5059b", "url": "https://updates.cdn-apple.com/2022FallSeed/fullrestores/032-08112/957EA73A-7C95-4B3C-B99C-2C2C47555832/UniversalMac_13.1_22C5059b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.1 Developer Beta 3", "build": "22C5050e", "url": "https://updates.cdn-apple.com/2022FallSeed/fullrestores/032-06252/946CBF92-8F27-49B1-A692-81F54C73D2F0/UniversalMac_13.1_22C5050e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.1 Developer Beta 2", "build": "22C5044e", "url": "https://updates.cdn-apple.com/2022FallSeed/fullrestores/032-02019/670C9BA6-67EB-4AE6-A02E-88976F6F3118/UniversalMac_13.1_22C5044e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.1 Developer Beta 1", "build": "22C5033e", "url": "https://updates.cdn-apple.com/2022FallSeed/fullrestores/012-82062/10E6B723-51B8-4B2C-BA3B-12A18ED4E719/UniversalMac_13.1_22C5033e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0.1", "build": "22A400", "url": "https://updates.cdn-apple.com/2022FallFCS/fullrestores/012-93802/A7270B0F-05F8-43D1-A9AD-40EF5699E82C/UniversalMac_13.0.1_22A400_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0", "build": "22A380", "url": "https://updates.cdn-apple.com/2022FallFCS/fullrestores/012-92188/2C38BCD1-2BFF-4A10-B358-94E8E28BE805/UniversalMac_13.0_22A380_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 RC", "build": "22A379", "url": "https://updates.cdn-apple.com/2022FallFCS/fullrestores/071-08994/1118ADF4-1CC9-4554-9333-B1F64CF0C820/UniversalMac_13.0_22A379_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 11", "build": "22A5373b", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-84563/2FC38C63-3213-4BB6-8E41-2B066332CBE6/UniversalMac_13.0_22A5373b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 10", "build": "22A5365d", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-83054/16ECAA12-3A1B-4663-B49B-B1563ECD4314/UniversalMac_13.0_22A5365d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 9", "build": "22A5358e", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-71790/AF5A04A6-FF20-44C1-9BFF-43081BDB4D8C/UniversalMac_13.0_22A5358e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 8", "build": "22A5352e", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-70113/6F1F08B7-9A1B-48A9-93DB-55EE21121C87/UniversalMac_13.0_22A5352e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 7", "build": "22A5342f", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-66750/108EF06D-FBEE-4910-BA83-56A5C9B54110/UniversalMac_13.0_22A5342f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 6", "build": "22A5331f", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-61458/80300AD0-69E5-4429-AE3E-A936CA83B5FC/UniversalMac_13.0_22A5331f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 5", "build": "22A5321d", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-51397/8EF0874D-388A-4F62-B58A-89F968DD3082/UniversalMac_13.0_22A5321d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 4", "build": "22A5311f", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-43316/6CE4D83A-E44C-4DD1-B47F-DE168355662E/UniversalMac_13.0_22A5311f_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 3 (22A5295i)", "build": "22A5295i", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-38309/6EDC76A0-4432-4C64-83C5-F43C885A75D6/UniversalMac_13.0_22A5295i_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 3", "build": "22A5295h", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-34274/130176F5-C4CB-4664-A2F0-F29CA1281694/UniversalMac_13.0_22A5295h_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 2", "build": "22A5286j", "url": "https://updates.cdn-apple.com/2022SummerSeed/fullrestores/012-30346/9DD787A7-044B-4650-86D4-84E80B6B9C36/UniversalMac_13.0_22A5286j_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "ventura", "name": "macOS 13.0 Developer Beta 1", "build": "22A5266r", "url": "https://developer.apple.com/services-account/download?path=%2FWWDC_2022%2FmacOS_13_beta%2FUniversalMac_13.0_22A5266r_Restore.ipsw", "channel": "devbeta", "needsCookie": true }, { "group": "monterey", "name": "macOS 12.6.1", "build": "21G217", "url": "https://updates.cdn-apple.com/2022FallFCS/fullrestores/012-66032/8D8D90C6-A876-4FFF-BBF4-D158939B3841/UniversalMac_12.6.1_21G217_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.6", "build": "21G115", "url": "https://updates.cdn-apple.com/2022FallFCS/fullrestores/012-40537/0EC7C669-13E9-49FB-BD64-9EECC1D174B2/UniversalMac_12.6_21G115_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.5.1", "build": "21G83", "url": "https://updates.cdn-apple.com/2022SummerFCS/fullrestores/012-51674/A7019DDB-3355-470F-A355-4162A187AB6C/UniversalMac_12.5.1_21G83_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.5", "build": "21G72", "url": "https://updates.cdn-apple.com/2022SummerFCS/fullrestores/012-42731/BD9917E0-262C-41C5-A69F-AC316A534A39/UniversalMac_12.5_21G72_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.5 RC", "build": "21G69", "url": "https://updates.cdn-apple.com/2022SummerFCS/fullrestores/012-40368/5DD0A524-140A-46AF-91ED-5F28EA9DEC01/UniversalMac_12.5_21G69_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.5 Developer Beta 5", "build": "21G5063a", "url": "https://updates.cdn-apple.com/2022FallSeed/fullrestores/012-36748/52342C55-6598-4A86-AAB8-8901145792C8/UniversalMac_12.5_21G5063a_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.5 Developer Beta 4", "build": "21G5056b", "url": "https://updates.cdn-apple.com/2022FallSeed/fullrestores/012-26441/AE0AC638-2773-49D3-BF84-950B10BF39E9/UniversalMac_12.5_21G5056b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.5 Developer Beta 3", "build": "21G5046c", "url": "https://updates.cdn-apple.com/2022FallSeed/fullrestores/012-18271/FFF202B2-E4B6-4A3E-9681-42A0F3F81B11/UniversalMac_12.5_21G5046c_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.5 Developer Beta 2", "build": "21G5037d", "url": "https://updates.cdn-apple.com/2022FallSeed/fullrestores/012-10648/1CC63FC5-5A22-4A5A-9A7B-C19C8C4A6731/UniversalMac_12.5_21G5037d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.5 Developer Beta 1", "build": "21G5027d", "url": "https://updates.cdn-apple.com/2022FallSeed/fullrestores/002-93712/5F234425-6096-43FC-B518-1E9D7B4D0254/UniversalMac_12.5_21G5027d_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.4", "build": "21F79", "url": "https://updates.cdn-apple.com/2022SpringFCS/fullrestores/012-06874/9CECE956-D945-45E2-93E9-4FFDC81BB49A/UniversalMac_12.4_21F79_Restore.ipsw", "channel": "regular", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.4 Developer Beta 4", "build": "21F5071b", "url": "https://updates.cdn-apple.com/2022SpringSeed/fullrestores/002-95106/0F7A6388-C4B5-4B8E-B8B2-F62C030699D0/UniversalMac_12.4_21F5071b_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.4 Developer Beta 3", "build": "21F5063e", "url": "https://updates.cdn-apple.com/2022SpringSeed/fullrestores/002-90009/DA6BD192-1698-48B3-AB6D-9D3A045ED1B1/UniversalMac_12.4_21F5063e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.4 Developer Beta 2", "build": "21F5058e", "url": "https://updates.cdn-apple.com/2022SpringSeed/fullrestores/002-87587/BC2EBE80-F0F4-4B56-BCDC-340E0AD8E985/UniversalMac_12.4_21F5058e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.4 Developer Beta 1", "build": "21F5048e", "url": "https://updates.cdn-apple.com/2022SpringSeed/fullrestores/002-85721/A21FF659-8493-4A16-A989-2C3141F48D8C/UniversalMac_12.4_21F5048e_Restore.ipsw", "channel": "devbeta", "needsCookie": false }, { "group": "monterey", "name": "macOS 12.3.1", "build": "21E258", "url": "https://updates.cdn-apple.com/2022SpringFCS/fullrestores/002-79219/851BEDF0-19DB-4040-B765-0F4089D1530D/UniversalMac_12.3.1_21E258_Restore.ipsw", "channel": "regular", "needsCookie": false } ] } ================================================ FILE: data/ipsws_v2.json ================================================ { "apiVersion" : 2, "channels" : [ { "icon" : "checkmark.seal", "id" : "regular", "name" : "Release", "note" : "Public, stable releases." }, { "icon" : "wrench.and.screwdriver", "id" : "devbeta", "name" : "Developer Beta", "note" : "Betas meant for developer testing." } ], "deviceSupportVersions" : [ { "id" : "device_support_macOS26_beta", "instructions" : "Your Mac needs an update to install virtual machines that use this version of macOS.\n\nHere’s how you can install the update:\n\n1. Head over to the [Apple Developer downloads page](https:\/\/developer.apple.com\/download\/).\n2. Under “Operating Systems”, find “Device Support for macOS 26”.\n3. Click the link below that item.\n4. Double-click the downloaded DMG and open the installer. Follow the instructions.\n5. Quit and relaunch VirtualBuddy. Then, try again.\n\nInstalling the latest Xcode beta will also install the required update.", "mobileDeviceMinVersion" : "1810.0.0", "osVersion" : "26.0.0", "title" : "Device Support Update Required" } ], "features" : [ { "detail" : "For previous versions, you can enable file sharing in System Preferences > Sharing.", "id" : "file_sharing", "minVersionGuest" : "13.0.0", "minVersionHost" : "13.0.0", "name" : "File sharing", "unsupportedPlatform" : false }, { "id" : "guest_app", "minVersionGuest" : "13.0.0", "minVersionHost" : "13.0.0", "name" : "VirtualBuddyGuest app", "unsupportedPlatform" : false }, { "id" : "trackpad", "minVersionGuest" : "13.0.0", "minVersionHost" : "13.0.0", "name" : "Trackpad", "unsupportedPlatform" : false }, { "id" : "mac_keyboard", "minVersionGuest" : "13.0.0", "minVersionHost" : "13.0.0", "name" : "Mac keyboard", "unsupportedPlatform" : false }, { "id" : "state_restoration", "minVersionGuest" : "14.0.0", "minVersionHost" : "14.0.0", "name" : "State restoration", "unsupportedPlatform" : false }, { "id" : "display_resize", "minVersionGuest" : "14.0.0", "minVersionHost" : "14.0.0", "name" : "Automatic display configuration", "unsupportedPlatform" : false }, { "id" : "rosetta_sharing", "minVersionGuest" : "14.0.0", "minVersionHost" : "14.0.0", "name" : "Rosetta Sharing", "unsupportedPlatform" : true } ], "groups" : [ { "darkImage" : { "id" : "tahoe", "thumbnail" : { "blurHash" : "UfG0bS?9S1Rn~Pr@R.ofs+S6jFt3EBIZt5oJ", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/tahoe-dark-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/tahoe-dark.heic" }, "id" : "tahoe", "image" : { "id" : "tahoe", "thumbnail" : { "blurHash" : "UfG0bS?9S1Rn~Pr@R.ofs+S6jFt3EBIZt5oJ", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/tahoe-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/tahoe.heic" }, "majorVersion" : "26.0.0", "name" : "macOS Tahoe" }, { "darkImage" : { "id" : "sequoia", "thumbnail" : { "blurHash" : "eN86q8M1Hbyya7t-g$MpnTx,b;k9X8Vgr?osbHenWEeYoGj@aNaPah", "height" : 399, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/sequoia-dark-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/sequoia-dark.heic" }, "id" : "sequoia", "image" : { "id" : "sequoia", "thumbnail" : { "blurHash" : "egK9[+5Y0:}iNe-S=rI^SixW$JoIwbNfOYocn$NeWXNfShfkS5S5WX", "height" : 399, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/sequoia-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/sequoia.heic" }, "majorVersion" : "15.0.0", "name" : "macOS Sequoia" }, { "darkImage" : { "id" : "sonoma", "thumbnail" : { "blurHash" : "ec8rzYaIWBj?a}iqosaxj?fkRFa2axayj[t%ofV[ayf6V]pGkBf5fi", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/sonoma-dark-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/sonoma-dark.heic" }, "id" : "sonoma", "image" : { "id" : "sonoma", "thumbnail" : { "blurHash" : "e:Hx.{wFRlbFaxm#xToba{j?tmetnOjsa#J,tRsln$a#NINff+oIj?", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/sonoma-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/sonoma.heic" }, "majorVersion" : "14.0.0", "name" : "macOS Sonoma" }, { "darkImage" : { "id" : "ventura", "thumbnail" : { "blurHash" : "enHk%=ocoeW:Nb-8xEODaya#1fWWJAWExDEjR-jGazoJWCagw^s-Wp", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/ventura-dark-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/ventura-dark.heic" }, "id" : "ventura", "image" : { "id" : "ventura", "thumbnail" : { "blurHash" : "eqOd?[TcFgR.a}LM$yWFs+oJ2{N{Iuj[jZJrj]slS5a#NNafw@ocoI", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/ventura-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/ventura.heic" }, "majorVersion" : "13.0.0", "name" : "macOS Ventura" }, { "darkImage" : { "id" : "monterey", "thumbnail" : { "blurHash" : "eb6$?*bER}o3j?oToNjqa^jsS7oOoMa_axoOWqa}oNjrbJa~a#a}oM", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/monterey-dark-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/monterey-dark.heic" }, "id" : "monterey", "image" : { "id" : "monterey", "thumbnail" : { "blurHash" : "euJi.3I.R6snaz~UjLa1fOafPQxBoLaiaeXVX4g2o0jZoaW?X9bXoL", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/monterey-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/monterey.heic" }, "majorVersion" : "12.0.0", "name" : "macOS Monterey" } ], "minAppVersion" : "2.0.0", "requirementSets" : [ { "id" : "min_host_12", "minCPUCount" : 2, "minMemorySizeMB" : 4096, "minVersionHost" : "12.0.0" }, { "id" : "min_host_13", "minCPUCount" : 2, "minMemorySizeMB" : 4096, "minVersionHost" : "13.0.0" } ], "restoreImages" : [ { "build" : "25E243", "channel" : "devbeta", "downloadSize" : 19734806625, "group" : "tahoe", "id" : "25E243", "mobileDeviceMinVersion" : "1827.100.14", "name" : "macOS 26.4 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterFCS\/fullrestores\/122-08082\/B9E8695D-7851-435A-8897-6D84A80F4F84\/UniversalMac_26.4_25E243_Restore.ipsw", "version" : "26.4.0" }, { "build" : "25E5233c", "channel" : "devbeta", "downloadSize" : 19731757690, "group" : "tahoe", "id" : "25E5233c", "mobileDeviceMinVersion" : "1827.100.14", "name" : "macOS 26.4 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterSeed\/fullrestores\/047-94466\/DDE04370-287E-4321-8FAB-8656591CAF70\/UniversalMac_26.4_25E5233c_Restore.ipsw", "version" : "26.4.0" }, { "build" : "25E5223i", "channel" : "devbeta", "downloadSize" : 19222852846, "group" : "tahoe", "id" : "25E5223i", "mobileDeviceMinVersion" : "1827.100.14", "name" : "macOS 26.4 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterSeed\/fullrestores\/047-83754\/2EAF0D9F-ADAF-402B-9C29-874D9B1E6378\/UniversalMac_26.4_25E5223i_Restore.ipsw", "version" : "26.4.0" }, { "build" : "25E5218f", "channel" : "devbeta", "downloadSize" : 19289481885, "group" : "tahoe", "id" : "25E5218f", "mobileDeviceMinVersion" : "1827.100.14", "name" : "macOS 26.4 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterSeed\/fullrestores\/047-83734\/6F2EB9BB-586D-4CCA-8C8D-28A02E38AAA2\/UniversalMac_26.4_25E5218f_Restore.ipsw", "version" : "26.4.0" }, { "build" : "25E5207k", "channel" : "devbeta", "downloadSize" : 19289462929, "group" : "tahoe", "id" : "25E5207k", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.4 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterSeed\/fullrestores\/047-01191\/B8945931-E74E-43AC-9370-BFCA2E35392E\/UniversalMac_26.4_25E5207k_Restore.ipsw", "version" : "26.4.0" }, { "build" : "25D2140", "channel" : "regular", "downloadSize" : 19330789958, "group" : "tahoe", "id" : "25D2140", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.3.2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterFCS\/fullrestores\/047-94879\/40A2B65E-4E49-4EAA-8BEC-62A305007488\/UniversalMac_26.3.2_25D2140_Restore.ipsw", "version" : "26.3.2" }, { "build" : "25D2128", "channel" : "regular", "downloadSize" : 19330833456, "group" : "tahoe", "id" : "25D2128", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.3.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterFCS\/fullrestores\/047-88313\/2E098049-1731-4415-A206-546D09301973\/UniversalMac_26.3.1_25D2128_Restore.ipsw", "version" : "26.3.1" }, { "build" : "25D125", "channel" : "regular", "downloadSize" : 18877622182, "group" : "tahoe", "id" : "25D125", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterFCS\/fullrestores\/047-60229\/6D5DBEA5-75A0-4BEF-ACC9-5ACF9B8DF6B7\/UniversalMac_26.3_25D125_Restore.ipsw", "version" : "26.3.0" }, { "build" : "25D122", "channel" : "devbeta", "downloadSize" : 18877545740, "group" : "tahoe", "id" : "25D122", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.3 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterFCS\/fullrestores\/082-57168\/EE47B566-E405-4A7C-B99E-5BBCA418BF73\/UniversalMac_26.3_25D122_Restore.ipsw", "version" : "26.3.0" }, { "build" : "25D5112c", "channel" : "devbeta", "downloadSize" : 18873995756, "group" : "tahoe", "id" : "25D5112c", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.3 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterSeed\/fullrestores\/047-27561\/D17A405D-58FC-4CD3-9CBD-2FF572E5B45C\/UniversalMac_26.3_25D5112c_Restore.ipsw", "version" : "26.3.0" }, { "build" : "25D5101c", "channel" : "devbeta", "downloadSize" : 18806520160, "group" : "tahoe", "id" : "25D5101c", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.3 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2026WinterSeed\/fullrestores\/047-11932\/34528828-0245-4624-9D72-AFB752B2BEA2\/UniversalMac_26.3_25D5101c_Restore.ipsw", "version" : "26.3.0" }, { "build" : "25D5087f", "channel" : "devbeta", "downloadSize" : 18789116337, "group" : "tahoe", "id" : "25D5087f", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.3 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallSeed\/fullrestores\/089-61648\/B3DA1DA6-F227-4898-87F1-D6F07AEDC708\/UniversalMac_26.3_25D5087f_Restore.ipsw", "version" : "26.3.0" }, { "build" : "25C56", "channel" : "regular", "downloadSize" : 18741508230, "group" : "tahoe", "id" : "25C56", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallFCS\/fullrestores\/093-37399\/E144C918-CF99-4BBC-B1D0-3E739B9A3F2D\/UniversalMac_26.2_25C56_Restore.ipsw", "version" : "26.2.0" }, { "build" : "25C5048a", "channel" : "devbeta", "downloadSize" : 18721157967, "group" : "tahoe", "id" : "25C5048a", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.2 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallSeed\/fullrestores\/089-47061\/439CA6BD-EFD9-4FFF-9C04-6A63838AB6A9\/UniversalMac_26.2_25C5048a_Restore.ipsw", "version" : "26.2.0" }, { "build" : "25C5037j", "channel" : "devbeta", "downloadSize" : 18720456518, "group" : "tahoe", "id" : "25C5037j", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.2 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallSeed\/fullrestores\/089-37244\/F3E98428-E443-4226-849D-48649C50B10B\/UniversalMac_26.2_25C5037j_Restore.ipsw", "version" : "26.2.0" }, { "build" : "25C5031i", "channel" : "devbeta", "downloadSize" : 18688911675, "group" : "tahoe", "id" : "25C5031i", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.2 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallSeed\/fullrestores\/089-21529\/D038FF08-78B0-4FFE-9E39-2F5A64AE46AB\/UniversalMac_26.2_25C5031i_Restore.ipsw", "version" : "26.2.0" }, { "build" : "25B78", "channel" : "regular", "downloadSize" : 18718884780, "group" : "tahoe", "id" : "25B78", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallFCS\/fullrestores\/089-04148\/791B6F00-A30B-4EB0-B2E3-257167F7715B\/UniversalMac_26.1_25B78_Restore.ipsw", "version" : "26.1.0" }, { "build" : "25B77", "channel" : "devbeta", "downloadSize" : 18718876862, "group" : "tahoe", "id" : "25B77", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.1 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallFCS\/fullrestores\/082-71650\/9F912A37-12E8-4AC7-9ABE-F12B1A38D1B1\/UniversalMac_26.1_25B77_Restore.ipsw", "version" : "26.1.0" }, { "build" : "25B5072a", "channel" : "devbeta", "downloadSize" : 18580910549, "group" : "tahoe", "id" : "25B5072a", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.1 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallSeed\/fullrestores\/093-85256\/C134DA7F-3E56-47A6-9E51-A83457E42A15\/UniversalMac_26.1_25B5072a_Restore.ipsw", "version" : "26.1.0" }, { "build" : "25B5062e", "channel" : "devbeta", "downloadSize" : 18573497955, "group" : "tahoe", "id" : "25B5062e", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.1 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallSeed\/fullrestores\/093-84191\/18D74779-3C02-4F5B-AADC-77259C29ADA1\/UniversalMac_26.1_25B5062e_Restore.ipsw", "version" : "26.1.0" }, { "build" : "25B5057f", "channel" : "devbeta", "downloadSize" : 18573453168, "group" : "tahoe", "id" : "25B5057f", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.1 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallSeed\/fullrestores\/093-73644\/970FD02E-AB88-4138-B23A-B8B490E7A2D6\/UniversalMac_26.1_25B5057f_Restore.ipsw", "version" : "26.1.0" }, { "build" : "25B5042k", "channel" : "devbeta", "downloadSize" : 19012528170, "group" : "tahoe", "id" : "25B5042k", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.1 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallSeed\/fullrestores\/082-91406\/9745D030-E51F-497A-9E26-1B40A07FE9AE\/UniversalMac_26.1_25B5042k_Restore.ipsw", "version" : "26.1.0" }, { "build" : "25A362", "channel" : "regular", "downloadSize" : 18267682409, "group" : "tahoe", "id" : "25A362", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallFCS\/fullrestores\/093-50898\/60AE7E97-3E60-441B-9B34-E603C694C5C1\/UniversalMac_26.0.1_25A362_Restore.ipsw", "version" : "26.0.1" }, { "build" : "25A354", "channel" : "regular", "downloadSize" : 18267586270, "group" : "tahoe", "id" : "25A354", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallFCS\/fullrestores\/093-37622\/CE01FAB2-7F26-48EE-AEE4-5E57A7F6D8BB\/UniversalMac_26.0_25A354_Restore.ipsw", "version" : "26.0.0" }, { "build" : "25A353", "channel" : "devbeta", "downloadSize" : 18250895075, "group" : "tahoe", "id" : "25A353", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025FallFCS\/fullrestores\/093-37294\/119120C1-6306-4287-AC2B-0AF964CD0B3C\/UniversalMac_26.0_25A353_Restore.ipsw", "version" : "26.0.0" }, { "build" : "25A5351b", "channel" : "devbeta", "downloadSize" : 18110649555, "group" : "tahoe", "id" : "25A5351b", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0 Developer Beta 9", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerSeed\/fullrestores\/093-28610\/036AE4A3-8A7D-44F1-A26F-ED754C346A74\/UniversalMac_26.0_25A5351b_Restore.ipsw", "version" : "26.0.0" }, { "build" : "25A5349a", "channel" : "devbeta", "downloadSize" : 18110732600, "group" : "tahoe", "id" : "25A5349a", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0 Developer Beta 8", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerSeed\/fullrestores\/093-26148\/FDF54F82-98DF-4285-A0DB-CB30F4F93C71\/UniversalMac_26.0_25A5349a_Restore.ipsw", "version" : "26.0.0" }, { "build" : "25A5346a", "channel" : "devbeta", "downloadSize" : 18110708260, "group" : "tahoe", "id" : "25A5346a", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0 Developer Beta 7", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerSeed\/fullrestores\/093-15545\/4C191A6F-E63B-4A9F-8597-6B1880EE09CC\/UniversalMac_26.0_25A5346a_Restore.ipsw", "version" : "26.0.0" }, { "build" : "25A5338b", "channel" : "devbeta", "downloadSize" : 18110631556, "group" : "tahoe", "id" : "25A5338b", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0 Developer Beta 6", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerSeed\/fullrestores\/093-05416\/26D38526-AE12-4E87-8542-D71CA2EA1A0F\/UniversalMac_26.0_25A5338b_Restore.ipsw", "version" : "26.0.0" }, { "build" : "25A5327h", "channel" : "devbeta", "downloadSize" : 17958910966, "group" : "tahoe", "id" : "25A5327h", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0 Developer Beta 5", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerSeed\/fullrestores\/093-03417\/C35F3BA3-6DCD-473C-9266-A3BF390520BE\/UniversalMac_26.0_25A5327h_Restore.ipsw", "version" : "26.0.0" }, { "build" : "25A5316i", "channel" : "devbeta", "downloadSize" : 18615400612, "group" : "tahoe", "id" : "25A5316i", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerSeed\/fullrestores\/082-89871\/D95E6C0D-147C-4F0D-878B-A4534D9B4763\/UniversalMac_26.0_25A5316i_Restore.ipsw", "version" : "26.0.0" }, { "build" : "25A5306g", "channel" : "devbeta", "downloadSize" : 18833175626, "group" : "tahoe", "id" : "25A5306g", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerSeed\/fullrestores\/082-72248\/F32F08F8-FC66-4D24-847B-F03C6CF7C410\/UniversalMac_26.0_25A5306g_Restore.ipsw", "version" : "26.0.0" }, { "build" : "24G90", "channel" : "regular", "downloadSize" : 16814137790, "group" : "sequoia", "id" : "24G90", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.6.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerFCS\/fullrestores\/093-10809\/CFD6DD38-DAF0-40DA-854F-31AAD1294C6F\/UniversalMac_15.6.1_24G90_Restore.ipsw", "version" : "15.6.1" }, { "build" : "24G84", "channel" : "regular", "downloadSize" : 16814076831, "group" : "sequoia", "id" : "24G84", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.6", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerFCS\/fullrestores\/082-08674\/51294E4D-A273-44BE-A280-A69FC347FB87\/UniversalMac_15.6_24G84_Restore.ipsw", "version" : "15.6.0" }, { "build" : "24G5074c", "channel" : "devbeta", "downloadSize" : 16877900839, "group" : "sequoia", "id" : "24G5074c", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.6 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringSeed\/fullrestores\/082-70173\/CA418AAE-B538-4B79-AA99-303369C6A512\/UniversalMac_15.6_24G5074c_Restore.ipsw", "version" : "15.6.0" }, { "build" : "24G5065c", "channel" : "devbeta", "downloadSize" : 16877786738, "group" : "sequoia", "id" : "24G5065c", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.6 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringSeed\/fullrestores\/082-62583\/9DB8F3CA-FCE0-45B5-8C17-6A4BF5E5A706\/UniversalMac_15.6_24G5065c_Restore.ipsw", "version" : "15.6.0" }, { "build" : "25A5295e", "channel" : "devbeta", "downloadSize" : 18740071846, "group" : "tahoe", "id" : "25A5295e", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerSeed\/fullrestores\/082-58035\/4434D4B0-E61D-483F-87A0-4643F9D4750D\/UniversalMac_26.0_25A5295e_Restore.ipsw", "version" : "26.0.0" }, { "build" : "25A5279m", "channel" : "devbeta", "downloadSize" : 18287329125, "group" : "tahoe", "id" : "25A5279m", "mobileDeviceMinVersion" : "1810.0.0", "name" : "macOS 26.0 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SummerSeed\/fullrestores\/082-55592\/95F6DA16-4116-491E-B332-7165C051E1C5\/UniversalMac_26.0_25A5279m_Restore.ipsw", "version" : "26.0.0" }, { "build" : "24G5054d", "channel" : "devbeta", "downloadSize" : 16877678522, "group" : "sequoia", "id" : "24G5054d", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.6 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringSeed\/fullrestores\/082-29051\/6E674436-7F60-4CEC-B318-C0062D336517\/UniversalMac_15.6_24G5054d_Restore.ipsw", "version" : "15.6.0" }, { "build" : "24F74", "channel" : "regular", "downloadSize" : 16879691985, "group" : "sequoia", "id" : "24F74", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.5", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringFCS\/fullrestores\/082-44534\/CE6C1054-99A3-4F67-A823-3EE9E6510CDE\/UniversalMac_15.5_24F74_Restore.ipsw", "version" : "15.5.0" }, { "build" : "24F5068b", "channel" : "devbeta", "downloadSize" : 16876550506, "group" : "sequoia", "id" : "24F5068b", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.5 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringSeed\/fullrestores\/082-42002\/44CF75E3-7765-4356-A88C-5EEEC4471CA4\/UniversalMac_15.5_24F5068b_Restore.ipsw", "version" : "15.5.0" }, { "build" : "24F5053j", "channel" : "devbeta", "downloadSize" : 16809364641, "group" : "sequoia", "id" : "24F5053j", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.5 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringSeed\/fullrestores\/072-92980\/8E6DFD3B-8438-423F-8FFE-A95234775E84\/UniversalMac_15.5_24F5053j_Restore.ipsw", "version" : "15.5.0" }, { "build" : "24F5053f", "channel" : "devbeta", "downloadSize" : 16876267533, "group" : "sequoia", "id" : "24F5053f", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.5 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringSeed\/fullrestores\/082-24561\/B199D1B2-296D-43E5-A29A-5FC27796F36D\/UniversalMac_15.5_24F5053f_Restore.ipsw", "version" : "15.5.0" }, { "build" : "24F5042g", "channel" : "devbeta", "downloadSize" : 16875810487, "group" : "sequoia", "id" : "24F5042g", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.5 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringSeed\/fullrestores\/072-94893\/B85463BB-3ED6-4A5A-BE78-92AD86B531D7\/UniversalMac_15.5_24F5042g_Restore.ipsw", "version" : "15.5.0" }, { "build" : "24E263", "channel" : "regular", "downloadSize" : 16808445859, "group" : "sequoia", "id" : "24E263", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.4.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringFCS\/fullrestores\/082-23340\/61923341-EE73-4C6E-BB3E-DAB3069548BF\/UniversalMac_15.4.1_24E263_Restore.ipsw", "version" : "15.4.1" }, { "build" : "24E248", "channel" : "regular", "downloadSize" : 16808408047, "group" : "sequoia", "id" : "24E248", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringFCS\/fullrestores\/082-16517\/AACDDC33-9683-4431-98AF-F04EF7C15EE3\/UniversalMac_15.4_24E248_Restore.ipsw", "version" : "15.4.0" }, { "build" : "24E247", "channel" : "devbeta", "downloadSize" : 16808392577, "group" : "sequoia", "id" : "24E247", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.4 RC 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringFCS\/fullrestores\/082-13483\/7D547A66-A1A6-48C1-A8E1-FE78559BDD34\/UniversalMac_15.4_24E247_Restore.ipsw", "version" : "15.4.0" }, { "build" : "24E246", "channel" : "devbeta", "downloadSize" : 16808427372, "group" : "sequoia", "id" : "24E246", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.4 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025SpringFCS\/fullrestores\/082-13013\/C3CAC9AF-B139-4924-BE1B-10ED6EF931A4\/UniversalMac_15.4_24E246_Restore.ipsw", "version" : "15.4.0" }, { "build" : "24E5238a", "channel" : "devbeta", "downloadSize" : 16805266642, "group" : "sequoia", "id" : "24E5238a", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.4 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025WinterSeed\/fullrestores\/082-08323\/FD7D7539-514F-4F2E-AD3B-4D6D0D248706\/UniversalMac_15.4_24E5238a_Restore.ipsw", "version" : "15.4.0" }, { "build" : "24E5228e", "channel" : "devbeta", "downloadSize" : 16805273503, "group" : "sequoia", "id" : "24E5228e", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.4 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025WinterSeed\/fullrestores\/072-96972\/D76A7AE6-3A35-4295-B102-E5FC5C84B4DC\/UniversalMac_15.4_24E5228e_Restore.ipsw", "version" : "15.4.0" }, { "build" : "24E5222f", "channel" : "devbeta", "downloadSize" : 16602610887, "group" : "sequoia", "id" : "24E5222f", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.4 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025WinterSeed\/fullrestores\/072-90273\/76B9EE73-0836-47FB-8CBA-E3E902ACD21A\/UniversalMac_15.4_24E5222f_Restore.ipsw", "version" : "15.4.0" }, { "build" : "24E5206s", "channel" : "devbeta", "downloadSize" : 16598479538, "group" : "sequoia", "id" : "24E5206s", "mobileDeviceMinVersion" : "1774.0.0", "name" : "macOS 15.4 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025WinterSeed\/fullrestores\/072-92205\/1FCDCB29-B8A3-4461-A98A-270F1D1121C8\/UniversalMac_15.4_24E5206s_Restore.ipsw", "version" : "15.4.0" }, { "build" : "24D81", "channel" : "regular", "downloadSize" : 16532705221, "group" : "sequoia", "id" : "24D81", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.3.2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025WinterFCS\/fullrestores\/082-01504\/828B8EF9-8134-49D5-B24A-0BA504FC5ECC\/UniversalMac_15.3.2_24D81_Restore.ipsw", "version" : "15.3.2" }, { "build" : "24D70", "channel" : "regular", "downloadSize" : 16532837554, "group" : "sequoia", "id" : "24D70", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.3.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025WinterFCS\/fullrestores\/072-70618\/42F1A8CC-7E07-4329-958A-757FF600C303\/UniversalMac_15.3.1_24D70_Restore.ipsw", "version" : "15.3.1" }, { "build" : "24D60", "channel" : "regular", "downloadSize" : 16532796141, "group" : "sequoia", "id" : "24D60", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025WinterFCS\/fullrestores\/072-08269\/7CAAB9F7-E970-428D-8764-4CD7BCD105CD\/UniversalMac_15.3_24D60_Restore.ipsw", "version" : "15.3.0" }, { "build" : "24D5055b", "channel" : "devbeta", "downloadSize" : 16529416640, "group" : "sequoia", "id" : "24D5055b", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.3 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025WinterSeed\/fullrestores\/072-61910\/64022727-D907-4A4F-963E-CADF5C74E1A7\/UniversalMac_15.3_24D5055b_Restore.ipsw", "version" : "15.3.0" }, { "build" : "24D5040f", "channel" : "devbeta", "downloadSize" : 16529508256, "group" : "sequoia", "id" : "24D5040f", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.3 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2025WinterSeed\/fullrestores\/072-51416\/CB0B4DD0-1512-47FF-91B8-862ECB6CD2CF\/UniversalMac_15.3_24D5040f_Restore.ipsw", "version" : "15.3.0" }, { "build" : "24D5034f", "channel" : "devbeta", "downloadSize" : 16528933369, "group" : "sequoia", "id" : "24D5034f", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.3 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallSeed\/fullrestores\/072-29298\/D5353B95-E6B5-42DA-8DC3-664A28FFB1B9\/UniversalMac_15.3_24D5034f_Restore.ipsw", "version" : "15.3.0" }, { "build" : "24C101", "channel" : "regular", "downloadSize" : 16532456817, "group" : "sequoia", "id" : "24C101", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallFCS\/fullrestores\/072-44245\/E811A1B0-28A9-4FCD-AE32-322E796F0EB8\/UniversalMac_15.2_24C101_Restore.ipsw", "version" : "15.2.0" }, { "build" : "24C100", "channel" : "devbeta", "downloadSize" : 16532519706, "group" : "sequoia", "id" : "24C100", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.2 RC 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallFCS\/fullrestores\/072-39903\/D467F0BB-9D59-4DCF-A355-DD200CE808A8\/UniversalMac_15.2_24C100_Restore.ipsw", "version" : "15.2.0" }, { "build" : "24C98", "channel" : "devbeta", "downloadSize" : 16532504882, "group" : "sequoia", "id" : "24C98", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.2 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallFCS\/fullrestores\/072-38683\/C48192CB-7F42-4DD2-9EE6-0F6691DC088E\/UniversalMac_15.2_24C98_Restore.ipsw", "version" : "15.2.0" }, { "build" : "24C5089c", "channel" : "devbeta", "downloadSize" : 16529207615, "group" : "sequoia", "id" : "24C5089c", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.2 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallSeed\/fullrestores\/072-35468\/AF95DCD4-137A-4E7D-A5D8-E12009F4B0EF\/UniversalMac_15.2_24C5089c_Restore.ipsw", "version" : "15.2.0" }, { "build" : "24C5079e", "channel" : "devbeta", "downloadSize" : 16529245021, "group" : "sequoia", "id" : "24C5079e", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.2 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallSeed\/fullrestores\/072-18002\/44DBAF90-E56D-43E0-81F1-016B12E7FE27\/UniversalMac_15.2_24C5079e_Restore.ipsw", "version" : "15.2.0" }, { "build" : "24C5073e", "channel" : "devbeta", "downloadSize" : 16529788046, "group" : "sequoia", "id" : "24C5073e", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.2 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallSeed\/fullrestores\/072-11046\/F7A6FD60-0556-437D-88B6-6F2DD2CD8B96\/UniversalMac_15.2_24C5073e_Restore.ipsw", "version" : "15.2.0" }, { "build" : "24B2091", "channel" : "regular", "downloadSize" : 15516666958, "group" : "sequoia", "id" : "24B2091", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.1.1 (24B2091)", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallFCS\/fullrestores\/072-29960\/5EEC3C20-D7CB-4DD1-9CE4-7C177F531A41\/UniversalMac_15.1.1_24B2091_Restore.ipsw", "version" : "15.1.1" }, { "build" : "24B91", "channel" : "regular", "downloadSize" : 15755042223, "group" : "sequoia", "id" : "24B91", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.1.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallFCS\/fullrestores\/072-30094\/44BD016F-6EE3-4EE5-8890-6F9AA008C537\/UniversalMac_15.1.1_24B91_Restore.ipsw", "version" : "15.1.1" }, { "build" : "24B2083", "channel" : "regular", "downloadSize" : 15516638342, "group" : "sequoia", "id" : "24B2083", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.1 (24B2083)", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallFCS\/fullrestores\/072-12302\/3786987A-AD94-4BFB-81B8-56D3841CA81B\/UniversalMac_15.1_24B2083_Restore.ipsw", "version" : "15.1.0" }, { "build" : "24B83", "channel" : "regular", "downloadSize" : 15738221177, "group" : "sequoia", "id" : "24B83", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallFCS\/fullrestores\/072-12340\/78D28AC4-CCFC-45D2-BD27-1E5D915E43F9\/UniversalMac_15.1_24B83_Restore.ipsw", "version" : "15.1.0" }, { "build" : "24B82", "channel" : "devbeta", "downloadSize" : 15755058094, "group" : "sequoia", "id" : "24B82", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.1 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallFCS\/fullrestores\/062-24344\/4BBFA6BD-C58F-4C82-B793-6ECA98024379\/UniversalMac_15.1_24B82_Restore.ipsw", "version" : "15.1.0" }, { "build" : "24B5070a", "channel" : "devbeta", "downloadSize" : 15734880172, "group" : "sequoia", "id" : "24B5070a", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.1 Developer Beta 6", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallSeed\/fullrestores\/072-00383\/7EB106DF-9B03-4C2A-ACD1-997B0BFF9364\/UniversalMac_15.1_24B5070a_Restore.ipsw", "version" : "15.1.0" }, { "build" : "24B5055e", "channel" : "devbeta", "downloadSize" : 15734924985, "group" : "sequoia", "id" : "24B5055e", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.1 Developer Beta 5", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallSeed\/fullrestores\/062-91081\/FADE991A-C188-4D32-8F97-313F57E27359\/UniversalMac_15.1_24B5055e_Restore.ipsw", "version" : "15.1.0" }, { "build" : "24B5046f", "channel" : "devbeta", "downloadSize" : 15870172423, "group" : "sequoia", "id" : "24B5046f", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.1 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallSeed\/fullrestores\/062-80453\/61A07BD2-553D-425D-8B93-7E5A730AA4CA\/UniversalMac_15.1_24B5046f_Restore.ipsw", "version" : "15.1.0" }, { "build" : "24B5035e", "channel" : "devbeta", "downloadSize" : 15869388891, "group" : "sequoia", "id" : "24B5035e", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.1 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerSeed\/fullrestores\/062-74506\/D50AC8F9-4795-4711-9C1A-907B6EB829A2\/UniversalMac_15.1_24B5035e_Restore.ipsw", "version" : "15.1.0" }, { "build" : "24A348", "channel" : "regular", "downloadSize" : 15737037399, "group" : "sequoia", "id" : "24A348", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.0.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallFCS\/fullrestores\/072-01423\/566E5B4E-1100-4643-91B3-131247351844\/UniversalMac_15.0.1_24A348_Restore.ipsw", "version" : "15.0.1" }, { "build" : "24A335", "channel" : "regular", "downloadSize" : 15736994120, "group" : "sequoia", "id" : "24A335", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.0", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024FallFCS\/fullrestores\/062-78489\/BDA44327-C79E-4608-A7E0-455A7E91911F\/UniversalMac_15.0_24A335_Restore.ipsw", "version" : "15.0.0" }, { "build" : "24A5331b", "channel" : "devbeta", "downloadSize" : 15598810418, "group" : "sequoia", "id" : "24A5331b", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.0 Developer Beta 8", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerSeed\/fullrestores\/062-71949\/A67919DD-2AAC-4324-99BF-70765065DD70\/UniversalMac_15.0_24A5331b_Restore.ipsw", "version" : "15.0.0" }, { "build" : "24A5327a", "channel" : "devbeta", "downloadSize" : 15598903779, "group" : "sequoia", "id" : "24A5327a", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.0 Developer Beta 7", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerSeed\/fullrestores\/062-64520\/F98C92F6-656A-46BB-A1DB-F447698DBB72\/UniversalMac_15.0_24A5327a_Restore.ipsw", "version" : "15.0.0" }, { "build" : "24A5320a", "channel" : "devbeta", "downloadSize" : 15598703819, "group" : "sequoia", "id" : "24A5320a", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.0 Developer Beta 6", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerSeed\/fullrestores\/062-59123\/7B77FE01-E040-4D4A-8E93-64BBF212A351\/UniversalMac_15.0_24A5320a_Restore.ipsw", "version" : "15.0.0" }, { "build" : "24A5309e", "channel" : "devbeta", "downloadSize" : 16020119097, "group" : "sequoia", "id" : "24A5309e", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.0 Developer Beta 5", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerSeed\/fullrestores\/062-45756\/5B063A0C-5ECA-434B-A462-CD1F65737105\/UniversalMac_15.0_24A5309e_Restore.ipsw", "version" : "15.0.0" }, { "build" : "24A5298h", "channel" : "devbeta", "downloadSize" : 15892020340, "group" : "sequoia", "id" : "24A5298h", "mobileDeviceMinVersion" : "1754.0.0", "name" : "macOS 15.0 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerSeed\/fullrestores\/062-39288\/8A58D88A-ED62-4E46-A406-13F0294EB4F5\/UniversalMac_15.0_24A5298h_Restore.ipsw", "version" : "15.0.0" }, { "build" : "24A5289h", "channel" : "devbeta", "downloadSize" : 15756935317, "group" : "sequoia", "id" : "24A5289h", "mobileDeviceMinVersion" : "1742.0.0", "name" : "macOS 15.0 Developer Beta 3 (24A5289h)", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerSeed\/fullrestores\/062-36090\/83523325-6540-4A17-BA93-A5849E4E9AC2\/UniversalMac_15.0_24A5289h_Restore.ipsw", "version" : "15.0.0" }, { "build" : "24A5289g", "channel" : "devbeta", "downloadSize" : 15756393703, "group" : "sequoia", "id" : "24A5289g", "mobileDeviceMinVersion" : "1742.0.0", "name" : "macOS 15.0 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerSeed\/fullrestores\/062-33928\/E22622FB-DAFC-4C64-8CC6-B7CAF89477F7\/UniversalMac_15.0_24A5289g_Restore.ipsw", "version" : "15.0.0" }, { "build" : "24A5279h", "channel" : "devbeta", "downloadSize" : 16230968934, "group" : "sequoia", "id" : "24A5279h", "mobileDeviceMinVersion" : "1742.0.0", "name" : "macOS 15.0 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerSeed\/fullrestores\/062-22022\/AB066FFB-B7FE-4132-83AC-E58A323805C1\/UniversalMac_15.0_24A5279h_Restore.ipsw", "version" : "15.0.0" }, { "build" : "24A5264n", "channel" : "devbeta", "downloadSize" : 15824875957, "group" : "sequoia", "id" : "24A5264n", "mobileDeviceMinVersion" : "1742.0.0", "name" : "macOS 15.0 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerSeed\/fullrestores\/052-49083\/ED8F54D6-A7BF-488A-85E5-617E08C41383\/UniversalMac_15.0_24A5264n_Restore.ipsw", "version" : "15.0.0" }, { "build" : "23G93", "channel" : "regular", "downloadSize" : 14790601661, "group" : "sonoma", "id" : "23G93", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.6.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerFCS\/fullrestores\/062-52859\/932E0A8F-6644-4759-82DA-F8FA8DEA806A\/UniversalMac_14.6.1_23G93_Restore.ipsw", "version" : "14.6.1" }, { "build" : "23G80", "channel" : "regular", "downloadSize" : 14790718754, "group" : "sonoma", "id" : "23G80", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.6", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SummerFCS\/fullrestores\/052-69922\/F5DA2B64-25EB-4370-9E89-FA5689859796\/UniversalMac_14.6_23G80_Restore.ipsw", "version" : "14.6.0" }, { "build" : "23G5075b", "channel" : "devbeta", "downloadSize" : 14634053502, "group" : "sonoma", "id" : "23G5075b", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.6 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SpringSeed\/fullrestores\/062-37092\/7D35E462-CD30-4B65-A1D6-D2AF0DA9AED8\/UniversalMac_14.6_23G5075b_Restore.ipsw", "version" : "14.6.0" }, { "build" : "23G5066c", "channel" : "devbeta", "downloadSize" : 14631054851, "group" : "sonoma", "id" : "23G5066c", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.6 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SpringSeed\/fullrestores\/062-28262\/A4400A1D-9F38-41F7-B1CD-B3CB782DA2A3\/UniversalMac_14.6_23G5066c_Restore.ipsw", "version" : "14.6.0" }, { "build" : "23G5061b", "channel" : "devbeta", "downloadSize" : 14647192213, "group" : "sonoma", "id" : "23G5061b", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.6 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SpringSeed\/fullrestores\/062-20934\/B03E5DE3-1484-489F-AC53-956AC17DB7F0\/UniversalMac_14.6_23G5061b_Restore.ipsw", "version" : "14.6.0" }, { "build" : "23G5052d", "channel" : "devbeta", "downloadSize" : 14640728273, "group" : "sonoma", "id" : "23G5052d", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.6 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SpringSeed\/fullrestores\/052-91270\/121EBE77-A313-4250-9F8D-55F2AABBCD4C\/UniversalMac_14.6_23G5052d_Restore.ipsw", "version" : "14.6.0" }, { "build" : "23F79", "channel" : "regular", "downloadSize" : 14801044197, "group" : "sonoma", "id" : "23F79", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.5", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SpringFCS\/fullrestores\/062-01897\/C874907B-9F82-4109-87EB-6B3C9BF1507D\/UniversalMac_14.5_23F79_Restore.ipsw", "version" : "14.5.0" }, { "build" : "23F5074a", "channel" : "devbeta", "downloadSize" : 14631266856, "group" : "sonoma", "id" : "23F5074a", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.5 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SpringSeed\/fullrestores\/052-93222\/8E2C4935-6FAA-4624-88D5-008966BD1A4C\/UniversalMac_14.5_23F5074a_Restore.ipsw", "version" : "14.5.0" }, { "build" : "23F5064f", "channel" : "devbeta", "downloadSize" : 14612241697, "group" : "sonoma", "id" : "23F5064f", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.5 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SpringSeed\/fullrestores\/052-86827\/74D9C6E6-396C-451B-8ACD-DB523D9FA93F\/UniversalMac_14.5_23F5064f_Restore.ipsw", "version" : "14.5.0" }, { "build" : "23F5059e", "channel" : "devbeta", "downloadSize" : 14625397506, "group" : "sonoma", "id" : "23F5059e", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.5 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SpringSeed\/fullrestores\/052-84344\/599ECB0B-BF84-449F-B0D1-1428CFFAACC5\/UniversalMac_14.5_23F5059e_Restore.ipsw", "version" : "14.5.0" }, { "build" : "23F5049f", "channel" : "devbeta", "downloadSize" : 14617671816, "group" : "sonoma", "id" : "23F5049f", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.5 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024SpringSeed\/fullrestores\/052-63630\/11AFEFAF-A420-4CAE-84E7-D66530A7E9CA\/UniversalMac_14.5_23F5049f_Restore.ipsw", "version" : "14.5.0" }, { "build" : "23E224", "channel" : "regular", "downloadSize" : 14744062393, "group" : "sonoma", "id" : "23E224", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.4.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024WinterFCS\/fullrestores\/052-77579\/4569734E-120C-4F31-AD08-FC1FF825D059\/UniversalMac_14.4.1_23E224_Restore.ipsw", "version" : "14.4.1" }, { "build" : "23E214", "channel" : "regular", "downloadSize" : 14744505377, "group" : "sonoma", "id" : "23E214", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024WinterFCS\/fullrestores\/052-61990\/47F0DD06-1106-4F2E-9CD6-AE6B361A0EC6\/UniversalMac_14.4_23E214_Restore.ipsw", "version" : "14.4.0" }, { "build" : "23E5211a", "channel" : "devbeta", "downloadSize" : 14553892980, "group" : "sonoma", "id" : "23E5211a", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.4 Developer Beta 5", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024WinterSeed\/fullrestores\/052-59970\/42D9D34A-DD6E-40D5-AA2B-EEB8E65EE64E\/UniversalMac_14.4_23E5211a_Restore.ipsw", "version" : "14.4.0" }, { "build" : "23E5205c", "channel" : "devbeta", "downloadSize" : 14556773482, "group" : "sonoma", "id" : "23E5205c", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.4 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024WinterSeed\/fullrestores\/052-58286\/18DAC58E-4161-45D4-BE02-AE47B12B87B8\/UniversalMac_14.4_23E5205c_Restore.ipsw", "version" : "14.4.0" }, { "build" : "23E5196e", "channel" : "devbeta", "downloadSize" : 14552980775, "group" : "sonoma", "id" : "23E5196e", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.4 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024WinterSeed\/fullrestores\/052-51632\/C6A7A58A-3CB1-4271-9CED-1D5DAE9078CF\/UniversalMac_14.4_23E5196e_Restore.ipsw", "version" : "14.4.0" }, { "build" : "23E5191e", "channel" : "devbeta", "downloadSize" : 14560421496, "group" : "sonoma", "id" : "23E5191e", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.4 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024WinterSeed\/fullrestores\/052-45324\/985FEED9-1A9E-49EB-A904-467E5E7EEE9C\/UniversalMac_14.4_23E5191e_Restore.ipsw", "version" : "14.4.0" }, { "build" : "23E5180j", "channel" : "devbeta", "downloadSize" : 14526761461, "group" : "sonoma", "id" : "23E5180j", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.4 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024WinterSeed\/fullrestores\/052-42583\/F09F735D-D3C3-4AF7-9BEC-F9C5D3B84363\/UniversalMac_14.4_23E5180j_Restore.ipsw", "version" : "14.4.0" }, { "build" : "23D60", "channel" : "regular", "downloadSize" : 14503964718, "group" : "sonoma", "id" : "23D60", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.3.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024WinterFCS\/fullrestores\/052-40770\/72916BCC-D357-422D-A4A2-EF1DEDF6968C\/UniversalMac_14.3.1_23D60_Restore.ipsw", "version" : "14.3.1" }, { "build" : "23D56", "channel" : "regular", "downloadSize" : 14505910832, "group" : "sonoma", "id" : "23D56", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2024WinterFCS\/fullrestores\/042-78241\/B45074EB-2891-4C05-BCA4-7463F3AC0982\/UniversalMac_14.3_23D56_Restore.ipsw", "version" : "14.3.0" }, { "build" : "23D5051b", "channel" : "devbeta", "downloadSize" : 14345532149, "group" : "sonoma", "id" : "23D5051b", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.3 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterSeed\/fullrestores\/052-27952\/6B395980-45C8-4E7C-9252-9F4CE35C7EDB\/UniversalMac_14.3_23D5051b_Restore.ipsw", "version" : "14.3.0" }, { "build" : "23D5043d", "channel" : "devbeta", "downloadSize" : 14354286748, "group" : "sonoma", "id" : "23D5043d", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.3 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterSeed\/fullrestores\/052-23267\/60655998-DD9A-40BF-BFAB-6D2A4442DE83\/UniversalMac_14.3_23D5043d_Restore.ipsw", "version" : "14.3.0" }, { "build" : "23D5033f", "channel" : "devbeta", "downloadSize" : 14349582906, "group" : "sonoma", "id" : "23D5033f", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.3 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterSeed\/fullrestores\/052-04657\/4126C431-B3EA-42C2-BC36-ED83715B8700\/UniversalMac_14.3_23D5033f_Restore.ipsw", "version" : "14.3.0" }, { "build" : "23C71", "channel" : "regular", "downloadSize" : 14489499738, "group" : "sonoma", "id" : "23C71", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.2.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallFCS\/fullrestores\/052-22662\/ECE59A41-DACC-4CA5-AB23-FDED1A4567DE\/UniversalMac_14.2.1_23C71_Restore.ipsw", "version" : "14.2.1" }, { "build" : "23C64", "channel" : "regular", "downloadSize" : 14488791535, "group" : "sonoma", "id" : "23C64", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallFCS\/fullrestores\/052-15117\/DC2EE605-ABF3-41AE-9652-D137A8AA5907\/UniversalMac_14.2_23C64_Restore.ipsw", "version" : "14.2.0" }, { "build" : "23C63", "channel" : "devbeta", "downloadSize" : 14489489499, "group" : "sonoma", "id" : "23C63", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.2 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallFCS\/fullrestores\/052-14744\/7215DBB4-BEAD-4A9C-9202-276ABA6832D5\/UniversalMac_14.2_23C63_Restore.ipsw", "version" : "14.2.0" }, { "build" : "23C5055b", "channel" : "devbeta", "downloadSize" : 14332799879, "group" : "sonoma", "id" : "23C5055b", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.2 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterFCS\/fullrestores\/052-05962\/A8344C85-06CE-43C7-9FF6-7B477A4DB8BA\/UniversalMac_14.2_23C5055b_Restore.ipsw", "version" : "14.2.0" }, { "build" : "23C5047e", "channel" : "devbeta", "downloadSize" : 14324106377, "group" : "sonoma", "id" : "23C5047e", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.2 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallSeed\/fullrestores\/042-99526\/0A9085CC-B36A-400A-86D8-B9FE23B1DA29\/UniversalMac_14.2_23C5047e_Restore.ipsw", "version" : "14.2.0" }, { "build" : "23C5041e", "channel" : "devbeta", "downloadSize" : 14343066933, "group" : "sonoma", "id" : "23C5041e", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.2 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallSeed\/fullrestores\/042-92143\/F642F928-DEE0-4C3F-A416-85745C778855\/UniversalMac_14.2_23C5041e_Restore.ipsw", "version" : "14.2.0" }, { "build" : "23C5030f", "channel" : "devbeta", "downloadSize" : 14015403903, "group" : "sonoma", "id" : "23C5030f", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.2 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallSeed\/fullrestores\/042-71093\/ECEFE157-E28B-40B4-9F21-CFD075129029\/UniversalMac_14.2_23C5030f_Restore.ipsw", "version" : "14.2.0" }, { "build" : "23B92", "channel" : "regular", "downloadSize" : 13980816431, "group" : "sonoma", "id" : "23B92", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.1.2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallFCS\/fullrestores\/052-09443\/E8752548-0B80-480C-9FB4-67246672C1B5\/UniversalMac_14.1.2_23B92_Restore.ipsw", "version" : "14.1.2" }, { "build" : "23B81", "channel" : "regular", "downloadSize" : 13981156491, "group" : "sonoma", "id" : "23B81", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.1.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallFCS\/fullrestores\/042-89681\/55BD14DB-5535-4203-9359-E2C070E43FBE\/UniversalMac_14.1.1_23B81_Restore.ipsw", "version" : "14.1.1" }, { "build" : "23B74", "channel" : "regular", "downloadSize" : 13981550665, "group" : "sonoma", "id" : "23B74", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallFCS\/fullrestores\/042-86430\/DBE44960-58A6-4715-948B-D64F33F769BD\/UniversalMac_14.1_23B74_Restore.ipsw", "version" : "14.1.0" }, { "build" : "23B73", "channel" : "devbeta", "downloadSize" : 13981665474, "group" : "sonoma", "id" : "23B73", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.1 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallFCS\/fullrestores\/042-10900\/347B734E-BC0B-41FA-9671-8000FCB5B0BB\/UniversalMac_14.1_23B73_Restore.ipsw", "version" : "14.1.0" }, { "build" : "23B5067a", "channel" : "devbeta", "downloadSize" : 13964726406, "group" : "sonoma", "id" : "23B5067a", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.1 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallSeed\/fullrestores\/042-73325\/B24FC9CF-34D1-44A6-B977-FA718FE83DEB\/UniversalMac_14.1_23B5067a_Restore.ipsw", "version" : "14.1.0" }, { "build" : "23B5056e", "channel" : "devbeta", "downloadSize" : 13913662198, "group" : "sonoma", "id" : "23B5056e", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.1 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallSeed\/fullrestores\/042-65885\/0FA6C4A7-C21A-4A5F-84C8-8FEE0D98A153\/UniversalMac_14.1_23B5056e_Restore.ipsw", "version" : "14.1.0" }, { "build" : "23B5046f", "channel" : "devbeta", "downloadSize" : 13909776599, "group" : "sonoma", "id" : "23B5046f", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.1 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallSeed\/fullrestores\/042-60177\/C4B6F5B3-8B66-461A-A048-4A9925F36FCD\/UniversalMac_14.1_23B5046f_Restore.ipsw", "version" : "14.1.0" }, { "build" : "23A344", "channel" : "regular", "downloadSize" : 13905898651, "group" : "sonoma", "id" : "23A344", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.0", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallFCS\/fullrestores\/042-54934\/0E101AD6-3117-4B63-9BF1-143B6DB9270A\/UniversalMac_14.0_23A344_Restore.ipsw", "version" : "14.0.0" }, { "build" : "23A339", "channel" : "devbeta", "downloadSize" : 13905892847, "group" : "sonoma", "id" : "23A339", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.0 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallFCS\/fullrestores\/002-81996\/596571C1-9856-4BB3-B5BF-B5A48F4B406E\/UniversalMac_14.0_23A339_Restore.ipsw", "version" : "14.0.0" }, { "build" : "23A5337a", "channel" : "devbeta", "downloadSize" : 13817165626, "group" : "sonoma", "id" : "23A5337a", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.0 Developer Beta 7", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerSeed\/fullrestores\/042-41500\/D1789AEF-013B-4112-8A1E-401589023267\/UniversalMac_14.0_23A5337a_Restore.ipsw", "version" : "14.0.0" }, { "build" : "23A5328b", "channel" : "devbeta", "downloadSize" : 13818265607, "group" : "sonoma", "id" : "23A5328b", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.0 Developer Beta 6", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerSeed\/fullrestores\/042-37824\/AA6B32A0-3C2C-4BEB-95A1-64E601934330\/UniversalMac_14.0_23A5328b_Restore.ipsw", "version" : "14.0.0" }, { "build" : "23A5312d", "channel" : "devbeta", "downloadSize" : 13902876317, "group" : "sonoma", "id" : "23A5312d", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.0 Developer Beta 5", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerSeed\/fullrestores\/042-27168\/7E046825-8EBA-4AAE-8ECC-DDD51B9306D2\/UniversalMac_14.0_23A5312d_Restore.ipsw", "version" : "14.0.0" }, { "build" : "23A5301h", "channel" : "devbeta", "downloadSize" : 13966115260, "group" : "sonoma", "id" : "23A5301h", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.0 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerSeed\/fullrestores\/042-25548\/9EA6EC3D-5A7D-4D53-A17C-70EE71393921\/UniversalMac_14.0_23A5301h_Restore.ipsw", "version" : "14.0.0" }, { "build" : "23A5286i", "channel" : "devbeta", "downloadSize" : 14040882686, "group" : "sonoma", "id" : "23A5286i", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.0 Developer Beta 3 (23A5286i)", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerSeed\/fullrestores\/042-13887\/3B4075C1-B695-49EA-82D9-4B720699D341\/UniversalMac_14.0_23A5286i_Restore.ipsw", "version" : "14.0.0" }, { "build" : "23A5286g", "channel" : "devbeta", "downloadSize" : 14039361424, "group" : "sonoma", "id" : "23A5286g", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.0 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerSeed\/fullrestores\/042-06324\/379026FA-C14F-4095-99FD-19F607D10EBF\/UniversalMac_14.0_23A5286g_Restore.ipsw", "version" : "14.0.0" }, { "build" : "23A5276g", "channel" : "devbeta", "downloadSize" : 14011239154, "group" : "sonoma", "id" : "23A5276g", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.0 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerSeed\/fullrestores\/032-95861\/67600C59-8516-4A46-B9D7-4007D395CEF5\/UniversalMac_14.0_23A5276g_Restore.ipsw", "version" : "14.0.0" }, { "build" : "23A5257q", "channel" : "devbeta", "downloadSize" : 14073497027, "group" : "sonoma", "id" : "23A5257q", "mobileDeviceMinVersion" : "1600.0.0", "name" : "macOS 14.0 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerSeed\/fullrestores\/032-94355\/CBE8CBE1-750D-487E-A393-B90FEF60CEBA\/UniversalMac_14.0_23A5257q_Restore.ipsw", "version" : "14.0.0" }, { "build" : "22G120", "channel" : "regular", "downloadSize" : 12893555341, "group" : "ventura", "id" : "22G120", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.6", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023FallFCS\/fullrestores\/042-55833\/C0830847-A2F8-458F-B680-967991820931\/UniversalMac_13.6_22G120_Restore.ipsw", "version" : "13.6.0" }, { "build" : "22G91", "channel" : "regular", "downloadSize" : 12892043230, "group" : "ventura", "id" : "22G91", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.5.2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerFCS\/fullrestores\/042-43686\/945D434B-DA5D-48DB-A558-F6D18D11AD69\/UniversalMac_13.5.2_22G91_Restore.ipsw", "version" : "13.5.2" }, { "build" : "22G90", "channel" : "regular", "downloadSize" : 12892897195, "group" : "ventura", "id" : "22G90", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.5.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerFCS\/fullrestores\/042-25658\/2D6BE8DB-5549-4F85-8C54-39FC23BABC68\/UniversalMac_13.5.1_22G90_Restore.ipsw", "version" : "13.5.1" }, { "build" : "22G74", "channel" : "regular", "downloadSize" : 12893195726, "group" : "ventura", "id" : "22G74", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.5", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SummerFCS\/fullrestores\/032-69606\/D3E05CDF-E105-434C-A4A1-4E3DC7668DD0\/UniversalMac_13.5_22G74_Restore.ipsw", "version" : "13.5.0" }, { "build" : "22G5072a", "channel" : "devbeta", "downloadSize" : 12876188620, "group" : "ventura", "id" : "22G5072a", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.5 Developer Beta 5", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringSeed\/fullrestores\/042-09570\/8DA0B0AA-6FD4-42C4-A54E-BC0D53B92AC0\/UniversalMac_13.5_22G5072a_Restore.ipsw", "version" : "13.5.0" }, { "build" : "22G5059d", "channel" : "devbeta", "downloadSize" : 12881087818, "group" : "ventura", "id" : "22G5059d", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.5 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringSeed\/fullrestores\/042-03209\/55CBE04D-FD90-483B-A6D7-45E0FBC1C94F\/UniversalMac_13.5_22G5059d_Restore.ipsw", "version" : "13.5.0" }, { "build" : "22G5048d", "channel" : "devbeta", "downloadSize" : 12874423585, "group" : "ventura", "id" : "22G5048d", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.5 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringSeed\/fullrestores\/032-93679\/1D39F2AC-8FD4-46A3-A159-478C76472B16\/UniversalMac_13.5_22G5048d_Restore.ipsw", "version" : "13.5.0" }, { "build" : "22G5038d", "channel" : "devbeta", "downloadSize" : 12727337348, "group" : "ventura", "id" : "22G5038d", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.5 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringSeed\/fullrestores\/032-92523\/E476F5EC-D046-4A76-889B-F19DA354459E\/UniversalMac_13.5_22G5038d_Restore.ipsw", "version" : "13.5.0" }, { "build" : "22G5027e", "channel" : "devbeta", "downloadSize" : 12729623010, "group" : "ventura", "id" : "22G5027e", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.5 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringSeed\/fullrestores\/032-86178\/CE6C5645-C5C3-41A9-B986-D5F0BD7BB10B\/UniversalMac_13.5_22G5027e_Restore.ipsw", "version" : "13.5.0" }, { "build" : "22F82", "channel" : "regular", "downloadSize" : 12739802320, "group" : "ventura", "id" : "22F82", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.4.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringFCS\/fullrestores\/042-01877\/2F49A9FE-7033-41D0-9D0C-64EFCE6B4C22\/UniversalMac_13.4.1_22F82_Restore.ipsw", "version" : "13.4.1" }, { "build" : "22F66", "channel" : "regular", "downloadSize" : 12739995153, "group" : "ventura", "id" : "22F66", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringFCS\/fullrestores\/032-84884\/F97A22EE-9B5E-4FD5-94C1-B39DCEE8D80F\/UniversalMac_13.4_22F66_Restore.ipsw", "version" : "13.4.0" }, { "build" : "22F63", "channel" : "devbeta", "downloadSize" : 12739473501, "group" : "ventura", "id" : "22F63", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.4 RC 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringFCS\/fullrestores\/032-83954\/6E06237C-1B56-4932-A8E1-3A07A3EE03A8\/UniversalMac_13.4_22F63_Restore.ipsw", "version" : "13.4.0" }, { "build" : "22F62", "channel" : "devbeta", "downloadSize" : 12738586980, "group" : "ventura", "id" : "22F62", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.4 RC", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringFCS\/fullrestores\/032-44024\/731F1533-53BE-4CEB-AA05-74F333CA904A\/UniversalMac_13.4_22F62_Restore.ipsw", "version" : "13.4.0" }, { "build" : "22F5059b", "channel" : "devbeta", "downloadSize" : 12721895514, "group" : "ventura", "id" : "22F5059b", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.4 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringSeed\/fullrestores\/032-79565\/BA9CBFB7-152C-4FB6-B0B3-47769997BFA1\/UniversalMac_13.4_22F5059b_Restore.ipsw", "version" : "13.4.0" }, { "build" : "22F5049e", "channel" : "devbeta", "downloadSize" : 12720838737, "group" : "ventura", "id" : "22F5049e", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.4 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringSeed\/fullrestores\/032-76661\/573284E7-4A4A-440C-AC01-6065C7A8E667\/UniversalMac_13.4_22F5049e_Restore.ipsw", "version" : "13.4.0" }, { "build" : "22F5037d", "channel" : "devbeta", "downloadSize" : 12743516873, "group" : "ventura", "id" : "22F5037d", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.4 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringSeed\/fullrestores\/032-69885\/ECFA1532-C633-4ACE-9D2C-3B5FD19510D4\/UniversalMac_13.4_22F5037d_Restore.ipsw", "version" : "13.4.0" }, { "build" : "22F5027f", "channel" : "devbeta", "downloadSize" : 12728262691, "group" : "ventura", "id" : "22F5027f", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.4 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023SpringSeed\/fullrestores\/032-69187\/B11709E0-1CF5-4460-A069-D12E1243E2AD\/UniversalMac_13.4_22F5027f_Restore.ipsw", "version" : "13.4.0" }, { "build" : "22E261", "channel" : "regular", "downloadSize" : 12726460903, "group" : "ventura", "id" : "22E261", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.3.1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterFCS\/fullrestores\/032-66602\/418BC37A-FCD9-400A-B4FA-022A19576CD4\/UniversalMac_13.3.1_22E261_Restore.ipsw", "version" : "13.3.1" }, { "build" : "22E252", "channel" : "regular", "downloadSize" : 12727918028, "group" : "ventura", "id" : "22E252", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterSeed\/fullrestores\/002-75537\/8250FA0E-0962-46D6-8A90-57A390B9FFD7\/UniversalMac_13.3_22E252_Restore.ipsw", "version" : "13.3.0" }, { "build" : "22E5246b", "channel" : "devbeta", "downloadSize" : 12713804212, "group" : "ventura", "id" : "22E5246b", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.3 Developer Beta 4", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterSeed\/fullrestores\/032-63669\/7C0F9BA8-35C0-457F-AF56-6943D58A2CDB\/UniversalMac_13.3_22E5246b_Restore.ipsw", "version" : "13.3.0" }, { "build" : "22E5236f", "channel" : "devbeta", "downloadSize" : 12711164370, "group" : "ventura", "id" : "22E5236f", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.3 Developer Beta 3", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterSeed\/fullrestores\/032-60411\/1DDA996F-B620-4770-8FFE-87AB2043784D\/UniversalMac_13.3_22E5236f_Restore.ipsw", "version" : "13.3.0" }, { "build" : "22E5230e", "channel" : "devbeta", "downloadSize" : 12702079514, "group" : "ventura", "id" : "22E5230e", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.3 Developer Beta 2", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterSeed\/fullrestores\/032-54760\/AE02E378-FD59-474D-93AB-C52617103C72\/UniversalMac_13.3_22E5230e_Restore.ipsw", "version" : "13.3.0" }, { "build" : "22E5219e", "channel" : "devbeta", "downloadSize" : 12690065479, "group" : "ventura", "id" : "22E5219e", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.3 Developer Beta 1", "requirements" : "min_host_13", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterSeed\/fullrestores\/032-01932\/676E0981-4535-4942-A4AE-E14C604CE719\/UniversalMac_13.3_22E5219e_Restore.ipsw", "version" : "13.3.0" }, { "build" : "22D68", "channel" : "regular", "downloadSize" : 12494476408, "group" : "ventura", "id" : "22D68", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.2.1", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterFCS\/fullrestores\/032-48346\/EFF99C1E-C408-4E7A-A448-12E1468AF06C\/UniversalMac_13.2.1_22D68_Restore.ipsw", "version" : "13.2.1" }, { "build" : "22D49", "channel" : "regular", "downloadSize" : 12494248692, "group" : "ventura", "id" : "22D49", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.2", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterFCS\/fullrestores\/032-35688\/0350BB21-2B4B-4850-BF77-70B830283B28\/UniversalMac_13.2_22D49_Restore.ipsw", "version" : "13.2.0" }, { "build" : "22D5038i", "channel" : "devbeta", "downloadSize" : 12269915297, "group" : "ventura", "id" : "22D5038i", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.2 Developer Beta 2", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterSeed\/fullrestores\/032-33181\/62ECE236-5806-4136-AD08-EDC026FD80A5\/UniversalMac_13.2_22D5038i_Restore.ipsw", "version" : "13.2.0" }, { "build" : "22D5027d", "channel" : "devbeta", "downloadSize" : 12278161108, "group" : "ventura", "id" : "22D5027d", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.2 Developer Beta 1", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2023WinterSeed\/fullrestores\/032-12640\/6B472BA3-E678-4251-92D1-7AA23B66F53E\/UniversalMac_13.2_22D5027d_Restore.ipsw", "version" : "13.2.0" }, { "build" : "22C65", "channel" : "regular", "downloadSize" : 12279576859, "group" : "ventura", "id" : "22C65", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.1", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallFCS\/fullrestores\/012-60270\/0A7F49BA-FC31-4AD9-8E45-49B1FB9128A6\/UniversalMac_13.1_22C65_Restore.ipsw", "version" : "13.1.0" }, { "build" : "22C5059b", "channel" : "devbeta", "downloadSize" : 12261527012, "group" : "ventura", "id" : "22C5059b", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.1 Developer Beta 4", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallSeed\/fullrestores\/032-08112\/957EA73A-7C95-4B3C-B99C-2C2C47555832\/UniversalMac_13.1_22C5059b_Restore.ipsw", "version" : "13.1.0" }, { "build" : "22C5050e", "channel" : "devbeta", "downloadSize" : 12258951167, "group" : "ventura", "id" : "22C5050e", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.1 Developer Beta 3", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallSeed\/fullrestores\/032-06252\/946CBF92-8F27-49B1-A692-81F54C73D2F0\/UniversalMac_13.1_22C5050e_Restore.ipsw", "version" : "13.1.0" }, { "build" : "22C5044e", "channel" : "devbeta", "downloadSize" : 11918902102, "group" : "ventura", "id" : "22C5044e", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.1 Developer Beta 2", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallSeed\/fullrestores\/032-02019\/670C9BA6-67EB-4AE6-A02E-88976F6F3118\/UniversalMac_13.1_22C5044e_Restore.ipsw", "version" : "13.1.0" }, { "build" : "22C5033e", "channel" : "devbeta", "downloadSize" : 11886880578, "group" : "ventura", "id" : "22C5033e", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.1 Developer Beta 1", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallSeed\/fullrestores\/012-82062\/10E6B723-51B8-4B2C-BA3B-12A18ED4E719\/UniversalMac_13.1_22C5033e_Restore.ipsw", "version" : "13.1.0" }, { "build" : "22A400", "channel" : "regular", "downloadSize" : 12197932579, "group" : "ventura", "id" : "22A400", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0.1", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallFCS\/fullrestores\/012-93802\/A7270B0F-05F8-43D1-A9AD-40EF5699E82C\/UniversalMac_13.0.1_22A400_Restore.ipsw", "version" : "13.0.1" }, { "build" : "22A380", "channel" : "regular", "downloadSize" : 12197669257, "group" : "ventura", "id" : "22A380", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallFCS\/fullrestores\/012-92188\/2C38BCD1-2BFF-4A10-B358-94E8E28BE805\/UniversalMac_13.0_22A380_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A379", "channel" : "devbeta", "downloadSize" : 12197156665, "group" : "ventura", "id" : "22A379", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 RC", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallFCS\/fullrestores\/071-08994\/1118ADF4-1CC9-4554-9333-B1F64CF0C820\/UniversalMac_13.0_22A379_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5373b", "channel" : "devbeta", "downloadSize" : 12173276873, "group" : "ventura", "id" : "22A5373b", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 Developer Beta 11", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-84563\/2FC38C63-3213-4BB6-8E41-2B066332CBE6\/UniversalMac_13.0_22A5373b_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5365d", "channel" : "devbeta", "downloadSize" : 12175865747, "group" : "ventura", "id" : "22A5365d", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 Developer Beta 10", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-83054\/16ECAA12-3A1B-4663-B49B-B1563ECD4314\/UniversalMac_13.0_22A5365d_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5358e", "channel" : "devbeta", "downloadSize" : 12211984012, "group" : "ventura", "id" : "22A5358e", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 Developer Beta 9", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-71790\/AF5A04A6-FF20-44C1-9BFF-43081BDB4D8C\/UniversalMac_13.0_22A5358e_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5352e", "channel" : "devbeta", "downloadSize" : 12202491122, "group" : "ventura", "id" : "22A5352e", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 Developer Beta 8", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-70113\/6F1F08B7-9A1B-48A9-93DB-55EE21121C87\/UniversalMac_13.0_22A5352e_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5342f", "channel" : "devbeta", "downloadSize" : 12036111685, "group" : "ventura", "id" : "22A5342f", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 Developer Beta 7", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-66750\/108EF06D-FBEE-4910-BA83-56A5C9B54110\/UniversalMac_13.0_22A5342f_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5331f", "channel" : "devbeta", "downloadSize" : 12020352498, "group" : "ventura", "id" : "22A5331f", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 Developer Beta 6", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-61458\/80300AD0-69E5-4429-AE3E-A936CA83B5FC\/UniversalMac_13.0_22A5331f_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5321d", "channel" : "devbeta", "downloadSize" : 12013062572, "group" : "ventura", "id" : "22A5321d", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 Developer Beta 5", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-51397\/8EF0874D-388A-4F62-B58A-89F968DD3082\/UniversalMac_13.0_22A5321d_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5311f", "channel" : "devbeta", "downloadSize" : 12177142220, "group" : "ventura", "id" : "22A5311f", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 Developer Beta 4", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-43316\/6CE4D83A-E44C-4DD1-B47F-DE168355662E\/UniversalMac_13.0_22A5311f_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5295i", "channel" : "devbeta", "downloadSize" : 12110805518, "group" : "ventura", "id" : "22A5295i", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 Developer Beta 3 (22A5295i)", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-38309\/6EDC76A0-4432-4C64-83C5-F43C885A75D6\/UniversalMac_13.0_22A5295i_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5295h", "channel" : "devbeta", "downloadSize" : 12109750298, "group" : "ventura", "id" : "22A5295h", "mobileDeviceMinVersion" : "1400.0.0", "name" : "macOS 13.0 Developer Beta 3", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-34274\/130176F5-C4CB-4664-A2F0-F29CA1281694\/UniversalMac_13.0_22A5295h_Restore.ipsw", "version" : "13.0.0" }, { "build" : "22A5286j", "channel" : "devbeta", "downloadSize" : 12103954377, "group" : "ventura", "id" : "22A5286j", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 13.0 Developer Beta 2", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerSeed\/fullrestores\/012-30346\/9DD787A7-044B-4650-86D4-84E80B6B9C36\/UniversalMac_13.0_22A5286j_Restore.ipsw", "version" : "13.0.0" }, { "build" : "21G217", "channel" : "regular", "downloadSize" : 14086558415, "group" : "monterey", "id" : "21G217", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.6.1", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallFCS\/fullrestores\/012-66032\/8D8D90C6-A876-4FFF-BBF4-D158939B3841\/UniversalMac_12.6.1_21G217_Restore.ipsw", "version" : "12.6.1" }, { "build" : "21G115", "channel" : "regular", "downloadSize" : 14081739387, "group" : "monterey", "id" : "21G115", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.6", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallFCS\/fullrestores\/012-40537\/0EC7C669-13E9-49FB-BD64-9EECC1D174B2\/UniversalMac_12.6_21G115_Restore.ipsw", "version" : "12.6.0" }, { "build" : "21G83", "channel" : "regular", "downloadSize" : 14088266331, "group" : "monterey", "id" : "21G83", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.5.1", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerFCS\/fullrestores\/012-51674\/A7019DDB-3355-470F-A355-4162A187AB6C\/UniversalMac_12.5.1_21G83_Restore.ipsw", "version" : "12.5.1" }, { "build" : "21G72", "channel" : "regular", "downloadSize" : 14084058716, "group" : "monterey", "id" : "21G72", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.5", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerFCS\/fullrestores\/012-42731\/BD9917E0-262C-41C5-A69F-AC316A534A39\/UniversalMac_12.5_21G72_Restore.ipsw", "version" : "12.5.0" }, { "build" : "21G69", "channel" : "devbeta", "downloadSize" : 14085080132, "group" : "monterey", "id" : "21G69", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.5 RC", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SummerFCS\/fullrestores\/012-40368\/5DD0A524-140A-46AF-91ED-5F28EA9DEC01\/UniversalMac_12.5_21G69_Restore.ipsw", "version" : "12.5.0" }, { "build" : "21G5063a", "channel" : "devbeta", "downloadSize" : 13938297123, "group" : "monterey", "id" : "21G5063a", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.5 Developer Beta 5", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallSeed\/fullrestores\/012-36748\/52342C55-6598-4A86-AAB8-8901145792C8\/UniversalMac_12.5_21G5063a_Restore.ipsw", "version" : "12.5.0" }, { "build" : "21G5056b", "channel" : "devbeta", "downloadSize" : 13934172302, "group" : "monterey", "id" : "21G5056b", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.5 Developer Beta 4", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallSeed\/fullrestores\/012-26441\/AE0AC638-2773-49D3-BF84-950B10BF39E9\/UniversalMac_12.5_21G5056b_Restore.ipsw", "version" : "12.5.0" }, { "build" : "21G5046c", "channel" : "devbeta", "downloadSize" : 13868797741, "group" : "monterey", "id" : "21G5046c", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.5 Developer Beta 3", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallSeed\/fullrestores\/012-18271\/FFF202B2-E4B6-4A3E-9681-42A0F3F81B11\/UniversalMac_12.5_21G5046c_Restore.ipsw", "version" : "12.5.0" }, { "build" : "21G5037d", "channel" : "devbeta", "downloadSize" : 13829637373, "group" : "monterey", "id" : "21G5037d", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.5 Developer Beta 2", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallSeed\/fullrestores\/012-10648\/1CC63FC5-5A22-4A5A-9A7B-C19C8C4A6731\/UniversalMac_12.5_21G5037d_Restore.ipsw", "version" : "12.5.0" }, { "build" : "21G5027d", "channel" : "devbeta", "downloadSize" : 13831129170, "group" : "monterey", "id" : "21G5027d", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.5 Developer Beta 1", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022FallSeed\/fullrestores\/002-93712\/5F234425-6096-43FC-B518-1E9D7B4D0254\/UniversalMac_12.5_21G5027d_Restore.ipsw", "version" : "12.5.0" }, { "build" : "21F79", "channel" : "regular", "downloadSize" : 13837340777, "group" : "monterey", "id" : "21F79", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.4", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SpringFCS\/fullrestores\/012-06874\/9CECE956-D945-45E2-93E9-4FFDC81BB49A\/UniversalMac_12.4_21F79_Restore.ipsw", "version" : "12.4.0" }, { "build" : "21F5071b", "channel" : "devbeta", "downloadSize" : 13819236155, "group" : "monterey", "id" : "21F5071b", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.4 Developer Beta 4", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SpringSeed\/fullrestores\/002-95106\/0F7A6388-C4B5-4B8E-B8B2-F62C030699D0\/UniversalMac_12.4_21F5071b_Restore.ipsw", "version" : "12.4.0" }, { "build" : "21F5063e", "channel" : "devbeta", "downloadSize" : 13818052632, "group" : "monterey", "id" : "21F5063e", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.4 Developer Beta 3", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SpringSeed\/fullrestores\/002-90009\/DA6BD192-1698-48B3-AB6D-9D3A045ED1B1\/UniversalMac_12.4_21F5063e_Restore.ipsw", "version" : "12.4.0" }, { "build" : "21F5058e", "channel" : "devbeta", "downloadSize" : 13832392408, "group" : "monterey", "id" : "21F5058e", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.4 Developer Beta 2", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SpringSeed\/fullrestores\/002-87587\/BC2EBE80-F0F4-4B56-BCDC-340E0AD8E985\/UniversalMac_12.4_21F5058e_Restore.ipsw", "version" : "12.4.0" }, { "build" : "21F5048e", "channel" : "devbeta", "downloadSize" : 13812557221, "group" : "monterey", "id" : "21F5048e", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.4 Developer Beta 1", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SpringSeed\/fullrestores\/002-85721\/A21FF659-8493-4A16-A989-2C3141F48D8C\/UniversalMac_12.4_21F5048e_Restore.ipsw", "version" : "12.4.0" }, { "build" : "21E258", "channel" : "regular", "downloadSize" : 13875260060, "group" : "monterey", "id" : "21E258", "mobileDeviceMinVersion" : "1351.0.0", "name" : "macOS 12.3.1", "requirements" : "min_host_12", "url" : "https:\/\/updates.cdn-apple.com\/2022SpringFCS\/fullrestores\/002-79219\/851BEDF0-19DB-4040-B765-0F4089D1530D\/UniversalMac_12.3.1_21E258_Restore.ipsw", "version" : "12.3.1" } ] } ================================================ FILE: data/linux_v1.json ================================================ { "apiVersion": 1, "channels": [ { "id": "stable", "name": "Stable", "note": "Public, stable releases.", "icon": "checkmark.seal" }, { "id": "daily", "name": "Daily", "note": "Daily builds meant for testing.", "icon": "wrench.and.screwdriver" } ], "groups": [ { "id": "ubuntu", "name": "Ubuntu", "majorVersion": "22.0", "minHostVersion": "13.0" } ], "restoreImages": [ { "group": "ubuntu", "name": "Jammy Jellyfish Daily Build", "build": "22.04.3 LTS", "url": "https://cdimage.ubuntu.com/jammy/daily-live/current/jammy-desktop-arm64.iso", "channel": "daily" } ] } ================================================ FILE: data/linux_v2.json ================================================ { "apiVersion" : 2, "channels" : [ { "icon" : "checkmark.seal", "id" : "regular", "name" : "Release", "note" : "Public, stable releases." }, { "icon": "checkmark.seal", "id": "lts", "name": "LTS", "note": "Long-term support releases" }, { "icon" : "wrench.and.screwdriver", "id" : "daily", "name" : "Daily", "note" : "Daily builds meant for testing." } ], "features" : [ { "id" : "file_sharing", "minVersionGuest" : "0.0.0", "minVersionHost" : "0.0.0", "name" : "File sharing", "unsupportedPlatform" : true }, { "id" : "guest_app", "minVersionGuest" : "0.0.0", "minVersionHost" : "0.0.0", "name" : "VirtualBuddyGuest app", "unsupportedPlatform" : true }, { "id" : "trackpad", "minVersionGuest" : "0.0.0", "minVersionHost" : "0.0.0", "name" : "Trackpad", "unsupportedPlatform" : true }, { "id" : "mac_keyboard", "minVersionGuest" : "0.0.0", "minVersionHost" : "0.0.0", "name" : "Mac keyboard", "unsupportedPlatform" : true }, { "id" : "state_restoration", "minVersionGuest" : "0.0.0", "minVersionHost" : "0.0.0", "name" : "State restoration", "unsupportedPlatform" : true }, { "id" : "display_resize", "minVersionGuest" : "0.0.0", "minVersionHost" : "0.0.0", "name" : "Automatic display configuration", "unsupportedPlatform" : true }, { "id" : "rosetta_sharing", "minVersionGuest" : "0.0.0", "minVersionHost" : "14.0.0", "name" : "Rosetta Sharing", "unsupportedPlatform" : false } ], "deviceSupportVersions": [ ], "groups" : [ { "darkImage" : { "id" : "jammyjellyfish", "thumbnail" : { "blurHash" : "UIE0281ds7Ey1a$MWYaysSjvoMjsWoWUo2jv", "height" : 405, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/jammyjellyfish-dark-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/jammyjellyfish-dark.heic" }, "id" : "ubuntu", "image" : { "id" : "jammyjellyfish", "thumbnail" : { "blurHash" : "UIE0281ds7Ey1a$MWYaysSjvoMjsWoWUo2jv", "height" : 405, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/jammyjellyfish-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/jammyjellyfish.heic" }, "majorVersion" : "22.0.0", "name" : "Ubuntu" }, { "darkImage" : { "id" : "debian", "thumbnail" : { "blurHash" : "U96%Xrw$I%oLS,NWR#ohRyR}bGohW8s?oNWU", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/debian-dark-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/debian-dark.heic" }, "id" : "debian", "image" : { "id" : "debian", "thumbnail" : { "blurHash" : "U96%Xrw$I%oLS,NWR#ohRyR}bGohW8s?oNWU", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/debian-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/debian.heic" }, "majorVersion" : "12.0.0", "name" : "Debian" }, { "darkImage" : { "id" : "fedora", "thumbnail" : { "blurHash" : "UQ6+b#kCMcflyGaeR4f5R4ayozj[Mbofx^a}", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/fedora-dark-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/fedora-dark.heic" }, "id" : "fedora", "image" : { "id" : "fedora", "thumbnail" : { "blurHash" : "UE6.Evf5D6j[*0ahMdf8Htf*xufQD5kC%fbG", "height" : 720, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/fedora-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/fedora.heic" }, "majorVersion" : "41.0.0", "name" : "Fedora" }, { "darkImage" : { "id" : "kali", "thumbnail" : { "blurHash" : "UD7CB$.AnftURMkEo$ozMwRPRjawWBa#f-ae", "height" : 405, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/kali-dark-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/kali-dark.heic" }, "id" : "kali", "image" : { "id" : "kali", "thumbnail" : { "blurHash" : "UD7CB$.AnftURMkEo$ozMwRPRjawWBa#f-ae", "height" : 405, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/kali-thumbnail.heic", "width" : 720 }, "url" : "https:\/\/api.virtualbuddy.app\/v2\/images\/kali.heic" }, "majorVersion" : "2025.0.0", "name" : "Kali" } ], "minAppVersion" : "2.0.0", "requirementSets" : [ { "id" : "min_host_13", "minCPUCount" : 1, "minMemorySizeMB" : 1024, "minVersionHost" : "13.0.0" } ], "restoreImages" : [ { "build": "25.10", "channel": "regular", "downloadSize": 4981665792, "group": "ubuntu", "id": "25.10_Desktop", "mobileDeviceMinVersion": "0.0.0", "name": "Ubuntu Desktop 25.10", "requirements": "min_host_13", "url": "https://cdimage.ubuntu.com/releases/25.10/release/ubuntu-25.10-desktop-arm64.iso", "version": "25.10" }, { "build" : "24.04.4 LTS", "channel" : "lts", "downloadSize" : 3059724288, "group" : "ubuntu", "id" : "24.04.4 LTS", "mobileDeviceMinVersion" : "0.0.0", "name" : "Ubuntu Server 24.04.4 LTS", "requirements" : "min_host_13", "url" : "https://cdimage.ubuntu.com/releases/24.04/release/ubuntu-24.04.4-live-server-arm64.iso", "version" : "24.04.4" }, { "build": "25.10", "channel": "regular", "downloadSize": 2394763264, "group": "ubuntu", "id": "25.10_Server", "mobileDeviceMinVersion": "0.0.0", "name": "Ubuntu Server 25.10", "requirements": "min_host_13", "url": "https://cdimage.ubuntu.com/releases/25.10/release/ubuntu-25.10-live-server-arm64.iso", "version": "25.10" }, { "build" : "42.1.1", "channel" : "regular", "downloadSize" : 1006553088, "group" : "fedora", "id" : "42.1.1", "mobileDeviceMinVersion" : "0.0.0", "name" : "Fedora 42.1.1 Net Install", "requirements" : "min_host_13", "url" : "https:\/\/download.fedoraproject.org\/pub\/fedora\/linux\/releases\/42\/Everything\/aarch64\/iso\/Fedora-Everything-netinst-aarch64-42-1.1.iso", "version" : "42.1.1" }, { "build" : "2025.2", "channel" : "regular", "downloadSize" : 3767808000, "group" : "kali", "id" : "2025.2", "mobileDeviceMinVersion" : "0.0.0", "name" : "Kali Linux 2025.2 Full", "requirements" : "min_host_13", "url" : "https:\/\/cdimage.kali.org\/kali-2025.2\/kali-linux-2025.2-installer-arm64.iso", "version" : "2025.2.0" }, { "build": "12.11", "channel": "regular", "downloadSize": 3993284608, "group": "debian", "id": "12.11", "mobileDeviceMinVersion": "0.0.0", "name": "Debian 12.11", "requirements": "min_host_13", "url": "https://cdimage.debian.org/debian-cd/current/arm64/iso-dvd/debian-12.11.0-arm64-DVD-1.iso", "version": "12.11" } ] }