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
20 changes: 20 additions & 0 deletions Whisky/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -10069,6 +10069,26 @@
}
}
},
"migration.error.message.%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Whisky will try again the next time it opens. %@"
}
}
}
},
"migration.error.title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Couldn't Import Data from Upstream Whisky"
}
}
}
},
"open.bottle" : {
"localizations" : {
"ar" : {
Expand Down
2 changes: 1 addition & 1 deletion Whisky/Views/Bottle/BottleCreationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct BottleCreationView: View {

@State private var newBottleName: String = ""
@State private var newBottleVersion: WinVersion = .win10
@State private var newBottleURL: URL = UserDefaults.standard.url(forKey: "defaultBottleLocation")
@State private var newBottleURL: URL = UserDefaults.standard.url(forKey: BottleData.defaultBottleLocationKey)
?? BottleData.defaultBottleDir
@State private var nameValid: Bool = false

Expand Down
2 changes: 1 addition & 1 deletion Whisky/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct SettingsView: View {
@AppStorage("SUEnableAutomaticChecks") var whiskyUpdate = true
@AppStorage("killOnTerminate") var killOnTerminate = true
@AppStorage("checkWhiskyWineUpdates") var checkWhiskyWineUpdates = true
@AppStorage("defaultBottleLocation") var defaultBottleLocation = BottleData.defaultBottleDir
@AppStorage(BottleData.defaultBottleLocationKey) var defaultBottleLocation = BottleData.defaultBottleDir

var body: some View {
Form {
Expand Down
14 changes: 14 additions & 0 deletions Whisky/Views/WhiskyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import WhiskyKit
@main
struct WhiskyApp: App {
@State var showSetup: Bool = false
@State private var migrationError: Error?
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@Environment(\.openURL) var openURL
private let updaterController: SPUStandardUpdaterController
Expand All @@ -31,13 +32,26 @@ struct WhiskyApp: App {
updaterController = SPUStandardUpdaterController(startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil)
do {
try UpstreamDataMigration.migrateIfNeeded()
} catch {
_migrationError = State(initialValue: error)
}
}

private var isShowingMigrationError: Binding<Bool> {
Binding(get: { migrationError != nil }, set: { if !$0 { migrationError = nil } })
}

var body: some Scene {
WindowGroup {
ContentView(showSetup: $showSetup)
.frame(minWidth: ViewWidth.large, minHeight: 316)
.environmentObject(BottleVM.shared)
.alert("migration.error.title", isPresented: isShowingMigrationError) {
} message: {
Text("migration.error.message.\(migrationError?.localizedDescription ?? "")")
}
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false

Expand Down
9 changes: 9 additions & 0 deletions WhiskyCmd/Main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ struct Whisky: ParsableCommand {
Shellenv.self
/*Install.self,
Uninstall.self*/])

static func main() {
do {
try UpstreamDataMigration.migrateIfNeeded()
} catch {
FileHandle.standardError.write(Data("Failed to migrate data from upstream Whisky: \(error)\n".utf8))
}
main(nil)
}
}

extension Whisky {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ public extension Bundle {
static var whiskyBundleIdentifier: String {
return Bundle.main.bundleIdentifier ?? "com.istorry.Whisky"
}

/// The identifier upstream Whisky stored its data under, before this fork
internal static let upstreamWhiskyBundleIdentifier = "com.isaacmarovitz.Whisky"
}
78 changes: 60 additions & 18 deletions WhiskyKit/Sources/WhiskyKit/Whisky/BottleData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,37 @@
import Foundation
import SemanticVersion

public struct BottleData: Codable {
public static let containerDir = FileManager.default.homeDirectoryForCurrentUser
.appending(path: "Library")
.appending(path: "Containers")
.appending(path: Bundle.whiskyBundleIdentifier)
enum BottleDataError: Error {
case unsupportedFileVersion(SemanticVersion)
}

public static let bottleEntriesDir = containerDir
.appending(path: "BottleVM")
.appendingPathExtension("plist")
public struct BottleData: Codable {
public static let containerDir = containerDir(for: Bundle.whiskyBundleIdentifier)
public static let bottleEntriesDir = bottleEntriesDir(in: containerDir)
public static let defaultBottleDir = defaultBottleDir(in: containerDir)

public static let defaultBottleDir = containerDir
.appending(path: "Bottles")
/// `UserDefaults` key for the folder new bottles are created in
public static let defaultBottleLocationKey = "defaultBottleLocation"

static let currentVersion = SemanticVersion(1, 0, 0)

static func containerDir(for bundleIdentifier: String) -> URL {
FileManager.default.homeDirectoryForCurrentUser
.appending(path: "Library")
.appending(path: "Containers")
.appending(path: bundleIdentifier)
}

static func bottleEntriesDir(in containerDir: URL) -> URL {
containerDir
.appending(path: "BottleVM")
.appendingPathExtension("plist")
}

static func defaultBottleDir(in containerDir: URL) -> URL {
containerDir.appending(path: "Bottles")
}

private var fileVersion: SemanticVersion
public var paths: [URL] = [] {
didSet {
Expand All @@ -49,6 +65,27 @@ public struct BottleData: Codable {
}
}

private init(paths: [URL]) {
fileVersion = Self.currentVersion
self.paths = paths
}

/// Appends the bottles listed in another bottle list that are not already in ours
static func importPaths(fromEntriesAt otherEntriesURL: URL) throws {
let other = try read(from: otherEntriesURL)
guard other.fileVersion == currentVersion else {
throw BottleDataError.unsupportedFileVersion(other.fileVersion)
}

let existingPaths = FileManager.default.fileExists(atPath: bottleEntriesDir.path(percentEncoded: false))
? try read(from: bottleEntriesDir).paths
: []
let importedPaths = other.paths.filter { !existingPaths.contains($0) }
guard !importedPaths.isEmpty else { return }

try BottleData(paths: existingPaths + importedPaths).write()
}

public mutating func loadBottles() -> [Bottle] {
var bottles: [Bottle] = []

Expand All @@ -68,12 +105,14 @@ public struct BottleData: Codable {
return bottles
}

private static func read(from entriesURL: URL) throws -> BottleData {
try PropertyListDecoder().decode(BottleData.self, from: Data(contentsOf: entriesURL))
}

@discardableResult
private mutating func decode() -> Bool {
let decoder = PropertyListDecoder()
do {
let data = try Data(contentsOf: Self.bottleEntriesDir)
self = try decoder.decode(BottleData.self, from: data)
self = try Self.read(from: Self.bottleEntriesDir)
if self.fileVersion != Self.currentVersion {
print("Invalid file version \(self.fileVersion)")
return false
Expand All @@ -84,15 +123,18 @@ public struct BottleData: Codable {
}
}

@discardableResult
private func encode() -> Bool {
private func write() throws {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml

try FileManager.default.createDirectory(at: Self.containerDir, withIntermediateDirectories: true)
try encoder.encode(self).write(to: Self.bottleEntriesDir)
}

@discardableResult
private func encode() -> Bool {
do {
try FileManager.default.createDirectory(at: Self.containerDir, withIntermediateDirectories: true)
let data = try encoder.encode(self)
try data.write(to: Self.bottleEntriesDir)
try write()
return true
} catch {
return false
Expand Down
89 changes: 89 additions & 0 deletions WhiskyKit/Sources/WhiskyKit/Whisky/UpstreamDataMigration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// UpstreamDataMigration.swift
// WhiskyKit
//
// This file is part of Whisky.
//
// Whisky is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
//
// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Whisky.
// If not, see https://www.gnu.org/licenses/.
//

import Foundation

/// Carries over the bottle list, WhiskyWine and settings from upstream Whisky.
///
/// Bottles are added to our bottle list where they are rather than moved, because programs
/// inside a Wine prefix record absolute paths that moving the bottle would break.
public enum UpstreamDataMigration {
private static let completedKey = "upstreamDataMigrationCompleted"
private static let upstreamContainerDir = BottleData.containerDir(for: Bundle.upstreamWhiskyBundleIdentifier)

/// Every step is safe to repeat, so a failed migration is retried on the next launch
public static func migrateIfNeeded() throws {
guard storedSettings()[completedKey] as? Bool != true else { return }

migrateSettings()
try migrateWhiskyWine()
try migrateBottleList()
updateSettings { $0[completedKey] = true }
}

private static func migrateSettings() {
let upstreamIdentifier = Bundle.upstreamWhiskyBundleIdentifier
guard let upstreamDefaults = UserDefaults(suiteName: upstreamIdentifier),
let upstreamSettings = upstreamDefaults.persistentDomain(forName: upstreamIdentifier) else {
return
}

// A location pointing at upstream's own bottle folder would keep new bottles in upstream's container
let upstreamDefaultBottleDir = BottleData.defaultBottleDir(in: upstreamContainerDir)
let usesUpstreamDefaultBottleDir = upstreamDefaults.url(forKey: BottleData.defaultBottleLocationKey)?
.standardizedFileURL.pathComponents == upstreamDefaultBottleDir.standardizedFileURL.pathComponents

updateSettings { settings in
for (key, value) in upstreamSettings where settings[key] == nil {
if key == BottleData.defaultBottleLocationKey && usesUpstreamDefaultBottleDir { continue }
settings[key] = value
}
}
}

private static func migrateBottleList() throws {
let upstreamEntries = BottleData.bottleEntriesDir(in: upstreamContainerDir)
guard FileManager.default.fileExists(atPath: upstreamEntries.path(percentEncoded: false)) else { return }

try BottleData.importPaths(fromEntriesAt: upstreamEntries)
}

private static func migrateWhiskyWine() throws {
let fileManager = FileManager.default
let upstreamLibraryFolder = WhiskyWineInstaller.libraryFolder(
in: WhiskyWineInstaller.applicationFolder(for: Bundle.upstreamWhiskyBundleIdentifier)
)
guard fileManager.fileExists(atPath: upstreamLibraryFolder.path(percentEncoded: false)),
!fileManager.fileExists(atPath: WhiskyWineInstaller.libraryFolder.path(percentEncoded: false)) else {
return
}

try fileManager.createDirectory(at: WhiskyWineInstaller.applicationFolder, withIntermediateDirectories: true)
try fileManager.moveItem(at: upstreamLibraryFolder, to: WhiskyWineInstaller.libraryFolder)
}

private static func storedSettings() -> [String: Any] {
UserDefaults.standard.persistentDomain(forName: Bundle.whiskyBundleIdentifier) ?? [:]
}

private static func updateSettings(_ update: (inout [String: Any]) -> Void) {
var settings = storedSettings()
update(&settings)
UserDefaults.standard.setPersistentDomain(settings, forName: Bundle.whiskyBundleIdentifier)
}
}
17 changes: 12 additions & 5 deletions WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,23 @@ import SemanticVersion

public class WhiskyWineInstaller {
/// The Whisky application folder
public static let applicationFolder = FileManager.default.urls(
for: .applicationSupportDirectory, in: .userDomainMask
)[0].appending(path: Bundle.whiskyBundleIdentifier)
public static let applicationFolder = applicationFolder(for: Bundle.whiskyBundleIdentifier)

/// The folder of all the libfrary files
public static let libraryFolder = applicationFolder.appending(path: "Libraries")
/// The folder of all the library files
public static let libraryFolder = libraryFolder(in: applicationFolder)

/// URL to the installed `wine` `bin` directory
public static let binFolder: URL = libraryFolder.appending(path: "Wine").appending(path: "bin")

static func applicationFolder(for bundleIdentifier: String) -> URL {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appending(path: bundleIdentifier)
}

static func libraryFolder(in applicationFolder: URL) -> URL {
applicationFolder.appending(path: "Libraries")
}

public static func isWhiskyWineInstalled() -> Bool {
return whiskyWineVersion() != nil
}
Expand Down
Loading
Loading