Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Android app's new version code format #7206

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 181 additions & 14 deletions mullvad-version/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -11,53 +12,219 @@ pub const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/product-versio
pub struct Version {
pub year: String,
pub incremental: String,
pub beta: Option<String>,
/// 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<PreStableType>,
/// All versions may have an optional -dev-[commit hash] suffix.
pub dev: Option<String>,
}

#[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(())
}
}

impl FromStr for Version {
type Err = String;

fn from_str(version: &str) -> Result<Self, Self::Err> {
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<Regex> = LazyLock::new(|| Regex::new(VERSION_REGEX).unwrap());
static VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?x) # enable insignificant whitespace mode
20(?<year>\d{2})\. # the last two digits of the year
(?<incremental>[1-9]\d?) # the incrementing version number
(?: # (optional) alpha or beta or dev
-alpha(?<alpha>[1-9]\d?\d?)|
-beta(?<beta>[1-9]\d?\d?)
)?
(?:
-dev-(?<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");
}
}
94 changes: 75 additions & 19 deletions mullvad-version/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use mullvad_version::Version;
use mullvad_version::{PreStableType, Version};
use std::{env, process::exit};

const ANDROID_VERSION: &str =
Expand Down Expand Up @@ -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");
}
}
5 changes: 4 additions & 1 deletion prepare-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading