feat(macos): add Sparkle updates and release docs
This commit is contained in:
10
appcast.xml
Normal file
10
appcast.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rss version="2.0"
|
||||||
|
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<channel>
|
||||||
|
<title>Clawdis Updates</title>
|
||||||
|
<link>https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml</link>
|
||||||
|
<description>Signed update feed for the Clawdis macOS companion app.</description>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "d88e9364f346bbb20f6e4f0bba6328ce6780b32d4645e22c3a9acc8802298c52",
|
"originHash" : "9d6819a603c065346890e6bfc47d0239e92e1b6510e22766b85e6bdf4f891831",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "asyncxpcconnection",
|
"identity" : "asyncxpcconnection",
|
||||||
@@ -19,6 +19,15 @@
|
|||||||
"version" : "1.2.2"
|
"version" : "1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sparkle",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5581748cef2bae787496fe6d61139aebe0a451f6",
|
||||||
|
"version" : "2.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-subprocess",
|
"identity" : "swift-subprocess",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/ChimeHQ/AsyncXPCConnection", from: "1.3.0"),
|
.package(url: "https://github.com/ChimeHQ/AsyncXPCConnection", from: "1.3.0"),
|
||||||
.package(url: "https://github.com/orchetect/MenuBarExtraAccess", exact: "1.2.2"),
|
.package(url: "https://github.com/orchetect/MenuBarExtraAccess", exact: "1.2.2"),
|
||||||
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.1.0"),
|
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.1.0"),
|
||||||
|
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.8.1"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
@@ -32,6 +33,7 @@ let package = Package(
|
|||||||
.product(name: "AsyncXPCConnection", package: "AsyncXPCConnection"),
|
.product(name: "AsyncXPCConnection", package: "AsyncXPCConnection"),
|
||||||
.product(name: "MenuBarExtraAccess", package: "MenuBarExtraAccess"),
|
.product(name: "MenuBarExtraAccess", package: "MenuBarExtraAccess"),
|
||||||
.product(name: "Subprocess", package: "swift-subprocess"),
|
.product(name: "Subprocess", package: "swift-subprocess"),
|
||||||
|
.product(name: "Sparkle", package: "Sparkle"),
|
||||||
],
|
],
|
||||||
resources: [
|
resources: [
|
||||||
.copy("Resources/Clawdis.icns"),
|
.copy("Resources/Clawdis.icns"),
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AboutSettings: View {
|
struct AboutSettings: View {
|
||||||
|
weak var updater: UpdaterProviding?
|
||||||
@State private var iconHover = false
|
@State private var iconHover = false
|
||||||
|
@AppStorage("autoUpdateEnabled") private var autoCheckEnabled = true
|
||||||
|
@State private var didLoadUpdaterState = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
@@ -54,6 +57,25 @@ struct AboutSettings: View {
|
|||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
|
|
||||||
|
if let updater {
|
||||||
|
Divider()
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
if updater.isAvailable {
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
Toggle("Check for updates automatically", isOn: self.$autoCheckEnabled)
|
||||||
|
.toggleStyle(.checkbox)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
|
||||||
|
Button("Check for Updates…") { updater.checkForUpdates(nil) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Updates unavailable in this build.")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text("© 2025 Peter Steinberger — MIT License.")
|
Text("© 2025 Peter Steinberger — MIT License.")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -65,6 +87,15 @@ struct AboutSettings: View {
|
|||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
.padding(.bottom, 24)
|
.padding(.bottom, 24)
|
||||||
|
.onAppear {
|
||||||
|
guard let updater, !self.didLoadUpdaterState else { return }
|
||||||
|
// Keep Sparkle’s auto-check setting in sync with the persisted toggle.
|
||||||
|
updater.automaticallyChecksForUpdates = self.autoCheckEnabled
|
||||||
|
self.didLoadUpdaterState = true
|
||||||
|
}
|
||||||
|
.onChange(of: self.autoCheckEnabled) { _, newValue in
|
||||||
|
self.updater?.automaticallyChecksForUpdates = newValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var versionString: String {
|
private var versionString: String {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ struct ClawdisApp: App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
MenuBarExtra { MenuContent(state: self.state) } label: {
|
MenuBarExtra { MenuContent(state: self.state, updater: self.delegate.updaterController) } label: {
|
||||||
CritterStatusLabel(
|
CritterStatusLabel(
|
||||||
isPaused: self.state.isPaused,
|
isPaused: self.state.isPaused,
|
||||||
isWorking: self.state.isWorking,
|
isWorking: self.state.isWorking,
|
||||||
@@ -38,7 +38,7 @@ struct ClawdisApp: App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Settings {
|
Settings {
|
||||||
SettingsRootView(state: self.state)
|
SettingsRootView(state: self.state, updater: self.delegate.updaterController)
|
||||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight, alignment: .topLeading)
|
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight, alignment: .topLeading)
|
||||||
}
|
}
|
||||||
.defaultSize(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
.defaultSize(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||||
@@ -52,6 +52,7 @@ struct ClawdisApp: App {
|
|||||||
|
|
||||||
private struct MenuContent: View {
|
private struct MenuContent: View {
|
||||||
@ObservedObject var state: AppState
|
@ObservedObject var state: AppState
|
||||||
|
let updater: UpdaterProviding?
|
||||||
@ObservedObject private var relayManager = RelayProcessManager.shared
|
@ObservedObject private var relayManager = RelayProcessManager.shared
|
||||||
@ObservedObject private var healthStore = HealthStore.shared
|
@ObservedObject private var healthStore = HealthStore.shared
|
||||||
@Environment(\.openSettings) private var openSettings
|
@Environment(\.openSettings) private var openSettings
|
||||||
@@ -69,6 +70,9 @@ private struct MenuContent: View {
|
|||||||
Button("Settings…") { self.open(tab: .general) }
|
Button("Settings…") { self.open(tab: .general) }
|
||||||
.keyboardShortcut(",", modifiers: [.command])
|
.keyboardShortcut(",", modifiers: [.command])
|
||||||
Button("About Clawdis") { self.open(tab: .about) }
|
Button("About Clawdis") { self.open(tab: .about) }
|
||||||
|
if let updater, updater.isAvailable {
|
||||||
|
Button("Check for Updates…") { updater.checkForUpdates(nil) }
|
||||||
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Button("Quit") { NSApplication.shared.terminate(nil) }
|
Button("Quit") { NSApplication.shared.terminate(nil) }
|
||||||
}
|
}
|
||||||
@@ -82,7 +86,6 @@ private struct MenuContent: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var statusRow: some View {
|
private var statusRow: some View {
|
||||||
let relay = self.relayManager.status
|
|
||||||
let health = self.healthStore.state
|
let health = self.healthStore.state
|
||||||
let isRefreshing = self.healthStore.isRefreshing
|
let isRefreshing = self.healthStore.isRefreshing
|
||||||
let lastAge = self.healthStore.lastSuccess.map { age(from: $0) }
|
let lastAge = self.healthStore.lastSuccess.map { age(from: $0) }
|
||||||
@@ -491,6 +494,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate
|
|||||||
private let xpcLogger = Logger(subsystem: "com.steipete.clawdis", category: "xpc")
|
private let xpcLogger = Logger(subsystem: "com.steipete.clawdis", category: "xpc")
|
||||||
private let webChatAutoLogger = Logger(subsystem: "com.steipete.clawdis", category: "WebChat")
|
private let webChatAutoLogger = Logger(subsystem: "com.steipete.clawdis", category: "WebChat")
|
||||||
private let allowedTeamIDs: Set<String> = ["Y5PE65HELJ"]
|
private let allowedTeamIDs: Set<String> = ["Y5PE65HELJ"]
|
||||||
|
let updaterController: UpdaterProviding = makeUpdaterController()
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
@@ -609,3 +613,76 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate
|
|||||||
private func writeEndpoint(_ endpoint: NSXPCListenerEndpoint) {}
|
private func writeEndpoint(_ endpoint: NSXPCListenerEndpoint) {}
|
||||||
@MainActor private func writeEndpointIfAvailable() {}
|
@MainActor private func writeEndpointIfAvailable() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Sparkle updater (disabled for unsigned/dev builds)
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
protocol UpdaterProviding: AnyObject {
|
||||||
|
var automaticallyChecksForUpdates: Bool { get set }
|
||||||
|
var isAvailable: Bool { get }
|
||||||
|
func checkForUpdates(_ sender: Any?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No-op updater used for debug/dev runs to suppress Sparkle dialogs.
|
||||||
|
final class DisabledUpdaterController: UpdaterProviding {
|
||||||
|
var automaticallyChecksForUpdates: Bool = false
|
||||||
|
let isAvailable: Bool = false
|
||||||
|
func checkForUpdates(_: Any?) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(Sparkle)
|
||||||
|
import Sparkle
|
||||||
|
|
||||||
|
extension SPUStandardUpdaterController: UpdaterProviding {
|
||||||
|
var automaticallyChecksForUpdates: Bool {
|
||||||
|
get { self.updater.automaticallyChecksForUpdates }
|
||||||
|
set { self.updater.automaticallyChecksForUpdates = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var isAvailable: Bool { true }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isDeveloperIDSigned(bundleURL: URL) -> Bool {
|
||||||
|
var staticCode: SecStaticCode?
|
||||||
|
guard SecStaticCodeCreateWithPath(bundleURL as CFURL, SecCSFlags(), &staticCode) == errSecSuccess,
|
||||||
|
let code = staticCode
|
||||||
|
else { return false }
|
||||||
|
|
||||||
|
var infoCF: CFDictionary?
|
||||||
|
guard SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &infoCF) == errSecSuccess,
|
||||||
|
let info = infoCF as? [String: Any],
|
||||||
|
let certs = info[kSecCodeInfoCertificates as String] as? [SecCertificate],
|
||||||
|
let leaf = certs.first
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let summary = SecCertificateCopySubjectSummary(leaf) as String? {
|
||||||
|
return summary.hasPrefix("Developer ID Application:")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeUpdaterController() -> UpdaterProviding {
|
||||||
|
let bundleURL = Bundle.main.bundleURL
|
||||||
|
let isBundledApp = bundleURL.pathExtension == "app"
|
||||||
|
guard isBundledApp, isDeveloperIDSigned(bundleURL: bundleURL) else { return DisabledUpdaterController() }
|
||||||
|
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
let autoUpdateKey = "autoUpdateEnabled"
|
||||||
|
// Default to true; honor the user's last choice otherwise.
|
||||||
|
let savedAutoUpdate = (defaults.object(forKey: autoUpdateKey) as? Bool) ?? true
|
||||||
|
|
||||||
|
let controller = SPUStandardUpdaterController(
|
||||||
|
startingUpdater: false,
|
||||||
|
updaterDelegate: nil,
|
||||||
|
userDriverDelegate: nil)
|
||||||
|
controller.updater.automaticallyChecksForUpdates = savedAutoUpdate
|
||||||
|
controller.startUpdater()
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
private func makeUpdaterController() -> UpdaterProviding {
|
||||||
|
DisabledUpdaterController()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ struct SettingsRootView: View {
|
|||||||
@ObservedObject private var permissionMonitor = PermissionMonitor.shared
|
@ObservedObject private var permissionMonitor = PermissionMonitor.shared
|
||||||
@State private var monitoringPermissions = false
|
@State private var monitoringPermissions = false
|
||||||
@State private var selectedTab: SettingsTab = .general
|
@State private var selectedTab: SettingsTab = .general
|
||||||
|
let updater: UpdaterProviding?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: self.$selectedTab) {
|
TabView(selection: self.$selectedTab) {
|
||||||
@@ -41,7 +42,7 @@ struct SettingsRootView: View {
|
|||||||
.tag(SettingsTab.debug)
|
.tag(SettingsTab.debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
AboutSettings()
|
AboutSettings(updater: self.updater)
|
||||||
.tabItem { Label("About", systemImage: "info.circle") }
|
.tabItem { Label("About", systemImage: "info.circle") }
|
||||||
.tag(SettingsTab.about)
|
.tag(SettingsTab.about)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,4 +91,4 @@ struct Response { ok: Bool; message?: String; payload?: Data }
|
|||||||
- Where to place the dev symlink `bin/clawdis-mac` (repo root vs. `apps/macos/bin`)?
|
- Where to place the dev symlink `bin/clawdis-mac` (repo root vs. `apps/macos/bin`)?
|
||||||
- Should `runShell` support streaming stdout/stderr (XPC with AsyncSequence) or just buffered? (Start buffered; streaming later.)
|
- Should `runShell` support streaming stdout/stderr (XPC with AsyncSequence) or just buffered? (Start buffered; streaming later.)
|
||||||
- Icon: reuse Clawdis lobster or new mac-specific glyph?
|
- Icon: reuse Clawdis lobster or new mac-specific glyph?
|
||||||
- Sparkle updates: out of scope initially; add later if we ship signed builds.
|
- Sparkle updates: bundled via Sparkle; release builds point at `https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml` and enable auto-checks, while debug builds leave the feed blank and disable checks.
|
||||||
|
|||||||
67
docs/mac/release.md
Normal file
67
docs/mac/release.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
summary: "Clawdis macOS release checklist (Sparkle feed, packaging, signing)"
|
||||||
|
read_when:
|
||||||
|
- Cutting or validating a Clawdis macOS release
|
||||||
|
- Updating the Sparkle appcast or feed assets
|
||||||
|
---
|
||||||
|
|
||||||
|
# Clawdis macOS release (Sparkle)
|
||||||
|
|
||||||
|
This app now ships Sparkle auto-updates. Release builds must be Developer ID–signed, zipped, and published with a signed appcast entry.
|
||||||
|
|
||||||
|
## Prereqs
|
||||||
|
- Developer ID Application cert installed (`Developer ID Application: Peter Steinberger (Y5PE65HELJ)` is expected).
|
||||||
|
- Sparkle private key path set in the environment as `SPARKLE_PRIVATE_KEY_FILE`; key lives in `/Users/steipete/Library/CloudStorage/Dropbox/Backup/Sparkle` (same key as Trimmy; public key baked into Info.plist).
|
||||||
|
- `pnpm` deps installed (`pnpm install --config.node-linker=hoisted`).
|
||||||
|
- Sparkle tools are fetched automatically via SwiftPM at `apps/macos/.build/artifacts/sparkle/Sparkle/bin/` (`sign_update`, `generate_appcast`, etc.).
|
||||||
|
|
||||||
|
## Build & package
|
||||||
|
```bash
|
||||||
|
# From repo root; set release IDs so Sparkle feed is enabled
|
||||||
|
BUNDLE_ID=com.steipete.clawdis \
|
||||||
|
APP_VERSION=0.1.0 \
|
||||||
|
APP_BUILD=0.1.0 \
|
||||||
|
BUILD_CONFIG=release \
|
||||||
|
SIGN_IDENTITY="Developer ID Application: Peter Steinberger (Y5PE65HELJ)" \
|
||||||
|
scripts/package-mac-app.sh
|
||||||
|
|
||||||
|
# Zip for distribution (includes resource forks for Sparkle delta support)
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent dist/Clawdis.app dist/Clawdis-0.1.0.zip
|
||||||
|
|
||||||
|
# Optional: ship dSYM alongside the release
|
||||||
|
ditto -c -k --keepParent apps/macos/.build/release/Clawdis.app.dSYM dist/Clawdis-0.1.0.dSYM.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
## Appcast entry
|
||||||
|
1. Generate the ed25519 signature (requires `SPARKLE_PRIVATE_KEY_FILE`):
|
||||||
|
```bash
|
||||||
|
SPARKLE_PRIVATE_KEY_FILE=/Users/steipete/Library/CloudStorage/Dropbox/Backup/Sparkle/ed25519-private-key \
|
||||||
|
apps/macos/.build/artifacts/sparkle/Sparkle/bin/sign_update dist/Clawdis-0.1.0.zip
|
||||||
|
```
|
||||||
|
Copy the reported signature and file size.
|
||||||
|
2. Edit `appcast.xml` (root of repo), add a new `<item>` at the top pointing to the GitHub release asset. Example snippet to adapt:
|
||||||
|
```xml
|
||||||
|
<item>
|
||||||
|
<title>Clawdis 0.1.0</title>
|
||||||
|
<sparkle:releaseNotesLink>https://github.com/steipete/clawdis/releases/tag/v0.1.0</sparkle:releaseNotesLink>
|
||||||
|
<pubDate>Sun, 07 Dec 2025 12:00:00 +0000</pubDate>
|
||||||
|
<enclosure url="https://github.com/steipete/clawdis/releases/download/v0.1.0/Clawdis-0.1.0.zip"
|
||||||
|
sparkle:edSignature="<signature from sign_update>"
|
||||||
|
sparkle:version="0.1.0"
|
||||||
|
sparkle:shortVersionString="0.1.0"
|
||||||
|
length="<zip byte size>"
|
||||||
|
type="application/octet-stream" />
|
||||||
|
</item>
|
||||||
|
```
|
||||||
|
Keep the newest item first; leave the channel metadata intact.
|
||||||
|
3. Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing.
|
||||||
|
|
||||||
|
## Publish & verify
|
||||||
|
- Upload `Clawdis-0.1.0.zip` (and `Clawdis-0.1.0.dSYM.zip`) to the GitHub release for tag `v0.1.0`.
|
||||||
|
- Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml`.
|
||||||
|
- Sanity checks:
|
||||||
|
- `curl -I https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml` returns 200.
|
||||||
|
- `curl -I <enclosure url>` returns 200 after assets upload.
|
||||||
|
- On a previous public build, run “Check for Updates…” from the About tab and verify Sparkle installs the new build cleanly.
|
||||||
|
|
||||||
|
Definition of done: signed app + appcast are published, update flow works from an older installed version, and release assets are attached to the GitHub release.
|
||||||
@@ -69,6 +69,11 @@ sign_item() {
|
|||||||
codesign --force --options runtime --timestamp=none --entitlements "$ENT_TMP" --sign "$IDENTITY" "$target"
|
codesign --force --options runtime --timestamp=none --entitlements "$ENT_TMP" --sign "$IDENTITY" "$target"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sign_plain_item() {
|
||||||
|
local target="$1"
|
||||||
|
codesign --force --options runtime --timestamp=none --sign "$IDENTITY" "$target"
|
||||||
|
}
|
||||||
|
|
||||||
# Sign main binary and CLI helper if present
|
# Sign main binary and CLI helper if present
|
||||||
if [ -f "$APP_BUNDLE/Contents/MacOS/Clawdis" ]; then
|
if [ -f "$APP_BUNDLE/Contents/MacOS/Clawdis" ]; then
|
||||||
echo "Signing main binary"; sign_item "$APP_BUNDLE/Contents/MacOS/Clawdis"
|
echo "Signing main binary"; sign_item "$APP_BUNDLE/Contents/MacOS/Clawdis"
|
||||||
@@ -84,10 +89,26 @@ if [ -d "$APP_BUNDLE/Contents/Resources/Relay" ]; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Sign any embedded frameworks/dylibs if they ever appear
|
# Sign Sparkle deeply if present
|
||||||
|
SPARKLE="$APP_BUNDLE/Contents/Frameworks/Sparkle.framework"
|
||||||
|
if [ -d "$SPARKLE" ]; then
|
||||||
|
echo "Signing Sparkle framework and helpers"
|
||||||
|
sign_plain_item "$SPARKLE/Versions/B/Sparkle"
|
||||||
|
sign_plain_item "$SPARKLE/Versions/B/Autoupdate"
|
||||||
|
sign_plain_item "$SPARKLE/Versions/B/Updater.app/Contents/MacOS/Updater"
|
||||||
|
sign_plain_item "$SPARKLE/Versions/B/Updater.app"
|
||||||
|
sign_plain_item "$SPARKLE/Versions/B/XPCServices/Downloader.xpc/Contents/MacOS/Downloader"
|
||||||
|
sign_plain_item "$SPARKLE/Versions/B/XPCServices/Downloader.xpc"
|
||||||
|
sign_plain_item "$SPARKLE/Versions/B/XPCServices/Installer.xpc/Contents/MacOS/Installer"
|
||||||
|
sign_plain_item "$SPARKLE/Versions/B/XPCServices/Installer.xpc"
|
||||||
|
sign_plain_item "$SPARKLE/Versions/B"
|
||||||
|
sign_plain_item "$SPARKLE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sign any other embedded frameworks/dylibs
|
||||||
if [ -d "$APP_BUNDLE/Contents/Frameworks" ]; then
|
if [ -d "$APP_BUNDLE/Contents/Frameworks" ]; then
|
||||||
find "$APP_BUNDLE/Contents/Frameworks" \( -name "*.framework" -o -name "*.dylib" \) -print0 | while IFS= read -r -d '' f; do
|
find "$APP_BUNDLE/Contents/Frameworks" \( -name "*.framework" -o -name "*.dylib" \) ! -path "*Sparkle.framework*" -print0 | while IFS= read -r -d '' f; do
|
||||||
echo "Signing framework: $f"; sign_item "$f"
|
echo "Signing framework: $f"; sign_plain_item "$f"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ BUILD_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|||||||
GIT_COMMIT=$(cd "$ROOT_DIR" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
GIT_COMMIT=$(cd "$ROOT_DIR" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
APP_VERSION="${APP_VERSION:-$PKG_VERSION}"
|
APP_VERSION="${APP_VERSION:-$PKG_VERSION}"
|
||||||
APP_BUILD="${APP_BUILD:-$PKG_VERSION}"
|
APP_BUILD="${APP_BUILD:-$PKG_VERSION}"
|
||||||
|
BUILD_CONFIG="${BUILD_CONFIG:-debug}"
|
||||||
|
SPARKLE_PUBLIC_ED_KEY="${SPARKLE_PUBLIC_ED_KEY:-AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=}"
|
||||||
|
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml}"
|
||||||
|
AUTO_CHECKS=true
|
||||||
|
if [[ "$BUNDLE_ID" == *.debug ]]; then
|
||||||
|
SPARKLE_FEED_URL=""
|
||||||
|
AUTO_CHECKS=false
|
||||||
|
fi
|
||||||
|
|
||||||
echo "📦 Ensuring deps (pnpm install)"
|
echo "📦 Ensuring deps (pnpm install)"
|
||||||
(cd "$ROOT_DIR" && pnpm install --no-frozen-lockfile --config.node-linker=hoisted)
|
(cd "$ROOT_DIR" && pnpm install --no-frozen-lockfile --config.node-linker=hoisted)
|
||||||
@@ -22,16 +30,17 @@ echo "📦 Building JS (pnpm exec tsc)"
|
|||||||
|
|
||||||
cd "$ROOT_DIR/apps/macos"
|
cd "$ROOT_DIR/apps/macos"
|
||||||
|
|
||||||
echo "🔨 Building $PRODUCT (debug)"
|
echo "🔨 Building $PRODUCT ($BUILD_CONFIG)"
|
||||||
swift build -c debug --product "$PRODUCT" --product "${PRODUCT}CLI" --build-path "$BUILD_PATH"
|
swift build -c "$BUILD_CONFIG" --product "$PRODUCT" --product "${PRODUCT}CLI" --build-path "$BUILD_PATH"
|
||||||
|
|
||||||
BIN="$BUILD_PATH/debug/$PRODUCT"
|
BIN="$BUILD_PATH/$BUILD_CONFIG/$PRODUCT"
|
||||||
CLI_BIN="$BUILD_PATH/debug/ClawdisCLI"
|
CLI_BIN="$BUILD_PATH/$BUILD_CONFIG/ClawdisCLI"
|
||||||
echo "🧹 Cleaning old app bundle"
|
echo "🧹 Cleaning old app bundle"
|
||||||
rm -rf "$APP_ROOT"
|
rm -rf "$APP_ROOT"
|
||||||
mkdir -p "$APP_ROOT/Contents/MacOS"
|
mkdir -p "$APP_ROOT/Contents/MacOS"
|
||||||
mkdir -p "$APP_ROOT/Contents/Resources"
|
mkdir -p "$APP_ROOT/Contents/Resources"
|
||||||
mkdir -p "$APP_ROOT/Contents/Resources/Relay"
|
mkdir -p "$APP_ROOT/Contents/Resources/Relay"
|
||||||
|
mkdir -p "$APP_ROOT/Contents/Frameworks"
|
||||||
|
|
||||||
echo "📄 Writing Info.plist"
|
echo "📄 Writing Info.plist"
|
||||||
cat > "$APP_ROOT/Contents/Info.plist" <<PLIST
|
cat > "$APP_ROOT/Contents/Info.plist" <<PLIST
|
||||||
@@ -61,6 +70,12 @@ cat > "$APP_ROOT/Contents/Info.plist" <<PLIST
|
|||||||
<string>${BUILD_TS}</string>
|
<string>${BUILD_TS}</string>
|
||||||
<key>ClawdisGitCommit</key>
|
<key>ClawdisGitCommit</key>
|
||||||
<string>${GIT_COMMIT}</string>
|
<string>${GIT_COMMIT}</string>
|
||||||
|
<key>SUFeedURL</key>
|
||||||
|
<string>${SPARKLE_FEED_URL}</string>
|
||||||
|
<key>SUPublicEDKey</key>
|
||||||
|
<string>${SPARKLE_PUBLIC_ED_KEY}</string>
|
||||||
|
<key>SUEnableAutomaticChecks</key>
|
||||||
|
<${AUTO_CHECKS}/>
|
||||||
<key>NSUserNotificationUsageDescription</key>
|
<key>NSUserNotificationUsageDescription</key>
|
||||||
<string>Clawdis needs notification permission to show alerts for agent actions.</string>
|
<string>Clawdis needs notification permission to show alerts for agent actions.</string>
|
||||||
<key>NSScreenCaptureDescription</key>
|
<key>NSScreenCaptureDescription</key>
|
||||||
@@ -79,6 +94,14 @@ echo "🚚 Copying binary"
|
|||||||
cp "$BIN" "$APP_ROOT/Contents/MacOS/Clawdis"
|
cp "$BIN" "$APP_ROOT/Contents/MacOS/Clawdis"
|
||||||
chmod +x "$APP_ROOT/Contents/MacOS/Clawdis"
|
chmod +x "$APP_ROOT/Contents/MacOS/Clawdis"
|
||||||
|
|
||||||
|
SPARKLE_FRAMEWORK="$BUILD_PATH/$BUILD_CONFIG/Sparkle.framework"
|
||||||
|
if [ -d "$SPARKLE_FRAMEWORK" ]; then
|
||||||
|
echo "✨ Embedding Sparkle.framework"
|
||||||
|
cp -R "$SPARKLE_FRAMEWORK" "$APP_ROOT/Contents/Frameworks/"
|
||||||
|
chmod -R a+rX "$APP_ROOT/Contents/Frameworks/Sparkle.framework"
|
||||||
|
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP_ROOT/Contents/MacOS/Clawdis"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "🖼 Copying app icon"
|
echo "🖼 Copying app icon"
|
||||||
cp "$ROOT_DIR/apps/macos/Sources/Clawdis/Resources/Clawdis.icns" "$APP_ROOT/Contents/Resources/Clawdis.icns"
|
cp "$ROOT_DIR/apps/macos/Sources/Clawdis/Resources/Clawdis.icns" "$APP_ROOT/Contents/Resources/Clawdis.icns"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user