-
Notifications
You must be signed in to change notification settings - Fork 373
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
Fix our continuous pre-releases #1458
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
66e0996
Look for suffixes
emilk 7498835
Add max version number to reserve some bits for future use
emilk 46c4271
Support +prerelease suffix
emilk 28f2cf6
Use +prerelease for our patched pre-releases
emilk 60ce000
Build fix
emilk 97e124d
Ignore +build-metadata suffix
emilk 1d6e5ed
Suffix continuous pre-releases with githash
emilk f57416e
Rename RustVersion to CrateVersion for clarity
emilk 771c0de
Cleanup
emilk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
/// We disallow version numbers larger than this in order to keep a few bits for future use. | ||
/// | ||
/// If you are running up against this limit then feel free to bump it! | ||
const MAX_NUM: u8 = 31; | ||
|
||
const IS_ALPHA_BIT: u8 = 1 << 7; | ||
|
||
/// The version of a Rerun crate. | ||
/// | ||
/// Sub-set of semver supporting `major.minor.patch` plus an optional `-alpha.X`. | ||
/// | ||
/// When parsing, any `+metadata` suffix is ignored. | ||
/// | ||
/// Examples: `1.2.3`, `1.2.3-alpha.4`. | ||
/// | ||
/// We use `-alpha.X` when we publish pre-releases to crates.io and PyPI. | ||
/// | ||
/// We use a `+githash` suffix for continuous pre-releases that you can download from our GitHub. | ||
/// We do NOT store that in this struct. See also `scripts/version_util.py`. | ||
/// | ||
/// The version numbers aren't allowed to be very large (current max: 31). | ||
/// This limited subset it chosen so that we can encode the version in 32 bits | ||
/// in our `.rrd` files and on the wire. | ||
#[derive(Clone, Copy, Debug, PartialEq, Eq)] | ||
pub struct CrateVersion { | ||
major: u8, | ||
minor: u8, | ||
patch: u8, | ||
alpha: Option<u8>, | ||
} | ||
|
||
impl CrateVersion { | ||
pub const fn new(major: u8, minor: u8, patch: u8) -> Self { | ||
assert!( | ||
major <= MAX_NUM && minor <= MAX_NUM && patch <= MAX_NUM, | ||
"Too large number in version string" | ||
); | ||
Self { | ||
major, | ||
minor, | ||
patch, | ||
alpha: None, | ||
} | ||
} | ||
|
||
/// From a compact 32-bit representation crated with [`Self::to_bytes`]. | ||
pub fn from_bytes([major, minor, patch, suffix_byte]: [u8; 4]) -> Self { | ||
let is_alpha = (suffix_byte & IS_ALPHA_BIT) != 0; | ||
let alpha_version = suffix_byte & 0b0111_1111; | ||
|
||
Self { | ||
major, | ||
minor, | ||
patch, | ||
alpha: is_alpha.then_some(alpha_version), | ||
} | ||
} | ||
|
||
/// A compact 32-bit representation. See also [`Self::from_bytes`]. | ||
pub fn to_bytes(self) -> [u8; 4] { | ||
let Self { | ||
major, | ||
minor, | ||
patch, | ||
alpha, | ||
} = self; | ||
|
||
let suffix_byte = if let Some(alpha) = alpha { | ||
IS_ALPHA_BIT | alpha | ||
} else { | ||
0 | ||
}; | ||
|
||
[major, minor, patch, suffix_byte] | ||
} | ||
|
||
pub fn is_compatible_with(self, other: CrateVersion) -> bool { | ||
if self.alpha != other.alpha { | ||
return false; // Alphas can contain breaking changes | ||
} | ||
|
||
if self.major == 0 { | ||
// before 1.0.0 we break compatibility using the minor: | ||
(self.major, self.minor) == (other.major, other.minor) | ||
} else { | ||
// major version is the only breaking change: | ||
self.major == other.major | ||
} | ||
} | ||
|
||
/// Parse a semver version string, ignoring any trailing `+metadata`. | ||
pub const fn parse(version_string: &str) -> Self { | ||
// Note that this is a const function, which means we are extremely limited in what we can do! | ||
|
||
const fn parse_u8(s: &[u8], begin: usize, end: usize) -> u8 { | ||
assert!(begin < end); | ||
assert!( | ||
s[begin] != b'0' || begin + 1 == end, | ||
"multi-digit number cannot start with zero" | ||
); | ||
|
||
let mut num = 0u64; | ||
let mut i = begin; | ||
|
||
while i < end { | ||
let c = s[i]; | ||
assert!( | ||
b'0' <= c && c <= b'9', | ||
"Unexpected non-digit in version string" | ||
); | ||
let digit = c - b'0'; | ||
num = num * 10 + digit as u64; | ||
assert!(num <= MAX_NUM as _, "Too large number in rust version"); | ||
i += 1; | ||
} | ||
assert!(num <= u8::MAX as u64); | ||
num as _ | ||
} | ||
|
||
let s = version_string.as_bytes(); | ||
|
||
let mut i = 0; | ||
while s[i] != b'.' { | ||
i += 1; | ||
} | ||
let major = parse_u8(s, 0, i); | ||
|
||
i += 1; | ||
let minor_start = i; | ||
while s[i] != b'.' { | ||
i += 1; | ||
} | ||
let minor = parse_u8(s, minor_start, i); | ||
|
||
i += 1; | ||
let patch_start = i; | ||
while i < s.len() && s[i] != b'-' && s[i] != b'+' { | ||
i += 1; | ||
} | ||
let patch = parse_u8(s, patch_start, i); | ||
|
||
if i == s.len() { | ||
return Self::new(major, minor, patch); | ||
} | ||
|
||
let alpha = if s[i] == b'-' { | ||
// `-alpha.X` suffix (Called "pre-release version" in semver). | ||
// Comparing strings in `const` functions is fun: | ||
assert!( | ||
s[i] == b'-' | ||
&& s[i + 1] == b'a' | ||
&& s[i + 2] == b'l' | ||
&& s[i + 3] == b'p' | ||
&& s[i + 4] == b'h' | ||
&& s[i + 5] == b'a' | ||
&& s[i + 6] == b'.', | ||
"Expected `-alpha.X` suffix" | ||
); | ||
i += 7; | ||
|
||
let alpha_start = i; | ||
while i < s.len() && s[i] != b'+' { | ||
i += 1; | ||
} | ||
Some(parse_u8(s, alpha_start, i)) | ||
} else { | ||
None | ||
}; | ||
|
||
if i < s.len() { | ||
// We ignore `+metadata` suffixes. | ||
assert!(s[i] == b'+', "Unexpected suffix"); | ||
}; | ||
|
||
Self { | ||
major, | ||
minor, | ||
patch, | ||
alpha, | ||
} | ||
} | ||
} | ||
|
||
impl std::fmt::Display for CrateVersion { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
let Self { | ||
major, | ||
minor, | ||
patch, | ||
alpha, | ||
} = *self; | ||
|
||
write!(f, "{major}.{minor}.{patch}")?; | ||
if let Some(alpha) = alpha { | ||
write!(f, "-alpha.{alpha}")?; | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_parse_version() { | ||
let parse = CrateVersion::parse; | ||
assert_eq!(parse("0.2.0"), CrateVersion::new(0, 2, 0)); | ||
assert_eq!(parse("1.2.3"), CrateVersion::new(1, 2, 3)); | ||
assert_eq!(parse("12.23.24"), CrateVersion::new(12, 23, 24)); | ||
assert_eq!( | ||
parse("12.23.24-alpha.31"), | ||
CrateVersion { | ||
major: 12, | ||
minor: 23, | ||
patch: 24, | ||
alpha: Some(31), | ||
} | ||
); | ||
assert_eq!( | ||
parse("12.23.24+foo"), | ||
CrateVersion { | ||
major: 12, | ||
minor: 23, | ||
patch: 24, | ||
alpha: None, | ||
} | ||
); | ||
assert_eq!( | ||
parse("12.23.24-alpha.31+bar"), | ||
CrateVersion { | ||
major: 12, | ||
minor: 23, | ||
patch: 24, | ||
alpha: Some(31), | ||
} | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_format_parse_roundtrip() { | ||
let parse = CrateVersion::parse; | ||
for version in [ | ||
"0.2.0", | ||
"1.2.3", | ||
"12.23.24", | ||
"12.23.24-alpha.31", | ||
// These do NOT round-trip, because we ignore the `+metadata`: | ||
// "12.23.24+githash", | ||
// "12.23.24-alpha.31+foobar", | ||
] { | ||
assert_eq!(parse(version).to_string(), version); | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_compatibility() { | ||
fn are_compatible(a: &str, b: &str) -> bool { | ||
CrateVersion::parse(a).is_compatible_with(CrateVersion::parse(b)) | ||
} | ||
|
||
assert!(are_compatible("0.2.0", "0.2.0")); | ||
assert!(are_compatible("0.2.0", "0.2.1")); | ||
assert!(are_compatible("1.2.0", "1.3.0")); | ||
assert!(!are_compatible("0.2.0", "1.2.0")); | ||
assert!(!are_compatible("0.2.0", "0.3.0")); | ||
assert!(are_compatible("0.2.0-alpha.0", "0.2.0-alpha.0")); | ||
assert!( | ||
!are_compatible("0.2.0-alpha.0", "0.2.0-alpha.1"), | ||
"Alphas are always incompatible" | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of the following is old code, but I renamed the file, and git failed to figure that one out 😭