From d917673164124cbac31a4cea0737618be2736ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kalle=20Lindstr=C3=B6m?= Date: Thu, 28 Nov 2024 12:37:45 +0100 Subject: [PATCH] Support Android app's new version code format --- mullvad-version/src/lib.rs | 195 +++++++++++++++++++++++++++++++++--- mullvad-version/src/main.rs | 94 +++++++++++++---- prepare-release.sh | 5 +- 3 files changed, 260 insertions(+), 34 deletions(-) diff --git a/mullvad-version/src/lib.rs b/mullvad-version/src/lib.rs index 71cb27e55da8..ce493ea1d78a 100644 --- a/mullvad-version/src/lib.rs +++ b/mullvad-version/src/lib.rs @@ -2,6 +2,7 @@ use std::fmt::Display; use std::str::FromStr; use std::sync::LazyLock; +use crate::PreStableType::{Alpha, Beta}; use regex::Regex; /// The Mullvad VPN app product version @@ -11,27 +12,66 @@ pub const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/product-versio pub struct Version { pub year: String, pub incremental: String, - pub beta: Option, + /// A version can have an optional pre-stable type, e.g. alpha or beta. If `pre_stable` + /// and `dev` both are None the version is stable. + pub pre_stable: Option, + /// All versions may have an optional -dev-[commit hash] suffix. + pub dev: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum PreStableType { + Alpha(String), + Beta(String), } impl Version { pub fn parse(version: &str) -> Version { Version::from_str(version).unwrap() } + + pub fn is_stable(&self) -> bool { + self.pre_stable.is_none() && self.dev.is_none() + } + + pub fn alpha(&self) -> Option<&str> { + match &self.pre_stable { + Some(PreStableType::Alpha(v)) => Some(v), + _ => None, + } + } + + pub fn beta(&self) -> Option<&str> { + match &self.pre_stable { + Some(PreStableType::Beta(beta)) => Some(beta), + _ => None, + } + } } impl Display for Version { - /// Format Version as a string: year.incremental{-beta} + /// Format Version as a string: year.incremental-{alpha|beta}-{dev} fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Version { year, incremental, - beta, + pre_stable, + dev, } = &self; - match beta { - Some(beta) => write!(f, "{year}.{incremental}-{beta}"), - None => write!(f, "{year}.{incremental}"), + + write!(f, "{year}.{incremental}")?; + + match pre_stable { + Some(PreStableType::Alpha(version)) => write!(f, "-alpha{version}")?, + Some(PreStableType::Beta(version)) => write!(f, "-beta{version}")?, + None => (), + }; + + if let Some(commit_hash) = dev { + write!(f, "-dev-{commit_hash}")?; } + + Ok(()) } } @@ -39,25 +79,152 @@ impl FromStr for Version { type Err = String; fn from_str(version: &str) -> Result { - const VERSION_REGEX: &str = - r"^20([0-9]{2})\.([1-9][0-9]?)(-beta([1-9][0-9]?))?(-dev-[0-9a-f]+)?$"; - static RE: LazyLock = LazyLock::new(|| Regex::new(VERSION_REGEX).unwrap()); + static VERSION_REGEX: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?x) # enable insignificant whitespace mode + 20(?\d{2})\. # the last two digits of the year + (?[1-9]\d?) # the incrementing version number + (?: # (optional) alpha or beta or dev + -alpha(?[1-9]\d?\d?)| + -beta(?[1-9]\d?\d?) + )? + (?: + -dev-(?[0-9a-f]+) + )?$ + ", + ) + .unwrap() + }); - let captures = RE + let captures = VERSION_REGEX .captures(version) .ok_or_else(|| format!("Version does not match expected format: {version}"))?; - let year = captures.get(1).expect("Missing year").as_str().to_owned(); + + let year = captures + .name("year") + .expect("Missing year") + .as_str() + .to_owned(); + let incremental = captures - .get(2) + .name("incremental") .ok_or("Missing incremental")? .as_str() .to_owned(); - let beta = captures.get(4).map(|m| m.as_str().to_owned()); + + let alpha = captures.name("alpha").map(|m| m.as_str().to_owned()); + let beta = captures.name("beta").map(|m| m.as_str().to_owned()); + let dev = captures.name("dev").map(|m| m.as_str().to_owned()); + + let pre_stable = match (alpha, beta) { + (None, None) => None, + (Some(v), None) => Some(Alpha(v)), + (None, Some(v)) => Some(Beta(v)), + _ => return Err(format!("Invalid version: {version}")), + }; Ok(Version { year, incremental, - beta, + pre_stable, + dev, }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse() { + let version = "2021.34"; + let parsed = Version::parse(version); + assert_eq!(parsed.year, "21"); + assert_eq!(parsed.incremental, "34"); + assert_eq!(parsed.alpha(), None); + assert_eq!(parsed.beta(), None); + assert_eq!(parsed.dev, None); + assert!(parsed.is_stable()); + } + + #[test] + fn test_parse_with_alpha() { + let version = "2023.1-alpha77"; + let parsed = Version::parse(version); + assert_eq!(parsed.year, "23"); + assert_eq!(parsed.incremental, "1"); + assert_eq!(parsed.alpha(), Some("77")); + assert_eq!(parsed.beta(), None); + assert_eq!(parsed.dev, None); + assert!(!parsed.is_stable()); + + let version = "2021.34-alpha777"; + let parsed = Version::parse(version); + assert_eq!(parsed.alpha(), Some("777")); + } + + #[test] + fn test_parse_with_beta() { + let version = "2021.34-beta5"; + let parsed = Version::parse(version); + assert_eq!(parsed.year, "21"); + assert_eq!(parsed.incremental, "34"); + assert_eq!(parsed.alpha(), None); + assert_eq!(parsed.beta(), Some("5")); + assert_eq!(parsed.dev, None); + assert!(!parsed.is_stable()); + + let version = "2021.34-beta453"; + let parsed = Version::parse(version); + assert_eq!(parsed.beta(), Some("453")); + } + + #[test] + fn test_parse_with_dev() { + let version = "2021.34-dev-0b60e4d87"; + let parsed = Version::parse(version); + assert_eq!(parsed.year, "21"); + assert_eq!(parsed.incremental, "34"); + assert!(!parsed.is_stable()); + assert_eq!(parsed.dev, Some("0b60e4d87".to_string())); + assert_eq!(parsed.alpha(), None); + assert_eq!(parsed.beta(), None); + } + + #[test] + fn test_parse_both_beta_and_dev() { + let version = "2024.8-beta1-dev-e5483d"; + let parsed = Version::parse(version); + assert_eq!(parsed.year, "24"); + assert_eq!(parsed.incremental, "8"); + assert_eq!(parsed.alpha(), None); + assert_eq!(parsed.beta(), Some("1")); + assert_eq!(parsed.dev, Some("e5483d".to_string())); + assert!(!parsed.is_stable()); + } + + #[test] + #[should_panic] + fn test_panics_on_invalid_version() { + Version::parse("2021"); + } + + #[test] + #[should_panic] + fn test_panics_on_invalid_version_type_number() { + Version::parse("2021.1-beta001"); + } + + #[test] + #[should_panic] + fn test_panics_on_alpha_and_beta_in_same_version() { + Version::parse("2021.1-beta5-alpha2"); + } + + #[test] + #[should_panic] + fn test_panics_on_dev_without_commit_hash() { + Version::parse("2021.1-dev"); + } +} diff --git a/mullvad-version/src/main.rs b/mullvad-version/src/main.rs index ef72286eb7be..2e83cf24ee69 100644 --- a/mullvad-version/src/main.rs +++ b/mullvad-version/src/main.rs @@ -1,4 +1,4 @@ -use mullvad_version::Version; +use mullvad_version::{PreStableType, Version}; use std::{env, process::exit}; const ANDROID_VERSION: &str = @@ -35,42 +35,98 @@ fn to_semver(version: &str) -> String { /// Takes a version in the normal Mullvad VPN app version format and returns the Android /// `versionCode` formatted version. /// -/// The format of the code is: YYVV00XX -/// Last two digits of the year (major) ^^ -/// Incrementing version (minor) ^^ -/// Unused ^^ -/// Beta number, 99 if stable ^^ +/// The format of the code is: YYVVXZZZ +/// Last two digits of the year (major)---------^^ +/// Incrementing version (minor)------------------^^ +/// Build type (0=alpha, 1=beta, 9=stable/dev)------^ +/// Build number (000 if stable/dev)-----------------^^^ /// /// # Examples /// +/// Version: 2021.1-alpha1 +/// versionCode: 21010001 +/// /// Version: 2021.34-beta5 -/// versionCode: 21340005 +/// versionCode: 21341005 /// /// Version: 2021.34 -/// versionCode: 21340099 +/// versionCode: 21349000 +/// +/// Version: 2021.34-dev +/// versionCode: 21349000 fn to_android_version_code(version: &str) -> String { - const ANDROID_STABLE_VERSION_CODE_SUFFIX: &str = "99"; - let version = Version::parse(version); + + let (build_type, build_number) = if version.dev.is_some() { + ("9", "000") + } else { + match &version.pre_stable { + Some(PreStableType::Alpha(v)) => ("0", v.as_str()), + Some(PreStableType::Beta(v)) => ("1", v.as_str()), + // Stable version + None => ("9", "000"), + } + }; + format!( - "{}{:0>2}00{:0>2}", - version.year, - version.incremental, - version - .beta - .unwrap_or(ANDROID_STABLE_VERSION_CODE_SUFFIX.to_string()) + "{}{:0>2}{}{:0>3}", + version.year, version.incremental, build_type, build_number, ) } -fn to_windows_h_format(version: &str) -> String { +fn to_windows_h_format(version_str: &str) -> String { + let version = Version::parse(version_str); + assert!( + is_valid_windows_version(&version), + "Invalid Windows version: {version:?}" + ); + let Version { year, incremental, .. - } = Version::parse(version); + } = version; format!( "#define MAJOR_VERSION 20{year} #define MINOR_VERSION {incremental} #define PATCH_VERSION 0 -#define PRODUCT_VERSION \"{version}\"" +#define PRODUCT_VERSION \"{version_str}\"" ) } + +/// On Windows we currently support the following versions: stable, beta and dev. +fn is_valid_windows_version(version: &Version) -> bool { + version.is_stable() + || version.beta().is_some() + || (version.dev.is_some() && version.alpha().is_none()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_version_code() { + assert_eq!("21349000", to_android_version_code("2021.34")); + } + + #[test] + fn test_version_code_alpha() { + assert_eq!("21010001", to_android_version_code("2021.1-alpha1")); + } + + #[test] + fn test_version_code_beta() { + assert_eq!("21341005", to_android_version_code("2021.34-beta5")); + } + + #[test] + fn test_version_code_dev() { + assert_eq!("21349000", to_android_version_code("2021.34-dev-be846a5f0")); + } + + #[test] + #[should_panic] + fn test_invalid_windows_version_code() { + to_windows_h_format("2021.34-alpha1"); + } +} diff --git a/prepare-release.sh b/prepare-release.sh index 21627daec5c9..7e6b44356946 100755 --- a/prepare-release.sh +++ b/prepare-release.sh @@ -57,7 +57,10 @@ if [[ "$DESKTOP" == "true" && $(grep "^## \\[$PRODUCT_VERSION\\] - " CHANGELOG.m exit 1 fi -if [[ "$ANDROID" == "true" && $(grep "^## \\[android/$PRODUCT_VERSION\\] - " android/CHANGELOG.md) == "" ]]; then +if [[ "$ANDROID" == "true" && + $PRODUCT_VERSION != *"alpha"* && + $(grep "^## \\[android/$PRODUCT_VERSION\\] - " android/CHANGELOG.md) == "" ]]; then + echo "It looks like you did not add $PRODUCT_VERSION to the changelog?" echo "Please make sure the changelog is up to date and correct before you proceed." exit 1