diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..606bb2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +virtualOS.xcodeproj/xcuserdata +*.xcuserdatad +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5d1758c --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2022 Jahn Bertsch +Copyright 2021 Khaos Tian + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the License. + + – + +Copyright © 2021 Apple Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..84874d3 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# virtualOS + +Run a virtual macOS machine on your Apple Silicon computer. + +On first start, the latest macOS restore image is automatically downloaded from Apple servers. After installation is finished, you can start using the virtual machine by performing the initial operating system setup. + +You can configure the following virtual machine parameters: +- CPU count +- RAM +- Screen size + +## Download + +You can download this app from the [macOS AppStore](https://apps.apple.com/us/app/virtualos/id1614659226) + +This application is open source software, source code is available at: https://github.com/yep/virtualOS + +Mac and macOS are trademarks of Apple Inc., registered in the U.S. and other countries and regions. diff --git a/virtualOS.xcodeproj/project.pbxproj b/virtualOS.xcodeproj/project.pbxproj new file mode 100644 index 0000000..159abc7 --- /dev/null +++ b/virtualOS.xcodeproj/project.pbxproj @@ -0,0 +1,645 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 0005A77A27E2809E0013BE83 /* VirtualMachineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0005A77927E2809E0013BE83 /* VirtualMachineView.swift */; }; + 0044A65527F601E60007988A /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0044A65427F601E60007988A /* MainViewModel.swift */; }; + 0044A65A27F76BD30007988A /* URL+Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0044A65927F76BD30007988A /* URL+Paths.swift */; }; + 006504E727F9D59300723BCA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 006504E627F9D59300723BCA /* SettingsView.swift */; }; + 007987AF27E2487200960D74 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 007987AE27E2487200960D74 /* LICENSE */; }; + 007987B127E24A8400960D74 /* VirtualMac.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007987B027E24A8400960D74 /* VirtualMac.swift */; }; + 0090AF6127E25F6F0077D35F /* UInt64+Byte.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0090AF6027E25F6F0077D35F /* UInt64+Byte.swift */; }; + 00989C6427E2340C0048776B /* virtualOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C6327E2340C0048776B /* virtualOSApp.swift */; }; + 00989C6827E2340D0048776B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00989C6727E2340D0048776B /* Assets.xcassets */; }; + 00989C6B27E2340D0048776B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00989C6A27E2340D0048776B /* Preview Assets.xcassets */; }; + 00989C7627E2340D0048776B /* virtualOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C7527E2340D0048776B /* virtualOSTests.swift */; }; + 00989C8027E2340D0048776B /* virtualOSUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C7F27E2340D0048776B /* virtualOSUITests.swift */; }; + 00989C8227E2340D0048776B /* virtualOSUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C8127E2340D0048776B /* virtualOSUITestsLaunchTests.swift */; }; + 00989C9627E236A10048776B /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C9427E236A10048776B /* MainView.swift */; }; + 00989C9A27E238930048776B /* VirtualMacConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00989C9927E238930048776B /* VirtualMacConfiguration.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 00989C7227E2340D0048776B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00989C5827E2340C0048776B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 00989C5F27E2340C0048776B; + remoteInfo = virtualOS; + }; + 00989C7C27E2340D0048776B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00989C5827E2340C0048776B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 00989C5F27E2340C0048776B; + remoteInfo = virtualOS; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0005A77927E2809E0013BE83 /* VirtualMachineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMachineView.swift; sourceTree = ""; }; + 0044A65427F601E60007988A /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = ""; }; + 0044A65927F76BD30007988A /* URL+Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Paths.swift"; sourceTree = ""; }; + 006504E627F9D59300723BCA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 007987AE27E2487200960D74 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + 007987B027E24A8400960D74 /* VirtualMac.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMac.swift; sourceTree = ""; }; + 0090AF6027E25F6F0077D35F /* UInt64+Byte.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UInt64+Byte.swift"; sourceTree = ""; }; + 00989C6027E2340C0048776B /* virtualOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = virtualOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 00989C6327E2340C0048776B /* virtualOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = virtualOSApp.swift; sourceTree = ""; }; + 00989C6727E2340D0048776B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 00989C6A27E2340D0048776B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 00989C6C27E2340D0048776B /* virtualOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = virtualOS.entitlements; sourceTree = ""; }; + 00989C7127E2340D0048776B /* virtualOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = virtualOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00989C7527E2340D0048776B /* virtualOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = virtualOSTests.swift; sourceTree = ""; }; + 00989C7B27E2340D0048776B /* virtualOSUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = virtualOSUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00989C7F27E2340D0048776B /* virtualOSUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = virtualOSUITests.swift; sourceTree = ""; }; + 00989C8127E2340D0048776B /* virtualOSUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = virtualOSUITestsLaunchTests.swift; sourceTree = ""; }; + 00989C9427E236A10048776B /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; + 00989C9927E238930048776B /* VirtualMacConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMacConfiguration.swift; sourceTree = ""; }; + 00BA26AC2826DAF200E80B76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 00989C5D27E2340C0048776B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 00989C6E27E2340D0048776B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 00989C7827E2340D0048776B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0090AF5F27E25F6F0077D35F /* Extension */ = { + isa = PBXGroup; + children = ( + 0090AF6027E25F6F0077D35F /* UInt64+Byte.swift */, + 0044A65927F76BD30007988A /* URL+Paths.swift */, + ); + path = Extension; + sourceTree = ""; + }; + 00989C5727E2340C0048776B = { + isa = PBXGroup; + children = ( + 007987AE27E2487200960D74 /* LICENSE */, + 00989C6227E2340C0048776B /* virtualOS */, + 00989C7427E2340D0048776B /* virtualOSTests */, + 00989C7E27E2340D0048776B /* virtualOSUITests */, + 00989C6127E2340C0048776B /* Products */, + ); + sourceTree = ""; + }; + 00989C6127E2340C0048776B /* Products */ = { + isa = PBXGroup; + children = ( + 00989C6027E2340C0048776B /* virtualOS.app */, + 00989C7127E2340D0048776B /* virtualOSTests.xctest */, + 00989C7B27E2340D0048776B /* virtualOSUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 00989C6227E2340C0048776B /* virtualOS */ = { + isa = PBXGroup; + children = ( + 00BA26AC2826DAF200E80B76 /* Info.plist */, + 00989C6327E2340C0048776B /* virtualOSApp.swift */, + 00989C9127E236A10048776B /* Model */, + 00989C9327E236A10048776B /* View */, + 0090AF5F27E25F6F0077D35F /* Extension */, + 00989C6727E2340D0048776B /* Assets.xcassets */, + 00989C6C27E2340D0048776B /* virtualOS.entitlements */, + 00989C6927E2340D0048776B /* Preview Content */, + ); + path = virtualOS; + sourceTree = ""; + }; + 00989C6927E2340D0048776B /* Preview Content */ = { + isa = PBXGroup; + children = ( + 00989C6A27E2340D0048776B /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 00989C7427E2340D0048776B /* virtualOSTests */ = { + isa = PBXGroup; + children = ( + 00989C7527E2340D0048776B /* virtualOSTests.swift */, + ); + path = virtualOSTests; + sourceTree = ""; + }; + 00989C7E27E2340D0048776B /* virtualOSUITests */ = { + isa = PBXGroup; + children = ( + 00989C7F27E2340D0048776B /* virtualOSUITests.swift */, + 00989C8127E2340D0048776B /* virtualOSUITestsLaunchTests.swift */, + ); + path = virtualOSUITests; + sourceTree = ""; + }; + 00989C9127E236A10048776B /* Model */ = { + isa = PBXGroup; + children = ( + 0044A65427F601E60007988A /* MainViewModel.swift */, + 007987B027E24A8400960D74 /* VirtualMac.swift */, + 00989C9927E238930048776B /* VirtualMacConfiguration.swift */, + ); + path = Model; + sourceTree = ""; + }; + 00989C9327E236A10048776B /* View */ = { + isa = PBXGroup; + children = ( + 00989C9427E236A10048776B /* MainView.swift */, + 0005A77927E2809E0013BE83 /* VirtualMachineView.swift */, + 006504E627F9D59300723BCA /* SettingsView.swift */, + ); + path = View; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 00989C5F27E2340C0048776B /* virtualOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00989C8527E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOS" */; + buildPhases = ( + 00989C5C27E2340C0048776B /* Sources */, + 00989C5D27E2340C0048776B /* Frameworks */, + 00989C5E27E2340C0048776B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = virtualOS; + productName = virtualOS; + productReference = 00989C6027E2340C0048776B /* virtualOS.app */; + productType = "com.apple.product-type.application"; + }; + 00989C7027E2340D0048776B /* virtualOSTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00989C8827E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOSTests" */; + buildPhases = ( + 00989C6D27E2340D0048776B /* Sources */, + 00989C6E27E2340D0048776B /* Frameworks */, + 00989C6F27E2340D0048776B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 00989C7327E2340D0048776B /* PBXTargetDependency */, + ); + name = virtualOSTests; + productName = virtualOSTests; + productReference = 00989C7127E2340D0048776B /* virtualOSTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 00989C7A27E2340D0048776B /* virtualOSUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00989C8B27E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOSUITests" */; + buildPhases = ( + 00989C7727E2340D0048776B /* Sources */, + 00989C7827E2340D0048776B /* Frameworks */, + 00989C7927E2340D0048776B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 00989C7D27E2340D0048776B /* PBXTargetDependency */, + ); + name = virtualOSUITests; + productName = virtualOSUITests; + productReference = 00989C7B27E2340D0048776B /* virtualOSUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 00989C5827E2340C0048776B /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1330; + LastUpgradeCheck = 1330; + TargetAttributes = { + 00989C5F27E2340C0048776B = { + CreatedOnToolsVersion = 13.3; + }; + 00989C7027E2340D0048776B = { + CreatedOnToolsVersion = 13.3; + TestTargetID = 00989C5F27E2340C0048776B; + }; + 00989C7A27E2340D0048776B = { + CreatedOnToolsVersion = 13.3; + TestTargetID = 00989C5F27E2340C0048776B; + }; + }; + }; + buildConfigurationList = 00989C5B27E2340C0048776B /* Build configuration list for PBXProject "virtualOS" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 00989C5727E2340C0048776B; + productRefGroup = 00989C6127E2340C0048776B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 00989C5F27E2340C0048776B /* virtualOS */, + 00989C7027E2340D0048776B /* virtualOSTests */, + 00989C7A27E2340D0048776B /* virtualOSUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 00989C5E27E2340C0048776B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 007987AF27E2487200960D74 /* LICENSE in Resources */, + 00989C6B27E2340D0048776B /* Preview Assets.xcassets in Resources */, + 00989C6827E2340D0048776B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 00989C6F27E2340D0048776B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 00989C7927E2340D0048776B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 00989C5C27E2340C0048776B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00989C9627E236A10048776B /* MainView.swift in Sources */, + 0005A77A27E2809E0013BE83 /* VirtualMachineView.swift in Sources */, + 00989C6427E2340C0048776B /* virtualOSApp.swift in Sources */, + 007987B127E24A8400960D74 /* VirtualMac.swift in Sources */, + 0044A65A27F76BD30007988A /* URL+Paths.swift in Sources */, + 006504E727F9D59300723BCA /* SettingsView.swift in Sources */, + 00989C9A27E238930048776B /* VirtualMacConfiguration.swift in Sources */, + 0090AF6127E25F6F0077D35F /* UInt64+Byte.swift in Sources */, + 0044A65527F601E60007988A /* MainViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 00989C6D27E2340D0048776B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00989C7627E2340D0048776B /* virtualOSTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 00989C7727E2340D0048776B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00989C8227E2340D0048776B /* virtualOSUITestsLaunchTests.swift in Sources */, + 00989C8027E2340D0048776B /* virtualOSUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00989C7327E2340D0048776B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 00989C5F27E2340C0048776B /* virtualOS */; + targetProxy = 00989C7227E2340D0048776B /* PBXContainerItemProxy */; + }; + 00989C7D27E2340D0048776B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 00989C5F27E2340C0048776B /* virtualOS */; + targetProxy = 00989C7C27E2340D0048776B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 00989C8327E2340D0048776B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 00989C8427E2340D0048776B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.3; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 00989C8627E2340D0048776B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = virtualOS/virtualOS.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 2; + DEVELOPMENT_ASSET_PATHS = "\"virtualOS/Preview Content\""; + DEVELOPMENT_TEAM = 2AD47BTDQ6; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = virtualOS/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 00989C8727E2340D0048776B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = virtualOS/virtualOS.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 2; + DEVELOPMENT_ASSET_PATHS = "\"virtualOS/Preview Content\""; + DEVELOPMENT_TEAM = 2AD47BTDQ6; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = virtualOS/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 00989C8927E2340D0048776B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2AD47BTDQ6; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.3; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOSTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/virtualOS.app/Contents/MacOS/virtualOS"; + }; + name = Debug; + }; + 00989C8A27E2340D0048776B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2AD47BTDQ6; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.3; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOSTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/virtualOS.app/Contents/MacOS/virtualOS"; + }; + name = Release; + }; + 00989C8C27E2340D0048776B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2AD47BTDQ6; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOSUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = virtualOS; + }; + name = Debug; + }; + 00989C8D27E2340D0048776B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2AD47BTDQ6; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOSUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = virtualOS; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 00989C5B27E2340C0048776B /* Build configuration list for PBXProject "virtualOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00989C8327E2340D0048776B /* Debug */, + 00989C8427E2340D0048776B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 00989C8527E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00989C8627E2340D0048776B /* Debug */, + 00989C8727E2340D0048776B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 00989C8827E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOSTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00989C8927E2340D0048776B /* Debug */, + 00989C8A27E2340D0048776B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 00989C8B27E2340D0048776B /* Build configuration list for PBXNativeTarget "virtualOSUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00989C8C27E2340D0048776B /* Debug */, + 00989C8D27E2340D0048776B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 00989C5827E2340C0048776B /* Project object */; +} diff --git a/virtualOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/virtualOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/virtualOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/virtualOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/virtualOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/virtualOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/virtualOS.xcodeproj/xcshareddata/xcschemes/virtualOS.xcscheme b/virtualOS.xcodeproj/xcshareddata/xcschemes/virtualOS.xcscheme new file mode 100644 index 0000000..9262224 --- /dev/null +++ b/virtualOS.xcodeproj/xcshareddata/xcschemes/virtualOS.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/virtualOS/Assets.xcassets/AccentColor.colorset/Contents.json b/virtualOS/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/virtualOS/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/virtualOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..6b2539c --- /dev/null +++ b/virtualOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "virtualOS-macOS-16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "virtualOS-macOS-32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "virtualOS-macOS-32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "virtualOS-macOS-64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "virtualOS-macOS-128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "virtualOS-macOS-256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "virtualOS-macOS-256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "virtualOS-macOS-512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "virtualOS-macOS-512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "virtualOS-macOS-1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-1024.png b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-1024.png new file mode 100644 index 0000000..95a3916 Binary files /dev/null and b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-1024.png differ diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-128.png b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-128.png new file mode 100644 index 0000000..297da38 Binary files /dev/null and b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-128.png differ diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-16.png b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-16.png new file mode 100644 index 0000000..5e4af66 Binary files /dev/null and b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-16.png differ diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-256.png b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-256.png new file mode 100644 index 0000000..09ffe1b Binary files /dev/null and b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-256.png differ diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-32.png b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-32.png new file mode 100644 index 0000000..c0f180f Binary files /dev/null and b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-32.png differ diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-512.png b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-512.png new file mode 100644 index 0000000..159dc7c Binary files /dev/null and b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-512.png differ diff --git a/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-64.png b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-64.png new file mode 100644 index 0000000..edf36e8 Binary files /dev/null and b/virtualOS/Assets.xcassets/AppIcon.appiconset/virtualOS-macOS-64.png differ diff --git a/virtualOS/Assets.xcassets/Contents.json b/virtualOS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/virtualOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/virtualOS/Extension/UInt64+Byte.swift b/virtualOS/Extension/UInt64+Byte.swift new file mode 100644 index 0000000..06a2256 --- /dev/null +++ b/virtualOS/Extension/UInt64+Byte.swift @@ -0,0 +1,18 @@ +// +// UInt+Byte.swift +// virtualOS +// +// Created by Jahn Bertsch on 16.03.22. +// + +import Foundation + +extension UInt64 { + func bytesToGigabytes() -> UInt64 { + return self / (1024 * 1024 * 1024) + } + + func gigabytesToBytes() -> UInt64 { + return self * 1024 * 1024 * 1024 + } +} diff --git a/virtualOS/Extension/URL+Paths.swift b/virtualOS/Extension/URL+Paths.swift new file mode 100644 index 0000000..40322bc --- /dev/null +++ b/virtualOS/Extension/URL+Paths.swift @@ -0,0 +1,19 @@ +// +// URL+Paths.swift +// virtualOS +// + +import Foundation + +extension URL { + static let restoreImageURL = URL(fileURLWithPath: NSHomeDirectory() + "/RestoreImage.ipsw") + + static let vmBundlePath = NSHomeDirectory() + "/virtualOS.bundle/" + static let vmBundleURL = URL(fileURLWithPath: vmBundlePath) + static let diskImageURL = URL(fileURLWithPath: vmBundlePath + "Disk.img") + static let auxiliaryStorageURL = URL(fileURLWithPath: vmBundlePath + "AuxiliaryStorage") + static let machineIdentifierURL = URL(fileURLWithPath: vmBundlePath + "MachineIdentifier") + static let hardwareModelURL = URL(fileURLWithPath: vmBundlePath + "HardwareModel") + static let parametersURL = URL(fileURLWithPath: vmBundlePath + "Parameters.txt") +} + diff --git a/virtualOS/Info.plist b/virtualOS/Info.plist new file mode 100644 index 0000000..bc11256 --- /dev/null +++ b/virtualOS/Info.plist @@ -0,0 +1,8 @@ + + + + + ITSAppUsesNonExemptEncryption + + + diff --git a/virtualOS/Model/MainViewModel.swift b/virtualOS/Model/MainViewModel.swift new file mode 100644 index 0000000..ff52cd0 --- /dev/null +++ b/virtualOS/Model/MainViewModel.swift @@ -0,0 +1,275 @@ +// +// MainViewModel.swift +// virtualOS +// +// Created by Jahn Bertsch on 31.03.22. +// + +#if arch(arm64) + +import Foundation +import Virtualization + +final class MainViewModel: NSObject, ObservableObject { + enum State: String { + case Downloading + case Installing + case Starting + case Running + case Stopping + case Stopped + } + + @Published var virtualMac = VirtualMac() + @Published var virtualMachine: VZVirtualMachine? + @Published var statusLabel = "" + @Published var buttonLabel = "" + @Published var buttonDisabled = false + @Published var installProgress: Progress? + @Published var showLicenseInformationModal = false + @Published var showConfirmationAlert = false + @Published var licenseInformationTitleString = "" + @Published var licenseInformationString = "" + @Published var confirmationText = "" + @Published var confirmationHandler: CompletionHander = {_ in} + @Published var state = State.Stopped { + didSet { + debugLog(self.state.rawValue) + updateLabels(for: self.state) + } + } + static var bundleExists: Bool { + return FileManager.default.fileExists(atPath: URL.vmBundlePath) + } + static var diskImageExists: Bool { + return FileManager.default.fileExists(atPath: URL.diskImageURL.path) + } + static var restoreImageExists: Bool { + return FileManager.default.fileExists(atPath: URL.restoreImageURL.path) + } + var settingsShown: Bool { + return (Self.diskImageExists || Self.restoreImageExists) && state == .Stopped + } + + override init() { + super.init() + updateLabels(for: state) + readParametersFromDisk() + loadLicenseInformationFromBundle() + } + + func buttonPressed() { + switch state { + case .Stopped: + start() + case .Downloading: + virtualMac.stopDownload() + state = .Stopped + case .Installing, .Starting, .Running, .Stopping: + stop() + } + } + + func deleteRestoreImage() { + confirmationText = "Restore Image" + confirmationHandler = { _ in + do { + try FileManager.default.removeItem(atPath: URL.restoreImageURL.path) + } catch { + self.display(errorString: "Error: Could not delete restore image") + } + } + showConfirmationAlert = !showConfirmationAlert + } + + func deleteVirtualMachine() { + confirmationText = "Virtual Machine" + confirmationHandler = { _ in + if Self.bundleExists { + self.stop() + do { + try FileManager.default.removeItem(at: URL.vmBundleURL) + self.updateLabels(for: self.state) + } catch { + self.display(errorString: "Error: Could not delete virtual machine") + } + } + } + showConfirmationAlert = !showConfirmationAlert + } + + func loadLicenseInformationFromBundle() { + if let filepath = Bundle.main.path(forResource: "LICENSE", ofType: "") { + do { + let contents = try String(contentsOfFile: filepath) + licenseInformationString = contents + } catch { + licenseInformationString = "Failed to load license information" + } + } else { + licenseInformationString = "License information not found" + } + + licenseInformationTitleString = "virtualOS" + if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, + let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + { + licenseInformationTitleString += " \(version) (Build \(build))" + } + } + + // MARK: - Private + + fileprivate func readParametersFromDisk() { + if Self.diskImageExists { + // read previous vm settings + if let errorString = virtualMac.readFromDisk(delegate: self) { + display(errorString: errorString) + } + } else if Self.restoreImageExists { + virtualMac.loadParametersFromRestoreImage { (errorString: String?) in + if let errorString = errorString { + self.display(errorString: errorString) + } + } + } + } + + fileprivate func start() { + debugLog("Using storage directory \(URL.vmBundlePath)") + if FileManager.default.fileExists(atPath: URL.diskImageURL.path) { + startFromDiskImage() + } else if FileManager.default.fileExists(atPath: URL.restoreImageURL.path) { + install(virtualMac: virtualMac) + } else { + downloadAndInstall() + } + } + + fileprivate func downloadAndInstall() { + state = .Downloading + buttonLabel = "Stop" + + virtualMac.downloadRestoreImage { (progress: Progress) in + debugLog("Download progress: \(progress.fractionCompleted * 100)%") + self.installProgress = progress + self.updateLabels(for: self.state) + } completionHandler: { (errorString: String?) in + if let errorString = errorString { + self.display(errorString: "Download of restore image failed: \(errorString)") + } else { + debugLog("Download of restore image completed") + self.install(virtualMac: self.virtualMac) + } + } + } + + fileprivate func install(virtualMac: VirtualMac) { + state = .Installing + virtualMac.install(delegate: self) { (progress: Progress) in + debugLog("Install progress: \(progress.completedUnitCount)%") + self.installProgress = progress + self.updateLabels(for: self.state) + } completionHandler: { (errorMessage: String?, virtualMachine: VZVirtualMachine?) in + self.installProgress = nil + if let errorMessage = errorMessage { + self.display(errorString: errorMessage) + } else if let virtualMachine = virtualMachine { + self.start(virtualMachine: virtualMachine) + } else { + self.display(errorString: "Error: Install finished but no virtual machine created") + } + } + } + + fileprivate func startFromDiskImage() { + guard let virtualMachine = virtualMac.createVirtualMachine(delegate: self) else { + display(errorString: "Error: Failed to read virtual machine from disk") + return + } + + start(virtualMachine: virtualMachine) + } + + fileprivate func start(virtualMachine: VZVirtualMachine) { + self.state = .Starting + self.virtualMachine = virtualMachine + + if let errorString = virtualMac.writeParametersToDisk() { + display(errorString: errorString) + } + + virtualMachine.start { (result: Result) in + switch result { + case .success: + self.state = .Running + case .failure(let error): + self.display(errorString: "Error while starting: \(error)") + } + } + } + + fileprivate func stop() { + guard let virtualMachine = virtualMachine else { + return // already stopped + } + state = .Stopping + + virtualMachine.stop(completionHandler: { (error: Error?) in + self.state = .Stopped + if let error = error { + self.display(errorString: error.localizedDescription) + } + self.virtualMachine = nil + }) + } + + fileprivate func display(errorString: String) { + debugLog(errorString) + self.state = .Stopped + self.statusLabel = errorString + } + + fileprivate func updateLabels(for: State) { + switch state { + case .Stopped: + statusLabel = state.rawValue + buttonLabel = "Start" + case .Downloading: + if let installProgress = installProgress { + statusLabel = String(format: "Downloading restore image: %2.2f%%", installProgress.fractionCompleted * 100) + } + buttonLabel = "Stop" + case .Installing: + if let installProgress = installProgress { + if installProgress.completedUnitCount == 0 { + statusLabel = "Installing: Waiting for begin …" + } else { + statusLabel = "Installing: \(installProgress.completedUnitCount)%" + } + } + buttonLabel = "Stop" + case .Starting, .Running, .Stopping: + statusLabel = state.rawValue + buttonLabel = "Stop" + } + + if state == .Installing { + buttonDisabled = true // installing can not be canceled + } else { + buttonDisabled = false + } + } +} + +extension MainViewModel: VZVirtualMachineDelegate { + func guestDidStop(_ vm: VZVirtualMachine) { + state = .Stopped + } + + func virtualMachine(_ vm: VZVirtualMachine, didStopWithError error: Error) { + display(errorString: error.localizedDescription) + } +} + +#endif diff --git a/virtualOS/Model/VirtualMac.swift b/virtualOS/Model/VirtualMac.swift new file mode 100644 index 0000000..19b5a45 --- /dev/null +++ b/virtualOS/Model/VirtualMac.swift @@ -0,0 +1,339 @@ +// +// VirtualMac.swift +// virtualOS +// +// Created by Jahn Bertsch on 16.03.22. +// + +#if arch(arm64) + +import Virtualization + +final class VirtualMac: ObservableObject { + struct Parameters: Codable { + var cpuCount = 1 + var cpuCountMin = 1 + var cpuCountMax = 2 + var diskSizeInGB: UInt64 = 64 + var memorySizeInGB: UInt64 = 1 + var memorySizeInGBMin: UInt64 = 1 + var memorySizeInGBMax: UInt64 = 2 + var screenWidth = 1500 + var screenHeight = 900 + var pixelsPerInch = 250 + var microphoneEnabled = false + } + + typealias InstallCompletionHander = (String?, VZVirtualMachine?) -> Void + + var parameters = Parameters() + var virtualMachineConfiguration: VirtualMacConfiguration? + fileprivate var progressObserver: NSKeyValueObservation? + fileprivate var downloadTask: URLSessionDownloadTask? + + func readFromDisk(delegate: VZVirtualMachineDelegate) -> String? { + if let errorString = readParametersFromDisk() { + return errorString + } + + let virtualMacConfiguration = VirtualMacConfiguration() + virtualMacConfiguration.readFromDisk(using: ¶meters) + + do { + try virtualMacConfiguration.validate() + self.virtualMachineConfiguration = virtualMacConfiguration + } catch { + return "Error: Failed to validate virtual machine configuration from disk" + } + + return nil + } + + func downloadRestoreImage(progressHandler: @escaping ProgressHandler, completionHandler: @escaping CompletionHander) { + if let errorString = createBundle() { + completionHandler(errorString) + return + } + + if FileManager.default.fileExists(atPath: URL.restoreImageURL.path) { + completionHandler(nil) // done: already downloaded + } else { + fetchLatestSupportedRestoreImage(progressHandler: progressHandler, completionHandler: { (errorString: String?) in + if let errorString = errorString { + completionHandler(errorString) + } else { + completionHandler(nil) + } + }) + } + } + + func install(delegate: VZVirtualMachineDelegate, progressHandler: @escaping ProgressHandler, completionHandler: @escaping InstallCompletionHander) { + loadParametersFromRestoreImage { (errorString: String?) in + if let errorString = errorString { + completionHandler(errorString, nil) + } else { + self.loadRestoreImage(delegate: delegate, progressHandler: progressHandler, completionHandler: completionHandler) + } + } + } + + func loadParametersFromRestoreImage(completionHandler: @escaping CompletionHander) { + if let errorString = createBundle() { + completionHandler(errorString) + return + } + + VZMacOSRestoreImage.load(from: URL.restoreImageURL) { (result: Result) in + switch result { + case .success(let restoreImage): + self.loaded(restoreImage: restoreImage, completionHandler: completionHandler) + case .failure(_): + completionHandler("Error: failure reading restore image") + } + } + } + + func loadRestoreImage(delegate: VZVirtualMachineDelegate, progressHandler: @escaping ProgressHandler, completionHandler: @escaping InstallCompletionHander) { + VZMacOSRestoreImage.load(from: URL.restoreImageURL) { (result: Result) in + switch result { + case .success(let restoreImage): + if let errorString = self.restore(from: restoreImage) { + completionHandler(errorString, nil) + } else if let virtualMachineConfiguration = self.virtualMachineConfiguration { + self.startInstall(ipswURL: URL.restoreImageURL, virtualMacConfiguration: virtualMachineConfiguration, delegate: delegate, progressHandler: progressHandler, completionHandler: completionHandler) + } else { + completionHandler("Error: No virtual machine configuration found", nil) + } + case .failure(let failure): + completionHandler("Loading restore image failed: \(failure)", nil) + return + } + } + } + + func createVirtualMachine(delegate: VZVirtualMachineDelegate) -> VZVirtualMachine? { + guard let virtualMacConfiguration = virtualMachineConfiguration else { + return nil + } + virtualMacConfiguration.configure(with: ¶meters) + + do { + try virtualMacConfiguration.validate() + self.virtualMachineConfiguration = virtualMacConfiguration + } catch (let error) { + debugLog("Error: \(error.localizedDescription)") + return nil + } + + if let errorString = writeParametersToDisk() { + debugLog(errorString) + return nil + } + + let virtualMachine = VZVirtualMachine(configuration: virtualMacConfiguration, queue: .main) + virtualMachine.delegate = delegate + + debugLog("Using \(virtualMacConfiguration.cpuCount) cores, \(virtualMacConfiguration.memorySize.bytesToGigabytes()) GB RAM and screen size \(parameters.screenWidth)x\(parameters.screenHeight) px at \(parameters.pixelsPerInch) ppi") + return virtualMachine + } + + func stop(virtualMachine: VZVirtualMachine, completionHandler: @escaping InstallCompletionHander) { + virtualMachine.stop(completionHandler: { (error: Error?) in + if let error = error { + debugLog("Error while stopping: \(error)") + completionHandler(error.localizedDescription, nil) + } else { + debugLog("Stopped") + completionHandler(nil, virtualMachine) // nil: no error + } + }) + } + + func stopDownload() { + downloadTask?.cancel() + } + + func writeParametersToDisk() -> String? { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + do { + let jsonData = try encoder.encode(parameters) + if let json = String(data: jsonData, encoding: .utf8) { + try json.write(to: URL.parametersURL, atomically: true, encoding: String.Encoding.utf8) + } + } catch { + return "Error: Failed to write current CPU and RAM configuration to disk" + } + return nil + } + + // MARK: - Private + + fileprivate func readParametersFromDisk() -> String? { + let decoder = JSONDecoder() + do { + let json = try Data.init(contentsOf: URL.parametersURL) + parameters = try decoder.decode(Parameters.self, from: json) + } catch { + return "Error: Failed to read parameters, please delete virtual machine in 'File' menu" + } + return nil + } + + fileprivate func fetchLatestSupportedRestoreImage(progressHandler: @escaping ProgressHandler, completionHandler: @escaping CompletionHander) { + debugLog("Attempting to download latest available restore image") + VZMacOSRestoreImage.fetchLatestSupported { [self](result: Result) in + switch result { + case let .failure(error): + completionHandler(error.localizedDescription) + case let .success(restoreImage): + downloaded(restoreImage: restoreImage, progressHandler: progressHandler, completionHandler: completionHandler) + } + } + } + + fileprivate func downloaded(restoreImage: VZMacOSRestoreImage, progressHandler: @escaping ProgressHandler, completionHandler: @escaping CompletionHander) { + let downloadTask = URLSession.shared.downloadTask(with: restoreImage.url) { localURL, response, error in + if let error = error { + completionHandler(error.localizedDescription) + return + } + if let localURL = localURL { + try? FileManager.default.moveItem(at: localURL, to: URL.restoreImageURL) + } else { + completionHandler("Error: Failed to move downloaded restore image to \(URL.restoreImageURL)") + return + } + + completionHandler(nil) // no error + } + + self.downloadTask = downloadTask + progressObserver = downloadTask.progress.observe(\.fractionCompleted, options: [.initial, .new]) { (progress, change) in + DispatchQueue.main.async { + progressHandler(downloadTask.progress) + } + } + downloadTask.resume() + } + + fileprivate func loaded(restoreImage: VZMacOSRestoreImage, completionHandler: @escaping CompletionHander) { + virtualMachineConfiguration = VirtualMacConfiguration() + virtualMachineConfiguration?.getBestHardwareConfig(parameters: ¶meters) + + guard let mostFeaturefulSupportedConfiguration = restoreImage.mostFeaturefulSupportedConfiguration else { + completionHandler("Error: No supported hardware configuration available") + return + } + + parameters.cpuCountMin = mostFeaturefulSupportedConfiguration.minimumSupportedCPUCount + parameters.memorySizeInGBMin = mostFeaturefulSupportedConfiguration.minimumSupportedMemorySize.bytesToGigabytes() + + if let errorMessage = writeParametersToDisk() { + completionHandler(errorMessage) + return + } + let version = restoreImage.operatingSystemVersion + debugLog("Restore Image operating system version: \(version.majorVersion).\(version.minorVersion).\(version.patchVersion) (Build \(restoreImage.buildVersion))") + debugLog("Host hardware model is supported: \(mostFeaturefulSupportedConfiguration.hardwareModel.isSupported)") + debugLog("Parameters from disk image: \(parameters)") + + completionHandler(nil) // no error + } + + fileprivate func restore(from restoreImage: VZMacOSRestoreImage) -> String? { + guard let mostFeaturefulSupportedConfiguration = restoreImage.mostFeaturefulSupportedConfiguration else { + return "Error: No supported hardware configuration available" + } + + parameters.cpuCountMin = mostFeaturefulSupportedConfiguration.minimumSupportedCPUCount + parameters.memorySizeInGBMin = mostFeaturefulSupportedConfiguration.minimumSupportedMemorySize.bytesToGigabytes() + + if let errorString = VirtualMac.createDiskImage(sizeInGB: parameters.diskSizeInGB) { + return errorString + } + + let virtualMacConfiguration = VirtualMacConfiguration() + virtualMacConfiguration.create(using: ¶meters, macHardwareModel: mostFeaturefulSupportedConfiguration.hardwareModel) + + do { + try virtualMacConfiguration.validate() + virtualMachineConfiguration = virtualMacConfiguration + } catch { + return "Error: Failed to validate virtual machine configuration during install" + } + + return nil + } + + fileprivate func startInstall(ipswURL: URL, virtualMacConfiguration: VirtualMacConfiguration, delegate: VZVirtualMachineDelegate, progressHandler: @escaping ProgressHandler, completionHandler: @escaping InstallCompletionHander) { + self.virtualMachineConfiguration = virtualMacConfiguration + guard let virtualMachine = createVirtualMachine(delegate: delegate) else { + completionHandler("Error: Could not create virtual machine for install", nil) + return + } + + DispatchQueue.main.async { + let installer = VZMacOSInstaller(virtualMachine: virtualMachine, restoringFromImageAt: ipswURL) + + installer.install { result in + switch result { + case .success: + debugLog("Install finished") + self.stop(virtualMachine: virtualMachine, completionHandler: completionHandler) + case .failure(let error): + completionHandler("Error: Install failed: \(error)", nil) + } + } + + self.progressObserver = installer.progress.observe(\.fractionCompleted, options: [.initial, .new]) { (progress, change) in + progressHandler(installer.progress) + } + } + } + + fileprivate func createBundle() -> String? { + if FileManager.default.fileExists(atPath: URL.vmBundlePath) { + return nil // already exists + } + + let bundleFileDescriptor = mkdir(URL.vmBundlePath, S_IRWXU | S_IRWXG | S_IRWXO) + if bundleFileDescriptor == -1 { + if errno == EEXIST { + return "Error: Failed to create VM bundle: the base directory already exists" + } + return "Error: Failed to create VM bundle" + } + + let result = close(bundleFileDescriptor) + if result != 0 { + debugLog("Error: Failed to close VM bundle (\(result))") + } + + return nil // no error + } + + static func createDiskImage(sizeInGB: UInt64) -> String? { + let diskFd = open(URL.diskImageURL.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR) + if diskFd == -1 { + return "Error: Cannot create disk image" + } + + let diskSize = sizeInGB.gigabytesToBytes() + var result = ftruncate(diskFd, Int64(diskSize)) + if result != 0 { + return "Error: Expanding disk image failed" + } + + result = close(diskFd) + if result != 0 { + return "Error: Failed to close the disk image" + } + + return nil // no error + } +} + +#endif diff --git a/virtualOS/Model/VirtualMacConfiguration.swift b/virtualOS/Model/VirtualMacConfiguration.swift new file mode 100644 index 0000000..05c724f --- /dev/null +++ b/virtualOS/Model/VirtualMacConfiguration.swift @@ -0,0 +1,185 @@ +// +// VirtualMacConfiguration.swift +// virtualOS +// +// Created by Jahn Bertsch on 16.03.22. +// + +#if arch(arm64) + +import Virtualization +import AVFoundation + +final class VirtualMacConfiguration: VZVirtualMachineConfiguration { + fileprivate(set) var machineIdentifier = VZMacMachineIdentifier() + + func create(using parameters: inout VirtualMac.Parameters, macHardwareModel: VZMacHardwareModel) { + if !configurePlatform(parameters: parameters, macHardwareModel: macHardwareModel) { + return // error + } + configure(with: ¶meters) + } + + func readFromDisk(using parameters: inout VirtualMac.Parameters) { + let (errorString, platform) = readPlaformFromDisk() + if let errorString = errorString { + debugLog(errorString) + } else if let platform = platform { + self.platform = platform + configure(with: ¶meters) + } else { + debugLog("Error: Reading platform from disk failed") + } + } + + func getBestHardwareConfig(parameters: inout VirtualMac.Parameters) { + let cpuCountMax = computeCPUCount() + let bytesMax = VZVirtualMachineConfiguration.maximumAllowedMemorySize + let bytesMaxMinus2GB = bytesMax - UInt64(2).gigabytesToBytes() // substract 2 GB + + cpuCount = cpuCountMax - 1 // substract one core + memorySize = bytesMaxMinus2GB + + parameters.cpuCount = cpuCount + parameters.cpuCountMax = cpuCountMax + parameters.memorySizeInGB = memorySize.bytesToGigabytes() + parameters.memorySizeInGBMax = bytesMax.bytesToGigabytes() + } + + func configure(with parameters: inout VirtualMac.Parameters) { + cpuCount = parameters.cpuCount + memorySize = parameters.memorySizeInGB.gigabytesToBytes() + pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()] + entropyDevices = [VZVirtioEntropyDeviceConfiguration()] + bootLoader = VZMacOSBootLoader() + keyboards = [VZUSBKeyboardConfiguration()] + + configureAudioDevice(parameters: parameters) + configureGraphicsDevice(parameters: parameters) + configureStorageDevice(parameters: parameters) + configureNetworkDevices() + } + + // MARK: - Private + + fileprivate func configurePlatform(parameters: VirtualMac.Parameters, macHardwareModel: VZMacHardwareModel) -> Bool { + let platformConfiguration = VZMacPlatformConfiguration() + platformConfiguration.hardwareModel = macHardwareModel + + do { + platformConfiguration.auxiliaryStorage = try VZMacAuxiliaryStorage( + creatingStorageAt: URL.auxiliaryStorageURL, + hardwareModel: macHardwareModel, + options: [.allowOverwrite] + ) + } catch { + debugLog("Error: could not create auxiliary storage device") + return false + } + + do { + try platformConfiguration.hardwareModel.dataRepresentation.write(to: URL.hardwareModelURL) + try platformConfiguration.machineIdentifier.dataRepresentation.write(to: URL.machineIdentifierURL) + } catch { + debugLog("Error: could store platform information to disk") + return false + } + + platform = platformConfiguration + return true // success + } + + fileprivate func configureNetworkDevices() { + let networkDevice = VZVirtioNetworkDeviceConfiguration() + let networkAttachment = VZNATNetworkDeviceAttachment() + networkDevice.attachment = networkAttachment + networkDevices = [networkDevice] + } + + fileprivate func configureAudioDevice(parameters: VirtualMac.Parameters) { + let audioDevice = VZVirtioSoundDeviceConfiguration() + + if parameters.microphoneEnabled { + AVCaptureDevice.requestAccess(for: .audio) { (granted: Bool) in + debugLog("Microphone request granted: \(granted)") + } + + let inputStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration() + inputStreamConfiguration.source = VZHostAudioInputStreamSource() + audioDevice.streams.append(inputStreamConfiguration) + } + + let outputStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration() + outputStreamConfiguration.sink = VZHostAudioOutputStreamSink() + audioDevice.streams.append(outputStreamConfiguration) + + audioDevices = [audioDevice] + } + + fileprivate func configureGraphicsDevice(parameters: VirtualMac.Parameters) { + let graphicsDevice = VZMacGraphicsDeviceConfiguration() + graphicsDevice.displays = [VZMacGraphicsDisplayConfiguration( + widthInPixels: parameters.screenWidth, + heightInPixels: parameters.screenHeight, + pixelsPerInch: parameters.pixelsPerInch + )] + graphicsDevices = [graphicsDevice] + } + + fileprivate func configureStorageDevice(parameters: VirtualMac.Parameters) { + if let diskImageStorageDeviceAttachment = try? VZDiskImageStorageDeviceAttachment(url: URL.diskImageURL, readOnly: false) { + let blockDeviceConfiguration = VZVirtioBlockDeviceConfiguration(attachment: diskImageStorageDeviceAttachment) + storageDevices = [blockDeviceConfiguration] + } else { + debugLog("Error: could not create storage device") + } + } + + fileprivate func computeCPUCount() -> Int { + let totalAvailableCPUs = ProcessInfo.processInfo.processorCount + + var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs + virtualCPUCount = max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount) + virtualCPUCount = min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount) + + return virtualCPUCount + } + + fileprivate func readPlaformFromDisk() -> (String?, VZMacPlatformConfiguration?) { + let macPlatform = VZMacPlatformConfiguration() + + let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: URL.auxiliaryStorageURL) + macPlatform.auxiliaryStorage = auxiliaryStorage + + if !FileManager.default.fileExists(atPath: URL.vmBundlePath) { + return ("Error: Missing virtual machine bundle at \(URL.vmBundlePath).", nil) + } + + guard let hardwareModelData = try? Data(contentsOf: URL.hardwareModelURL) else { + return ("Error: Failed to retrieve hardware model data", nil) + } + + guard let hardwareModel = VZMacHardwareModel(dataRepresentation: hardwareModelData) else { + return ("Error: Failed to create hardware model", nil) + } + + if !hardwareModel.isSupported { + return ("Error: The hardware model is not supported on the current host", nil) + } + macPlatform.hardwareModel = hardwareModel + + // Retrieve the machine identifier; you should save this value to disk during installation. + guard let machineIdentifierData = try? Data(contentsOf: URL.machineIdentifierURL) else { + return ("Error: Failed to retrieve machine identifier data.", nil) + } + + guard let machineIdentifier = VZMacMachineIdentifier(dataRepresentation: machineIdentifierData) else { + return ("Error: Failed to create machine identifier.", nil) + } + macPlatform.machineIdentifier = machineIdentifier + + return (nil, macPlatform) + } +} + +#endif diff --git a/virtualOS/Model/VirtualMachineConfiguration.swift b/virtualOS/Model/VirtualMachineConfiguration.swift new file mode 100644 index 0000000..312841b --- /dev/null +++ b/virtualOS/Model/VirtualMachineConfiguration.swift @@ -0,0 +1,105 @@ +// +// VirtualMachineConfiguration.swift +// virtualOS +// +// Created by Jahn Bertsch on 16.03.22. +// + +import Foundation +import Virtualization +import AVFoundation + +final class VirtualMacConfiguration: VZVirtualMachineConfiguration { + struct VirtualMachineParameters { + var hardwareModel: VZMacHardwareModel + var machineIdentifier: VZMacMachineIdentifier + var screenWidth: Int + var screenHeight: Int + var pixelsPerInch: Int + var storageDeviceURL: URL + var memorySizeInGB: UInt64 + var microphoneEnabled: Bool + var auxiliaryStorageURL: URL + } + + fileprivate(set) var microphoneEnabled = false + var memorySizeInGB: UInt64 { + get { + return self.memorySize / 1024 + } + set { + self.memorySize = newValue * 1024 + } + } + + override init() { + super.init() + + pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()] + networkDevices = [VZVirtioNetworkDeviceConfiguration()] + entropyDevices = [VZVirtioEntropyDeviceConfiguration()] + bootLoader = VZMacOSBootLoader() + keyboards = [VZUSBKeyboardConfiguration()] + + networkDevices.first?.attachment = VZNATNetworkDeviceAttachment() + } + + func setup(parameters: VirtualMachineParameters) { + microphoneEnabled = parameters.microphoneEnabled + memorySizeInGB = parameters.memorySizeInGB + + setupAudioDevice(parameters: parameters) + setupGraphicsDevice(parameters: parameters) + setupStorageDevice(parameters: parameters) + setupAuxStorage(parameters: parameters) + } + + fileprivate func setupPlatform(parameters: VirtualMachineParameters) { + let platformConfiguration = VZMacPlatformConfiguration() + platformConfiguration.hardwareModel = parameters.hardwareModel + platformConfiguration.machineIdentifier = parameters.machineIdentifier + + platformConfiguration.auxiliaryStorage = try? VZMacAuxiliaryStorage( + creatingStorageAt: parameters.auxiliaryStorageURL, + hardwareModel: parameters.hardwareModel, + options: [.allowOverwrite] + ) + + platform = platformConfiguration + } + + fileprivate func setupAudioDevice(parameters: VirtualMachineParameters) { + let audioDevice = VZVirtioSoundDeviceConfiguration() + let soundDeviceOutputStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration() + soundDeviceOutputStreamConfiguration.sink = VZHostAudioOutputStreamSink() + audioDevice.streams.append(soundDeviceOutputStreamConfiguration) + + if microphoneEnabled { + AVCaptureDevice.requestAccess(for: .audio) { (granted: Bool) in + print("microphone request granted: \(granted)") + } + let soundDeviceInputStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration() + soundDeviceInputStreamConfiguration.source = VZHostAudioInputStreamSource() + audioDevice.streams.append(soundDeviceInputStreamConfiguration) + } + + audioDevices = [audioDevice] + } + + fileprivate func setupGraphicsDevice(parameters: VirtualMachineParameters) { + let graphicsDevice = VZMacGraphicsDeviceConfiguration() + graphicsDevice.displays = [VZMacGraphicsDisplayConfiguration( + widthInPixels: parameters.screenWidth, + heightInPixels: parameters.screenHeight, + pixelsPerInch: parameters.pixelsPerInch + )] + graphicsDevices = [graphicsDevice] + } + + fileprivate func setupStorageDevice(parameters: VirtualMachineParameters) { + if let diskImageStorageDeviceAttachment = try? VZDiskImageStorageDeviceAttachment(url: parameters.storageDeviceURL, readOnly: false) { + let blockDeviceConfiguration = VZVirtioBlockDeviceConfiguration(attachment: diskImageStorageDeviceAttachment) + storageDevices = [blockDeviceConfiguration] + } + } +} diff --git a/virtualOS/Preview Content/Preview Assets.xcassets/Contents.json b/virtualOS/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/virtualOS/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/virtualOS/View/MainView.swift b/virtualOS/View/MainView.swift new file mode 100644 index 0000000..23e2094 --- /dev/null +++ b/virtualOS/View/MainView.swift @@ -0,0 +1,44 @@ +// +// MainView.swift +// virtualOS +// +// Created by Jahn Bertsch on 16.03.22. +// + +#if arch(arm64) + +import SwiftUI + +struct MainView: View { + @ObservedObject var viewModel: MainViewModel + + var body: some View { + VStack { + Spacer() + HStack { + Text(viewModel.statusLabel) + Spacer() + Button { + viewModel.buttonPressed() + } label: { + Text(viewModel.buttonLabel) + }.disabled(viewModel.buttonDisabled) + }.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)) + + if viewModel.settingsShown { + SettingsView(viewModel: viewModel) + } else { + VirtualMachineView(virtualMachine: $viewModel.virtualMachine) + } + } + .frame(minWidth: 400, minHeight: 300) + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + MainView(viewModel: MainViewModel()) + } +} + +#endif diff --git a/virtualOS/View/SettingsView.swift b/virtualOS/View/SettingsView.swift new file mode 100644 index 0000000..4087ca1 --- /dev/null +++ b/virtualOS/View/SettingsView.swift @@ -0,0 +1,108 @@ +// +// SettingsView.swift +// virtualOS +// +// Created by Jahn Bertsch on 03.04.22. +// + +#if arch(arm64) + +import SwiftUI + +struct SettingsView: View { + @ObservedObject var viewModel: MainViewModel + fileprivate let sliderTextWidth = CGFloat(150) + @State fileprivate var cpuCountSliderValue: Float = 0 { + didSet { + viewModel.virtualMac.parameters.cpuCount = Int(cpuCountSliderValue) + } + } + @State fileprivate var memorySliderValue: Float = 0 { + didSet { + viewModel.virtualMac.parameters.memorySizeInGB = UInt64(memorySliderValue) + } + } + @State fileprivate var screenWidthValue: Float = 0 { + didSet { + viewModel.virtualMac.parameters.screenWidth = Int(screenWidthValue) + } + } + @State fileprivate var screenHeightValue: Float = 0 { + didSet { + viewModel.virtualMac.parameters.screenHeight = Int(screenHeightValue) + } + } + + var body: some View { + VStack { + Spacer() + VStack { + let parameters = viewModel.virtualMac.parameters + Text("Virtual Machine Configuration").font(.title) + + Slider(value: Binding(get: { + cpuCountSliderValue + }, set: { (newValue) in + cpuCountSliderValue = newValue + }), in: Float(parameters.cpuCountMin) ... Float(parameters.cpuCountMax), step: 1) { + Text("CPU Count: \(viewModel.virtualMac.parameters.cpuCount)") + .frame(minWidth: sliderTextWidth, alignment: .leading) + } + + Slider(value: Binding(get: { + memorySliderValue + }, set: { (newValue) in + memorySliderValue = newValue + }), in: Float(parameters.memorySizeInGBMin) ... Float(parameters.memorySizeInGBMax), step: 1) { + Text("RAM: \(viewModel.virtualMac.parameters.memorySizeInGB) GB") + .frame(minWidth: sliderTextWidth, alignment: .leading) + } + + Slider(value: Binding(get: { + screenWidthValue + }, set: { (newValue) in + screenWidthValue = newValue + }), in: 800 ... Float(NSScreen.main?.frame.width ?? CGFloat(parameters.screenWidth)), step: 100) { + Text("Screen Width: \(viewModel.virtualMac.parameters.screenWidth) px") + .frame(minWidth: sliderTextWidth, alignment: .leading) + } + + Slider(value: Binding(get: { + screenHeightValue + }, set: { (newValue) in + screenHeightValue = newValue + }), in: 600 ... Float(NSScreen.main?.frame.height ?? CGFloat(parameters.screenHeight)), step: 50) { + Text("Screen Height: \(viewModel.virtualMac.parameters.screenHeight) px") + .frame(minWidth: sliderTextWidth, alignment: .leading) + } + } + .padding() + .overlay { + RoundedRectangle(cornerRadius: 10) + .stroke(.tertiary, lineWidth: 1) + } + + Spacer() + } + .padding() + .frame(maxWidth: 400) + .onAppear { + let parameters = viewModel.virtualMac.parameters + cpuCountSliderValue = Float(parameters.cpuCount) + memorySliderValue = Float(parameters.memorySizeInGB) + screenWidthValue = Float(parameters.screenWidth) + screenHeightValue = Float(parameters.screenHeight) + } + } +} + +struct SettingsViewProvider_Previews: PreviewProvider { + static var previews: some View { + VStack { + SettingsView(viewModel: MainViewModel()) + .colorScheme(.light) + } + } +} + +#endif diff --git a/virtualOS/View/VirtualMachineView.swift b/virtualOS/View/VirtualMachineView.swift new file mode 100644 index 0000000..f706fe5 --- /dev/null +++ b/virtualOS/View/VirtualMachineView.swift @@ -0,0 +1,24 @@ +// +// VirtualMachineView.swift +// virtualOS +// +// Created by Jahn Bertsch on 16.03.22. +// + +import SwiftUI +import Virtualization + +struct VirtualMachineView: NSViewRepresentable { + @Binding var virtualMachine: VZVirtualMachine? + + func makeNSView(context: Context) -> VZVirtualMachineView { + let view = VZVirtualMachineView() + view.capturesSystemKeys = true + return view + } + + func updateNSView(_ nsView: VZVirtualMachineView, context: Context) { + nsView.virtualMachine = virtualMachine + nsView.window?.makeFirstResponder(nsView) + } +} diff --git a/virtualOS/virtualOS.entitlements b/virtualOS/virtualOS.entitlements new file mode 100644 index 0000000..843b6ed --- /dev/null +++ b/virtualOS/virtualOS.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.virtualization + + + diff --git a/virtualOS/virtualOSApp.swift b/virtualOS/virtualOSApp.swift new file mode 100644 index 0000000..fe238dd --- /dev/null +++ b/virtualOS/virtualOSApp.swift @@ -0,0 +1,75 @@ +// +// virtualOSApp.swift +// virtualOS +// +// Created by Jahn Bertsch on 16.03.22. +// + +import Foundation +import SwiftUI + +func debugLog(_ message: String) { +#if DEBUG + print(message) +#endif +} + +typealias CompletionHander = (String?) -> Void +typealias ProgressHandler = (Progress) -> Void + +@main +struct virtualOSApp: App { + #if arch(arm64) + @ObservedObject var viewModel = MainViewModel() + #endif + + var body: some Scene { + WindowGroup { + #if arch(arm64) + + MainView(viewModel: viewModel) + .alert("Delete \(viewModel.confirmationText)", isPresented: $viewModel.showConfirmationAlert) { + Button("OK") { + viewModel.showConfirmationAlert = !viewModel.showConfirmationAlert + viewModel.confirmationHandler("") + } + Button("Cancel") { + viewModel.showConfirmationAlert = !viewModel.showConfirmationAlert + } + } message: { + Text("Are you sure you want to delete the \(viewModel.confirmationText.lowercased())?") + } + .alert(viewModel.licenseInformationTitleString, isPresented: $viewModel.showLicenseInformationModal, actions: {}, message: { + Text(viewModel.licenseInformationString) + }) + + #else + Text("Sorry, virtualization requires an Apple Silicon computer.") + .frame(minWidth: 400, minHeight: 300) + #endif + } + #if arch(arm64) + .commands { + CommandGroup(replacing: .appInfo) { + Button("About virtualOS") { + viewModel.showLicenseInformationModal = !viewModel.showLicenseInformationModal + } + } + CommandGroup(replacing: .newItem) {} + CommandGroup(after: .newItem) { + Button("Delete Restore Image", action: { + viewModel.deleteRestoreImage() + }).disabled(!MainViewModel.restoreImageExists) + Button("Delete Virtual Machine", action: { + viewModel.deleteVirtualMachine() + }) + } + CommandGroup(replacing: .appTermination) { + Button("Quit", action: { + exit(1) + }).keyboardShortcut("Q") + } + } + #endif + } +} diff --git a/virtualOSTests/virtualOSTests.swift b/virtualOSTests/virtualOSTests.swift new file mode 100644 index 0000000..70e671c --- /dev/null +++ b/virtualOSTests/virtualOSTests.swift @@ -0,0 +1,36 @@ +// +// virtualOSTests.swift +// virtualOSTests +// +// Created by Jahn Bertsch on 16.03.22. +// + +import XCTest +@testable import virtualOS + +class virtualOSTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/virtualOSUITests/virtualOSUITests.swift b/virtualOSUITests/virtualOSUITests.swift new file mode 100644 index 0000000..556d641 --- /dev/null +++ b/virtualOSUITests/virtualOSUITests.swift @@ -0,0 +1,41 @@ +// +// virtualOSUITests.swift +// virtualOSUITests +// +// Created by Jahn Bertsch on 16.03.22. +// + +import XCTest + +class virtualOSUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/virtualOSUITests/virtualOSUITestsLaunchTests.swift b/virtualOSUITests/virtualOSUITestsLaunchTests.swift new file mode 100644 index 0000000..cd594bb --- /dev/null +++ b/virtualOSUITests/virtualOSUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// virtualOSUITestsLaunchTests.swift +// virtualOSUITests +// +// Created by Jahn Bertsch on 16.03.22. +// + +import XCTest + +class virtualOSUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +}