Skip to content

Commit

Permalink
Merge branch 'improve-generated-version-code-droid-485'
Browse files Browse the repository at this point in the history
  • Loading branch information
Rawa committed Nov 28, 2024
2 parents 7cce580 + d917673 commit 64a6fcb
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 34 deletions.
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

0 comments on commit 64a6fcb

Please sign in to comment.