Add build version (commit hash) to CLI, GUI, and Android

Surface the commit a build came from as a placeholder version identifier,
until a proper version scheme exists.

- rust/lib: build.rs captures the commit hash at compile time (CI env
  override, else jj/git, else "unknown") and exposes it via a new
  version module plus a build_info() uniffi export.
- CLI: new 'about' command printing version and full commit hash.
- GUI: About card in settings, fed by a BuildInfo Slint global.
- Android: About section in settings, with the commit selectable/copyable.
- CI: pass GITEA_SHA into the APK build step so the embedded hash is
  deterministic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Greg Shuflin
2026-06-12 15:11:55 -07:00
parent aa7470af56
commit 3a133a28eb
13 changed files with 266 additions and 1 deletions
+4
View File
@@ -19,6 +19,10 @@ jobs:
- name: Build APK
if: ${{ gitea.event.inputs.skip_build != 'true' }}
env:
# Embedded into the build by rust/lib/build.rs (the About screen /
# `syn about` command) so the APK reports the commit it came from.
GITEA_SHA: ${{ gitea.sha }}
run: nix develop --command build-apk-ci
- name: Verify APK exists
@@ -119,6 +119,9 @@ class NativeLib {
// Infallible: pure computation in Rust with no `Result` return.
fun listTimezones(): List<String> = uniffi.synchronicity.listTimezones()
/** Build/version metadata for the About section. Static, computed in Rust. */
fun buildInfo(): uniffi.synchronicity.BuildInfo = uniffi.synchronicity.buildInfo()
/** Ping timeout (seconds), sourced from shared Rust so it matches the GUI. */
fun pingTimeoutSecs(): Int = uniffi.synchronicity.pingTimeoutSecs().toInt()
@@ -9,12 +9,14 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.gregshuflin.synchronicity.NativeLib
@@ -34,6 +36,7 @@ fun SettingsView(
onNodeKeyClick: () -> Unit = {},
) {
val nativeLib = remember { NativeLib() }
val buildInfo = remember { nativeLib.buildInfo() }
var showResetConfirmation by remember { mutableStateOf(false) }
var isResetting by remember { mutableStateOf(false) }
var resetError by remember { mutableStateOf<String?>(null) }
@@ -220,6 +223,53 @@ fun SettingsView(
}
}
// About / version
Text(
text = "About",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
)
Text(
text =
"The version of Synchronicity this node is running. For now a build is identified by the commit it came from.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
modifier = Modifier.padding(bottom = 4.dp),
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Version",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
modifier = Modifier.weight(1f),
)
Text(text = buildInfo.version, style = MaterialTheme.typography.bodyMedium)
}
// SelectionContainer keeps the full hash selectable/copyable.
SelectionContainer {
Row(
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
verticalAlignment = Alignment.Top,
) {
Text(
text = "Commit",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
modifier = Modifier.weight(1f),
)
Text(
text = buildInfo.commitHash,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
modifier = Modifier.weight(2f),
)
}
}
// Spacer to push dangerous actions to bottom
Spacer(modifier = Modifier.weight(1f))
+8 -1
View File
@@ -24,6 +24,8 @@ pub(crate) struct CliArguments {
pub(crate) enum Cmd {
Node(NodeCmd),
Data(DataCmd),
/// Show version and build information
About,
}
fn cmd() -> impl Parser<Cmd> {
@@ -35,7 +37,12 @@ fn cmd() -> impl Parser<Cmd> {
.command("e")
.map(|sub| Cmd::Node(NodeCmd::Ensemble { sub }))
.hide();
construct!([node, e_alias, data])
let about = bpaf::pure(Cmd::About)
.to_options()
.descr("Show version and build information")
.command("about")
.help("Show version and build information");
construct!([node, e_alias, data, about])
}
#[derive(Bpaf, Debug, Clone)]
+14
View File
@@ -42,6 +42,7 @@ fn run() -> Result<()> {
let store_path = cli.store.unwrap_or_else(synchronicity::default_store_path);
match cli.command {
Cmd::About => cmd_about(),
Cmd::Node(NodeCmd::Daemon { sub: DaemonCmd::Start { foreground } }) => {
// Validate the store exists before forking (daemon re-opens it after fork)
Store::open(&store_path).map(drop)?;
@@ -939,6 +940,19 @@ fn handle_settings(store_path: &Path, sub: cli::SettingsCmd) -> Result<()> {
Ok(())
}
/// `syn about`: print version and build information. Identifies the build by
/// the commit it came from until a proper version scheme exists.
fn cmd_about() -> Result<()> {
use colored::Colorize;
use synchronicity::version;
let label = |s: &str| s.dimmed();
println!("{}", "Synchronicity".bold());
println!("{} {}", label("Version :"), version::PKG_VERSION);
println!("{} {}", label("Commit :"), version::COMMIT_HASH);
Ok(())
}
fn parse_id(s: &str) -> Result<DocumentID> {
DocumentID::from_str(s).map_err(|_| anyhow::anyhow!("invalid document id: {s}"))
}
+8
View File
@@ -155,6 +155,14 @@ fn slint_main(store: Store, cache_dir: PathBuf) -> Result<(), Box<dyn std::error
// Set the initialization status
ui.set_is_initialized(is_initialized);
// Populate build/version info for the About section (static, set once).
{
let build_info = ui.global::<BuildInfo>();
build_info.set_version(synchronicity::version::PKG_VERSION.into());
build_info.set_commit(synchronicity::version::COMMIT_HASH.into());
build_info.set_commit_short(synchronicity::version::short_commit().into());
}
// Populate sidebar agents from the registry
{
let registry = AgentRegistry::default();
+2
View File
@@ -22,6 +22,8 @@ import {
BookmarkItem,
} from "main_screen.slint";
import { Navigation } from "navigation.slint";
import { BuildInfo } from "build_info.slint";
export { BuildInfo }
export component AppWindow inherits Window {
in-out property <bool> dark-theme: false;
+7
View File
@@ -0,0 +1,7 @@
// Build/version metadata, populated once from Rust at startup and read by the
// About card on the settings screen.
export global BuildInfo {
in property <string> version;
in property <string> commit;
in property <string> commit-short;
}
+68
View File
@@ -8,6 +8,7 @@ import {
} from "std-widgets.slint";
import { CopyButton } from "buttons/copy.slint";
import { Theme, ThemeModeSelector } from "components/theme.slint";
import { BuildInfo } from "build_info.slint";
// A single tab in the settings section tab bar. Selected tabs are accented and
// underlined; unselected tabs are muted.
@@ -344,6 +345,73 @@ export component SettingsScreen inherits VerticalBox {
}
}
}
// About / version section
Rectangle {
background: Theme.bg-card;
border-radius: Theme.radius-lg;
border-width: 1px;
border-color: Theme.border-card;
VerticalBox {
padding: Theme.spacing-xl;
spacing: Theme.spacing-lg;
Text {
text: "About";
font-size: Theme.font-text-xl;
font-weight: 600;
color: Theme.text-primary;
}
Text {
text: "The version of Synchronicity this node is running. For now a build is identified by the commit it came from.";
font-size: Theme.font-text-md;
color: Theme.text-muted;
wrap: word-wrap;
}
HorizontalLayout {
spacing: Theme.spacing-md;
alignment: start;
Text {
text: "Version";
font-size: Theme.font-text-md;
color: Theme.text-muted;
vertical-alignment: center;
}
Text {
text: BuildInfo.version;
font-size: Theme.font-text-md;
color: Theme.text-primary;
vertical-alignment: center;
horizontal-stretch: 1;
}
}
HorizontalLayout {
spacing: Theme.spacing-md;
alignment: start;
Text {
text: "Commit";
font-size: Theme.font-text-md;
color: Theme.text-muted;
vertical-alignment: center;
}
// Read-only TextInput so the full hash is selectable/copyable.
TextInput {
text: BuildInfo.commit;
read-only: true;
single-line: true;
font-family: "monospace";
font-size: Theme.font-text-xs;
color: Theme.text-input;
horizontal-stretch: 1;
vertical-alignment: center;
}
}
}
}
}
// Spacer
+50
View File
@@ -0,0 +1,50 @@
use std::process::Command;
/// Capture the commit this build came from and expose it to the crate as the
/// `SYNCHRONICITY_GIT_SHA` compile-time environment variable (read by
/// `src/version.rs`). Mirrors the way the release CI tags builds from the
/// commit hash.
fn main() {
// Re-run when an explicit build SHA is provided (CI) or when the local VCS
// HEAD moves, so the embedded commit stays in sync. Paths are relative to
// this crate (rust/lib); the repo root is two levels up.
println!("cargo::rerun-if-env-changed=SYNCHRONICITY_BUILD_SHA");
println!("cargo::rerun-if-env-changed=GITEA_SHA");
println!("cargo::rerun-if-changed=../../.git/HEAD");
println!("cargo::rerun-if-changed=../../.git/refs");
println!("cargo::rerun-if-changed=../../.jj/working_copy");
println!("cargo::rustc-env=SYNCHRONICITY_GIT_SHA={}", build_sha());
}
fn build_sha() -> String {
// 1. Explicit override from CI, which knows the commit even when the build
// runs over a source tree with VCS metadata stripped (e.g. Nix).
for var in ["SYNCHRONICITY_BUILD_SHA", "GITEA_SHA"] {
if let Ok(val) = std::env::var(var) {
let val = val.trim();
if !val.is_empty() {
return val.to_string();
}
}
}
// 2. Jujutsu working copy (this project's primary VCS).
if let Some(sha) = run("jj", &["log", "--no-graph", "-r", "@", "-T", "commit_id"]) {
return sha;
}
// 3. Plain git checkout.
if let Some(sha) = run("git", &["rev-parse", "HEAD"]) {
return sha;
}
// 4. Nothing available — build without VCS context.
"unknown".to_string()
}
fn run(cmd: &str, args: &[&str]) -> Option<String> {
let output = Command::new(cmd).args(args).output().ok()?;
if !output.status.success() {
return None;
}
let sha = String::from_utf8(output.stdout).ok()?.trim().to_string();
if sha.is_empty() { None } else { Some(sha) }
}
+20
View File
@@ -606,6 +606,26 @@ pub fn list_timezones() -> Vec<String> {
chrono_tz::TZ_VARIANTS.iter().map(|tz| tz.name().to_string()).collect()
}
/// Build/version metadata surfaced in the About section of the settings screen.
#[derive(uniffi::Record)]
pub struct BuildInfo {
/// Package version (e.g. "0.1.0").
pub version: String,
/// Full commit hash this build came from, or "unknown".
pub commit_hash: String,
/// Truncated commit hash for compact display.
pub commit_short: String,
}
#[uniffi::export]
pub fn build_info() -> BuildInfo {
BuildInfo {
version: crate::version::PKG_VERSION.to_string(),
commit_hash: crate::version::COMMIT_HASH.to_string(),
commit_short: crate::version::short_commit().to_string(),
}
}
#[uniffi::export]
pub fn search_podcasts(query: String) -> Result<String, SyncError> {
crate::agents::podcast::search_podcasts(&query).map_err(SyncError::from)
+1
View File
@@ -21,6 +21,7 @@ pub mod stores;
pub mod sync_prefs;
pub mod typst;
pub mod vault;
pub mod version;
/// Get the default store path for the primary node.
///
+31
View File
@@ -0,0 +1,31 @@
//! Build and version information embedded at compile time.
//!
//! The commit hash is captured by `build.rs` (from `$SYNCHRONICITY_BUILD_SHA` /
//! `$GITEA_SHA` in CI, otherwise the local `jj`/`git` checkout) and surfaced
//! here for the CLI `about` command and the GUI/Android settings screens.
//!
//! This is a placeholder for a proper version number — for now a build is
//! identified solely by the commit it came from.
/// Full commit hash this build came from, or `"unknown"` if it could not be
/// determined at build time.
pub const COMMIT_HASH: &str = env!("SYNCHRONICITY_GIT_SHA");
/// Package version from `Cargo.toml` (e.g. `"0.1.0"`).
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Number of leading hex characters shown for the short commit. Mirrors the
/// truncation the release CI applies when tagging builds (`${GITEA_SHA:0:7}`).
pub const SHORT_COMMIT_LEN: usize = 7;
/// The first [`SHORT_COMMIT_LEN`] characters of [`COMMIT_HASH`], or the whole
/// string when it is shorter (e.g. the `"unknown"` placeholder). Commit hashes
/// are ASCII hex, so byte slicing is sound.
pub fn short_commit() -> &'static str {
&COMMIT_HASH[..SHORT_COMMIT_LEN.min(COMMIT_HASH.len())]
}
/// One-line human-readable version, e.g. `"0.1.0 (a1b2c3d)"`.
pub fn version_string() -> String {
format!("{PKG_VERSION} ({})", short_commit())
}