diff --git a/native/package.json b/native/package.json index a6f9efb..4df0810 100644 --- a/native/package.json +++ b/native/package.json @@ -1,6 +1,6 @@ { "name": "kanbangit-native", - "version": "0.1.2", + "version": "0.1.3", "private": true, "type": "module", "scripts": { @@ -24,5 +24,5 @@ "devDependencies": { "@tauri-apps/cli": "2.11.4" }, - "releaseNotes": "macOS updates can now be checked automatically or from the KanbanGit menu. Install releases from a drag-and-drop DMG. About KanbanGit shows the app version and build. Updates preserve your server settings and stored sessions and restart only after confirmation." + "releaseNotes": "About KanbanGit and Software updates now show the installed app build number correctly." } diff --git a/native/src-tauri/Cargo.lock b/native/src-tauri/Cargo.lock index ad8ddaa..c69890d 100644 --- a/native/src-tauri/Cargo.lock +++ b/native/src-tauri/Cargo.lock @@ -1770,7 +1770,7 @@ dependencies = [ [[package]] name = "kanbangit-native" -version = "0.1.2" +version = "0.1.3" dependencies = [ "semver", "serde", diff --git a/native/src-tauri/Cargo.toml b/native/src-tauri/Cargo.toml index f3bb6ae..badf744 100644 --- a/native/src-tauri/Cargo.toml +++ b/native/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanbangit-native" -version = "0.1.2" +version = "0.1.3" description = "KanbanGit native application" edition = "2021" publish = false diff --git a/native/src-tauri/src/desktop.rs b/native/src-tauri/src/desktop.rs index 88d2654..0798424 100644 --- a/native/src-tauri/src/desktop.rs +++ b/native/src-tauri/src/desktop.rs @@ -285,6 +285,16 @@ pub fn context() -> tauri::Context { tauri::generate_context!() } +fn about_metadata(version: String) -> AboutMetadata<'static> { + AboutMetadata { + name: Some(en::APP_NAME.into()), + version: Some(version), + #[cfg(target_os = "macos")] + short_version: Some(crate::metadata::build_number().into()), + ..Default::default() + } +} + pub fn builder() -> tauri::Builder { tauri::Builder::default().setup(|app| { let (server, phase) = match Server::load(&configuration_path(app.handle())?) { @@ -301,12 +311,7 @@ pub fn builder() -> tauri::Builder { &SubmenuBuilder::new(app, en::APP_NAME) .about_with_text( en::ABOUT, - Some(AboutMetadata { - name: Some(en::APP_NAME.into()), - short_version: Some(app.package_info().version.to_string()), - version: app.config().bundle.macos.bundle_version.clone(), - ..Default::default() - }), + Some(about_metadata(app.package_info().version.to_string())), ) .text("check-updates", en::CHECK_UPDATES) .item(&MenuItem::with_id( @@ -439,6 +444,18 @@ pub fn builder() -> tauri::Builder { mod tests { use super::*; + #[cfg(target_os = "macos")] + #[test] + fn about_shows_app_version_followed_by_the_actual_build_number() { + let metadata = about_metadata("9.8.7".into()); + assert_eq!(metadata.version.as_deref(), Some("9.8.7")); + assert_eq!( + metadata.short_version.as_deref(), + Some(crate::metadata::build_number()) + ); + assert_ne!(metadata.version, metadata.short_version); + } + #[test] fn external_links_accept_only_web_urls_without_credentials() { for input in [ diff --git a/native/src-tauri/src/lib.rs b/native/src-tauri/src/lib.rs index c08141b..75b1184 100644 --- a/native/src-tauri/src/lib.rs +++ b/native/src-tauri/src/lib.rs @@ -6,6 +6,8 @@ mod desktop; mod ios; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod locale; +#[cfg(target_os = "macos")] +mod metadata; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod server; #[cfg(target_os = "macos")] diff --git a/native/src-tauri/src/metadata.rs b/native/src-tauri/src/metadata.rs new file mode 100644 index 0000000..d037822 --- /dev/null +++ b/native/src-tauri/src/metadata.rs @@ -0,0 +1,69 @@ +use std::{path::Path, sync::LazyLock}; + +fn build_from_executable(executable: &Path) -> Option { + let macos = executable.parent()?; + let contents = macos.parent()?; + if macos.file_name()? != "MacOS" || contents.file_name()? != "Contents" { + return None; + } + let output = std::process::Command::new("/usr/bin/plutil") + .args(["-extract", "CFBundleVersion", "raw", "-o", "-"]) + .arg(contents.join("Info.plist")) + .output() + .ok()?; + if !output.status.success() { + return Some("unknown".into()); + } + let build = String::from_utf8(output.stdout).ok()?; + Some(build.trim().to_owned()) +} + +pub fn build_number() -> &'static str { + static BUILD: LazyLock = LazyLock::new(|| { + std::env::current_exe() + .ok() + .and_then(|executable| build_from_executable(&executable)) + .unwrap_or_else(|| { + // Tauri omits macOS bundle settings from its generated runtime config. + let config: serde_json::Value = + serde_json::from_str(include_str!("../tauri.conf.json")).unwrap(); + config["bundle"]["macOS"]["bundleVersion"] + .as_str() + .unwrap_or("unknown") + .to_owned() + }) + }); + &BUILD +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn installed_build_comes_from_the_app_plist_instead_of_the_app_version() { + let root = std::env::temp_dir().join(format!("kanbangit-build-{}", std::process::id())); + let contents = root.join("KanbanGit.app/Contents"); + std::fs::create_dir_all(contents.join("MacOS")).unwrap(); + let executable = contents.join("MacOS/kanbangit-native"); + std::fs::write(contents.join("Info.plist"), r#"CFBundleShortVersionString0.1.3CFBundleVersion42"#).unwrap(); + assert_eq!(build_from_executable(&executable).as_deref(), Some("42")); + std::fs::remove_file(contents.join("Info.plist")).unwrap(); + assert_eq!( + build_from_executable(&executable).as_deref(), + Some("unknown") + ); + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn unbundled_execution_uses_the_configured_build() { + assert!(build_from_executable(Path::new("/tmp/target/debug/kanbangit-native")).is_none()); + let config: serde_json::Value = + serde_json::from_str(include_str!("../tauri.conf.json")).unwrap(); + assert_eq!( + build_number(), + config["bundle"]["macOS"]["bundleVersion"].as_str().unwrap() + ); + } +} diff --git a/native/src-tauri/src/updates.rs b/native/src-tauri/src/updates.rs index 653d790..7a59b4b 100644 --- a/native/src-tauri/src/updates.rs +++ b/native/src-tauri/src/updates.rs @@ -442,13 +442,7 @@ pub fn setup(app: &AppHandle) -> tauri::Result<()> { view: View { phase: if error.is_some() { "error" } else { "idle" }, current_version: app.package_info().version.to_string(), - build: app - .config() - .bundle - .macos - .bundle_version - .clone() - .unwrap_or_else(|| app.package_info().version.to_string()), + build: crate::metadata::build_number().into(), version: None, notes: None, downloaded: 0, diff --git a/native/src-tauri/tauri.conf.json b/native/src-tauri/tauri.conf.json index aedc45a..6cc217e 100644 --- a/native/src-tauri/tauri.conf.json +++ b/native/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "KanbanGit", - "version": "0.1.2", + "version": "0.1.3", "identifier": "de.kanbangit.desktop", "build": { "frontendDist": "../ui" @@ -25,7 +25,7 @@ ], "macOS": { "minimumSystemVersion": "14.0", - "bundleVersion": "2", + "bundleVersion": "3", "dmg": { "windowSize": { "width": 660, diff --git a/native/src-tauri/tests/updater_smoke.rs b/native/src-tauri/tests/updater_smoke.rs index 416e507..4cfd397 100644 --- a/native/src-tauri/tests/updater_smoke.rs +++ b/native/src-tauri/tests/updater_smoke.rs @@ -16,8 +16,16 @@ fn main() { }; use tauri::webview::PageLoadEvent; let passed = Arc::new(AtomicBool::new(false)); + let config: serde_json::Value = + serde_json::from_str(include_str!("../tauri.conf.json")).unwrap(); + let expected_installed = format!( + "Installed version {} (build {})", + config["version"].as_str().unwrap(), + config["bundle"]["macOS"]["bundleVersion"].as_str().unwrap() + ); let plugin = tauri::plugin::Builder::::new("updater-smoke") + .js_init_script(format!("window.expectedInstalled = {};", serde_json::to_string(&expected_installed).unwrap())) .on_page_load(|window, payload| { if window.label() != "updates" || payload.event() != PageLoadEvent::Finished { return; } window.eval(r#" @@ -26,6 +34,8 @@ fn main() { for (let i = 0; i < 100 && !window.updateStatus; i++) await new Promise(r => setTimeout(r, 50)); if (!window.updateStatus) throw new Error('Update UI did not load'); const $ = id => document.getElementById(id); + for (let i = 0; i < 100 && !$('installed').textContent; i++) await new Promise(r => setTimeout(r, 50)); + if ($('installed').textContent !== window.expectedInstalled) throw new Error('Actual installed version/build failed: ' + $('installed').textContent); const state = { phase: 'current', currentVersion: '0.1.2', build: '2', version: null, notes: null, downloaded: 0, total: null, error: null, preferences: { automatic: true, intervalHours: 6 } }; window.updateStatus(state); if (!$('installed').textContent.includes('0.1.2 (build 2)') || !$('install').hidden || $('check').disabled) throw new Error('Current version state failed');