diff --git a/Cargo.lock b/Cargo.lock index b89c283c87a0..19466ab477a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3455,9 +3455,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -3467,9 +3467,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -3478,9 +3478,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "resolv-conf" diff --git a/mullvad-version/Cargo.toml b/mullvad-version/Cargo.toml index f4586f6569eb..092156a06372 100644 --- a/mullvad-version/Cargo.toml +++ b/mullvad-version/Cargo.toml @@ -17,4 +17,4 @@ workspace = true [dependencies] -regex = "1.6.0" +regex = "1.11.1" diff --git a/mullvad-version/src/lib.rs b/mullvad-version/src/lib.rs index 71cb27e55da8..ed5d15ab6de0 100644 --- a/mullvad-version/src/lib.rs +++ b/mullvad-version/src/lib.rs @@ -11,26 +11,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, + pub version_type: VersionType, +} + +impl Version {} + +#[derive(Debug, Clone, PartialEq)] +pub enum VersionType { + Alpha(String), + Beta(String), + Dev(String), + Release, } impl Version { pub fn parse(version: &str) -> Version { Version::from_str(version).unwrap() } + + pub fn is_release(&self) -> bool { + matches!(&self.version_type, VersionType::Release) + } + + pub fn alpha(&self) -> Option<&str> { + match &self.version_type { + VersionType::Alpha(v) => Some(v), + _ => None, + } + } + + pub fn beta(&self) -> Option<&str> { + match &self.version_type { + VersionType::Beta(v) => Some(v), + _ => None, + } + } + + pub fn dev(&self) -> Option<&str> { + match &self.version_type { + VersionType::Dev(v) => Some(v), + _ => 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, + version_type, } = &self; - match beta { - Some(beta) => write!(f, "{year}.{incremental}-{beta}"), - None => write!(f, "{year}.{incremental}"), + + write!(f, "{year}.{incremental}")?; + + match version_type { + VersionType::Alpha(version) => write!(f, "-alpha{version}"), + VersionType::Beta(version) => write!(f, "-beta{version}"), + VersionType::Dev(commit_hash) => write!(f, "-dev-{commit_hash}"), + VersionType::Release => Ok(()), } } } @@ -39,25 +79,118 @@ 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) + 20(?[0-9]{2})\. # the last two digits of the year + (?[1-9][0-9]?) # the incrementing version number + (?: # (optional) alpha or beta or dev + -alpha(?[1-9][0-9]?)| + -beta(?[1-9][0-9]?)| + -dev-(?[0-9a-f]+)| + -dev + )?$ + ", + ) + .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 sub_type = match (alpha, beta, dev) { + (Some(v), _, _) => VersionType::Alpha(v), + (_, Some(v), _) => VersionType::Beta(v), + (_, _, Some(v)) => VersionType::Dev(v), + (None, None, None) => VersionType::Release, + }; Ok(Version { year, incremental, - beta, + version_type: sub_type, }) } } + +#[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_eq!(parsed.is_release(), true); + } + + #[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_eq!(parsed.is_release(), false); + } + + #[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_eq!(parsed.is_release(), false); + } + + #[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_eq!(parsed.alpha(), None); + assert_eq!(parsed.beta(), None); + assert_eq!(parsed.dev(), Some("0b60e4d87")); + assert_eq!(parsed.is_release(), false); + } + + #[test] + #[should_panic] + fn test_panics_on_invalid_version() { + Version::parse("2021"); + } + + #[test] + #[should_panic] + fn test_panics_on_alpha_and_beta_in_same_version() { + Version::parse("2021.1-beta5-alpha2"); + } +} diff --git a/mullvad-version/src/main.rs b/mullvad-version/src/main.rs index ef72286eb7be..490130b9fbf1 100644 --- a/mullvad-version/src/main.rs +++ b/mullvad-version/src/main.rs @@ -1,4 +1,4 @@ -use mullvad_version::Version; +use mullvad_version::{Version, VersionType}; use std::{env, process::exit}; const ANDROID_VERSION: &str = @@ -35,30 +35,37 @@ 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) = match &version.version_type { + VersionType::Alpha(v) => ("0", v.as_str()), + VersionType::Beta(v) => ("1", v.as_str()), + VersionType::Dev(_) | VersionType::Release => ("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, ) } @@ -74,3 +81,29 @@ fn to_windows_h_format(version: &str) -> String { #define PRODUCT_VERSION \"{version}\"" ) } + +#[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")); + assert_eq!("21349000", to_android_version_code("2021.34-dev-be846a5f0")); + } +} 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