diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 3ffc5a6b3..9e9c594ba 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -37,21 +37,23 @@ jobs:
- name: Lint
run: swiftlint --strict --reporter github-actions-logging
- - name: Cache Swift packages
+ # Only the Git mirrors: a cached checkout missing a binary artifact (Sparkle.xcframework) is never
+ # downloaded again and fails the build, while checkouts and artifacts are quick to recreate
+ - name: Cache Swift package repositories
uses: actions/cache@v6
with:
- path: .build/SourcePackages
- key: spm-${{ hashFiles('Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved', 'WhiskyKit/Package.swift') }}
+ path: .build/SourcePackages/repositories
+ key: spm-repositories-${{ 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
+ -clonedSourcePackagesDirPath .build/SourcePackages -onlyUsePackageVersionsFromResolvedFile
CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY=""
- name: Build WhiskyCmd
run: xcodebuild build -project Whisky.xcodeproj -scheme WhiskyCmd -configuration Release
- -clonedSourcePackagesDirPath .build/SourcePackages -disableAutomaticPackageResolution
+ -clonedSourcePackagesDirPath .build/SourcePackages -onlyUsePackageVersionsFromResolvedFile
CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY=""
diff --git a/README.md b/README.md
index ce6dfed00..a5d35216b 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
*Windows games on your Mac, set up in a few clicks*
- 
+ 
---
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
index d97747d70..dbcc27a86 100644
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
index 543bb52a4..514959dab 100644
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
index 90d063fba..6dda6a2b6 100644
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
index 25f96f32e..74fa038f5 100644
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
index 543bb52a4..514959dab 100644
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
index 5f13526ea..a6d366772 100644
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
index 25f96f32e..74fa038f5 100644
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
index 1c28a9e48..d97cbc7c1 100644
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
index 5f13526ea..a6d366772 100644
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
index f4f67562d..3fc9e80bd 100644
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ
diff --git a/Whisky/Info.plist b/Whisky/Info.plist
index cd0b8f8f7..6c0f14084 100644
--- a/Whisky/Info.plist
+++ b/Whisky/Info.plist
@@ -54,7 +54,7 @@
SUFeedURL
- https://github.com/iStorry/Whisky/releases/latest/download/appcast.xml
+ https://github.com/iStorry/Brandy/releases/latest/download/appcast.xml
UTExportedTypeDeclarations
diff --git a/Whisky/Views/WhiskyApp.swift b/Whisky/Views/WhiskyApp.swift
index d54319088..8ff59ffdd 100644
--- a/Whisky/Views/WhiskyApp.swift
+++ b/Whisky/Views/WhiskyApp.swift
@@ -124,7 +124,7 @@ struct WhiskyApp: App {
}
CommandGroup(replacing: .help) {
Button("help.github") {
- if let url = URL(string: "https://github.com/iStorry/Whisky") {
+ if let url = URL(string: "https://github.com/iStorry/Brandy") {
openURL(url)
}
}
diff --git a/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift b/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift
index 7c4446135..5be539bbb 100644
--- a/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift
+++ b/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift
@@ -48,7 +48,7 @@ public class WhiskyWineInstaller {
/// Assets of the release for the supported major version, which `scripts/publish-whiskywine.sh` publishes
private static let distributionURL: URL = {
- let release = "https://github.com/iStorry/Whisky/releases/download/whiskywine-\(supportedMajorVersion)/"
+ let release = "https://github.com/iStorry/Brandy/releases/download/whiskywine-\(supportedMajorVersion)/"
guard let url = URL(string: release) else {
preconditionFailure("Invalid WhiskyWine distribution URL")
}
diff --git a/WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png b/WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png
index 3793b5fd9..a6d366772 100644
Binary files a/WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png and b/WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png differ
diff --git a/images/brandy-icon.png b/images/brandy-icon.png
index f4f67562d..3fc9e80bd 100644
Binary files a/images/brandy-icon.png and b/images/brandy-icon.png differ
diff --git a/scripts/generate-app-icon.swift b/scripts/generate-app-icon.swift
new file mode 100644
index 000000000..7e9394589
--- /dev/null
+++ b/scripts/generate-app-icon.swift
@@ -0,0 +1,278 @@
+#!/usr/bin/env swift
+//
+// generate-app-icon.swift
+// Brandy
+//
+// 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/.
+//
+
+// Renders the Brandy app icon at every size the asset catalogs need.
+// Usage: swift scripts/generate-app-icon.swift (run from the repository root)
+
+import AppKit
+import CoreGraphics
+
+/// All geometry is authored on Apple's 1024 pt macOS icon grid, y pointing down.
+let canvas: CGFloat = 1024
+let tileRect = CGRect(x: 100, y: 100, width: 824, height: 824)
+let tileSquircleExponent: CGFloat = 5
+
+let sunCenter = CGPoint(x: 640, y: 300)
+let sunRadius: CGFloat = 70
+
+/// One band of liquid: a sine wave across the tile, filled with a vertical gradient below it.
+struct Stratum {
+ let baseline: CGFloat
+ let amplitude: CGFloat
+ let wavelength: CGFloat
+ let phase: CGFloat
+ let topColor: UInt32
+ let bottomColor: UInt32
+ let crestColor: UInt32
+}
+
+/// Ordered from the top band down, distilling from deep wine at the bottom to gold at the top.
+let strata: [Stratum] = [
+ Stratum(baseline: 470, amplitude: 34, wavelength: 760, phase: 0.6,
+ topColor: 0xFFE3A2, bottomColor: 0xF5A947, crestColor: 0xFFF6DA),
+ Stratum(baseline: 568, amplitude: 38, wavelength: 700, phase: 2.2,
+ topColor: 0xF6A443, bottomColor: 0xD0621D, crestColor: 0xFFD08A),
+ Stratum(baseline: 664, amplitude: 40, wavelength: 820, phase: 3.9,
+ topColor: 0xD8502A, bottomColor: 0x9A2420, crestColor: 0xF38A5A),
+ Stratum(baseline: 758, amplitude: 36, wavelength: 740, phase: 5.3,
+ topColor: 0x921A2A, bottomColor: 0x560B18, crestColor: 0xC8455A),
+ Stratum(baseline: 848, amplitude: 30, wavelength: 680, phase: 1.4,
+ topColor: 0x5A0C1A, bottomColor: 0x2E040C, crestColor: 0x8E2238)
+]
+
+let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) ?? CGColorSpaceCreateDeviceRGB()
+
+func color(_ hex: UInt32, _ alpha: CGFloat = 1) -> CGColor {
+ CGColor(
+ colorSpace: colorSpace,
+ components: [
+ CGFloat((hex >> 16) & 0xFF) / 255,
+ CGFloat((hex >> 8) & 0xFF) / 255,
+ CGFloat(hex & 0xFF) / 255,
+ alpha
+ ]
+ ) ?? CGColor(gray: 0, alpha: alpha)
+}
+
+func gradient(_ stops: [(CGColor, CGFloat)]) -> CGGradient? {
+ CGGradient(colorsSpace: colorSpace, colors: stops.map(\.0) as CFArray, locations: stops.map(\.1))
+}
+
+func fill(_ ctx: CGContext, _ path: CGPath, with gradient: CGGradient?, from start: CGPoint, to end: CGPoint) {
+ guard let gradient else { return }
+ ctx.saveGState()
+ ctx.addPath(path)
+ ctx.clip()
+ ctx.drawLinearGradient(gradient, start: start, end: end,
+ options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
+ ctx.restoreGState()
+}
+
+func fillRadial(_ ctx: CGContext, _ path: CGPath, with gradient: CGGradient?, center: CGPoint, radius: CGFloat) {
+ guard let gradient else { return }
+ ctx.saveGState()
+ ctx.addPath(path)
+ ctx.clip()
+ ctx.drawRadialGradient(gradient, startCenter: center, startRadius: 0,
+ endCenter: center, endRadius: radius, options: [])
+ ctx.restoreGState()
+}
+
+/// Superellipse tile, the continuous-corner shape macOS icons use.
+func squirclePath(in rect: CGRect, exponent: CGFloat) -> CGPath {
+ let path = CGMutablePath()
+ let steps = 720
+ for step in 0...steps {
+ let angle = CGFloat(step) / CGFloat(steps) * 2 * .pi
+ let cosine = cos(angle)
+ let sine = sin(angle)
+ let unitX = pow(abs(cosine), 2 / exponent) * (cosine < 0 ? -1 : 1)
+ let unitY = pow(abs(sine), 2 / exponent) * (sine < 0 ? -1 : 1)
+ let point = CGPoint(x: rect.midX + unitX * rect.width / 2, y: rect.midY + unitY * rect.height / 2)
+ if step == 0 {
+ path.move(to: point)
+ } else {
+ path.addLine(to: point)
+ }
+ }
+ path.closeSubpath()
+ return path
+}
+
+extension Stratum {
+ func surfaceY(atX column: CGFloat) -> CGFloat {
+ let angle = 2 * .pi * column / wavelength + phase
+ return baseline + amplitude * sin(angle) + amplitude * 0.3 * sin(angle * 2.1 + 1)
+ }
+
+ func crest() -> CGPath {
+ let path = CGMutablePath()
+ let range = stride(from: tileRect.minX - 20, through: tileRect.maxX + 20, by: 4)
+ for column in range {
+ let point = CGPoint(x: column, y: surfaceY(atX: column))
+ if path.isEmpty {
+ path.move(to: point)
+ } else {
+ path.addLine(to: point)
+ }
+ }
+ return path
+ }
+
+ func body() -> CGPath {
+ let path = CGMutablePath()
+ path.addPath(crest())
+ path.addLine(to: CGPoint(x: tileRect.maxX + 20, y: canvas))
+ path.addLine(to: CGPoint(x: tileRect.minX - 20, y: canvas))
+ path.closeSubpath()
+ return path
+ }
+}
+
+func drawTile(_ ctx: CGContext, _ tile: CGPath) {
+ ctx.saveGState()
+ ctx.setShadow(offset: CGSize(width: 0, height: 10), blur: 22, color: color(0x000000, 0.3))
+ ctx.addPath(tile)
+ ctx.setFillColor(color(0x1C080C))
+ ctx.fillPath()
+ ctx.restoreGState()
+
+ fill(
+ ctx, tile,
+ with: gradient([(color(0x17070B), 0), (color(0x2E0F14), 0.5), (color(0x5A2218), 1)]),
+ from: CGPoint(x: 512, y: tileRect.minY),
+ to: CGPoint(x: 512, y: strata[0].baseline)
+ )
+}
+
+func drawSun(_ ctx: CGContext, _ tile: CGPath) {
+ fillRadial(
+ ctx, tile,
+ with: gradient([
+ (color(0xFFC36A, 0.55), 0), (color(0xF58A3A, 0.18), 0.45), (color(0xF58A3A, 0), 1)
+ ]),
+ center: sunCenter,
+ radius: 330
+ )
+ let sun = CGPath(
+ ellipseIn: CGRect(x: sunCenter.x - sunRadius, y: sunCenter.y - sunRadius,
+ width: sunRadius * 2, height: sunRadius * 2),
+ transform: nil
+ )
+ fill(
+ ctx, sun,
+ with: gradient([(color(0xFFF7E2), 0), (color(0xFFD892), 0.6), (color(0xFFB862), 1)]),
+ from: CGPoint(x: sunCenter.x, y: sunCenter.y - sunRadius),
+ to: CGPoint(x: sunCenter.x, y: sunCenter.y + sunRadius)
+ )
+}
+
+func drawStratum(_ ctx: CGContext, _ stratum: Stratum) {
+ let body = stratum.body()
+
+ ctx.saveGState()
+ ctx.setShadow(offset: CGSize(width: 0, height: -8), blur: 26, color: color(0x0A0204, 0.55))
+ ctx.addPath(body)
+ ctx.setFillColor(color(stratum.bottomColor))
+ ctx.fillPath()
+ ctx.restoreGState()
+
+ fill(
+ ctx, body,
+ with: gradient([(color(stratum.topColor), 0), (color(stratum.bottomColor), 1)]),
+ from: CGPoint(x: 512, y: stratum.baseline - stratum.amplitude),
+ to: CGPoint(x: 512, y: stratum.baseline + 150)
+ )
+
+ ctx.saveGState()
+ ctx.addPath(body)
+ ctx.clip()
+ ctx.addPath(stratum.crest())
+ ctx.setLineWidth(7)
+ ctx.setStrokeColor(color(stratum.crestColor, 0.75))
+ ctx.strokePath()
+ ctx.restoreGState()
+}
+
+func drawTileEdge(_ ctx: CGContext) {
+ let edge = squirclePath(in: tileRect.insetBy(dx: 1.5, dy: 1.5), exponent: tileSquircleExponent)
+ .copy(strokingWithWidth: 3, lineCap: .round, lineJoin: .round, miterLimit: 10)
+ fill(
+ ctx, edge,
+ with: gradient([
+ (color(0xFFE2C0, 0.22), 0), (color(0xFFE2C0, 0.03), 0.5), (color(0xFFE2C0, 0.08), 1)
+ ]),
+ from: CGPoint(x: 512, y: tileRect.minY),
+ to: CGPoint(x: 512, y: tileRect.maxY)
+ )
+}
+
+func renderIcon(pixels: Int) -> Data? {
+ guard let ctx = CGContext(
+ data: nil, width: pixels, height: pixels, bitsPerComponent: 8, bytesPerRow: 0,
+ space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
+ ) else { return nil }
+
+ let scale = CGFloat(pixels) / canvas
+ ctx.translateBy(x: 0, y: CGFloat(pixels))
+ ctx.scaleBy(x: scale, y: -scale)
+
+ let tile = squirclePath(in: tileRect, exponent: tileSquircleExponent)
+ drawTile(ctx, tile)
+
+ ctx.saveGState()
+ ctx.addPath(tile)
+ ctx.clip()
+ drawSun(ctx, tile)
+ strata.forEach { drawStratum(ctx, $0) }
+ ctx.restoreGState()
+
+ drawTileEdge(ctx)
+
+ guard let image = ctx.makeImage() else { return nil }
+ return NSBitmapImageRep(cgImage: image).representation(using: .png, properties: [:])
+}
+
+let outputs: [(path: String, pixels: Int)] = [
+ ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png", 16),
+ ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png", 32),
+ ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png", 32),
+ ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png", 64),
+ ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png", 128),
+ ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png", 256),
+ ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png", 256),
+ ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png", 512),
+ ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png", 512),
+ ("Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png", 1024),
+ ("WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png", 512),
+ ("images/brandy-icon.png", 1024)
+]
+
+for output in outputs {
+ guard let png = renderIcon(pixels: output.pixels) else {
+ FileHandle.standardError.write(Data("Failed to render \(output.path)\n".utf8))
+ exit(1)
+ }
+ do {
+ try png.write(to: URL(fileURLWithPath: output.path))
+ } catch {
+ FileHandle.standardError.write(Data("Failed to write \(output.path): \(error)\n".utf8))
+ exit(1)
+ }
+}
diff --git a/scripts/publish-whiskywine.sh b/scripts/publish-whiskywine.sh
index 7d48ad7aa..2523f9179 100755
--- a/scripts/publish-whiskywine.sh
+++ b/scripts/publish-whiskywine.sh
@@ -13,7 +13,7 @@
set -euo pipefail
# Must match WhiskyWineInstaller.distributionURL; this checkout also has the upstream remote
-readonly REPOSITORY="iStorry/Whisky"
+readonly REPOSITORY="iStorry/Brandy"
readonly ARCHIVE_NAME="Libraries.tar.gz"
readonly VERSION_FILE="WhiskyWineVersion.plist"