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:
@@ -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))
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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}"))
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user