memgraph/release/get_version.py
2021-10-14 16:07:40 +02:00

270 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import os
import re
import subprocess
import sys
import time
from functools import wraps
# This script is used to determine the current version of Memgraph. The script
# determines the current version using `git` automatically. The user can also
# manually specify a version override and then the supplied version will be
# used instead of the version determined by `git`. All versions (automatically
# detected and manually specified) can have a custom version suffix added to
# them.
#
# The current version can be one of either:
# - release version
# - development version
#
# The release version is either associated with a `release/X.Y` branch
# (automatic detection) or is manually specified. When the version is
# automatically detected from the branch a `.0` is appended to the version.
# Example 1:
# - branch: release/0.50
# - version: 0.50.0
# Example 2:
# - manually supplied version: 0.50.1
# - version: 0.50.1
#
# The development version is always determined using `git` in the following
# way:
# - release version - nearest (older) `release/X.Y` branch version
# - distance from the release branch - Z commits
# - the current commit short hash
# Example:
# - release version: 0.50.0 (nearest older branch `release/0.50`)
# - distance from the release branch: 42 (commits)
# - current commit short hash: 7e1eef94
#
# The script then uses the collected information to generate the versions that
# will be used in the binary, DEB package and RPM package. All of the versions
# have different naming conventions that have to be met and they differ among
# each other.
#
# The binary version is determined using the following two templates:
# Release version:
# <VERSION>-<OFFERING>[-<SUFFIX>]
# Development version:
# <VERSION>+<DISTANCE>~<SHORTHASH>-<OFFERING>[-<SUFFIX>]
# Examples:
# Release version:
# 0.50.1-open-source
# 0.50.1
# 0.50.1-veryimportantcustomer
# Development version (master, 12 commits after release/0.50):
# 0.50.0+12~7e1eef94-open-source
# 0.50.0+12~7e1eef94
# 0.50.0+12~7e1eef94-veryimportantcustomer
#
# The DEB package version is determined using the following two templates:
# Release version:
# <VERSION>-<OFFERING>[-<SUFFIX>]-1
# Development version (master, 12 commits after release/0.50):
# <VERSION>+<DISTANCE>~<SHORTHASH>-<OFFERING>[-<SUFFIX>]-1
# Examples:
# Release version:
# 0.50.1-open-source-1
# 0.50.1-1
# 0.50.1-veryimportantcustomer-1
# Development version (master, 12 commits after release/0.50):
# 0.50.0+12~7e1eef94-open-source-1
# 0.50.0+12~7e1eef94-1
# 0.50.0+12~7e1eef94-veryimportantcustomer-1
# For more documentation about the DEB package naming conventions see:
# https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
#
# The RPM package version is determined using the following two templates:
# Release version:
# <VERSION>_1.<OFFERING>[.<SUFFIX>]
# Development version:
# <VERSION>_0.<DISTANCE>.<SHORTHASH>.<OFFERING>[.<SUFFIX>]
# Examples:
# Release version:
# 0.50.1_1.open-source
# 0.50.1_1
# 0.50.1_1.veryimportantcustomer
# Development version:
# 0.50.0_0.12.7e1eef94.open-source
# 0.50.0_0.12.7e1eef94
# 0.50.0_0.12.7e1eef94.veryimportantcustomer
# For more documentation about the RPM package naming conventions see:
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Versioning/
# https://fedoraproject.org/wiki/Package_Versioning_Examples
def retry(retry_limit, timeout=100):
def inner_func(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(retry_limit):
try:
return func(*args, **kwargs)
except Exception:
time.sleep(timeout)
return func(*args, **kwargs)
return wrapper
return inner_func
@retry(3)
def get_output(*cmd, multiple=False):
ret = subprocess.run(cmd, stdout=subprocess.PIPE, check=True)
if multiple:
return list(map(lambda x: x.strip(), ret.stdout.decode("utf-8").strip().split("\n")))
return ret.stdout.decode("utf-8").strip()
def format_version(variant, version, offering, distance=None, shorthash=None, suffix=None):
if not distance:
# This is a release version.
if variant == "deb":
# <VERSION>-<OFFERING>[-<SUFFIX>]-1
ret = "{}{}".format(version, "-" + offering if offering else "")
if suffix:
ret += "-" + suffix
ret += "-1"
return ret
elif variant == "rpm":
# <VERSION>_1.<OFFERING>[.<SUFFIX>]
ret = "{}_1{}".format(version, "." + offering if offering else "")
if suffix:
ret += "." + suffix
return ret
else:
# <VERSION>-<OFFERING>[-<SUFFIX>]
ret = "{}{}".format(version, "-" + offering if offering else "")
if suffix:
ret += "-" + suffix
return ret
else:
# This is a development version.
if variant == "deb":
# <VERSION>+<DISTANCE>~<SHORTHASH>-<OFFERING>[-<SUFFIX>]-1
ret = "{}+{}~{}{}".format(version, distance, shorthash, "-" + offering if offering else "")
if suffix:
ret += "-" + suffix
ret += "-1"
return ret
elif variant == "rpm":
# <VERSION>_0.<DISTANCE>.<SHORTHASH>.<OFFERING>[.<SUFFIX>]
ret = "{}_0.{}.{}{}".format(version, distance, shorthash, "." + offering if offering else "")
if suffix:
ret += "." + suffix
return ret
else:
# <VERSION>+<DISTANCE>~<SHORTHASH>-<OFFERING>[-<SUFFIX>]
ret = "{}+{}~{}{}".format(version, distance, shorthash, "-" + offering if offering else "")
if suffix:
ret += "-" + suffix
return ret
# Parse arguments.
parser = argparse.ArgumentParser(description="Get the current version of Memgraph.")
parser.add_argument("--open-source", action="store_true", help="set the current offering to 'open-source'")
parser.add_argument("version", help="manual version override, if supplied the version isn't " "determined using git")
parser.add_argument("suffix", help="custom suffix for the current version being built")
parser.add_argument(
"--variant",
choices=("binary", "deb", "rpm"),
default="binary",
help="which variant of the version string should be generated",
)
parser.add_argument(
"--memgraph-root-dir", help="The root directory of the checked out " "Memgraph repository.", default="."
)
args = parser.parse_args()
if not os.path.isdir(args.memgraph_root_dir):
raise Exception("The root directory ({}) is not a valid directory".format(args.memgraph_root_dir))
os.chdir(args.memgraph_root_dir)
offering = "open-source" if args.open_source else None
# Check whether the version was manually supplied.
if args.version:
if not re.match(r"^[0-9]+\.[0-9]+\.[0-9]+$", args.version):
raise Exception("Invalid version supplied '{}'!".format(args.version))
print(format_version(args.variant, args.version, offering, suffix=args.suffix), end="")
sys.exit(0)
# Within CI, after the regular checkout, master is sometimes (e.g. in the case
# of an epic or task branch) NOT created as a local branch. cpack depends on
# variables generated by calling this script during the cmake phase. This
# script needs master to be the local branch. `git fetch origin master:master`
# is creating the local master branch without checking it out. Does nothing if
# master is already there.
try:
current_branch = get_output("git", "rev-parse", "--abbrev-ref", "HEAD")
if current_branch != "master":
branches = get_output("git", "branch")
if "master" in branches:
# If master is present locally, the fetch is allowed to fail
# because this script will still be able to compare against the
# master branch.
try:
get_output("git", "fetch", "origin", "master:master")
except Exception:
pass
else:
# If master is not present locally, the fetch command has to
# succeed because something else will fail otherwise.
get_output("git", "fetch", "origin", "master:master")
except Exception:
print("Fatal error while ensuring local master branch.")
sys.exit(1)
# Get current commit hashes.
current_hash = get_output("git", "rev-parse", "HEAD")
current_hash_short = get_output("git", "rev-parse", "--short", "HEAD")
# We want to find branches that exist on some remote and that are named
# `release/[0-9]+\.[0-9]+`.
branch_regex = re.compile(r"^remotes/[a-zA-Z0-9]+/release/([0-9]+\.[0-9]+)$")
# Find all existing versions.
versions = []
branches = get_output("git", "branch", "--all", multiple=True)
for branch in branches:
match = branch_regex.match(branch)
if match is not None:
version = tuple(map(int, match.group(1).split(".")))
master_branch_merge = get_output("git", "merge-base", "master", branch)
versions.append((version, branch, master_branch_merge))
versions.sort(reverse=True)
# Check which existing version branch is closest to the current commit. We are
# only interested in branches that were branched out before this commit was
# created.
current_version = None
for version in versions:
version_tuple, branch, master_branch_merge = version
current_branch_merge = get_output("git", "merge-base", current_hash, branch)
master_current_merge = get_output("git", "merge-base", current_hash, "master")
# The first check checks whether this commit is a child of `master` and
# the version branch was created before us.
# The second check checks whether this commit is a child of the version
# branch.
if master_branch_merge == current_branch_merge or master_branch_merge == master_current_merge:
current_version = version
break
# Determine current version.
if current_version is None:
raise Exception("You are attempting to determine the version for a very " "old version of Memgraph!")
version, branch, master_branch_merge = current_version
distance = int(get_output("git", "rev-list", "--count", "--first-parent", master_branch_merge + ".." + current_hash))
version_str = ".".join(map(str, version)) + ".0"
if distance == 0:
print(format_version(args.variant, version_str, offering, suffix=args.suffix), end="")
else:
print(
format_version(
args.variant, version_str, offering, distance=distance, shorthash=current_hash_short, suffix=args.suffix
),
end="",
)