Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Scripts/check_guest_app_deployment_target.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env zsh

# This script prevents raising the deployment target for VirtualBuddyGuest without placing the corresponding legacy archive in data/LegacyGuest,
# ensuring that older releases are always made available to users running legacy guest OSes

set -e

source "Scripts/legacy_guest_env.zsh"

# This only runs for builds using Managed configurations (Beta configurations are also managed)
if [[ "$CONFIGURATION" == *"Managed"* || "$CONFIGURATION" == *"Beta"* ]]; then
EXPECTED_GUEST_ARCHIVE="${SRCROOT}/${LEGACY_GUEST_ARCHIVE_DIR}/VirtualBuddyGuest_minOS_${LATEST_LEGACY_GUEST_APP_ARCHIVE_DEPLOYMENT_TARGET}.dmg"

if [ ! -f "${EXPECTED_GUEST_ARCHIVE}" ]; then
echo "error: The deployment target for VirtualBuddyGuest has been raised to ${MACOSX_DEPLOYMENT_TARGET} without a legacy archive being created for the latest version that supported ${LATEST_LEGACY_GUEST_APP_ARCHIVE_DEPLOYMENT_TARGET}. Please use dmgdist against a notarized build of VirtualBuddyGuest to generate an archive and register it with vctool before proceeding with this release."
exit 1
fi
fi
9 changes: 9 additions & 0 deletions Scripts/legacy_guest_env.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env zsh

# Path to legacy guest app archive directory relative to VirtualBuddy repo root
export LEGACY_GUEST_ARCHIVE_DIR=data/LegacyGuestApp

# The deployment target for the latest VirtualBuddyGuest app archive
# If a managed build is attempted with a different value without an archive
# existing with this version number suffix, the build fails.
export LATEST_LEGACY_GUEST_APP_ARCHIVE_DEPLOYMENT_TARGET="14.0"
26 changes: 26 additions & 0 deletions VirtualBuddy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
F43B01472AD85A7D00164CD1 /* URLQueryItemCoder in Frameworks */ = {isa = PBXBuildFile; productRef = F43B01462AD85A7D00164CD1 /* URLQueryItemCoder */; };
F43B014B2AD85ABB00164CD1 /* DeepLinkAuthDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B01492AD85ABB00164CD1 /* DeepLinkAuthDialog.swift */; };
F43B014E2AD86BFA00164CD1 /* DeepLinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43B014D2AD86BFA00164CD1 /* DeepLinkHandler.swift */; };
F43ED2BE2FDD9631001D143B /* LegacyGuestAppCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43ED2BD2FDD9631001D143B /* LegacyGuestAppCommand.swift */; };
F443620A29B7947A00745B43 /* GuestAdditionsDiskImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F443620929B7947A00745B43 /* GuestAdditionsDiskImage.swift */; };
F443620C29B79A6800745B43 /* VirtualBuddyGuest.app in Embed Guest App */ = {isa = PBXBuildFile; fileRef = F4C18A4228491B8500335EC7 /* VirtualBuddyGuest.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
F443620F29B7A0C600745B43 /* CreateGuestImage.sh in Resources */ = {isa = PBXBuildFile; fileRef = F443620E29B7A0C600745B43 /* CreateGuestImage.sh */; };
Expand Down Expand Up @@ -701,6 +702,7 @@
F43B01432AD85A6500164CD1 /* VirtualBuddyDeepLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualBuddyDeepLinks.swift; sourceTree = "<group>"; };
F43B01492AD85ABB00164CD1 /* DeepLinkAuthDialog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkAuthDialog.swift; sourceTree = "<group>"; };
F43B014D2AD86BFA00164CD1 /* DeepLinkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkHandler.swift; sourceTree = "<group>"; };
F43ED2BD2FDD9631001D143B /* LegacyGuestAppCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGuestAppCommand.swift; sourceTree = "<group>"; };
F443620929B7947A00745B43 /* GuestAdditionsDiskImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestAdditionsDiskImage.swift; sourceTree = "<group>"; };
F443620E29B7A0C600745B43 /* CreateGuestImage.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = CreateGuestImage.sh; sourceTree = "<group>"; };
F444D0C92DF321CD0086537A /* CatalogGroupPlaceholder.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = CatalogGroupPlaceholder.heic; sourceTree = "<group>"; };
Expand Down Expand Up @@ -965,6 +967,7 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
F43EC1962FDD8233001D143B /* Scripts */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Scripts; sourceTree = "<group>"; };
F4EFB5642FDB2A0D00AF2A63 /* VirtualInstallation */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (F4EFB5AD2FDB2A7600AF2A63 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = VirtualInstallation; sourceTree = "<group>"; };
F4EFB5B52FDB2BD400AF2A63 /* VirtualInstallationService */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (F4EFB5C02FDB2BD400AF2A63 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = VirtualInstallationService; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
Expand Down Expand Up @@ -1467,6 +1470,7 @@
F453C4312DF0B7A5007EAD5F /* Core */,
F453C4322DF0B7A5007EAD5F /* CatalogCommand.swift */,
F453C4332DF0B7A5007EAD5F /* GroupCommand.swift */,
F43ED2BD2FDD9631001D143B /* LegacyGuestAppCommand.swift */,
F453C4342DF0B7A5007EAD5F /* ImageCommand.swift */,
F453C4352DF0B7A5007EAD5F /* IPSWCommand.swift */,
F453C4362DF0B7A5007EAD5F /* MigrateCommand.swift */,
Expand Down Expand Up @@ -1834,6 +1838,7 @@
F4BE9C4527FF052100B648F8 = {
isa = PBXGroup;
children = (
F43EC1962FDD8233001D143B /* Scripts */,
F4BE9C5027FF052100B648F8 /* VirtualBuddy */,
F4BE9C6627FF053A00B648F8 /* VirtualCore */,
F498ACFF2884BF13006F1C00 /* VirtualUI */,
Expand Down Expand Up @@ -2379,6 +2384,7 @@
F4C18A4028491B8500335EC7 /* Resources */,
F4C18A5628491B9D00335EC7 /* Embed Frameworks */,
F41369BE2991861C002CE8D3 /* Embed Login Item */,
F43EC19B2FDD8920001D143B /* ShellScript */,
);
buildRules = (
);
Expand Down Expand Up @@ -2426,6 +2432,7 @@
dependencies = (
);
fileSystemSynchronizedGroups = (
F43EC1962FDD8233001D143B /* Scripts */,
F4EFB5642FDB2A0D00AF2A63 /* VirtualInstallation */,
);
name = VirtualInstallation;
Expand Down Expand Up @@ -2634,6 +2641,24 @@
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
F43EC19B2FDD8920001D143B /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "./Scripts/check_guest_app_deployment_target.sh\n";
};
F453C4282DF0B65B007EAD5F /* Create Command-Line Tool Symlinks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
Expand Down Expand Up @@ -2871,6 +2896,7 @@
F453C4422DF0B7A5007EAD5F /* MobileDeviceCommand.swift in Sources */,
F453C4432DF0B7A5007EAD5F /* BuildManifest+Fetch.swift in Sources */,
F453C4682DF10181007EAD5F /* BlurHashCommand.swift in Sources */,
F43ED2BE2FDD9631001D143B /* LegacyGuestAppCommand.swift in Sources */,
F453C44E2DF0B870007EAD5F /* VirtualBuddyCLI.swift in Sources */,
F453C4442DF0B7A5007EAD5F /* TreeStringConvertible.swift in Sources */,
F453C4452DF0B7A5007EAD5F /* MigrateCommand.swift in Sources */,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
</CommandLineArgument>
<CommandLineArgument
argument = "-VBForceBuiltInSoftwareCatalog YES"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-VBDisableMemoryLeakAssertions YES"
Expand All @@ -69,7 +69,7 @@
</CommandLineArgument>
<CommandLineArgument
argument = "-WHVerbosePacketLogging YES"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-VerboseUILoggingEnabled YES"
Expand All @@ -79,13 +79,22 @@
argument = "-VBForceVirtualInstallationBackend YES"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-VBAllowAllGuestAppVersions YES"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "VI_TEST_MODE"
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "VB_TOOL"
value = "vctool"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
Expand Down
12 changes: 7 additions & 5 deletions VirtualBuddy/Bootstrap/VirtualBuddyAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,22 @@ import SwiftUI
private var cancellables = Set<AnyCancellable>()

func applicationDidFinishLaunching(_ notification: Notification) {
GuestAdditionsDiskImage.current.$state.sink { state in
GuestAdditionsDiskImage.default.$state.sink { state in
switch state {
case .ready:
self.logger.debug("Guest disk image ready")
self.logger.debug("Default guest disk image ready")
case .downloading:
self.logger.debug("Default guest disk image downloading")
case .installing:
self.logger.debug("Guest disk image installing")
self.logger.debug("Default guest disk image installing")
case .installFailed(let error):
self.logger.debug("Guest disk image installation failed - \(error, privacy: .public)")
self.logger.debug("Default guest disk image installation failed - \(error, privacy: .public)")
}
}
.store(in: &cancellables)

Task {
try? await GuestAdditionsDiskImage.current.installIfNeeded()
try? await GuestAdditionsDiskImage.default.installIfNeeded()
}

#if DEBUG
Expand Down
3 changes: 2 additions & 1 deletion VirtualBuddy/CommandLine/vctool/CatalogCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ struct CatalogCommand: AsyncParsableCommand {
subcommands: [
ImageCommand.self,
GroupCommand.self,
MigrateCommand.self
MigrateCommand.self,
LegacyGuestAppCommand.self
]
)
}
133 changes: 133 additions & 0 deletions VirtualBuddy/CommandLine/vctool/LegacyGuestAppCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import Foundation
import ArgumentParser
import AppKit
import BuddyFoundation
import VirtualUI
import CryptoKit

extension CatalogCommand {
struct LegacyGuestAppCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "legacyguestapp",
abstract: "Modify legacy guest app disk images.",
subcommands: [
AddCommand.self,
]
)

struct AddCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "add",
abstract: "Adds a legacy guest app disk image to the catalog.",
discussion: """
This command is used to add legacy builds of the VirtualBuddyGuest app to the catalog so that users running legacy guest OSes
can still have a version of the guest app that supports them.

If run with a disk image matching the file name of a disk image already in the catalog, the corresponding entry in the catalog is updated.
"""
)

@Option(name: [.short, .long], help: "Path to an existing catalog JSON file that will be updated with the new group.")
var output: String

@Option(help: "Remote base URL where legacy guest app disk images will be served from.")
var baseURL: String = "https://raw.githubusercontent.com/insidegui/VirtualBuddy/refs/heads/main/data/LegacyGuestApp"

@Argument(help: "Path to VirtualBuddyGuest app DMG created with dmgdist.", completion: .file(extensions: ["dmg"]))
var input: String

func run() async throws {
let catalogURL = try output.resolvedURL.ensureExistingFile()
var catalog = try SoftwareCatalog(contentsOf: catalogURL)

let imageURL = URL(filePath: input)
let imageData = try Data(contentsOf: imageURL, options: .mappedIfSafe)
let baseURL = try URL(string: self.baseURL).require("Invalid base URL \(self.baseURL.quoted)")
let remoteURL = baseURL.appending(path: imageURL.lastPathComponent)

let mountURL = FileManager.default.temporaryDirectory
.appending(path: "VirtualBuddyGuest-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: mountURL, withIntermediateDirectories: true)
var isMounted = false
defer {
if isMounted {
let hdiutilDetach = Process()
hdiutilDetach.executableURL = URL(filePath: "/usr/bin/hdiutil")
hdiutilDetach.arguments = [
"detach",
mountURL.path(percentEncoded: false),
"-quiet",
]

if (try? hdiutilDetach.run()) != nil {
hdiutilDetach.waitUntilExit()
}
}

try? FileManager.default.removeItem(at: mountURL)
}

let hdiutilAttach = Process()
hdiutilAttach.executableURL = URL(filePath: "/usr/bin/hdiutil")
hdiutilAttach.arguments = [
"attach",
input,
"-readonly",
"-nobrowse",
"-mountpoint",
mountURL.path(percentEncoded: false),
]
try hdiutilAttach.run()
hdiutilAttach.waitUntilExit()

try (hdiutilAttach.terminationStatus == 0).require("Error mounting disk image.")
isMounted = true

let bundleURL = mountURL.appending(path: "VirtualBuddyGuest.app")

try bundleURL.requireExistingDirectory()

let bundle = try Bundle(url: bundleURL).require("Error constructing bundle at \(bundleURL.path(percentEncoded: false).quoted).")

let infoDict = try bundle.infoDictionary.require("Bundle has no info dictionary!")

let appVersionString = try cast(infoDict["CFBundleShortVersionString"], as: String.self, "CFBundleShortVersionString not a string.")
let minOSVersionString = try cast(infoDict["LSMinimumSystemVersion"], as: String.self, "LSMinimumSystemVersion not a string.")

let appVersion = try SoftwareVersion(string: appVersionString).require("Invalid app version string \(appVersionString.quoted).")
let minOSVersion = try SoftwareVersion(string: minOSVersionString).require("Invalid min OS version string \(minOSVersionString.quoted).")
let maxOSVersion = SoftwareVersion(major: minOSVersion.major, minor: 99, patch: 99)

let sha384 = SHA384.hash(data: imageData)
.map { String(format: "%02x", $0) }
.joined()

let entry = CatalogLegacyGuestAppVersion(
id: imageURL.deletingPathExtension().lastPathComponent,
url: remoteURL,
sha384: sha384,
guestAppVersion: appVersion,
minGuestVersion: minOSVersion,
maxGuestVersion: maxOSVersion
)

let isUpdate: Bool
if let index = catalog.legacyGuestAppVersions.firstIndex(where: { $0.id == entry.id }) {
catalog.legacyGuestAppVersions[index] = entry
isUpdate = true
} else {
catalog.legacyGuestAppVersions.append(entry)
isUpdate = false
}

try catalog.write(to: catalogURL)

if isUpdate {
print("✅ Updated \(entry.id)")
} else {
print("✅ Added \(entry.id)")
}
}
}
}
}
2 changes: 1 addition & 1 deletion VirtualBuddy/CommandLine/vctool/MigrateCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ extension CatalogCommand {
} else {
fputs("Creating empty version 2 catalog for migration\n", stderr)

catalog = SoftwareCatalog(apiVersion: 2, minAppVersion: .init(string: "2.0.0")!, channels: [], groups: [], restoreImages: [], features: [], requirementSets: [], deviceSupportVersions: [])
catalog = SoftwareCatalog(apiVersion: 2, minAppVersion: .init(string: "2.0.0")!, channels: [], groups: [], restoreImages: [], features: [], requirementSets: [], deviceSupportVersions: [], legacyGuestAppVersions: [])
}

for legacyChannel in legacyCatalog.channels {
Expand Down
3 changes: 3 additions & 0 deletions VirtualCore/Source/Definitions/PreviewSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,8 @@ public extension ResolvedCatalogGroup {
public extension ResolvedRestoreImage {
static let previewMac = ResolvedCatalog.previewMac.groups[0].restoreImages[0]
static let previewLinux = ResolvedCatalog.previewLinux.groups[0].restoreImages[0]

static let previewMacLegacyVentura = ResolvedCatalog.previewMac.groups.first(where: { $0.id == "ventura" })!.restoreImages[0]
static let previewMacLegacyMonterey = ResolvedCatalog.previewMac.groups.first(where: { $0.id == "monterey" })!.restoreImages[0]
}
#endif
Loading