├── .gitignore ├── LICENSE ├── README.md ├── virtualOS.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── virtualOS.xcscheme └── virtualOS ├── AppDelegate.swift ├── Extension ├── OperatingSystemVersion+String.swift ├── UInt64+Byte.swift ├── URL+Paths.swift └── UserDefaults+Settings.swift ├── Model ├── Bookmark.swift ├── Bundle.swift ├── Constants.swift ├── FileModel.swift ├── Logger.swift ├── MainViewModel.swift ├── ParametersViewDataSource.swift ├── ParametersViewDelegate.swift ├── TableViewDataSource.swift └── TextFieldDelegate.swift ├── Resources ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── virtual os app icon-1024.png │ │ ├── virtual os app icon-128.png │ │ ├── virtual os app icon-16.png │ │ ├── virtual os app icon-256.png │ │ ├── virtual os app icon-32.png │ │ ├── virtual os app icon-512.png │ │ └── virtual os app icon-64.png │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist └── virtualOS.entitlements ├── RestoreImage ├── RestoreImageDownload.swift └── RestoreImageInstall.swift ├── VM ├── MacPlatformConfiguration.swift ├── VMConfiguration.swift └── VMParameters.swift └── ViewController ├── AlertController.swift ├── MainViewController.swift ├── ProgressViewController.swift ├── RestoreImageViewController.swift ├── SettingsViewController.swift └── VMViewController.swift /.gitignore: -------------------------------------------------------------------------------- 1 | virtualOS.xcodeproj/xcuserdata 2 | *.xcuserdatad 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022-2023 Jahn Bertsch 2 | Copyright 2021 Khaos Tian 3 | 4 | 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 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | 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. 9 | See the License for the specific language governing permissions and limitations under the License. 10 | 11 | – 12 | 13 | Copyright © 2021 Apple Inc. 14 | 15 | 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: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | 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. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # virtualOS 2 | 3 | Run a virtual macOS machine on your Apple Silicon computer. 4 | 5 | On first start, the latest macOS restore image can be downloaded from Apple servers. After installation has finished, you can start using the virtual machine by performing the initial operating system (OS) setup. 6 | 7 | You can configure the following virtual machine parameters: 8 | - CPU count 9 | - RAM 10 | - Screen size 11 | - Shared folder 12 | 13 | To use USB disks, you can set the location where VM files are stored. 14 | 15 | Unlike other apps on the AppStore, no In-App purchases are required for managing multiple virtual machines, setting CPU count or the amount of RAM. 16 | 17 | ## Download 18 | 19 | You can download this app from the [macOS AppStore](https://apps.apple.com/us/app/virtualos/id1614659226) 20 | 21 | This application is free and open source software, source code is available at: https://github.com/yep/virtualOS 22 | 23 | Mac and macOS are trademarks of Apple Inc., registered in the U.S. and other countries and regions. 24 | -------------------------------------------------------------------------------- /virtualOS.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXFileReference section */ 10 | 015228552CB27BC100209934 /* virtualOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = virtualOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 11 | 01D610692D95489600FFF92D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 12 | 01D6106A2D95489D00FFF92D /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 13 | 01D6106D2D95580000FFF92D /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; 14 | /* End PBXFileReference section */ 15 | 16 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 17 | 014188A22CD756B200DCD9A0 /* Exceptions for "virtualOS" folder in "virtualOS" target */ = { 18 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 19 | membershipExceptions = ( 20 | Resources/Info.plist, 21 | ); 22 | target = 015228542CB27BC100209934 /* virtualOS */; 23 | }; 24 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 25 | 26 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 27 | 015228572CB27BC100209934 /* virtualOS */ = { 28 | isa = PBXFileSystemSynchronizedRootGroup; 29 | exceptions = ( 30 | 014188A22CD756B200DCD9A0 /* Exceptions for "virtualOS" folder in "virtualOS" target */, 31 | ); 32 | path = virtualOS; 33 | sourceTree = ""; 34 | }; 35 | /* End PBXFileSystemSynchronizedRootGroup section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 015228522CB27BC100209934 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 0152284C2CB27BC100209934 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 01D6106D2D95580000FFF92D /* .gitignore */, 52 | 01D610692D95489600FFF92D /* README.md */, 53 | 01D6106A2D95489D00FFF92D /* LICENSE */, 54 | 015228572CB27BC100209934 /* virtualOS */, 55 | 015228562CB27BC100209934 /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | 015228562CB27BC100209934 /* Products */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 015228552CB27BC100209934 /* virtualOS.app */, 63 | ); 64 | name = Products; 65 | sourceTree = ""; 66 | }; 67 | /* End PBXGroup section */ 68 | 69 | /* Begin PBXNativeTarget section */ 70 | 015228542CB27BC100209934 /* virtualOS */ = { 71 | isa = PBXNativeTarget; 72 | buildConfigurationList = 015228642CB27BC200209934 /* Build configuration list for PBXNativeTarget "virtualOS" */; 73 | buildPhases = ( 74 | 015228512CB27BC100209934 /* Sources */, 75 | 015228522CB27BC100209934 /* Frameworks */, 76 | 015228532CB27BC100209934 /* Resources */, 77 | ); 78 | buildRules = ( 79 | ); 80 | dependencies = ( 81 | ); 82 | fileSystemSynchronizedGroups = ( 83 | 015228572CB27BC100209934 /* virtualOS */, 84 | ); 85 | name = virtualOS; 86 | packageProductDependencies = ( 87 | ); 88 | productName = virtualOS; 89 | productReference = 015228552CB27BC100209934 /* virtualOS.app */; 90 | productType = "com.apple.product-type.application"; 91 | }; 92 | /* End PBXNativeTarget section */ 93 | 94 | /* Begin PBXProject section */ 95 | 0152284D2CB27BC100209934 /* Project object */ = { 96 | isa = PBXProject; 97 | attributes = { 98 | BuildIndependentTargetsInParallel = 1; 99 | LastSwiftUpdateCheck = 1600; 100 | LastUpgradeCheck = 1610; 101 | TargetAttributes = { 102 | 015228542CB27BC100209934 = { 103 | CreatedOnToolsVersion = 16.0; 104 | }; 105 | }; 106 | }; 107 | buildConfigurationList = 015228502CB27BC100209934 /* Build configuration list for PBXProject "virtualOS" */; 108 | developmentRegion = en; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | Base, 113 | ); 114 | mainGroup = 0152284C2CB27BC100209934; 115 | minimizedProjectReferenceProxies = 1; 116 | preferredProjectObjectVersion = 77; 117 | productRefGroup = 015228562CB27BC100209934 /* Products */; 118 | projectDirPath = ""; 119 | projectRoot = ""; 120 | targets = ( 121 | 015228542CB27BC100209934 /* virtualOS */, 122 | ); 123 | }; 124 | /* End PBXProject section */ 125 | 126 | /* Begin PBXResourcesBuildPhase section */ 127 | 015228532CB27BC100209934 /* Resources */ = { 128 | isa = PBXResourcesBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXResourcesBuildPhase section */ 135 | 136 | /* Begin PBXSourcesBuildPhase section */ 137 | 015228512CB27BC100209934 /* Sources */ = { 138 | isa = PBXSourcesBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | /* End PBXSourcesBuildPhase section */ 145 | 146 | /* Begin XCBuildConfiguration section */ 147 | 015228622CB27BC200209934 /* Debug */ = { 148 | isa = XCBuildConfiguration; 149 | buildSettings = { 150 | ALWAYS_SEARCH_USER_PATHS = NO; 151 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 152 | CLANG_ANALYZER_NONNULL = YES; 153 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 154 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 155 | CLANG_ENABLE_MODULES = YES; 156 | CLANG_ENABLE_OBJC_ARC = YES; 157 | CLANG_ENABLE_OBJC_WEAK = YES; 158 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 159 | CLANG_WARN_BOOL_CONVERSION = YES; 160 | CLANG_WARN_COMMA = YES; 161 | CLANG_WARN_CONSTANT_CONVERSION = YES; 162 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 163 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 164 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 165 | CLANG_WARN_EMPTY_BODY = YES; 166 | CLANG_WARN_ENUM_CONVERSION = YES; 167 | CLANG_WARN_INFINITE_RECURSION = YES; 168 | CLANG_WARN_INT_CONVERSION = YES; 169 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 170 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 171 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 172 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 173 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 174 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 175 | CLANG_WARN_STRICT_PROTOTYPES = YES; 176 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 177 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 178 | CLANG_WARN_UNREACHABLE_CODE = YES; 179 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 180 | COPY_PHASE_STRIP = NO; 181 | DEAD_CODE_STRIPPING = YES; 182 | DEBUG_INFORMATION_FORMAT = dwarf; 183 | ENABLE_STRICT_OBJC_MSGSEND = YES; 184 | ENABLE_TESTABILITY = YES; 185 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 186 | GCC_C_LANGUAGE_STANDARD = gnu17; 187 | GCC_DYNAMIC_NO_PIC = NO; 188 | GCC_NO_COMMON_BLOCKS = YES; 189 | GCC_OPTIMIZATION_LEVEL = 0; 190 | GCC_PREPROCESSOR_DEFINITIONS = ( 191 | "DEBUG=1", 192 | "$(inherited)", 193 | ); 194 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 195 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 196 | GCC_WARN_UNDECLARED_SELECTOR = YES; 197 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 198 | GCC_WARN_UNUSED_FUNCTION = YES; 199 | GCC_WARN_UNUSED_VARIABLE = YES; 200 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 201 | MACOSX_DEPLOYMENT_TARGET = 15.0; 202 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 203 | MTL_FAST_MATH = YES; 204 | ONLY_ACTIVE_ARCH = YES; 205 | SDKROOT = macosx; 206 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 207 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 208 | }; 209 | name = Debug; 210 | }; 211 | 015228632CB27BC200209934 /* Release */ = { 212 | isa = XCBuildConfiguration; 213 | buildSettings = { 214 | ALWAYS_SEARCH_USER_PATHS = NO; 215 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 216 | CLANG_ANALYZER_NONNULL = YES; 217 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 218 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 219 | CLANG_ENABLE_MODULES = YES; 220 | CLANG_ENABLE_OBJC_ARC = YES; 221 | CLANG_ENABLE_OBJC_WEAK = YES; 222 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 223 | CLANG_WARN_BOOL_CONVERSION = YES; 224 | CLANG_WARN_COMMA = YES; 225 | CLANG_WARN_CONSTANT_CONVERSION = YES; 226 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 227 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 228 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 229 | CLANG_WARN_EMPTY_BODY = YES; 230 | CLANG_WARN_ENUM_CONVERSION = YES; 231 | CLANG_WARN_INFINITE_RECURSION = YES; 232 | CLANG_WARN_INT_CONVERSION = YES; 233 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 234 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 235 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 236 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 237 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 238 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 239 | CLANG_WARN_STRICT_PROTOTYPES = YES; 240 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 241 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 242 | CLANG_WARN_UNREACHABLE_CODE = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | COPY_PHASE_STRIP = NO; 245 | DEAD_CODE_STRIPPING = YES; 246 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 247 | ENABLE_NS_ASSERTIONS = NO; 248 | ENABLE_STRICT_OBJC_MSGSEND = YES; 249 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 250 | GCC_C_LANGUAGE_STANDARD = gnu17; 251 | GCC_NO_COMMON_BLOCKS = YES; 252 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 253 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 254 | GCC_WARN_UNDECLARED_SELECTOR = YES; 255 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 256 | GCC_WARN_UNUSED_FUNCTION = YES; 257 | GCC_WARN_UNUSED_VARIABLE = YES; 258 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 259 | MACOSX_DEPLOYMENT_TARGET = 15.0; 260 | MTL_ENABLE_DEBUG_INFO = NO; 261 | MTL_FAST_MATH = YES; 262 | SDKROOT = macosx; 263 | SWIFT_COMPILATION_MODE = wholemodule; 264 | }; 265 | name = Release; 266 | }; 267 | 015228652CB27BC200209934 /* Debug */ = { 268 | isa = XCBuildConfiguration; 269 | buildSettings = { 270 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 271 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 272 | CODE_SIGN_ENTITLEMENTS = virtualOS/Resources/virtualOS.entitlements; 273 | CODE_SIGN_IDENTITY = "Apple Development"; 274 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 275 | CODE_SIGN_STYLE = Automatic; 276 | COMBINE_HIDPI_IMAGES = YES; 277 | CURRENT_PROJECT_VERSION = 20; 278 | DEAD_CODE_STRIPPING = YES; 279 | DEVELOPMENT_TEAM = 2AD47BTDQ6; 280 | ENABLE_HARDENED_RUNTIME = YES; 281 | GENERATE_INFOPLIST_FILE = YES; 282 | INFOPLIST_FILE = virtualOS/Resources/Info.plist; 283 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 284 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 285 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 286 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 287 | LD_RUNPATH_SEARCH_PATHS = ( 288 | "$(inherited)", 289 | "@executable_path/../Frameworks", 290 | ); 291 | MARKETING_VERSION = 2.1; 292 | PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; 293 | PRODUCT_NAME = "$(TARGET_NAME)"; 294 | PROVISIONING_PROFILE_SPECIFIER = ""; 295 | SWIFT_EMIT_LOC_STRINGS = YES; 296 | SWIFT_VERSION = 5.0; 297 | }; 298 | name = Debug; 299 | }; 300 | 015228662CB27BC200209934 /* Release */ = { 301 | isa = XCBuildConfiguration; 302 | buildSettings = { 303 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 304 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 305 | CODE_SIGN_ENTITLEMENTS = virtualOS/Resources/virtualOS.entitlements; 306 | CODE_SIGN_IDENTITY = "Apple Development"; 307 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 308 | CODE_SIGN_STYLE = Automatic; 309 | COMBINE_HIDPI_IMAGES = YES; 310 | CURRENT_PROJECT_VERSION = 20; 311 | DEAD_CODE_STRIPPING = YES; 312 | DEVELOPMENT_TEAM = 2AD47BTDQ6; 313 | ENABLE_HARDENED_RUNTIME = YES; 314 | GENERATE_INFOPLIST_FILE = YES; 315 | INFOPLIST_FILE = virtualOS/Resources/Info.plist; 316 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 317 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 318 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 319 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 320 | LD_RUNPATH_SEARCH_PATHS = ( 321 | "$(inherited)", 322 | "@executable_path/../Frameworks", 323 | ); 324 | MARKETING_VERSION = 2.1; 325 | PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; 326 | PRODUCT_NAME = "$(TARGET_NAME)"; 327 | PROVISIONING_PROFILE_SPECIFIER = ""; 328 | SWIFT_EMIT_LOC_STRINGS = YES; 329 | SWIFT_VERSION = 5.0; 330 | }; 331 | name = Release; 332 | }; 333 | /* End XCBuildConfiguration section */ 334 | 335 | /* Begin XCConfigurationList section */ 336 | 015228502CB27BC100209934 /* Build configuration list for PBXProject "virtualOS" */ = { 337 | isa = XCConfigurationList; 338 | buildConfigurations = ( 339 | 015228622CB27BC200209934 /* Debug */, 340 | 015228632CB27BC200209934 /* Release */, 341 | ); 342 | defaultConfigurationIsVisible = 0; 343 | defaultConfigurationName = Release; 344 | }; 345 | 015228642CB27BC200209934 /* Build configuration list for PBXNativeTarget "virtualOS" */ = { 346 | isa = XCConfigurationList; 347 | buildConfigurations = ( 348 | 015228652CB27BC200209934 /* Debug */, 349 | 015228662CB27BC200209934 /* Release */, 350 | ); 351 | defaultConfigurationIsVisible = 0; 352 | defaultConfigurationName = Release; 353 | }; 354 | /* End XCConfigurationList section */ 355 | }; 356 | rootObject = 0152284D2CB27BC100209934 /* Project object */; 357 | } 358 | -------------------------------------------------------------------------------- /virtualOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /virtualOS.xcodeproj/xcshareddata/xcschemes/virtualOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /virtualOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Cocoa 10 | 11 | @main 12 | final class AppDelegate: NSObject, NSApplicationDelegate { 13 | func applicationDidFinishLaunching(_ aNotification: Notification) { 14 | // Insert code here to initialize your application 15 | } 16 | 17 | func applicationWillTerminate(_ aNotification: Notification) { 18 | // Insert code here to tear down your application 19 | } 20 | 21 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 22 | return true 23 | } 24 | 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /virtualOS/Extension/OperatingSystemVersion+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OsVersion.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Virtualization 10 | 11 | #if arch(arm64) 12 | 13 | extension VZMacOSRestoreImage { 14 | var operatingSystemVersionString: String { 15 | return "macOS \(operatingSystemVersion.majorVersion).\(operatingSystemVersion.minorVersion).\(operatingSystemVersion.patchVersion) (Build \(buildVersion))" 16 | } 17 | } 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /virtualOS/Extension/UInt64+Byte.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UInt+Byte.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UInt64 { 12 | func bytesToGigabytes() -> UInt64 { 13 | return self / (1024 * 1024 * 1024) 14 | } 15 | 16 | func gigabytesToBytes() -> UInt64 { 17 | return self * 1024 * 1024 * 1024 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /virtualOS/Extension/URL+Paths.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Paths.swift 3 | // virtualOS 4 | // 5 | 6 | import Foundation 7 | 8 | extension URL { 9 | static let basePath = NSHomeDirectory() + "/Documents" 10 | static let restoreImageURL = URL(fileURLWithPath: basePath + "/RestoreImage.ipsw") 11 | static let bundleName = "virtualOS.bundle/" 12 | static let defaultVMBundlePath = basePath + "/\(bundleName)" 13 | 14 | static var baseURL: URL { 15 | return URL(fileURLWithPath: basePath) 16 | } 17 | static var vmBundleURL: URL { 18 | return URL(fileURLWithPath: defaultVMBundlePath) 19 | } 20 | var auxiliaryStorageURL: URL { 21 | return self.appending(path: "AuxiliaryStorage") 22 | } 23 | var hardwareModelURL: URL { 24 | return self.appending(path: "HardwareModel") 25 | } 26 | var diskImageURL: URL { 27 | return self.appending(path: "Disk.img") 28 | } 29 | var machineIdentifierURL: URL { 30 | return self.appending(path: "MachineIdentifier") 31 | } 32 | var parametersURL: URL { 33 | return self.appending(path: "Parameters.txt") 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /virtualOS/Extension/UserDefaults+Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Settings.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UserDefaults { 12 | fileprivate static let diskSizeKey = "diskSize" 13 | fileprivate static let vmFilesDirectoryKey = "vmFilesDirectoryKey" 14 | fileprivate static let vmFilesDirectoryBookmarkData = "vmFilesDirectoryBookmarkData" 15 | 16 | var diskSize: Int { 17 | get { 18 | if object(forKey: Self.diskSizeKey) != nil { 19 | return integer(forKey: Self.diskSizeKey) 20 | } 21 | return 30 // default value 22 | } 23 | set { 24 | set(newValue, forKey: Self.diskSizeKey) 25 | synchronize() 26 | } 27 | } 28 | 29 | var vmFilesDirectory: String? { 30 | get { 31 | return string(forKey: Self.vmFilesDirectoryKey) 32 | } 33 | set { 34 | set(newValue, forKey: Self.vmFilesDirectoryKey) 35 | synchronize() 36 | } 37 | } 38 | 39 | var vmFilesDirectoryBookmarkData: Data? { 40 | get { 41 | return data(forKey: Self.vmFilesDirectoryBookmarkData) 42 | } 43 | set { 44 | set(newValue, forKey: Self.vmFilesDirectoryBookmarkData) 45 | synchronize() 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /virtualOS/Model/Bookmark.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Bookmark.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file.. 7 | // 8 | 9 | import Foundation 10 | import OSLog 11 | 12 | struct Bookmark { 13 | static let vmFilesLocation: String = "virtualOS://files" 14 | 15 | fileprivate static var accessedURLs: [String: URL] = [:] 16 | 17 | static func createBookmarkData(fromUrl url: URL) -> Data? { 18 | if let bookmarkData = try? url.bookmarkData(options: .withSecurityScope, relativeTo: nil) { 19 | return bookmarkData 20 | } 21 | return nil 22 | } 23 | 24 | static func startAccess(bookmarkData: Data?, for absoluteURL: String) -> URL? { 25 | var bookmarkDataIsStale = false 26 | if let bookmarkData = bookmarkData, 27 | let bookmarkURL = try? URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &bookmarkDataIsStale), 28 | !bookmarkDataIsStale 29 | { 30 | // stop accessing previous resource 31 | if let previousURL = accessedURLs[absoluteURL], 32 | previousURL != bookmarkURL 33 | { 34 | previousURL.stopAccessingSecurityScopedResource() 35 | } 36 | 37 | if accessedURLs[absoluteURL] != bookmarkURL { 38 | // resource not already accessed, start access 39 | _ = bookmarkURL.startAccessingSecurityScopedResource() 40 | accessedURLs[absoluteURL] = bookmarkURL 41 | } 42 | return bookmarkURL 43 | } 44 | 45 | return nil 46 | } 47 | 48 | static func stopAccess(url: URL) { 49 | url.stopAccessingSecurityScopedResource() 50 | Self.accessedURLs[url.absoluteString] = nil 51 | } 52 | 53 | static func stopAllAccess() { 54 | for (_, accessedURL) in accessedURLs { 55 | accessedURL.stopAccessingSecurityScopedResource() 56 | } 57 | Self.accessedURLs = [:] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /virtualOS/Model/Bundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | 11 | struct VMBundle: Identifiable, Hashable { 12 | var id: String { 13 | return url.absoluteString 14 | } 15 | var url: URL 16 | var name: String { 17 | return url.lastPathComponent.replacingOccurrences(of: ".bundle", with: "") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /virtualOS/Model/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | struct Constants { 12 | static let restoreImageNameLatest = "latest" 13 | static let selectedRestoreImage = "selectedRestoreImage" 14 | static let restoreImageNameSelectedNotification = Notification.Name("restoreImageSelected") 15 | static let didChangeVMLocationNotification = Notification.Name("didChangeVMLocation") 16 | 17 | } 18 | -------------------------------------------------------------------------------- /virtualOS/Model/FileModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileModel.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | 11 | struct FileModel { 12 | var bundleExists: Bool { 13 | return FileManager.default.fileExists(atPath: URL.vmBundleURL.path()) 14 | } 15 | 16 | var restoreImageExists: Bool { 17 | return FileManager.default.fileExists(atPath: URL.restoreImageURL.path) 18 | } 19 | 20 | func getVMBundles() -> [VMBundle] { 21 | var result: [VMBundle] = [] 22 | var hardDiskDirectoryURL: URL 23 | 24 | if let hardDiskDirectoryPath = UserDefaults.standard.vmFilesDirectory as String? { 25 | hardDiskDirectoryURL = URL(fileURLWithPath: hardDiskDirectoryPath) 26 | } else { 27 | hardDiskDirectoryURL = URL.baseURL 28 | } 29 | 30 | if let urls = try? FileManager.default.contentsOfDirectory(at: hardDiskDirectoryURL, includingPropertiesForKeys: nil, options: []) 31 | { 32 | for url in urls { 33 | if url.lastPathComponent.hasSuffix("bundle") { 34 | result.append(VMBundle(url: url)) 35 | } 36 | } 37 | } 38 | return result 39 | } 40 | 41 | func getRestoreImages() -> [String] { 42 | var result: [String] = [] 43 | if let urls = try? FileManager.default.contentsOfDirectory(at: URL.baseURL, includingPropertiesForKeys: nil, options: []) { 44 | for url in urls { 45 | if url.lastPathComponent.hasSuffix("ipsw") { 46 | result.append(url.lastPathComponent) 47 | } 48 | } 49 | } 50 | return result 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /virtualOS/Model/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import OSLog 10 | 11 | extension Logger { 12 | static let shared = Logger.init(subsystem: "com.github.virtualOS", category: "log") 13 | } 14 | -------------------------------------------------------------------------------- /virtualOS/Model/MainViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewModel.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | #if arch(arm64) 12 | 13 | final class MainViewModel { 14 | let tableViewDataSource = TableViewDataSource() 15 | let parametersViewDataSource = ParametersViewDataSource() 16 | let parametersViewDelegate = ParametersViewDelegate() 17 | let textFieldDelegate = TextFieldDelegate() 18 | var vmBundle: VMBundle? 19 | var selectedRow: Int? = 0 20 | var vmParameters: VMParameters? 21 | 22 | init() { 23 | parametersViewDataSource.mainViewModel = self 24 | } 25 | 26 | func storeParametersToDisk() { 27 | if let vmParameters = vmParameters, 28 | let vmBundleUrl = vmBundle?.url 29 | { 30 | vmParameters.writeToDisk(bundleURL: vmBundleUrl) 31 | } 32 | } 33 | 34 | func deleteVM(selection: NSApplication.ModalResponse, vmBundle: VMBundle) { 35 | try? FileManager.default.removeItem(at: vmBundle.url) 36 | if let selectedRow, 37 | selectedRow > tableViewDataSource.rows() - 1 38 | { 39 | self.selectedRow = tableViewDataSource.rows() - 1 // select last table row 40 | } 41 | } 42 | 43 | func set(sharedFolderUrl: URL?) { 44 | var sharedFolderData: Data? = nil 45 | 46 | if let sharedFolderUrl { 47 | sharedFolderData = Bookmark.createBookmarkData(fromUrl: sharedFolderUrl) 48 | if let sharedFolderData { 49 | _ = Bookmark.startAccess(bookmarkData: sharedFolderData, for: sharedFolderUrl.absoluteString) 50 | 51 | if let selectedRow { 52 | let bundle = tableViewDataSource.vmBundle(forRow: selectedRow) 53 | if let bundleURL = bundle?.url { 54 | var vmParameters = VMParameters.readFrom(url: bundleURL) 55 | vmParameters?.sharedFolderURL = sharedFolderUrl 56 | vmParameters?.sharedFolderData = sharedFolderData 57 | vmParameters?.writeToDisk(bundleURL: bundleURL) 58 | self.vmParameters = vmParameters 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /virtualOS/Model/ParametersViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParametersViewDataSource.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | #if arch(arm64) 12 | 13 | final class ParametersViewDataSource: NSObject, NSOutlineViewDataSource { 14 | weak var mainViewModel: MainViewModel? 15 | 16 | func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { 17 | if mainViewModel?.vmParameters != nil { 18 | return 5 19 | } else { 20 | return 0 21 | } 22 | } 23 | 24 | func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { 25 | if let vmParameters = mainViewModel?.vmParameters { 26 | switch index { 27 | case 0: 28 | return ["CPU Count", "\(vmParameters.cpuCount)"] 29 | case 1: 30 | return ["Memory Size (GB)", "\(vmParameters.memorySizeInGB)"] 31 | case 2: 32 | return ["Disk Size (GB)", "\(vmParameters.diskSizeInGB)"] 33 | case 3: 34 | let sharedFolderString = sharedFolderInfo(vmParameters: vmParameters) 35 | return ["Shared Folder", sharedFolderString] 36 | case 4: 37 | return ["Version", "\(vmParameters.version)"] 38 | default: 39 | return ["index \(index)", "value \(index)"] 40 | } 41 | } 42 | 43 | return ["index \(index)", "value \(index)"] 44 | } 45 | 46 | fileprivate func sharedFolderInfo(vmParameters: VMParameters) -> String { 47 | if let sharedFolderURL = vmParameters.sharedFolderURL, 48 | let sharedFolderData = vmParameters.sharedFolderData, 49 | let bookmarkURL = Bookmark.startAccess(bookmarkData: sharedFolderData, for: sharedFolderURL.absoluteString) 50 | { 51 | return bookmarkURL.path() 52 | } 53 | return "No shared folder" 54 | } 55 | } 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /virtualOS/Model/ParametersViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParametersViewDelegate.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | final class ParametersViewDelegate: NSObject, NSOutlineViewDelegate { 12 | func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { 13 | var string: String 14 | 15 | if let identifier = tableColumn?.identifier, 16 | let array = item as? [String] 17 | { 18 | switch identifier.rawValue { 19 | case "AutomaticTableColumnIdentifier.0": 20 | string = array[0] 21 | case "AutomaticTableColumnIdentifier.1": 22 | string = array[1] 23 | default: 24 | string = "default" 25 | 26 | } 27 | } else { 28 | string = "unknown" 29 | } 30 | 31 | return NSTextField(labelWithString: string) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /virtualOS/Model/TableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDataSource.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | final class TableViewDataSource: NSObject, NSTableViewDataSource { 12 | fileprivate let fileModel = FileModel() 13 | 14 | func rows() -> Int { 15 | return fileModel.getVMBundles().count 16 | } 17 | 18 | func numberOfRows(in tableView: NSTableView) -> Int { 19 | return fileModel.getVMBundles().count 20 | } 21 | 22 | func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { 23 | return vmBundle(forRow: row)?.name 24 | } 25 | 26 | func vmBundle(forRow row: Int) -> VMBundle? { 27 | let bundles = fileModel.getVMBundles() 28 | if 0 <= row && row < bundles.count { 29 | return bundles[row] 30 | } else { 31 | return nil 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /virtualOS/Model/TextFieldDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldDelegate.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | final class TextFieldDelegate: NSObject, NSTextFieldDelegate { 12 | var vmBundle: VMBundle? 13 | 14 | func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool { 15 | if let vmBundle = vmBundle, 16 | vmBundle.name != fieldEditor.string 17 | { 18 | let newFilename = "\(fieldEditor.string).bundle" 19 | let newUrl = vmBundle.url.deletingLastPathComponent().appendingPathComponent(newFilename) 20 | 21 | try? FileManager.default.moveItem(at: vmBundle.url, to: newUrl) 22 | } 23 | 24 | return true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "virtual os app icon-16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "virtual os app icon-32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "virtual os app icon-32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "virtual os app icon-64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "virtual os app icon-128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "virtual os app icon-256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "virtual os app icon-256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "virtual os app icon-512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "virtual os app icon-512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "virtual os app icon-1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/8dc21f866edceac44a0639be6b85aa1c96b5807d/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-1024.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/8dc21f866edceac44a0639be6b85aa1c96b5807d/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-128.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/8dc21f866edceac44a0639be6b85aa1c96b5807d/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-16.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/8dc21f866edceac44a0639be6b85aa1c96b5807d/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-256.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/8dc21f866edceac44a0639be6b85aa1c96b5807d/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-32.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/8dc21f866edceac44a0639be6b85aa1c96b5807d/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-512.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yep/virtualOS/8dc21f866edceac44a0639be6b85aa1c96b5807d/virtualOS/Resources/Assets.xcassets/AppIcon.appiconset/virtual os app icon-64.png -------------------------------------------------------------------------------- /virtualOS/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /virtualOS/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /virtualOS/Resources/virtualOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.virtualization 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /virtualOS/RestoreImage/RestoreImageDownload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Download.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Virtualization 10 | import Combine 11 | import OSLog 12 | 13 | #if arch(arm64) 14 | 15 | protocol ProgressDelegate: AnyObject { 16 | func progress(_ progress: Double, progressString: String) 17 | func done(error: Error?) 18 | } 19 | 20 | final class RestoreImageDownload { 21 | weak var delegate: ProgressDelegate? 22 | fileprivate var observation: NSKeyValueObservation? 23 | fileprivate var downloadTask: URLSessionDownloadTask? 24 | fileprivate var downloading = true 25 | 26 | deinit { 27 | observation?.invalidate() 28 | } 29 | 30 | func fetch() { 31 | VZMacOSRestoreImage.fetchLatestSupported { [self](result: Result) in 32 | switch result { 33 | case let .success(restoreImage): 34 | download(restoreImage: restoreImage) 35 | case let .failure(error): 36 | delegate?.done(error: error) 37 | } 38 | } 39 | } 40 | 41 | func cancel() { 42 | downloadTask?.cancel() 43 | } 44 | 45 | // MARK: - Private 46 | 47 | fileprivate func download(restoreImage: VZMacOSRestoreImage) { 48 | Logger.shared.log(level: .default, "fetched, macOS \(restoreImage.operatingSystemVersionString)") 49 | 50 | let downloadTask = URLSession.shared.downloadTask(with: restoreImage.url) {localUrl, response, error in 51 | self.downloading = false 52 | self.downloadFinished(localURL: localUrl, error: error) 53 | } 54 | observation = downloadTask.progress.observe(\.fractionCompleted) { _, _ in } 55 | downloadTask.resume() 56 | self.downloadTask = downloadTask 57 | 58 | Logger.shared.log(level: .default, "downloading") 59 | 60 | func updateDownloadProgress() { 61 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in 62 | let progressString: String 63 | 64 | if let byteCompletedCount = downloadTask.progress.userInfo[ProgressUserInfoKey("NSProgressByteCompletedCountKey")] as? Int, 65 | let byteTotalCount = downloadTask.progress.userInfo[ProgressUserInfoKey("NSProgressByteTotalCountKey")] as? Int 66 | { 67 | let mbCompleted = byteCompletedCount / (1024 * 1024) 68 | let mbTotal = byteTotalCount / (1024 * 1024) 69 | progressString = "Restore Image\nDownloading \(Int(downloadTask.progress.fractionCompleted * 100))% (\(mbCompleted) of \(mbTotal) MB)" 70 | } else { 71 | progressString = "Restore Image\nDownloading \(Int(downloadTask.progress.fractionCompleted * 100))%" 72 | } 73 | Logger.shared.log(level: .default, "\(progressString)") 74 | 75 | self?.delegate?.progress(downloadTask.progress.fractionCompleted, progressString: progressString) 76 | 77 | if let downloading = self?.downloading, downloading { 78 | updateDownloadProgress() 79 | } 80 | } 81 | } 82 | 83 | updateDownloadProgress() 84 | } 85 | 86 | fileprivate func downloadFinished(localURL: URL?, error: Error?) { 87 | Logger.shared.log(level: .default, "download finished") 88 | delegate?.progress(100, progressString: "Done") 89 | 90 | if let error = error { 91 | Logger.shared.log(level: .default, "\(error.localizedDescription)") 92 | delegate?.done(error: error) 93 | } 94 | 95 | if let localURL = localURL { 96 | try? FileManager.default.moveItem(at: localURL, to: URL.restoreImageURL) 97 | Logger.shared.log(level: .default, "moved restore image to \(URL.restoreImageURL)") 98 | delegate?.done(error: nil) 99 | } else { 100 | Logger.shared.log(level: .default, "failed to move downloaded restore image to \(URL.restoreImageURL)") 101 | return 102 | } 103 | } 104 | } 105 | 106 | #endif 107 | -------------------------------------------------------------------------------- /virtualOS/RestoreImage/RestoreImageInstall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestoreImageInstall.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Foundation 10 | import Virtualization 11 | import OSLog 12 | 13 | #if arch(arm64) 14 | 15 | final class RestoreImageInstall { 16 | struct RestoreError: Error { 17 | var localizedDescription = "Restore Error" 18 | } 19 | 20 | weak var delegate: ProgressDelegate? 21 | var restoreImageName: String? 22 | var diskImageSize: Int? 23 | 24 | fileprivate var observation: NSKeyValueObservation? 25 | fileprivate var installing = true 26 | fileprivate var installer: VZMacOSInstaller? 27 | fileprivate let queue = DispatchQueue.global(qos: .userInteractive) 28 | 29 | deinit { 30 | observation?.invalidate() 31 | } 32 | 33 | func install() { 34 | let restoreImageURL: URL 35 | if let restoreImageName { 36 | restoreImageURL = URL.baseURL.appendingPathComponent(restoreImageName) 37 | } else { 38 | restoreImageURL = URL.restoreImageURL 39 | } 40 | 41 | if !FileManager.default.fileExists(atPath: restoreImageURL.path) { 42 | Logger.shared.log(level: .default, "no restore image") 43 | delegate?.progress(0, progressString: "error: no restore image") 44 | return 45 | } 46 | 47 | loadParametersFromRestoreImage(restoreImageURL: restoreImageURL) 48 | } 49 | 50 | func cancel() { 51 | stopVM() 52 | } 53 | 54 | // MARK: - Private 55 | 56 | fileprivate func loadParametersFromRestoreImage(restoreImageURL: URL?) { 57 | let bundleURl = createBundleURL() 58 | if let error = createBundle(at: bundleURl) { 59 | self.delegate?.done(error: error) 60 | return 61 | } 62 | 63 | guard let restoreImageURL else { 64 | self.delegate?.done(error: RestoreError(localizedDescription: "Restore image URL unavailable.")) 65 | return // error 66 | } 67 | 68 | VZMacOSRestoreImage.load(from: restoreImageURL) { (result: Result) in 69 | switch result { 70 | case .success(let restoreImage): 71 | self.startInstall(restoreImage: restoreImage, bundleURL: bundleURl) 72 | case .failure(let error): 73 | self.delegate?.done(error: error) 74 | } 75 | } 76 | } 77 | 78 | fileprivate func startInstall(restoreImage: VZMacOSRestoreImage, bundleURL: URL) { 79 | var versionString = "" 80 | guard let macPlatformConfiguration = MacPlatformConfiguration.createDefault(fromRestoreImage: restoreImage, versionString: &versionString, bundleURL: bundleURL) else { 81 | return 82 | } 83 | 84 | var vmParameters = VMParameters() 85 | let vmConfiguration = VMConfiguration() 86 | vmConfiguration.platform = macPlatformConfiguration 87 | 88 | if let diskImageSize = diskImageSize { 89 | vmParameters.diskSizeInGB = UInt64(diskImageSize) 90 | if createDiskImage(diskImageURL: bundleURL.diskImageURL, sizeInGB: UInt64(vmParameters.diskSizeInGB)) { 91 | return 92 | } 93 | } 94 | 95 | vmConfiguration.setDefault(parameters: &vmParameters) 96 | vmConfiguration.setup(parameters: vmParameters, macPlatformConfiguration: macPlatformConfiguration, bundleURL: bundleURL) 97 | 98 | vmParameters.version = restoreImage.operatingSystemVersionString 99 | vmParameters.writeToDisk(bundleURL: bundleURL) 100 | 101 | do { 102 | try vmConfiguration.validate() 103 | Logger.shared.log(level: .default, "vm configuration is valid, using \(vmParameters.cpuCount) cpus and \(vmParameters.memorySizeInGB) gb ram") 104 | } catch let error { 105 | Logger.shared.log(level: .default, "failed to validate vm configuration: \(error.localizedDescription)") 106 | return 107 | } 108 | 109 | let vm = VZVirtualMachine(configuration: vmConfiguration, queue: queue) 110 | 111 | var restoreImageURL = URL.restoreImageURL 112 | if let restoreImageName { 113 | // use custom restore image 114 | restoreImageURL = URL.baseURL.appendingPathComponent(restoreImageName) 115 | } 116 | 117 | queue.async { [weak self] in 118 | let installer = VZMacOSInstaller(virtualMachine: vm, restoringFromImageAt: restoreImageURL) 119 | self?.installer = installer 120 | 121 | installer.install { result in 122 | self?.installing = false 123 | switch result { 124 | case .success(): 125 | self?.installFinisehd(installer: installer) 126 | case .failure(let error): 127 | self?.delegate?.done(error: error) 128 | } 129 | } 130 | 131 | self?.observation = installer.progress.observe(\.fractionCompleted) { _, _ in } 132 | 133 | func updateInstallProgress() { 134 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in 135 | var progressString = "Installing \(Int(installer.progress.fractionCompleted * 100))%" 136 | if installer.progress.fractionCompleted == 0 { 137 | progressString += " (Please wait)" 138 | } 139 | progressString += "\n\(versionString)" 140 | 141 | if let installing = self?.installing, installing { 142 | self?.delegate?.progress(installer.progress.fractionCompleted, progressString: progressString) 143 | updateInstallProgress() 144 | } 145 | } 146 | } 147 | 148 | updateInstallProgress() 149 | } 150 | } 151 | 152 | fileprivate func installFinisehd(installer: VZMacOSInstaller) { 153 | Logger.shared.log(level: .default, "Install finished") 154 | installing = false 155 | delegate?.progress(installer.progress.fractionCompleted, progressString: "Install finished successfully.") 156 | delegate?.done(error: nil) 157 | stopVM() 158 | } 159 | 160 | fileprivate func stopVM() { 161 | if let installer = installer { 162 | queue.async { 163 | if installer.virtualMachine.canStop { 164 | installer.virtualMachine.stop(completionHandler: { error in 165 | if let error { 166 | Logger.shared.log(level: .default, "Error stopping VM: \(error.localizedDescription)") 167 | } else { 168 | Logger.shared.log(level: .default, "VM stopped") 169 | } 170 | }) 171 | } 172 | } 173 | } 174 | } 175 | 176 | fileprivate func createBundleURL() -> URL { 177 | var url = URL.vmBundleURL 178 | 179 | if let vmFilesDirectoryURL = UserDefaults.standard.vmFilesDirectory, 180 | let vmFilesDirectoryBookmarkData = UserDefaults.standard.vmFilesDirectoryBookmarkData, 181 | let bookmark = Bookmark.startAccess(bookmarkData: vmFilesDirectoryBookmarkData, for: vmFilesDirectoryURL) 182 | { 183 | url = bookmark.appending(path: URL.bundleName) 184 | } 185 | print(url.absoluteString) 186 | 187 | // try to find a filename that does not exist 188 | var exists = true 189 | var i = 1 190 | while exists { 191 | var filename = url.lastPathComponent 192 | filename = filename.replacingOccurrences(of: ".bundle", with: "") 193 | let filenameComponents = filename.split(separator: "_") 194 | if filenameComponents.count > 0 { 195 | filename = String(filenameComponents[0]) 196 | } 197 | filename += "_\(i).bundle" 198 | 199 | url = URL(fileURLWithPath: url.deletingLastPathComponent().appendingPathComponent(filename, conformingTo: .bundle).path()) 200 | 201 | if FileManager.default.fileExists(atPath: url.path()) { 202 | i += 1 203 | } else { 204 | exists = false 205 | } 206 | } 207 | Logger.shared.log(level: .default, "using bundle url \(url.absoluteString)") 208 | return url 209 | } 210 | 211 | fileprivate func createBundle(at bundleURl: URL) -> RestoreError? { 212 | if FileManager.default.fileExists(atPath: bundleURl.path()) { 213 | return nil // already exists, no error 214 | } 215 | 216 | let bundleFileDescriptor = mkdir(bundleURl.path(), S_IRWXU | S_IRWXG | S_IRWXO) 217 | if bundleFileDescriptor == -1 { 218 | var errorMessage = "Failed to create VM bundle at \(bundleURl.path()) (error number \(errno))." 219 | if errno == EEXIST { 220 | errorMessage += " The base directory already exists." 221 | } 222 | Logger.shared.log(level: .default, "\(errorMessage)") 223 | return RestoreError(localizedDescription: errorMessage) 224 | } 225 | 226 | let result = close(bundleFileDescriptor) 227 | if result != 0 { 228 | let errorMessage = "warning: failed to close VM bundle at \(bundleURl.path()) (error number \(result))." 229 | Logger.shared.log(level: .default, "\(errorMessage)") 230 | return nil // no error 231 | } 232 | 233 | return nil // no error 234 | } 235 | 236 | fileprivate func createDiskImage(diskImageURL: URL, sizeInGB: UInt64) -> Bool { 237 | let diskImageFileDescriptor = open(diskImageURL.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR) 238 | if diskImageFileDescriptor == -1 { 239 | Logger.shared.log(level: .default, "error: cannot create disk image") 240 | return false // failure 241 | } 242 | 243 | let diskSize = sizeInGB.gigabytesToBytes() 244 | var result = ftruncate(diskImageFileDescriptor, Int64(diskSize)) 245 | if result != 0 { 246 | Logger.shared.log(level: .default, "error: expanding disk image failed") 247 | return false // failure 248 | } 249 | 250 | result = close(diskImageFileDescriptor) 251 | if result != 0 { 252 | Logger.shared.log(level: .default, "error: failed to close the disk image") 253 | return false // failure 254 | } 255 | 256 | return false // failure 257 | } 258 | 259 | } 260 | 261 | #endif 262 | -------------------------------------------------------------------------------- /virtualOS/VM/MacPlatformConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacPlatformConfiguration.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Virtualization 10 | import OSLog 11 | 12 | #if arch(arm64) 13 | 14 | final class MacPlatformConfiguration: VZMacPlatformConfiguration { 15 | var versionString: String? 16 | 17 | static func read(fromBundleURL bundleURL: URL) -> VZMacPlatformConfiguration? { 18 | let macPlatformConfiguration = MacPlatformConfiguration() 19 | 20 | let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: bundleURL.auxiliaryStorageURL) 21 | macPlatformConfiguration.auxiliaryStorage = auxiliaryStorage 22 | 23 | guard let hardwareModelData = try? Data(contentsOf: bundleURL.hardwareModelURL) else { 24 | Logger.shared.log(level: .default, "Error: Failed to retrieve hardware model data") 25 | return nil 26 | } 27 | 28 | guard let hardwareModel = VZMacHardwareModel(dataRepresentation: hardwareModelData) else { 29 | Logger.shared.log(level: .default, "Error: Failed to create hardware model") 30 | return nil 31 | } 32 | 33 | if !hardwareModel.isSupported { 34 | Logger.shared.log(level: .default, "Error: The hardware model is not supported on the current host") 35 | return nil 36 | } 37 | macPlatformConfiguration.hardwareModel = hardwareModel 38 | 39 | guard let machineIdentifierData = try? Data(contentsOf: bundleURL.machineIdentifierURL) else { 40 | Logger.shared.log(level: .default, "Error: Failed to retrieve machine identifier data.") 41 | return nil 42 | } 43 | 44 | guard let machineIdentifier = VZMacMachineIdentifier(dataRepresentation: machineIdentifierData) else { 45 | Logger.shared.log(level: .default, "Error: Failed to create machine identifier.") 46 | return nil 47 | } 48 | macPlatformConfiguration.machineIdentifier = machineIdentifier 49 | 50 | return macPlatformConfiguration 51 | } 52 | 53 | static func createDefault(fromRestoreImage restoreImage: VZMacOSRestoreImage, versionString: inout String, bundleURL: URL) -> VZMacPlatformConfiguration? { 54 | let macPlatformConfiguration = MacPlatformConfiguration() 55 | 56 | versionString = restoreImage.operatingSystemVersionString 57 | let versionString = versionString 58 | Logger.shared.log(level: .default, "restore image version: \(versionString)") 59 | 60 | guard let mostFeaturefulSupportedConfiguration = restoreImage.mostFeaturefulSupportedConfiguration else { 61 | Logger.shared.log(level: .default, "restore image for macOS version \(versionString) is not supported on this machine") 62 | return nil 63 | } 64 | guard mostFeaturefulSupportedConfiguration.hardwareModel.isSupported else { 65 | Logger.shared.log(level: .default, "hardware model required by restore image for macOS version \(versionString) is not supported on this machine") 66 | return macPlatformConfiguration 67 | } 68 | 69 | let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: URL.baseURL.auxiliaryStorageURL) 70 | macPlatformConfiguration.auxiliaryStorage = auxiliaryStorage 71 | 72 | guard let macPlatformConfiguration = macPlatformConfiguration.createPlatformConfiguration(macHardwareModel: mostFeaturefulSupportedConfiguration.hardwareModel, bundleURL: bundleURL) else { 73 | return nil 74 | } 75 | 76 | var vmParameters = VMParameters() 77 | vmParameters.cpuCountMin = mostFeaturefulSupportedConfiguration.minimumSupportedCPUCount 78 | vmParameters.memorySizeInGBMin = mostFeaturefulSupportedConfiguration.minimumSupportedMemorySize.bytesToGigabytes() 79 | 80 | return macPlatformConfiguration 81 | } 82 | 83 | fileprivate func createPlatformConfiguration(macHardwareModel: VZMacHardwareModel, bundleURL: URL) -> VZMacPlatformConfiguration? { 84 | let platformConfiguration = VZMacPlatformConfiguration() 85 | platformConfiguration.hardwareModel = macHardwareModel 86 | 87 | do { 88 | platformConfiguration.auxiliaryStorage = try VZMacAuxiliaryStorage(creatingStorageAt: bundleURL.auxiliaryStorageURL, hardwareModel: macHardwareModel, options: [.allowOverwrite] 89 | ) 90 | } catch { 91 | Logger.shared.log(level: .default, "Error: could not create auxiliary storage device") 92 | return nil 93 | } 94 | 95 | do { 96 | try platformConfiguration.hardwareModel.dataRepresentation.write(to: bundleURL.hardwareModelURL) 97 | try platformConfiguration.machineIdentifier.dataRepresentation.write(to: bundleURL.machineIdentifierURL) 98 | } catch { 99 | Logger.shared.log(level: .default, "could store platform information to disk") 100 | return nil 101 | } 102 | 103 | return platformConfiguration // success 104 | } 105 | } 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /virtualOS/VM/VMConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VM.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | #if arch(arm64) 10 | 11 | import Virtualization 12 | import AVFoundation // for audio 13 | import OSLog 14 | 15 | final class VMConfiguration: VZVirtualMachineConfiguration { 16 | func setup(parameters: VMParameters, macPlatformConfiguration: VZMacPlatformConfiguration, bundleURL: URL) { 17 | cpuCount = parameters.cpuCount 18 | memorySize = parameters.memorySizeInGB.gigabytesToBytes() 19 | pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()] 20 | entropyDevices = [VZVirtioEntropyDeviceConfiguration()] 21 | keyboards = [VZUSBKeyboardConfiguration()] 22 | bootLoader = VZMacOSBootLoader() 23 | 24 | configureAudioDevice(parameters: parameters) 25 | configureGraphicsDevice(parameters: parameters) 26 | configureStorageDevice(parameters: parameters, bundleURL: bundleURL) 27 | configureNetworkDevices(parameters: parameters) 28 | configureSharedFolder(parameters: parameters) 29 | configureClipboardSharing() 30 | configureUSB() 31 | 32 | platform = macPlatformConfiguration 33 | } 34 | 35 | func setDefault(parameters: inout VMParameters) { 36 | let cpuCountMax = computeCPUCount() 37 | let bytesMax = VZVirtualMachineConfiguration.maximumAllowedMemorySize 38 | cpuCount = cpuCountMax - 1 // substract one core 39 | memorySize = bytesMax - UInt64(3).gigabytesToBytes() // substract 3 GB 40 | 41 | parameters.cpuCount = cpuCount 42 | parameters.cpuCountMax = cpuCountMax 43 | parameters.memorySizeInGB = memorySize.bytesToGigabytes() 44 | parameters.memorySizeInGBMax = bytesMax.bytesToGigabytes() 45 | } 46 | 47 | // MARK: - Private 48 | 49 | fileprivate func configureAudioDevice(parameters: VMParameters) { 50 | let audioDevice = VZVirtioSoundDeviceConfiguration() 51 | 52 | if parameters.microphoneEnabled { 53 | AVCaptureDevice.requestAccess(for: .audio) { (granted: Bool) in 54 | Logger.shared.log(level: .default, "microphone request granted: \(granted)") 55 | } 56 | 57 | let inputStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration() 58 | inputStreamConfiguration.source = VZHostAudioInputStreamSource() 59 | audioDevice.streams.append(inputStreamConfiguration) 60 | } 61 | 62 | let outputStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration() 63 | outputStreamConfiguration.sink = VZHostAudioOutputStreamSink() 64 | audioDevice.streams.append(outputStreamConfiguration) 65 | 66 | audioDevices = [audioDevice] 67 | } 68 | 69 | fileprivate func configureGraphicsDevice(parameters: VMParameters) { 70 | let graphicsDevice = VZMacGraphicsDeviceConfiguration() 71 | if parameters.useMainScreenSize, let mainScreen = NSScreen.main { 72 | graphicsDevice.displays = [VZMacGraphicsDisplayConfiguration(for: mainScreen, sizeInPoints: NSSize(width: parameters.screenWidth, height: parameters.screenHeight))] 73 | } else { 74 | graphicsDevice.displays = [VZMacGraphicsDisplayConfiguration( 75 | widthInPixels: parameters.screenWidth, 76 | heightInPixels: parameters.screenHeight, 77 | pixelsPerInch: parameters.pixelsPerInch 78 | )] 79 | } 80 | graphicsDevices = [graphicsDevice] 81 | } 82 | 83 | fileprivate func configureStorageDevice(parameters: VMParameters, bundleURL: URL) { 84 | let diskImageStorageDeviceAttachment: VZDiskImageStorageDeviceAttachment? 85 | do { 86 | diskImageStorageDeviceAttachment = try VZDiskImageStorageDeviceAttachment(url: bundleURL.diskImageURL, readOnly: false) 87 | } catch let error { 88 | Logger.shared.log(level: .default, "could not create storage device: \(error.localizedDescription)") 89 | return 90 | } 91 | 92 | if let diskImageStorageDeviceAttachment { 93 | let blockDeviceConfiguration = VZVirtioBlockDeviceConfiguration(attachment: diskImageStorageDeviceAttachment) 94 | storageDevices = [blockDeviceConfiguration] 95 | } 96 | 97 | if let diskImageStorageDeviceAttachment = try? VZDiskImageStorageDeviceAttachment(url: bundleURL.diskImageURL, readOnly: false) { 98 | let blockDeviceConfiguration = VZVirtioBlockDeviceConfiguration(attachment: diskImageStorageDeviceAttachment) 99 | storageDevices = [blockDeviceConfiguration] 100 | } else { 101 | Logger.shared.log(level: .default, "could not create storage device") 102 | } 103 | } 104 | 105 | fileprivate func configureNetworkDevices(parameters: VMParameters) { 106 | let networkDevice = VZVirtioNetworkDeviceConfiguration() 107 | let networkAttachment = VZNATNetworkDeviceAttachment() 108 | networkDevice.attachment = networkAttachment 109 | networkDevice.macAddress = VZMACAddress(string: parameters.macAddress) ?? .randomLocallyAdministered() 110 | networkDevices = [networkDevice] 111 | } 112 | 113 | fileprivate func configureSharedFolder(parameters: VMParameters) { 114 | guard let sharedFolderURL = parameters.sharedFolderURL, 115 | let sharedFolderBookmarkData = Bookmark.startAccess(bookmarkData: parameters.sharedFolderData, for: sharedFolderURL.absoluteString) else 116 | { 117 | return 118 | } 119 | 120 | let sharedDirectory = VZSharedDirectory(url: sharedFolderBookmarkData, readOnly: false) 121 | let singleDirectoryShare = VZSingleDirectoryShare(directory: sharedDirectory) 122 | let sharingConfiguration = VZVirtioFileSystemDeviceConfiguration(tag: VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag) 123 | sharingConfiguration.share = singleDirectoryShare 124 | 125 | directorySharingDevices = [sharingConfiguration] 126 | } 127 | 128 | fileprivate func configureClipboardSharing() { 129 | let consoleDevice = VZVirtioConsoleDeviceConfiguration() 130 | 131 | let spiceAgentPortConfiguration = VZVirtioConsolePortConfiguration() 132 | spiceAgentPortConfiguration.name = VZSpiceAgentPortAttachment.spiceAgentPortName 133 | spiceAgentPortConfiguration.attachment = VZSpiceAgentPortAttachment() 134 | consoleDevice.ports[0] = spiceAgentPortConfiguration 135 | 136 | consoleDevices.append(consoleDevice) 137 | } 138 | 139 | fileprivate func configureUSB() { 140 | let usbControllerConfiguration = VZXHCIControllerConfiguration() 141 | usbControllers = [usbControllerConfiguration] 142 | } 143 | 144 | fileprivate func computeCPUCount() -> Int { 145 | let totalAvailableCPUs = ProcessInfo.processInfo.processorCount 146 | 147 | var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs 148 | virtualCPUCount = max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount) 149 | virtualCPUCount = min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount) 150 | 151 | return virtualCPUCount 152 | } 153 | } 154 | 155 | #endif 156 | -------------------------------------------------------------------------------- /virtualOS/VM/VMParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMParameters.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | #if arch(arm64) 10 | 11 | import Virtualization 12 | 13 | struct VMParameters: Codable { 14 | var cpuCount = 1 15 | var cpuCountMin = 1 16 | var cpuCountMax = 2 17 | var diskSizeInGB: UInt64 = UInt64(UserDefaults.standard.diskSize) 18 | var memorySizeInGB: UInt64 = 1 19 | var memorySizeInGBMin: UInt64 = 1 20 | var memorySizeInGBMax: UInt64 = 2 21 | var useMainScreenSize = true 22 | var screenWidth = 1500 23 | var screenHeight = 900 24 | var pixelsPerInch = 250 25 | var microphoneEnabled = false 26 | var sharedFolderURL: URL? 27 | var sharedFolderData: Data? 28 | var macAddress = VZMACAddress.randomLocallyAdministered().string 29 | var version = "" 30 | 31 | init() {} 32 | 33 | init(from decoder: Decoder) throws { 34 | let container = try decoder.container(keyedBy: CodingKeys.self) 35 | cpuCount = try container.decode(Int.self, forKey: .cpuCount) 36 | cpuCountMin = try container.decode(Int.self, forKey: .cpuCountMin) 37 | cpuCountMax = try container.decode(Int.self, forKey: .cpuCountMax) 38 | diskSizeInGB = try container.decode(UInt64.self, forKey: .diskSizeInGB) 39 | memorySizeInGB = try container.decode(UInt64.self, forKey: .memorySizeInGB) 40 | memorySizeInGBMin = try container.decode(UInt64.self, forKey: .memorySizeInGBMin) 41 | memorySizeInGBMax = try container.decode(UInt64.self, forKey: .memorySizeInGBMax) 42 | useMainScreenSize = try container.decodeIfPresent(Bool.self, forKey: .useMainScreenSize) ?? true // optional 43 | screenWidth = try container.decode(Int.self, forKey: .screenWidth) 44 | screenHeight = try container.decode(Int.self, forKey: .screenHeight) 45 | pixelsPerInch = try container.decode(Int.self, forKey: .pixelsPerInch) 46 | microphoneEnabled = try container.decode(Bool.self, forKey: .microphoneEnabled) 47 | sharedFolderURL = try container.decodeIfPresent(URL.self, forKey: .sharedFolderURL) ?? nil // optional 48 | sharedFolderData = try container.decodeIfPresent(Data.self, forKey: .sharedFolderData) ?? nil // optional 49 | macAddress = try container.decodeIfPresent(String.self, forKey: .macAddress) ?? VZMACAddress.randomLocallyAdministered().string // optional 50 | version = try container.decodeIfPresent(String.self, forKey: .version) ?? "" // optional 51 | 52 | } 53 | 54 | static func readFrom(url: URL) -> VMParameters? { 55 | let decoder = JSONDecoder() 56 | do { 57 | let json = try Data.init(contentsOf: url.appendingPathComponent("Parameters.txt", conformingTo: .text)) 58 | return try decoder.decode(VMParameters.self, from: json) 59 | } catch { 60 | print("failed to read parameters") 61 | } 62 | return nil 63 | } 64 | 65 | func writeToDisk(bundleURL: URL) { 66 | let encoder = JSONEncoder() 67 | encoder.outputFormatting = .prettyPrinted 68 | 69 | do { 70 | let jsonData = try encoder.encode(self) 71 | if let json = String(data: jsonData, encoding: .utf8) { 72 | try json.write(to: bundleURL.parametersURL, atomically: true, encoding: String.Encoding.utf8) 73 | } 74 | } catch { 75 | print("failed to write current CPU and RAM configuration to disk") 76 | } 77 | } 78 | } 79 | 80 | #endif 81 | 82 | -------------------------------------------------------------------------------- /virtualOS/ViewController/AlertController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | 11 | extension NSAlert { 12 | static func okCancelAlert(messageText: String, informativeText: String, showCancelButton: Bool = true, accessoryView: NSView? = nil, alertStyle: NSAlert.Style = .informational) -> NSAlert { 13 | let alert: NSAlert = NSAlert() 14 | 15 | alert.messageText = messageText 16 | alert.informativeText = informativeText 17 | alert.accessoryView = accessoryView 18 | alert.alertStyle = alertStyle 19 | alert.addButton(withTitle: "OK") 20 | if showCancelButton { 21 | alert.addButton(withTitle: "Cancel") 22 | } 23 | 24 | return alert 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /virtualOS/ViewController/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Cocoa 10 | import Virtualization 11 | import OSLog 12 | 13 | #if arch(arm64) 14 | 15 | final class MainViewController: NSViewController { 16 | @IBOutlet weak var tableView: NSTableView! 17 | @IBOutlet weak var vmNameTextField: NSTextField! 18 | @IBOutlet weak var parameterOutlineView: NSOutlineView! 19 | @IBOutlet weak var startButton: NSButton! 20 | @IBOutlet weak var sharedFolderButton: NSButton! 21 | @IBOutlet weak var deleteButton: NSButton! 22 | @IBOutlet weak var cpuCountLabel: NSTextField! 23 | @IBOutlet weak var cpuCountSlider: NSSlider! 24 | @IBOutlet weak var ramLabel: NSTextField! 25 | @IBOutlet weak var ramSlider: NSSlider! 26 | 27 | fileprivate let mainStoryBoard = NSStoryboard(name: "Main", bundle: nil) 28 | fileprivate let viewModel = MainViewModel() 29 | fileprivate var diskImageSize = 1 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | tableView.dataSource = viewModel.tableViewDataSource 35 | tableView.delegate = self 36 | parameterOutlineView.dataSource = viewModel.parametersViewDataSource 37 | parameterOutlineView.delegate = viewModel.parametersViewDelegate 38 | vmNameTextField.delegate = viewModel.textFieldDelegate 39 | 40 | NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: NSApplication.didBecomeActiveNotification, object: nil) 41 | NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: NSControl.textDidEndEditingNotification, object: nil) 42 | NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: Constants.didChangeVMLocationNotification, object: nil) 43 | 44 | NotificationCenter.default.addObserver(self, selector: #selector(restoreImageSelected), name: Constants.restoreImageNameSelectedNotification, object: nil) 45 | 46 | ramSlider.target = self 47 | ramSlider.action = #selector(memorySliderChanged(sender:)) 48 | cpuCountSlider.target = self 49 | cpuCountSlider.action = #selector(cpuCountChanged(sender:)) 50 | } 51 | 52 | deinit { 53 | NotificationCenter.default.removeObserver(self) 54 | } 55 | 56 | override func viewWillAppear() { 57 | super.viewWillAppear() 58 | view.window?.delegate = self 59 | vmNameTextField.resignFirstResponder() 60 | if let bookmarkURL = UserDefaults.standard.vmFilesDirectory, 61 | let bookmarkData = UserDefaults.standard.vmFilesDirectoryBookmarkData 62 | { 63 | _ = Bookmark.startAccess(bookmarkData: bookmarkData, for: bookmarkURL) 64 | } 65 | self.updateUI() 66 | } 67 | 68 | @IBAction func startButtonPressed(_ sender: NSButton) { 69 | if let windowController = mainStoryBoard.instantiateController(withIdentifier: "NSWindowController") as? NSWindowController, 70 | let vmViewController = mainStoryBoard.instantiateController(withIdentifier: "VMViewController") as? VMViewController 71 | { 72 | vmViewController.vmBundle = viewModel.vmBundle 73 | vmViewController.vmParameters = viewModel.vmParameters 74 | windowController.showWindow(self) 75 | windowController.contentViewController = vmViewController 76 | } else { 77 | Logger.shared.log(level: .default, "show vm window failed") 78 | } 79 | } 80 | 81 | @IBAction func installButtonPressed(_ sender: NSButton) { 82 | if let windowController = mainStoryBoard.instantiateController(withIdentifier: "NSWindowController") as? NSWindowController, 83 | let restoreImageViewController = mainStoryBoard.instantiateController(withIdentifier: "RestoreImageViewController") as? RestoreImageViewController 84 | { 85 | windowController.showWindow(self) 86 | windowController.contentViewController = restoreImageViewController 87 | if let parentFrame = view.window?.frame, 88 | let childWindow = restoreImageViewController.view.window 89 | { 90 | childWindow.setFrame(parentFrame.offsetBy(dx: 200, dy: 10), display: true) 91 | } 92 | } else { 93 | Logger.shared.log(level: .default, "show restore image window failed") 94 | } 95 | } 96 | 97 | @IBAction func deleteButtonPressed(_ sender: Any) { 98 | guard let vmBundle = viewModel.vmBundle else { 99 | return 100 | } 101 | 102 | let alert: NSAlert = NSAlert.okCancelAlert(messageText: "Delete VM '\(vmBundle.name)'?", informativeText: "This can not be undone.", alertStyle: .warning) 103 | let selection = alert.runModal() 104 | if selection == NSApplication.ModalResponse.alertFirstButtonReturn || 105 | selection == NSApplication.ModalResponse.OK 106 | { 107 | viewModel.deleteVM(selection: selection, vmBundle: vmBundle) 108 | viewModel.vmBundle = nil 109 | viewModel.vmParameters = nil 110 | } 111 | 112 | self.updateUI() 113 | } 114 | 115 | @IBAction func sharedFolderButtonPressed(_ sender: Any) { 116 | let openPanel = NSOpenPanel() 117 | openPanel.allowsMultipleSelection = false 118 | openPanel.canChooseDirectories = true 119 | openPanel.canChooseFiles = false 120 | openPanel.prompt = "Select" 121 | let modalResponse = openPanel.runModal() 122 | var sharedFolderURL: URL? 123 | if modalResponse == .OK, 124 | let selectedURL = openPanel.url 125 | { 126 | sharedFolderURL = selectedURL 127 | } else if modalResponse == .cancel { 128 | sharedFolderURL = nil 129 | } 130 | 131 | viewModel.set(sharedFolderUrl: sharedFolderURL) 132 | updateOutlineView() 133 | } 134 | 135 | @objc func restoreImageSelected(notification: Notification) { 136 | if let userInfo = notification.userInfo, 137 | let restoreImageName = userInfo[Constants.selectedRestoreImage] as? String 138 | { 139 | if restoreImageName != Constants.restoreImageNameLatest { 140 | let accessoryView = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 20)) 141 | accessoryView.stringValue = "\(UserDefaults.standard.diskSize)" 142 | 143 | let alert = NSAlert.okCancelAlert(messageText: "Disk Image Size in GB", informativeText: "Disk size can not be changed after VM is created. Minimum disk size is 30 GB. During install, a lot of RAM is used, ignore the warning about low system memory.", accessoryView: accessoryView) 144 | let modalResponse = alert.runModal() 145 | accessoryView.becomeFirstResponder() 146 | 147 | if modalResponse == .OK || modalResponse == .alertFirstButtonReturn { 148 | diskImageSize = Int(accessoryView.intValue) 149 | } else { 150 | return // cancel install 151 | } 152 | if diskImageSize < 30 { 153 | self.diskImageSize = 30 154 | } 155 | } 156 | 157 | showSheet(mode: .install, restoreImageName: restoreImageName, diskImageSize: self.diskImageSize) 158 | } 159 | } 160 | 161 | @objc func cpuCountChanged(sender: NSSlider) { 162 | updateUIAndStoreParametersToDisk() 163 | } 164 | 165 | @objc func memorySliderChanged(sender: NSSlider) { 166 | updateUIAndStoreParametersToDisk() 167 | } 168 | 169 | @objc func updateUI() { 170 | self.tableView.reloadData() 171 | cpuCountSlider.isEnabled = false 172 | ramSlider.isEnabled = false 173 | 174 | if let selectedRow = viewModel.selectedRow, 175 | let vmBundle = viewModel.tableViewDataSource.vmBundle(forRow: selectedRow) 176 | { 177 | vmNameTextField.stringValue = vmBundle.name 178 | tableView.selectRowIndexes(IndexSet(integer: selectedRow), byExtendingSelection: false) 179 | viewModel.vmBundle = vmBundle 180 | if let vmParameters = VMParameters.readFrom(url: vmBundle.url) { 181 | viewModel.vmParameters = vmParameters 182 | viewModel.textFieldDelegate.vmBundle = vmBundle 183 | updateButtons(enabled: true) 184 | updateLabels(setZero: false) 185 | updateCpuCount(vmParameters) 186 | updateRam(vmParameters) 187 | } 188 | } else { 189 | vmNameTextField.stringValue = "" 190 | viewModel.selectedRow = 0 191 | updateButtons(enabled: false) 192 | updateLabels(setZero: true) 193 | } 194 | 195 | updateOutlineView() 196 | } 197 | 198 | func showErrorAlert(error: Error) { 199 | var messageText = "Error" 200 | var informativeText = "An unknown error occurred." 201 | 202 | if let vzError = error as? VZError, 203 | let reason = vzError.userInfo[NSLocalizedFailureErrorKey] as? String, 204 | let failureReason = vzError.userInfo[NSLocalizedFailureReasonErrorKey] as? String, 205 | let underlyingError = vzError.userInfo[NSUnderlyingErrorKey] as? NSError 206 | { 207 | messageText = failureReason 208 | informativeText = reason + " " + underlyingError.localizedDescription + "\n\n(Error Code: \(vzError.errorCode), Underlying Error Domain: \(underlyingError.domain), Underlying Error Code: \(underlyingError.code))" 209 | Logger.shared.log(level: .default, "vz error: \(messageText) \(informativeText)") 210 | } else { 211 | informativeText = error.localizedDescription 212 | Logger.shared.log(level: .default, "error: \(error.localizedDescription)") 213 | } 214 | 215 | let alert = NSAlert.okCancelAlert(messageText: messageText, informativeText: informativeText, showCancelButton: false) 216 | let _ = alert.runModal() 217 | } 218 | 219 | // MARK: - Private 220 | 221 | fileprivate func updateButtons(enabled: Bool) { 222 | startButton.isEnabled = enabled 223 | sharedFolderButton.isEnabled = enabled 224 | deleteButton.isEnabled = enabled 225 | vmNameTextField.isEnabled = enabled 226 | } 227 | 228 | fileprivate func updateCpuCount(_ vmParameters: VMParameters) { 229 | cpuCountSlider.minValue = Double(vmParameters.cpuCountMin) 230 | cpuCountSlider.maxValue = Double(vmParameters.cpuCountMax) 231 | cpuCountSlider.numberOfTickMarks = Int(cpuCountSlider.maxValue - cpuCountSlider.minValue) 232 | cpuCountSlider.doubleValue = Double(vmParameters.cpuCount) 233 | cpuCountSlider.isEnabled = true 234 | } 235 | 236 | fileprivate func updateRam(_ vmParameters: VMParameters) { 237 | ramSlider.minValue = max(Double(vmParameters.memorySizeInGBMin), 2.0) 238 | ramSlider.maxValue = Double(vmParameters.memorySizeInGBMax) 239 | ramSlider.numberOfTickMarks = Int(ramSlider.maxValue - ramSlider.minValue) 240 | ramSlider.doubleValue = Double(vmParameters.memorySizeInGB) 241 | ramSlider.isEnabled = true 242 | } 243 | 244 | fileprivate func updateLabels(setZero: Bool) { 245 | let cpuCount = Int(round(cpuCountSlider.doubleValue)) 246 | let memorySizeInGB = Int(round(ramSlider.doubleValue)) 247 | viewModel.vmParameters?.cpuCount = cpuCount 248 | viewModel.vmParameters?.memorySizeInGB = UInt64(memorySizeInGB) 249 | 250 | if setZero { 251 | cpuCountLabel.stringValue = "CPU Count" 252 | ramLabel.stringValue = "RAM" 253 | } else { 254 | cpuCountLabel.stringValue = "CPU Count: \(cpuCount)" 255 | ramLabel.stringValue = "RAM: \(memorySizeInGB) GB" 256 | } 257 | } 258 | 259 | fileprivate func updateOutlineView() { 260 | parameterOutlineView.reloadData() 261 | } 262 | 263 | fileprivate func updateUIAndStoreParametersToDisk() { 264 | viewModel.storeParametersToDisk() 265 | updateLabels(setZero: false) 266 | updateOutlineView() 267 | } 268 | 269 | fileprivate func updateButtonEnabledState() { 270 | var enabled = false 271 | if viewModel.selectedRow != nil { 272 | enabled = true 273 | } 274 | vmNameTextField.isEnabled = enabled 275 | startButton.isEnabled = enabled 276 | sharedFolderButton.isEnabled = enabled 277 | deleteButton.isEnabled = enabled 278 | ramSlider.isEnabled = enabled 279 | cpuCountSlider.isEnabled = enabled 280 | } 281 | 282 | fileprivate func showSheet(mode: ProgressViewController.Mode, restoreImageName: String?, diskImageSize: Int?) { 283 | if let progressWindowController = mainStoryBoard.instantiateController(withIdentifier: "ProgressWindowController") as? NSWindowController, 284 | let progressWindow = progressWindowController.window 285 | { 286 | if let progressViewController = progressWindow.contentViewController as? ProgressViewController { 287 | progressViewController.mode = mode 288 | progressViewController.diskImageSize = diskImageSize 289 | progressViewController.restoreImageName = restoreImageName 290 | presentAsSheet(progressViewController) 291 | } 292 | } else { 293 | Logger.shared.log(level: .default, "show modal failed") 294 | } 295 | } 296 | } 297 | 298 | extension MainViewController: NSTableViewDelegate { 299 | func tableViewSelectionDidChange(_ notification: Notification) { 300 | var row: Int? = nil 301 | 302 | if let userInfo = notification.userInfo, 303 | let indexSet = userInfo["NSTableViewCurrentRowSelectionUserInfoKey"] as? NSIndexSet { 304 | if indexSet.count > 0 { 305 | row = indexSet.firstIndex 306 | } 307 | } 308 | 309 | if let row = row { 310 | viewModel.selectedRow = row 311 | updateUI() 312 | updateButtonEnabledState() 313 | } 314 | } 315 | } 316 | 317 | extension MainViewController: NSWindowDelegate { 318 | func windowShouldClose(_ sender: NSWindow) -> Bool { 319 | let alert: NSAlert = NSAlert.okCancelAlert(messageText: "Quit", informativeText: "Quitting the app will stop all virtual machines.", alertStyle: .warning) 320 | let selection = alert.runModal() 321 | if selection == NSApplication.ModalResponse.alertFirstButtonReturn || 322 | selection == NSApplication.ModalResponse.OK 323 | { 324 | NSApplication.shared.terminate(self) 325 | return true 326 | } else { 327 | return false 328 | } 329 | } 330 | } 331 | 332 | #else 333 | 334 | // minimum implementation used for intel cpus 335 | 336 | final class MainViewController: NSViewController { 337 | @IBOutlet weak var vmNameTextField: NSTextField! 338 | @IBOutlet weak var installButton: NSButton! 339 | @IBOutlet weak var startButton: NSButton! 340 | @IBOutlet weak var sharedFolderButton: NSButton! 341 | @IBOutlet weak var deleteButton: NSButton! 342 | @IBOutlet weak var cpuCountLabel: NSTextField! 343 | @IBOutlet weak var cpuCountSlider: NSSlider! 344 | @IBOutlet weak var ramLabel: NSTextField! 345 | @IBOutlet weak var ramSlider: NSSlider! 346 | 347 | override func viewWillAppear() { 348 | super.viewWillAppear() 349 | vmNameTextField.stringValue = "Virtualization requires an Apple Silicon machine" 350 | vmNameTextField.isEditable = false 351 | installButton.isEnabled = false 352 | startButton.isEnabled = false 353 | sharedFolderButton.isEnabled = false 354 | deleteButton.isEnabled = false 355 | cpuCountSlider.isEnabled = false 356 | ramSlider.isEnabled = false 357 | cpuCountLabel.stringValue = "" 358 | ramLabel.stringValue = "" 359 | } 360 | } 361 | 362 | #endif 363 | 364 | // place all code before #else 365 | -------------------------------------------------------------------------------- /virtualOS/ViewController/ProgressViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressViewController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | import OSLog 11 | 12 | #if arch(arm64) 13 | 14 | final class ProgressViewController: NSViewController { 15 | enum Mode { 16 | case download 17 | case install 18 | } 19 | 20 | @IBOutlet weak var progressIndicator: NSProgressIndicator! 21 | @IBOutlet weak var statusTextField: NSTextField! 22 | @IBOutlet weak var cancelButton: NSButton! 23 | 24 | var mode: Mode = .download 25 | var restoreImageName: String? 26 | var diskImageSize: Int? = 0 27 | fileprivate let restoreImageDownload = RestoreImageDownload() 28 | fileprivate var restoreImageInstall = RestoreImageInstall() 29 | 30 | override func viewWillAppear() { 31 | super.viewWillAppear() 32 | progressIndicator.doubleValue = 0 33 | statusTextField.stringValue = "Starting" 34 | // Logger.shared.log(level: .default, "\(progressViewController.mode): \(mode))") 35 | } 36 | 37 | override func viewDidAppear() { 38 | super.viewDidAppear() 39 | if restoreImageName == Constants.restoreImageNameLatest || 40 | mode == .download 41 | { 42 | restoreImageDownload.delegate = self 43 | restoreImageDownload.fetch() 44 | mode = .download // restoreImageNameLatest is also a download 45 | } else if mode == .install { 46 | restoreImageInstall.restoreImageName = restoreImageName 47 | restoreImageInstall.diskImageSize = diskImageSize 48 | restoreImageInstall.delegate = self 49 | restoreImageInstall.install() 50 | } 51 | } 52 | 53 | override func viewWillDisappear() { 54 | super.viewWillDisappear() 55 | cancel() 56 | } 57 | 58 | @IBAction func cancelButtonPressed(_ sender: NSButton) { 59 | cancel() 60 | if let mainViewController = presentingViewController as? MainViewController { 61 | mainViewController.updateUI() 62 | mainViewController.dismiss(self) 63 | } 64 | } 65 | 66 | fileprivate func cancel() { 67 | if mode == .download { 68 | restoreImageDownload.cancel() 69 | } else if mode == .install { 70 | restoreImageInstall.cancel() 71 | } 72 | } 73 | } 74 | 75 | extension ProgressViewController: ProgressDelegate { 76 | func progress(_ progress: Double, progressString: String) { 77 | func updateUI() { 78 | progressIndicator.doubleValue = progress * 100 79 | statusTextField.stringValue = progressString 80 | } 81 | 82 | if Thread.isMainThread { 83 | updateUI() 84 | } else { 85 | DispatchQueue.main.async { 86 | updateUI() 87 | } 88 | } 89 | } 90 | 91 | func done(error: Error? = nil) { 92 | DispatchQueue.main.async { [weak self] in 93 | if let mainViewController = self?.presentingViewController as? MainViewController { 94 | mainViewController.dismiss(self) 95 | if let error = error { 96 | mainViewController.showErrorAlert(error: error) 97 | self?.statusTextField.stringValue = "Install Failed." 98 | self?.cancelButton.title = "Close" 99 | } else { 100 | self?.cancelButton.title = "Done" 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /virtualOS/ViewController/RestoreImageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestoreImageViewController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | import Virtualization 11 | import OSLog 12 | 13 | #if arch(arm64) 14 | 15 | final class RestoreImageViewController: NSViewController { 16 | let fileModel = FileModel() 17 | fileprivate var selectedRestoreImage = "" 18 | 19 | @IBOutlet weak var tableView: NSTableView! 20 | @IBOutlet weak var installButton: NSButton! 21 | @IBOutlet weak var infoTextField: NSTextField! 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | tableView.dataSource = self 26 | tableView.delegate = self 27 | } 28 | 29 | override func viewWillAppear() { 30 | super.viewWillAppear() 31 | tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) 32 | updateInfoLabel() 33 | if tableView.numberOfRows > 0 { 34 | installButton.isEnabled = true 35 | } else { 36 | installButton.isEnabled = false 37 | } 38 | } 39 | 40 | @IBAction func installButtonPressed(_ sender: NSButton) { 41 | if tableView.selectedRow != -1 { 42 | let notification = Notification(name: Constants.restoreImageNameSelectedNotification, userInfo: [Constants.selectedRestoreImage: self.selectedRestoreImage]) 43 | NotificationCenter.default.post(notification) 44 | view.window?.close() 45 | } 46 | } 47 | 48 | @IBAction func downloadLatestButtonPressed(_ sender: Any) { 49 | let notification = Notification(name: Constants.restoreImageNameSelectedNotification, userInfo: [Constants.selectedRestoreImage: Constants.restoreImageNameLatest]) 50 | NotificationCenter.default.post(notification) 51 | 52 | view.window?.close() 53 | } 54 | 55 | fileprivate func updateInfoLabel() { 56 | if tableView.selectedRow < fileModel.getRestoreImages().count && 57 | tableView.selectedRow != -1 58 | { 59 | let name = fileModel.getRestoreImages()[tableView.selectedRow] 60 | let url = URL.baseURL.appendingPathComponent(name) 61 | VZMacOSRestoreImage.load(from: url) { result in 62 | switch result { 63 | case .success(let restoreImage): 64 | DispatchQueue.main.async { [weak self] in 65 | self?.infoTextField.stringValue = restoreImage.operatingSystemVersionString 66 | } 67 | case .failure(let error): 68 | Logger.shared.log(level: .default, "\(error)") 69 | } 70 | } 71 | } else { 72 | infoTextField.stringValue = "" 73 | } 74 | } 75 | } 76 | 77 | extension RestoreImageViewController: NSTableViewDataSource { 78 | func numberOfRows(in tableView: NSTableView) -> Int { 79 | return fileModel.getRestoreImages().count 80 | } 81 | 82 | func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { 83 | let restoreImages = fileModel.getRestoreImages() 84 | if row < restoreImages.count { 85 | return restoreImages[row] 86 | } else { 87 | return "Unknown" 88 | } 89 | } 90 | } 91 | 92 | extension RestoreImageViewController: NSTableViewDelegate { 93 | func tableViewSelectionDidChange(_ notification: Notification) { 94 | let restoreImages = fileModel.getRestoreImages() 95 | 96 | let selectedRow = tableView.selectedRow 97 | if selectedRow != -1 && selectedRow < restoreImages.count { 98 | selectedRestoreImage = restoreImages[selectedRow] 99 | installButton.isEnabled = true 100 | } else { 101 | installButton.isEnabled = false 102 | } 103 | updateInfoLabel() 104 | } 105 | } 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /virtualOS/ViewController/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import AppKit 10 | import OSLog 11 | 12 | final class SettingsViewController: NSViewController { 13 | @IBOutlet weak var vmFilesURLLabel: NSTextField! 14 | 15 | @IBAction func selectFolderButtonPressed(_ sender: Any) { 16 | let openPanel = NSOpenPanel() 17 | openPanel.allowsMultipleSelection = false 18 | openPanel.canChooseDirectories = true 19 | openPanel.canChooseFiles = false 20 | openPanel.prompt = "Select" 21 | let modalResponse = openPanel.runModal() 22 | 23 | guard modalResponse == .OK, 24 | let vmFilesURL = openPanel.url else 25 | { 26 | return 27 | } 28 | 29 | if let bookmarkData = Bookmark.createBookmarkData(fromUrl: vmFilesURL), 30 | Bookmark.startAccess(bookmarkData: bookmarkData, for: Bookmark.vmFilesLocation) != nil 31 | { 32 | UserDefaults.standard.vmFilesDirectory = vmFilesURL.path() 33 | UserDefaults.standard.vmFilesDirectoryBookmarkData = bookmarkData 34 | postNotification() 35 | } else { 36 | Logger.shared.log("Could not create or start accessing bookmark \(vmFilesURL.absoluteString)") 37 | } 38 | } 39 | 40 | @IBAction func resetButtonPressed(_ sender: Any) { 41 | UserDefaults.standard.vmFilesDirectory = nil 42 | UserDefaults.standard.vmFilesDirectoryBookmarkData = nil 43 | postNotification() 44 | } 45 | 46 | @IBAction func showInFinderButtonPressed(_ sender: Any) { 47 | var url: URL 48 | if let hardDiskDirectoryString = UserDefaults.standard.vmFilesDirectory { 49 | url = URL(fileURLWithPath: hardDiskDirectoryString) 50 | } else { 51 | url = URL.baseURL 52 | } 53 | NSWorkspace.shared.activateFileViewerSelecting([url]) 54 | } 55 | 56 | override func viewWillAppear() { 57 | updateVMFilesLabel() 58 | } 59 | 60 | fileprivate func updateVMFilesLabel() { 61 | vmFilesURLLabel.stringValue = "Storing VM files at:\n\(UserDefaults.standard.vmFilesDirectory ?? URL.basePath)" 62 | } 63 | 64 | fileprivate func postNotification() { 65 | updateVMFilesLabel() 66 | NotificationCenter.default.post(name: Constants.didChangeVMLocationNotification, object: nil) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /virtualOS/ViewController/VMViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMViewController.swift 3 | // virtualOS 4 | // 5 | // Created by Jahn Bertsch. 6 | // Licensed under the Apache License, see LICENSE file. 7 | // 8 | 9 | import Virtualization 10 | import OSLog 11 | 12 | #if arch(arm64) 13 | 14 | final class VMViewController: NSViewController { 15 | @IBOutlet var containerView: NSView! 16 | @IBOutlet weak var statusLabel: NSTextField! 17 | 18 | var vmBundle: VMBundle? 19 | var vmParameters: VMParameters? 20 | fileprivate var vmConfiguration: VMConfiguration? 21 | fileprivate var vm: VZVirtualMachine? 22 | fileprivate let vmView = VZVirtualMachineView() 23 | fileprivate let queue = DispatchQueue.global(qos: .userInteractive) 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | createVM() 29 | setupConstraints() 30 | 31 | queue.async { [weak self] in 32 | self?.vm?.start { (result: Result) in 33 | switch result { 34 | case .success: 35 | Logger.shared.log(level: .default, "running") 36 | case .failure(let error): 37 | Logger.shared.log(level: .default, "running failed \(error.localizedDescription)") 38 | } 39 | } 40 | } 41 | } 42 | 43 | // MARK: - Private 44 | 45 | fileprivate func createVM() { 46 | guard let bundleURL = vmBundle?.url, 47 | let vmParameters = vmParameters, 48 | let macPlatformConfiguration = MacPlatformConfiguration.read(fromBundleURL: bundleURL) else 49 | { 50 | Logger.shared.log(level: .default, "could not create vm config") 51 | return 52 | } 53 | 54 | let vmConfiguration = VMConfiguration() 55 | vmConfiguration.setup(parameters: vmParameters, macPlatformConfiguration: macPlatformConfiguration, bundleURL: bundleURL) 56 | self.vmConfiguration = vmConfiguration 57 | 58 | do { 59 | try vmConfiguration.validate() 60 | Logger.shared.log(level: .default, "vm configuration is valid, using \(vmParameters.cpuCount) cpus and \(vmParameters.memorySizeInGB) gb ram") 61 | } catch let error { 62 | Logger.shared.log(level: .default, "failed to validate vm configuration: \(error.localizedDescription)") 63 | return 64 | } 65 | 66 | let vm = VZVirtualMachine(configuration: vmConfiguration, queue: queue) 67 | vm.delegate = self 68 | 69 | vmView.virtualMachine = vm 70 | vmView.automaticallyReconfiguresDisplay = true 71 | vmView.capturesSystemKeys = true 72 | self.vm = vm 73 | } 74 | 75 | fileprivate func setupConstraints() { 76 | if let containerView { 77 | let top = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: vmView, attribute: .top, multiplier: 1, constant: 0) 78 | let bottom = NSLayoutConstraint(item: containerView, attribute: .bottom, relatedBy: .equal, toItem: vmView, attribute: .bottom, multiplier: 1, constant: 0) 79 | let leading = NSLayoutConstraint(item: containerView, attribute: .leading, relatedBy: .equal, toItem: vmView, attribute: .leading, multiplier: 1, constant: 0) 80 | let trailing = NSLayoutConstraint(item: containerView, attribute: .trailing, relatedBy: .equal, toItem: vmView, attribute: .trailing, multiplier: 1, constant: 0) 81 | 82 | containerView.addSubview(vmView) 83 | containerView.addConstraint(top) 84 | containerView.addConstraint(bottom) 85 | containerView.addConstraint(leading) 86 | containerView.addConstraint(trailing) 87 | 88 | let centerX = NSLayoutConstraint(item: containerView, attribute: .centerX, relatedBy: .equal, toItem: statusLabel, attribute: .centerX, multiplier: 1, constant: 1) 89 | let centerY = NSLayoutConstraint(item: containerView, attribute: .centerY, relatedBy: .equal, toItem: statusLabel, attribute: .centerY, multiplier: 1, constant: 0) 90 | 91 | statusLabel.stringValue = "" 92 | statusLabel.removeFromSuperview() 93 | containerView.addSubview(statusLabel) 94 | containerView.addConstraint(centerX) 95 | containerView.addConstraint(centerY) 96 | } 97 | } 98 | } 99 | 100 | extension VMViewController: VZVirtualMachineDelegate { 101 | func guestDidStop(_ virtualMachine: VZVirtualMachine) { 102 | let message = "Guest did stop" 103 | DispatchQueue.main.async { [weak self] in 104 | self?.statusLabel.stringValue = message 105 | } 106 | Logger.shared.log(level: .default, "\(message)") 107 | } 108 | 109 | func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: any Error) { 110 | DispatchQueue.main.async { [weak self] in 111 | self?.statusLabel.stringValue = "Guest did stop with error: \(error.localizedDescription)" 112 | } 113 | Logger.shared.log(level: .default, "\(self.statusLabel.stringValue)") 114 | } 115 | } 116 | 117 | #endif 118 | --------------------------------------------------------------------------------