Skip to content

Commit

Permalink
Merge pull request #6115 from utmapp/dev/remote
Browse files Browse the repository at this point in the history
Introduce UTM Remote client for iOS/visionOS
  • Loading branch information
osy authored Feb 26, 2024
2 parents 62bd84c + aa071bd commit 8a7a531
Show file tree
Hide file tree
Showing 90 changed files with 6,734 additions and 555 deletions.
50 changes: 27 additions & 23 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ on:
default: 'false'

env:
BUILD_XCODE_PATH: /Applications/Xcode_15.1.app
BUILD_XCODE_PATH: /Applications/Xcode_15.2.app
RUNNER_IMAGE: macos-13

jobs:
Expand Down Expand Up @@ -53,7 +53,7 @@ jobs:
strategy:
matrix:
arch: [arm64]
platform: [ios, ios_simulator, ios-tci, macos, visionos, visionos_simulator, visionos-tci]
platform: [ios, ios_simulator, ios-tci, ios_simulator-tci, macos, visionos, visionos_simulator, visionos-tci, visionos_simulator-tci]
include:
# x86_64 supported only for macOS and simulators
- arch: x86_64
Expand Down Expand Up @@ -91,7 +91,7 @@ jobs:
if: steps.cache-sysroot.outputs.cache-hit != 'true' || github.event.inputs.rebuild_sysroot == 'true'
run: ./scripts/build_dependencies.sh -p ${{ matrix.platform }} -a ${{ matrix.arch }}
env:
NCPU: ${{ matrix.platform == 'ios-tci' && '2' || '0' }} # limit 2 CPU for TCI build due to memory issues, 0 = unlimited for other builds
NCPU: ${{ endsWith(matrix.platform, '-tci') && '4' || '0' }} # limit 4 CPU for TCI build due to memory issues, 0 = unlimited for other builds
- name: Compress Sysroot
if: steps.cache-sysroot.outputs.cache-hit != 'true' || github.event_name == 'release' || github.event.inputs.test_release == 'true'
run: tar -acf sysroot.tgz sysroot*
Expand Down Expand Up @@ -152,14 +152,16 @@ jobs:
needs: [configuration, build-sysroot]
strategy:
matrix:
arch: [arm64]
platform: [ios, ios_simulator, ios-tci, macos, visionos, visionos_simulator, visionos-tci]
include:
# x86_64 supported only for macOS and simulators
- arch: x86_64
platform: macos
- arch: x86_64
platform: ios_simulator
configuration: [
{arch: "arm64", sdk: "iphoneos", platform: "ios", scheme: "iOS"},
{arch: "arm64", sdk: "iphoneos", platform: "ios-tci", scheme: "iOS-SE"},
{arch: "arm64", sdk: "iphoneos", platform: "ios-tci", scheme: "iOS-Remote"},
{arch: "arm64", sdk: "xros", platform: "visionos", scheme: "iOS"},
{arch: "arm64", sdk: "xros", platform: "visionos-tci", scheme: "iOS-SE"},
{arch: "arm64", sdk: "xros", platform: "visionos-tci", scheme: "iOS-Remote"},
{arch: "arm64", sdk: "macosx", platform: "macos", scheme: "macOS"},
{arch: "x86_64", sdk: "macosx", platform: "macos", scheme: "macOS"},
]
steps:
- name: Checkout
uses: actions/checkout@v3
Expand All @@ -169,8 +171,8 @@ jobs:
id: cache-sysroot
uses: osy/actions-cache@v3
with:
path: sysroot-${{ matrix.platform }}-${{ matrix.arch }}
key: ${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('scripts/build_dependencies.sh') }}-${{ hashFiles('patches/**') }}
path: sysroot-${{ matrix.configuration.platform }}-${{ matrix.configuration.arch }}
key: ${{ matrix.configuration.platform }}-${{ matrix.configuration.arch }}-${{ hashFiles('scripts/build_dependencies.sh') }}-${{ hashFiles('patches/**') }}
- name: Check Cache
if: steps.cache-sysroot.outputs.cache-hit != 'true'
uses: actions/github-script@v6
Expand All @@ -182,12 +184,12 @@ jobs:
[[ "$(xcode-select -p)" == "${{ env.BUILD_XCODE_PATH }}"* ]] || sudo xcode-select -s "${{ env.BUILD_XCODE_PATH }}"
- name: Build UTM
run: |
./scripts/build_utm.sh -p ${{ matrix.platform }} -a ${{ matrix.arch }} -o UTM
./scripts/build_utm.sh -k ${{ matrix.configuration.sdk }} -s ${{ matrix.configuration.scheme }} -a ${{ matrix.configuration.arch }} -o UTM
tar -acf UTM.xcarchive.tgz UTM.xcarchive
- name: Upload UTM
uses: actions/upload-artifact@v3
with:
name: UTM-${{ matrix.platform }}-${{ matrix.arch }}
name: UTM-${{ matrix.configuration.scheme }}-${{ matrix.configuration.platform }}-${{ matrix.configuration.arch }}
path: UTM.xcarchive.tgz
build-universal:
name: Build UTM (Universal Mac)
Expand Down Expand Up @@ -215,7 +217,7 @@ jobs:
[[ "$(xcode-select -p)" == "${{ env.BUILD_XCODE_PATH }}"* ]] || sudo xcode-select -s "${{ env.BUILD_XCODE_PATH }}"
- name: Build UTM
run: |
./scripts/build_utm.sh -t "$SIGNING_TEAM_ID" -p macos -a "arm64 x86_64" -o UTM
./scripts/build_utm.sh -t "$SIGNING_TEAM_ID" -k macosx -s macOS -a "arm64 x86_64" -o UTM
tar -acf UTM.xcarchive.tgz UTM.xcarchive
env:
SIGNING_TEAM_ID: ${{ vars.SIGNING_TEAM_ID }}
Expand All @@ -231,12 +233,14 @@ jobs:
strategy:
matrix:
configuration: [
{platform: "ios", mode: "ipa", name: "UTM.ipa", path: "UTM.ipa"},
{platform: "ios-tci", mode: "ipa-se", name: "UTM-SE.ipa", path: "UTM SE.ipa"},
{platform: "ios", mode: "ipa-hv", name: "UTM-HV.ipa", path: "UTM.ipa"},
{platform: "ios", mode: "deb", name: "UTM.deb", path: "UTM.deb"},
{platform: "visionos", mode: "ipa", name: "UTM-visionOS.ipa", path: "UTM.ipa"},
{platform: "visionos-tci", mode: "ipa-se", name: "UTM-SE-visionOS.ipa", path: "UTM SE.ipa"}
{platform: "ios", scheme: "iOS", mode: "ipa", name: "UTM.ipa", path: "UTM.ipa"},
{platform: "ios-tci", scheme: "iOS-SE", mode: "ipa-se", name: "UTM-SE.ipa", path: "UTM SE.ipa"},
{platform: "ios", scheme: "iOS", mode: "ipa-hv", name: "UTM-HV.ipa", path: "UTM.ipa"},
{platform: "ios", scheme: "iOS", mode: "deb", name: "UTM.deb", path: "UTM.deb"},
{platform: "visionos", scheme: "iOS", mode: "ipa", name: "UTM-visionOS.ipa", path: "UTM.ipa"},
{platform: "visionos-tci", scheme: "iOS-SE", mode: "ipa-se", name: "UTM-SE-visionOS.ipa", path: "UTM SE.ipa"},
{platform: "ios-tci", scheme: "iOS-Remote", mode: "ipa-remote", name: "UTM-Remote.ipa", path: "UTM Remote.ipa"},
{platform: "visionos-tci", scheme: "iOS-Remote", mode: "ipa-remote", name: "UTM-Remote-visionOS.ipa", path: "UTM Remote.ipa"},
]
if: github.event_name == 'release' || github.event.inputs.test_release == 'true'
steps:
Expand All @@ -245,7 +249,7 @@ jobs:
- name: Download Artifact
uses: actions/download-artifact@v3
with:
name: UTM-${{ matrix.configuration.platform }}-arm64
name: UTM-${{ matrix.configuration.scheme }}-${{ matrix.configuration.platform }}-arm64
- name: Install ldid + dpkg
run: brew install ldid dpkg
- name: Fakesign IPA
Expand Down
20 changes: 10 additions & 10 deletions Configuration/QEMUConstant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -424,20 +424,20 @@ extension QEMUArchitecture {
default: return true
}
}

var hasHypervisorSupport: Bool {
guard jb_has_hypervisor() else {
guard UTMCapabilities.current.contains(.hasHypervisorSupport) else {
return false
}
if UTMCapabilities.current.contains(.isAarch64) {
return self == .aarch64
} else if UTMCapabilities.current.contains(.isX86_64) {
return self == .x86_64
} else {
return false
}
#if arch(arm64)
return self == .aarch64
#elseif arch(x86_64)
return self == .x86_64
#else
return false
#endif
}

/// TSO is supported on jailbroken iOS devices with Hypervisor support
var hasTSOSupport: Bool {
#if os(iOS) || os(visionOS)
Expand Down
2 changes: 1 addition & 1 deletion Configuration/UTMConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ extension UTMConfiguration {
#endif
// is it a legacy QEMU config?
let dict = try NSDictionary(contentsOf: configURL, error: ()) as! [AnyHashable : Any]
let name = UTMQemuVirtualMachine.virtualMachineName(for: packageURL)
let name = ConcreteVirtualMachine.virtualMachineName(for: packageURL)
let legacy = UTMLegacyQemuConfiguration(dictionary: dict, name: name, path: packageURL)
return UTMQemuConfiguration(migrating: legacy)
} else if stub.backend == .qemu {
Expand Down
7 changes: 5 additions & 2 deletions Configuration/UTMConfigurationDrive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
//

import Foundation
import QEMUKitInternal

/// Settings for single disk device
protocol UTMConfigurationDrive: Codable, Hashable, Identifiable {
Expand Down Expand Up @@ -101,13 +100,17 @@ extension UTMConfigurationDrive {
try handle.close()
}.value
}

private func createQcow2Image(at newURL: URL, size sizeMib: Int) async throws {
#if WITH_REMOTE
fatalError("Not implemented")
#else
try await Task.detached {
if !QEMUGenerateDefaultQcow2File(newURL as CFURL, sizeMib) {
throw UTMConfigurationError.cannotCreateDiskImage
}
}.value
#endif
}

#if os(macOS)
Expand Down
121 changes: 105 additions & 16 deletions Configuration/UTMQemuConfiguration+Arguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ import Virtualization // for getting network interfaces
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("swtpm")
}

/// Used only if in remote sever mode.
var monitorPipeURL: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qmp")
}

/// Used only if in remote sever mode.
var guestAgentPipeURL: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qga")
}

/// Used only if in remote sever mode.
var spiceTlsKeyUrl: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("pem")
}

/// Used only if in remote sever mode.
var spiceTlsCertUrl: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("crt")
}

/// Combined generated and user specified arguments.
@QEMUArgumentBuilder var allArguments: [QEMUArgument] {
generatedArguments
Expand Down Expand Up @@ -109,16 +129,48 @@ import Virtualization // for getting network interfaces

@QEMUArgumentBuilder private var spiceArguments: [QEMUArgument] {
f("-spice")
"unix=on"
"addr=\(spiceSocketURL.lastPathComponent)"
"disable-ticketing=on"
"image-compression=off"
"playback-compression=off"
"streaming-video=off"
"gl=\(isGLOn ? "on" : "off")"
if let port = qemu.spiceServerPort {
if qemu.isSpiceServerTlsEnabled {
"tls-port=\(port)"
"tls-channel=default"
"x509-key-file="
spiceTlsKeyUrl
"x509-cert-file="
spiceTlsCertUrl
"x509-cacert-file="
spiceTlsCertUrl
} else {
"port=\(port)"
}
} else {
"unix=on"
"addr=\(spiceSocketURL.lastPathComponent)"
}
if let _ = qemu.spiceServerPassword {
"password-secret=secspice0"
} else {
"disable-ticketing=on"
}
if !isRemoteSpice {
"image-compression=off"
"playback-compression=off"
"streaming-video=off"
} else {
"streaming-video=filter"
}
"gl=\(isGLSupported && !isRemoteSpice ? "on" : "off")"
f()
f("-chardev")
f("spiceport,id=org.qemu.monitor.qmp,name=org.qemu.monitor.qmp.0")
if isRemoteSpice {
"pipe"
"path="
monitorPipeURL
} else {
"spiceport"
"name=org.qemu.monitor.qmp.0"
}
"id=org.qemu.monitor.qmp"
f()
f("-mon")
f("chardev=org.qemu.monitor.qmp,mode=control")
if !isSparc { // disable -vga and other default devices
Expand All @@ -128,8 +180,28 @@ import Virtualization // for getting network interfaces
f("-vga")
f("none")
}
if let password = qemu.spiceServerPassword {
// assume anyone who can read this is in our trust domain
f("-object")
f("secret,id=secspice0,data=\(password)")
}
}


private func filterDisplayIfRemote(_ display: any QEMUDisplayDevice) -> any QEMUDisplayDevice {
if isRemoteSpice {
let rawValue = display.rawValue
if rawValue.hasSuffix("-gl") {
return AnyQEMUConstant(rawValue: String(rawValue.dropLast(3)))!
} else if rawValue.contains("-gl-") {
return AnyQEMUConstant(rawValue: String(rawValue.replacingOccurrences(of: "-gl-", with: "-")))!
} else {
return display
}
} else {
return display
}
}

@QEMUArgumentBuilder private var displayArguments: [QEMUArgument] {
if displays.isEmpty {
f("-nographic")
Expand All @@ -143,7 +215,7 @@ import Virtualization // for getting network interfaces
} else {
for display in displays {
f("-device")
display.hardware
filterDisplayIfRemote(display.hardware)
if let vgaRamSize = displays[0].vgaRamMib {
"vgamem_mb=\(vgaRamSize)"
}
Expand All @@ -152,7 +224,7 @@ import Virtualization // for getting network interfaces
}
}

private var isGLOn: Bool {
private var isGLSupported: Bool {
displays.contains { display in
display.hardware.rawValue.contains("-gl-") || display.hardware.rawValue.hasSuffix("-gl")
}
Expand All @@ -161,7 +233,11 @@ import Virtualization // for getting network interfaces
private var isSparc: Bool {
system.architecture == .sparc || system.architecture == .sparc64
}


private var isRemoteSpice: Bool {
qemu.spiceServerPort != nil
}

@QEMUArgumentBuilder private var serialArguments: [QEMUArgument] {
for i in serials.indices {
f("-chardev")
Expand Down Expand Up @@ -318,9 +394,9 @@ import Virtualization // for getting network interfaces
}
let tbSize = system.jitCacheSize > 0 ? system.jitCacheSize : system.memorySize / 4
"tb-size=\(tbSize)"
#if !WITH_QEMU_TCI
#if WITH_JIT
// use mirror mapping when we don't have JIT entitlements
if !jb_has_jit_entitlement() {
if !UTMCapabilities.current.contains(.hasJitEntitlements) {
"split-wx=on"
}
#endif
Expand Down Expand Up @@ -433,6 +509,10 @@ import Virtualization // for getting network interfaces
#if os(iOS) || os(visionOS)
return false
#else
// only support SPICE audio if we are running remotely
if isRemoteSpice {
return false
}
// force CoreAudio backend for mac99 which only supports 44100 Hz
// pcspk doesn't work with SPICE audio
if sound.contains(where: { $0.hardware.rawValue == "screamer" || $0.hardware.rawValue == "pcspk" }) {
Expand Down Expand Up @@ -671,7 +751,7 @@ import Virtualization // for getting network interfaces
f("usb-mouse,bus=usb-bus.0")
f("-device")
f("usb-kbd,bus=usb-bus.0")
#if !WITH_QEMU_TCI
#if WITH_USB
let maxDevices = input.maximumUsbShare
let buses = (maxDevices + 2) / 3
if input.usbBusSupport == .usb3_0 {
Expand Down Expand Up @@ -859,7 +939,16 @@ import Virtualization // for getting network interfaces
f("-device")
f("virtserialport,chardev=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
f("-chardev")
f("spiceport,id=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
if isRemoteSpice {
"pipe"
"path="
guestAgentPipeURL
} else {
"spiceport"
"name=org.qemu.guest_agent.0"
}
"id=org.qemu.guest_agent"
f()
}
if isSpiceAgentUsed {
f("-device")
Expand Down
9 changes: 9 additions & 0 deletions Configuration/UTMQemuConfigurationQEMU.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ struct UTMQemuConfigurationQEMU: Codable {
/// Set to true to request UEFI variable reset. Not saved.
var isUefiVariableResetRequested: Bool = false

/// Set to open a port for remote SPICE session. Not saved.
var spiceServerPort: UInt16?

/// If true, all SPICE channels will be over TLS. Not saved.
var isSpiceServerTlsEnabled: Bool = false

/// Set to a password shared with the client. Not saved.
var spiceServerPassword: String?

enum CodingKeys: String, CodingKey {
case hasDebugLog = "DebugLog"
case hasUefiBoot = "UEFIBoot"
Expand Down
Loading

0 comments on commit 8a7a531

Please sign in to comment.