Files
nucleic/Sources/NucleicApp/BuildBanner.swift
T

149 lines
6.3 KiB
Swift

import SwiftUI
/// How this copy of Nucleic was built. Selected at build time by the
/// `NUCLEIC_CHANNEL` make/env setting (see Package.swift), which also sets the
/// executable's process name. Drives the warning header that keeps a non-release
/// build from being mistaken for a shipping release.
enum BuildChannel {
/// The `dev` channel — process `nucleic-local`, red "Local Development Build" banner.
case local
/// The `canary` channel — process `nucleic-canary`, canary-yellow "Canary" banner. The
/// bleeding-edge distributed channel, cut before beta (see BUILD.md).
case canary
/// The `beta` channel — process `nucleic-beta`, blue "Beta" banner.
case beta
/// The `rc` channel — process `nucleic-rc`, gold "Release Candidate" banner.
case releaseCandidate
/// The `stable` channel — process `nucleic`, no banner.
case release
}
/// Build identity for the running app: which channel it came from and the commit it
/// was built from. The commit is embedded at build time by the `EmbedGitCommit`
/// prebuild plugin (`GitCommit.hash`), so it's baked into the binary and stays
/// correct even on a packaged beta / rc / stable archive where git isn't present.
struct BuildInfo {
let channel: BuildChannel
/// The short git commit the binary was built from (e.g. "a1b2c3d", "+" suffix when
/// the tree was dirty). Falls back to `CFBundleVersion` / "local" if the plugin
/// couldn't resolve a hash.
let buildLabel: String
/// `CFBundleShortVersionString` when present, else `nil`.
let version: String?
/// The monotonic `CFBundleVersion` build number (from `NUCLEIC_BUILD_NUMBER`),
/// or `nil` when not present in the bundle.
let buildNumber: String?
/// Resolved once — none of this changes while the app is running.
static let current = BuildInfo()
init() {
let info = Bundle.main.infoDictionary
version = info?["CFBundleShortVersionString"] as? String
buildNumber = info?["CFBundleVersion"] as? String
let commit = GitCommit.hash
buildLabel = (commit == "unknown" ? nil : commit)
?? (info?["CFBundleVersion"] as? String)
?? "local"
channel = Self.detectChannel()
}
/// The channel is fixed at build time by the `NUCLEIC_CHANNEL` setting, which
/// Package.swift turns into one of these defines. Defaults to `.local` (the `dev`
/// channel) when nothing is set.
private static func detectChannel() -> BuildChannel {
#if NUCLEIC_STABLE
return .release
#elseif NUCLEIC_RC
return .releaseCandidate
#elseif NUCLEIC_BETA
return .beta
#elseif NUCLEIC_CANARY
return .canary
#else
return .local
#endif
}
}
/// Header strip warning that the window is a non-release build: a red tint for
/// locally-built / pre-prod copies, canary-yellow for canary, blue for beta, gold for a release candidate. The
/// leading edge names the channel; the commit hash and an all-caps "EXPERIMENTAL"
/// badge sit on the trailing
/// edge. The tint is kept translucent so it reads as a quiet marker rather than
/// fighting the window background. Renders nothing for a shipping App Store release.
struct BuildBanner: View {
var info: BuildInfo = .current
var body: some View {
if let style = Style(channel: info.channel) {
HStack(spacing: 8) {
Image(systemName: style.icon)
.font(.system(size: style.iconSize, weight: .heavy))
Text(style.label)
.font(.body.weight(.semibold))
Spacer(minLength: 12)
Text(info.buildLabel)
.font(.caption.monospaced())
.opacity(0.85)
Text("EXPERIMENTAL")
.font(.caption2.weight(.heavy))
.tracking(0.9)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(.white.opacity(0.16), in: .capsule)
}
.foregroundStyle(.white.opacity(0.92))
.padding(.horizontal, 12)
.padding(.vertical, 5)
.frame(maxWidth: .infinity)
// A plain translucent tint, not its own glass: this strip sits directly
// under the window's unified toolbar, whose Liquid Glass samples and blurs it
// — so the *toolbar* is the glassy header and this is just its colored
// backdrop. Giving the strip its own `glassEffect` drew a bright glass edge on
// it; the toolbar's own glass is pinned visible in `RootView` instead.
.background(style.tint.opacity(0.5))
.help(style.tooltip(version: info.version, build: info.buildLabel))
}
}
/// Per-channel appearance. `nil` for `.release` so the banner disappears entirely.
private struct Style {
var tint: Color
var label: String
var icon: String
// Most channels share the default glyph size; the canary bell-with-waves
// glyph is unusually wide, so it's shrunk to sit in line with its label.
var iconSize: CGFloat = 17
init?(channel: BuildChannel) {
switch channel {
case .local:
tint = Color(red: 0.78, green: 0.18, blue: 0.18) // red
label = "Local Development Build"
icon = "hammer.fill"
case .canary:
tint = Color(red: 1.00, green: 0.87, blue: 0.00) // bright canary yellow
label = "Canary Build"
icon = "bell.and.waves.left.and.right.fill"
iconSize = 13
case .beta:
tint = Color(red: 0.13, green: 0.40, blue: 0.86) // blue
label = "Public Beta"
icon = "person.3.fill"
case .releaseCandidate:
tint = Color(red: 0.83, green: 0.65, blue: 0.12) // gold
label = "Release Candidate"
icon = "checkmark.seal.fill"
case .release:
return nil
}
}
func tooltip(version: String?, build: String) -> String {
let v = version.map { "\($0) " } ?? ""
return "\(label)\(v)commit \(build). Not a release build."
}
}
}