diff --git a/Configuration/UTMQemuConfiguration+Arguments.swift b/Configuration/UTMQemuConfiguration+Arguments.swift index 0857e3dff..b88110d0f 100644 --- a/Configuration/UTMQemuConfiguration+Arguments.swift +++ b/Configuration/UTMQemuConfiguration+Arguments.swift @@ -472,8 +472,9 @@ import Virtualization // for getting network interfaces "if=pflash" "format=raw" "unit=0" - "file=" + "file.filename=" bios + "file.locking=off" "readonly=on" f() f("-drive") @@ -733,13 +734,16 @@ import Virtualization // for getting network interfaces } "id=drive\(drive.id)" if let imageURL = drive.imageURL { - "file=" + "file.filename=" imageURL } else if !isCd { - "file=" + "file.filename=" placeholderUrl } if drive.isReadOnly || isCd { + if drive.imageURL != nil { + "file.locking=off" + } "readonly=on" } else { "discard=unmap" diff --git a/Documentation/MacDevelopment.md b/Documentation/MacDevelopment.md index 9d923163d..801ad1b3e 100644 --- a/Documentation/MacDevelopment.md +++ b/Documentation/MacDevelopment.md @@ -16,7 +16,9 @@ git submodule update --init --recursive ## Dependencies -The easy way is to get the prebuilt dependences from [GitHub Actions][1]. Pick the latest release and download all of the `Sysroot-macos-*` artifacts. You need to be logged in to GitHub to download artifacts. If you only intend to run locally, it is alright to just download the sysroot for your architecture. +The easy way is to get the prebuilt dependences from [GitHub Actions][1]. Pick the latest release and download all of the `Sysroot-macos-*` artifacts. You need to be logged in to GitHub to download artifacts. If you only intend to run locally, it is alright to just download the sysroot for your architecture. After downloading the prebuilt artifacts of your choice, extract them to the root directory where you cloned the repository. + +To build UTM, make sure you have the latest version of Xcode installed. ### Building Dependencies (Advanced) @@ -58,7 +60,7 @@ If you are developing QEMU and wish to pass in a custom path to QEMU, you can us You can build UTM with the script: ```sh -./scripts/build_utm.sh -t TEAMID -p macos -a ARCH -o /path/to/output/directory +./scripts/build_utm.sh -t TEAMID -k macosx -s macos -a ARCH -o /path/to/output/directory ``` `ARCH` can be `x86_64` or `arm64` or `"arm64 x86_64"` (quotes are required) for a universal binary. The built artifact is an unsigned `.xcarchive` which you can use with the package tool (see below). diff --git a/Documentation/iOSDevelopment.md b/Documentation/iOSDevelopment.md index 8a22b9912..5bde558ef 100644 --- a/Documentation/iOSDevelopment.md +++ b/Documentation/iOSDevelopment.md @@ -15,12 +15,20 @@ Alternatively, run `git submodule update --init --recursive` after cloning if yo The easy way is to get the prebuilt dependences from [GitHub Actions][1]. Pick the latest release and download the `Sysroot-*` artifact for the targets you wish to develop on. You need to be logged in to GitHub to download artifacts. -| | Intel | Apple Silicon | -|--------------|----------------------------|---------------------------| -| iOS | N/A | `ios-arm64` | -| iOS SE | N/A | `ios-tci-arm64` | -| Simulator | `ios_simulator-x86_64` | `ios_simulator-arm64` | -| Simulator SE | `ios_simulator-tci-x86_64` | `ios_simulator-tci-arm64` | +| | Intel | Apple Silicon | +|-----------------------|----------------------------|--------------------------------| +| iOS | N/A | `ios-arm64` | +| iOS SE | N/A | `ios-tci-arm64` | +| iOS Simulator | `ios_simulator-x86_64` | `ios_simulator-arm64` | +| iOS Simulator SE | `ios_simulator-tci-x86_64` | `ios_simulator-tci-arm64` | +| visionOS | N/A | `visionos-arm64` | +| visionOS SE | N/A | `visionos-tci-arm64` | +| visionOS Simulator | N/A | `visionos_simulator-arm64` | +| visionOS Simulator SE | N/A | `visionos_simulator-tci-arm64` | + +After downloading the prebuilt artifacts of your choice, extract them to the root directory where you cloned the repository. + +To build UTM, make sure you have the latest version of Xcode installed. ### Building Dependencies (Advanced) @@ -39,13 +47,13 @@ If you want to build the dependencies yourself, it is highly recommended that yo ### Command Line -You can build UTM with the script: +You can build UTM for iOS with the script (run `./scripts/build_utm.sh` for all options): ``` -./scripts/build_utm.sh -p ios -a arm64 -o /path/to/output/directory +./scripts/build_utm.sh -k iphoneos -s iOS -a arm64 -o /path/to/output/directory ``` -The built artifact is an unsigned `.xcarchive` which you can use with the package tool (see below). Replace `ios` with `ios-tci` to build UTM SE. +The built artifact is an unsigned `.xcarchive` which you can use with the package tool (see below). Replace `iOS` with `iOS-SE` to build UTM SE. Replace `iphoneos` with `xros` to build for visionOS. ### Packaging diff --git a/Platform/Main.swift b/Platform/Main.swift index 23c448a4c..09dd6bfdb 100644 --- a/Platform/Main.swift +++ b/Platform/Main.swift @@ -15,6 +15,7 @@ // import Logging +import TipKit let logger = Logger(label: "com.utmapp.UTM") { label in var utmLogger = UTMLoggingSwift(label: label) @@ -60,6 +61,10 @@ class Main { #if os(iOS) || os(visionOS) // register defaults registerDefaultsFromSettingsBundle() + // register tips + if #available(iOS 17, macOS 14, *) { + try? Tips.configure() + } #endif UTMApp.main() } diff --git a/Platform/Shared/ContentView.swift b/Platform/Shared/ContentView.swift index eb19f3362..d0cca7edc 100644 --- a/Platform/Shared/ContentView.swift +++ b/Platform/Shared/ContentView.swift @@ -19,6 +19,7 @@ import UniformTypeIdentifiers #if os(iOS) import IQKeyboardManagerSwift #endif +import TipKit // on visionOS, there is no text to show more than UTM #if WITH_QEMU_TCI && !os(visionOS) @@ -48,6 +49,9 @@ struct ContentView: View { .disabled(data.busy && !data.showNewVMSheet && !data.showSettingsModal) .sheet(isPresented: $releaseHelper.isReleaseNotesShown, onDismiss: { releaseHelper.closeReleaseNotes() + if #available(iOS 17, macOS 14, *) { + UTMTipCreateVM.isVMListEmpty = data.virtualMachines.count == 0 + } }, content: { VMReleaseNotesView(helper: releaseHelper).padding() }) @@ -80,15 +84,19 @@ struct ContentView: View { .onAppear { Task { await data.listRefresh() + await releaseHelper.fetchReleaseNotes() + if #available(iOS 17, macOS 14, *) { + if !releaseHelper.isReleaseNotesShown { + UTMTipCreateVM.isVMListEmpty = data.virtualMachines.count == 0 + UTMTipDonate.timesLaunched += 1 + } + } #if os(macOS) if isServerAutostart { await data.remoteServer.start() } #endif } - Task { - await releaseHelper.fetchReleaseNotes() - } #if os(macOS) NSWindow.allowsAutomaticWindowTabbing = false #else @@ -121,6 +129,17 @@ struct ContentView: View { #endif #endif } + #if WITH_SERVER + .onChange(of: isServerAutostart) { newValue in + if newValue { + Task { + if isServerAutostart && !data.remoteServer.state.isServerActive { + await data.remoteServer.start() + } + } + } + } + #endif } private func handleURL(url: URL) { diff --git a/Platform/Shared/RAMSlider.swift b/Platform/Shared/RAMSlider.swift index 66e0b5458..2b4051355 100644 --- a/Platform/Shared/RAMSlider.swift +++ b/Platform/Shared/RAMSlider.swift @@ -60,7 +60,7 @@ struct RAMSlider: View { } NumberTextField("", number: $systemMemory, prompt: "Size", onEditingChanged: validateMemorySize) .frame(width: 80) - Text("MB") + Text("MiB") } }.frame(height: 30) } diff --git a/Platform/Shared/SizeTextField.swift b/Platform/Shared/SizeTextField.swift index 3997dc6de..1c51c99a1 100644 --- a/Platform/Shared/SizeTextField.swift +++ b/Platform/Shared/SizeTextField.swift @@ -40,9 +40,9 @@ struct SizeTextField: View { Button(action: { isGiB.toggle() }, label: { Group { if isGiB { - Text("GB") + Text("GiB") } else { - Text("MB") + Text("MiB") } }.foregroundColor(.blue) }).buttonStyle(.plain) diff --git a/Platform/Shared/UTMTips.swift b/Platform/Shared/UTMTips.swift new file mode 100644 index 000000000..c125a806c --- /dev/null +++ b/Platform/Shared/UTMTips.swift @@ -0,0 +1,82 @@ +// +// Copyright © 2024 osy. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import TipKit + +@available(iOS 17, macOS 14, *) +struct UTMTipDonate: Tip { + @Parameter + static var timesLaunched: Int = 0 + + var title: Text { + Text("Support UTM") + } + + var message: Text? { + Text("Enjoying the app? Consider making a donation to support development.") + } + + var actions: [Action] { + Action(id: "donate", title: "Donate") + Action(id: "no-thanks", title: "No Thanks") + } + + var rules: [Rule] { + #Rule(Self.$timesLaunched) { + $0 > 3 + } + } +} + +@available(iOS 17, macOS 14, *) +struct UTMTipHideToolbar: Tip { + @Parameter + static var didHideToolbar: Bool = true + + var title: Text { + Text("Tap to hide/show toolbar") + } + + var message: Text? { + Text("When the toolbar is hidden, the icon will disappear after a few seconds. To show the icon again, tap anywhere on the screen.") + } + + var rules: [Rule] { + #Rule(Self.$didHideToolbar) { + !$0 + } + } +} + +@available(iOS 17, macOS 14, *) +struct UTMTipCreateVM: Tip { + @Parameter(.transient) + static var isVMListEmpty: Bool = false + + var title: Text { + Text("Start Here") + } + + var message: Text? { + Text("Create a new virtual machine or import an existing one.") + } + + var rules: [Rule] { + #Rule(Self.$isVMListEmpty) { + $0 + } + } +} diff --git a/Platform/Shared/UTMUnavailableVMView.swift b/Platform/Shared/UTMUnavailableVMView.swift index 8e687345d..6252417e2 100644 --- a/Platform/Shared/UTMUnavailableVMView.swift +++ b/Platform/Shared/UTMUnavailableVMView.swift @@ -73,6 +73,8 @@ fileprivate struct WrappedVMDetailsView: View { } #if os(macOS) .frame(width: 230) + #else + .padding() #endif } } @@ -92,6 +94,8 @@ fileprivate struct UnsupportedVMDetailsView: View { } #if os(macOS) .frame(width: 230) + #else + .padding() #endif } } diff --git a/Platform/Shared/VMConfigSystemView.swift b/Platform/Shared/VMConfigSystemView.swift index a2c1ce884..606f97b76 100644 --- a/Platform/Shared/VMConfigSystemView.swift +++ b/Platform/Shared/VMConfigSystemView.swift @@ -55,7 +55,7 @@ struct VMConfigSystemView: View { HStack { NumberTextField("", number: $config.jitCacheSize, prompt: "Default", onEditingChanged: validateMemorySize) .multilineTextAlignment(.trailing) - Text("MB") + Text("MiB") } } } diff --git a/Platform/Shared/VMNavigationListView.swift b/Platform/Shared/VMNavigationListView.swift index 2b3c2ea31..acb50c7a2 100644 --- a/Platform/Shared/VMNavigationListView.swift +++ b/Platform/Shared/VMNavigationListView.swift @@ -15,6 +15,7 @@ // import SwiftUI +import TipKit struct VMNavigationListView: View { @EnvironmentObject private var data: UTMData @@ -106,6 +107,29 @@ private struct VMListModifier: ViewModifier { @State private var sheetPresented = false @State private var donatePresented = false + private let _donateTip: Any? + private let _createTip: Any? + + @available(iOS 17, macOS 14, *) + private var donateTip: UTMTipDonate { + _donateTip as! UTMTipDonate + } + + @available(iOS 17, macOS 14, *) + private var createTip: UTMTipCreateVM { + _createTip as! UTMTipCreateVM + } + + init() { + if #available(iOS 17, macOS 14, *) { + _donateTip = UTMTipDonate() + _createTip = UTMTipCreateVM() + } else { + _donateTip = nil + _createTip = nil + } + } + func body(content: Content) -> some View { content #if os(macOS) @@ -124,15 +148,39 @@ private struct VMListModifier: ViewModifier { #else #if !WITH_REMOTE // FIXME: implement remote feature ToolbarItem(placement: .navigationBarLeading) { - newButton + if #available(iOS 17, visionOS 99, *) { + Button { + createTip.invalidate(reason: .actionPerformed) + data.newVM() + } label: { + Image(systemName: "plus") // SwiftUI bug: tip won't show up if this is a label + }.help("Create a new VM") + .popoverTip(createTip, arrowEdge: .top) + } else { + newButton + } } #endif #if !WITH_REMOTE ToolbarItem(placement: .navigationBarLeading) { - Button { - donatePresented.toggle() - } label: { - Label("Donate", systemImage: "heart.fill") + if #available(iOS 17, visionOS 99, *) { + Button { + donateTip.invalidate(reason: .actionPerformed) + donatePresented.toggle() + } label: { + Image(systemName: "heart.fill") // SwiftUI bug: tip won't show up if this is a label + }.popoverTip(donateTip, arrowEdge: .top) { action in + donateTip.invalidate(reason: .actionPerformed) + if action.id == "donate" { + donatePresented.toggle() + } + } + } else { + Button { + donatePresented.toggle() + } label: { + Label("Donate", systemImage: "heart.fill") + } } } #endif diff --git a/Platform/Shared/VMSettingsAddDeviceMenuView.swift b/Platform/Shared/VMSettingsAddDeviceMenuView.swift index e3d8e0126..636266fa0 100644 --- a/Platform/Shared/VMSettingsAddDeviceMenuView.swift +++ b/Platform/Shared/VMSettingsAddDeviceMenuView.swift @@ -74,12 +74,12 @@ struct VMSettingsAddDeviceMenuView: View { Button { isImportDriveShown.toggle() } label: { - Label("Import Drive", systemImage: "externaldrive") + Label("Import Drive…", systemImage: "externaldrive") } Button { isCreateDriveShown.toggle() } label: { - Label("New Drive", systemImage: "externaldrive.badge.plus") + Label("New Drive…", systemImage: "externaldrive.badge.plus") } #endif } label: { diff --git a/Platform/Shared/VMWizardDrivesView.swift b/Platform/Shared/VMWizardDrivesView.swift index 3daa362c4..4da457c2d 100644 --- a/Platform/Shared/VMWizardDrivesView.swift +++ b/Platform/Shared/VMWizardDrivesView.swift @@ -28,7 +28,7 @@ struct VMWizardDrivesView: View { NumberTextField("", number: $wizardState.storageSizeGib) .textFieldStyle(.roundedBorder) .frame(maxWidth: 50) - Text("GB") + Text("GiB") } } header: { Text("Size") diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index 5af4ccca2..b1364bcb8 100644 --- a/Platform/UTMData.swift +++ b/Platform/UTMData.swift @@ -619,9 +619,13 @@ struct AlertMessage: Identifiable { /// - Parameter asShortcut: Create a shortcut rather than a copy func importUTM(from url: URL, asShortcut: Bool = true) async throws { guard url.isFileURL else { return } - _ = url.startAccessingSecurityScopedResource() - defer { url.stopAccessingSecurityScopedResource() } - + let isScopedAccess = url.startAccessingSecurityScopedResource() + defer { + if isScopedAccess { + url.stopAccessingSecurityScopedResource() + } + } + logger.info("importing: \(url)") // attempt to turn temp URL to presistent bookmark early otherwise, // when stopAccessingSecurityScopedResource() is called, we lose access diff --git a/Platform/UTMReleaseHelper.swift b/Platform/UTMReleaseHelper.swift index 4848aa648..684d529c4 100644 --- a/Platform/UTMReleaseHelper.swift +++ b/Platform/UTMReleaseHelper.swift @@ -47,10 +47,10 @@ class UTMReleaseHelper: ObservableObject { return } let configuration = URLSessionConfiguration.ephemeral - configuration.allowsCellularAccess = false + configuration.allowsCellularAccess = true configuration.allowsExpensiveNetworkAccess = false configuration.allowsConstrainedNetworkAccess = false - configuration.waitsForConnectivity = true + configuration.waitsForConnectivity = false configuration.httpAdditionalHeaders = ["Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"] let session = URLSession(configuration: configuration) diff --git a/Platform/VMData.swift b/Platform/VMData.swift index f96826e22..fb70720b5 100644 --- a/Platform/VMData.swift +++ b/Platform/VMData.swift @@ -521,7 +521,7 @@ extension VMRemoteDataError: LocalizedError { case .notImplemented: return NSLocalizedString("This function is not implemented.", comment: "VMData") case .backendNotSupported: - return NSLocalizedString("This VM is configured for a backend that does not support remote clients.", comment: "VMData") + return NSLocalizedString("This VM is not available or is configured for a backend that does not support remote clients.", comment: "VMData") } } } diff --git a/Platform/de.lproj/Localizable.strings b/Platform/de.lproj/Localizable.strings index 9b0a636de..ad4f2ecd1 100644 --- a/Platform/de.lproj/Localizable.strings +++ b/Platform/de.lproj/Localizable.strings @@ -364,7 +364,7 @@ "Force Multicore" = "Multicore erzwingen"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB Debug Stub"; @@ -520,7 +520,7 @@ "Maximum Shared USB Devices" = "Anzahl weitergeleiteter USB-Geräte (max.)"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "Speicher"; diff --git a/Platform/es-419.lproj/Localizable.strings b/Platform/es-419.lproj/Localizable.strings index 05f1b542b..da3fb3e79 100644 --- a/Platform/es-419.lproj/Localizable.strings +++ b/Platform/es-419.lproj/Localizable.strings @@ -675,7 +675,7 @@ "Full Graphics" = "Gráficos completos"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB Debug Stub"; @@ -797,7 +797,7 @@ "Import…" = "Importar..."; /* No comment provided by engineer. */ -"Import Drive" = "Importar unidad"; +"Import Drive…" = "Importar unidad..."; /* No comment provided by engineer. */ "Import VHDX Image" = "Importar una imagen VHDX"; @@ -989,7 +989,7 @@ "Maximum Shared USB Devices" = "Número máximo de dispositivos USB compartidos"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "Memoria"; @@ -1055,7 +1055,7 @@ "New…" = "Nuevo..."; /* No comment provided by engineer. */ -"New Drive" = "Nueva unidad"; +"New Drive…" = "Nueva unidad..."; /* No comment provided by engineer. */ "New from template…" = "Nuevo desde plantilla..."; diff --git a/Platform/fi.lproj/Localizable.strings b/Platform/fi.lproj/Localizable.strings index 58d87eaef..6b01b6fcf 100644 --- a/Platform/fi.lproj/Localizable.strings +++ b/Platform/fi.lproj/Localizable.strings @@ -440,7 +440,7 @@ "Full Graphics" = "Täysi grafiikka"; /* No comment provided by engineer. */ -"GB" = "Gt"; +"GiB" = "GiB"; /* No comment provided by engineer. */ "Generate Windows Installer ISO" = "Luo Windows Installer ISO"; @@ -521,7 +521,7 @@ "Import…" = "Tuo..."; /* No comment provided by engineer. */ -"Import Drive" = "Tuo asema"; +"Import Drive…" = "Tuo asema..."; /* No comment provided by engineer. */ "Import VHDX Image" = "Importer une image VHDX"; @@ -650,7 +650,7 @@ "Maximum Shared USB Devices" = "Jaettujen USB-laitteiden enimmäismäärä"; /* No comment provided by engineer. */ -"MB" = "Mt"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "Muisti"; @@ -680,7 +680,7 @@ "New" = "Uusi"; /* No comment provided by engineer. */ -"New Drive" = "Uusi asema"; +"New Drive…" = "Uusi asema…"; /* VMConfigPortForwardingViewController */ "New port forward" = "Uusi portitus"; diff --git a/Platform/fr.lproj/Localizable.strings b/Platform/fr.lproj/Localizable.strings index d10130b03..806bb2839 100644 --- a/Platform/fr.lproj/Localizable.strings +++ b/Platform/fr.lproj/Localizable.strings @@ -463,11 +463,11 @@ // RAMSlider.swift "Size" = "Taille"; -"MB" = "MB"; +"MiB" = "MiB"; // SizeTextField.swift "The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "La quantité de stockage à allouer pour cette image. Ignoré si importation d’image. Si c'est une image brute, un fichier vide de la même taille sera enregistré avec la VM. Sinon, l’image disque sera dynamiquement étendue jusqu’à cette taille."; -"GB" = "GB"; +"GiB" = "GiB"; // VMCardView.swift "Run" = "Démarrer"; @@ -679,8 +679,8 @@ "Removable" = "Amovible"; // VMSettingsAddDeviceMenuView.swift -"Import Drive" = "Importer un lecteur"; -"New Drive" = "Nouveau lecteur"; +"Import Drive…" = "Importer un lecteur…"; +"New Drive…" = "Nouveau lecteur…";= // VMToolbarModifier.swift "Remove selected shortcut" = "Supprimer le raccourci sélectionné"; diff --git a/Platform/iOS/Display/VMDisplayTerminalViewController.swift b/Platform/iOS/Display/VMDisplayTerminalViewController.swift index b2195884b..f65a52c4e 100644 --- a/Platform/iOS/Display/VMDisplayTerminalViewController.swift +++ b/Platform/iOS/Display/VMDisplayTerminalViewController.swift @@ -85,14 +85,26 @@ extension VMDisplayTerminalViewController { var useAutoLayout: Bool { get { true } } - + + // This prevents curved edge from cutting off the content + var additionalTopPadding: CGFloat { + if UIDevice.current.userInterfaceIdiom == .pad { + let scenes = UIApplication.shared.connectedScenes + let windowScene = scenes.first as? UIWindowScene + guard let window = windowScene?.windows.first else { return 0 } + return window.safeAreaInsets.bottom + } else { + return 0 + } + } + func makeFrame (keyboardDelta: CGFloat, _ fn: String = #function, _ ln: Int = #line) -> CGRect { if useAutoLayout { return CGRect.zero } else { return CGRect (x: view.safeAreaInsets.left, - y: view.safeAreaInsets.top, + y: view.safeAreaInsets.top + additionalTopPadding, width: view.frame.width - view.safeAreaInsets.left - view.safeAreaInsets.right, height: view.frame.height - view.safeAreaInsets.top - keyboardDelta) } @@ -101,13 +113,16 @@ extension VMDisplayTerminalViewController { func setupKeyboardMonitor () { if #available(iOS 15.0, *), useAutoLayout { + #if os(visionOS) + let inputAccessoryHeight: CGFloat = 0 + #else + let inputAccessoryHeight = terminalView.inputAccessoryView?.frame.height ?? 0 + #endif terminalView.translatesAutoresizingMaskIntoConstraints = false - terminalView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true + terminalView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: additionalTopPadding).isActive = true terminalView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true terminalView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true - terminalView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true - terminalView.keyboardLayoutGuide.topAnchor.constraint(equalTo: terminalView.bottomAnchor).isActive = true - terminalView.keyboardLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true + terminalView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -inputAccessoryHeight).isActive = true } else { NotificationCenter.default.addObserver( self, diff --git a/Platform/iOS/VMDrivesSettingsView.swift b/Platform/iOS/VMDrivesSettingsView.swift index c8cab8012..3e703a89f 100644 --- a/Platform/iOS/VMDrivesSettingsView.swift +++ b/Platform/iOS/VMDrivesSettingsView.swift @@ -35,6 +35,16 @@ struct VMDrivesSettingsView: View { attemptDelete = offsets } .onMove(perform: moveDrives) + Button { + isImportDriveShown.toggle() + } label: { + Text("Import Drive…") + } + Button { + isCreateDriveShown.toggle() + } label: { + Text("New Drive…") + } .nonbrokenSheet(isPresented: $isCreateDriveShown) { CreateDrive(newDrive: UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target), onDismiss: newDrive) } @@ -71,7 +81,7 @@ struct VMDrivesSettingsView: View { switch result { case .success(let url): await MainActor.run { - var drive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: true) + var drive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: false) drive.imageURL = url config.drives.append(drive) } diff --git a/Platform/iOS/VMToolbarDisplayMenuView.swift b/Platform/iOS/VMToolbarDisplayMenuView.swift index f0d1e4f46..574f8c3ff 100644 --- a/Platform/iOS/VMToolbarDisplayMenuView.swift +++ b/Platform/iOS/VMToolbarDisplayMenuView.swift @@ -55,7 +55,10 @@ struct VMToolbarDisplayMenuView: View { Picker("", selection: externalWindowBinding.device) { MenuLabel("None", systemImage: "rectangle.dashed").tag(nil as VMWindowState.Device?) ForEach(session.devices) { device in - if case .display(_, let index) = device { + switch device { + case .serial(_, let index): + MenuLabel("Serial \(index): \(session.qemuConfig.serials[index].target.prettyValue)", systemImage: "rectangle.connected.to.line.below").tag(device as VMWindowState.Device?) + case .display(_, let index): MenuLabel("Display \(index): \(session.qemuConfig.displays[index].hardware.prettyValue)", systemImage: "display").tag(device as VMWindowState.Device?) } } diff --git a/Platform/iOS/VMToolbarView.swift b/Platform/iOS/VMToolbarView.swift index 5a9d2bfc0..2d7141be5 100644 --- a/Platform/iOS/VMToolbarView.swift +++ b/Platform/iOS/VMToolbarView.swift @@ -15,9 +15,10 @@ // import SwiftUI +import TipKit struct VMToolbarView: View { - @AppStorage("ToolbarIsCollapsed") private var isCollapsed: Bool = true + @AppStorage("ToolbarIsCollapsed") private var isCollapsed: Bool = false @AppStorage("ToolbarLocation") private var location: ToolbarLocation = .topRight @State private var shake: Bool = true @State private var isMoving: Bool = false @@ -145,6 +146,7 @@ struct VMToolbarView: View { } label: { Label("Hide", systemImage: isCollapsed ? nameOfHideIcon : nameOfShowIcon) }.buttonStyle(.toolbar(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass)) + .modifier(HideToolbarTipModifier(isCollapsed: $isCollapsed)) .opacity(toolbarToggleOpacity) .modifier(Shake(shake: shake)) .position(position(for: geometry)) @@ -387,3 +389,35 @@ extension MenuStyle where Self == ToolbarMenuStyle { DispatchQueue.main.asyncAfter(deadline: .now() + 15, execute: longIdleTask!) } } + +private struct HideToolbarTipModifier: ViewModifier { + @Binding var isCollapsed: Bool + private let _hideToolbarTip: Any? + + @available(iOS 17, *) + private var hideToolbarTip: UTMTipHideToolbar { + _hideToolbarTip as! UTMTipHideToolbar + } + + init(isCollapsed: Binding) { + _isCollapsed = isCollapsed + if #available(iOS 17, *) { + _hideToolbarTip = UTMTipHideToolbar() + } else { + _hideToolbarTip = nil + } + } + + @ViewBuilder + func body(content: Content) -> some View { + if #available(iOS 17, *) { + content + .popoverTip(hideToolbarTip, arrowEdge: .top) + .onAppear { + UTMTipHideToolbar.didHideToolbar = isCollapsed + } + } else { + content + } + } +} diff --git a/Platform/it.lproj/Localizable.strings b/Platform/it.lproj/Localizable.strings index 3dff1a3f8..da616dfe0 100644 --- a/Platform/it.lproj/Localizable.strings +++ b/Platform/it.lproj/Localizable.strings @@ -703,7 +703,7 @@ "Full Graphics" = "Grafica Completa"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB Debug Stub"; @@ -825,7 +825,7 @@ "Import…" = "Importa..."; /* No comment provided by engineer. */ -"Import Drive" = "Importa Disco"; +"Import Drive…" = "Importa Disco..."; /* No comment provided by engineer. */ "Import VHDX Image" = "Importa un'Immagine VHDX"; @@ -1100,7 +1100,7 @@ "Maximum Shared USB Devices" = "Dispositivi USB Condivisi Massimi"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "Memoria"; @@ -1166,7 +1166,7 @@ "New…" = "Crea..."; /* No comment provided by engineer. */ -"New Drive" = "Nuovo Disco"; +"New Drive…" = "Nuovo Disco..."; /* No comment provided by engineer. */ "New from template…" = "Nuovo da template..."; diff --git a/Platform/ja.lproj/Localizable.strings b/Platform/ja.lproj/Localizable.strings index f9e02579b..2a264df13 100644 --- a/Platform/ja.lproj/Localizable.strings +++ b/Platform/ja.lproj/Localizable.strings @@ -566,11 +566,11 @@ // RAMSlider.swift "Size" = "サイズ"; -"MB" = "MB"; +"MiB" = "MiB"; // SizeTextField.swift "The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "このイメージに割り当てるストレージ領域です。イメージを読み込む場合は無視されます。生イメージの場合、このサイズの空のファイルが仮想マシンに保存されます。そうでない場合、ディスクイメージはこのサイズまで動的に拡張されます。"; -"GB" = "GB"; +"GiB" = "GiB"; // VMCardView.swift "Run" = "実行"; @@ -863,8 +863,8 @@ "%@ %@" = "%1$@ %2$@"; // VMSettingsAddDeviceMenuView.swift -"Import Drive" = "ドライブを読み込む"; -"New Drive" = "新規ドライブ"; +"Import Drive…" = "ドライブを読み込む…"; +"New Drive…" = "新規ドライブ…"; // VMToolbarModifier.swift "Remove selected shortcut" = "選択したショートカットを削除します"; diff --git a/Platform/ko.lproj/Localizable.strings b/Platform/ko.lproj/Localizable.strings index f62f6508d..b33febc92 100644 --- a/Platform/ko.lproj/Localizable.strings +++ b/Platform/ko.lproj/Localizable.strings @@ -398,7 +398,7 @@ "Manager being deallocated, killing pending RPC." = "Manager being deallocated, killing pending RPC."; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "메모리"; diff --git a/Platform/macOS/SettingsView.swift b/Platform/macOS/SettingsView.swift index f97779d42..9225a95ea 100644 --- a/Platform/macOS/SettingsView.swift +++ b/Platform/macOS/SettingsView.swift @@ -223,9 +223,12 @@ struct ServerSettingsView: View { .multilineTextAlignment(.trailing) .help("Specify a port number to listen on. This is required if external clients are permitted.") .onChange(of: serverPort) { newValue in - if serverPort == 0 { + if newValue == 0 { isServerExternal = false } + if newValue < 0 || newValue >= UInt16.max { + serverPort = defaultPort + } } } Section(header: Text("Authentication")) { diff --git a/Platform/pl.lproj/Localizable.strings b/Platform/pl.lproj/Localizable.strings index 34965726b..a97aa4fa0 100644 --- a/Platform/pl.lproj/Localizable.strings +++ b/Platform/pl.lproj/Localizable.strings @@ -422,8 +422,8 @@ "If checked, the drive image will be stored with the VM." = "Jeśli zaznaczone, obraz dysku będzie przechowywany wraz z wirtualną maszyną."; "Size" = "Rozmiar"; "The amount of storage to allocate for this image. An empty file of this size will be stored with the VM." = "Ilość pamięci masowej przydzielonej dla tego obrazu. Pusty plik takiego rozmiaru będzie przechowywany wraz z maszyną wirtualną"; -"GB" = "GB"; -"MB" = "MB"; +"GiB" = "GiB"; +"MiB" = "MiB"; /* VMConfigAppleDriveDetailsView.swift */ "Name" = "Nazwa"; @@ -547,14 +547,14 @@ /* RAMSlider.swift */ "Size" = "Rozmiar"; -"MB" = "MB"; +"MiB" = "MiB"; /* FileBrowseField.swift */ "Path" = "Ścieżka"; /* SizeTextField.swift */ "The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "Ilość pamięci masowej do przydzielenia dla tego obrazu. Ignorowane, jeśli importujesz obraz. Jeśli jest to surowy obraz, wtedy plusty plik tego rozmiaru będzie przechowywany wraz z wirtualną maszyną. W przeciwnym wypadku, obraz dysku będzie dynamicznie się rozszerzał do docelowego rozmiaru."; -"GB" = "GB"; +"GiB" = "GiB"; /* VMCardView.swift */ "Run" = "Uruchom"; @@ -787,8 +787,8 @@ "%@ %@" = "%@ %@"; /* VMSettingsAddDeviceMenuView.swift */ -"Import Drive" = "Importuj dysk"; -"New Drive" = "Nowy dysk"; +"Import Drive…" = "Importuj dysk..."; +"New Drive…" = "Nowy dysk..."; /* VMToolbarModifier.swift */ "Remove selected shortcut" = "Uruchom wybrany skrót"; @@ -1292,4 +1292,4 @@ "Floppy Drive" = "Napęd dyskietek"; "VirtIO Drive" = "Dysk VirtIO"; "NVMe Drive" = "Dysk NVMe"; -"USB Drive" = "Dysk USB"; \ No newline at end of file +"USB Drive" = "Dysk USB"; diff --git a/Platform/ru.lproj/Localizable.strings b/Platform/ru.lproj/Localizable.strings index f6f8eff9e..fe87d1ab1 100644 --- a/Platform/ru.lproj/Localizable.strings +++ b/Platform/ru.lproj/Localizable.strings @@ -244,7 +244,7 @@ "Change…" = "Изменить…"; /* No comment provided by engineer. */ -"Clear" = "Отчистить"; +"Clear" = "Очистить"; /* No comment provided by engineer. */ "Clipboard Sharing" = "Обмен буфером обмена"; @@ -681,7 +681,7 @@ "Full Graphics" = "Полная графика"; /* No comment provided by engineer. */ -"GB" = "ГБ"; +"GiB" = "ГиБ"; /* UTMQemuConstants */ "GDB Debug Stub" = "Плагин отладки GDB"; @@ -809,7 +809,7 @@ "Import…" = "Импорт…"; /* No comment provided by engineer. */ -"Import Drive" = "Импортировать диск"; +"Import Drive…" = "Импортировать диск…"; /* No comment provided by engineer. */ "Import VHDX Image" = "Импортировать образ VHDX"; @@ -1004,7 +1004,7 @@ "Maximum Shared USB Devices" = "Максимальное кол-во совместно используемых USB-устройств"; /* No comment provided by engineer. */ -"MB" = "МБ"; +"MiB" = "МиБ"; /* No comment provided by engineer. */ "Memory" = "Память"; @@ -1070,7 +1070,7 @@ "New…" = "Новый…"; /* No comment provided by engineer. */ -"New Drive" = "Новый диск"; +"New Drive…" = "Новый диск…"; /* No comment provided by engineer. */ "New from template…" = "Новая из шаблона…"; diff --git a/Platform/zh-HK.lproj/Localizable.strings b/Platform/zh-HK.lproj/Localizable.strings index 330e6fc9e..03497c89c 100644 --- a/Platform/zh-HK.lproj/Localizable.strings +++ b/Platform/zh-HK.lproj/Localizable.strings @@ -542,7 +542,7 @@ "Force shut down" = "強行關機"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB Debug Stub"; @@ -725,7 +725,7 @@ "Maximum Shared USB Devices" = "最多分享 USB 裝置"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "記憶體"; @@ -1861,7 +1861,7 @@ "Image Type" = "映像檔種類"; /* No comment provided by engineer. */ -"Import Drive" = "輸入磁碟機"; +"Import Drive…" = "輸入磁碟機⋯"; /* No comment provided by engineer. */ "Import VHDX Image" = "輸入 VHDX 映像檔"; @@ -1945,7 +1945,7 @@ "Network Mode" = "網絡模式"; /* No comment provided by engineer. */ -"New Drive" = "新增磁碟機"; +"New Drive…" = "新增磁碟機⋯"; /* No comment provided by engineer. */ "New from template…" = "由此範本新增⋯"; diff --git a/Platform/zh-Hans.lproj/Localizable.strings b/Platform/zh-Hans.lproj/Localizable.strings index 81ccee3d4..07e2bd160 100644 --- a/Platform/zh-Hans.lproj/Localizable.strings +++ b/Platform/zh-Hans.lproj/Localizable.strings @@ -542,7 +542,7 @@ "Force shut down" = "强制关机"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB 调试存根"; @@ -725,7 +725,7 @@ "Maximum Shared USB Devices" = "最大共享 USB 设备数"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "内存"; @@ -1861,7 +1861,7 @@ "Image Type" = "映像类型"; /* No comment provided by engineer. */ -"Import Drive" = "导入驱动器"; +"Import Drive…" = "导入驱动器…"; /* No comment provided by engineer. */ "Import VHDX Image" = "导入 VHDX 映像"; @@ -1945,7 +1945,7 @@ "Network Mode" = "网络模式"; /* No comment provided by engineer. */ -"New Drive" = "新建驱动器"; +"New Drive…" = "新建驱动器…"; /* No comment provided by engineer. */ "New from template…" = "从此模板新建…"; diff --git a/Platform/zh-Hant.lproj/Localizable.strings b/Platform/zh-Hant.lproj/Localizable.strings index 8070b72bd..39729e922 100644 --- a/Platform/zh-Hant.lproj/Localizable.strings +++ b/Platform/zh-Hant.lproj/Localizable.strings @@ -720,7 +720,7 @@ "FPS Limit" = "FPS 上限"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB 除錯 stub"; @@ -858,7 +858,7 @@ "Image Type" = "映像檔類型"; /* No comment provided by engineer. */ -"Import Drive" = "匯入磁碟機"; +"Import Drive…" = "匯入磁碟機⋯"; /* No comment provided by engineer. */ "Import IPSW" = "匯入 IPSW"; @@ -1023,7 +1023,7 @@ "Maximum Shared USB Devices" = "最大共享 USB 裝置"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "記憶體"; @@ -1080,7 +1080,7 @@ "New" = "新增"; /* No comment provided by engineer. */ -"New Drive" = "新增磁碟機"; +"New Drive…" = "新增磁碟機…"; /* No comment provided by engineer. */ "New from template…" = "從樣板建立…"; diff --git a/README.bn.md b/README.bn.md new file mode 100644 index 000000000..6ad88255f --- /dev/null +++ b/README.bn.md @@ -0,0 +1,83 @@ +# UTM +[![Build](https://github.com/utmapp/UTM/workflows/Build/badge.svg?branch=master&event=push)][1] + +> এমন মেশিন আবিষ্কার করা সম্ভব যা যেকোনো ধরনের সিকোয়েনশিয়াল গণনা করতে পারে তা যত কঠিন গণনাই হোক না কেন + +-- অ্যালান টুরিং, ১৯৩৬ + +UTM হল iOS এবং macOS-এর জন্য সমস্ত ফিচার বিশিষ্ট সিস্টেম ইমিউলেটর এবং ভার্চুয়াল মেশিন হোস্ট। এটি QEMU এর উপর বেজ করে বানানো হয়েছে । সংক্ষেপে বলা যায় যে, UTM আপনাকে আপনার ম্যাক, আইফোন এবং আইপ্যাডে উইন্ডোজ, লিনাক্স এবং আরও অনেক অপারেটিং সিস্টেম চালানোর অনুমতি দেয়। লিঙ্ক গুলোতে আরও বিস্তারিত জানতে পারবেনঃ +১। https://getutm.app/ এবং +২। https://mac.getutm.app/ + +

+ একটি আইফোনে UTM চলছে +
+ একটি ম্যাকবুকে UTM চলছে +

+ +## UTM এ যা যা ফিচার আছেঃ + +* QEMU ব্যবহার করে সম্পূর্ণ সিস্টেম ইমিউলেশন (MMU, ডিভাইস, ইত্যাদি) +* x86_64, ARM64, এবং RISC-V সহ 30+ প্রসেসর সাপোর্টেড +* SPICE এবং QXL ব্যবহার করে VGA গ্রাফিক্স মোড +* টেক্সট টার্মিনাল মোড +* ইউএসবি ডিভাইস +* QEMU TCG ব্যবহার করে JIT ভিত্তিক এক্সিলারেশন +* সবচেয়ে লেটেস্ট এবং সেরা API গুলো ব্যবহার করে macOS 11 এবং iOS 11+ এর জন্য স্ক্র্যাচ থেকে ডিজাইন করা ফ্রন্টেন্ড +* আপনার ডিভাইস থেকে সরাসরি VM তৈরি করুন, ম্যানেজ করুন, চালান + +## macOS এর ক্ষেত্রে অতিরিক্ত যা যা ফিচার আছে + +* Hypervisor.framework এবং QEMU ব্যবহার করে হার্ডওয়্যার এক্সিলারেটেড ভার্চুয়ালাইজেশন +* macOS 12+ এ Virtualization.framework সহ macOS গেস্ট বুট করুন + +## UTM SE + +UTM/QEMU-এর সর্বোচ্চ পারফরমেন্স এর জন্য ডায়নামিক কোড জেনারেশন (JIT) প্রয়োজন। iOS ডিভাইসে JIT-এর জন্য দরকার একটি জেলব্রোকেন ডিভাইস, অথবা iOS-এর নির্দিষ্ট ভার্শন এর জন্য যেকোনো ওয়ার্ক এরাউন্ড (আরো বিশদ বিবরণের জন্য "ইনস্টল" পার্ট টি দেখুন)। + +UTM SE ("স্লো এডিশন") একটি [থ্রেডেড ইন্টারপ্রেটার][3] ব্যবহার করে যা একটি ট্র্যাডিশনাল ইন্টারপ্রেটার এর চেয়ে যদিও ভাল পারফর্ম করে কিন্তু এখনও JIT এর চেয়ে স্লো। এই টেকনিকটি ডাইনামিক এক্সিকিউশন এর জন্য [iSH][4] যা করে তার মতোই। ফলস্বরূপ, UTM SE-এর জন্য জেলব্রেকিং বা কোনো JIT সমাধানের প্রয়োজন নেই এবং রেগুলার অ্যাপ হিসেবে সাইডলোড করা যায়। + +সাইজ এবং বিল্ড এর সময় অপ্টিমাইজ করার জন্য, শুধুমাত্র নিচের আর্কিটেকচারগুলি UTM SE-তে ইনক্লুড করা হয়েছে: ARM, PPC, RISC-V, এবং x86 (সমস্তই 32-বিট এবং 64-বিট ভেরিয়েন্টের সাথে)। + +## ইনস্টল + +iOS এর জন্য UTM (SE): https://getutm.app/install/ + + macOS এর জন্য UTM নামাতে পারবেন এখান থেকে: https://mac.getutm.app/ + +## ডেভেলপমেন্ট + +### [macOS ডেভেলপমেন্ট](Documentation/MacDevelopment.md) + +### [iOS ডেভেলপমেন্ট](Documentation/iOSDevelopment.md) + +## রিলেটেড + +* [iSH][4]: iOS এ x86 Linux অ্যাপ্লিকেশন চালানোর জন্য একটি usermode Linux টার্মিনাল ইন্টারফেস ইমিউলেট করে +* [a-shell][5]: সাধারণ ইউনিক্স কমান্ড এবং ইউটিলিটি যেগুলো iOS এর জন্য নেটিভ সেগুলো এটি প্যাকেজ করে দেয় এবং এটি টার্মিনাল ইন্টারফেসের মাধ্যমে অ্যাক্সেস করা যায়। + +## লাইসেন্স + +UTM পারমিসিভ Apache 2.0 লাইসেন্সের অধীনে ডিস্ট্রিবিউট করা হচ্ছে। সেই সাথে এটি বেশ কয়েকটি (L)GPL কম্পোনেন্ট ব্যবহার করছে। বেশিরভাগই ডাইন্যামিক্যালি লিঙ্কড কিন্তু gstreamer প্লাগইনগুলি স্ট্যাটিকভাবে লিঙ্ক করা এবং কোডের কিছু অংশ qemu থেকে নেওয়া। আপনি যদি এই অ্যাপ্লিকেশনটি পুনরায় ডিস্ট্রিবিউট করতে চান তবে দয়া করে এই বিষয় গুলো সম্পর্কে খেয়াল রাখবেন৷ +Some icons made by [Freepik](https://www.freepik.com) [www.flaticon.com](https://www.flaticon.com/). + +[www.flaticon.com](https://www.flaticon.com/) থেকে [ফ্রিপিকের](https://www.freepik.com) তৈরি কিছু আইকন এখানে ব্যবহার করা হয়েছে। + + +এছাড়াও, UTM ফ্রন্টএন্ড নিম্নলিখিত MIT/BSD লাইসেন্স কম্পোনেন্ট এর উপর নির্ভর করে: + +* [IQKeyboardManager](https://github.com/hackiftekhar/IQKeyboardManager) +* [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm) +* [ZIP Foundation](https://github.com/weichsel/ZIPFoundation) +* [InAppSettingsKit](https://github.com/futuretap/InAppSettingsKit) + +কন্টিনিউয়াস ইন্টিগ্রেশন হোস্টিং টি [MacStadium](https://www.macstadium.com/opensource) প্রোভাইড করছে + + +[MacStadium logo](https://www.macstadium.com) + + [1]: https://github.com/utmapp/UTM/actions?query=event%3Arelease+workflow%3ABuild + [2]: screen.png + [3]: https://github.com/ktemkin/qemu/blob/with_tcti/tcg/aarch64-tcti/README.md + [4]: https://github.com/ish-app/ish + [5]: https://github.com/holzschu/a-shell diff --git a/Remote/UTMRemoteServer.swift b/Remote/UTMRemoteServer.swift index 7a50dede6..0dd017167 100644 --- a/Remote/UTMRemoteServer.swift +++ b/Remote/UTMRemoteServer.swift @@ -131,7 +131,7 @@ actor UTMRemoteServer { registerNotifications() listener = Task { await withErrorNotification { - if isServerExternal && serverPort > 0 { + if isServerExternal && serverPort > 0 && serverPort <= UInt16.max { natPort = Port.TCP(internalPort: UInt16(serverPort)) natPort!.mappingChangedHandler = { port in Task { @@ -146,7 +146,7 @@ actor UTMRemoteServer { } } } - let port = serverPort > 0 ? NWEndpoint.Port(integerLiteral: UInt16(serverPort)) : .any + let port = serverPort > 0 && serverPort <= UInt16.max ? NWEndpoint.Port(integerLiteral: UInt16(serverPort)) : .any for try await connection in Connection.advertise(on: port, forServiceType: service, txtRecord: metadata, connectionQueue: connectionQueue, identity: keyManager.identity) { let connection = try? await Connection(connection: connection, connectionQueue: connectionQueue) { connection, error in Task { @@ -683,13 +683,16 @@ extension UTMRemoteServer { } @MainActor - private func findVM(withId id: UUID) throws -> VMData { + private func findVM(withId id: UUID, allowNotLoaded: Bool = false) throws -> VMData { let vm = data.virtualMachines.first(where: { $0.id == id }) - if let vm = vm, let _ = vm.wrapped { - return vm - } else { - throw UTMRemoteServer.ServerError.notFound(id) + if let vm = vm { + if let _ = vm.wrapped { + return vm + } else if allowNotLoaded { + return vm + } } + throw UTMRemoteServer.ServerError.notFound(id) } @MainActor @@ -735,7 +738,7 @@ extension UTMRemoteServer { private func _getVirtualMachineInformation(parameters: M.GetVirtualMachineInformation.Request) async throws -> M.GetVirtualMachineInformation.Reply { let informations = try await Task { @MainActor in try parameters.ids.map { id in - let vm = try findVM(withId: id) + let vm = try findVM(withId: id, allowNotLoaded: true) let mountedDrives = vm.registryEntry?.externalDrives.mapValues({ $0.path }) ?? [:] let isTakeoverAllowed = data.vmWindows[vm] is VMRemoteSessionState && (vm.state == .started || vm.state == .paused) return M.VirtualMachineInformation(id: vm.id, diff --git a/Services/UTMAppleVirtualMachine.swift b/Services/UTMAppleVirtualMachine.swift index f42008659..71ba4b53a 100644 --- a/Services/UTMAppleVirtualMachine.swift +++ b/Services/UTMAppleVirtualMachine.swift @@ -170,6 +170,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { } } } + try? updateLastModified() } func start(options: UTMVirtualMachineStartOptions = []) async throws { @@ -358,6 +359,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { } } } + try? updateLastModified() } #endif @@ -393,6 +395,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { } await registryEntry.setIsSuspended(false) try FileManager.default.removeItem(at: vmSavedStateURL) + try? updateLastModified() } #if arch(arm64) diff --git a/Services/UTMQemuVirtualMachine.swift b/Services/UTMQemuVirtualMachine.swift index 7d8c824e1..2994fea08 100644 --- a/Services/UTMQemuVirtualMachine.swift +++ b/Services/UTMQemuVirtualMachine.swift @@ -437,6 +437,11 @@ extension UTMQemuVirtualMachine { self.spicePort = spicePort } #endif + + // update timestamp + if !isRunningAsDisposible { + try? updateLastModified() + } } func start(options: UTMVirtualMachineStartOptions = []) async throws { @@ -549,6 +554,7 @@ extension UTMQemuVirtualMachine { if result.localizedCaseInsensitiveContains("Error") { throw UTMQemuVirtualMachineError.qemuError(result) } + try? updateLastModified() } func saveSnapshot(name: String? = nil) async throws { @@ -580,6 +586,7 @@ extension UTMQemuVirtualMachine { if result.localizedCaseInsensitiveContains("Error") { throw UTMQemuVirtualMachineError.qemuError(result) } + try? updateLastModified() } } @@ -784,9 +791,11 @@ extension UTMQemuVirtualMachine { } private func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL, isAccessOnly: Bool) async throws { - _ = url.startAccessingSecurityScopedResource() + let isScopedAccess = url.startAccessingSecurityScopedResource() defer { - url.stopAccessingSecurityScopedResource() + if isScopedAccess { + url.stopAccessingSecurityScopedResource() + } } let tempBookmark = try url.bookmarkData() try await eject(drive, isForced: true) @@ -803,7 +812,7 @@ extension UTMQemuVirtualMachine { } await registryEntry.updateExternalDriveRemoteBookmark(bookmark, forId: drive.id) if let qemu = await monitor, qemu.isConnected && !isAccessOnly { - try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path) + try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path, locking: false) } } diff --git a/Services/UTMSpiceVirtualMachine.swift b/Services/UTMSpiceVirtualMachine.swift index 7cd50a5b5..0be031bc3 100644 --- a/Services/UTMSpiceVirtualMachine.swift +++ b/Services/UTMSpiceVirtualMachine.swift @@ -105,9 +105,11 @@ extension UTMSpiceVirtualMachine { func changeSharedDirectory(to url: URL) async throws { await clearSharedDirectory() - _ = url.startAccessingSecurityScopedResource() + let isScopedAccess = url.startAccessingSecurityScopedResource() defer { - url.stopAccessingSecurityScopedResource() + if isScopedAccess { + url.stopAccessingSecurityScopedResource() + } } let file = try await UTMRegistryEntry.File(url: url, isReadOnly: config.sharing.isDirectoryShareReadOnly) await registryEntry.setSingleSharedDirectory(file) diff --git a/Services/UTMVirtualMachine.swift b/Services/UTMVirtualMachine.swift index b7eec04e1..14ae742b2 100644 --- a/Services/UTMVirtualMachine.swift +++ b/Services/UTMVirtualMachine.swift @@ -401,6 +401,14 @@ extension UTMVirtualMachine { try reload(from: newPath) try await updateRegistryBasics() // update bookmark } + // update last modified date + try? updateLastModified() + } + + /// Set the package's last modified time + /// - Parameter date: Last modified date + nonisolated func updateLastModified(to date: Date = Date()) throws { + try FileManager.default.setAttributes([.modificationDate: date], ofItemAtPath: pathUrl.path) } } diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index 4603c8241..d898c1555 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/UTM.xcodeproj/project.pbxproj @@ -892,6 +892,10 @@ CEC794BD2949663C00121A9F /* UTMScripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC794BB2949663C00121A9F /* UTMScripting.swift */; }; CED234ED254796E500ED0A57 /* NumberTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED234EC254796E500ED0A57 /* NumberTextField.swift */; }; CED234EE254796E500ED0A57 /* NumberTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED234EC254796E500ED0A57 /* NumberTextField.swift */; }; + CED779E52C78C82A00EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; }; + CED779E62C78C82A00EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; }; + CED779E72C78C82A00EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; }; + CED779E82C79062500EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; }; CED814E924C79F070042F0F1 /* VMConfigDriveCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */; }; CED814EA24C79F070042F0F1 /* VMConfigDriveCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */; }; CED814EC24C7C2850042F0F1 /* VMConfigInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */; }; @@ -2017,6 +2021,7 @@ CECF02562B706ADD00409FC0 /* UTMRemoteConnectInterface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMRemoteConnectInterface.h; sourceTree = ""; }; CECF02572B70909900409FC0 /* Info-Remote.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Remote.plist"; sourceTree = ""; }; CED234EC254796E500ED0A57 /* NumberTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberTextField.swift; sourceTree = ""; }; + CED779E42C78C82A00EB82AE /* UTMTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMTips.swift; sourceTree = ""; }; CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigDriveCreateView.swift; sourceTree = ""; }; CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigInfoView.swift; sourceTree = ""; }; CED814EE24C7EB760042F0F1 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; @@ -2972,6 +2977,7 @@ 83034C0626AB630F006B4BAF /* UTMPendingVMView.swift */, 84909A8C27CACD5C005605F1 /* UTMPlaceholderVMView.swift */, 84909A9027CADAE0005605F1 /* UTMUnavailableVMView.swift */, + CED779E42C78C82A00EB82AE /* UTMTips.swift */, ); path = Shared; sourceTree = ""; @@ -3657,6 +3663,7 @@ CE2D958F24AD4FF00059923A /* VMCardView.swift in Sources */, 8432329028C2CDAD00CFBC97 /* VMNavigationListView.swift in Sources */, 841E58CE28937FED00137A20 /* UTMSingleWindowView.swift in Sources */, + CED779E52C78C82A00EB82AE /* UTMTips.swift in Sources */, CE2D930B24AD46670059923A /* UTMLegacyQemuConfiguration+Sharing.m in Sources */, 84C505AC28C588EC007CE8FF /* SizeTextField.swift in Sources */, 8471770627CC974F00D3A50B /* DefaultTextField.swift in Sources */, @@ -3720,6 +3727,7 @@ 8432329628C2ED9000CFBC97 /* FileBrowseField.swift in Sources */, 848A98C2286A2257006F0550 /* UTMAppleConfigurationMacPlatform.swift in Sources */, 84B36D2B27B790BE00C22685 /* DestructiveButton.swift in Sources */, + CED779E82C79062500EB82AE /* UTMTips.swift in Sources */, CE9B154A2B12A87E003A32DD /* GenerateKey.c in Sources */, CE020BAC24AEE00000B44AB6 /* UTMLoggingSwift.swift in Sources */, 848D99BA28630A780055C215 /* VMConfigSerialView.swift in Sources */, @@ -3924,6 +3932,7 @@ CEA45E63263519B5002FA97D /* UTMLegacyViewState.m in Sources */, CEA45E64263519B5002FA97D /* UTMLoggingSwift.swift in Sources */, 841619A7284315C1000034B2 /* UTMQemuConfiguration.swift in Sources */, + CED779E62C78C82A00EB82AE /* UTMTips.swift in Sources */, 84C2E8662AA429E800B17308 /* VMWizardContent.swift in Sources */, CEF0305F26A2AFDF00667B63 /* VMWizardState.swift in Sources */, CEA45E69263519B5002FA97D /* VMConfigQEMUView.swift in Sources */, @@ -4152,6 +4161,7 @@ CEF7F5F92AEEDCC400E34952 /* VMToolbarDriveMenuView.swift in Sources */, CE08334B2B784FD400522C03 /* RemoteContentView.swift in Sources */, CEF7F5FA2AEEDCC400E34952 /* VMSettingsView.swift in Sources */, + CED779E72C78C82A00EB82AE /* UTMTips.swift in Sources */, CEF7F5FB2AEEDCC400E34952 /* VMDisplayViewController.swift in Sources */, CEF7F5FC2AEEDCC400E34952 /* VMWizardStartView.swift in Sources */, CEF7F5FD2AEEDCC400E34952 /* QEMUConstantGenerated.swift in Sources */, diff --git a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 932af6201..d5f72bb46 100644 --- a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -52,7 +52,7 @@ "location" : "https://github.com/utmapp/QEMUKit.git", "state" : { "branch" : "main", - "revision" : "1019ed76278a1d5cbe871ff5e51c62b5d8c9a032" + "revision" : "c0c978d5566928b2f2b93005c2aa720bba01157a" } }, { diff --git a/patches/qemu-7.2.0-utm.patch b/patches/qemu-7.2.0-utm.patch index 66fd5f2bb..452529827 100644 --- a/patches/qemu-7.2.0-utm.patch +++ b/patches/qemu-7.2.0-utm.patch @@ -7054,3 +7054,132 @@ index 0df9dd8fd5..0e9a3e469d 100644 -- 2.41.0 +From 7129cfcc829baeb05ba2c7267ef7cc8e19ed9cef Mon Sep 17 00:00:00 2001 +From: osy +Date: Thu, 22 Aug 2024 16:42:50 -0500 +Subject: [PATCH] block: support locking on change medium + +New optional argument for 'blockdev-change-medium' QAPI command to allow +the caller to specify if they wish to enable file locking. +--- + block/qapi-sysemu.c | 22 ++++++++++++++++++++++ + monitor/hmp-cmds.c | 1 + + qapi/block.json | 23 ++++++++++++++++++++++- + ui/cocoa/app_controller.m | 1 + + 4 files changed, 46 insertions(+), 1 deletion(-) + +diff --git a/block/qapi-sysemu.c b/block/qapi-sysemu.c +index 680c7ee342..888e13e539 100644 +--- a/block/qapi-sysemu.c ++++ b/block/qapi-sysemu.c +@@ -321,6 +321,8 @@ void qmp_blockdev_change_medium(bool has_device, const char *device, + bool has_force, bool force, + bool has_read_only, + BlockdevChangeReadOnlyMode read_only, ++ bool has_file_locking_mode, ++ BlockdevChangeFileLockingMode file_locking_mode, + Error **errp) + { + BlockBackend *blk; +@@ -374,6 +376,26 @@ void qmp_blockdev_change_medium(bool has_device, const char *device, + qdict_put_str(options, "driver", format); + } + ++ if (!has_file_locking_mode) { ++ file_locking_mode = BLOCKDEV_CHANGE_FILE_LOCKING_MODE_AUTO; ++ } ++ ++ switch (file_locking_mode) { ++ case BLOCKDEV_CHANGE_FILE_LOCKING_MODE_AUTO: ++ break; ++ ++ case BLOCKDEV_CHANGE_FILE_LOCKING_MODE_OFF: ++ qdict_put_str(options, "file.locking", "off"); ++ break; ++ ++ case BLOCKDEV_CHANGE_FILE_LOCKING_MODE_ON: ++ qdict_put_str(options, "file.locking", "on"); ++ break; ++ ++ default: ++ abort(); ++ } ++ + medium_bs = bdrv_open(filename, NULL, options, bdrv_flags, errp); + if (!medium_bs) { + goto fail; +diff --git a/monitor/hmp-cmds.c b/monitor/hmp-cmds.c +index 01b789a79e..9e42634c17 100644 +--- a/monitor/hmp-cmds.c ++++ b/monitor/hmp-cmds.c +@@ -1499,6 +1499,7 @@ void hmp_change(Monitor *mon, const QDict *qdict) + qmp_blockdev_change_medium(true, device, false, NULL, target, + !!arg, arg, true, force, + !!read_only, read_only_mode, ++ false, 0, + &err); + } + +diff --git a/qapi/block.json b/qapi/block.json +index 5fe068f903..0034ebe941 100644 +--- a/qapi/block.json ++++ b/qapi/block.json +@@ -303,6 +303,23 @@ + { 'enum': 'BlockdevChangeReadOnlyMode', + 'data': ['retain', 'read-only', 'read-write'] } + ++## ++# @BlockdevChangeFileLockingMode: ++# ++# Specifies the new locking mode of a file image passed to the ++# @blockdev-change-medium command. ++# ++# @auto: Use locking if API is available ++# ++# @off: Disable file image locking ++# ++# @on: Enable file image locking ++# ++# Since: 9.2 ++## ++{ 'enum': 'BlockdevChangeFileLockingMode', ++ 'data': ['auto', 'off', 'on'] } ++ + ## + # @blockdev-change-medium: + # +@@ -324,6 +341,9 @@ + # @read-only-mode: change the read-only mode of the device; defaults + # to 'retain' + # ++# @file-locking-mode: change the locking mode of the file image; defaults ++# to 'auto' ++# + # @force: if false (the default), an eject request through blockdev-open-tray + # will be sent to the guest if it has locked the tray (and the tray + # will not be opened immediately); if true, the tray will be opened +@@ -371,7 +391,8 @@ + 'filename': 'str', + '*format': 'str', + '*force': 'bool', +- '*read-only-mode': 'BlockdevChangeReadOnlyMode' } } ++ '*read-only-mode': 'BlockdevChangeReadOnlyMode', ++ '*file-locking-mode': 'BlockdevChangeFileLockingMode' } } + + ## + # @DEVICE_TRAY_MOVED: +diff --git a/ui/cocoa/app_controller.m b/ui/cocoa/app_controller.m +index 496036162d..0a062950cf 100644 +--- a/ui/cocoa/app_controller.m ++++ b/ui/cocoa/app_controller.m +@@ -373,6 +373,7 @@ - (void)changeDeviceMedia:(id)sender + "raw", + true, false, + false, 0, ++ false, 0, + &err); + qemu_mutex_unlock_iothread(); + handleAnyDeviceErrors(err); +-- +2.41.0 + diff --git a/scripts/build_utm.sh b/scripts/build_utm.sh index 00fa6a5b3..155eaa149 100755 --- a/scripts/build_utm.sh +++ b/scripts/build_utm.sh @@ -7,7 +7,7 @@ command -v realpath >/dev/null 2>&1 || realpath() { BASEDIR="$(dirname "$(realpath $0)")" usage () { - echo "Usage: $(basename $0) [-t teamid] [-p platform] [-s scheme] [-a architecture] [-t targetversion] [-o output]" + echo "Usage: $(basename $0) [-t teamid] [-k SDK] [-s scheme] [-a architecture] [-o output]" echo "" echo " -t teamid Team Identifier for app groups. Optional for iOS. Required for macOS." echo " -k sdk Target SDK. Default iphoneos. [iphoneos|iphonesimulator|xros|xrsimulator|macosx]" diff --git a/scripts/package.sh b/scripts/package.sh index b08062675..6465f9bd0 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -142,7 +142,7 @@ cat >"$DEB_TMP/DEBIAN/control" <=14.0), firmware-sbin, net.angelxwind.appsyncunified Installed-Size: ${SIZE_KIB} Maintainer: osy