diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 9b239961d..f385732c7 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -139,7 +139,11 @@ jobs: # GhosttyKit is the framework that is built from Zig for our native # Mac app to access. Build this in release mode. - name: Build GhosttyKit - run: nix develop -c zig build -Doptimize=ReleaseFast + run: | + nix develop -c \ + zig build \ + -Doptimize=ReleaseFast \ + -Dversion-string=${GHOSTTY_VERSION} # The native app is built with native XCode tooling. This also does # codesigning. IMPORTANT: this must NOT run in a Nix environment. diff --git a/dist/macos/update_appcast_tag.py b/dist/macos/update_appcast_tag.py index de9b1259a..86aa0bed0 100644 --- a/dist/macos/update_appcast_tag.py +++ b/dist/macos/update_appcast_tag.py @@ -52,8 +52,8 @@ channel = et.find("channel") # the same version, Sparkle will report invalid signatures if it picks # the wrong one when updating. for item in channel.findall("item"): - version = item.find("sparkle:version", namespaces) - if version is not None and version.text == build: + sparkle_version = item.find("sparkle:version", namespaces) + if sparkle_version is not None and sparkle_version.text == build: channel.remove(item) # We also remove any item that doesn't have a pubDate. This should diff --git a/macos/Sources/Features/Update/UpdateDelegate.swift b/macos/Sources/Features/Update/UpdateDelegate.swift index 7b41c816c..4699ba14a 100644 --- a/macos/Sources/Features/Update/UpdateDelegate.swift +++ b/macos/Sources/Features/Update/UpdateDelegate.swift @@ -3,11 +3,17 @@ import Cocoa class UpdaterDelegate: NSObject, SPUUpdaterDelegate { func feedURLString(for updater: SPUUpdater) -> String? { - // Eventually w want to support multiple channels. Sparkle itself supports - // channels but we probably don't want some appcasts in the same file (i.e. - // tip) so this would be the place to change that. For now, we hardcode the - // tip appcast URL since it is all we support. - return "https://tip.files.ghostty.org/appcast.xml" + guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { + return nil + } + + // Sparkle supports a native concept of "channels" but it requires that + // you share a single appcast file. We don't want to do that so we + // do this instead. + switch (appDelegate.ghostty.config.autoUpdateChannel) { + case .tip: return "https://tip.files.ghostty.org/appcast.xml" + case .stable: return "https://release.files.ghostty.org/appcast.xml" + } } func updaterWillRelaunchApplication(_ updater: SPUUpdater) { diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 3a58455d9..ee37c8cc5 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -408,6 +408,17 @@ extension Ghostty { return AutoUpdate(rawValue: str) ?? defaultValue } + var autoUpdateChannel: AutoUpdateChannel { + let defaultValue = AutoUpdateChannel.stable + guard let config = self.config else { return defaultValue } + var v: UnsafePointer? = nil + let key = "auto-update-channel" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue } + guard let ptr = v else { return defaultValue } + let str = String(cString: ptr) + return AutoUpdateChannel(rawValue: str) ?? defaultValue + } + var autoSecureInput: Bool { guard let config = self.config else { return true } var v = false; diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index a4d1914e0..e7d9d98fd 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -200,7 +200,12 @@ extension Ghostty { case visible case hidden } - + + /// Enum for auto-update-channel config option + enum AutoUpdateChannel: String { + case tip + case stable + } } // MARK: Surface Notification diff --git a/src/build_config.zig b/src/build_config.zig index 41e2767bf..1f3b35e03 100644 --- a/src/build_config.zig +++ b/src/build_config.zig @@ -58,6 +58,15 @@ pub const BuildConfig = struct { "{}", .{self.version}, )); + step.addOption( + ReleaseChannel, + "release_channel", + channel: { + const pre = self.version.pre orelse break :channel .stable; + if (pre.len == 0) break :channel .stable; + break :channel .tip; + }, + ); } /// Rehydrate our BuildConfig from the comptime options. Note that not all @@ -82,6 +91,9 @@ pub const BuildConfig = struct { pub const version = options.app_version; pub const version_string = options.app_version_string; +/// The release channel for this build. +pub const release_channel = std.meta.stringToEnum(ReleaseChannel, @tagName(options.release_channel)).?; + /// The optimization mode as a string. pub const mode_string = mode: { const m = @tagName(builtin.mode); @@ -180,3 +192,12 @@ pub const ExeEntrypoint = enum { bench_grapheme_break, bench_page_init, }; + +/// The release channel for the build. +pub const ReleaseChannel = enum { + /// Unstable builds on every commit. + tip, + + /// Stable tagged releases. + stable, +}; diff --git a/src/cli/version.zig b/src/cli/version.zig index 259cb7453..b781398f2 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -25,6 +25,10 @@ pub fn run(alloc: Allocator) !u8 { try stdout.print("Ghostty {s}\n\n", .{build_config.version_string}); if (tty) try stdout.print("\x1b]8;;\x1b\\", .{}); + try stdout.print("Version\n", .{}); + try stdout.print(" - version: {s}\n", .{build_config.version_string}); + try stdout.print(" - channel: {s}\n", .{@tagName(build_config.release_channel)}); + try stdout.print("Build Config\n", .{}); try stdout.print(" - Zig version: {s}\n", .{builtin.zig_version_string}); try stdout.print(" - build mode : {}\n", .{builtin.mode}); diff --git a/src/config/Config.zig b/src/config/Config.zig index 99c0663cf..13ab65117 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -12,6 +12,7 @@ const Config = @This(); const std = @import("std"); const builtin = @import("builtin"); +const build_config = @import("../build_config.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; @@ -1843,6 +1844,28 @@ term: []const u8 = "xterm-ghostty", /// Changing this value at runtime works after a small delay. @"auto-update": AutoUpdate = .check, +/// The release channel to use for auto-updates. +/// +/// The default value of this matches the release channel of the currently +/// running Ghostty version. If you download a pre-release version of Ghostty +/// then this will be set to `tip` and you will receive pre-release updates. +/// If you download a stable version of Ghostty then this will be set to +/// `stable` and you will receive stable updates. +/// +/// Valid values are: +/// +/// * `stable` - Stable, tagged releases such as "1.0.0". +/// * `tip` - Pre-release versions generated from each commit to the +/// main branch. This is the version that was in use during private +/// beta testing by thousands of people. It is generally stable but +/// will likely have more bugs than the stable channel. +/// +/// Changing this configuration requires a full restart of +/// Ghostty to take effect. +/// +/// This only works on macOS since only macOS has an auto-update feature. +@"auto-update-channel": ?build_config.ReleaseChannel = null, + /// This is set by the CLI parser for deinit. _arena: ?ArenaAllocator = null, @@ -3055,6 +3078,12 @@ pub fn finalize(self: *Config) !void { ); } } + + // We can't set this as a struct default because our config is + // loaded in environments where a build config isn't available. + if (self.@"auto-update-channel" == null) { + self.@"auto-update-channel" = build_config.release_channel; + } } /// Callback for src/cli/args.zig to allow us to handle special cases