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
3 changes: 3 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ jobs:
path: .build/SourcePackages
key: spm-${{ hashFiles('Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved', 'WhiskyKit/Package.swift') }}

- name: Test WhiskyKit
run: swift test --package-path WhiskyKit

- name: Build Whisky
run: xcodebuild build -project Whisky.xcodeproj -scheme Whisky -configuration Release
-clonedSourcePackagesDirPath .build/SourcePackages -disableAutomaticPackageResolution
Expand Down
26 changes: 0 additions & 26 deletions .github/workflows/Release.yml

This file was deleted.

4 changes: 4 additions & 0 deletions Whisky.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
6E5B61B62AC34E6D007B1913 /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 6E5B61B52AC34E6D007B1913 /* SemanticVersion */; };
6E621CEF2A5F631300C9AAB3 /* Winetricks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E621CEE2A5F631200C9AAB3 /* Winetricks.swift */; };
6E6C0CF22A419A6800356232 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6C0CF12A419A6800356232 /* WelcomeView.swift */; };
6E7A1F012F00000000000001 /* SetupFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7A1F012F00000000000002 /* SetupFailureView.swift */; };
6E6C0CF42A419A7600356232 /* RosettaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6C0CF32A419A7600356232 /* RosettaView.swift */; };
6E6C0CF62A419A8300356232 /* WhiskyWineDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6C0CF52A419A8300356232 /* WhiskyWineDownloadView.swift */; };
6E6C0CF82A419A8C00356232 /* WhiskyWineInstallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6C0CF72A419A8C00356232 /* WhiskyWineInstallView.swift */; };
Expand Down Expand Up @@ -138,6 +139,7 @@
6E50D98429CDF25B008C39F6 /* BottleCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleCreationView.swift; sourceTree = "<group>"; };
6E621CEE2A5F631200C9AAB3 /* Winetricks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Winetricks.swift; sourceTree = "<group>"; };
6E6C0CF12A419A6800356232 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
6E7A1F012F00000000000002 /* SetupFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupFailureView.swift; sourceTree = "<group>"; };
6E6C0CF32A419A7600356232 /* RosettaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RosettaView.swift; sourceTree = "<group>"; };
6E6C0CF52A419A8300356232 /* WhiskyWineDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiskyWineDownloadView.swift; sourceTree = "<group>"; };
6E6C0CF72A419A8C00356232 /* WhiskyWineInstallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiskyWineInstallView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -368,6 +370,7 @@
children = (
6EF557972A410599001A4F09 /* SetupView.swift */,
6E6C0CF12A419A6800356232 /* WelcomeView.swift */,
6E7A1F012F00000000000002 /* SetupFailureView.swift */,
6E6C0CF32A419A7600356232 /* RosettaView.swift */,
6E6C0CF52A419A8300356232 /* WhiskyWineDownloadView.swift */,
6E6C0CF72A419A8C00356232 /* WhiskyWineInstallView.swift */,
Expand Down Expand Up @@ -618,6 +621,7 @@
6E49E0212AECB7DB00009CAC /* SettingsView.swift in Sources */,
6E7C07C02AAF570100F6E66B /* FileOpenView.swift in Sources */,
6E6C0CF22A419A6800356232 /* WelcomeView.swift in Sources */,
6E7A1F012F00000000000001 /* SetupFailureView.swift in Sources */,
6E40498129CCA8B0006E3F1B /* BottleView.swift in Sources */,
6E355E5E29D7D85D002D83BE /* ProgramView.swift in Sources */,
);
Expand Down
70 changes: 70 additions & 0 deletions Whisky/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -8164,6 +8164,56 @@
}
}
},
"create.error.message.%@.%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "“%1$@” could not be created. %2$@"
}
}
}
},
"create.error.prefixNotCreated.%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wine did not set up the bottle. Wine output: %@"
}
}
}
},
"create.error.title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Couldn't Create Bottle"
}
}
}
},
"create.error.unsupportedLocation.%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wine can't use folders whose names end with a period. Choose a different location than %@."
}
}
}
},
"create.error.whiskyWineNotInstalled" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "WhiskyWine is not installed. Install it from Whisky > Setup, then try again."
}
}
}
},
"create.name" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -17298,6 +17348,16 @@
}
}
},
"setup.whiskywine.download.failed" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Download Failed"
}
}
}
},
"setup.whiskywine.download.subtitle" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -17706,6 +17766,16 @@
}
}
},
"setup.whiskywine.install.failed" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Installation Failed"
}
}
}
},
"setup.whiskywine.install.subtitle" : {
"localizations" : {
"ar" : {
Expand Down
105 changes: 75 additions & 30 deletions Whisky/View Models/BottleVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,62 +17,107 @@
//

import Foundation
import os
import SemanticVersion
import WhiskyKit

enum BottleCreationError: LocalizedError {
case whiskyWineNotInstalled
case unsupportedLocation(URL)
case prefixNotCreated(wineOutput: String)

var errorDescription: String? {
switch self {
case .whiskyWineNotInstalled:
return String(localized: "create.error.whiskyWineNotInstalled")
case .unsupportedLocation(let url):
return String(localized: "create.error.unsupportedLocation.\(url.prettyPath())")
case .prefixNotCreated(let wineOutput):
return String(localized: "create.error.prefixNotCreated.\(wineOutput.suffix(1000).description)")
}
}
}

struct BottleCreationFailure: Identifiable {
let id = UUID()
let bottleName: String
let error: Error
}

// swiftlint:disable:next todo
// TODO: Don't use unchecked!
final class BottleVM: ObservableObject, @unchecked Sendable {
@MainActor static let shared = BottleVM()

var bottlesList = BottleData()
@Published var bottles: [Bottle] = []
@Published var bottleCreationFailure: BottleCreationFailure?

/// Bottles still being created are not in the bottle list yet, so they are kept across reloads
@MainActor
func loadBottles() {
bottles = bottlesList.loadBottles()
let bottlesInCreation = bottles.filter { $0.inFlight && !bottlesList.paths.contains($0.url) }
bottles = bottlesList.loadBottles() + bottlesInCreation
}

func countActive() -> Int {
return bottles.filter { $0.isAvailable == true }.count
}

@MainActor
func createNewBottle(bottleName: String, winVersion: WinVersion, bottleURL: URL) -> URL {
let newBottleDir = bottleURL.appending(path: UUID().uuidString)

Task.detached {
var bottleId: Bottle?
Task {
do {
try FileManager.default.createDirectory(atPath: newBottleDir.path(percentEncoded: false),
withIntermediateDirectories: true)
let bottle = Bottle(bottleUrl: newBottleDir, inFlight: true)
bottleId = bottle

await MainActor.run {
self.bottles.append(bottle)
}

bottle.settings.windowsVersion = winVersion
bottle.settings.name = bottleName
try await Wine.changeWinVersion(bottle: bottle, win: winVersion)
let wineVer = try await Wine.wineVersion()
bottle.settings.wineVersion = SemanticVersion(wineVer) ?? SemanticVersion(0, 0, 0)
// Add record
await MainActor.run {
self.bottlesList.paths.append(newBottleDir)
self.loadBottles()
}
try await setUpBottle(at: newBottleDir, name: bottleName, winVersion: winVersion)
} catch {
print("Failed to create new bottle: \(error)")
if let bottle = bottleId {
await MainActor.run {
if let index = self.bottles.firstIndex(of: bottle) {
self.bottles.remove(at: index)
}
}
}
Logger.wineKit.error("Failed to create bottle `\(bottleName)`: \(error)")
discardBottle(at: newBottleDir)
bottleCreationFailure = BottleCreationFailure(bottleName: bottleName, error: error)
}
}
return newBottleDir
}

@MainActor
private func setUpBottle(at bottleDir: URL, name: String, winVersion: WinVersion) async throws {
guard WhiskyWineInstaller.isWhiskyWineInstalled() else {
throw BottleCreationError.whiskyWineNotInstalled
}
// Wine drops trailing dots when mapping Unix paths to Windows ones, so the prefix cannot load
guard !bottleDir.pathComponents.contains(where: { $0.hasSuffix(".") }) else {
throw BottleCreationError.unsupportedLocation(bottleDir)
}

try FileManager.default.createDirectory(at: bottleDir, withIntermediateDirectories: true)
let bottle = Bottle(bottleUrl: bottleDir, inFlight: true)
bottles.append(bottle)

bottle.settings.windowsVersion = winVersion
bottle.settings.name = name
let wineOutput = try await Wine.changeWinVersion(bottle: bottle, win: winVersion)
guard FileManager.default.fileExists(atPath: bottleDir.appending(path: "drive_c").path(percentEncoded: false))
else {
throw BottleCreationError.prefixNotCreated(wineOutput: wineOutput)
}

let wineVer = try await Wine.wineVersion()
bottle.settings.wineVersion = SemanticVersion(wineVer) ?? SemanticVersion(0, 0, 0)
bottlesList.paths.append(bottleDir)
loadBottles()
}

@MainActor
private func discardBottle(at bottleDir: URL) {
bottles.removeAll { $0.url == bottleDir }
guard FileManager.default.fileExists(atPath: bottleDir.path(percentEncoded: false)) else { return }
do {
try FileManager.default.removeItem(at: bottleDir)
} catch {
Logger.wineKit.error(
"Failed to remove incomplete bottle `\(bottleDir.path(percentEncoded: false))`: \(error)"
)
}
}
}
64 changes: 44 additions & 20 deletions Whisky/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import SwiftUI
import UniformTypeIdentifiers
import WhiskyKit
import SemanticVersion
import os

struct ContentView: View {
@AppStorage("selectedBottleURL") private var selectedBottleURL: URL?
Expand All @@ -28,6 +29,7 @@ struct ContentView: View {

@State private var selected: URL?
@State private var showBottleCreation: Bool = false
@State private var showWhiskyWineUpdate: Bool = false
@State private var bottlesLoaded: Bool = false
@State private var showBottleSelection: Bool = false
@State private var newlyCreatedBottleURL: URL?
Expand Down Expand Up @@ -77,6 +79,17 @@ struct ContentView: View {
.sheet(isPresented: $showSetup) {
SetupView(showSetup: $showSetup, firstTime: false)
}
.sheet(isPresented: $showWhiskyWineUpdate) {
SetupView(showSetup: $showWhiskyWineUpdate, firstTime: false, initialPath: [.whiskyWineDownload])
}
.alert(
"create.error.title",
isPresented: isShowingBottleCreationFailure,
presenting: bottleVM.bottleCreationFailure
) { _ in
} message: { failure in
Text("create.error.message.\(failure.bottleName).\(failure.error.localizedDescription)")
}
.sheet(item: $openedFileURL) { url in
FileOpenView(fileURL: url,
currentBottle: selected,
Expand Down Expand Up @@ -104,28 +117,39 @@ struct ContentView: View {
if !WhiskyWineInstaller.isWhiskyWineInstalled() {
showSetup = true
}
let task = Task.detached {
return await WhiskyWineInstaller.shouldUpdateWhiskyWine()
}
let updateInfo = await task.value
if updateInfo.0 {
let alert = NSAlert()
alert.messageText = String(localized: "update.whiskywine.title")
alert.informativeText = String(format: String(localized: "update.whiskywine.description"),
String(WhiskyWineInstaller.whiskyWineVersion()
?? SemanticVersion(0, 0, 0)),
String(updateInfo.1))
alert.alertStyle = .warning
alert.addButton(withTitle: String(localized: "update.whiskywine.update"))
alert.addButton(withTitle: String(localized: "button.removeAlert.cancel"))
await offerWhiskyWineUpdate()
}
}

let response = alert.runModal()
private var isShowingBottleCreationFailure: Binding<Bool> {
Binding(
get: { bottleVM.bottleCreationFailure != nil },
set: { if !$0 { bottleVM.bottleCreationFailure = nil } }
)
}

if response == .alertFirstButtonReturn {
WhiskyWineInstaller.uninstall()
showSetup = true
}
}
private func offerWhiskyWineUpdate() async {
let remoteVersion: SemanticVersion?
do {
remoteVersion = try await WhiskyWineInstaller.availableUpdate()
} catch {
Logger.wineKit.error("Failed to check for a WhiskyWine update: \(error)")
return
}
guard let remoteVersion else { return }

let alert = NSAlert()
alert.messageText = String(localized: "update.whiskywine.title")
alert.informativeText = String(format: String(localized: "update.whiskywine.description"),
String(WhiskyWineInstaller.whiskyWineVersion()
?? SemanticVersion(0, 0, 0)),
String(remoteVersion))
alert.alertStyle = .warning
alert.addButton(withTitle: String(localized: "update.whiskywine.update"))
alert.addButton(withTitle: String(localized: "button.removeAlert.cancel"))

if alert.runModal() == .alertFirstButtonReturn {
showWhiskyWineUpdate = true
}
}

Expand Down
Loading
Loading