Skip to content

Commit

Permalink
Add support for relaxed semvers
Browse files Browse the repository at this point in the history
  • Loading branch information
fmeum committed Jun 2, 2023
1 parent 4abb877 commit 8ab30d2
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 38 deletions.
10 changes: 5 additions & 5 deletions internal/bzlmod/go_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,12 @@ def _go_deps_impl(module_ctx):
replace_map.update(go_mod_replace_map)
else:
# Register this Bazel module as providing the specified Go module. It participates
# in version resolution using its registry version, which is assumed to be an
# actual semver. An empty version string signals an override, which is assumed to
# be newer than any other version.
# TODO: Decide whether and how to handle non-semver versions.
# in version resolution using its registry version, which uses a relaxed variant of
# semver that can however still be compared to strict semvers.
# An empty version string signals an override, which is assumed to be newer than any
# other version.
raw_version = _canonicalize_raw_version(module.version)
version = semver.to_comparable(raw_version) if raw_version else _HIGHEST_VERSION_SENTINEL
version = semver.to_comparable(raw_version, relaxed = True) if raw_version else _HIGHEST_VERSION_SENTINEL
if module_path not in bazel_deps or version > bazel_deps[module_path].version:
bazel_deps[module_path] = struct(
module_name = module.name,
Expand Down
29 changes: 19 additions & 10 deletions internal/bzlmod/semver.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ _COMPARES_LOWEST_SENTINEL = ""
# Compares higher than any valid non-numeric identifier (containing only [A-Za-z0-9-]).
_COMPARES_HIGHEST_SENTINEL = "{"

def _identifier_to_comparable(ident):
def _identifier_to_comparable(ident, *, numeric_only):
if not ident:
fail("Identifiers in semantic version strings must not be empty")
if ident.isdigit():
Expand All @@ -20,36 +20,45 @@ def _identifier_to_comparable(ident):
# 11.4.3:
# "Numeric identifiers always have lower precedence than non-numeric identifiers."
return (_COMPARES_LOWEST_SENTINEL, int(ident))
elif numeric_only:
fail("Expected a numeric identifier, got: " + ident)
else:
# 11.4.2:
# "Identifiers with letters or hyphens are compared lexically in ASCII sort order."
return (ident,)

def _semver_to_comparable(v):
def _semver_to_comparable(v, *, relaxed = False):
"""
Parses a string representation of a semver version into an opaque comparable object.
Args:
v: The string representation of the version.
relaxed: If true, the release version string is allowed to have an arbitrary number of
dot-separated components, each of which is allowed to contain the same set of characters
as a pre-release segment. This is the version string format used by Bazel modules.
"""

# Strip build metadata as it is not relevant for comparisons.
v, _, _ = v.partition("+")

major_minor_patch_str, _, prerelease_str = v.partition("-")
release_str, _, prerelease_str = v.partition("-")
if prerelease_str:
# 11.4.4:
# "A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding
# identifiers are equal."
prerelease = tuple([_identifier_to_comparable(ident) for ident in prerelease_str.split(".")])
prerelease = [_identifier_to_comparable(ident, numeric_only = False) for ident in prerelease_str.split(".")]
else:
# 11.3:
# "When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version."
prerelease = ((_COMPARES_HIGHEST_SENTINEL,),)
prerelease = [(_COMPARES_HIGHEST_SENTINEL,)]

release = release_str.split(".")
if not relaxed and len(release) != 3:
fail("Semantic version strings must have exactly three dot-separated components, got: " + v)

major_str, minor_str, patch_str = major_minor_patch_str.split(".")
return (
int(major_str),
int(minor_str),
int(patch_str),
prerelease,
tuple([_identifier_to_comparable(s, numeric_only = not relaxed) for s in release]),
tuple(prerelease),
)

semver = struct(
Expand Down
69 changes: 46 additions & 23 deletions tests/bzlmod/semver_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
load("//internal/bzlmod:semver.bzl", "semver")

_SORTED_TEST_VERSIONS = [
"0.1-a",
"0.1",
"1.0.0-0.3.7",
"1.0.0-alpha",
"1.0.0-alpha+001",
Expand All @@ -13,42 +15,63 @@ _SORTED_TEST_VERSIONS = [
"1.0.0-beta.11",
"1.0.0-rc.1",
"1.0.0-x.7.z.92",
"1.0.0-x-y-z.",
"1.0.0+21AF26D3-117B344092BD",
"1.0.0-x-y-z.--",
"1.0.0+21AF26D3----117B344092BD",
"1.0.0+20130313144700",
"1.0.0",
"2.0.0",
"2.1.0",
"2.1.1-0",
"2.1.1",
"2.1.1.0",
"2.1.1.1-a",
"2.1.1.1",
"2.1.1.a",
"2.1.2",
"3",
"a",
]

_SCRAMBLED_TEST_VERSIONS = [
"2.1.1",
"2.1.0",
"2.0.0",
"1.0.0+21AF26D3—-117B344092BD",
"1.0.0+20130313144700",
"1.0.0",
"1.0.0-x.7.z.92",
"1.0.0-x-y-z.–",
"1.0.0-rc.1",
"1.0.0-beta.11",
"1.0.0-beta.2",
"1.0.0-beta+exp.sha.5114f85",
"1.0.0-beta",
"1.0.0-alpha.beta",
"1.0.0-alpha.1",
"1.0.0-alpha",
"1.0.0-alpha+001",
"1.0.0-0.3.7",
]
_SCRAMBLED_TEST_VERSIONS = {
"a": True,
"3": True,
"2.1.2": False,
"2.1.1.a": True,
"2.1.1.1": True,
"2.1.1.1-a": True,
"2.1.1.0": True,
"2.1.1": False,
"2.1.1-0": False,
"2.1.0": False,
"2.0.0": False,
"1.0.0+21AF26D3----117B344092BD": False,
"1.0.0+20130313144700": False,
"1.0.0": False,
"1.0.0-x.7.z.92": False,
"1.0.0-x-y-z.--": False,
"1.0.0-rc.1": False,
"1.0.0-beta.11": False,
"1.0.0-beta.2": False,
"1.0.0-beta+exp.sha.5114f85": False,
"1.0.0-beta": False,
"1.0.0-alpha.beta": False,
"1.0.0-alpha.1": False,
"1.0.0-alpha": False,
"1.0.0-alpha+001": False,
"1.0.0-0.3.7": False,
"0.1-a": True,
"0.1": True,
}

def _semver_test_impl(ctx):
env = unittest.begin(ctx)
asserts.equals(
env,
_SORTED_TEST_VERSIONS,
sorted(_SCRAMBLED_TEST_VERSIONS, key = semver.to_comparable),
sorted(
_SCRAMBLED_TEST_VERSIONS.keys(),
key = lambda x: semver.to_comparable(x, relaxed = _SCRAMBLED_TEST_VERSIONS[x]),
),
)
return unittest.end(env)

Expand Down

0 comments on commit 8ab30d2

Please sign in to comment.