diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index dc652531..4959d1c5 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -45,6 +45,7 @@ chore: use a custom log directory #930 chore: bump tauri_nspanel to v2.1 #933 refactor: show_coco/hide_coco now use NSPanel's function on macOS #933 refactor: procedure that convert_pages() into a func #934 +refactor: custom_version_comparator() now compares semantic versions #941 ## 0.8.0 (2025-09-28) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 11d1a43d..8c4c5e20 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -90,7 +90,7 @@ pub fn run() { .plugin(tauri_plugin_process::init()) .plugin( tauri_plugin_updater::Builder::new() - .default_version_comparator(crate::util::updater::custom_version_comparator) + .default_version_comparator(crate::util::version::custom_version_comparator) .build(), ) .plugin(tauri_plugin_windows_version::init()) diff --git a/src-tauri/src/util/mod.rs b/src-tauri/src/util/mod.rs index 141500df..fa9f8293 100644 --- a/src-tauri/src/util/mod.rs +++ b/src-tauri/src/util/mod.rs @@ -6,7 +6,7 @@ pub(crate) mod path; pub(crate) mod platform; pub(crate) mod prevent_default; pub(crate) mod system_lang; -pub(crate) mod updater; +pub(crate) mod version; use std::{path::Path, process::Command}; use tauri::AppHandle; diff --git a/src-tauri/src/util/updater.rs b/src-tauri/src/util/updater.rs deleted file mode 100644 index 1a984d08..00000000 --- a/src-tauri/src/util/updater.rs +++ /dev/null @@ -1,87 +0,0 @@ -use semver::Version; -use tauri_plugin_updater::RemoteRelease; - -/// Helper function to extract the build number out of `version`. -/// -/// If the version string is in the `x.y.z` format and does not include a build -/// number, we assume a build number of 0. -fn extract_build_number(version: &Version) -> u32 { - let pre = &version.pre; - - if pre.is_empty() { - // A special value for the versions that do not have array - 0 - } else { - let pre_str = pre.as_str(); - let build_number_str = { - match pre_str.strip_prefix("SNAPSHOT-") { - Some(str) => str, - None => pre_str, - } - }; - let build_number : u32 = build_number_str.parse().unwrap_or_else(|e| { - panic!( - "invalid build number, cannot parse [{}] to a valid build number, error [{}], version [{}]", - build_number_str, e, version - ) - }); - - build_number - } -} - -/// # Local version format -/// -/// Packages built in our CI use the following format: -/// -/// * `x.y.z-SNAPSHOT-` -/// * `x.y.z-` -/// -/// If you build Coco from src, the version will be in format `x.y.z` -/// -/// # Remote version format -/// -/// `x.y.z-` -/// -/// # How we compare versions -/// -/// We compare versions based solely on the build number. -/// If the version string is in the `x.y.z` format and does not include a build number, -/// we assume a build number of 0. As a result, such versions are considered older -/// than any version with an explicit build number. -pub(crate) fn custom_version_comparator(local: Version, remote_release: RemoteRelease) -> bool { - let remote = remote_release.version; - - let local_build_number = extract_build_number(&local); - let remote_build_number = extract_build_number(&remote); - - let should_update = remote_build_number > local_build_number; - log::debug!( - "custom version comparator invoked, local version [{}], remote version [{}], should update [{}]", - local, - remote, - should_update - ); - - should_update -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_extract_build_number() { - // 0.6.0 => 0 - let version = Version::parse("0.6.0").unwrap(); - assert_eq!(extract_build_number(&version), 0); - - // 0.6.0-2371 => 2371 - let version = Version::parse("0.6.0-2371").unwrap(); - assert_eq!(extract_build_number(&version), 2371); - - // 0.6.0-SNAPSHOT-2371 => 2371 - let version = Version::parse("0.6.0-SNAPSHOT-2371").unwrap(); - assert_eq!(extract_build_number(&version), 2371); - } -} diff --git a/src-tauri/src/util/version.rs b/src-tauri/src/util/version.rs new file mode 100644 index 00000000..a6a3e8be --- /dev/null +++ b/src-tauri/src/util/version.rs @@ -0,0 +1,236 @@ +use semver::{BuildMetadata, Prerelease, Version}; +use tauri_plugin_updater::RemoteRelease; + +const SNAPSHOT_DASH: &str = "SNAPSHOT-"; +const SNAPSHOT_DASH_LEN: usize = SNAPSHOT_DASH.len(); +// trim the last dash +const SNAPSHOT: &str = SNAPSHOT_DASH.split_at(SNAPSHOT_DASH_LEN - 1).0; + +/// Coco AI app adopt SemVer but the version string format does not adhere to +/// the SemVer specification, this function does the conversion. +/// +/// # Example cases +/// +/// * 0.8.0 => 0.8.0 +/// +/// You may see this when you develop Coco locally +/// +/// * 0.8.0- => 0.8.0 +/// +/// This is the official release for 0.8.0 +/// +/// * 0.9.0-SNAPSHOT- => 0.9.0-SNAPSHOT. +/// +/// A pre-release of 0.9.0 +fn to_semver(version: &Version) -> Version { + let pre = &version.pre; + + if pre.is_empty() { + return Version::new(version.major, version.minor, version.patch); + } + let is_pre_release = pre.starts_with(SNAPSHOT_DASH); + + let build_number_str = if is_pre_release { + &pre[SNAPSHOT_DASH_LEN..] + } else { + pre.as_str() + }; + // Parse the build number to validate it, we do not need the actual number though. + build_number_str.parse::().unwrap_or_else(|_| { + panic!( + "looks like Coco changed the version string format, but forgot to update this function" + ); + }); + + // Return after checking the build number is valid + if !is_pre_release { + return Version::new(version.major, version.minor, version.patch); + } + + let pre = { + let pre_str = format!("{}.{}", SNAPSHOT, build_number_str); + Prerelease::new(&pre_str).unwrap_or_else(|e| panic!("invalid Prerelease: {}", e)) + }; + + Version { + major: version.major, + minor: version.minor, + patch: version.patch, + pre, + build: BuildMetadata::EMPTY, + } +} + +pub(crate) fn custom_version_comparator(local: Version, remote_release: RemoteRelease) -> bool { + let remote = remote_release.version; + let local_semver = to_semver(&local); + let remote_semver = to_semver(&remote); + + let should_update = remote_semver > local_semver; + + log::debug!( + "custom version comparator invoked, local version [{}], remote version [{}], should update [{}]", + local, + remote, + should_update + ); + + should_update +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use tauri_plugin_updater::RemoteReleaseInner; + + #[test] + fn test_try_into_semver_local_dev() { + // Case: 0.8.0 => 0.8.0 + // Local development version without any pre-release or build metadata + let input = Version::parse("0.8.0").unwrap(); + let result = to_semver(&input); + + assert_eq!(result.major, 0); + assert_eq!(result.minor, 8); + assert_eq!(result.patch, 0); + assert_eq!(result.pre, Prerelease::EMPTY); + assert!(result.build.is_empty()); + assert_eq!(result.to_string(), "0.8.0"); + } + + #[test] + fn test_try_into_semver_official_release() { + // Case: 0.8.0- => 0.8.0 + // Official release with build number in pre-release field + let input = Version::parse("0.8.0-123").unwrap(); + let result = to_semver(&input); + + assert_eq!(result.major, 0); + assert_eq!(result.minor, 8); + assert_eq!(result.patch, 0); + assert_eq!(result.pre, Prerelease::EMPTY); + assert!(result.build.is_empty()); + assert_eq!(result.to_string(), "0.8.0"); + } + + #[test] + fn test_try_into_semver_pre_release() { + // Case: 0.9.0-SNAPSHOT- => 0.9.0-SNAPSHOT. + // Pre-release version with SNAPSHOT prefix + let input = Version::parse("0.9.0-SNAPSHOT-456").unwrap(); + let result = to_semver(&input); + + assert_eq!(result.major, 0); + assert_eq!(result.minor, 9); + assert_eq!(result.patch, 0); + assert_eq!(result.pre.as_str(), "SNAPSHOT.456"); + assert!(result.build.is_empty()); + assert_eq!(result.to_string(), "0.9.0-SNAPSHOT.456"); + } + + #[test] + fn test_try_into_semver_official_release_different_version() { + // Test with different version numbers + let input = Version::parse("1.2.3-9999").unwrap(); + let result = to_semver(&input); + + assert_eq!(result.major, 1); + assert_eq!(result.minor, 2); + assert_eq!(result.patch, 3); + assert_eq!(result.pre, Prerelease::EMPTY); + assert!(result.build.is_empty()); + assert_eq!(result.to_string(), "1.2.3"); + } + + #[test] + fn test_try_into_semver_snapshot_different_version() { + // Test SNAPSHOT with different version numbers + let input = Version::parse("2.0.0-SNAPSHOT-777").unwrap(); + let result = to_semver(&input); + + assert_eq!(result.major, 2); + assert_eq!(result.minor, 0); + assert_eq!(result.patch, 0); + assert_eq!(result.pre.as_str(), "SNAPSHOT.777"); + assert!(result.build.is_empty()); + assert_eq!(result.to_string(), "2.0.0-SNAPSHOT.777"); + } + + #[test] + #[should_panic(expected = "looks like Coco changed the version string format")] + fn test_try_into_semver_invalid_build_number() { + // Should panic when build number is not a valid number + let input = Version::parse("0.8.0-abc").unwrap(); + to_semver(&input); + } + + #[test] + #[should_panic(expected = "looks like Coco changed the version string format")] + fn test_try_into_semver_invalid_snapshot_build_number() { + // Should panic when SNAPSHOT build number is not a valid number + let input = Version::parse("0.9.0-SNAPSHOT-xyz").unwrap(); + to_semver(&input); + } + + #[test] + fn test_custom_version_comparator() { + fn new_local(str: &str) -> Version { + Version::parse(str).unwrap() + } + fn new_remote_release(str: &str) -> RemoteRelease { + let version = Version::parse(str).unwrap(); + + RemoteRelease { + version, + notes: None, + pub_date: None, + data: RemoteReleaseInner::Static { + platforms: HashMap::new(), + }, + } + } + + assert_eq!( + custom_version_comparator(new_local("0.8.0"), new_remote_release("0.8.0-2518")), + false + ); + assert_eq!( + custom_version_comparator(new_local("0.8.0-2518"), new_remote_release("0.8.0")), + false + ); + assert_eq!( + custom_version_comparator(new_local("0.9.0-SNAPSHOT-1"), new_remote_release("0.9.0")), + true + ); + assert_eq!( + custom_version_comparator(new_local("0.9.0-SNAPSHOT-1"), new_remote_release("0.8.1")), + false + ); + assert_eq!( + custom_version_comparator(new_local("0.9.0-SNAPSHOT-1"), new_remote_release("0.9.0-2")), + true + ); + assert_eq!( + custom_version_comparator( + new_local("0.9.0-SNAPSHOT-1"), + new_remote_release("0.9.0-SNAPSHOT-1") + ), + false + ); + assert_eq!( + custom_version_comparator( + new_local("0.9.0-SNAPSHOT-11"), + new_remote_release("0.9.0-SNAPSHOT-9") + ), + false + ); + assert_eq!( + custom_version_comparator( + new_local("0.9.0-SNAPSHOT-11"), + new_remote_release("0.9.0-SNAPSHOT-19") + ), + true + ); + } +}