eleven / ocho Public

mac: About panel shows the git SHA the build came from (#1417)

"Which build am I actually running?" had no answer on a locally-built
Mac app — version and build number are static in project.yml, so every
build since 1.0 (2) looks identical.

BuildInfo.swift has claimed to answer this since #2219805e, but never
could: `#if GIT_SHA` is a compilation *condition* — a flag, not a value
— so it can't carry a string, and nothing referenced the struct anyway.
CI fed it via GCC_PREPROCESSOR_DEFINITIONS, which Swift ignores
entirely. It has been reading "unknown" the whole time. Both halves of
that dead mechanism go.

The SHA now rides in the built Info.plist, stamped by a post-build
phase. Deliberately a custom GitSHA key and not CFBundleVersion:
Sparkle orders updates by that, so a SHA there would break auto-update.
The phase runs before the product's CodeSign step, so the seal covers
the edited plist, and it declares no outputs (dependency analysis off)
so it re-stamps on every build — otherwise switching branches would
leave the previous commit's SHA sitting in the bundle. A tree with
uncommitted edits stamps `-dirty`, since a local build's SHA otherwise
implies the app matches that commit when it doesn't. No .git to ask (a
source tarball) skips the stamp rather than failing the build.

Rendering reuses the standard About panel via `.version` — the
parenthetical slot — so there's no custom window to maintain:

    Version 1.0 (2 · 13622e22-dirty)

Verified: stamp matches rev-parse, signature survives the plist edit,
and a no-op incremental build overwrites a deliberately-corrupted value
(so branch switches can't go stale).
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
James Adam pushed by codex agent, operated by paul@keymail.dev 8b2b19ffe6824d20d30c5e7cf3b0440ca3763f10 parent 8c13055
4 files changed, +66 −15
  • .github/workflows/build-apps.yml +0 −1
  • ios/LChatMac/BuildInfo.swift +35 −14
  • ios/LChatMac/LChatMacApp.swift +7 −0
  • ios/project.yml +24 −0
diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml
index 9d499c4..e6f6394 100644
--- a/.github/workflows/build-apps.yml
+++ b/.github/workflows/build-apps.yml
@@ -56,7 +56,6 @@ jobs:
-derivedDataPath build \
CODE_SIGN_STYLE=Automatic \
CODE_SIGN_IDENTITY='-' \
- GCC_PREPROCESSOR_DEFINITIONS='GIT_SHA="${{ github.sha }}"' \
build
- name: Package app
diff --git a/ios/LChatMac/BuildInfo.swift b/ios/LChatMac/BuildInfo.swift
index ba9e45b..b8ae2bd 100644
--- a/ios/LChatMac/BuildInfo.swift
+++ b/ios/LChatMac/BuildInfo.swift
@@ -1,20 +1,41 @@
+import AppKit
import Foundation
-struct BuildInfo {
- #if GIT_SHA
- static let gitSha = String(stringLiteral: GIT_SHA).prefix(7)
- #else
- static let gitSha = "unknown"
- #endif
-
- static let versionString: String = {
- let bundle = Bundle.main
- let version = bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
- let build = bundle.infoDictionary?["CFBundleVersion"] as? String ?? "1"
- return "\(version) (build \(build))"
+// Where the running app came from, for the About panel. The short git SHA is
+// stamped into the built Info.plist by the "Stamp git SHA" phase in
+// ios/project.yml — NOT baked in at compile time: a Swift compilation
+// condition (`#if GIT_SHA`) is a flag, not a value, so it can't carry the
+// string, and CFBundleVersion is off-limits because Sparkle orders updates by
+// it. The stamp is absent in a checkout without .git (a source tarball), and
+// then the panel is simply the stock one.
+enum BuildInfo {
+ /// Short SHA of the commit the app was built from, `-dirty` when the tree
+ /// had uncommitted changes. Nil when the build had no git to ask.
+ static let gitSHA: String? = {
+ let sha = Bundle.main.infoDictionary?["GitSHA"] as? String
+ return (sha?.isEmpty ?? true) ? nil : sha
}()
- static var buildDescription: String {
- "Eleven \(versionString)\nGit: \(gitSha)"
+ /// The bundle's display name — "Eleven", or "Eleven Dev" for a dev alt, so
+ /// the menu item names the app you're actually looking at.
+ static var appName: String {
+ let info = Bundle.main.infoDictionary
+ return info?["CFBundleDisplayName"] as? String
+ ?? info?["CFBundleName"] as? String
+ ?? "Eleven"
+ }
+
+ /// Options for the standard About panel. The SHA rides in `.version` — the
+ /// parenthetical next to the marketing version — so it reads "Version 1.0
+ /// (2 · a1b2c3d)" with no custom window to maintain.
+ static var aboutPanelOptions: [NSApplication.AboutPanelOptionKey: Any] {
+ guard let sha = gitSHA else { return [:] }
+ let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
+ return [.version: build.isEmpty ? sha : "\(build) · \(sha)"]
+ }
+
+ static func showAboutPanel() {
+ NSApp.activate(ignoringOtherApps: true)
+ NSApp.orderFrontStandardAboutPanel(options: aboutPanelOptions)
}
}
diff --git a/ios/LChatMac/LChatMacApp.swift b/ios/LChatMac/LChatMacApp.swift
index 5ea75c1..7f84ce5 100644
--- a/ios/LChatMac/LChatMacApp.swift
+++ b/ios/LChatMac/LChatMacApp.swift
@@ -144,6 +144,13 @@ struct LChatMacApp: App {
Button("Create a space") { app.requestCreateSpace() }
Button("Add from link…") { app.requestAddFromLink() }
}
+ // Our own About item, so the standard panel can carry the git SHA
+ // the build was stamped with (BuildInfo). Same position and title
+ // as the one SwiftUI provides — it just opens the panel with
+ // options.
+ CommandGroup(replacing: .appInfo) {
+ Button("About \(BuildInfo.appName)") { BuildInfo.showAboutPanel() }
+ }
// Sits right below "About Eleven" in the app menu.
CommandGroup(after: .appInfo) {
CheckForUpdatesView(updater: appDelegate.updaterController.updater)
diff --git a/ios/project.yml b/ios/project.yml
index 1f79852..5fd5ad3 100644
--- a/ios/project.yml
+++ b/ios/project.yml
@@ -151,6 +151,30 @@ targets:
# timestamp keep notarization happy. Runs before the product's own CodeSign
# step, so the app seal covers it.
postBuildScripts:
+ # Stamp the commit the app was built from into the bundle, so the About
+ # panel can show it (BuildInfo.swift) — the answer to "which build is
+ # this?" for a locally-built app. A custom Info.plist key on purpose:
+ # CFBundleVersion is how Sparkle orders updates, so the SHA must never
+ # go there. Runs before the product's CodeSign step, like the phase
+ # below, so the seal covers the edited plist. No outputs (and dependency
+ # analysis off) so it re-stamps on EVERY build — otherwise switching
+ # branches would leave the previous SHA in the bundle. Silently skipped
+ # when there's no git to ask (a source tarball), rather than failing.
+ - name: Stamp git SHA
+ shell: /bin/bash
+ basedOnDependencyAnalysis: false
+ script: |
+ set -euo pipefail
+ SHA="$(git -C "$SRCROOT/.." rev-parse --short HEAD 2>/dev/null || true)"
+ [ -n "$SHA" ] || exit 0
+ # A local build usually has edits in flight; say so, or the SHA
+ # implies the running app matches that commit when it doesn't.
+ if [ -n "$(git -C "$SRCROOT/.." status --porcelain 2>/dev/null)" ]; then
+ SHA="$SHA-dirty"
+ fi
+ # $INFOPLIST_PATH, not a hardcoded name: PRODUCT_NAME varies (a dev
+ # alt's bundle is "Eleven Dev.app").
+ plutil -replace GitSHA -string "$SHA" "$TARGET_BUILD_DIR/$INFOPLIST_PATH"
- name: Embed eleven CLI
shell: /bin/bash
outputFiles: